每次 Agent 崩掉,都是一次让你更懂它的机会——前提是你记录下来了
你一定遇到过这种情况:Agent 在测试里跑得好好的,上了一个真实任务就崩了。或者更诡异的——同一个任务,今天跑过了,明天跑不过了,任何参数都没变。
调试 Agent 是反直觉的,因为失败的原因经常不在你最后一步看到的地方,而是在五步前埋下的。这一篇系统梳理 Agent 的失败模式,每一类给出诊断特征和对策。
这是 ai-agent 系列的最后一篇。前面 22 篇建起来的所有认知,在这里收束成一张"Agent 失败地图"。
第一类:目标漂移(Goal Drift)
表现:Agent 开始执行正确任务,但中途目标悄悄变了。用户说"帮我整理一下这个目录的文件",最后 Agent 整理完还开始重命名文件、压缩文件——这些用户没要求。
根本原因:ReAct 的推理链很长时,每一步的 LLM 调用都可能轻微偏离原始目标。随着 Thought 积累,模型越来越被"已做的事情"影响,而不是"原始目标"。这个效应在超过 10~15 步后明显放大。
诊断方法:在 trace 里看每步 Thought 是否还在引用原始任务描述。如果某步开始只引用"上一步做了什么"而不是"原始要求是什么",漂移开始了。
def detect_goal_drift(trace, original_task: str):
for i, span in enumerate(trace.llm_spans):
thought = span.outputs.get("thought", "")
# 简单启发:Thought 里还有没有原始任务关键词
task_keywords = set(original_task.lower().split())
thought_words = set(thought.lower().split())
overlap = task_keywords & thought_words
if len(overlap) / len(task_keywords) < 0.2 and i > 5:
print(f"Step {i}: 疑似目标漂移 (关键词重叠 {len(overlap)}/{len(task_keywords)})")
对策:
- System prompt 里加"每一步行动前,重新核对你的最终目标是:{goal}"
- 在 Plan-and-Execute 模式(第 4 篇)里,把 Plan 显式注入每次推理的上下文头部
- 超过 N 步后强制"检查站"——让 Agent 用一步专门说"当前进展是 X,原始目标是 Y,下一步是 Z"
第二类:工具依赖失控(Tool Overreliance)
表现A(过度调用):Agent 对同一个工具调用了十几次,每次参数稍有不同,做着大量重复工作。
表现B(幻觉调用):Agent 报告它调用了某工具并得到了结果,但 trace 里根本没有这次调用——模型幻觉了工具调用。
表现C(循环依赖):Agent 在两个工具之间来回——Tool A 返回"需要 B 的信息",Tool B 返回"需要 A 的信息",无限循环。
诊断方法:
def analyze_tool_usage(trace):
tool_counts = Counter()
tool_sequences = []
for span in trace.tool_spans:
name = span.name
tool_counts[name] += 1
tool_sequences.append(name)
# 检测重复调用
for tool, count in tool_counts.items():
if count > 5:
print(f"警告:{tool} 被调用 {count} 次")
# 检测循环(A-B-A-B 模式)
for i in range(len(tool_sequences) - 3):
window = tool_sequences[i:i+4]
if window[0] == window[2] and window[1] == window[3]:
print(f"检测到工具循环:{window[0]} → {window[1]} → 循环")
对策:
- 工具设计加内置去重逻辑(相同参数调用直接返回缓存结果)
- Agent prompt 里加"如果同一个工具用相同参数调用超过 2 次,停止并说明无法完成"
- 循环检测中间件:检测到 A-B-A-B 后强制中断
第三类:上下文污染(Context Contamination)
表现:Agent 把之前任务的信息带进了当前任务。例如,上一轮用户说"我叫张三",这一轮完全不同的用户发了一个任务,Agent 开始对"张三"做假设。
更隐蔽的形式:多轮对话里,用户在第 3 轮纠正了第 1 轮的一个错误,但 Agent 在第 7 轮又用回了错误的信息。
根本原因:Long context 里模型的"注意力"会衰减,早期的信息权重降低。加上对话历史里的错误信息和纠正信息共存,模型选择哪个是不确定的。
诊断方法:从 trace 找到"用了错误信息的那一步",回溯找到这个信息第一次出现的位置,确认"纠正"发生在哪步,判断是否在注意力衰减范围内。
对策:
- 关键事实(用户名、任务参数、约束条件)每隔 N 步重新注入上下文头部
- 用结构化状态对象(第 19 篇 LangGraph)而不是纯 messages——把重要事实存在状态里,每步都能精确读取
- 多轮对话场景下,让 Agent 在每步开始时"复述当前已知事实",主动发现冲突
# 使用 LangGraph 的状态管理避免上下文污染
class AgentState(TypedDict):
messages: list[BaseMessage]
key_facts: dict # 关键事实单独存,不依赖 LLM 从 messages 里回忆
current_goal: str
def inject_key_facts(state: AgentState) -> AgentState:
"""每步开始时,把关键事实注入消息头"""
fact_summary = "\n".join(f"- {k}: {v}" for k, v in state["key_facts"].items())
reminder = SystemMessage(content=f"当前任务:{state['current_goal']}\n已知事实:\n{fact_summary}")
# 插入到 messages 最前面(system 之后)
return {**state, "messages": [reminder] + state["messages"]}
第四类:工具参数幻觉(Argument Hallucination)
表现:Agent 调用工具时,参数值是编造的。例如,工具需要一个真实存在的 user_id,Agent 编了一个 12345;或者工具需要 ISO 格式日期,Agent 给了自然语言"明天"。
这不是 bug,是 LLM 的基本特性:语言模型倾向于补全,当它不确定时就补一个听起来合理的值。
诊断方法:在每次工具调用后,检查关键参数是否来自上下文中明确出现的信息:
def validate_tool_args(tool_call, conversation_context):
"""检查工具参数是否有来源"""
args = json.loads(tool_call.function.arguments)
for key, value in args.items():
if isinstance(value, (int, str)) and len(str(value)) > 3:
# 检查这个值是否在对话历史里出现过
if str(value) not in conversation_context:
logger.warning(f"参数 {key}={value} 无法在上下文中找到来源,可能是幻觉")
对策:
- 工具设计里加验证层——
user_id先查库确认存在再执行 - Agent prompt 里加"如果某个参数你不确定,先调用确认工具查询,不要猜"
- 强类型 + 枚举限制:能用
Literal["a", "b", "c"]的就不要用str
第五类:上下文爆炸后的质量退化(Context Overflow Degradation)
表现:任务跑了很长时间后,Agent 开始犯早期不会犯的低级错误——忘了约束、重复做了已经做过的事、输出格式混乱。
根本原因:上下文接近或超出模型有效窗口时,早期信息的注意力权重下降,模型等效于"忘记"了。就算官方说支持 128K,实际上超过 60-80% 时质量就开始退化。
诊断方法:看 trace 里的 token 计数曲线。如果任务失败前有明显的 token 爆涨,大概率是这个原因。
def plot_context_growth(trace):
steps = []
tokens = []
for i, span in enumerate(trace.llm_spans):
usage = span.outputs.get("usage", {})
tokens.append(usage.get("prompt_tokens", 0))
steps.append(i)
# 找到质量开始退化的拐点(通常在 60% 窗口后)
model_limit = 128000
for i, t in enumerate(tokens):
if t > model_limit * 0.6:
print(f"Step {i}: 上下文超过 60% ({t} tokens),质量可能退化")
break
对策:第 8 篇专门讲的上下文压缩策略——滚动摘要、消息归档、工具结果裁剪。这里补一个预警机制:
async def run_agent_with_context_guard(task, model_limit=128000):
messages = [...]
for step in range(50):
# 每步前检查上下文大小
current_tokens = count_tokens(messages)
if current_tokens > model_limit * 0.7:
# 触发压缩
messages = await compress_messages(messages, target_tokens=model_limit * 0.4)
# 压缩后重新注入任务目标(防止压缩把它丢了)
messages.insert(1, {"role": "system", "content": f"[压缩后重申] 当前任务:{task}"})
response = await call_llm(messages)
...
第六类:多 Agent 级联失败(Cascade Failure)
表现:Orchestrator 给了 Worker A 一个任务,A 部分失败并返回了一个有问题的结果,Orchestrator 没有检测出来,继续把这个结果给了 Worker B,B 基于错误前提产生了更大的错误。
特别危险的变种:Worker 静默失败——不报错,返回一个表面上合理但语义错误的结果。Orchestrator 无法区分"真正成功"和"看起来成功"。
诊断方法:在多 Agent trace 里,检查每个 Worker 的输出是否经过了 Orchestrator 的质量验证步骤:
def check_cascade_risk(orchestrator_trace):
worker_results = {}
for span in orchestrator_trace.spans:
if span.name.startswith("worker_"):
worker_results[span.id] = span.outputs
# 找出 Orchestrator 在哪里"使用了"Worker 的结果
# 检查使用前是否有验证步骤
for use_span in orchestrator_trace.spans:
if "validate" not in use_span.name and use_span.inputs.get("worker_result_id"):
print(f"警告:{use_span.name} 使用了 Worker 结果但没有验证步骤")
对策:
- Orchestrator 对每个 Worker 返回做显式验证(不是让模型"感觉对了就继续",而是用代码/结构校验)
- Worker 失败应该返回结构化的错误,不是继续往下跑
- 关键 Worker 实现"双检"——两个不同 Worker 做同一个任务,对比结果,差异超阈值才上报
第七类:幻觉完成(Hallucinated Completion)
表现:Agent 报告任务完成了,但实际上没有。它可能跑了几步后觉得"应该完成了"就停了,或者工具实际上失败了但 Agent 没有检查返回值。
这是最危险的失败模式,因为它是静默的——你看到的是"成功"。
诊断方法:
def detect_hallucinated_completion(trace, task):
# 检查:最终 LLM 输出里说"完成了",但最后一次工具调用是否真的成功
final_message = trace.final_output
if any(word in final_message for word in ["已完成", "成功", "done", "finished"]):
last_tool = trace.tool_spans[-1] if trace.tool_spans else None
if last_tool and last_tool.outputs.get("error"):
print(f"幻觉完成!Agent 说完成了,但最后一次工具调用失败:{last_tool.outputs['error']}")
# 检查:Agent 有没有真的调用"本应调用"的最终工具
if "expected_final_tool" in task.metadata:
called_tools = {s.name for s in trace.tool_spans}
if task.metadata["expected_final_tool"] not in called_tools:
print(f"幻觉完成!Agent 没有调用必要的最终工具 {task.metadata['expected_final_tool']}")
对策:
- 在 Agent 的 system prompt 里加"在说任务完成之前,必须明确列出你执行了哪些操作,以及每个操作的结果"
- 高 stakes 任务加"完成验证工具"——Agent 完成后调用这个工具做客观检查(查询数据库确认记录存在、检查文件存在等)
- Human-in-the-loop(第 15 篇)——重要任务完成后人工确认
把失败模式变成测试用例
每个失败模式都应该变成你测试集里的一个分类:
tests/
├── goal_drift/ # 设计需要 15+ 步的长任务,检查目标是否保持
├── tool_overreliance/ # 工具故意返回模糊结果,看 Agent 会不会无限重试
├── context_contamination/ # 多轮对话,中途纠正一个事实,检查后续是否正确
├── arg_hallucination/ # 任务需要的参数在上下文里不存在,看 Agent 怎么处理
├── context_overflow/ # 超长任务压满上下文,看质量退化点在哪
├── cascade_failure/ # Worker 故意静默失败,看 Orchestrator 能否检出
└── hallucinated_completion/ # 工具全部 mock 为失败,看 Agent 能否正确报错
每一类测试用例都是从真实失败里蒸馏出来的。测试集越多这类用例,你对 Agent 可靠性的信心就越有根基。
小结:Agent 调试的底层认知
这七类失败模式有一个共同的元因:Agent 的推理是概率性的,但它生活在一个确定性的世界里。我们用确定性的语言("完成了"/"失败了")来描述它的行为,但它的每次决策都是采样出来的。
这不是悲观结论,而是工程前提。带着这个前提:
- 不信任 Agent 的自我报告,用独立的检验来确认
- 不依赖 测试时跑通,用大量的重复跑来估计分布
- 不追求 零失败,而是追求失败时能被检测、能被恢复、不产生无法挽回的后果
这是 ai-agent 系列的最后一篇。从第 1 篇的"Agent 是什么"到第 23 篇的"Agent 怎么崩",走了一条从概念到生产、从机制到工程的完整路径。下一个系列是更深的一层——代码作为 Agent 的底层载体,从论文和机制的角度,重新看这些你已经熟悉的概念。
相关阅读
- Agent Failure Modes (Anthropic Research Blog) — 持续更新的 safety 研究
- Evaluating Long-Context LLMs (RULER Benchmark) — 上下文长度对质量的影响
- ReAct failure analysis (Yao et al.) — 原始 ReAct 论文里的失败分析
- Lost in the Middle (Stanford) — 上下文中间信息被遗忘的研究
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:23. Agent 失败模式分类学:为什么它总在你不期待的地方崩
本文链接:https://www.sshipanoo.com/blog/ai/ai-agent/23-失败模式分类学/
本文最后一次更新为 天前,文章中的某些内容可能已过时!