第 8 步 · 健壮化 —— 错误分层 / 重试退避 / 超时 / 日志

llm.ts 从"一把梭"升级成"能扛抖动"的网络组件

配套可运行项目:f:\myagent\task8 | 对比基准:task 7

8

健壮化:错误分层 · 重试退避 · 超时 · 日志

一句话:网络请求是整个 Agent 里最不可靠的环节,task 8 把它武装到牙齿。

为什么现在才做健壮化? 前 7 步我们一直在"让功能变多":多 Provider、REPL、历史、技能。但每一步里,请求模型的那段代码(llm.ts)都是"一把梭"——发出去、拿到就返回、拿不到就 process.exit(1) 直接退出。真实世界里网络会抖动、服务会限流、接口会超时,一个会"因网络抖动直接崩掉"的 Agent 是不合格的。心智转变:不是"请求会失败"的问题,而是"你准备怎么失败"的问题。这一步只做一件事:把失败处理做对。

8.1 · 本步改了什么

文件变化程度
src/llm.ts新增错误类(RetryableError / FatalError)、waitAndRetry 指数退避、makeRequest(30s 超时 + 最多 3 次重试 + [llm] 日志);askLLM 不再 process.exit(1),错误上抛。返回结构 LLMResponse 不变(task 7 已有)核心改造
src/agent.ts仅一行:map 回调里 (t: {...}) 改成 (t) => (t as {...})(strict 下正确的断言写法)顺手适配
src/main.ts 等其余 7 个文件沿用 task 7,一字未改无变化

8.2 · 核心改造:llm.ts(升级前 vs 升级后)

点开"显示升级前"并排对比:右边是 task 8 的新版(155 行),左边是 task 7 的原版(87 行)。

src/llm.ts · task 8(升级后)
src/llm.ts · task 7(升级前) 默认折叠 · 可点开并排对比

      

8.3 · 错误分层:一张表决定"重不重试"

失败场景代码路径判定实际行为
限流 / 服务端抖动HTTP 429 / 5xx → throw RetryableError暂时性非最后一轮:退避重试(1s → 2s)
参数错误 / key 错HTTP 4xx(非 429)→ throw FatalError永久性语义上"不重试",但实现里非最后一轮仍会退避重试,最后一轮才抛出(见下方 note)
超时AbortError(30s)→ catch 里 throw FatalError永久性立即放弃,不重试(再等只会继续超时)
网络错误 / 未知错误catch 兜底分支当作暂时性非最后一轮:退避重试;最后一轮包装成 RetryableError 抛出
如实看待一个"实现 vs 注释"的细节 8.2 代码里 catch 分支是 if (err instanceof RetryableError || err instanceof FatalError) { if (isLastAttempt) throw err }——FatalError 在非最后一轮并不会立刻抛出,而是和 RetryableError 一样走退避重试。也就是说 401 也会被重试 3 次、等到最后一轮才报错。这跟注释里"永久性错误直接放弃"有偏差,是个小小的实现简化(task 9 沿用)。读源码时能发现这类细节,比照着注释背结论更有价值。
这一步埋了一个"坑",也是设计的一部分 askLLM 现在会抛错误,但 task 8 的 main.ts 还是 task 7 的样子——REPL 里 runAgent 一旦抛错,就是一个未捕获的 Promise 拒绝,进程会异常退出。这不是遗漏:它是"分层推进"的典型节奏——task 8 先让 llm.ts 学会"抛得出来",task 9 再让 main.ts 学会"接得住"。

8.4 · 顺手适配:agent.ts 的一行

tsconfig 开了 strictreadonly unknown[] 的 map 回调里不能直接标注 (t: {...}),改成 as 断言才是正确写法。逻辑零变化。

src/agent.ts · task 8(升级后)
src/agent.ts · task 7(升级前) 默认折叠 · 可点开并排对比

      

8.5 · 运行验收

终端 · 验收命令
验收标准 ① 对话正常,每轮请求都有 [llm] HTTP 200(耗时 xxms) 日志;② 断网或错 key 时,你能在日志里看到失败原因、重试间隔在拉长(1s → 2s),且错误信息里有明确的原因分类(限流 / 超时 / 参数错误 / 网络错误)。"看清楚它怎么失败"比"它没失败"更重要。

8.6 · 与 pi 的对应

pi 的网络层分布在 packages/ai/src:同样的重试 + 退避思路、provider 层的限流处理,以及 packages/ai/src/convert.ts 对错误响应体 {error: {message}} 的统一解析。你现在写的这三件事(错误分层、退避重试、超时)就是它的最小骨架。

字段来源速查表(task 8 新增:健壮化相关)

名称含义来源
RetryableError暂时性错误(429 / 5xx / 网络瞬断),重试可能成功llm.ts 导出的类
FatalError永久性错误(4xx / 超时),重试无意义llm.ts 导出的类
MAX_ATTEMPTS最多尝试次数(3)llm.ts 常量
TIMEOUT_MS单次请求超时(30s)llm.ts 常量
waitAndRetry(attempt)指数退避等待(2n-1 秒)llm.ts 内部函数
makeRequest(messages, tools)带超时 + 重试 + 日志的请求体,返回协议 Messagellm.ts 内部函数
[llm] HTTP xxx(耗时 xxms)每次请求的日志行llm.ts makeRequest
(t as { function: { name: string } })strict 下 map 回调的正确断言写法agent.ts 第 45 行
← Home