提示词也要工程化, partial、MessagesPlaceholder 与模板库
项目最初只有一个 Prompt,写在哪里都能工作。
几周后,客服、翻译、摘要和代码审核都有了自己的模板。每个部门又分普通用户、企业用户和内部人员。为了复用,大家复制一份旧 Prompt 再修改两句。
很快就出现了十几个几乎相同的版本。
有人修复了安全约束,却只改了其中三份。有人调整变量名,线上模板还在使用旧参数。多轮历史被手工拼在字符串末尾,测试环境与生产环境甚至拉取了不同版本。
Prompt 一旦进入业务,就不再只是文案。它有输入变量、角色、版本、测试、发布和回滚,也需要工程化治理。
本文重点介绍 partial()、MessagesPlaceholder、模板组合和模板库,再把它们与 LangSmith 的 Prompt Commit、staging 和 production 环境连接起来。
先让一个模板真正跑起来
下面的第一个示例故意写得完整一些。它从依赖、环境变量、模型初始化到调用都包含在内,读者不需要先看其他文章。
pip install -U langchain langchain-openai "langsmith>=0.7.0" python-dotenv pydantic
在项目根目录创建 .env,只在本机保存密钥,不要提交到版本库。
OPENAI_API_KEY=replace_with_your_openai_key
# 只有后文需要从 LangSmith 拉取 Prompt 时才需要这一项。
LANGSMITH_API_KEY=replace_with_your_langsmith_key
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
load_dotenv()
model = init_chat_model("openai:gpt-5.4-mini", temperature=0)
base_template = ChatPromptTemplate.from_messages(
[
(
"system",
"你是{role}。目标受众是{audience}。"
"回答应准确、简洁,无法确认时说明依据不足。",
),
("human", "{task}"),
]
)
support_template = base_template.partial(
role="售后支持专员",
audience="普通用户",
)
response = (support_template | model).invoke(
{"task": "解释为什么退款仍在处理中。"}
)
print(response.content)
输出的具体措辞会随模型变化,但它应是一段面向普通用户的退款处理说明。这里使用 OpenAI 集成;若换成其他供应商,需要安装对应集成包、配置对应环境变量,并更换模型标识。后面的局部示例只聚焦模板行为,真正调用模型时可以沿用这一段初始化方式。
先区分三类输入
一个聊天模板中的数据通常来自三个不同生命周期。
固定配置适合预填。历史消息适合消息占位符。本次问题适合普通模板变量。
如果把三类数据都塞进一个字符串变量,模板会失去角色边界,也很难测试。
使用 partial 预填固定变量
假设多个客服场景共享同一模板,但角色和受众不同。
from langchain_core.prompts import ChatPromptTemplate
base_template = ChatPromptTemplate.from_messages(
[
(
"system",
"你是{role}。目标受众是{audience}。"
"回答应准确、简洁,无法确认时说明依据不足。",
),
("human", "{task}"),
]
)
customer_support_template = base_template.partial(
role="售后支持专员",
audience="普通用户",
)
messages = customer_support_template.format_messages(
task="解释为什么退款仍在处理中。"
)
partial() 返回一个预填了部分变量的新模板,不会修改原模板。这样可以从同一基础定义派生多个稳定变体。
固定变量不等于秘密。不要把 API Key、访问令牌或用户密码放进 Prompt 模板。模型不需要知道的敏感信息不应进入上下文。
partial 也可以接受动态值
部分变量可以由函数在格式化时生成,例如当前日期。
from datetime import date
def current_date() -> str:
"""返回运行当天日期,供模板格式化时动态填充。"""
return date.today().isoformat()
policy_template = ChatPromptTemplate.from_messages(
[
(
"system",
"你是政策问答助理,目标受众是企业用户。当前日期是{today}。",
),
("human", "{task}"),
]
)
dated_template = policy_template.partial(
today=current_date,
)
messages = dated_template.format_messages(
task="说明当前申请流程。"
)
时间、地区和用户权限若会影响业务结论,最好作为结构化输入显式传入,而不是在模板内部隐式读取全局状态。这样测试才能固定这些条件。
使用 MessagesPlaceholder 插入消息历史
多轮历史已经是消息列表,不应该再转成一段带 Human: 和 AI: 标签的字符串。
from langchain.messages import AIMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
chat_template = ChatPromptTemplate.from_messages(
[
("system", "你是订单支持助手,只根据已提供信息回答。"),
MessagesPlaceholder(variable_name="history", optional=True),
("human", "{question}"),
]
)
history = [
HumanMessage("订单 A-100 已发货。"),
AIMessage("我已经记下订单状态。"),
]
messages = chat_template.format_messages(
history=history,
question="我刚才说的是哪张订单?",
)
MessagesPlaceholder 保留了每条历史消息的角色、工具调用和内容块。optional=True 允许首次对话不传历史。
消息占位符不会自动裁剪历史。传入前仍应执行 token 预算、摘要和工具消息完整性检查。
简写 placeholder 与显式对象
模板支持简写。
template = ChatPromptTemplate.from_messages(
[
("system", "你是简洁的助手。"),
("placeholder", "{history}"),
("human", "{question}"),
]
)
显式 MessagesPlaceholder 更容易看出变量名和是否可选,适合团队代码。简写适合结构非常简单、团队已经统一规范的场景。
模板组合应该组合职责,而不是拼碎片
模板可以用 + 组合。
role_template = ChatPromptTemplate.from_messages(
[("system", "你是数据库专家。")]
)
task_template = ChatPromptTemplate.from_messages(
[("human", "请评审下面的 SQL,重点检查{focus}。\n\n{sql}")]
)
review_template = role_template + task_template
组合适合复用具有明确职责的消息片段。不要把一句 Prompt 拆成十几个字符串组件,只为了追求抽象。碎片过多会让最终指令难以阅读,也更难判断冲突顺序。
一个实用标准是,独立片段是否有自己的角色、测试和复用场景。如果没有,保持在同一个模板里通常更清楚。
建立模板库
模板库不必从一个庞大类开始。可以使用按领域拆分的模块和创建函数。
prompts/
├── __init__.py
├── support.py
├── translation.py
├── summarization.py
└── review.py
# prompts/support.py
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
def build_support_prompt() -> ChatPromptTemplate:
"""构建售后问答模板,不在此处绑定具体模型或用户数据。"""
return ChatPromptTemplate.from_messages(
[
("system", "你是售后支持助理,只依据传入的政策和订单信息回答。"),
MessagesPlaceholder("history", optional=True),
("human", "政策:\n{policy}\n\n用户问题:\n{question}"),
]
)
创建函数比全局可变模板更容易测试和扩展。模板模块只负责消息结构,模型选择、用户授权和知识检索放在其他层。
给模板定义输入契约
Prompt 变量不是随意字典。可以用 Pydantic 在进入模板前校验。
from pydantic import BaseModel, Field
class SupportPromptInput(BaseModel):
policy: str = Field(min_length=1, max_length=8_000)
question: str = Field(min_length=1, max_length=1_000)
validated = SupportPromptInput(
policy="签收后七天内可以申请退货。",
question="我昨天签收,可以退货吗?",
)
messages = build_support_prompt().format_messages(
policy=validated.policy,
question=validated.question,
history=[],
)
长度限制不是安全方案的全部,但可以避免空输入、意外大文本和明显超出上下文预算的数据。
Prompt 版本不应靠文件名猜
当模板进入多人协作和生产环境,仅靠 prompt_final_v3_really_final.py 管理版本会很快失控。
LangSmith Prompt Management 将每次保存视为 Prompt Commit。普通 Commit tag 指向一个确定版本;staging 和 production 则是受平台管理的保留环境标签,应通过 Promote 流程推广 Commit,而不是在自由标签列表里随手移动。
先把远端 Prompt 的输入契约固定下来。下面的脚本只需在受控的管理环境运行一次,它会创建一个只接受 {question} 的私有 Prompt。随后在 LangSmith 的 Prompt 详情页把该 Commit 推广到 Production,运行时才引用 support-answer:production。
from langchain_core.prompts import ChatPromptTemplate
from langsmith import Client
managed_prompt = ChatPromptTemplate.from_messages(
[
("system", "你是售后支持助理,只依据传入信息回答。"),
("human", "{question}"),
]
)
Client().push_prompt("support-answer", object=managed_prompt)
远端 Prompt 应被当作可执行配置,而不是任意文本。只拉取经过审查的本工作区 Prompt,不要把未知公共 Prompt 直接接入生产请求。更重要的是,拉取模板和内置兜底模板必须使用同一组输入变量,否则版本切换会在运行时变成一次隐藏的接口变更。
动态拉取 Prompt 很方便,也引入运行时依赖。真实服务里,远端暂时不可用不该让每个用户请求都失败;但永久缓存又会让 production 标签更新后长期不生效。LangSmith SDK 自身默认也有全局 Prompt 缓存,因此下面的示例显式关闭 SDK 缓存,只保留一个行为清楚的五分钟本地缓存、最后一次成功版本和内置兜底模板。
import logging
from time import monotonic
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langsmith import Client
logger = logging.getLogger(__name__)
EXPECTED_INPUT_VARIABLES = {"question"}
FALLBACK_PROMPT = ChatPromptTemplate.from_messages(
[
("system", "你是售后支持助理,只依据传入信息回答。"),
("human", "{question}"),
]
)
class PromptResolver:
"""按固定时间刷新远端 Prompt,失败时保留最后一次成功版本。"""
def __init__(self, reference: str, refresh_seconds: float = 300) -> None:
# 只使用本类的缓存策略,避免 SDK 全局缓存延长 production 切换延迟。
self._client = Client(disable_prompt_cache=True)
self._reference = reference
self._refresh_seconds = refresh_seconds
self._cached_prompt = None
self._refresh_after = 0.0
@staticmethod
def _validate_input_contract(prompt) -> None:
"""拒绝输入变量发生漂移的远端模板,避免调用点在运行时失配。"""
actual_variables = set(prompt.input_variables)
if actual_variables != EXPECTED_INPUT_VARIABLES:
raise ValueError(
f"Prompt 输入变量应为 {EXPECTED_INPUT_VARIABLES},实际为 {actual_variables}"
)
def get(self):
"""返回当前可用模板,不把一次远端抖动放大成用户请求失败。"""
now = monotonic()
if self._cached_prompt is not None and now < self._refresh_after:
return self._cached_prompt
try:
prompt = self._client.pull_prompt(self._reference, skip_cache=True)
self._validate_input_contract(prompt)
except Exception:
# 这里只保护远端拉取边界,日志不记录用户问题或 Prompt 正文。
logger.warning("无法刷新远端 Prompt,继续使用可用版本", exc_info=True)
self._refresh_after = now + self._refresh_seconds
if self._cached_prompt is not None:
return self._cached_prompt
return FALLBACK_PROMPT
self._cached_prompt = prompt
self._refresh_after = now + self._refresh_seconds
return prompt
load_dotenv()
resolver = PromptResolver("support-answer:production")
model = init_chat_model("openai:gpt-5.4-mini", temperature=0)
response = (resolver.get() | model).invoke(
{"question": "退款为什么还没有到账?"}
)
print(response.content)
将 support-answer 改成工作区中真实的 Prompt 标识,并保证其输入变量仍然只有 question。这个例子每五分钟尝试刷新一次,拉取失败或输入契约漂移时优先使用最后一次成功模板,第一次就失败才使用内置模板。若业务要求一次发布中的结果绝对固定,应改为拉取明确 Commit ID,并把更新当作一次受控部署;若希望不发版切换,则引用 :production,在管理界面完成推广或回滚。两种策略都成立,区别在于是否接受运行中的版本变动。
无论选择哪种方式,都不要在每次用户请求时联网拉取。关键版本还可以与自己的 Git 仓库和 CI/CD 流程同步。
模板测试分三层
结构测试
检查角色顺序、必填变量和历史插入位置。
行为评测
用固定数据集比较回答正确性、格式、品牌风格和拒答行为。
生产监控
按 Prompt 版本记录 Trace、延迟、成本、用户反馈和失败案例。
Prompt 工程化不是把几句话放进更复杂的类。它是让固定配置、会话历史和本轮输入各归其位,让版本可追踪,让修改可评测,让发布可回滚。
当 Prompt 能被测试和治理,它才真正成为软件的一部分。
延伸阅读
更多推荐

所有评论(0)