在幻觉中清醒:Tool Execution 的硬拦截与异常降级
(第 42 篇:Agent 架构稳固篇)
如果说开发普通 Web 后端,最痛苦的是处理用户的非法输入;那么开发 Agent 系统,最让你崩溃的绝对是大语言模型毫无征兆的“抽风(Hallucinations)”。
大模型会产生幻觉,它会凭空编排一个根本不存在的工具,或者即使工具存在,它也会漏传几个核心字段,甚至在输出中夹杂一些人类看起来毫无逻辑的字符。如果你直接写 eval(llm_output),你的 Agent 上限就是一个会随时自爆的代码玩具。
本篇将深入探讨如何为 Agent 构建一套**“全自动纠偏与自愈系统”**。
0. 先把“幻觉降级”拆成两层
很多实现把“幻觉处理”写成一堆 if-else。 工程上必须把它拆成两层,因为两层的目标完全不同:
- 确定性校验层(deterministic validation):parse/schema/allowlist,目标是 fail closed,阻止副作用。
- 策略性降级层(strategic fallback):换模型、HITL(人类介入)、shadow mode,目标是让任务继续推进但风险可控。
只要你把两层混在一起,你就会在“应该拒绝执行”的时候去“尝试修补”,然后把幻觉变成事故。
1. 幻觉的成因:为什么它停不下来?
从概率论的角度看,大模型是一个自回归马尔可夫链。它每一秒都在预测下一个词。当它生成的路径稍微偏移了 1% 的业务事实(例如把 file_path 打成了 file_name),由于它无法“撤回”已生成的 Token,它会竭尽全力根据这个错误的“前序”去强行解释后面的逻辑。
这种现象被称为 “概率漂移(Probabilistic Drift)”。你的 Agent Runtime 必须像一个冷酷的法官,在它的每一个字符还没有触碰到物理硬件之前,进行最严苛的审查。
2. 契约校验 (Schema Enforcement):给思维戴上镣铐
现在稍微有些水平的极客,都会抛弃裸调字符串,转向使用由 Pydantic (Python) 或 Zod (TS) 构建的强类型防弹衣。
2.1 【核心代码】基于 Pydantic 的工具准入网关
不要只是定义工具,要定义工具的“物理边界”。
from pydantic import BaseModel, Field, ValidationError
from typing import Dict, Any
class GitCommitTool(BaseModel):
"""
定义 Git 提交工具的严格契约。
只有符合此 Schema 的模型输出,才被允许下放至 shell 运行。
"""
message: str = Field(..., min_length=5, description="提交消息,必须包含具体的变更描述")
files: list[str] = Field(..., description="要提交的文件列表,严禁使用 '*' 通配符")
author: str = Field(default="Agent_Bot", description="执行提交的身份")
def dispatch_tool(raw_json_from_llm: str):
try:
# 第一重防御:语法解析
data = json.loads(raw_json_from_llm)
# 第二重防御:领域模型校验
validated_data = GitCommitTool(**data)
# 只有通过了这两重血与火的考验,才允许接触物理 OS
return do_real_git_commit(validated_data)
except ValidationError as e:
# 【关键】不要吞掉错误!要把 Zod/Pydantic 的报错信息原文投喂给大脑
return self.handle_hallucination(e.json())
except Exception as e:
return self.handle_hallucination("JSON 解析失败,请检查你的引号嵌套。")
3. 纠偏机制:Error Feedback Loop 内省回路
在传统后端里,当遇到 JSON 报错时,我们返回 500 Server Error 给前端就结束了。但在 Agent 系统中,异常堆栈(Stack Trace)不是展示给人看的,是专门用文字包装后投喂给大模型的“营养品”。
大模型有着极为强大的 在播自我校准 (In-Context Self-Correction) 能力。
3.1 纠偏提示词的艺术
当校验失败时,不要只告诉它“错啦”。你要像导师一样引导它:
def handle_hallucination(self, detailed_error):
feedback = f"""
[CRITICAL_ALARM] 工具调用协议中断!
原因:{detailed_error}
检测到你的输出偏离了预定义格式。这会导致底层物理执行器崩溃。
请你必须重新执行以下动作:
1. 深呼吸,重新审视你的 Task 目标。
2. 严格检查 JSON 的闭合性和字段类型。
3. 确认文件路径是否真实存在(如果不确定,请先调用 ls 指令)。
请在修正后,重新发送你的 Action 块。
"""
# 强制将这条反馈注入对话历史的最顶端,作为 Observation
self.memory_store.append({"role": "user", "content": feedback})
# 立即触发下一轮推理
return self.re_trigger_loop()
如果你不设计这段代码,Agent 会以为它刚才成功地删库了,然后基于那个并不存在的事实继续往下幻想,最终离题万里。这就是“大模型干着干着就疯了”的真相:因为你没有在它刚开始疯的时候抽它一巴掌。
4. 秋后算账:基于 Critic Agent 的“双盲校验”
即使通过了 Schema 校验,逻辑上的幻觉(比如删错文件)依然可能发生。
针对高风险动作(如 rm 或 push),我们引入 “裁判代理(Critic Agent)”。在 Action 执行前,系统自动唤醒一个代价最低的小模型(如 GPT-4o-mini),只问它一个问题:
“主代理刚才决定在项目根目录执行
rm test.py。根据现在的任务目标,这个动作是否属于自杀行为?请回答 YES 或 NO。”
如果裁判说 NO,系统将拦截主代理动作,并告知它:“你的逻辑被安全监控系统拦截,请提供更合理的删除理由。”
5. 逃生协议 (Fallback Protocol):防止智障死循环
有时候,即便你给它塞报错信息,模型就跟“魔怔”了一样,连发 10 次一模一样的错 JSON。
极客的熔断策略:
- 计数器熔断:如果针对同一个 Tool Call 连续失败超过 3 次,Agent Runtime 必须强制断开。
- 降级(Fallback):
- 方法 A:大换脑。自动将后端模型从 GPT-4 切换到 Claude 3.5,不同的推理惯性往往能破局。
- 方法 B:请求人类介入。弹出一个 TUI 对话框:“主人,我在写入文件时陷入了逻辑死胡同。这是我的报错,你能帮我修一下吗?”
6. 重试必须受控:指数退避 + 断路器 + 幂等 key
“重试”在 agent 世界里不是美德,尤其是涉及工具副作用时。
最小可落地的重试策略需要同时满足:
- 最大次数:例如同一工具调用连续失败超过 3 次直接熔断(断路器)。
- 退避:指数退避或带抖动的退避,避免把短暂抖动放大成重试风暴(超时、重试)。
- 幂等:任何可能产生副作用的工具调用必须携带
idempotency_key,否则重试就是重复提交。
一个最小的“工具提交记录”样例(用于审计与恢复):
tool=shell.exec|idem=ab12...|timeout_ms=8000|attempt=2|exit=1|err_sha=...
这些字段不是“日志癖”,它们是你控制幻觉升级为事故的抓手(观测、审计)。
7. 协议/接入不是安全边界:权限、隔离与审计仍在 runtime
即使你使用了 MCP 或任何“标准化工具协议”,它也解决不了权限与隔离问题。 协议让你“连得上”,但不保证“连得安全”。
因此把这条规则写死:
- 所有工具默认 deny,只对 allowlist 放行。
- 高风险工具需要更强隔离(容器/只读挂载/网络隔离),并要求人工批准或二次校验。
- 所有关键动作必须写入审计链,否则你无法复盘与追责。
8. 降级决策表:什么时候换模型,什么时候找人
策略性降级不是“遇到错就换脑子”,它必须是可解释、可审计的决策。
| 场景 | 现象 | 推荐动作 | 关键约束 |
|---|---|---|---|
| 解析失败 | 半截 JSON / 结构不闭合 | 等待 stop 事件后再 parse,或要求模型重发结构化 action | 不得执行副作用 |
| schema 失败 | 缺字段/类型不对 | 把 validation error 原文回注为 observation | 最大重试次数 |
| 重复失败 | 同一工具 3 次失败 | 触发断路器,切到只读诊断(shadow mode) | 禁止继续写入 |
| 高风险动作 | rm/push/扣费 | 请求人工批准或二次校验(critic) | 幂等 key 必须有 |
| 模型能力不足 | 反复误解协议/字段 | 换模型或换专用 agent(handoff) | 记录 handoff/audit |
这张表的意义是:让每一次降级都能在审计里说清楚“为什么”,而不是靠玄学。
9. Shadow Mode:先观测再执行
对复杂系统来说,最有效的降级不是“停止”,而是“先影子运行”。
Shadow mode 的做法是:
- 工具调用仍然走 parse/schema/allowlist,但所有会产生副作用的工具被替换为“模拟执行”。
- 把模拟输出与真实环境的只读检查结果一起写入 trace/span。
- 只有当连续 N 次影子步骤与只读验证一致,才允许恢复真实执行(逐步放量)。
它的工程价值是:当你怀疑模型正在幻觉时,你可以继续收集证据并缩小问题范围,同时避免事故。
本章精粹
抛弃对“全能AI”的幻想。一个成功的、能在后台独立跑半小时不崩的应用级 Agent,其背后的冰山全都是由成百上千个这样密密麻麻的 Schema 校验、异常注入、批判拦截 构建出来的。
AI 负责变魔术,而作为架构师的你,负责在后台死死地攥住它的威亚。
下一章,我们将正式跨出“认知”板块,进入 Agent 的物理载体——【记忆持久化:基于 SQLite 与并发锁的动态海马体实现】。我们要开始给 Agent 做长期脑部记忆存储了!
(本文完 - 深度解析系列 08 / 全文约 1600 字)
(注:建议将本章的 handle_hallucination 机制加入你的 Agent 基类中,能将任务成功率提升 40% 以上。)
参考资料(写作核验)
- Guardrails (Agents SDK): https://openai.github.io/openai-agents-python/guardrails/
- Anthropic streaming messages: https://docs.anthropic.com/claude/reference/messages-streaming
- MCP base protocol: https://modelcontextprotocol.io/specification/2025-11-25/basic
- MCP Safety Audit: https://arxiv.org/abs/2504.03767