出错、限流、变慢,怎么定位?
本课一问:agent 跑慢了、报错了、被限流了,先看哪里?谁来重试、隔多久重试?
这一课先不碰:流式拼装本身(第 2 课);上下文太长导致的变慢(A5);首 token 时间的机制(LLM 课 C13 第 5 课)。
从已有出发
第 2 课在 T5 第 7 轮的时间轴上看到一段 886 ms 的静默。这一课把整次运行的 9 轮一起摊开。
先自己设计:看总耗时;出错就原地重试
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
最直接的监控是一个数:这次运行花了多久。最直接的容错是一个循环:请求失败就睡一会儿再发,不管失败的原因是网络断了、服务端 5xx,还是 429 限流。
两件事都要拆开:时间按 span 拆到每一次模型调用、每一次工具执行、每一帧;重试按失败的层次拆——传输层只管「这一次 HTTP 没成」,流断了由流这一层续,限流要看服务端给的重置时间,交给更上层决定。
dropped_tool_call 停)。只看这个数,不知道慢在哪、错在哪。拆成 span:9 次模型调用合计 6,617 ms,6 次工具执行合计只有 34 ms;第 8、9 轮首帧要 1,029 / 978 ms,是前 6 轮(323–519 ms)的两倍多。「出错就原地重试」也会出事:按 CoreCoder 传输层的写法(llm.py:262-278),429 在 1 s、2 s 后重发;项目测试桩返回的 Retry-After 是 7 s——两次重试都落在限流窗口里(构造输入 · 真算法)。 拆开时间:一次运行的 span 瀑布
span 由项目 trace 现算(T5,Qwen/Qwen3-8B-MLX-4bit · 本机 mlx_lm.server · 2026-09-26);属性名按 OTel GenAI 约定(Development,仍会变)。耗时受本机同时运行的其他负载影响,只能粗看;token 与轮数可比。
试三件事:① 点第 7 轮的 chat span:首帧 134 ms 就到(先流出一段正文),之后有一段红色斜纹——帧与帧之间的长静默,模型在写一个工具调用,服务端解析失败把它丢了;② 看第 8、9 轮:没有正文,首帧前的斜纹占了整根条,因为本机服务要等调用写完才发帧——而那个调用又被丢了;③ 找紫色菱形:harness 两次注入提醒「你的调用解析失败被丢掉了」,两次都没救回来(A1 第 3 课的「提醒重发」)。
span 的名字与属性按 OpenTelemetry 的 GenAI 语义约定写:推理 span 叫 chat default_model,工具 span 叫 execute_tool read_file,属性里的 gen_ai.response.time_to_first_chunk 就是首帧时间(秒)。这套约定的状态仍是 Development,属性名还会变 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条官方一手:仓库 README 与各文档头部都标 Status: Development,没有 release。本课项目没有接 OTel SDK,span 是由 trace 现算的。 出处OTel GenAI semantic conventions(2026-09-26 读取) ↗需联网 。注意它的 gen_ai.usage.input_tokens 要求包含缓存——和第 1 课看到的 Anthropic 口径相反。
拆开重试:三层,三种原因
| 层 | Codex(pin 8cabf5a) | CoreCoder v0.7.0 | 本课项目 miniagent |
|---|---|---|---|
| 传输层:这一次 HTTP 没成 | 最多重试 4 次,200 ms 起步指数退避 × 0.9–1.1 抖动;重试 5xx 与断线,429 不重试(retry_429: false) | 最多 3 次,等 1 s、2 s;429 也重试 | 不重试;429 抛 RateLimitError(带 retry_after) |
| 流这一层:流断在半路 | 优先用服务端给的延迟,没有就退避;重试用尽后从 WebSocket 降级到 HTTPS | 流中途断了整条重发(此时还没执行任何工具) | 不处理 |
| 上层:限流、额度 | 429 交给 turn 这一层按额度处理 | —— | loop 以 model_error 停,由调用方决定何时再跑 |
OpenCode 把重试次数原样交给 AI SDK:maxRetries: input.retries ?? 0(session/llm.ts:323),不传就是 0;更上层怎么处理,本课没有往下追。
Codex 需要知道「限流后多久能再试」时,并没有一个结构化字段可读:它用一条正则,从错误消息的文字里找「try again in 1.5s」这样的片段(下面两段)。能工作,但服务端改一下措辞就失效。好的工程常常是这样:在拿不到结构化信号的地方,老实地写一个启发式,并把它限定在一个小函数里。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 慢了知道慢在哪一步;错了知道该由哪一层、隔多久重试 |
| ② 为什么可能有效? | 不同失败的恢复时间差几个数量级:断线是毫秒级,限流按服务端给的窗口;放在同一层、同一个退避里重试,必然有一类被处理错 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条设计动机的归纳。Codex 的分层与 retry_429: false 是源码直证(retry.rs:9-49、lib.rs:263-270),但源码没写为什么这样选。 出处待补 |
| ③ 实验上呢? | T5 真实 trace:模型调用 6,617 ms、工具 34 ms;两次首帧 1 秒左右的轮次都对应被丢弃的调用 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测:单次运行、本机服务、关思考;本机有其他任务同时占用,耗时只能粗看。429 的行为只在测试桩上测过(test_429_is_not_retried_in_the_transport),没有真实限流录制。 出处待补 |
演进地图:你在这里
这一课属于演进图里「流式」那一步:流让你看得见每一帧,也就看得见时间花在哪。
- 起点 写死一家的线格式CoreCoder(OpenAI 兼容 + litellm 兜底)、本课项目 miniagent(标准库 urllib)解决了最短的路:CoreCoder 的
llm.py净 259 行,换 provider 只改 base URL 与 key。卡在同一段带工具调用的对话,换成 Anthropic Messages,9 个语义位置里 9 个换了路径或类型(参数从字符串变成对象,5 条消息变 3 条);Codex 干脆不认 Chat:配置wire_api = "chat"直接报错。 - 第一步 加一层抽象,或只留一种用满它Pi 自建 pi-ai · OpenCode 用 Vercel AI SDK · Codex 只留 Responses解决了Pi 把 10 种协议与 40 个 provider 做成正交的两维;OpenCode 挂了 19 个
@ai-sdk/*依赖;Codex 的WireApi只剩 1 个变体。卡在抽象层得替每一家补差异:OpenCode 逐 provider 改写消息的transform.ts有 1,909 行,还给 8 个@ai-sdk包打了补丁;而且抽象挡不住流式与缓存的细节。 - Anthropic 流式文档 · OpenAI function-calling 流式指南 · Codex 只收 output_item.done解决了开思考的一轮,第一帧 125.9 ms 就到;整包等回复的话,正文要等到 6,631 ms、工具调用 6,798 ms。卡在流式只让回复来得早,不让它便宜:T5 的 9 轮每轮整份重发,输入累计 15,921 token,是最后一轮 2,389 的 6.7 倍。
- 第三步 缓存感知的账单 + 稳定前缀Anthropic / OpenAI prompt caching · Codex prompt_cache_key · OpenCode applyCaching解决了按 usage 四段计价:T5 在 Claude Sonnet 5 价目下 $0.0140;CoreCoder 按全价估 $0.0371,是它的 2.64 倍。卡在缓存只省输入:开思考的 pd-v2-unlimited-s3 账单里输出占 94%;前缀里一处变、后面全失效——T10 刷新组第 7 轮 system 重建,命中从 2,072 掉到 294。(→ A9:能跑、能算账了,怎么知道它好不好?)
数字口径:pin 源码计数(Codex 8cabf5a、OpenCode v1.18.32、Pi 31b513e3、CoreCoder v0.7.0)、项目录制 T5 / T10 与逐轮 usage(本机 Qwen3-8B,token 按它的 tokenizer 数)、官方价格页快照 2026-09-26;由 a08-model-access.json 构建时现算。
一句话
先停在这里:回主线第 3 课,看每一帧 usage 怎么变成钱。
再深一层:给 miniagent 的 trace 加两个字段:每轮首帧时间、帧间最长静默;再写一条规则——首帧超过前几轮中位数两倍、又没有调用到达时,在 trace 里打「疑似被丢弃的调用」标签(A9 的失败归因会用到)。
下一个缺口
回主线:每轮都重发全部历史,钱花在哪?缓存能省多少? → 第 3 课
你可能会问
- 「本机服务会返回 429 吗?」 这次录制里没有出现。项目的 429 行为是用本机 HTTP 测试桩测的:桩返回 429 与
Retry-After: 7,流式与非流式各只收到 1 次请求。 - 「Langfuse 这类可观测性平台要不要接?」 Langfuse 是 MIT、可以自托管;本课不列它的云端控制台(需要登录)。本课的瀑布图说明:trace 里只要有每轮起止、每帧时刻、工具起止,就能画出同样的东西。
- 「流断了能从断点续吗?」 Anthropic 文档给了续写办法,并写明工具调用块与思考块不能部分恢复;所以流断在一个工具调用中间时,只能整条重来。