每次 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 的底层载体,从论文和机制的角度,重新看这些你已经熟悉的概念。

相关阅读

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:23. Agent 失败模式分类学:为什么它总在你不期待的地方崩

本文链接:https://www.sshipanoo.com/blog/ai/ai-agent/23-失败模式分类学/

本文最后一次更新为 天前,文章中的某些内容可能已过时!