Agent 工程体系 · 第 97/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。

Agent 故障应急:错误分类、止损、证据、回滚、补偿和复盘

Agent 进入生产环境后,故障不再只是“模型回答错了”。一次用户请求可能经过路由、模型回合、工具调用、权限校验、外部系统写入、记忆更新和异步任务投递。最终暴露给用户的,可能只是一个超时或一句错误提示,但系统内部已经发生了部分成功、重复执行、状态污染或不可逆副作用。

因此,Agent 故障应急的对象不是单个异常,而是一个正在演化的业务过程:

请求规划调用工具产生副作用确认状态回复\text{请求} \rightarrow \text{规划} \rightarrow \text{调用工具} \rightarrow \text{产生副作用} \rightarrow \text{确认状态} \rightarrow \text{回复}

其中任何一步失败,都必须回答六个问题:

  1. 这是什么错误?
  2. 它是否仍在扩大影响?
  3. 哪些证据已经可靠保存?
  4. 能否通过回滚恢复?
  5. 不能回滚时,如何补偿?
  6. 如何证明问题已经修复,并防止再次发生?

这六个问题分别对应错误分类、止损、证据、回滚、补偿和复盘。它们不是事后工作的六个并列栏目,而是一条有因果关系的应急链路。


一、先定义故障:Agent 的“错误”不是一种东西

1. 错误、失败、异常和事故

在工程语境中,几个词需要区分。

异常是程序观察到的非正常事件,例如超时、网络连接失败、JSON 解析失败。异常通常是技术层面的。

失败是一次操作没有达到预期结果。例如工具调用返回 500,或者模型生成了无法解析的结构化参数。

错误是导致失败的原因或状态,例如权限配置错误、Prompt 约束不充分、工具幂等键缺失。

事故是已经影响用户、业务数据、合规边界或成本预算的生产事件。事故不一定伴随程序异常:模型生成一个语法正确但业务错误的退款指令,同样可能构成严重事故。

可以将一次 Agent 运行抽象成:

R=(I,P,M,T,S,O)R = (I, P, M, T, S, O)

其中:

  • II:输入,包括用户请求和上下文;
  • PP:Prompt、策略、路由和工具定义;
  • MM:模型调用及模型回合;
  • TT:工具调用;
  • SS:外部状态变化;
  • OO:最终输出。

如果只检查异常 EE,只能发现:

E=程序是否抛出异常E = \text{程序是否抛出异常}

但生产正确性应检查:

C=f(I,P,M,T,S,O)C = f(I, P, M, T, S, O)

也就是:输入、推理过程、工具行为、外部状态和对用户的承诺是否共同满足业务不变量。

例如:

  • 模型调用成功,但选错了工具:没有异常,却是错误;
  • 工具返回超时,但实际已经扣款:表面失败,业务状态可能成功;
  • 最终回复说“退款已完成”,但退款工具只创建了申请:输出错误;
  • 第一次调用已成功,客户端因超时重试并产生第二笔订单:并发与幂等错误。

2. 六类故障分类

实际应急时,建议同时使用故障来源分类业务结果分类。只按 HTTP 状态码分类,会掩盖 Agent 特有的问题。

2.1 输入与上下文错误

输入错误包括:

  • 用户请求缺少必要字段;
  • 文件、图片或消息格式无法解析;
  • 用户身份、租户或权限上下文缺失;
  • 记忆内容过期、冲突或被错误注入;
  • 上游系统传入了错误的关联 ID。

这类错误通常可以安全地要求用户补充信息,但不能把所有输入错误都归咎于用户。例如,系统将上一位用户的会话记忆带入当前请求,属于上下文隔离故障。

2.2 模型与 Prompt 错误

模型与 Prompt 错误包括:

  • 意图识别错误;
  • 选择了不适合的工具;
  • 违反业务规则;
  • 输出格式不符合 schema;
  • 在不确定时编造结果;
  • 过度调用工具,形成循环;
  • 在工具失败后错误地继续执行;
  • 把工具的“已受理”理解为“已完成”。

这类错误常见的特点是:技术调用成功,但业务行为不正确。因此,不能用调用成功率替代业务正确率。

2.3 工具协议与实现错误

工具错误包括:

  • 参数校验失败;
  • schema 与实际接口不一致;
  • 工具返回格式变化;
  • 超时、限流、连接失败;
  • 工具执行成功但响应丢失;
  • 工具重复执行;
  • 工具缺少权限、审计或幂等控制。

