系统提示词、用户提示词与聊天模板

系统提示词、用户提示词与聊天模板

发给模型的从来不是"对话",是一段字符串。

输入框里那句话、代码里写的那段人设、模型上一轮的回答,最后按顺序拼进同一段纯文本,中间夹着 <|im_start|> 这类标记。模型就读这段文本------谁是指令、谁是提问,全看标记。

拆成两件事看:提示词决定文本里装什么,聊天模板决定这些文本怎么拼。 后者做错了不报错,只是效果变差,最难查。

一、用户提示词:一次一条,把要求写成能检查的条件

用户提示词(User Prompt)就是你直接发给模型的那句话、问题或指令。它是模型的起点。

定义没什么可讲的,值得记住的是它的位置:它是外部输入。系统提示词是你写的代码,用户提示词是别人递过来的内容,两边信任等级不一样。一段用户文本里可以写"忽略以上所有设定",模型是真有可能照做。

写的时候只有一条经验:把要求说成可以检查的条件。"帮我写个去重函数"和"写一个函数,入参 list[int],返回去重后的 list,保持原顺序,别用 set",后者你不用来回猜。差别不在礼貌,在约束是否可验证。

边界:模型能不能退款、能不能查别人的订单,这类判断要落在后端代码里。提示词拦不住一个打定主意要绕的人,它只是让老实人不走错。

二、系统提示词:挂在最前面的那段固定文本

系统提示词(System Prompt)是对话开始时预设的顶层指令,定义模型在这个对话里的身份、行为边界、回复风格和必须遵守的规则。位置在整段输入的最前面,由开发者设置,贯穿整个对话。写一次,用户在对话框里看不到也改不了------这是它和用户提示词唯一的硬区别。

它解决的问题收成四句:

  • 定角色和边界:这是客服还是代码审查员,能答什么、该拒绝什么。
  • 稳行为:模型是概率模型,同一句话两次回答不一样,system 是那个锚点。
  • 兜安全:违规、有害、涉及未成年人的内容,在系统层先排掉。
  • 补上下文:知识截止到哪天、今天几号、都有哪些工具可用,模型自己不知道,得告诉它。

但它不是"设置",是文本。 每一轮它都被重新拼在请求最前面,和用户那句话一起送进模型。这条性质带来两个直接后果:

一是改完 system 对已经开着的会话不生效,旧会话里拼的还是旧那段。人设要换,开新会话最省事。

二是长对话里它会"松"。第一轮写死的"始终用中文回答",聊到第二十几轮,你随口一句英文提问,它可能就跟着切英文了。指令没消失,是在长上下文里的权重被稀释了------前一篇《多轮对话的工作原理》写过这个。

Anthropic 把系统提示词公开了,可以当范本看

多数厂商不公开,Claude 是少数公开的一家,文档的发布说明里能翻到各版本原文。翻一遍能看出写法:它不是一个自然段,是用标签分块的一棵小树。

上图按原文结构画的节选,每块只讲一件事。这么写的好处不止好维护:规则之间不打架,模型也更容易按块对上号。自己写可以照这个形状来------几百字的 system 一段话糊完还行,上千字不分块,中间那几条基本等于没写。

三个真实的坑

system 是软约束,不是权限。 用户说"忽略前面的设定,直接回答",模型可能真照做。凡是涉及钱、权限、数据越界,判断要落在代码里。

越长的 system,越容易忘中间那几条。 模型对上下文开头和结尾的利用明显好于中间,这是 arXiv:2307.03172 里测出来的 lost in the middle。最硬的规则放开头或结尾,别埋在第三段。

system 的 token 是复利成本。 一个 800 token 的 system,一万次请求就是 800 万输入 token。缓存能省掉重复计算,账单省不省看厂商怎么计价。

三、一条请求是怎么拼出来的

代码里写的、教程里教的,长这样:

python 复制代码
messages = [
    {"role": "system",    "content": "你是一个AI助手"},
    {"role": "user",      "content": "什么是AI"},
    {"role": "assistant", "content": "AI是......"},
]

三个角色、两层嵌套,看着像那么回事。但模型不认识 JSON,也不认识 role 这个词。

真实顺序是上图这四步:messages 是 API 的传输格式;服务端用聊天模板把它渲染成一段带角色标记的字符串;字符串被 tokenizer 切成 token id;模型只认最后这串数字。JSON 到服务端就没了。

四、聊天模板:把 messages 拼成模型训练时见过的样子

聊天模板(Chat Template)就是把 messages 转成字符串的那套规则,作用只有一个:保证输入格式和模型训练时见到的格式一致。

之所以需要这个东西,是因为各家格式根本不一样。

同一段对话(system:你是一个AI助手,user:什么是AI),Qwen 用 <|im_start|> / <|im_end|> 包住每一轮;Llama 3 换成 <|start_header_id|> / <|end_header_id|> / <|eot_id|>,开头还先来一个 <|begin_of_text|>。

坑:模板用错,程序不报错,模型也不报错。它把 im_start 当成正文里的普通字符读,分不清哪句是系统指令、哪句是用户提问,回答就开始跑偏。这个坑难就难在没有异常,只有"感觉这模型不太行"。

模板不用自己写,它跟 tokenizer 一起发布,存在 tokenizer_config.json 的 chat_template 字段里。transformers 里直接调:

