🔗 原文链接:https://zhuanlan.zhihu.com/p/1996948771242800139
修复了部分我认为错误的地方,以及补充了一些内容。

在聊天场景中,通常存在基于历史对话消息,对最新的用户query做状态判断或指令执行。

可以很容易观察到一个现象:随着轮数增加,System Prompt指令失效,模型倾向于延续对话风格而非执行逻辑判定,这是目前大模型应用开发中非常典型的问题,这是一个非常深刻且体现了实际工程经验的问题。

1、常规对话式Prompt写法

通常做法是按照对话方式封装prompt:即把指令放在“system”中,历史消息放在“user/assistant”中,用户最新query消息放在最新一轮的“user”中。

[
{
"content": "#任务指令\n xxx,视为相关。\n - xxx,视为不相关。\n#输出要求\n请严格根据判定结果输出:\n- 相关输出:Y\n- 不相关输出:N",
"role": "system"
},
{
"content": "历史消息xxx",
"role": "user"
},
{
"content": "历史消息xxx",
"role": "assistant"
},
{
"content": "{用户最新query内容}",
"role": "user"
}
]

当消息轮数比较短时,这种做法效果是ok的,模型能够遵循指令输出"Y"或“N”。

2、Attention增强式Prompt写法

但当消息轮数比较多时,且"assistant"在历史消息的回复中出现多次相同的“口头语”或重复的pattern时,模型部分情况下会遵循指令输出"Y"或“N”,部分情况下不再遵循指令,而大多情况会以“口头语”开头进行输出或者承接“user”最新的query进行输出。

解法1:将指令放入“system”中,将所有消息放入“user"中并在尾部追加要求执行指令,比如 “请根据上述标准进行判定。“ 或 ”请基于以上历史消息,完成任务指令内容。”

[
{
"content": "#任务指令 \n ......\n 请根据<历史聊天记录>,...... \n xxx,视为相关。\n - xxx,视为不相关。\n\n# 输出要求\n请直接输出判定结果字符:\n- 相关输出:Y\n- 不相关输出:N\n",
"role": "system"
},
{
"content": "<历史聊天记录>user:xxx\nassistant:xxx\n...,\n</历史聊天记录>\n\n请根据上述标准进行判定。",
"role": "user"
}
]

解法1变种:将历史消息和用户最新query全部放入“system”中,将最新的"user"内容设置成指令内容,比如 “#请基于以上历史消息,完成任务指令内容。”

[
{
"content": "#任务指令\n xxx,视为相关。\n - xxx,视为不相关。\n#输出要求\n请严格根据判定结果输出:\n- 相关输出:Y\n- 不相关输出:N
\n#历史消息:\nuser:xxx\nassistant:xxx\nuser: {用户最新query内容}\n<历史消息>",
"role": "system"
},
{
"content": "#请基于以上历史消息,完成任务指令内容",
"role": "user"
}
]

解法2:只将历史消息放入“system”中,在最新的"user"的query内容后追加指令内容,比如 “{用户最新query内容} #请基于以上历史消息,完成任务指令内容”

[
{
"content": "#任务指令\n xxx,视为相关。\n - xxx,视为不相关。\n#输出要求\n请严格根据判定结果输出:\n- 相关输出:Y\n- 不相关输出:N
\n#历史消息:\nuser:xxx\nassistant:xxx<历史消息>",
"role": "system"
},
{
"content": "{用户最新query内容} #请基于以上历史消息,完成任务指令内容",
"role": "user"
}
]

解法3或4:把指令放在“system”中,历史消息放在“user/assistant”中,用户最新query消息放在最新一轮的“user”中或者用户最新query消息也放在历史消息中,类似解法1和2的情形,将指令内容放在“user”的尾部。验证发现解法3&4无效。

[
{
"content": "#任务指令\n xxx,视为相关。\n - xxx,视为不相关。\n#输出要求\n请严格根据判定结果输出:\n- 相关输出:Y\n- 不相关输出:N",
"role": "system"
},
{
"content": "历史消息xxx",
"role": "user"
},
{
"content": "历史消息xxx",
"role": "assistant"
},
{
"content": "{用户最新query内容} #请基于以上历史消息,完成任务指令内容",
"role": "user"
}
]

