第 11 章 API 韧性即生命 · Agent架构实操十一 开始测验

API 韧性即生命:决定 Agent 商业化成败的隐藏细节

凌晨两点跑了三小时的任务挂了——529 overloaded,没重试没恢复直接崩。问题不是 API 会不会出错,而是 Agent 能否在明确边界内恢复。
⏱ 约 15 分钟 🔧 三种故障三条路 ⏱ 退避+fallback+deadline +recovery

🎯 学习目标

学完能答
  • 三种故障(输出截断/输入过长/瞬态)的信号和恢复动作分别是什么?
  • 为什么禁止匹配异常文本(如 "429" in str)?改用什么?
  • finishReason==="length" 等于输入过长吗?怎么恢复?
  • 529 连续几次切 fallback?429 的 Retry-After 怎么处理?
  • 恢复层为什么不重新进入外层 Loop?一次重试算几个 turn?
  • 哪些错误原样失败不重试?为什么?

🧠 核心概念

点击展开。
1 · 三种故障,三条路
故障供应商无关信号恢复动作
输出被截断finishReason==="length"首次提升输出预算(8K→64K);之后在请求窗口内续写
输入上下文过长ModelPromptTooLongError保留当前 system prompt,响应式压缩请求快照一次
临时故障ModelRateLimitError/ModelOverloadedError遵守 Retry-After 或指数退避;连续3次529后切 fallback

三条路径统一在 RecoveryManager.complete()。恢复层不重新进入外层 Loop,一次 API 重试不被计成新 turn。

2 · 先归一化错误,禁止匹配异常文本

原文用 "429" in str(exc)"prompt" in str(exc) 判断,会把业务错误/代理页文本/程序异常误判成可重试。适配器只读 OpenAI SDK 稳定结构:status/error/headers/requestID。

分类规则显式:429→ModelRateLimitError(带 Retry-After);529→ModelOverloadedError;400 且 error.code 在 PROMPT_TOO_LONG_CODES 集合→ModelPromptTooLongError。errorCode 只来自平坦 error.code/type 或最多一层 error.error.code/type,不读 message,不递归搜索,不猜字符串。未知 400/其他 5xx/连接错误/超时/程序错误原样传播。

HTTP 200 也不可信 normalizeResponse() 在交给恢复层前逐字段验证:恰好一个 choice、role 只能 assistant、finish_reason 白名单、content 是 string|null、tool_calls 逐项完整、usage 三个计数非负整数。不符立即抛 OpenAIResponseError。
3 · 路径一:输出截断只存在于请求窗口

finishReason==="length" 是正常响应,不是输入过长异常。首次:把输出预算 8000→64000,丢弃残缺回复,重试同一请求。仍截断才续写:中间片段和续写提示只追加到 requestMessages 局部快照,不进 canonical history。

requestMessages = [...requestMessages, reply.message, userMessage(CONTINUATION_PROMPT)]

续写提示:"Continue exactly where you left off. Do not repeat..."。默认最多3次。最终所有文本片段按原样拼接(不补空格),工具调用完整保留。只有合并后的 assistantMessage 能进 canonical history——避免孤儿工具调用。

4 · 路径二:输入过长只压缩一次

来自 ModelPromptTooLongError,不能拿 finishReason==="length" 代替。复用第8章 compactOnPromptTooLong():保留首条动态 system prompt,其后请求快照按完整消息组压缩一次。

不修改原数组:canonical history、原始 ModelRequest、调用方只读快照都不改。transcript 记录「被压缩前的请求快照」非 canonical 别名。同一逻辑请求只允许响应式压缩一次,第二次由 PromptTooLongRetryError 失败。压缩和摘要也在同一取消/总时限边界内。

5 · 路径三:429/529/Retry-After/fallback

无有效 Retry-After 时默认退避:0.5s→1s→2s→4s→8s→16s→32s→32s...,每次加 0..base*25% 抖动。最多10次瞬态尝试,封顶32秒。

429 的 Retry-After 优先级更高:接受非负秒数和带时区 RFC HTTP-date。空值/负数/NaN/Infinity/坏日期/无时区抛 InvalidRetryAfterError,不 sleep 不重试。等待会达或越过剩余时限直接抛 RecoveryDeadlineExceeded(不先睡到超时)。

529 维护连续计数:阈值默认3,第三次更新状态并完成本次退避,下一次(第4个请求)才显式携带 fallback model。429/输入过长/成功响应都打断「连续529」。

6 · 取消与总时限是恢复的一部分

10次退避在最大抖动下仅等待就可能超159秒,所以默认 turn 总时限300秒。CancellationToken/单调时钟/UTC时钟/sleeper/jitter 都可注入。恢复层在模型调用、退避sleep、响应式压缩外层统一竞争取消事件与剩余deadline,把 AbortSignal 传给 ModelClient 和 HistorySummarizer。

取消令牌契约:cancel() 幂等只触发一次;subscribe() 拒绝非函数;已取消时新订阅立即执行。所有耗尽路径都有 typed failure:RecoveryCancelledError/RecoveryDeadlineExceeded/RecoveryRetriesExhausted/InvalidRetryAfterError/PromptTooLongRetryError。

7 · 接入唯一 Loop,不复制 Loop

新增窄协议 ModelRequestExecutor:beginTurn() + complete(request)。run() 在 memory turn lifecycle 后调一次 beginTurn(),每个外层模型轮只调一次 complete()。

关键 内部重试不重新运行 UserPrompt Hook、history preparation、动态 Prompt、工具 schema 快照或 Stop Hook。P01-P10 不注入执行器仍直接调 raw ModelClient,content_filter 由 Loop 转显式失败。能力位与配置必须成对:给 P10 传 recoveryConfig 报错,P11 不传也报错(否则静默退化成裸模型);第 11 章起 .env 需四字段,OPENAI_FALLBACK_MODEL 必填。

summarizer 绑定 raw model 而非 RecoveryManager(否则递归:输入过长→压缩→摘要→恢复→再压缩)。memory selector/extractor、压缩摘要器、一次性 subagent 也用 raw model。

▶️ 交互演示:三条恢复路径

选一种故障,看 RecoveryManager 如何按对应路径恢复。注意:输出截断不污染 canonical,输入过长只压缩一次,529 连续3次才切 fallback。

🔑 一句话总结

API 韧性不是吞掉每个异常,而是让每条恢复路径有可验证的输入、状态和终止条件

QA 测验