尤其要重视“响应丢失”:

请求已到达工具客户端未收到响应\text{请求已到达工具} \land \text{客户端未收到响应}

这并不等价于:

操作未执行\text{操作未执行}

支付、下单、发券、发消息等写操作,必须把“未知状态”作为独立状态处理,而不是简单重试。

2.4 外部系统与数据错误

外部系统错误包括:

  • 数据库不可用;
  • 依赖服务部分故障;
  • 读写延迟导致旧数据;
  • 事务只提交了一部分;
  • 消息已投递但消费失败;
  • 外部服务返回业务拒绝;
  • 多个系统之间出现状态不一致。

Agent 通常是分布式流程的编排者,无法仅靠一个本地事务覆盖模型服务、工具服务和业务数据库。因此,系统必须显式记录外部操作的状态。

2.5 安全、权限与策略错误

包括:

  • 未授权用户调用高风险工具;
  • 工具参数越权;
  • Prompt 注入导致违反工具边界;
  • 敏感数据进入模型上下文或 Trace;
  • 代理绕过人工审批;
  • 记忆内容跨租户泄露。

这类故障的首要目标不是恢复吞吐,而是封锁权限、冻结风险操作并保留审计证据。

2.6 发布与配置错误

Agent 的有效版本通常不是一个代码版本,而是一个组合:

V=(Vmodel,Vprompt,Vtool,Vmemory,Vrouter,Vpolicy)V = (V_{\text{model}}, V_{\text{prompt}}, V_{\text{tool}}, V_{\text{memory}}, V_{\text{router}}, V_{\text{policy}})

只回滚代码而不回滚 Prompt、模型路由或工具 schema,可能无法恢复原行为。

常见发布错误包括:

  • 新 Prompt 让模型更频繁调用写工具;
  • 新模型改变工具选择分布;
  • 工具 schema 发布后,旧 Agent 仍使用旧参数;
  • Memory 结构迁移不兼容;
  • 灰度流量未正确隔离;
  • 配置中心回滚延迟,造成新旧版本混跑。

二、用业务不变量判断影响,而不是只看异常日志

1. 什么是业务不变量

业务不变量是无论 Agent 如何规划、模型如何生成,都必须保持成立的事实。

以“退款 Agent”为例,可以定义:

  1. 一个订单最多存在一笔有效退款;
  2. 退款金额不超过可退款金额;
  3. 未通过身份校验不能执行退款;
  4. 回复“退款已完成”时,支付系统必须返回最终成功状态;
  5. 工具超时后不能直接再次创建退款;
  6. 每次退款操作都必须可关联到用户请求和审批记录。

令:

  • aa:退款金额;
  • rr:已退款金额;
  • pp:订单实付金额;
  • qq:身份校验是否通过;
  • ss:支付系统最终状态。

一个安全执行条件可以写为:

q=10<aprs=可退款q = 1 \land 0 < a \le p-r \land s = \text{可退款}

而“可以向用户承诺完成”的条件是:

s=退款成功s = \text{退款成功}

注意,退款申请已创建退款处理中退款成功不能合并成一个布尔值。否则 Agent 会把中间状态错误地解释成最终状态。

2. 完整算例:一次超时不等于一次失败

假设用户请求退款 100 元,流程如下:

时间 事件 外部状态 本地观察
10:00:00.100 创建 Trace 成功
10:00:00.180 模型选择 refund_order 成功
10:00:00.220 工具请求发出 未知 已发送
10:00:01.220 客户端超时 可能成功 超时
10:00:01.250 Agent 重试 可能已成功 第二次请求
10:00:01.400 工具返回“重复退款” 已有一笔 业务拒绝
10:00:01.500 Agent 回复“退款失败” 实际已成功 用户误解

如果工具没有幂等键,第二次请求可能真的造成重复退款。即使工具拒绝重复退款,最终回复“退款失败”仍然错误,因为第一次操作可能已经成功。

正确做法是把操作状态建模为:

NOT_STARTED
  -> SENT
  -> SUCCEEDED
  -> FAILED
  -> UNKNOWN

当发生超时时,状态应进入 UNKNOWN,而不是 FAILED

timeoutUNKNOWN\text{timeout} \Rightarrow \text{UNKNOWN}

之后通过查询接口、事件通知或人工核对确定最终状态:

