跳转到内容

第 30 课:可靠性工程

把 Pi Agent 从 demo 推向真实仓库时,如何设计超时、重试、幂等、partial failure、脏工作区保护、用户中断和恢复策略。

可靠性工程不是给 agent 外面包一层 try/catch。生产级 coding agent 要把每一步都设计成“可观察、可中断、可恢复、可证明没有重复副作用”的动作。

本课的核心模型是:先保护工作区,再执行工具;先记录检查点,再尝试重试;先判断副作用是否幂等,再恢复任务。Pi 源码已经给了几个重要锚点:provider 层有 timeoutMsmaxRetriesAbortSignalAgentSession 层有 auto-retry、abort、session persistence;bash 工具有 timeout、输出截断和进程树取消;extension 示例里有 dirty repo guard。我们要做的是把这些点组合成自己的可靠性策略。

阶段四的 forge-code task create "修复登录测试失败" 已经能跑起来,但真实仓库会不断遇到这些情况:

  • 模型流式请求超时,assistant message 只写了一半。
  • npm test 卡住,子进程还在后台跑。
  • edit 已经改了一个文件,第二个工具失败。
  • agent 在用户本来就有未提交修改的工作区里继续写代码。
  • 用户按下取消后,UI 停了,但 shell 命令或 retry sleep 没停。
  • 重启进程后,不知道上次任务是可以继续、应该回滚,还是必须人工接管。

这节课回答一个问题:怎么让 agent 失败时停在一个可解释、可检查、可恢复的位置,而不是留下不可追踪的半成品。

可靠性可以拆成五个控制面:

第一是时间边界。模型请求、WebSocket 连接、bash 命令、外部 API、子 agent 任务都要有 timeout。没有 timeout 的任务不叫长期任务,只是无人看守的挂起状态。

第二是重试边界。重试只适合 transient failure,例如 network_error429503timeout。如果错误已经产生不可重复副作用,例如 commit、push、npm publish,就不能直接重放同一个动作。

第三是幂等边界。读操作天然更容易重试;写文件可以通过“先读版本、再 diff、再写入”接近幂等;发布、提交、外部工单更新必须有 idempotency key、状态查询或人工确认。

第四是工作区边界。coding agent 不应该默认拥有当前目录里的所有改动。执行前要检查脏工作区,区分用户已有修改、agent 本轮修改、测试产生的缓存文件。

第五是恢复边界。每次任务都要留下 session、trace、checkpoint、changed files、last successful step 和 failure classification。恢复不是“继续 prompt 一下”,而是基于这些证据决定 resume、retry、rollback、handoff。

不要一上来写重试循环。先把失败分成四类:

  • 可重试:provider overloaded、网络断开、HTTP 5xx、短暂 timeout。
  • 不可重试但可恢复:测试失败、类型错误、工具参数不合法。
  • 有副作用的 partial failure:文件写了一半、commit 创建了、外部系统状态变了。
  • 用户控制流:用户取消、权限拒绝、脏工作区阻断。

这个分类决定后续动作。比如测试失败不是“系统不可靠”,而是 agent 应该读失败输出继续修复;npm publish 成功后网络断开则不能简单重试,因为包可能已经发布。

Pi 在 packages/ai/src/types.ts 的 provider options 里定义了 timeoutMswebsocketConnectTimeoutMsmaxRetriesmaxRetryDelayMs。这些是模型请求层的时间和重试边界。

在 coding agent 层,SettingsManager 把这些配置暴露成 retry.provider.timeoutMsretry.provider.maxRetrieshttpIdleTimeoutMswebsocketConnectTimeoutMs。这说明生产 agent 不能把 timeout 写死在代码里,要允许不同团队按网络环境和模型供应商调整。

AgentSession 会检查 assistant error message,识别 overloaded、rate limit、429、500、502、503、504、network error、connection lost、timeout 等可重试错误,然后用指数退避准备下一次 continue

关键点是:它会把错误 assistant message 从 agent state 里移除,但 session history 仍然保留失败证据。这是一种很实用的恢复策略:模型下一次继续时不被失败 message 污染,但审计时仍能看到失败发生过。

bash 工具的 timeout 不是简单返回错误。它会启动子进程,注册 AbortSignal,timeout 或 abort 时杀掉 process tree,最后把已收集输出、截断信息、full output path 和错误状态一起返回。

这比“命令失败了”更有用,因为恢复时要知道命令是否已经跑出部分输出、是否被截断、是否超时、是否由用户取消。

coding agent 的一次任务通常不是一个原子事务。比如:

read files -> edit file A -> edit file B -> run tests -> update summary

如果 edit file A 成功、edit file B 失败,不应该假装整个任务没发生。正确做法是记录:

  • 完成到哪一步。
  • 哪些文件被修改。
  • 下一步是否可安全重试。
  • 是否需要先跑 git diff 或恢复 worktree。
  • 是否需要用户决定保留还是丢弃已有改动。

用户取消不是异常小角落,而是 coding agent 的核心控制流。取消要同时打断 retry sleep、provider stream、tool execution、bash subprocess 和后台 task runner。否则 UI 停了,工作区仍在变化。

