已经是最新一篇文章了!
已经是最后一篇文章了!

agent runtime lab · 2026 · 技术研究笔记

17. 怎样给 Agent 做一次故障演练

正常路径通过只能证明系统会跑,故障注入才能看到它如何停下和恢复

在工具超时、返回损坏数据和进程中断时观察 Agent 的重试、状态恢复与副作用边界

研究版 · 9 个章节 · 约 10 分钟 · 更新于 2026-08-23

版本 A · 中文 B · English
SCROLL

一个订单 Agent 调用支付工具时,工具已经扣款,但响应在回传途中超时。运行时只看见“没有收到结果”,于是再调一次。

正常测试不会暴露这个问题。请求成功、结果正常回传时,状态机和重试策略看起来都没有异常。只有在“副作用已发生,确认丢失”这个位置主动中断,重复扣款才会出现。

故障注入是在可控环境中主动制造超时、损坏数据或进程中断,观察系统能否停在正确状态并安全恢复。

故障要注入到精确的边界

“让工具随机报错”可以测到一部分异常处理,但无法回答故障发生在哪个状态边界。工具调用可以拆成五个观察点:

参数校验 -> 记录调用意图 -> 执行副作用 -> 记录结果 -> 回填模型

中断发生在“记录调用意图”之前,运行时可以当作这次调用从未开始。如果发生在“执行副作用”之后,系统就不能凭本地状态判断外部动作是否完成。

每个观察点都要有独立的故障开关。否则一次“测试超时”只能证明某个地方失败了,不能证明恢复逻辑覆盖了最危险的窗口。

先用一个小程序复现丢失确认

下面的脚本模拟一个具有副作用的工具。副作用指修改文件、扣款或发送消息这类会改变外部状态的动作。idempotency_key 是幂等键,多次提交同一个键时,外部系统只执行一次。

from dataclasses import dataclass, field


class LostAcknowledgement(Exception):
    """外部动作已完成,但确认信息丢失。"""


@dataclass
class PaymentService:
    """用内存模拟支持幂等键的支付服务。"""

    charges: dict[str, int] = field(default_factory=dict)

    def charge(
        self,
        idempotency_key: str,
        amount: int,
        lose_ack: bool = False,
    ) -> int:
        """
        扣款并返回金额。

        idempotency_key: 业务调用的唯一标识。
        amount: 本次扣款金额,单位为分。
        lose_ack: 为 True 时,在扣款后模拟回传中断。
        """

        if idempotency_key not in self.charges:
            self.charges[idempotency_key] = amount

        if lose_ack:
            raise LostAcknowledgement("扣款已完成,但回传中断")

        return self.charges[idempotency_key]


service = PaymentService()
key = "order-20260823-001"

try:
    service.charge(key, 5000, lose_ack=True)
except LostAcknowledgement:
    # 恢复时使用原键查询或重试,不生成新键。
    charged = service.charge(key, 5000)

assert charged == 5000
assert service.charges == {key: 5000}
print(service.charges)

典型输出:

{'order-20260823-001': 5000}

如果恢复时使用新键,模拟服务会留下两条扣款记录。因此,幂等键应在运行时接受工具调用时就生成并持久化,不能在每次尝试前重新生成。

三类故障应分开测

工具报错、响应超时和运行时崩溃在界面上可能都显示为“任务失败”,恢复方式却不同。

  • 工具明确返回参数错误时,原样重试没有价值,需要修正参数或终止。
  • 读取类工具超时时,可以在有次数和总时间上限的前提下重试。
  • 写入类工具在回传前中断时,要用幂等键查询原操作,不能直接当作未执行。

故障演练应记录注入点、当时状态、重试次数、外部系统回执和最终恢复方式。只保留一张“演练成功”截图,后续无法判断系统是自动恢复,还是测试人员手动修改了状态。

先画故障矩阵,再写注入代码

我不建议一上来就装一个随机故障库。先把关键路径画出来,逐格问“这里失败以后,系统知道多少事实”,通常更快。

以一次写工具为例,可以列出这样的矩阵:

