OpenAI Agents SDK 工程笔记:AgentsException 族、失败分类与生产恢复禁区

千笔-AIWritePaper · https://www.aiwritepaper.com

线上 Agent 挂掉时,最危险的操作是「反正再跑一遍」。OpenAI Agents SDK 把运行失败收进 AgentsException 族:回合耗尽、模型行为异常、调用方误配、模型超时、工具超时、护栏触发等。它们有的挂着 run_data 快照,有的根本不在这个族里(例如缺 OPENAI_API_KEY 时的 openai.OpenAIError)。能不能重试,取决于异常类型 + 是否幂等 + 是否已经产生写副作用,不取决于值班同学的耐心。

本文在 openai-agents 0.23.1 下用脚本化假模型离线实跑:故意触发回合耗尽、未知工具、坏结构化 JSON、误配 call_model_input_filter、缺密钥,以及超时后盲目整段重试导致 charge 副作用翻倍。全部结果进 _w/agents-exception-smoke/ 的 CSV 与 smoke_output.txt。

图:上排为 AgentsException 族与恢复分类;中部为 Runner.run 成功/抛错路径与 run_data 字段;底部为 F1至F5 实测禁区。

目标说明

读完你应能独立完成五件事。

  1. 按类型把失败分进三桶:条件可重试、不可重试、必须人工介入;并把缺密钥等基础设施错误单独隔离。
  2. 在 except AgentsException 里读取 exc.run_data(若有),把 raw_responses、new_items、last_agent 写入结构化日志。
  3. 用 max_turns 故意打爆一轮,确认抛出 MaxTurnsExceeded 且 run_data.raw_responses 条数等于预算。
  4. 对写工具路径禁用「整段 Runner.run 重试」;超时只允许在幂等读路径或带幂等键的补偿路径重试。
  5. 把 5 条失败样本写进禁区表和告警分级:盲目重试翻倍、原样重跑耗尽、吞异常伪造成功、缺密钥空转、坏工具 JSON 伪健康。

适用与边界

适合:已经在跑 Runner.run / run_sync / run_streamed,需要统一异常出口、日志字段与告警;要把「再试一次」写成可执行策略,写进口头习惯不够。

不讲:tool 循环与单工具超时调参细节、as_tool、MCP 接线、guardrails 设计、lifecycle hooks、ModelSettings、RunContext、SQLiteSession 落盘(见 10-02 A)。护栏触发类异常只在分类表里占位,本文不展开 tripwire 业务规则。

失败含义:假模型按脚本返回调用,用来固定触发路径;「有没有挂上 run_data」「缺密钥是否属于 AgentsException」「坏工具 JSON 会不会默默回灌错误串」由 SDK 与 Runner 决定。ModelTimeoutError 的分类用例直接构造异常对象验证分桶;真实超时依赖 ModelSettings.timeout,以你安装版本为准。

机制:异常族、返回路径、日志字段

类型 典型触发 恢复分桶 告警建议
MaxTurnsExceeded max_turns 用尽 人工介入:加预算或拆任务 P1/P2,带 run_data
ModelBehaviorError 未知工具、坏结构化 JSON、非法工具载荷 不可重试:修工具表或输出约束 P1,禁止自动重跑
UserError 调用方误配(如 filter 返回类型错) 不可重试:修代码/配置 P1,走发布门禁
ModelTimeoutError 模型调用超过 timeout 仅幂等可读路径可重试 P2,写路径要幂等键
ToolTimeoutError 工具 timeout_behavior="raise_exception" 视工具是否幂等 P2
护栏 tripwire 诸类 输入/输出/工具护栏触发 默认人工或降级策略 按业务定级
openai.OpenAIError 等 缺密钥、凭据、网络基建 不在 AgentsException 族;禁盲目重试 P0 配置/密钥

Runner.run 成功时返回 RunResult(读 final_output)。失败时抛上述异常;Runner 会尽量把当时的 input、new_items、raw_responses、last_agent、护栏结果塞进 exc.run_data(RunErrorDetails)。日志至少落四类字段:exc_type、exc_message、run_id/trace_id、run_data 摘要(响应条数、最后 agent 名、new_items 类型直方图)。告警分级建议:配置/密钥 → P0;Behavior/User → P1 且禁止自动重跑;MaxTurns → P1/P2 并附预算;Timeout → P2,由幂等标记决定是否进入重试队列。

可选的 error_handlers(如 "max_turns" / "model_refusal" / "invalid_final_output")可以把部分错误收成受控 final_output,它替代抛错,不会替你撤销已经执行过的写工具。把它当成产品文案降级可以,当成「副作用自动回滚」不行。