UNKNOWN -> SUCCEEDED
UNKNOWN -> FAILED
UNKNOWN -> NEEDS_REVIEW

这一步是回滚和补偿的前提。如果没有状态确认,就无法知道应该撤销、重试还是等待。


三、止损:先限制故障增长,再追求恢复

1. 止损的定义

止损是通过降低流量、能力、权限或副作用,把事故的增长速率降到可控范围。

设每分钟产生的风险操作数为:

G=λ×ptool×pfailure×pretry×cG = \lambda \times p_{\text{tool}} \times p_{\text{failure}} \times p_{\text{retry}} \times c

其中:

  • λ\lambda:请求速率;
  • ptoolp_{\text{tool}}:请求进入高风险工具的概率;
  • pfailurep_{\text{failure}}:单次操作失败或状态未知的概率;
  • pretryp_{\text{retry}}:失败后再次执行的概率;
  • cc:一次错误操作的平均影响。

止损可以降低任意一个变量:

  • 限制流量,降低 λ\lambda
  • 关闭高风险工具,降低 ptoolp_{\text{tool}}
  • 切换健康依赖,降低 pfailurep_{\text{failure}}
  • 禁止不安全重试,降低 pretryp_{\text{retry}}
  • 只读降级,降低 cc

这解释了为什么事故处理中通常先执行“暂停写操作”,而不是先修改 Prompt。修改 Prompt 需要验证,暂停写操作可以立即阻止新副作用。

2. 止损动作的优先级

第一优先级:阻断不可逆副作用

例如临时禁用:

  • 支付、退款、转账;
  • 删除、覆盖、批量更新;
  • 发券、发短信、发邮件;
  • 创建工单、下单、提交审批;
  • 会改变长期记忆或权限状态的工具。

将工具状态从 ENABLED 改为 READ_ONLYDISABLED,并由工具服务端再次校验。不要只在 Prompt 中写“暂时不要调用”,因为模型指令不是安全边界。

第二优先级:冻结错误版本

发布治理至少要支持按版本冻结:

model_version
prompt_version
tool_schema_version
memory_schema_version
routing_config_version
policy_version

如果某个版本导致故障,应停止继续扩大灰度,并保留当前实例使用的完整版本组合。不能只记录“Agent v42”,却无法展开 v42 对应的模型、Prompt 和工具定义。

第三优先级:降低重试和并发

重试不是默认的恢复手段。对读操作,有限次指数退避通常风险较低;对写操作,只有满足幂等条件时才允许自动重试:

retryable(op)=idempotent(op)failure_is_transport_uncertaintybudget_available\text{retryable}(op) = \text{idempotent}(op) \land \text{failure\_is\_transport\_uncertainty} \land \text{budget\_available}

若写操作没有幂等键,超时后的自动重试应被禁止,转为查询或人工确认。

第四优先级:降级为人工或只读流程

降级不是简单返回“系统繁忙”,而是切换到已定义的安全路径:

高风险自动执行
    -> 只读查询
    -> 创建人工审核任务
    -> 返回处理中状态

降级响应必须准确表达状态。例如:

  • 正确:“请求已提交,当前正在确认支付状态。”
  • 错误:“退款失败,请稍后重试。”——这可能诱导用户重复提交。

四、证据:没有完整上下文,就无法判断回滚和补偿

1. 证据不是日志堆积

证据是能够回答“谁在什么版本、基于什么输入、做了什么操作、外部系统最终发生了什么”的可关联记录。

一条普通错误日志:

refund failed: timeout

几乎不能支持事故分析。至少需要:

{
  "trace_id": "trace_...",
  "run_id": "run_...",
  "request_id": "req_...",
  "tenant_id": "tenant_123",
  "user_id_hash": "hash_...",
  "agent_version": "agent_2026_09_01_17",
  "model_version": "model_x",
  "prompt_version": "prompt_42",
  "tool_name": "refund_order",
  "tool_call_id": "call_...",
  "idempotency_key": "refund:order_1001:request_abc",
  "external_operation_id": "payop_...",
  "attempt": 1,
  "status": "UNKNOWN",
  "error_class": "TRANSPORT_TIMEOUT",
  "started_at": "2026-09-01T10:00:00.220Z",
  "ended_at": "2026-09-01T10:00:01.220Z"
}

