Skip to content
文章目录
GitHub Star

给 Agent 加上可靠性机制后,为什么反而交不出结果? ​

给 Agent 加日志,是为了失败后查得清;加检查点,是为了中断后接着做。这些选择都有充分理由。但当一份结果已经保存,系统却因为后续记录失败而无法继续时,我们需要重新问:这套机制究竟在保护什么?

最近维护 ETF Agent 时,两个问题让这个疑问变得具体。一处可选的研究证据写入卷进数据库锁竞争,影响了正文提交;另一处工具读取已经完成,随后保存恢复检查点失败,独立的后续研究却被限制了。

它们并不意味着日志和恢复没有价值。真正需要检查的是:我们为了处理一种失败,是否又让主任务依赖了一个本不必成功的环节?

一、先问清楚,用户究竟在等什么 ​

假设用户让 Agent 比较几份材料,最后取得一份报告。这是一个简化的教学场景。

系统会做许多用户看不见的事:记录请求、保存工具结果、更新运行状态、写入恢复信息,再把正文交给用户。它们看起来都叫“保存”,承担的责任却不同。

有的记录决定能不能安全执行下一步。例如,外部操作可能产生不可逆效果,系统就需要在派发前保留足够的身份和状态,避免恢复时重复执行。有的记录保存已经发生的事实。有的只是为了以后更容易观察和排查。

如果把这些写入都当成同样重要,最省事的实现就是逐个等待:每一步成功,才允许下一步继续。正常时,这条路径很整齐;任何一个附加环节出问题,用户的报告也跟着停下。

因此,“失败就停止”是否合理,取决于不停止会造成什么具体风险。必要授权无法确认,应当阻止相应操作;少了一份可选调用副本,则应先问它是否真的影响原研究继续。

把这个区别说明白,比增加多少个异常处理分支更重要。

二、已经异步保存,为什么还会卡住正文? ​

第一个案例来自研究证据留存。系统希望留下模型请求与输出的有限副本,方便后续回查。这份副本不负责宣布模型请求有没有发生,也不负责决定正文能不能提交。

从调用关系看,它已经是旁路:原请求继续执行,证据进入有限队列,后台再写数据库。直觉上,这样就不会拖慢主流程了。

问题出在队列之后。

后台写入仍然使用同一个数据库,也要取得锁。在一次排查中,原数据库日志记录了三个事务之间的循环等待;证据写入持有来源相关的锁,又等待模型记录上的锁,正文提交也参与了这条等待链。调用是异步的,不代表资源已经隔离。

排查没有靠“后台保存应该没问题”结束。我们冻结旧版本,通过原存储入口安排受控竞争,复现了锁环,再检查哪一条等待可以解除。

修改落在可选证据写入这一侧:当它已经持有来源锁、却无法立即取得需要的模型记录锁时,让本次证据事务失败并回滚,释放已经占用的资源。失败仍被记录,不能伪装成证据保存成功;正文提交则沿自己的规则继续。

为什么不先延长超时,或者再加一次重试?因为循环等待没有因等待更久而消失。重试如果仍然以相同顺序持锁,也可能重新制造竞争。这里需要解除的是一条具体的资源依赖。

受控数据库验证中,正文的后续版本可以提交并读回,任务收尾和历史读取能够继续,后来的独立证据采集也能成功。这支持的是这条锁竞争路径的修复。原始事务中仍有未完全确认的调用细节,不能把受控复现写成已经还原了历史故障的全部原因。

这类问题也不限于 Agent。Google SRE 关于级联故障的讨论提醒我们,局部资源问题可能沿依赖关系扩散。对这个案例,更值得迁移的做法是:审查一个“可选后台任务”时,继续追到它实际使用的连接、锁和队列,而不是只看函数有没有被异步调用。

三、动作已经完成,为什么恢复记录还能让它失败? ​

第二个案例发生在动作完成之后。

工具读取已经结算,存储中有原动作的完成回执。系统随后补写恢复检查点,方便之后接续。受控故障注入让这一步在写入存储之前发生序列化错误。