实验结论:解法1&解法2,构造的Context的结尾都是指令内容解法1相对更简单和通用

解法1~4的有效性均已在Qwen3-235B-A22B-Instruct-2507进行过验证,这已然是开源界的扛把子。

3、增强Attention的第一性原理

这两种解法本质都是 “打破对话流,强化指令Recency” 之所以有效,可以从大模型的模型架构(Attention机制) 以及 训练过程(Pre-training -> SFT -> RLHF) 两个维度来深度解析。

3.1、核心原理概览:注意力机制与Recency Bias(近因效应)

从最底层的 Transformer架构来看,模型在生成下一个 token 时,是基于当前所有 Context 的自注意力(Self-Attention) 计算。

  1. System Prompt 的衰减(Lost in the Middle / Start): 在常规写法(写法1)中,System Prompt 位于 Context 的最开端。随着对话轮数增加(Context 变长),指令距离当前生成位置越来越远。虽然理论上 Transformer 可以关注到全文,但在实际推理中,模型往往存在 Recency Bias(近因效应),即模型对“最近”输入的信息关注度更高。
  2. Pattern 的惯性: 当历史消息中充斥着大量的 User -> Assistant 对话模式,且 Assistant 经常使用“好的”、“我明白了”等口头语时,这在 Context 中形成了一种强烈的文本模式(Pattern)
  • 写法1(失效):最后的输入是 {用户最新query}。模型的 Attention 机制会根据历史 Pattern 预测:下一个动作应该是“回复用户”,而不是“执行System中的分类指令”。
  • 解法1 & 2(有效):最后的输入变成了 #请基于以上消息完成任务这打破了“对话续写”的 Pattern,强制将 Attention 的 Query 向量聚焦在“执行指令”上(做分类任务),而不是“回复内容”上

3.2、从模型训练阶段深度解析

要理解为什么模型会“部分情况下不遵循指令”,需要回溯模型是如何被训练出来的。

1. 预训练阶段 (Pre-training):作为“文本补全机”的本能

  • 原理:预训练通过海量文本学习“预测下一个 token”。模型本质上是一个概率统计模型,它在寻找上下文中概率最高的续写方式。

  • 现象解释

    • 如果 Context 是:“A: 你好 B: 您好 A: 吃饭了吗 B: ...”,模型根据概率大概率补全“没吃”或“吃了”。
    • 如果 Context 是:“指令: 判断Y/N。 历史: ... 问题: 吃饭了吗 指令: 请判断”,模型根据概率会补全“Y”或“N”。
  • 为何解法1&2有效: 在常规写法中,长历史对话构建了一个“聊天语境”的概率场。当把 {用户最新query} 单独放在最后时,模型的第一直觉是续写对话。 而解法1&2(将指令放在最后),相当于手动修改了前缀概率分布,告诉模型:“现在的语境不是聊天,而是做题”。

2. 有监督微调阶段 (SFT / Instruction Tuning):对格式的过拟合

  • 原理:SFT 阶段教会了模型理解 <system><user><assistant> 等特殊 Token 的含义。模型学习到:<system> 是全局约束,<user> 是当前指令,<assistant> 是输出。

  • 训练数据的偏差

    • 绝大多数 SFT 数据是短对话单轮指令
    • SFT 数据中,模型通常被训练为:紧接着 User 的输入进行回复
    • 关键点:在长多轮对话的 SFT 数据中,User 的最后一句通常真的是“对话内容”,而不是“对历史的总结指令”。
  • 失效原因当历史记录很长时,模型进入了“沉浸式对话模式”。 常规写法中,最后一个 User role 只有 {query}这完全符合“正常对话”的 SFT 训练样本分布因此,模型“忘记”了 System 中的约束,转而激活了“我是个有礼貌的助手”的 SFT 权重,开始输出“好的,关于这个问题…”。

  • 解法原理解法1(将历史放入 System,User 放指令)实际上是构建了一个Reading Comprehension(阅读理解)的任务格式,而非Chat(聊天)格式。这种格式在 SFT 训练中通常对应“根据文章回答问题”的任务,这类任务要求模型严格遵循指令,而不是发散聊天。