敏感输入和输出不应无条件写入日志。应根据数据分级决定是否保存原文、脱敏值、哈希、摘要或仅保存长度与 schema 校验结果。

2. Trace、Span 与关联 ID

在 Agent 可观测性中:

  • Trace表示一次端到端工作流;
  • Span表示工作流中的一个有开始和结束时间的操作;
  • parent_id表示 Span 的父子关系;
  • group_id可以把同一会话中的多次 Trace 关联起来。

OpenAI Agents SDK 的内置 Trace 会覆盖运行过程,并为任务、模型回合、Agent、模型生成、函数工具调用、Guardrail 和 Handoff 等操作建立 Span。(openai.github.io)

一个典型层级如下:

trace: refund_workflow
└── task_span: request
    ├── turn_span: turn_1
    │   ├── agent_span: refund_agent
    │   └── generation_span: model_decision
    ├── function_span: verify_identity
    └── turn_span: turn_2
        ├── agent_span: refund_agent
        ├── generation_span: tool_selection
        └── function_span: refund_order

诊断时需要把以下 ID 串起来:

request_id
  -> trace_id
      -> span_id
          -> model_turn_id
          -> tool_call_id
              -> idempotency_key
                  -> external_operation_id
                      -> compensation_id

其中 trace_id回答“这次工作流发生了什么”,tool_call_id回答“Agent 发起了哪次调用”,idempotency_key回答“业务上哪些请求应被视为同一个操作”,external_operation_id回答“外部系统实际创建了哪一笔操作”。它们不能互相替代。

3. OpenAI Agents SDK 的证据边界

SDK Trace 默认会捕获模型生成和函数工具调用,但这些 Span 可能包含敏感输入输出;可以通过 RunConfig.trace_include_sensitive_data 控制是否记录这类数据。官方文档还指出,ZDR 组织不可使用该 Trace 能力。(openai.github.io)

长运行 Worker 还需要注意 Trace 导出时机。默认批处理处理器会在后台导出,但任务结束后不一定立即出现在仪表盘;如果需要一个工作单元结束时立即发送,可以在 Trace 上下文退出后调用 flush_traces()。该调用会阻塞到当前缓冲的 Trace 和 Span 导出完成,因此不能在 Trace 尚未关闭时调用。(openai.github.io)

示例:

from agents import Agent, Runner, RunConfig, flush_traces, trace

agent = Agent(
    name="Refund agent",
    instructions="Only create a refund after identity verification."
)

def handle_refund(user_text: str) -> str:
    try:
        with trace("refund_workflow") as current_trace:
            result = Runner.run_sync(
                agent,
                user_text,
                run_config=RunConfig(
                    trace_include_sensitive_data=False
                ),
            )
            return result.final_output
    except Exception:
        # 这里只负责记录并向上抛出;
        # 不要把异常吞掉后假装业务失败
        raise
    finally:
        # 适合短任务或需要及时看到证据的后台任务。
        # 高频同步请求中应评估阻塞和导出成本。
        flush_traces()

这里的 flush_traces() 只能保证 Trace 尽快导出,不能保证外部退款操作已成功,也不能替代业务操作记录。Trace 是诊断证据的一部分,不是业务事务日志。

4. 证据保存的并发问题

故障发生时,多个 Worker 可能同时处理同一请求或同一订单。证据记录必须具备:

  • 追加写或版本化写入;
  • 稳定的事件时间与接收时间;
  • 唯一事件 ID;
  • 去重规则;
  • 不依赖单个进程内存;
  • 与外部操作 ID 关联。

推荐记录事件,而不是只覆盖最终状态:

OperationRequested
OperationSent
OperationResponseReceived
OperationTimedOut
OperationStatusChecked
CompensationRequested
CompensationCompleted

因为“最终状态=失败”无法说明中间是否已经产生副作用。事件序列才能支持重建故障路径。


五、回滚:撤销系统版本,不等于撤销业务副作用

1. 回滚的两个含义

发布回滚是把 Agent 运行时切换到已知稳定的版本组合。

业务回滚是撤销已经发生的业务状态变化。

二者经常被混淆:

回滚 Prompt
≠ 撤销已经发出的退款
≠ 删除错误写入的记忆
≠ 自动恢复外部订单状态

发布回滚解决“继续产生错误”的问题;业务回滚解决“已经产生的错误结果”的问题。

2. 什么时候可以回滚

