进阶篇:如何根治弱模型在 ReAct 中的"幻觉 Observation"越界问题?
在基于开源小模型(如 Llama-3-8B、Qwen-7B)或经过重度量化的模型构建 ReAct Agent 时,我们经常会遇到一个令人抓狂的现象:模型在执行完第一轮动作后,开始自顾自地往下生成"Observation(观察结果,属于幻觉)",不仅不等待环境的真实返回,连我们在 Prompt 中再三警告"严禁输出 Observation"的指令也完全无视。
一、 底层原理解析:为什么小模型会"越俎代庖"?
要解决这个问题,我们需要脱离 Prompt 工程的表层,进入大模型的生成原理层面。
1. 自回归生成的"惯性"与条件概率
大语言模型的本质是自回归(Auto-regressive)的 Token 预测机。在生成第 ttt 个 Token 时,其目标是最大化条件概率: P(xt∣x1,x2,...,xt−1)P(x_t | x_1, x_2, \dots, x_{t-1})P(xt∣x1,x2,...,xt−1) 在传统的 ReAct 模板中(例如早期 LangChain 的设计),上下文通常是一个扁平的纯文本(Flat Text):
text
Thought: 我需要搜索北京的天气
Action: Search[北京天气]
Observation: 北京今天晴,25度。
Thought: 我已经知道答案了。
Action: Finish[北京今天晴]
当你在第一轮把环境真实的 Observation 拼接到上下文中并交给模型进行第二轮生成时,上下文的结尾是: ... Action: Search[北京天气]\nObservation:[环境返回的结果]\n
由于小模型的逻辑泛化能力较弱,它在自回归生成时,高度依赖模式匹配(Pattern Matching) 。它发现上文的规律是 Thought -> Action -> Observation 交替出现。因此,在它输出完第二轮的 Action 后,其隐空间(Latent Space)中 Observation 这个词的下一个 Token 预测概率 P("Observation:"∣Context)P(\text{"Observation:"} | \text{Context})P("Observation:"∣Context) 会急剧升高。它不是在"思考",它只是在"顺口溜"式地补全接下来的文本序列。
2. 负向指令(Negative Constraints)的注意力稀释
"即使在 prompt 中再三强调不要输出",模型也不听。 这是因为 LLM 的注意力机制(Self-Attention)在处理负向指令 (如 "Do NOT output...")时存在天然缺陷。 在公式表达中,Attention Score 的计算为: Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)VAttention(Q,K,V)=softmax(dk QKT)V 模型在计算时,"Observation" 这个词汇本身作为 Key,获得了极高的 Attention 权重,而 "Not" 或 "严禁" 这些逻辑否定词的权重在长上下文中被严重稀释。能力越弱、量化程度越高的模型,对这种复杂逻辑组合的 Attention 捕获能力就越差。
3. "扁平文本"导致的角色混淆
早期的 ReAct 直接把"模型生成的动作"和"环境返回的观察"以纯文本的形式拼接在一起,这打破了指令微调(SFT)阶段建立的角色隔离(Role Separation)。模型分不清哪些是"我"说的话,哪些是"外部环境"喂给我的信息,最终导致角色越界。
二、 工业级解决方案:从工程到推理框架的组合拳
在实际业务中,我们绝不会仅仅依赖"修改提示词"来解决这个问题。面对弱模型,我们需要在推理端和数据端施加硬性约束。
解法一:👍 推理层的硬截断(Stop Tokens / Early Stopping)------ 最简单直接的工业标配
核心逻辑 :不指望小模型听话,而是通过推理引擎(如 vLLM, TGI)的参数直接从物理层面打断它。 实现方式 : 在调用大模型 API 或推理引擎时,设置 stop 参数。
python
response = llm.generate(
prompt=react_prompt,
stop=["Observation:", "Observation", "<|observation|>"]
)
原理:无论模型内部有多想预测出 "Observation:" 这个词,只要推理框架的 Logits 处理层监测到生成的 Token 命中了 Stop Words 列表,就会立刻强制停止生成(Early Stopping),并将控制权交还给业务代码(外部环境)。这是目前几乎所有 Agent 框架(包括 LangChain)处理 ReAct 的兜底方案。
解法二:👍 重构上下文模板(ChatML Role-playing)------ 从源头隔离角色
技术的来龙去脉 : 如前文所述,扁平文本是万恶之源。因此,近一两年的 Agent 改造中,大厂普遍废弃了 Thought/Action/Observation 的纯文本拼接,转向使用结构化的 Chat Template(如 ChatML 格式)。
实现方式 : 我们将 ReAct 映射到 system, user, assistant 和 tool 四个原生角色中:
- Assistant :专职负责输出
Thought和Action。 - User/Tool :环境的返回结果
Observation不再作为文本拼在 Assistant 的回复后面,而是作为新的一轮 User 轮次(或专用的 Tool 轮次)输入给模型。
上下文结构变迁: [System]→[User: 任务]→[Assistant: 思考+工具调用]→[Tool: 观察结果]→[Assistant: 继续思考]\text{[System]} \rightarrow \text{[User: 任务]} \rightarrow \text{[Assistant: 思考+工具调用]} \rightarrow \text{[Tool: 观察结果]} \rightarrow \text{[Assistant: 继续思考]}[System]→[User: 任务]→[Assistant: 思考+工具调用]→[Tool: 观察结果]→[Assistant: 继续思考] 通过强制穿插 <|im_start|>tool 和 <|im_start|>assistant 这样的特殊 Control Tokens,模型在自回归时,就不会顺势把环境的台词给抢了。
解法三:👍 引导式解码(Guided Decoding / Logits Processor)------ 剥夺模型的自由发挥权
核心逻辑 :如果我们希望模型 100% 按照我们的格式输出,最暴力的手段是在解码阶段(Decoding Phase)对概率分布 P(xt∣ct)P(x_t | c_t)P(xt∣ct) 进行掩码干预。 实现方式 : 利用 Outlines 或 vLLM 支持的 JSON Schema 引导式解码,或者编写自定义的 Logits Processor。 强制约束模型当前轮次只能按照如下正则化语法(Grammar)或 JSON 结构生成:
json
{
"thought": "字符串",
"action": "工具名",
"action_input": "参数"
}
原理 :如果强行规定了生成格式是 JSON 且只有上述三个 Key,模型在生成完 "action_input" 后,推理引擎会自动将闭合括号 } 的概率调整为 1(或极高),从而直接结束生成,物理上阻断了它继续生成"Observation"的可能性。
解法四:对齐微调期的掩码惩罚(Loss Masking in SFT)------ 算法本源的纠偏
如果你有权限微调这个小模型,那么从数据层面解决是最彻底的。 在构建 ReAct 轨迹(Trajectory)进行 SFT 时,一个极为常见的算法错误是:让模型去拟合整段包含 Observation 的对话。
正确的做法是采用 Masked Language Modeling Loss : 设一条训练轨迹为 X=[System,User,Thought,Action,Observation,Thought,Finish]X =[System, User, Thought, Action, Observation, Thought, Finish]X=[System,User,Thought,Action,Observation,Thought,Finish]。 在计算 Cross-Entropy Loss 时: L=−∑tmtlogP(xt∣x<t)\mathcal{L} = - \sum_{t} m_t \log P(x_t | x_{<t})L=−∑tmtlogP(xt∣x<t) 这里的 mt∈{0,1}m_t \in \{0, 1\}mt∈{0,1} 是 Mask 掩码。
- 对于 System,User,ObservationSystem, User, ObservationSystem,User,Observation 的 Token,mt=0m_t = 0mt=0(不计算 Loss,即不要求模型学习预测这些内容)。
- 只有 对于 Thought,Action,FinishThought, Action, FinishThought,Action,Finish 的 Token,mt=1m_t = 1mt=1。 通过这种微调,模型在潜意识(权重)里就会知道:"Observation 不是我该生成的东西",从而在根本上消灭幻觉越界。
总结
将一个看似玄学的"Prompt 不听话"问题,拆解为自回归机制、解码层干预、角色模板以及 SFT 损失函数的系统性问题。