3. 人类反馈强化学习 (RLHF):Helpfulness vs. Instruction Following

  • 原理:RLHF 阶段通过 PPO 等算法,让模型更符合人类偏好(Helpful, Honest, Harmless)。
  • 副作用:RLHF 往往会强化模型的对话人格(Persona)。为了“Helpful”,模型倾向于给出详尽、礼貌的回复,而不是冷冰冰的“Y”或“N”。
  • 现象解释: 在常规写法中,如果历史消息里 Assistant 表现得很客气,RLHF 训练出的 Reward Model 会驱使模型继续保持这种客气(因为这通常能获得高分)。 直接输出 “Y” 或 “N” 在普通的对话场景中,往往会被 Reward Model 判定为“低质量回复”(太短、不礼貌)。
  • 为何解法1&2有效: 通过在 User 最后追加 “#请严格...输出”,这明确地切换了任务场景。这时候,RLHF 训练中关于“遵循显式约束”的权重占了上风,压倒了“礼貌回复”的偏好。模型意识到这是一个逻辑判断任务,而不是服务型对话任务
关于前缀概率分布【3.2.1中提及】

1. 基础:LLM 在做什么?
每次生成一个 token,模型实际上在计算:

P(下一个token | 所有已有token)

具体来说,模型输出的是一个词表大小的概率向量

输入: "今天天气真"
输出分布:
  "好"   → 0.42
  "不"   → 0.31
  "差"   → 0.12
  "棒"   → 0.08
  "热"   → 0.05
  ...    → 0.02

这个分布完全由输入的前缀决定。前缀不同 → 分布不同 → 生成结果不同。


2. 什么叫"前缀概率分布"

前缀 (Prefix) = 模型当前看到的所有 token(整个 context window 里的内容)。
前缀概率分布 = 这个前缀所激活的、对"下一个 token"的概率估计。

可以把它理解成:

前缀像一块磁铁,它的内容决定了"概率空间"里哪些词被吸引过来、吸引力有多强。


3. 为什么不同的前缀会激活不同的分布?

训练时,模型见过海量文本。不同的文本模式,在参数里留下了不同的"概率印记":

模式A:对话文本
"A: 吃饭了吗\nB: "  →  训练集里这之后大概率是  "没吃" / "吃了" / "还没呢"
                        → 模型把这类词的概率拉高

模式B:指令文本
"请判断以下内容是否符合要求,回答Y/N:\n..."  →  训练集里这之后大概率是  "Y" / "N"
                                              → 模型把 Y/N 的概率拉高

模型不理解语义,只是在做模式匹配式的概率估计。


4. 手动修改前缀概率分布

"手动修改"的意思是:通过改变 Prompt 结构,人为控制进入模型的前缀内容,从而改变它激活的概率分布。

来看个具体对比:

写法 A(指令在开头,query 在最后)

[系统指令] 你是一个判断助手,回答Y/N

[长对话历史]
用户: 今天怎么样?
助手: 还不错。
用户: 工作忙吗?
助手: 比较忙。
用户: 有没有按时吃饭?
助手: 有的。
...(20轮对话)

[用户最新query] 你开心吗?

此时模型看到的最近上下文(前缀末尾)是:

...有的。\n用户:你开心吗?\n助手:

这个前缀末尾激活的是 → 对话续写分布

"开心" → 0.38
"还好" → 0.29
"有点" → 0.18
"Y"    → 0.002  ← 极小!

写法 B(指令在最后,query 紧跟指令)

[长对话历史]
用户: 今天怎么样?
助手: 还不错。
...(20轮对话)

[用户最新query] 你开心吗?
[指令] 请根据以上对话判断用户情绪,仅回答Y/N:

此时前缀末尾变成了:

...你开心吗?\n请根据以上对话判断用户情绪,仅回答Y/N:

这个前缀末尾激活的是 → 指令执行分布

"Y"    → 0.61
"N"    → 0.35
"开心" → 0.02  ← 被压制

5. 为什么"末尾"的影响更大?
这涉及 Transformer 的 注意力机制 (Attention)

Attention(Q, K, V) = softmax(QK^T / √d) · V

生成下一个 token 时,Query 是当前位置,它会对所有历史 token 计算注意力权重。实际上靠近末尾的 token 权重往往更集中(因为它们在训练中更频繁地作为"直接前因"出现)。