python 复制代码
from agents import Agent, Runner, MaxTurnsExceeded, ModelBehaviorError, UserError
from agents import ModelTimeoutError, AgentsException

def classify(exc: BaseException) -> str:
    if isinstance(exc, MaxTurnsExceeded):
        return "human_intervene"
    if isinstance(exc, ModelTimeoutError):
        return "retryable_if_idempotent"
    if isinstance(exc, (ModelBehaviorError, UserError)):
        return "non_retryable"
    if isinstance(exc, AgentsException):
        return "agents_other_no_blind_retry"
    return "infra_config_no_retry"

try:
    result = await Runner.run(agent, user_input, max_turns=8)
except AgentsException as exc:
    log.exception("agent_failed", extra={
        "exc_type": type(exc).__name__,
        "recover": classify(exc),
        "raw_n": len(exc.run_data.raw_responses) if exc.run_data else 0,
        "agent": exc.run_data.last_agent.name if exc.run_data else None,
    })
    raise

离线 smoke:6 条通过 + 5 条失败

text 复制代码
P1  PASS 回合耗尽 MaxTurnsExceeded+run_data -> raw_responses=3 new_items=6 last_agent=budget side_effects=3
P2  PASS 未知工具名触发 ModelBehaviorError -> Tool no_such_tool not found
P3  PASS output_type 解析失败 ModelBehaviorError -> Invalid JSON when parsing model output
P4  PASS call_model_input_filter 误配 UserError -> must return a ModelInputData instance
P5  PASS 缺 OPENAI_API_KEY 落在 infra 层 -> openai.OpenAIError agents_exc=False
P6  PASS ModelTimeoutError 分类为条件可重试 -> timeout_seconds=1.5
F1  FAIL 超时后盲目整段重试导致写副作用翻倍 -> charge:99 × 2
F2  FAIL 回合耗尽后原样重跑仍耗尽 -> second MaxTurnsExceeded
F3  FAIL 吞掉 ModelBehaviorError 伪造成功 -> returned_ok=True
F4  FAIL 缺密钥盲目重试空转 -> OpenAIError × 3
F5  FAIL 坏工具 JSON 未抛异常仅回灌错误串 -> raised=False saw_tool_error_string=True
CSV rows: 11