Pi 的 AgentSession.abort() 同时调用 abortRetry()agent.abort() 并等待 idle;abortBash() 取消正在运行的 bash 命令。这些锚点说明:取消要向下传递,不要只改 UI 状态。

本课 lab starter 是 /Users/ryanchen/codespace/pi-agent-course-lab/src/20-reliability-scenarios.ts。它已经把场景压缩成四类:

type Scenario = "timeout" | "dirty-worktree" | "tool-failure" | "user-cancel";

课堂实践不是把这张表扩成更长的表,而是把每个 scenario 变成一个可执行的恢复决策:

  • timeout:记录 partial trace,标记 task retryable,但先确认没有重复副作用。
  • dirty-worktree:在写入前停止,报告 changed files,让用户决定归属。
  • tool-failure:记录 tool name、参数摘要、错误类别,判断 retryable 还是 permanent。
  • user-cancel:取消 provider 和 tools,持久化 checkpoint,让 worktree 保持可检查。

先从 starter 的 mitigation() 出发,把“建议列表”升级成“恢复计划”:

type RecoveryPlan = {
action: "retry" | "stop" | "resume" | "handoff";
retryable: boolean;
requireUserDecision: boolean;
evidence: string[];
};
function planRecovery(scenario: Scenario, changedFiles: string[]): RecoveryPlan {
if (scenario === "dirty-worktree") {
return {
action: "stop",
retryable: false,
requireUserDecision: true,
evidence: ["git status --porcelain", ...changedFiles],
};
}
if (scenario === "timeout") {
return {
action: "retry",
retryable: true,
requireUserDecision: changedFiles.length > 0,
evidence: ["trace.jsonl", "last tool call", "timeoutMs"],
};
}
if (scenario === "user-cancel") {
return {
action: "handoff",
retryable: false,
requireUserDecision: true,
evidence: ["checkpoint", "abort reason", "changed files"],
};
}
return {
action: "resume",
retryable: false,
requireUserDecision: false,
evidence: ["tool name", "args summary", "stderr"],
};
}

课堂上重点看变量怎么流:

  1. scenario 来自 runtime 观察,不是模型猜测。比如 timeout 来自 provider 或 bash executor;dirty worktree 来自 git status --porcelain
  2. changedFiles 决定能不能自动继续。只要有未归属写入,就不要盲目 retry。
  3. retryable 只回答“技术上能不能重试”,不代表“应该立刻重试”。
  4. evidence 是恢复入口。下一次 resume 时先读 evidence,再决定继续或交给用户。

然后把它接到任务 runner:

const before = await gitStatus();
if (before.dirty) return planRecovery("dirty-worktree", before.files);
try {
await runAgentTask({ timeoutMs: 120_000 });
} catch (error) {
const after = await gitStatus();
return planRecovery(classify(error), after.files);
}

这里的控制点在任务开始前和异常捕获后。开始前保护用户修改,失败后保护 agent 已产生的证据。

  • /Users/ryanchen/codespace/pi-agent-course-lab/src/20-reliability-scenarios.ts:本课 lab starter,用场景表建立恢复策略入口。
  • /Users/ryanchen/codespace/external-sources/pi/packages/ai/src/types.ts at 61babc2StreamOptions / ImagesOptions 定义 signaltimeoutMsmaxRetriesmaxRetryDelayMs
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/settings-manager.ts at 61babc2RetrySettings、provider retry 配置、HTTP idle timeout、配置迁移和 settings 持久化。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/agent-session.ts at 61babc2AgentSession 处理 session persistence、auto-retry、abort()abortRetry()abortBash()
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/bash-executor.ts at 61babc2:统一 bash 执行结果,记录 output、exitCode、cancelled、truncated 和 fullOutputPath。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/tools/bash.ts at 61babc2:bash tool 的 timeout、AbortSignal、进程树取消、输出截断和错误包装。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/dirty-repo-guard.ts at 61babc2:通过 session_before_switch / session_before_fork 在脏仓库里阻断 session 切换。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/agent-session-retry.test.ts at 61babc2:auto-retry 行为的测试锚点。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts at 61babc2:bash 取消和 aborted assistant message 持久化的测试锚点。

学完本课后,你应该能做到:

  • 解释 timeout、retry、abort、幂等、partial failure、恢复策略之间的区别。
  • 判断一个错误是否适合自动重试,还是应该停止并要求用户介入。
  • 在任务开始前设计脏工作区保护,避免覆盖用户未提交修改。
  • 为工具失败设计 evidence schema,而不是只返回一段错误字符串。
  • 说明为什么用户取消必须向 provider、tool、bash subprocess 和 retry sleep 传播。
  • 为自己的 forge-code 设计一个最小 checkpoint:task id、step、changed files、last event、failure class、resume action。
  1. 实践题:扩展 /Users/ryanchen/codespace/pi-agent-course-lab/src/20-reliability-scenarios.ts,把 mitigation() 改成返回 RecoveryPlan,并为 timeoutdirty-worktreetool-failureuser-cancel 各写一个示例输入输出。
  2. 思考题:如果一次任务已经完成 git commit,但在生成 PR 描述时网络失败,为什么不能简单从头重试整条任务链?