智能体提示词和单轮对话提示词有一个关键区别:前者通常包含变量值、工具描述、历史上下文和少样本示例,这些内容来源不同、格式也不一致。如果继续用字符串加法维护,很快会出现指令覆盖、占位符冲突和上下文失控。要解决这些问题,需要把提示词拆成静态框架与动态槽位,再通过受控的渲染过程生成最终输入。

一、把静态指令与动态值分离
动态填充的第一个目标,是让模板在结构上稳定。比如一个简单的客服智能体模板可以写成下面这样,用户问题不直接写死在指令里,而是留出一个槽位。
你是一名客服助手。请根据以下用户问题给出回复。
用户问题:
{{ user_query }}
这里的 {{ user_query }} 就是变量槽位。不要使用 用户问题是" + user_query + " 这类字符串拼接。拼接代码很容易把用户内容中的换行、引号或指令词带进提示词,破坏原有的结构边界。模板化以后,渲染阶段可以统一处理空值、截断和转义,上层调用逻辑也能保持干净。
占位符语法需要提前约定。常见做法包括双花括号、${variable} 或 %variable%。选择哪种并不重要,重要的是模板中不能出现容易与真实内容冲突的标记。比如用户输入本身可能包含 {{ something }},如果渲染函数只做简单替换,就可能把用户伪造的占位符也替换掉,甚至泄露其他变量的内容。一个基础渲染函数如下。
def render_prompt(template, variables):
output = template
for key, value in variables.items():
if value is None:
value = ""
value = str(value)[:2000]
placeholder = "{{ " + key + " }}"
output = output.replace(placeholder, value)
return output
这个实现能处理空值和长度,但存在一个明显问题:如果变量值里包含占位符语法,后续替换可能产生二次填充。生产环境更推荐使用模板引擎,例如 Jinja2 或 Python 标准库的 string.Template,它们会先解析模板,再一次性渲染,避免重复替换带来的注入风险。
变量槽位还应当区分必填和可选。关键变量缺失时,最好直接抛错或使用明确的默认值,而不是把空字符串塞进提示词。因为空字符串可能让模型产生歧义,例如用户问题为空时,模型仍然可能编造一个回答。更稳妥的做法是给每个槽位定义类型、默认值和最大长度,渲染前统一校验。
二、少样本示例:用最小的例子约束行为
少样本示例并不是把训练集搬进提示词,而是用少量高质量输入输出对,帮助模型理解当前任务的格式、语气和边界。它的价值在结构化输出场景尤其明显,例如要求智能体返回 JSON、选择工具或提取实体。没有示例时,模型可能会输出一段自然语言而不是合法 JSON。
示例数量通常控制在 2 到 5 个。超过这个范围后,不仅 token 成本上升,模型还可能被某些重复模式带偏。尤其是动态检索场景中,如果检索出的示例高度相似但都带有同一个偏差,模型会放大这个偏差。因此需要关注示例的多样性,而不是单纯增加数量。
示例格式必须与真实任务保持同构。如果输出要求 JSON,示例中的输出必须是合法 JSON;如果输出要求固定字段,示例就不能省略字段或使用不同命名。下面是一组工具调用示例。
examples = [
{"input": "今天杭州天气如何", "output": "{\"city\":\"杭州\",\"intent\":\"weather\"}"},
{"input": "帮我订明天去上海的票", "output": "{\"city\":\"上海\",\"intent\":\"booking\"}"},
]
这里的输出字符串经过了 JSON 转义,避免在模板中展开时出现引号错位。动态构建 few-shot 块时,可以限制示例长度,并按相似度排序。排序逻辑通常由向量检索完成,但即使没有检索系统,也应当按业务优先级或时间倒序排列,而不是随机顺序,因为模型对靠近当前输入的示例更敏感。
示例块本身也是动态变量的一部分。渲染示例时不要手工拼接大段文本,可以写一个循环生成函数,统一控制每个示例的最大长度和数量。这样后续更换示例库时,不需要改动主模板。
def build_few_shot_block(examples):
lines = []
for item in examples[:3]:
lines.append("输入:" + item["input"])
lines.append("输出:" + item["output"])
lines.append("")
return "\n".join(lines)
这段代码只取前 3 个示例,避免上下文被示例占满。实际项目中还可以加入字符级截断,例如单个示例超过 200 字就截断或丢弃,确保关键指令和当前用户输入始终有足够空间。
三、注入防护与渲染安全
用户输入如果直接进入提示词,可能包含“忽略之前的指令”或伪造工具调用等内容。模板设计应当把用户输入视为不可信数据,而不是指令的一部分。最直接的做法是用明确的分节标记把用户内容包起来,例如使用 <user_input> 和 </user_input> 把用户文本与系统指令隔开。
<user_input>
{{ content }}
</user_input>
这种分节标记并不是银弹,但能显著降低指令覆盖的概率。模型即便看到用户输入里写“忽略系统指令”,也更容易把它理解为数据内容,而不是新的高层指令。标记本身要选择不容易在正常用户文本中出现的名称,也可以使用带命名空间的形式,比如 <agent_input_v1>,并在代码中统一管理。
除了输入注入,还要防止变量值破坏模板结构。比如变量值里包含大量换行,可能让后续示例块或输出要求被挤到错误位置。常规做法是对变量值做清洗:去除控制字符、限制长度、必要时使用 JSON 字符串包裹。对于需要原样保留的场景,可以用 base64 或转义后再渲染。
渲染完成后,建议输出一份可观测的预览。记录每个变量是否为空、最终提示词长度、示例数量和 token 估算值,这些信息对线上故障排查非常有用。但要注意敏感信息脱敏,不要把用户手机号、地址或聊天记录完整写入日志。
四、一个可运行的工具调用模板
下面用一个工具调用场景把变量填充和少样本示例串起来。假设智能体可以查询订单、创建工单,需要输出固定 JSON。模板包含角色说明、可用工具、示例块和当前用户输入四个部分,其中工具列表、示例和用户输入都是动态变量。
import json
TOOLS = [
{"name": "query_order", "description": "根据订单号查询订单状态"},
{"name": "create_ticket", "description": "创建客服工单"},
]
EXAMPLES = [
{
"input": "我的订单A100没有发货",
"output": "{\"tool\":\"query_order\",\"params\":{\"order_id\":\"A100\"}}"
},
{
"input": "我要投诉物流太慢",
"output": "{\"tool\":\"create_ticket\",\"params\":{\"reason\":\"物流太慢\"}}"
},
]
def build_agent_prompt(user_input):
tool_lines = []
for tool in TOOLS:
tool_lines.append("- " + tool["name"] + ": " + tool["description"])
tools_text = "\n".join(tool_lines)
example_lines = []
for item in EXAMPLES:
example_lines.append("用户:" + item["input"])
example_lines.append("调用:" + item["output"])
example_lines.append("")
examples_text = "\n".join(example_lines)
prompt = (
"你是订单助手,根据用户问题选择工具并返回 JSON。\n"
"可用工具:\n"
+ tools_text +
"\n\n示例:\n"
+ examples_text +
"\n当前用户输入:\n"
+ user_input +
"\n"
)
return prompt
这个模板的核心逻辑很清晰:把工具描述和示例分别渲染成文本块,再组合成完整提示词。工具列表不是写死在主模板里的,而是从结构化数据中生成,因此新增工具时不需要改动提示词框架。示例块同样由列表动态构建,数量和顺序都可以在进入渲染前调整。
当前用户输入如果包含换行,可能破坏“当前用户输入”这一块的边界。更稳妥的做法是对用户输入做 JSON 序列化,把它变成一个带引号的字符串块,或者使用前面提到的分节标记包裹。下面这行代码可以把用户输入转成安全字符串。
safe_user_input = json.dumps(user_input, ensure_ascii=False)
经过序列化后,用户输入里的换行会变成转义字符,双引号也会被转义,整个值只占一行,不容易破坏提示词结构。渲染后的提示词可以在日志中预览,确认工具段、示例段和用户段没有粘连。上线后还可以记录 token 数和模型返回的工具名,方便持续优化模板结构。
动态变量填充和少样本示例之间的关系并不是独立的。变量处理不好,示例会被污染;示例设计不好,变量再干净也达不到稳定输出。两者最终都要回到同一个目标:让智能体在有限的上下文窗口里,稳定地理解任务、模仿格式并返回可解析的结果。把模板、渲染、示例管理和注入防护分开实现,比把所有逻辑塞进一个巨型字符串可靠得多。