逐条读法。

  • P1:max_turns=3 时模型每轮都调 charge,抛 MaxTurnsExceeded;run_data.raw_responses 恰好 3,且副作用日志已有 3 条。预算耗尽前写工具可能已经发生。
  • P2:模型点名不存在的工具,默认行为抛 ModelBehaviorError(可改为 tool_not_found_behavior="return_error_to_model" 回灌,那是另一条产品策略)。
  • P3:output_type 为 Pydantic 模型时,助手只回非 JSON 文本,抛 ModelBehaviorError: Invalid JSON when parsing model output。
  • P4:call_model_input_filter 返回了普通字符串,抛 UserError。这是调用方代码病,重试无意义。
  • P5:不设密钥走默认真模型路径,得到 openai.OpenAIError,它落在 AgentsException 族外。只写 except AgentsException 会漏掉它。
  • P6:ModelTimeoutError(1.5) 进入「条件可重试」桶;是否真重试还要看路径幂等。
  • F1:工具已 charge:99,随后模拟超时,调用方把整段 Runner.run 再跑一遍,副作用变成两条。这是写路径盲目重试的标准事故。
  • F2:第一次 max_turns=2 耗尽后,不改预算、不拆任务,原样再跑,第二次仍然 MaxTurnsExceeded。
  • F3:except ModelBehaviorError 后直接 return {"ok": True},监控显示成功,用户拿到空结果。
  • F4:缺密钥连打三次,三次同一基建错误,告警风暴与空耗。
  • F5:工具参数是 {not-json 时,SDK 默认把错误串写进 function_call_output 让模型继续,整次 run 成功返回。没有异常不等于工具健康。

生产禁区表

编号 禁区 实测证据 替代做法
D1 超时后整段重跑含写工具的 run F1:charge:99 × 2 写工具带幂等键;超时只重试未确认的读步骤
D2 MaxTurns 后原样同预算重跑 F2:第二次仍耗尽 升预算、拆子任务,或走 error_handlers["max_turns"] 给受控文案
D3 吞掉 Behavior/User 当成功 F3:ok=True 必须记账并失败返回;CI 禁止裸 except
D4 缺密钥/凭据当可重试 F4:空转 × 3 启动期校验密钥;该类进 P0,重试队列直接拒绝
D5 「没抛异常」当成工具成功 F5:错误串回灌 扫描 function_call_output 错误模式;指标单独计数
D6 只 catch AgentsException P5:OpenAIError 漏网 外层再接基建异常,字段进同一日志模式

告警与重试对照

分桶 谁进 自动动作 人工动作
条件可重试 幂等读上的 ModelTimeoutError 有限次退避重试 超限后升级
不可重试 ModelBehaviorError / UserError 不重试,记 P1 修工具表、输出约束或配置
人工介入 MaxTurnsExceeded 可选受控降级文案 加预算或拆任务,并看 run_data
基建禁重试 缺密钥等非 Agents 异常 拒绝入队 修密钥与网络

最小复现:断言「写副作用不会因超时翻倍」

smoke 里用内存列表当账本。你的项目里换成数据库幂等表即可。

python 复制代码
SIDE = []

@function_tool
def charge(amount: int) -> str:
    SIDE.append(f"charge:{amount}")
    return f"charged:{amount}"

# 业务层:同一 payment_id 只允许成功落账一次
async def run_payment(payment_id: str):
    if already_charged(payment_id):
        return "duplicate-ignored"
    await Runner.run(pay_agent, f"pay {payment_id}", max_turns=5)
    mark_charged(payment_id)

CI 断言:模拟「成功扣费 + 随后超时 + 调用方重试」后,SIDE.count("charge:99") == 1(或账本行数唯一)。谁把整段 run 当重试单元,谁就红。

常见误判

  • 「catch 住了 AgentsException 就等于覆盖所有运行失败」:P5 证明缺密钥走另一条继承树。
  • 「run 返回了 final_output 说明工具都成功」:F5 证明坏 JSON 可以只回灌错误串仍算跑完。
  • 「error_handlers 接住 max_turns 就等于没副作用」:P1 在抛错前已经执行了 3 次 charge;handler 只改返回,不回滚。
  • 「超时都可以重试」:只有幂等读或带幂等键的写才可以;F1 是反例。

与已写主题的咬合

相邻主题 关系 一句话
tool 循环 / 超时专文 单工具超时策略 本篇管异常出口与是否允许整段重跑
guardrails 专文 tripwire 如何设计 本篇只把 tripwire 放进分桶表
ModelSettings timeout 等旋钮来源 本篇消费超时异常的恢复边界
SQLiteSession(10-02 A) 失败时历史是否仍在 异常与 session 正交;run_data 不替代会话库

日志字段最小集

生产里建议每条失败日志固定带这些键,方便和 trace 对上。

字段 来源 用途
exc_type type(exc).__name__ 分桶与看板
recover 上文 classify(exc) 决定是否入重试队列
message str(exc) 截断 值班可读
raw_n len(run_data.raw_responses) 对照 max_turns
item_hist new_items 的 type 直方图 看失败前是否已调工具
last_agent run_data.last_agent.name 多 agent 时定位
side_effect_hint 业务侧幂等表查询 判断能否安全重试
trace_id RunConfig / 追踪系统 串联模型与工具跨度

缺 side_effect_hint 时,超时重试决策会退化成猜。P1 已经证明回合耗尽前 charge 可以发生三次;你若只看异常类型、不看账本,就会把「已经扣过」的请求再次送进 Runner。

与 error_handlers 的边界

SDK 允许为 "max_turns"、"model_refusal"、"invalid_final_output" 注册 handler,返回受控 final_output。这在产品层很有用,例如告诉用户「本轮步数不够,请缩小范围」。工程上要同时记住三件事。

  1. handler 成功后,调用方看到的是正常 RunResult,监控若只盯异常率会漏报。
  2. include_in_history 默认可能把降级文案写入会话;设 False 可避免污染下一轮上下文。
  3. 已执行的写工具不会因为 handler 而回滚。P1 的三次 charge 在抛 MaxTurnsExceeded 之前已经进了副作用日志。

因此 handler 属于对外文案与流程降级,不属于恢复禁区的豁免条款。禁区表里的 D1/D2 仍然成立。

验收清单

  • 所有 Runner.run 调用点有统一 classify + 结构化日志(含 run_data 摘要)。
  • 写工具具备幂等键;超时重试测试断言副作用不翻倍。
  • MaxTurnsExceeded 不进入无脑重试队列。
  • ModelBehaviorError / UserError 禁止自动重跑,CI 有裸 except 检查。
  • 启动期校验 API 密钥;缺密钥进 P0 而非重试。
  • CI 跑 _w/agents-exception-smoke/smoke.py,P 系列 PASS,F 系列保持可复现。

当天 30 分钟落地

  1. 5 分钟:搜仓库里所有 Runner.run,列出 except 范围是否覆盖非 Agents 异常。
  2. 10 分钟:接入上文 classify,日志加上 exc_type / recover / raw_n。
  3. 5 分钟:给支付/写库类工具补幂等键,加一条「超时重试不翻倍」测试。
  4. 5 分钟:MaxTurns 与 BehaviorError 的告警从「自动重跑」改成「工单」。
  5. 5 分钟:把 agents-exception-smoke.csv 存进仓库,升级 SDK 时复跑对照。

配置对照:四个问题定恢复策略

问题 是 否
失败路径上是否已执行写工具? 禁止整段重跑;查幂等表 才可考虑有限次退避
异常是否为 Behavior / User? 修代码与工具表 再看超时或 MaxTurns
是否 MaxTurnsExceeded? 人工改预算或拆任务 不要用同预算重跑碰运气
是否族外基建错误? P0 修密钥与网络 不要丢进 Agents 重试队列

这四问可以直接做成网关中间件的分支。谁跳过第一问,谁就会复现 F1。

踩坑

  • 把「再跑一遍」写进 on-call 手册的第一步:F1/F2/F4 都会被手册合法化。
  • 只在网关层 catch Exception 并返回 200:F3 的假阳性会直接进用户报表。
  • 看见 tool output 里有英文 error 仍展示「已完成」:F5 的静默失败最常见于演示环境。
  • 用升 max_turns 掩盖未知工具或坏 JSON:那是把 BehaviorError 熬成 MaxTurns,run_data 更难看。
  • 假设 error_handlers 会撤销工具副作用:SDK 文档写明它不重放、不回滚副作用。

总结

AgentsException 族给出的是可观测的失败类型 。它本身不附带「自动恢复许可证」。Runner.run 成功走 RunResult,失败走带(或不带)run_data 的异常;缺密钥等基建错误还在族外。生产能不能扛住,取决于分桶、幂等与告警:超时后整段重跑、耗尽后原样重跑、吞掉 BehaviorError、缺密钥空转、把错误串回灌当成健康,都会在账本或监控里留下可复现伤口。把 5 条失败写进禁区表,升级 SDK 时复跑同一份 smoke。本文实跑版本为 openai-agents 0.23.1,字段名以你安装的版本为准;若 run_data 或 error_handlers 语义有变,先以官方 Running agents / Exceptions 文档与本目录 CSV 对照。

写作与实验流程可用千笔-AIWritePaper 整理实验记录与禁区表,站点见文首链接。

text 复制代码
P1  PASS 回合耗尽 MaxTurnsExceeded+run_data -> msg='Max turns (3) exceeded' run_data=True raw_responses=3 new_items=6 last_agent=budget side_effects=3
P2  PASS 未知工具名触发 ModelBehaviorError -> type=ModelBehaviorError msg='Tool no_such_tool not found in agent tools' class=non_retryable
P3  PASS output_type 解析失败 ModelBehaviorError -> type=ModelBehaviorError msg='Invalid JSON when parsing model output'
P4  PASS call_model_input_filter 误配 UserError -> type=UserError msg='call_model_input_filter must return a ModelInputData instance'
P5  PASS 缺 OPENAI_API_KEY 落在 infra 层 -> type=openai.OpenAIError agents_exc=False class=infra_config_no_retry
相关推荐
AIGC大时代9 小时前
OpenAI Agents SDK 工程笔记:WebSearchTool 与 FileSearchTool 托管工具边界与生产禁区
agents sdk·websearchtool·filesearchtool·托管检索
AIGC大时代2 天前
OpenAI Agents SDK 工程笔记:tool_use_behavior 与 is_enabled 动态工具开关生产禁区
agents sdk·生产禁区·is_enabled
AIGC大时代3 天前
OpenAI Agents SDK 工程笔记:LocalShellTool 与 ApplyPatchTool 执行面边界与生产禁区
agents sdk·生产禁区·localshelltool·applypatchtool
爬点儿啥10 个月前
[Ai Agent] 12 Swarm 与 Agents SDK —— 去中心化的多智能体协作
去中心化·区块链·swarm·langgraph·agents sdk·handoff
缘友一世1 年前
Agents-SDK智能体开发[4]之集成MCP入门
llm·mcp·agents sdk