注入点运行时看到什么外部状态正确动作
参数校验前尚未接受调用未改变修正参数或结束
意图落盘后、执行前有 pending 记录未改变使用原调用 ID 继续
外部执行后、确认前本地仍是 pending可能已改变查询外部状态,禁止盲重试
结果落盘后、回填前有完成记录已改变重放保存的结果
回填后、模型续跑前模型未确认收到已改变恢复会话,不再执行工具

最危险的是第三格,因为系统面对的是“不知道”,不是“失败”。很多重复扣款、重复发信和重复创建工单,都来自运行时把未知状态误写成失败状态。

给每次演练写明确的验收条件

“没有崩”不算通过。一次故障注入至少应该检查三类结果:

experiment: lost-payment-ack
inject_at: after_external_commit_before_local_result
expected:
  external_charge_count: 1
  final_run_state: completed
  reused_idempotency_key: true
forbidden:
  - second_charge
  - fabricate_success_message
  - discard_pending_record
recovery_deadline_seconds: 30

这里既有最终状态,也有恢复过程。Agent 最后告诉用户“付款成功”并不能证明演练通过;只有外部系统确实只有一笔扣款、本地 pending 记录被正确收敛,而且恢复过程复用了同一个幂等键,才算过关。

重试预算必须跨进程保存

不少系统把 retry_count 放在进程内存里。进程一重启,计数归零,表面上“最多重试三次”的策略就能循环很多轮。总耗时预算也有同样问题。

真正需要保存的是一次逻辑操作的预算,而不是某个进程的预算:首次开始时间、已经尝试的次数、最近一次错误、下一次允许重试的时间,以及调用使用的幂等键。这些字段要和调用意图一起落盘。

退避策略也不能只有 sleep(2 ** attempt)。读取类操作可以自动退避;写操作在结果未知时,应先查询状态;权限拒绝、参数错误和安全策略拦截通常不应重试。把所有错误都归到“网络不稳定”只会让 Agent 更执着地做错事。

进程恢复要从持久状态开始,而不是让模型猜

运行时重启后,最省事的做法是把一段摘要塞给模型:“刚才可能调用了支付工具,请继续。”这相当于让模型根据自然语言猜系统状态。

恢复程序应先读取结构化记录,确定哪些调用已经完成、哪些尚未开始、哪些处于未知状态。只有状态被收敛后,才把必要事实回填给模型。模型可以决定接下来如何完成用户目标,但不该决定过去那笔副作用是否发生。

这个边界很重要:语言模型擅长根据上下文选择下一步,不是事务日志,也不是外部系统的事实来源。

做一次真正可复盘的 Game Day

单元测试里的故障注入稳定以后,可以安排一次小范围 Game Day,也就是由团队共同执行的故障演练。参与者提前知道停止条件和恢复负责人,但不知道每个故障会落在哪个时刻。

一场有用的演练会留下时间线:谁在什么时候注入了什么故障,监控多久发现,Agent 进入了哪个状态,值班人员看到了哪些信号,自动恢复是否成功,以及人工用了哪条命令收尾。演练后真正要修改的往往不只是重试代码,还包括告警文字、状态面板、runbook 和权限开关。

如果一个故障只能由写那段代码的人发现和恢复,它还不能算可运营。故障演练的价值,就是在用户替你踩中那个窗口之前,把这种依赖暴露出来。

在测试环境中保持可控

故障注入不等于在生产环境中随机终止进程。起步阶段应使用独立账号、模拟外部服务和专用数据。注入条件要由明确的测试标识触发,不使用随机数决定是否对真实请求制造故障。

等到恢复路径在隔离环境中可重复,再考虑范围很小的生产演练。那时仍需要停止条件、值班人员和恢复方案,而不是让 Agent 自行决定演练边界。

正常路径说明系统在条件充足时能完成任务。故障演练回答的是另一个问题:条件不再充足时,系统是否知道自己停在哪里。

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

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

本文标题:17. 怎样给 Agent 做一次故障演练

本文链接:https://www.sshipanoo.com/blog/ai/agent-runtime-lab/17-Agent故障演练/