第 十一 章 Agent 架构实操 深入学习 · 交互式 含 QA 测试

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

💡 三种故障三条路:截断→升级/续写,过长→响应式压缩,429/529→退避/fallback。
让同一个逻辑模型请求处理输出截断、输入过长和瞬态 API 故障;不污染正式历史、不重复执行 Hook、不隐藏失败。先归一只错误,禁止匹配异常文本。
本章进度
0%
1 本章要掌握的目标
  • 输出截断 finishReason=length:首次提升预算,之后在请求窗口内续写。
  • 输入上下文过长:保留当前 system prompt,响应式压缩请求快照一次。
  • 429 遵守 Retry-After 或指数退避;连续 3 次 529 切 fallback。
  • 取消与总时限是恢复的一部分;耗尽路径都有 typed failure。
  • 未知错误不重试,所有耗尽路径明确失败。
2 核心知识点
先归一化错误,禁止匹配异常文本

适配器只读取 OpenAI SDK 的稳定结构:status、error、headers、requestID。分类规则显式:429 → ModelRateLimitError;529 → ModelOverloadedError;400 + context_length_exceeded 等 → ModelPromptTooLongError。不读取 message 猜测。

if (status === 429) throw new ModelRateLimitError(...);
if (status === 529) throw new ModelOverloadedError(...);
if (status === 400 && PROMPT_TOO_LONG_CODES.has(errorCode))
  throw new ModelPromptTooLongError(...);

细节:code 统一转小写后再比对(大写也识别);判断用 instanceof APIError,伪造的 status=429 对象不识别;message 里的文案不作为契约。

HTTP 200 也是不可信边界

返回 200 不代表结构可信。normalizeResponse 逐字段验证:恰好一个 choice、role=assistant、finish_reason 只能落在白名单内——未知新枚举值明确失败而非宽容当 stop(否则截断回复会以完整答案身份进历史)。content=null 但有 refusal 时可当最终内容。content_filter 能通过适配器,但在 Loop 层显式失败(AgentRunError),不重试。

finish_reason 未知值  → OpenAIResponseError(不当成 stop)
content=null + refusal → content = 拒答文本
content_filter        → Loop 层 AgentRunError,不重试
路径一:输出截断只在请求窗口

首次 length:输出预算 8000→64000,丢弃残缺回复重试。仍截断才续写:把中间片段和 CONTINUATION_PROMPT 追加到 requestMessages 局部快照,默认最多 3 次,成功后合并为一个 assistantMessage。

if (reply.finishReason === "length" && !state.hasEscalated) {
  state.currentMaxTokens = escalatedMaxTokens;
  state.hasEscalated = true;
  continue;
}
// 仍截断 → requestMessages 局部快照 + 续写提示
路径二:输入过长只压缩请求快照一次

复用 CompactionManager.compactOnPromptTooLong()。保留首条 system prompt,其后的请求快照按完整消息组压缩。同一个逻辑请求只允许一次响应式压缩,第二次 PromptTooLongRetryError 明确失败。

const [leadingSystem, compactable] = splitLeadingSystem(requestMessages);
const outcome = await compaction.compactOnPromptTooLong(
  compactable, { retryCount: promptTooLongRetries }, signal);
requestMessages = [...leadingSystem, ...outcome.history];
路径三:429/529/Retry-After/fallback

退避基线 0.5→1→2→4…→32s,加 0..base*25% 抖动;默认最多 10 次。Retry-After 优先级更高;等待越限直接 RecoveryDeadlineExceeded。连续 3 次 529 后在下一个请求切 fallback model。

0.5s → 1s → 2s → 4s → 8s → 16s → 32s → 32s ...
连续 3 次 529 → state.currentModel = fallbackModel
取消与总时限

默认 turn 总时限 300s。CancellationToken、单调时钟、sleeper、jitter 可注入。模型调用、退避 sleep、压缩外层统一竞争取消与 deadline,把 AbortSignal 传给 ModelClient 和 Summarizer。

典型 typed failure:
- RecoveryCancelledError
- RecoveryDeadlineExceeded
- RecoveryRetriesExhausted
- InvalidRetryAfterError
- PromptTooLongRetryError
只包装主 Loop 请求;能力位与配置成对

恢复层只接管带工具的主 Loop 请求;记忆 selector/extractor、压缩摘要器、subagent 仍走裸模型——各自的会话所有权不同,共享 RecoveryState 会互相污染。能力位与配置必须成对:给 P10 传 recoveryConfig 报错,P11 不传也报错(否则静默退化成裸模型)。.env 从三字段变四字段:OPENAI_FALLBACK_MODEL 必填,file:// 协议的 baseUrl 被当没填。

主 Loop 请求 maxTokens = [8000, 64000]  ← 走恢复层
旁路请求 model = undefined             ← 裸模型
10 次重试等待 0.5+1+2+4+8+16+32… 总和正好解释 300s 默认时限
3 机制流程
1
model.complete

RecoveryManager 接管每个逻辑请求,内部重试不计入新 turn。

2
length

升级预算 → 仍截断则续写 → 成功合并或耗尽。

3
prompt-too-long

压缩请求快照一次 → 重试 → 二次失败。

4
429/529

Retry-After/退避/fallback;有效期与总时限约束。

5
成功

唯一合并后的 assistantMessage 进入 canonical history。

4 术语表
ModelRateLimitError携带 429;可带 Retry-After。
ModelOverloadedError携带 529;连续计数用于 fallback,beginTurn 清零(无跨 turn 熔断)。
ModelPromptTooLongError携带 400 + 明确的过长 code;只认平坦或一层嵌套的 error.code/type。
RecoveryConfig恢复参数;escalatedMaxTokens <= modelMaxTokens 在构造时校验。
CancellationToken幂等取消;subscribe 契约明确。
逻辑模型请求外层 Loop 眼里的一次模型调用;内部可能真实发出多次 HTTP 请求,重试不算新 turn。
恢复层边界不重试 500/503/连接错误(幂等性不明);不做流式恢复;不做主动 token 预算;无多 fallback 链。
5 QA 测试环节(自测题)
已完成 0 / 6 · 答对 0
Q1. finishReason === length 的恢复动作是?
Q2. 输入过长(prompt-too-long)会?
Q3. 连续多少次 529 后切换 fallback model?
Q4. 为什么不能匹配异常文本判断故障?
Q5. 最终成功的完成回复如何进入历史?
Q6. 未知 5xx / 连接错误 / 认证错误的处理是?
6 验证与实验
  • npm run test:ch11:28 个测试文件 / 258 个用例全部通过(本章 5 个测试文件共 43 个用例)。
  • 验证三类故障路径、退避、fallback、取消、总时限与耗尽;使用确定性模型、FakeClock、RecordingSleeper,不需要 API Key。
  • 从完整 Agent 观察:续写恢复后 canonical history 仍只有 user+assistant 两条、turns=1;content_filter 在 Loop 层显式失败。
  • 第 11 章起 .env 需要第四个字段 OPENAI_FALLBACK_MODEL(本地运行前置,见 .env.example)。