可以这样直觉理解:

距离生成位置的距离:

[系统指令]           ← 很远,权重已经"稀释"
[对话历史 x20轮]     ← 远,形成"聊天语境背景噪声"
[最新query]          ← 近
[末尾指令]           ← 最近,权重最集中 ← 决定性影响

手动修改前缀概率分布 = 通过精心设计 Prompt 的结构和位置,让进入模型的 token 序列末尾呈现出"指令执行"而非"对话续写"的模式,从而把下一个 token 的概率质量从自然语言回复区域,迁移到结构化答案区域(如 Y/N、JSON 等)。
本质上你没有修改模型参数,但你改变了激活路径——在概率空间里,你把模型推向了另一片区域。

3.3、为什么解法1&2本质相同?

这两种解法本质上都是在做 Attention Steering(注意力引导)和Prompt Sandwiching(提示词三明治)

常规写法: [System指令] … [<History User,History Assistant>](长距离)** … [User Query]**

* 距离太远,指令信号衰减。

**解法1 & 2: [System指令 + Context(History)] + [User Query(Trigger指令)] **

* **Recency(近因)**:指令紧贴着生成的起点。
* **Contextualization(语境化)**:将“历史消息”和“最新Query”打包成了**被分析的材料(Context)**,而将“最后的User”剥离出来变成了**指令操作符(Operator)**。

提示词三明治(中间截断策略保护):如果上下文超长导致截断,通常截断的是 System/History 的中间,而**开头的指令(System)和结尾的触发器(User)作为两端,大概率被保留。这就是经典的“三明治(Sandwiching)”**策略。

4、为什么把指令放在User最后这么有效?

“近因效应(Recency Bias)”和“打破Pattern”,停留在问题现象表面层面,没有触及Attention机制最核心的数学运算逻辑,特别是关于**Attention(注意力)**的解释。

4.1、不是简单的“遗忘”,而是“Query向量的语义漂移”

前面提到“随着轮数增加,System指令衰减”,容易被误解为模型“记性不好”或单纯的距离问题。

更准确的底层原理是:Self-Attention 的 Q Q Q(Query)与 K K K(Key)的匹配机制发生了“语义漂移”。

1. Transformer 的运作机制回顾

在 Transformer 中,模型生成下一个 Token(输出)时,会将当前最后一个 Token 转换成一个 Query 向量 ( Q Q Q),然后去和前面所有 Token 的 Key 向量 ( K K K) 做点积(计算相似度),从而决定关注哪些信息。

2. 常规写法(失效)的 Attention 视图

  • 输入结构[System指令] ... [<History User,History Assistant>] ... [User最新Query]
  • 生成时刻:模型站在 “[User最新Query]” 的末尾准备生成。
  • 关键问题:此时的 Q Q Q 是由 “{用户最新Query}” 的内容生成的。
    • 假设 Query 是“那这种情况下怎么办?”。
    • 这个 Q Q Q 的语义是**“寻求回复”**。
    • 在计算 Attention 时,这个 Q Q Q 会与历史记录中的 K K K(上下文)发生强烈共振(因为语义相关性高),寻找与“怎么办”相关的上下文逻辑。
    • 最致命的是:这个 Q Q Q 与开头的 “[System指令]”(如“判断相关性”)的语义匹配度极低!
    • 结果:Attention Score 集中在历史对话内容上,忽略了System指令。模型自然而然地顺着历史内容输出回复。