结果是:已经完成的读取没有丢,但下一轮被迫进入只允许收尾的状态,原本还可进行的独立读取不再可用。恢复机制没有帮系统接着研究,反而缩小了它继续工作的空间。

这里最容易想到两个极端:所有检查点失败都终止,或者所有检查点失败都忽略。两者都没有处理最关键的差别:失败时,哪些事情已经确定发生,哪些还不知道?

当前能够确认的事实应保留的判断
原动作已经完成,后续检查点确定在写入前失败不撤销原动作事实;核对原回执及继续执行条件后,局部处理这次检查点失败
检查点可能已经写入,只是确认丢失保留结果未知,不能当作“肯定没写”,也不能直接重放
缺少可信完成回执,或已失去执行租约不能借局部降级继续产生受保护效果

修复因此很窄:只有能够沿原身份核对已完成动作,并且确认是对应检查点的写前失败,才采用局部隔离。原有取消和租约检查仍然生效。下一次工具派发也必须通过它自己需要的持久化边界,不能拿“上一步已经完成”作为以后都免检查的理由。

修复后的受控路径中,原读取只执行一次,另一项独立读取能够继续,后续答案和收尾可以完成。结果未知、缺失或伪造回执等对照仍走原有处理。它说明的是特定故障后的继续行为,不是任意崩溃都可安全恢复。

这个案例改变了我们看待检查点的方式:它描述和支持执行过程,但不是一份可以随意改写已经提交事实的第二账本。

四、可靠性能力,也要接受反向验证 ​

两个案例表面不同:一个是数据库等待,一个是恢复状态保存。共同的问题是,主任务多承担了一条依赖,却没有充分说明这条依赖为什么必要。

普通功能测试常问:“记录能否成功保存?”可靠性测试还应该反过来问:“记录保存失败时,其他哪些事情应该仍然成立?”

以报告任务为例,可以故意让可选证据写入争用、让已完成动作后的检查点在写前失败,再观察:

  • 原本已经提交的内容还能不能读到?
  • 不依赖这次失败的后续工作能不能继续?
  • 原动作有没有被重复执行?
  • 写入未知、用户取消或执行权失效时,系统是否仍按原规则处理?

前两项防止故障扩散,后两项防止为了“继续”而跨过真正需要保留的边界。只测到“不抛异常”远远不够:任务可能没有报错,却已经失去了继续研究的能力。

也不必因此建立一套庞大的新故障管理系统。先找到具体等待、写入和回执所在的位置,在原来负责它的模块修复,往往更容易解释影响范围。

五、下一次增加可靠性机制,先写下它失败后的样子 ​

日志、追踪和检查点通常因成功路径而被引入:能看清一次调用,能接续一项任务,能复查一份结果。设计时也应该写清它们失败后的行为。

可以从三个问题开始:

  1. 它为什么必须成功? 如果答案只是“这样记录更完整”,就还不足以让它阻塞用户结果;如果它保护权限或防止重复外部效果,就需要指出具体的执行时点。
  2. 它与主流程共享了什么? 异步接口之后,仍可能共享数据库锁、连接、容量和失败处理路径。
  3. 失败后,系统还能准确承诺什么? 有些记录缺失,可以明确留下缺口;结果未知,则需要保留未知。两者都不能被包装成成功。

回到那份报告:缺少一次可选观察,不应该自动抹掉独立合法的成果;已确认完成的动作,也不应该因为补写记录失败就被重新解释为没有发生。与此同时,系统必须继续知道何时没有足够条件安全执行下一步。

可靠性机制的价值,不只在于它成功时补救了什么,也在于它失败时,仍然给主任务留下了多大的继续空间。

延伸阅读 ​

欢迎在 GitHub 文集讨论这些取舍;如果对你的工程判断有帮助,也欢迎点一个 Star。

与个人博客并行:这里按问题连续阅读,博客保留时间线。 · 联系邮箱:hpoenixf@foxmail.com · 微信:hpoenixf0510