让 llm.ts 从"一把梭"升级成"能扛抖动"的网络组件
一句话:网络请求是整个 Agent 里最不可靠的环节,task 8 把它武装到牙齿。
llm.ts)都是"一把梭"——发出去、拿到就返回、拿不到就 process.exit(1) 直接退出。真实世界里网络会抖动、服务会限流、接口会超时,一个会"因网络抖动直接崩掉"的 Agent 是不合格的。心智转变:不是"请求会失败"的问题,而是"你准备怎么失败"的问题。这一步只做一件事:把失败处理做对。| 文件 | 变化 | 程度 |
|---|---|---|
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,一字未改 | 无变化 |
点开"显示升级前"并排对比:右边是 task 8 的新版(155 行),左边是 task 7 的原版(87 行)。
| 失败场景 | 代码路径 | 判定 | 实际行为 |
|---|---|---|---|
| 限流 / 服务端抖动 | HTTP 429 / 5xx → throw RetryableError | 暂时性 | 非最后一轮:退避重试(1s → 2s) |
| 参数错误 / key 错 | HTTP 4xx(非 429)→ throw FatalError | 永久性 | 语义上"不重试",但实现里非最后一轮仍会退避重试,最后一轮才抛出(见下方 note) |
| 超时 | AbortError(30s)→ catch 里 throw FatalError | 永久性 | 立即放弃,不重试(再等只会继续超时) |
| 网络错误 / 未知错误 | catch 兜底分支 | 当作暂时性 | 非最后一轮:退避重试;最后一轮包装成 RetryableError 抛出 |
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 学会"接得住"。tsconfig 开了 strict,readonly unknown[] 的 map 回调里不能直接标注 (t: {...}),改成 as 断言才是正确写法。逻辑零变化。
[llm] HTTP 200(耗时 xxms) 日志;② 断网或错 key 时,你能在日志里看到失败原因、重试间隔在拉长(1s → 2s),且错误信息里有明确的原因分类(限流 / 超时 / 参数错误 / 网络错误)。"看清楚它怎么失败"比"它没失败"更重要。pi 的网络层分布在 packages/ai/src:同样的重试 + 退避思路、provider 层的限流处理,以及 packages/ai/src/convert.ts 对错误响应体 {error: {message}} 的统一解析。你现在写的这三件事(错误分层、退避重试、超时)就是它的最小骨架。
| 名称 | 含义 | 来源 |
|---|---|---|
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) | 带超时 + 重试 + 日志的请求体,返回协议 Message | llm.ts 内部函数 |
[llm] HTTP xxx(耗时 xxms) | 每次请求的日志行 | llm.ts makeRequest |
(t as { function: { name: string } }) | strict 下 map 回调的正确断言写法 | agent.ts 第 45 行 |