3. 增强写法(生效)的 Attention 视图

  • 输入结构[System/History] ... [User Query] + [ #请基于以上...指令 ]
  • 生成时刻:模型站在 [指令] 的末尾准备生成。
  • 关键转变:此时的 Q Q Q 是由 “#请基于以上...指令“ 生成的。
    • 假设指令是“请判断Y/N”。
    • 这个 Q Q Q 的语义是**“执行判断”**。
    • 在计算 Attention 时,这个 Q Q Q 会像探照灯一样,去前面的 Context 中寻找**“判断依据”**,而不是寻找“对话逻辑”。
    • 结果:Attention 机制成功将焦点拉回到了任务本身,输出自然就是 “Y” 或 “N”。

4.2、对“生成边界”的重新定义

如果只能说一个最重要的原因,那就是:通过改变 Prompt 结尾,强行切换了模型生成时的「状态机(State Machine)模式」。

大模型在微调(SFT)后,其实隐含了两种主要模式:

  1. Chat Mode(聊天模式):看到 “User: xxx” 结尾,激活“拟人化、续写、回复”的权重。
  2. Instruct Mode(指令模式):看到 “...请执行xxx” 结尾,激活“分析、抽取、遵循”的权重。

深度原理解析:

  • 常规写法中,指令虽然在 System 里,但 Prompt 的 “物理结尾” 是对话内容。在大模型的概率分布中,Prompt 的结尾对下一个 Token 的生成具有统治级的决定权。结尾是对话,模型就认为任务是“对话”。
  • 你的两种解法,本质上都是把 Prompt 的**“物理结尾”变成了指令**。
    • 解法1: ”User: #请执行任务
    • 解法2: ”User: ... #请执行任务“ 这种做法,在 Transformer 的最后一层运算中,强行将**“当前的语义场”**锚定在指令上。

4.3、Positional Encoding

从模型底层原理看,解法1(History进System,User放指令)在长文本下明显优于解法2

这涉及到 位置编码(Positional Encoding / RoPE) 的影响:

  1. Attention Sink(注意力汇聚点): 现在的模型(如 Llama, Qwen 等)通常会有 Attention Sink 现象,即模型倾向于强烈关注 序列的开头序列的结尾
  2. 解法2的隐患: “{用户最新query} #指令” 如果 Query 非常长(比如用户发了一大段文字),指令只是跟在屁股后面的一个小尾巴。由于 Attention 的局部性,长 Query 的内部语义可能会淹没掉尾部短指令的权重。

解法1的优势(最佳实践)

[   
     {"role": "system", "content": "指令 + 历史 + Query"},   
     {"role": "user", "content": "现在,请执行任务指令。"} 
]
* **独立性**:最后的 User 这一轮是**纯粹的指令**,没有杂质(没有用户的具体Query内容干扰)。
* **强烈的 Q 向量**:生成的 $Q$ 向量非常纯净,就是“我要执行任务”。
* **截断保护**:如果上下文超长导致截断,通常截断的是 System/History 的中间,而**开头的指令(System)**和**结尾的触发器(User)**作为两端,大概率被保留。这就是经典的**“三明治(Sandwiching)”**策略。

4.4、小结

  1. Attention 原理层面:并不是模型“忘记”了 System,而是常规写法导致生成时刻的 Query 向量 与 System 指令 不匹配。解法1&2通过修改结尾,修正了 Query 向量的搜索目标。
  2. 最重要原因:利用 Prompt 结尾效应,将模型的隐含模式从 Chat Mode(续写) 暴力切换为 Instruct Mode(执行)
  3. 最佳推荐解法1。因为它构建了最清晰的 Attention 结构——将“待处理的数据(System+History+Query)”与“操作指令(User)”在结构上完全隔离,最大程度保证了指令执行的纯粹性。

5、 总结与建议

本文采用的解法1是处理 Long Context + Logical Decision 场景下的最佳实践(Best Practice)

底层逻辑总结(解法1&2有效,解法3&4无效对比亦可验证):

  1. 对抗 Recency Bias:将关键指令(Instruction)移动到 Prompt 的最末端,使其在 Self-Attention 计算中拥有最大的权重
  2. 打破工作 Pattern:通过改变 Prompt 结构,破坏“对话续写”的概率惯性强制模型进入“阅读理解/指令遵循”模式
  3. 规避 RLHF 陷阱:明确的指令后缀能抑制模型在 RLHF 阶段学到的“过度礼貌/废话多”的倾向

工程建议: 推荐使用解法1(将历史和Query打包入System,User仅放指令)。

  • 理由:这种结构语义最清晰。它在逻辑上将“数据源”(历史+Query)与“执行逻辑”(判断Y/N)彻底解耦。对于某些对 Prompt 结构敏感的模型(如 Llama 3 或 Claude 3),这种**“Context - Instruction”分离的结构通常能获得更稳定的 JSON/分类输出效果**。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