LLM Agent 失败实时检测与修复:从事件流到策略梯队
LLM Agent 现在最值得关注的问题已经不是“能不能跑起来”而是“跑着跑着失败了怎么办”。Agent 在真实任务里会反复遇到工具调用报错、输出格式不符合协议、上下文超限、死循环、甚至模型明明没执行成功却告诉你已经完成。这篇文章要讲的就是 LLM Agent 失败的实时检测和修复怎么第一时间发现失败、怎么分类、怎么用不同梯度的策略把它救回来以及怎么判断这套检测修复体系是真的有效而不是在折腾日志。我会按落地顺序拆先明确失败长什么样再设计检测事件流然后讲修复策略的梯队最后给一个最小可运行的工作流和验收指标。适合正在做 Agent 项目、但被稳定性问题反复折磨的开发者。如果你只是跑通了一个 Demo还没进入批量任务和生产环境这篇文章同样值得看因为很多问题在 Demo 阶段不会暴露。1. Agent 失败不是“模型太笨”而是运行链路不可控1.1 先把失败的典型形态列清楚很多团队第一次接触 Agent 时默认最大的风险是模型能力不够。实际跑一段之后会发现真正的风险是运行链路的不可控。模型输出了内容但内容不符合下游工具需要工具调用成功但返回结果超出上下文窗口任务执行到一半外部接口超时Agent 陷入同一个失败动作的循环反复消耗 token 却不推进任务。我在实际项目里见过最常见的几类失败工具调用失败。参数类型不对、缺少必填字段、权限不足、外部接口返回 5xx。这类失败通常能拿到明确的错误信息但 Agent 不一定能正确理解错误信息。输出格式不符合协议。要求返回 JSON结果输出了一段自然语言要求包含特定字段结果字段名拼错要求枚举值结果返回了自由文本。这类问题在解析阶段会直接抛异常。上下文超限。Agent 执行多步骤任务时中间结果、工具返回、历史对话累积起来超过模型窗口。这时候常见的报错是上下文长度超限Agent 行为会变得不稳定。死循环。同一个工具调用失败后Agent 不换方案而是用几乎相同的参数反复重试。表面看一直在“努力”实际上任务没有任何推进。幻觉导致错误完成。这是最隐蔽的失败。外部工具可能根本没执行成功或者返回了空结果但模型生成了一段看似合理的完成描述。没有校验机制的情况下这种问题会直接污染下游数据。模型服务本身的故障。限流、连接超时、服务暂时不可用。这类失败往往不是 Agent 逻辑能修复的只能靠等待、切换模型或降级。这些失败没有一种是靠“换个更强的 Prompt”就能彻底解决的。因为它们分散在模型调用、工具调用、数据解析、外部服务等多个环节必须有一套系统性的检测和修复机制。1.2 为什么“实时检测和修复”是刚需如果你只是在 Jupyter Notebook 里手动执行几次 Agent 调用失败了大不了重新跑一次。但当你把任务放到后台队列、做成无人值守的批量任务、或者暴露成服务给其他系统调用时失败就不是“重跑一下”这么简单了。实时检测和修复的价值体现在三个层面响应速度失败发生到被处理之间的时间差决定了任务能不能继续。如果靠人工盯日志一批任务跑完才发现中间失败了 100 次这时候修复成本已经很高。成本控制一个失败的任务如果反复重试会消耗大量 token。实时检测可以在第一次失败时就把错误信息、相关上下文保存下来避免无意义重试。体验一致性给用户交付的 Agent 服务不能因为一次工具调用失败就让整个任务卡死。用户能接受慢一点但不能接受永远转圈。要注意的是“实时检测”不等于“每毫秒扫描一次”。对大多数 Agent 任务来说检测粒度可以放在步骤级别。也就是说每执行一个动作检查一次状态。这个粒度既能覆盖绝大多数失败场景又不会给系统带来太多额外开销。1.3 普通程序 bug 排查思路不能直接套用到 Agent 上普通程序的 bug 是可复现的输入和输出之间有严格逻辑映射。Agent 不一样同一个 Prompt 交给同一个模型两次输出可能完全不同。这意味着什么意味着你不能只靠“复现”来修问题也不能靠“加一个 if 分支”来覆盖所有失败场景。所以处理 Agent 失败需要的是可观测、可分类、可恢复的工程框架而不是针对某一条报错写死补丁。先让失败可见再对失败做分类然后按类型选择修复策略。这就是实时检测和修复的核心思路。2. 实时检测先有事件再有告警2.1 从散乱日志到结构化事件流很多 Agent 框架默认的日志输出是给人类看的适合调试不适合做自动化检测。比如你看到一行Entering new AgentExecutor chain... Thought: I need to call the weather API. Action: weather_api Action Input: {city: 上海} Observation: 401 Unauthorized这行日志信息很全但要写代码去解析这段文本非常脆弱。如果模型输出格式稍微变化解析规则就失效了。更好的做法是在 Agent 执行过程中主动生成结构化事件。每个事件就是一个 JSON 对象包含本次动作的核心字段。下面是我常用的最小字段集{ timestamp: 2025-01-01T12:00:00Z, task_id: task_001, agent_id: agent_weather, step: 3, event_type: tool_call, action: weather_api, input: {city: 上海}, output_status: failed, error: 401 Unauthorized, duration_ms: 1200, token_usage: {input: 512, output: 128} }有了这种结构化事件检测器就不需要理解自然语言只需要读取关键字段。我想强调的是event_type、output_status、error 这三个字段一定要单独声明不能塞在 detail 文本里。否则你后续写检测规则时还是要做文本解析。2.2 检测器的分类规则检测和语义检测检测器可以分成两层。第一层是规则检测速度快、结果稳定适合处理明确的失败信号。比如单次工具调用耗时超过阈值。同一个 action 连续失败超过 N 次。模型输出 JSON 解析失败。上下文长度超过窗口的 80%。外部接口返回 429、500、503。任务总耗时超过设定上限。规则检测的好处是逻辑透明每次检测结果都能追溯到具体规则。推荐把所有规则集中管理不要散落在 Agent 的业务代码里。第二层是语义检测这是规则检测的重要补充。有些失败没有显式报错但行为明显偏离任务目标。比如模型要求 Agent 给用户退款Agent 却只查了订单状态就说“已完成”。这种问题靠字符串匹配和状态码判断不出来需要用另一个模型对当前轨迹做分类。语义检测的实现方式也很简单并不需要额外训练模型。把任务描述、当前步骤、已执行动作、最新模型输出发给一个评判模型让它输出“正常 / 偏离 / 需要人工确认”三种判断之一。代价是会增加一次模型调用所以不要把语义检测用在高频路径上只在关键节点或规则检测无法判定时启用。2.3 检测点应该放在哪里检测点不是越密越好而是要在关键时点设置检查。我建议至少覆盖四个位置模型输出之后、解析之前。检查输出格式是否合法字段是否齐全。工具调用之前。检查参数是否满足基本约束避免把明显错误的数据扔给外部系统。工具调用之后。检查返回状态、耗时、数据大小。任务结束前。做一次完整性校验确认 Agent 声称完成的操作和实际执行结果一致。还有一种准实时检测适用于长任务。比如一个任务要执行 30 分钟可以每隔 2 分钟检查一次心跳。如果心跳长时间没有更新说明 Agent 可能卡死或进入死循环。这种粒度不叫“实时”但对生产环境已经够用了。3. 修复策略不要只重试要有梯队3.1 第一层格式修复和参数修复不少失败并不需要重新调用模型直接在代码层修复就行。这类修复成本最低应该最先执行。常见的情况包括模型输出 JSON 多了一个尾逗号、少了一个引号。可以用宽松的 JSON 解析器修复。输出包含多余的 Markdown 代码块标记。剥离后重新解析。工具参数缺失了可选字段。按默认值补上。输入文本超过长度限制。截断或摘要后再传给下游工具。工具返回了空数组。保留空结果并给后续提示词注入“结果为空”的说明避免模型误判。这一类修复不消耗额外 token也不会引入新的模型输出可以放心使用。但要注意格式修复不能改变工具的原本语义。比如把“退款金额”从 0.1 修正为 1这种操作绝不能做。3.2 第二层重试和提示词调整当格式修复解决不了问题时才进入模型层面的修复。最常见的方式是把错误信息追加到下一轮提示词里让模型根据错误重新生成输出。如果你用 LangChain、LlamaIndex 或自研的 Agent 循环实现上通常是这样第 1 次调用执行动作 - 失败 第 2 次调用注入错误信息 - 重新执行这里有一个必须注意的点不要无限重试。没有新信息的重试大概率还是失败。我通常的做法是最多重试 2 到 3 次并且每次重试前检查一下新增的错误信息是否和上一次相同。如果连续两次拿到完全相同的错误说明模型在当前配置下无法解决这个问题继续重试只是在浪费 token。另外可以考虑切换模型。同一个任务用小模型失败换成大模型可能成功反过来大模型超时或限流时降级到小模型也可以作为一种保底方案。这里要提前设计好模型路由不要在出错现场临时拼逻辑。3.3 第三层任务降级和人工兜底修复并不总是能成功。当任务经过格式修复、重试、模型切换后仍然失败就要走降级策略。降级策略的常见选择跳过当前任务记录失败原因继续处理下一个任务。把任务标记为“需人工处理”进入人工队列。使用备用工具或备用数据源完成相似目标。把当前部分结果保存到草稿区不对外发布。人工兜底不是失败而是整套系统的最后一道安全网。一个健康的 Agent 系统应该允许一部分任务进入人工队列而不是强制要求所有任务 100% 自动化。修复器本身也需要防护。否则可能出现“修复器死循环”检测到失败修复再检测还是失败再修复。我建议给修复器设置单独的循环上限并记录每次修复操作前后的状态变化。如果连续修复多次但状态没有改善直接转人工。4. 落地实现一个最小可运行的检测-修复工作流4.1 核心数据结构和接口为了让检测和修复不依赖特定 Agent 框架我建议先定义统一的事件对象和检测、修复接口。下面是一个用 Python 写的简化版本。from dataclasses import dataclass, field from typing import Any, Optional dataclass class AgentEvent: task_id: str agent_id: str step: int event_type: str # tool_call, model_output, task_end, error action: Optional[str] # 工具名或动作名 input_data: Optional[Any] output_data: Optional[Any] status: str # success, failed, retryable error: Optional[str] duration_ms: int token_usage: dict field(default_factorydict)检测器接口可以这样定义class BaseDetector: def detect(self, event: AgentEvent) - Optional[str]: 返回失败类型None 表示无异常 raise NotImplementedError修复器接口class BaseRepairer: def can_handle(self, failure_type: str) - bool: raise NotImplementedError def repair(self, event: AgentEvent, context: dict) - dict: 返回修复后的参数、提示词或动作 raise NotImplementedError这里不要求接口设计多复杂重点是让检测和修复成为独立组件可以单独测试也可以复用到不同的 Agent 任务上。4.2 一个简单的事件循环下面是集成检测和修复的最小 Agent 执行循环。为了可读性省略了具体模型调用和工具执行细节。def run_agent_with_recovery(agent, detectors, repairers, task): max_repair_rounds 3 repair_round 0 event None while True: try: event agent.step(task) except Exception as e: event AgentEvent( task_idtask.id, agent_idagent.id, stepagent.current_step, event_typeerror, statusfailed, errorstr(e), duration_ms0, ) failure_type None for detector in detectors: failure_type detector.detect(event) if failure_type: break if not failure_type: if event.event_type task_end: return event, None continue # 进入修复流程 repair_round 1 if repair_round max_repair_rounds: return event, failure_type repaired False for repairer in repairers: if repairer.can_handle(failure_type): repair_context repairer.repair(event, task.current_context()) if repair_context.get(strategy) skip: return event, failure_type task.apply_repair(repair_context) repaired True break if not repaired: return event, failure_type这个循环的逻辑是每执行一步先跑一遍检测器。没有失败就继续下一步有失败就进入修复流程修复后重新执行当前步骤而不是从头开始执行整个任务。这样可以保留已经完成的中间结果节省 token 和时间。4.3 批量任务的闭环单个任务跑通了批量任务还有很多额外问题要处理。我建议至少维护一张任务状态表字段包括字段含义示例值task_id任务唯一 IDtask_001status当前状态pending, running, success, failed, manualattempts当前重试次数2last_error最近一次错误tool_timeoutrepair_history修复记录JSON 数组final_output最终输出结构化结果批量任务处理时几个常见的实践每个任务独立写入日志和状态避免多个任务混在一起难排查。失败任务先进入“待重试队列”而不是直接丢弃。重试队列要设置最大尝试次数超限后转入人工队列。输出文件建议按 task_id 命名不要用时间戳否则出问题后很难把输出和任务对应起来。另外并发数不要一上来就拉满。先跑 3 到 5 个任务确认稳定后再扩大并发。并发上去之后外部服务的限流可能成为新的失败来源这是批量任务特有的问题。4.4 用日志验证检测是否生效很多问题不是没有检测而是检测到了却没有记录下来。我建议把检测结果作为独立日志输出记录失败类型、触发规则、修复策略和最终结果。这样在排查时可以回答三个问题为什么要修复这个任务修复做了什么修复后任务成功还是继续失败如果没有这三个信息后面的优化和复盘都无从谈起。5. 判断系统是否真的变稳看指标不要凭感觉5.1 核心指标引入检测修复机制后一定不能只看“感觉稳定多了”。要量化效果我建议至少跟踪以下几个指标。指标含义怎么判断任务最终成功率最终成功完成的任务占比越高越好但不要强求 100%一次通过率未经过任何修复就直接成功的比例反映 Agent 基础能力修复后通过率经过修复后成功的比例反映修复层有效性平均耗时 p50 / p95任务完成耗时分布如果修复引入大量额外调用耗时会明显上升每任务修复次数单个任务平均需要修复多少次次数过高说明基础执行质量差人工介入率进入人工队列的任务占比越低越好但不能为 0Token 消耗变化修复带来的额外 token 开销要结合成功率一起看重点看“一次通过率”和“修复后通过率”的关系。如果一次通过率只有 30%但修复后通过率到了 95%说明检测修复体系有效但 Agent 本身的基础执行还有很大改进空间。如果两者都低那你可能需要先优化单步执行质量再指望修复层救场。5.2 回归测试集怎么搭建要验证检测修复系统有没有效果需要一组固定的测试样例。不要拿一两百个随机任务来测因为 Agent 的失败具有很强的随机性。我建议准备三类样本正常任务样本。保证能顺利跑通的正常任务用来观察修复层是否误伤了正常流程。有缺陷的输入样本。故意输入缺字段、超长文本、错误格式确认检测器能识别。历史失败任务样本。把之前真实跑挂的任务记录下来等检测修复上线后用同一批数据回归。回归测试时固定模型版本、固定 Prompt 模板、固定环境否则结果没有可比性。这里要特别提醒Agent 输出有随机性同样的输入跑两次结果可能不一样。所以建议每个样本至少跑 2 到 3 次观察结果的稳定性。5.3 什么时候不应该加这套机制检测修复不是万能的有些场景强制加入反而增加复杂度和成本。单次交互、失败后能立即人工重试的场景可以直接做简单重试不需要完整的事件流和修复器。比如你只是内部调试 Agent旁边就坐着开发者根本不需要人工队列。另外高实时性要求的场景要谨慎。语义检测会引入额外的模型调用可能让响应时间增加几百毫秒甚至更多。如果你做的是用户直接对话的服务优先用规则检测尽量少用额外的语义检测。还有一个更容易被忽视的情况任务目标极不明确时检测器可能把正常的探索行为误判为失败。比如让 Agent 自己决定调用哪个工具它先试一个工具失败再换另一个这其实是正常行为你一刀切成“失败”反而会破坏它的灵活性。设计检测规则时要把这类探索性动作排除在失败判定之外。6. 常见坑点和排查链路6.1 修复器本身也会引入新问题修复器不是只能带来好处。我在实际排查中遇到过几种典型的“修复副作用”修复导致偏离目标。为了躲避某个工具错误修复器把参数改成了和原始任务无关的值任务最终“成功”了但结果完全不可用。修复循环但状态不变。每次修复后看起来在做不同的事但任务状态没有任何实质推进直到耗尽最大修复次数。多次修复污染上下文。错误信息、修复提示、工具返回全部塞进上下文可能导致模型忽略最初的用户指令。检测误报打断正常流程。规则设置得太严格把正常的探索行为当成失败白白浪费修复机会。解决思路是修复器必须保留原始任务目标每次修复都对比“当前状态”和“任务目标”的差距修复操作要记录前后状态避免无效果修复。6.2 排查顺序现象到输入到环境到逻辑当 Agent 任务还是不稳定时别急着调 Prompt先按下面的顺序排查先看现象。是报错、卡住、还是输出异常报错信息完整吗日志有没有记录再看输入。输入数据是否完整编码是否正确是否有特殊字符或超长文本很多时候 Agent 失败的根因不是模型不行而是输入里带了脏数据。再看环境。第三方 API 是否可用是否限流依赖库版本是否和框架兼容如果换了 Python 版本或某个包升级后开始出问题优先查这里。再看参数。上下文窗口上限、重试次数、超时时间、并发数这些参数在不同任务规模下需要调整。再看检测逻辑。检测器是不是误报了规则是不是太严格失败的判定是否覆盖了所有关键步骤最后看修复逻辑。修复器能不能处理该失败类型修复后是否正确验证了结果这个顺序是我日常使用最频繁的。多数情况下问题出在输入和环境而不是模型能力。先排查外部因素再做模型层面的优化能省下大量时间。6.3 哪些边界条件不要指望自动修复有些边界条件很难靠修复层解决必须在设计阶段就想清楚工具本身不可用。比如上游 API 停机维护任何修复策略都无法恢复只能降级或等待。权限不足。Agent 调用工具时没有足够权限这和参数无关修复器改参数也没用。任务目标存在歧义。用户给的目标本身不清楚模型执行半天后你根本不知道它该做什么这时候检测器也无法判断成功还是失败。复杂的多 Agent 协作。多个 Agent 互相等待、互相覆盖状态时修复一个 Agent 的行为可能影响另一个 Agent需要从协调层维度设计而不是单靠 Agent 内部修复。模型能力本身不够。任务需要复杂的多步推理或领域知识模型做不到就是做不到。这时候最实际的降级策略是换模型或转人工。6.4 长期维护把失败样本变成改进素材检测和修复系统上线之后不是一劳永逸。每次失败都应该沉淀为可复用的样本。我建议每两周做一次失败复盘步骤可以很短从日志里挑出修复次数 Top 50 的任务。看失败类型分布是集中在某个工具、某个模型、还是某个输入模式。针对高频率失败把原因补进检测规则或修复逻辑。更新回归测试集把新的失败样本纳入下一次回归。这套机制跑起来之后你会看到一次通过率逐渐上升修复次数下降。这个过程比任何一次“调大重试次数”都更有价值。最后留一个建议不要一开始就追求 100% 自动化。先把检测做好让每类失败可见、可统计再逐步增加修复策略。先把单条任务跑稳再上批量先把规则检测做好再上语义检测。稳定性是一层一层加出来的不是一次设计出来的。