回滚适用于以下条件:

  1. 新版本是主要变化来源;
  2. 旧版本仍可运行;
  3. 旧版本的工具 schema、记忆 schema 和权限配置兼容;
  4. 回滚不会让正在执行的任务突然改变语义;
  5. 已产生的副作用有明确的业务处理方案。

可以把版本选择写成:

Vactive={Vstable,if incident_activeVcandidate,otherwiseV_{\text{active}} = \begin{cases} V_{\text{stable}}, & \text{if incident\_active} \\ V_{\text{candidate}}, & \text{otherwise} \end{cases}

但对正在执行的 Run,通常应保持版本固定:

Vrun=VstartV_{\text{run}} = V_{\text{start}}

否则一次运行可能在第一个模型回合使用新 Prompt,在第二个回合使用旧 Prompt,导致难以重放和判断。

3. 回滚步骤

一个可验证的回滚流程如下:

发现异常
  -> 冻结候选版本
  -> 停止扩大灰度
  -> 记录当前版本组合
  -> 切换稳定版本
  -> 验证新请求
  -> 处理存量进行中任务
  -> 评估已产生副作用

每一步都要有验证条件:

步骤 动作 验证
冻结 禁止新部署和配置变更 版本指纹不再增长
停止灰度 将流量权重设为 0 新 Trace 不再出现候选版本
切换 恢复稳定组合 新请求使用稳定版本
冒烟 执行只读和低风险用例 工具选择、权限和输出正常
观察 比较错误率和业务指标 没有继续扩大
存量处理 标记旧版本进行中的 Run 不重复执行未知写操作

4. 回滚的反例

假设 Prompt v42 会把“查询退款进度”错误地路由到“创建退款”,事故期间将 Prompt 回滚到 v41。

如果某个 Run 已经执行了:

v42 -> model turn -> refund_order request sent -> timeout

此时直接让 Worker 使用 v41 继续跑,模型可能再次规划“查询订单”。如果查询接口返回旧数据,系统仍然可能把已创建的退款视为不存在,并重新发送退款请求。

因此,对状态未知的存量任务,正确动作不是“换旧 Prompt 再跑一次”,而是:

暂停自动继续
  -> 根据 external_operation_id 查询
  -> 确认最终状态
  -> 进入成功、失败或人工复核

六、补偿:当回滚做不到时,建立业务等价的修复动作

1. 为什么需要补偿

分布式 Agent 流程中,很多动作不可逆:

  • 已发送的短信不能真正撤回;
  • 已发出的邮件可能已经被阅读;
  • 已完成的支付无法靠恢复代码撤销;
  • 已写入第三方系统的数据无法通过本地数据库回滚;
  • 已进入模型上下文的敏感信息不能当作从未发生。

此时需要补偿事务:执行一个与错误操作相反、或在业务上抵消其影响的动作。

如果正向操作是 FF,补偿操作是 CC,理想关系为:

C(F(s))=sC(F(s)) = s

其中 ss 是原始业务状态。但现实中通常只能做到业务近似恢复:

d(C(F(s)),s)ϵd(C(F(s)), s) \le \epsilon

这里 dd表示状态差异,ϵ\epsilon表示无法完全消除的影响,例如用户已经收到通知、产生了手续费或留下了审计记录。

2. 补偿不是重试

重试的目标是完成原操作:

失败 -> 再次执行 F

补偿的目标是修正已经发生的错误:

错误成功 -> 执行 C

如果操作状态未知,不能直接选择重试或补偿:

UNKNOWN
  -> 查询最终状态
      -> SUCCEEDED: 补偿或确认
      -> FAILED: 可按策略重试
      -> UNKNOWN: 人工复核

3. 补偿必须幂等

补偿本身也可能超时,因此补偿操作同样需要幂等键:

compensation_key = "refund-correction:{original_operation_id}"

伪代码:

def reconcile_refund(op):
    status = payment.query(op.external_operation_id)

    if status == "SUCCEEDED":
        if op.amount > op.allowed_amount:
            key = f"refund-correction:{op.external_operation_id}"
            return payment.reverse_excess(
                operation_id=op.external_operation_id,
                amount=op.amount - op.allowed_amount,
                idempotency_key=key,
            )

        return {"action": "confirm_success"}

    if status == "FAILED":
        return {"action": "mark_failed"}

    return {"action": "manual_review"}

输入是原始业务操作记录,而不是模型重新生成的参数。补偿任务不应再次依赖模型决定退款金额、订单号或用户身份;这些值应来自经过校验的事件和数据库记录。

4. 补偿算例:错误发券

假设 Agent 因 Prompt 错误给不符合条件的用户发放 100 元优惠券。

正向操作:

F(s)=s+coupon(100)F(s) = s + \text{coupon}(100)

理想补偿:

C(F(s))=scoupon(100)C(F(s)) = s - \text{coupon}(100)

但如果优惠券已经被使用,补偿就不能简单删除。补偿流程可能变成:

  1. 查询优惠券是否已领取;
  2. 查询是否已使用;
  3. 未使用:撤销优惠券;
  4. 已使用:冻结后续权益并创建人工处理任务;
  5. 记录用户通知和审计原因;
  6. 评估是否需要退款或客服补救。

这说明补偿方案必须根据副作用的时间和可逆性设计,不能用统一的“把数据改回去”处理所有事故。


七、错误处理状态机:把“异常处理”变成可恢复流程

可以把一次高风险工具调用建模为状态机:

stateDiagram-v2
    [*] --> NOT_STARTED
    NOT_STARTED --> VALIDATING
    VALIDATING --> REJECTED: 参数/权限不合法
    VALIDATING --> READY: 校验通过
    READY --> SENT
    SENT --> SUCCEEDED: 明确成功响应
    SENT --> FAILED: 明确业务失败
    SENT --> UNKNOWN: 超时/连接断开/响应丢失
    UNKNOWN --> SUCCEEDED: 查询确认成功
    UNKNOWN --> FAILED: 查询确认失败
    UNKNOWN --> NEEDS_REVIEW: 无法确认
    SUCCEEDED --> COMPENSATING: 发现业务错误
    COMPENSATING --> COMPENSATED: 补偿成功
    COMPENSATING --> NEEDS_REVIEW: 补偿失败或部分成功
    FAILED --> RETRYABLE: 满足安全重试条件
    RETRYABLE --> SENT
    REJECTED --> [*]
    COMPENSATED --> [*]
    NEEDS_REVIEW --> [*]

关键规则如下:

  • VALIDATING阶段解决参数、权限和业务前置条件;
  • SENT表示请求已经离开本地系统;
  • UNKNOWN表示系统不知道外部最终状态;
  • FAILED只有在获得明确失败证据后才能进入;
  • RETRYABLE不是所有失败的默认去向;
  • SUCCEEDED后发现业务错误,必须进入 COMPENSATING,不能把记录改成“从未成功”。

状态机的价值在于,它限制了错误路径。例如,禁止:

UNKNOWN -> RETRY

除非先证明操作具备幂等性,或者通过外部查询确认原操作没有成功。


八、模型故障与工具故障必须分开处理

1. 模型生成错误

模型生成错误通常没有可靠的“撤销模型输出”动作。处理重点是:

  • 阻止模型输出直接成为高风险执行指令;
  • 通过结构化 schema、业务校验和权限校验拦截;
  • 将模型决定和工具实际执行分离;
  • 保存模型回合、工具候选和最终选择;
  • 用 Trace 评估路由、交接、Guardrail 和工具选择。

OpenAI 的 Agent Evals 文档将 Trace grading 定位为调试工作流级问题的入口:Trace 可以记录一次运行中的模型调用、工具调用、Guardrail 和 Handoff,再通过 Grader 对轨迹进行结构化评分。(developers.openai.com)

例如,不能只对最终答案打分:

最终答案:退款已完成
评分:正确

还应检查:

身份校验是否发生?
是否选对工具?
退款金额是否来自可信字段?
工具返回的是成功还是处理中?
是否在超时后重复调用?
是否违反了人工审批要求?

这些问题属于流程级正确性,单独评估最终文本无法发现。

2. 工具执行错误

工具执行错误要按“请求是否到达”和“外部状态是否确定”分类:

情况 是否到达工具 外部状态 动作
参数校验失败 否或未执行 未变化 修正参数或终止
连接建立失败 未知 未知 查询或人工确认
明确 4xx 业务拒绝 已到达 未变化 不重试
明确 5xx 且工具保证未执行 未执行 未变化 可有限重试
响应超时 可能已执行 未知 禁止盲目重试
成功响应但本地落库失败 已执行 已变化 通过查询和事件补齐本地状态

“5xx 可重试”也不能仅凭状态码判断。必须知道工具的执行语义:它是否可能在返回 5xx 前已经提交事务?如果不清楚,就应视为 UNKNOWN


九、故障期间的用户回复也是系统行为

Agent 的最终回复不是装饰层,它可能改变用户下一步行为,进而扩大事故。

1. 三种状态必须对应三种表述

业务状态 正确表达
明确成功 “退款已完成,金额为 100 元。”
明确失败 “退款未完成,原因是订单已超过可退款期限。”
状态未知或处理中 “退款请求已提交,系统正在确认最终状态,请勿重复提交。”

禁止把技术异常直接映射成业务结论:

try:
    result = call_refund()
    return "退款成功"
except TimeoutError:
    return "退款失败"

更安全的处理是:

try:
    result = call_refund(idempotency_key=key)
except TimeoutError:
    mark_operation_unknown(key)
    enqueue_reconciliation(key)
    return "请求已提交,正在确认状态,请勿重复操作。"

这里的关键不是措辞,而是用户被告知的动作必须与系统状态一致。否则用户可能重复点击、重复付款、重复提交工单。

2. 流式输出的特殊风险

流式 Agent 可能已经把部分文本发送给用户,然后才发现工具失败。例如:

正在为你办理退款……
退款已完成。

如果第二句在工具确认前就被发送,后续即使系统发现失败,也无法真正撤回用户看到的文本。

因此,高风险操作的流式输出应设置提交点:

规划阶段:可以流式输出进度
执行阶段:不输出最终成功承诺
确认阶段:只有拿到最终状态后输出结论

对于不可逆操作,最终承诺必须晚于业务确认,而不能早于工具调用。


十、复盘:从“谁改错了”转向“哪个控制面没有生效”

1. 复盘的对象

复盘不是重复时间线,而是验证控制面是否存在并生效:

  • 输入校验是否阻止了非法请求?
  • Prompt 是否允许模型越过工具边界?
  • 工具是否执行了服务端权限校验?
  • 写操作是否具有幂等键?
  • 超时是否进入 UNKNOWN
  • Trace 是否包含关键模型回合和工具调用?
  • 版本是否可以精确定位和回滚?
  • 灰度是否真正限制了影响范围?
  • 补偿是否可以自动执行?
  • 用户是否收到准确的中间状态?

2. 从 Trace 形成可重复评测

一次事故中,代表性 Trace 可以先用于定位单条故障路径;问题稳定后,再把脱敏后的输入、预期行为和故障轨迹整理为数据集,运行重复评测。

OpenAI 的评测文档建议:调试阶段先从单条 Trace 和 Trace grading 开始;当“什么是正确行为”已经明确后,再转向数据集和评测运行,以便重复比较 Prompt、路由和工作流变化。(developers.openai.com)

事故用例不应只保存最终文本,而应保存结构化断言:

{
  "input": "请退还订单 1001 的 100 元",
  "assertions": [
    "identity_verification_before_refund",
    "refund_amount_leq_refundable_amount",
    "no_retry_after_unknown_without_reconciliation",
    "final_success_claim_requires_payment_success",
    "trace_contains_refund_operation_id"
  ]
}

这样才能验证“修复没有引入另一种错误”。例如,修复重复退款后,可能导致所有退款都转人工;最终错误率下降了,但自动化目标也被破坏了。

3. 复盘指标应覆盖结果和过程

建议至少区分以下指标:

结果指标

  • 业务成功率;
  • 错误操作率;
  • 重复操作率;
  • 用户受影响数;
  • 补偿完成率;
  • 未确认操作积压量。

过程指标

  • 工具选择正确率;
  • 参数校验拦截率;
  • UNKNOWN 状态转最终状态的时间;
  • 自动重试次数;
  • 人工接管比例;
  • Trace 完整率;
  • 版本可定位率;
  • 从告警到止损的时间;
  • 从止损到恢复的时间。

单看“Agent 请求成功率”会掩盖最危险的情况:请求都返回 HTTP 200,但模型持续调用错误工具。

4. 根因分析的反例

“模型太笨”不是可执行的根因,因为它不能直接导出修复动作。

更有用的根因表述是:

Prompt v42 将“退款进度查询”和“创建退款”描述为相邻能力;
工具层未执行二次意图校验;
写操作缺少幂等键;
超时被映射为失败并触发自动重试;
因此在 2026 年 9 月 1 日 10:00 至 10:08 期间,部分订单产生重复退款。

这个根因包含:

  • 触发变化;
  • 失效的控制;
  • 故障传播路径;
  • 时间范围;
  • 可验证的修复方向。

十一、一个可落地的应急作业流程

阶段一:识别

通过以下信号发现事故:

  • 业务不变量被违反;
  • 高风险工具调用率异常;
  • UNKNOWN 操作积压;
  • 用户投诉与 Trace 聚集;
  • 模型版本、Prompt 版本发布后指标突变;
  • Trace grading 发现工具选择或 Guardrail 回归。

阶段二:分类

先确定:

影响对象:文本 / 权限 / 数据 / 业务副作用 / 合规
状态:失败 / 成功 / 未知 / 部分成功
范围:单租户 / 单版本 / 单工具 / 全局
可逆性:可回滚 / 可补偿 / 不可逆

若无法确认是否成功,应宁可标记为 UNKNOWN,不要强行归类为失败。

阶段三:止损

按风险从高到低执行:

  1. 关闭或只读化高风险工具;
  2. 停止候选版本灰度;
  3. 禁止未知状态操作的自动重试;
  4. 降低并发和入口流量;
  5. 将请求转入查询、人工审核或处理中状态。

阶段四:保存证据

固定保存:

request_id
trace_id
run_id
span_id
model_turn_id
tool_call_id
idempotency_key
external_operation_id
完整版本指纹
错误分类
操作状态
时间线
用户可见输出

同时检查敏感信息保护和 Trace 导出是否完成。

阶段五:回滚或补偿

  • 版本问题:回滚完整版本组合;
  • 状态未知:先查询外部状态;
  • 已成功但业务错误:执行幂等补偿;
  • 不可逆副作用:创建人工任务、通知用户并记录审计;
  • 失败且确认未执行:按安全条件重试。

阶段六:验证恢复

恢复不能只看服务健康检查。至少要验证:

  • 新请求不再进入错误版本;
  • 高风险工具恢复后仍有服务端校验;
  • 未知状态积压正在下降;
  • 业务不变量重新成立;
  • 用户回复与实际状态一致;
  • 代表性 Trace 和事故评测用例通过;
  • 没有重复补偿或重复执行。

十二、常见误解与边界

误解一:模型没有抛异常,所以 Agent 没故障

模型生成通常可以成功返回,但内容可能违反业务规则。正确性必须由业务校验、工具约束和流程断言共同定义。

误解二:超时就重试

超时只说明调用方没有在期限内获得响应,不说明外部操作没有发生。对写操作,超时首先意味着状态不确定。

误解三:回滚版本就能恢复一切

版本回滚只能阻止新的错误行为,不能撤销已经发生的支付、发券、消息发送或第三方写入。

误解四:Trace 等于审计日志

Trace 适合观察工作流和诊断模型、工具、Guardrail、Handoff 等过程;业务审计还需要不可抵赖的操作事件、审批记录、外部操作 ID 和状态变更记录。

误解五:记录全部 Prompt 和工具输入就最有利于排查

完整记录有助于复现,但可能造成敏感数据泄露,也可能违反数据保留策略。应按字段分级、脱敏和限制访问。Agents SDK 文档明确提示,模型生成和函数调用 Span 可能包含敏感数据。(openai.github.io)

误解六:把所有失败都交给人工就是安全

过度人工化会造成积压和隐性超时;更重要的是,它不能替代幂等、状态查询和补偿设计。人工接管应是状态机中的明确分支,而不是异常处理的垃圾桶。


Agent 故障应急的核心不是让模型“永不犯错”,而是让错误在进入业务系统前可拦截,在产生副作用后可确认,在无法恢复时可补偿,在事后能够重放、评测和修复。

一个成熟的 Agent 运行链路应满足:

可分类可止损可取证可回滚可补偿可复盘\text{可分类} \land \text{可止损} \land \text{可取证} \land \text{可回滚} \lor \text{可补偿} \land \text{可复盘}

其中最重要的工程判断是:不要把未知状态伪装成失败,不要把版本回滚伪装成业务回滚,也不要把 Trace 记录伪装成业务审计。 只有把模型行为、工具调用、外部状态和用户承诺放进同一个可关联、可验证的状态机里,Agent 才具备真正的生产可恢复性。


系列导航与关联阅读

官方资料

本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。