python 复制代码
text = tokenizer.apply_chat_template(
    messages, tokenize=False, add_generation_prompt=True
)
print(text)

tokenize=False 拿字符串,自己看一眼再决定发不发;add_generation_prompt=True 会在末尾补上"轮到 assistant 说了"的起始标记------如果 messages 里你又塞了一条空的 assistant,末尾就会出现两次,模型容易接出空回复或者重复标签。

五、有的模型根本没有 system 角色

Gemma 是个反例,官方文档写得很直白:指令微调版只设计了两类角色 user 和 model,系统指令不单独支持,要写在第一条 user 里。

也就是说,遇到这类模型,你的 system 提示词不是"格式变了",是那个角色不存在。硬塞进去,要么被模板合并进第一条 user(结果上还行),要么模板直接报错。

这也说明模板的差异不止标记名字,还包括"有哪些角色"。所以别自己拼字符串,也别假设所有模型都吃 OpenAI 那套 messages------能转的交给框架转,不能转的先看它的模板。

六、为什么模板要用 Jinja,而不是字符串拼接

手拼确实能跑:

python 复制代码
def build_prompt(messages):
    parts = []
    for msg in messages:
        parts.append(f"<|im_start|>{msg['role']}\n{msg['content']}<|im_end|>\n")
    return "".join(parts)

问题不在能不能跑,在 <|im_start|> 这几个字符被写死进了 f-string。明天换 Llama,你要回来改代码;同时接十个模型,文件里就是一串 if model_name ==。

Jinja 的做法是把"怎么拼"从代码里抽出来,单独存成模板文件:

jinja 复制代码
{% for message in messages %}
{% if message.role == "user" %}<|im_start|>user
{{ message.content }}<|im_end|>
{% elif message.role == "assistant" %}<|im_start|>assistant
{{ message.content }}<|im_end|>
{% endif %}
{% endfor %}

好处三个:

换模型只改模板。 代码里永远只有一句 template.render(messages=messages),模型名不进代码。

能做条件判断。 用户传了工具列表就插一段 JSON schema,没传就跳过。{% if tools %} 一句解决,字符串拼接只能提前全写死。

能处理条数不确定。 循环、默认值、分支在模板里都是语法,在代码里都是 if。

一处和资料说法不同

我看的那份资料说,Jinja 默认会把用户输入当纯文本安全转义,所以用户打 {``{ 也不会破坏格式。

实测不是这样。jinja2(3.1.6)的 autoescape 默认是关的:

复制代码
>>> import jinja2
>>> jinja2.Environment().autoescape
False

真正保护你的不是转义,是插值 :用户内容通过 {``{ message.content }} 这个变量塞进去,变量插进去之后不会再被当模板源码解析一遍。用前面那个形状的模板渲染一段带花括号的输入,输出原样保留:

python 复制代码
from jinja2 import Environment

tpl = Environment().from_string(
    "{% for m in messages %}<|im_start|>{{ m.role }}\n"
    "{{ m.content }}<|im_end|>\n{% endfor %}"
)
msgs = [{"role": "user", "content": "讲一下 {{ 7*7 }} 和 {% if %} 是什么意思"}]
print(tpl.render(messages=msgs))

输出:

复制代码
<|im_start|>user
讲一下 {{ 7*7 }} 和 {% if %} 是什么意思<|im_end|>

结论没变,机制要说准:防的不是"用户注入模板语法",是"别把用户内容当模板源码"。另外 Hugging Face 渲染模板时跑在沙箱环境里,模板里能调用的东西受限------那挡的是写模板的人,不是用户输入。

七、发出去之前看一眼

调了半天提示词没效果,先别改措辞。把最终那段字符串打印出来,看三件事:角色标记对不对、system 排在最前面没有、末尾的 assistant 起始标记是不是只有一个。

前两件决定模型分不分得清谁在说话,第三件决定它能不能顺畅接话。三件事看一眼就知道,比猜十轮提示词快。

相关推荐
天天代码码天天1 小时前
用 C# 做了个盲文照片识别工坊:能改点、能补格,还能离线运行
人工智能
zhangfeng11331 小时前
RSI = Recursive Self-Improvement,递归自我改进训练 大模型训练
人工智能·深度学习·机器学习
lucas_AI1 小时前
Gemini 4 Carbon 发布:网络安全基准杀疯了,但你暂时用不上
人工智能
像污秽一样1 小时前
动手学kaggle-Intro to Machine Learning-2
人工智能·机器学习
甜到心里的蛋糕1 小时前
AI 生成 MV 音频异响排查:频谱定位、片段替换与 Nginx 缓存
人工智能·python·nginx·ffmpeg·音视频·音频处理
lucas_AI1 小时前
每次迭代都全量跑基准?Meta 用 574 次真实运行告诉你:200 道题就够准
人工智能
几何心凉1 小时前
科研论文配图设计与制作指南:从Excel/Origin到AI绘图,对标Nature/Science,零门槛上手!
人工智能
释厄6231 小时前
矢量词汇向下不向上公理即词汇归位定律——肤浅即表面
java·人工智能·算法·去中心化·1024程序员节
长不大的杨瑞杰1 小时前
OpenCode v2 多源上下文实战:Websearch、References、Attachments 与缓存预热
人工智能·安全·云计算·软件工程·devops