一帆风顺的人生应该是不存在的。
如果一个人真的从没摔过跟头,大概率不是世界格外温柔,而是有人替他承担了那些崎岖。人最终还是要成全自己,在艰难困苦中不断纠错、反思,然后继续往前走。
Agent 也一样。
我们为 Agent 搭建 Loop,让它拥有持续行动的能力;又用上下文、状态机和工具维持任务连续性。但如果没有纠错机制,这些能力越强,错误能够传播的距离反而越远。
一次错误的工具参数可能变成重复调用,一次网络异常可能带崩整段会话,一条损坏的历史消息可能让模型 API 永远拒绝请求,一次错误修改甚至可能污染后续所有判断。
所以,健壮的 Agent 不是从不犯错,而是允许错误穿过系统。
这里的“穿过”不是忽略,也不是吞掉。错误需要被看见、被分类、被转换,然后交给仍然有能力处理它的那一层。
观察 Pi Agent 这类 Coding Agent 的 Harness,可以把这套机制归纳成四道逐级升级的防线:模型能处理的交给模型;模型处理不了的由运行时收敛;输入协议损坏的在上下文边界修复;当前路径已经失去可信度的,退回会话分叉重新开始。
它们分别对应自愈、容错、修复与恢复。
错误不应该消失,而应该找到负责人
讨论纠错之前,需要先回答一个问题:这个错误应该由谁处理?
如果模型看完错误以后,能够换一种参数、换一个工具或调整执行顺序,那么错误应该返回模型。它不是致命异常,而是下一轮推理需要的新观察。
如果错误来自网络、模型服务或进程中断,模型本身没有能力修复,那么运行时应该保护状态、标记停止原因,并安全结束当前执行。
如果问题出在发给模型的上下文不合法,例如窗口溢出或工具调用不成对,那么错误应该在模型调用边界被修复,而不是让模型猜测一份它根本收不到的请求。
如果模型已经把任务带进死胡同,继续重试只会扩大污染范围,那么系统需要停止修补当前路径,回到此前可信的会话节点重新分叉。
四道防线不是四套互不相关的异常处理,而是一条错误升级链。上一层仍有能力处理,就不要惊动下一层;上一层已经失去处理能力,就必须明确升级,不能让错误悬在系统里。
第一道防线:让模型看见错误,并尝试自愈
Agent 与普通程序最大的不同,是它面对错误时不一定只能退出。
LLM 能读懂“文件不存在”“命令返回非零状态”“参数类型错误”这些信息,也能结合当前目标调整下一步。Harness 要做的,是把运行时错误转换成模型能够消费的观察。
工具失败应该成为 Tool Result
假设 Agent 想读取一个不存在的文件。
如果文件工具直接把异常一路抛到主循环,当前 Turn 可能就此终止,模型也没有机会知道自己错在哪里。但如果工具边界把失败包装成结构化结果,并标记 isError: true,错误就能重新进入下一轮上下文。
模型看到的不是一段突然中断的程序,而是一条可以行动的信息:目标路径不存在。
它接下来可能先查看目录,确认真实文件名;也可能回到仓库搜索相关符号;如果是权限拦截器拒绝了危险命令,它还可以换一种风险更低的方式完成任务。
人摸到滚烫的炉子,会因为疼痛改变动作。疼痛不是系统故障,而是身体把危险转换成了能够指导下一步的信号。
对 Agent 来说,错误消息承担的就是这个角色。
但错误消息不能只是把整个堆栈原样塞给模型。真正有用的信息应该包括失败的动作、错误类型、关键原因、是否允许重试,以及可能的修正方向。
过多的内部堆栈既会污染上下文,也可能泄露路径、密钥或实现细节。面向模型的错误需要真实,但也需要经过安全裁剪。
参数错误要先修补,再强校验
模型生成工具参数时,经常出现一些格式偏差。
例如 Schema 要求数组,模型却传来一段可以明确解析的 JSON 字符串;或者同一个模型版本长期把某个可选字段包装在错误层级里。
对于这种已知、确定且不会改变语义的问题,工具管道可以先执行兼容修补,再进行 Schema 运行时校验。
关键在于顺序:先修复可以无歧义修复的格式缺陷,再用严格 Schema 守住真正的工具契约。
如果校验仍然失败,就返回清楚的错误结果,例如“path 应为 string,实际收到 number”,让模型重新生成参数。
兼容垫片不能变成万能猜测器。系统可以把 "[1,2]" 确定地解析成数组,却不应该擅自猜测数字 42 究竟代表路径、文件编号还是别的东西。
一旦修补开始改变参数语义,所谓自愈就可能变成静默执行错误动作。
不是所有异常都应该消息化
“让错误回到模型”有一个前提:它必须是预期内、可解释、可行动的运行错误。
命令失败、文件不存在、权限拒绝和参数校验失败,都属于模型可能修正的观察。但空指针、状态损坏、违反系统不变量等程序缺陷,不应该伪装成普通 Tool Result 让模型不断重试。
更准确的原则不是“底层绝对不能 throw”,而是:
可恢复的业务错误在边界处消息化;不可恢复的程序错误必须被记录、暴露并升级。
否则,Harness 看似永不报错,实际上只是把真正的故障藏进了循环。
第二道防线:模型救不了时,运行时要安全收场
并不是每个错误都发生在工具内部。
网络断开、模型 API 返回 500 或 503、额度耗尽、流式响应中途失败,甚至用户主动按下 Ctrl+C,都可能让一次 Turn 无法继续。
这些问题通常超出了模型的自愈范围。模型无法通过多想一轮让网络恢复,也无法在响应尚未完成时给自己补出缺失的 Token。
这时,纠错的责任需要从模型上升到运行时。
把传输异常收敛成明确的结束状态
模型适配层是处理这类错误的边界。
它可以捕获网络或 SDK 异常,将错误写入事件流,并把本轮停止原因规范化为 error。如果终止来自用户主动取消,则使用 aborted,不要把人的决定伪装成系统故障。
主循环收到停止原因后,需要完成本轮和本次 Agent 运行的结束事件,持久化已经产生的消息、状态与工件,然后把可理解的结果交给上层界面。
这里追求的不是“系统里永远没有异常”,而是异常不能越过正确的边界,顺手带走整个进程和此前会话。
一次运行可以失败,但会话不能因此失忆;一次请求可以终止,但程序不应该留下无法判断的半完成状态。
给循环安装刹车
模型自愈也可能失败。
它可能不断用同一参数调用工具,也可能在“报错、换一种写法、继续报错”之间循环。每一步单独看都像是在尝试解决问题,但整体上已经没有新的信息产生。
因此,主循环需要最大轮次、工具调用次数、重复调用检测、时间限制和 Token 预算等硬性边界。
maxTurns 不是简单粗暴地限制模型能力,而是在系统无法证明自己仍然取得进展时,为成本和可恢复状态设置最后一道刹车。
达到限制以后,系统应该留下明确的停止原因,而不是假装完成。用户需要知道任务停在预算耗尽、重复失败还是外部服务不可用,后续才能决定重试、修改目标或回退。
第三道防线:请求不合法时,先修复上下文
还有一类错误,既不是工具失败,也不是模型服务宕机。
模型甚至还没开始推理,请求就因为上下文过长或消息协议损坏,被 API 直接拒绝。
这种错误不能交给模型自愈,因为模型根本看不到这次请求。负责发送请求的上下文边界,必须先把输入修复到 API 可以接受的状态。
预防压缩之外,还需要应急压缩
正常情况下,Context Manager 会在请求发送前估算 Token,并提前压缩较旧的历史。
但 Token 估算不可能永远精确。不同模型的计算方式、工具 Schema、隐藏开销和服务端限制都可能造成偏差,最终仍然收到 context length exceeded。
因此,预防性压缩之外还需要应急压缩。
当调用边界确认错误来自窗口溢出时,系统可以进一步缩减旧历史、压缩长工具结果、重新生成摘要,然后在有限次数内自动重试。
这里必须保住不可丢失的信息,例如当前请求、系统规则、关键任务状态和最近的工具交接。应急压缩不是从尾部随便截断,而是按信息优先级重新组装上下文。
自动重试也必须有次数限制。如果同一请求连续压缩后仍然溢出,系统应该停止并暴露诊断信息,避免把一次 400 错误变成无限摘要循环。
修复不成对的工具消息
工具协议通常要求 tool_use 与对应的 tool_result 成对出现。
如果一次运行恰好在工具调用之后中断,会话历史里可能只留下调用请求,没有结果。下一次恢复时,消息转换层若直接发送这段历史,模型 API 可能因为协议不合法而拒绝整个请求。
在转换为模型报文之前,系统需要检查工具调用配对关系。
对于确定已经中断的调用,可以补上一条明确标记为“未执行”或“因中断无结果”的合成结果;对于无法可信修复的损坏片段,则应该从本次模型视图中剔除,并保留诊断记录。
补齐的结果绝不能伪装成成功。它的作用只是恢复协议完整性,同时如实告诉模型:这个动作没有得到可用结果。
这里还有一个重要边界:修复的是发送给模型的视图,不一定要篡改原始会话历史。
原始记录可以继续保持追加式保存,消息转换层根据当前分支生成一份协议合法的模型输入。这样既能继续运行,也不会抹掉当时确实发生过的中断。
第四道防线:当前路径救不回来,就回到分叉之前
前三道防线都在尝试保住当前执行路径。
但有时,模型已经连续多次纠错失败,错误假设写进了任务摘要,代码也被改得面目全非。继续向当前上下文追加解释,只会让它背着更多错误历史往前走。
这时不应该再问“怎样修好这一轮”,而应该问“为什么还要留在这条世界线”。
“Dormammu, I've come to bargain!”
到了这个地步,该回滚了。
Session Tree 保存的不是一条直线
如果会话消息采用追加式存储,每个节点记录自己的父节点,那么一段会话就不必只有一条只能向前的线。
假设节点 A1 之后产生了失败尝试 A2 和 A3。用户可以把当前指针移回 A1,从那里发起新的请求,生成另一条分支 B1。
旧的失败尝试不需要删除。它们仍然可以用于审计和复盘,只是不再进入当前分支的上下文。
这种设计的价值不是撤回一句聊天,而是允许系统承认:当前路径已经不值得继续,然后从最后一个可信认知状态重新出发。
从逻辑上看,切换当前会话分支只需要移动指针。但这并不意味着完整回滚一定是 O(1),因为 Agent 改变的不只有消息。
回退会话,不等于回退世界
这是 Session Tree 最容易被误解的地方。
把指针移回 A1,可以恢复当时的对话、任务摘要和模型视图,却不会自动撤销已经写入磁盘的代码、安装的依赖、发送的请求或修改的远程数据。
真正的恢复有两个坐标:Agent 相信的世界,以及外部世界实际处于什么状态。
会话分叉负责前者。文件快照、版本控制、隔离工作区、数据库事务或专门的补偿动作负责后者。
如果模型回到了 A1,工作区却仍然保留 A3 的错误修改,它会带着过去的认知面对未来的文件,新的分支从第一步就已经不可信。
因此,Session Tree 要成为终极纠错能力,必须与外部工件的版本或 Checkpoint 建立对应关系。回退时,系统需要确认会话节点、任务状态和工作区版本重新对齐。
对于无法撤销的副作用,系统更应该提前设置审批与隔离,而不是把希望寄托在事后回滚上。
四道防线,其实是三种不同能力
写到这里,可以再区分三个经常混用的词。
纠错是发现当前动作不对,并产生一个更正确的动作。模型根据错误消息重新生成参数,属于纠错。
容错是错误已经发生,但系统仍然保持可控。API 失败后安全结束、保存会话并留下停止原因,属于容错。
恢复是当前执行无法继续,需要回到此前可信状态重新开始。Session Tree 与工作区 Checkpoint 协同回退,属于恢复。
上下文修复处在三者之间。它既修正请求结构,也保护运行连续性,但核心仍然是让错误停留在最了解它的边界里。
成熟的 Harness 不会把所有错误都交给同一个 try/catch,也不会期待模型解决系统里的每一种故障。
它会根据错误的性质分配责任:能成为观察的,返回模型;需要结束运行的,收敛成状态;属于报文损坏的,在调用边界修复;已经污染整条路径的,回到可信节点重新分叉。
结语:允许错误穿过,但不能任其留下
人并不是因为从未受伤才变得强大,而是身体知道如何感知疼痛、止住失血、修复伤口,并在伤势太重时停下来休息。
Agent 的健壮性也不来自一条永远正确的执行链。
它来自错误消息、停止状态、重试边界、上下文修复、持久化会话和可对齐的恢复点。这些机制共同保证,一次失败不会直接演变成整个系统的失败。
错误不应该被吞掉,因为被吞掉的错误无法纠正;错误也不应该直接刺穿系统,因为失去运行现场以后,连纠正的机会都不会留下。
真正可靠的设计,是让错误穿过正确的层,在每一层都留下足够的信息,然后被仍有能力处理它的对象接住。
允许万事万物穿心而过,不是毫发无伤。
而是它们经过之后,你仍然知道自己是谁,走到了哪里,以及下一次应该怎样继续。