返回课程首页

36

生产化与测试

完成安全、恢复、评估、诊断、打包和升级检查。

源码基线:Pi v0.82.0 · commit 518855d

一个 Agent 能回答问题,只说明主路径可用。一个桌面 Agent 能进入生产,还必须证明:

  • 不该执行的动作确实不会执行;
  • 崩溃、取消和重试不会制造第二次副作用;
  • Session 能恢复;
  • Skills 与 Extensions 更新后仍兼容;
  • 模型行为变化能被评估;
  • 出错时能定位到 Provider、Tool、Extension、Session 或 Bridge。

本章把示例项目变成可发布、可升级、可回退的产品。

1. 学习目标

完成本章后,你应能够:

  1. 为 Prompt、Tool、Extension、Broker 与 OS 建立分层安全边界;
  2. 设计 Abort、Timeout、Retry 与 Idempotency 的组合;
  3. 使用 Faux Provider 和固定 Event Sequence 测试,不消耗真实模型 Token;
  4. 分别测试 Skill 的内容契约与 Extension 的运行时契约;
  5. 建立评估、诊断、兼容性、打包与回滚门禁。

2. 生产化不是一个开关

flowchart TD
    P["Prompt / Skill rules"] --> X["Extension Gate"]
    X --> A["Approval"]
    A --> B["Final Tool Broker Policy"]
    B --> S["OS / Container Sandbox"]
    S --> AU["Audit / Recovery"]

上层规则改善行为,下层边界限制能力:

  • Prompt/Skill:告诉模型应该怎样做;
  • Extension Gate:在受控执行链中检查/转换;
  • Approval:让用户对具体 Canonical 动作做决定;
  • Broker:以服务端规则重新验证;
  • Sandbox:限制进程实际能访问的文件、网络和能力;
  • Audit:为恢复、对账和责任追踪提供证据。

任何一层都不能单独代替全部安全设计。

3. Threat Model 先于测试用例

先列资产与攻击面:

资产 主要风险 最终防线
Workspace 文件 越界读写、路径穿越 Canonical Path + Broker + Sandbox
Shell 命令注入、继承环境 Structured Policy + Env Allowlist + Sandbox
API Key Prompt/ToolResult 泄漏 Auth Store + Redaction + Process Boundary
Session 篡改、跨租户读取 File Permission + Tenant Binding
Extension 任意本地代码 来源固定、审查、隔离、最小权限
Approval UI 欺骗、参数替换 Canonical Args + One-shot Capability
Provider Response Prompt Injection Trust Boundary + Tool Final Policy

ExtensionAPI、Project Trust、TypeScript 类型与子进程都不等于 OS Sandbox。

4. 权限模型

不要只问“这个 Tool 能不能用”,还要问:

谁
在什么 Session/Workspace
对哪个 Canonical Resource
执行什么动作
在什么时间窗口
可以执行几次

一次性 Capability 可以包含:

interface ToolCapability {
  toolCallId: string;
  sessionId: string;
  action: "write";
  canonicalTarget: string;
  argsHash: string;
  expiresAt: number;
  remainingUses: 1;
}

这是宿主设计。真正执行前,Broker 重新验证全部字段;不要只相信 UI 返回 approved: true

5. Timeout、Abort 与 Cleanup

三者含义不同:

  • Timeout:宿主不愿再等;
  • Abort:请求底层停止;
  • Cleanup:释放已经创建的资源。
timeout
→ abort controller
→ driver observes signal
→ process/fetch stops
→ finally cleanup
→ return explicit interrupted result

只用 Promise.race() 会停止等待,却不一定停止底层工作。Tool/Extension 必须把 Signal 传到 Fetch、Child Process、Database Driver 或 Remote Executor。

Pi 的取消也不是一个总开关:AgentSession.abort() 会停止 Auto-retry 和当前 Agent Run,并 等待 Agent Idle;独立 Bash、Compaction、Branch Summary 分别使用 abortBash()abortCompaction()abortBranchSummary()。测试必须按操作类型调用对应取消 API,并验证 其 Signal 到达底层进程。

6. 两层模型 Retry Budget

Pi v0.82.0 有两层模型重试:

配置/行为
Provider Transport Retry retry.provider;处理可重试传输错误,可读取 Retry-After
AgentSession Auto-retry retry.enabled/maxRetries/baseDelayMs;最终 Assistant Error 后延迟并 continue(),发 auto_retry_* Event

两层可能叠加,所以次数、总时间、Token 与成本要分别计算,也要分别验证 Abort。

两层共同满足:

  • 操作可安全重试,或有 Idempotency Key;
  • Retry 次数和总时间有上限;
  • Abort 后不再重试;
  • 用户能看见正在恢复;
  • 最终失败会收敛到 Settled/Diagnostic。

只有 Provider Transport Retry 能读取 Response Header 并尊重 Retry-After。AgentSession Auto-retry 拿不到这个 Header,只使用 baseDelayMs * 2^(attempt-1) 指数退避。

Provider/Auto-retry 与 Tool 副作用重试要分开。不要因为 Provider 暂时失败,自动重放一个可能已 写入成功的 Tool。

7. Idempotency 与不确定结果

最危险的状态不是“失败”,而是:

后端可能已经成功,但桌面进程在收到确认前崩溃。

用业务 ID 连接:

runId + toolCallId + operationId

恢复后先查询 Audit/Backend:

  • 已成功:恢复为 Completed;
  • 明确未执行:允许重新提交;
  • 无法判断:标为 Indeterminate,要求人工确认;
  • 可补偿:显示补偿动作,而不是静默回滚。

Exactly-once 不能靠 RPC Command ID 或再次发送相同 Prompt 获得。

8. Session 与多会话隔离

测试两个并发 Session:

  • Extension Closure 是否串状态;
  • Module Singleton 是否按 Tenant/Workspace Key;
  • Approval 是否路由到正确 Window;
  • Abort A 是否影响 B;
  • Model 切换是否泄漏;
  • Session Entry 是否写入错误文件;
  • Shared ModelRuntime/Auth 是否只有预期共享。

如果要隔离 Extension Instance,每个 Session 需要独立 ResourceLoader/Services;只创建两个 AgentSession 但复用同一个 Extension Runtime,并不会自动隔离。

9. Token、Context 与成本

记录三个层次:

层次 用途
Turn Usage 单次 Provider 调用与 Retry
Agent Run Usage 一次用户任务的多 Turn 总计
Session Usage 长期趋势与预算

还要记录:

  • Provider/Model;
  • Input/Output/Cache Token;
  • Tool Nested Model Usage(若有);
  • Compaction 前后 Context Size;
  • Estimated 与 Final 的区别;
  • Retry 造成的额外成本。

成本告警不要阻塞关键 Session Flush;但超预算时应在下一次 Provider 调用前由宿主 Policy 阻止或要求确认。

10. 测试金字塔

flowchart TD
    U["Pure Unit<br/>schema/policy/reducer"] --> F["Faux Agent Loop"]
    F --> E["Extension/Skill Contract"]
    E --> S["Session/Recovery"]
    S --> D["Desktop Bridge"]
    D --> R["Real Provider Smoke"]

越靠上越快、越确定、越便宜。真实 Provider 测试只保留少量 Smoke/Eval,不能把所有回归 都建立在外部模型上。

11. Pure Unit Test

优先测试纯函数:

  • Path Canonicalization;
  • SQL/Command Risk Classifier;
  • Approval Capability 校验;
  • Tool Schema;
  • Event Reducer;
  • Redaction;
  • Retry Budget;
  • Version Matrix。

这些测试不需要创建 AgentSession。

12. Faux Provider 测 Agent Loop

Pi 仓库内部 Test Harness 使用 Faux Provider 与预设 Assistant Response,可以确定性地产生:

text
tool call
parallel tool calls
provider error
thinking
stop

应用可以建立等价公开夹具:

import { join } from "node:path";
import {
  fauxAssistantMessage,
  fauxProvider,
  fauxText,
  fauxToolCall,
} from "@earendil-works/pi-ai";
import {
  createAgentSession,
  ModelRuntime,
  SessionManager,
} from "@earendil-works/pi-coding-agent";

const faux = fauxProvider();
const modelRuntime = await ModelRuntime.create({
  authPath: join(fixturePath, "auth.json"),
  modelsPath: null,
});
modelRuntime.registerNativeProvider(faux.provider);
faux.setResponses([
  fauxAssistantMessage(
    fauxToolCall("read", { path: "logs/error.log" }),
    { stopReason: "toolUse" },
  ),
  fauxAssistantMessage(
    fauxToolCall("controlled_write", {
      path: "config.json",
      content: "{}",
    }),
    { stopReason: "toolUse" },
  ),
  fauxAssistantMessage(fauxText("修复完成")),
]);

const { session } = await createAgentSession({
  cwd: fixturePath,
  sessionManager: SessionManager.inMemory(fixturePath),
  modelRuntime,
  model: faux.getModel(),
  customTools: [readTool, controlledWriteTool],
});

try {
  await session.prompt("读取错误日志并修复配置");
  // 断言事件、工具调用、ToolResult 与 Session Entry。
} finally {
  session.dispose();
}

断言 Event Order、Tool Execute Count、ToolResult、最终消息与 Session Entry。不要在测试中 调用真实付费 Provider。fauxProvider() 是不修改全局 API Registry 的现代组合;要把它的 Native Provider 注册进被测 ModelRuntime,再把同一 ModelRuntime 与 faux.getModel() 传给 AgentSession。Legacy registerFauxProvider() 只注册 Compat 全局 API Registry,不能单独让 独立 ModelRuntime 认识 Faux Provider。 测试使用 Fixture Auth Path 和内存 Model Store,避免读取真实用户的认证、模型与缓存;结束时 还要 Dispose Session,清理监听器和会话资源。

并行 Event Order 还要按配置断言:默认 Tool Batch 并行,但全局 toolExecution === "sequential",或批次中任一 Tool 的 executionMode === "sequential",都会让整批顺序执行。并行时 tool_execution_end 可按完成顺序到达,最终 ToolResult Message 则恢复为模型 ToolCall 顺序。

13. Skill 测试

Skill 需要三层测试。

静态结构

  • Frontmatter/Name/Description;
  • 触发摘要是否清楚;
  • 依赖文件是否存在;
  • 相对路径是否能解析;
  • 指令是否要求未提供的 Tool;
  • 是否意外包含 Secret/绝对机器路径。

触发与读取

脚本化 Faux 可验证:

  • 显式 Skill 展开;
  • 自动路径中模型发出的 Read 请求;
  • Tool/Event 编排;
  • 读取了完整 SKILL.md
  • 需要的 Reference 按顺序读取;
  • 输出/动作满足约束。

但 Faux Response 是预先写死的,不能证明模型“应触发时能识别、不相关时不误触发”。真正的 触发准确率必须进入固定模型的 Golden Eval 或少量 Real-provider Test。

安全与回归

  • 外部内容不能覆盖 System/Host Policy;
  • 缺文件时 Fail 可见;
  • Tool 被禁用时有合理降级;
  • Skill 更新后 Golden Task 仍通过。

Skill 是文本工作流,测试重点是“模型在给定工具和上下文下是否遵守契约”,不是只校验文件 能被 Loader 发现。

14. Extension 测试

至少覆盖:

  1. Factory 成功与失败;
  2. Pre-bind Provider/Flag 副作用;
  3. Handler 顺序与分类型错误语义;
  4. tool_call Block/Transform/Throw;
  5. tool_result Patch/Redaction,以及宿主 Timeout/Worker Isolation;
  6. Abort 传递;
  7. Reload Shutdown/Cleanup;
  8. New/Resume/Fork State Reconstruction;
  9. EventBus Unsubscribe;
  10. 多 Session/Module Singleton;
  11. Tool/Flag/Command 冲突;
  12. Pi 版本兼容。

ExtensionRunner 没有内置 Handler Timeout;一个永不 Resolve 的 Handler 会一直阻塞。测试中 要验证宿主 Timeout/进程终止策略。错误语义也不统一:tool_result Throw 会被记录后继续 后续 Handler;tool_call Throw 会终止后续 Hook,并最终变成阻止工具执行的 Error Result。

安全关键 Extension 加载失败时,要测试“宿主确实阻止高风险动作”,而不是只断言出现一条 Warning。

15. Session 与 Replay 测试

固定一份 Event/Entry Fixture,测试:

  • JSONL 打开与恢复;
  • Branch 切换;
  • Fork Before/At;
  • Compaction Summary;
  • Custom Entry;
  • Tool Details State;
  • 中断的 Assistant/Tool;
  • 文件尾损坏;
  • CWD/Trust Replacement;
  • 旧 Generation Event 被拒绝。

generationruntimeIdrunIdmessageUiKeyoperationId 以及 Extension Source/Version 多数不是 AgentSessionEvent 自带字段。它们由 Desktop Controller/Bridge 生成并包装进 Fixture;测试要同时验证 Envelope 生成与 Reducer,而不是在 Pi Event 类型里 寻找这些字段。

Replay 的目标是重建应用状态,不是重新执行 Tool。任何回放逻辑都必须保证 Side Effect 为零。

16. Desktop Bridge 测试

SDK

  • Session Replacement 前拆旧 Bridge;
  • 新 Session 先订阅再绑定 Extension;
  • runtime.dispose() 发 Shutdown;
  • Store 不保存 Session Object;
  • Window Close 取消 Approval。

RPC

  • LF-only Framing;
  • Command Response 乱序;
  • JSON Parse Error;
  • stdout/stdin Backpressure;
  • Extension UI Request/Response;
  • Worker Exit;
  • Pending 清除;
  • Snapshot + Live Event Rehydrate;
  • 无版本握手时的外层版本门禁。

测试时随机切分 JSON Byte Chunk,不能假设一行恰好等于一次 Pipe Read。

17. Evals:测“效果”,不是测代码分支

建立 Golden Task Dataset:

task
workspace fixture
allowed tools
expected invariants
forbidden actions
quality rubric
cost/latency budget

示例:

给定一个缺少端口配置的项目,Agent 应先读取日志和配置,提出最小修改;未经批准不能写入 Workspace 外部,也不能运行网络命令。

评估维度:

  • 任务完成率;
  • Tool 选择;
  • 权限违规率;
  • 不必要写入;
  • 引用证据正确性;
  • Turn/Token/Cost;
  • 延迟;
  • 恢复成功率。

模型输出非确定,所以比较分布和不变量,不要只 Snapshot 一段自然语言。

18. 日志与 Trace

每条记录至少带:

timestamp
runtimeId / generation
sessionId / runId / turn
messageUiKey / toolCallId / operationId
provider / model
extension source/version
event / duration / result class

默认不记录:

  • API Key/OAuth Token;
  • 完整 Environment;
  • 未脱敏 Prompt/Tool Content;
  • Approval 中的敏感参数;
  • 用户本地文件正文。

需要内容级调试时,使用显式 Opt-in、短期保留和访问控制。

19. 诊断不是一个 errors 数组

Controller 聚合:

  • Resource Loader 的 Extension Errors;
  • Skill/Prompt/Theme Diagnostics;
  • Runtime Diagnostics;
  • Model Fallback Message;
  • Provider/Auth Error;
  • Tool Result Error;
  • Session Recovery/Replacement Exception;
  • RPC Parse/Exit;
  • Host Policy/Audit Failure。

每条诊断应包含 Scope、Severity、Recoverable、User Action 与 Correlation ID。

20. 打包

SDK/Electron

  • 固定 Node 与 Pi Package 版本;
  • 审查 Production Dependencies;
  • 将 Auth/Session 放入正确用户目录;
  • 不把开发环境变量带入发布包;
  • 为 Native Addon/Child Process 建签名策略。

RPC/Sidecar

  • 把固定版本 Worker 随 App 打包;
  • 不从任意系统 PATH 发现 Pi;
  • 校验 Binary Hash/Signature;
  • 明确 CWD、Environment 与 Sandbox Profile;
  • 捕获 stdout/stderr/Exit;
  • Worker 与 App 使用同一兼容矩阵。

当前 Pi RPC 没有内置 Version/Capabilities Handshake,所以版本校验要在协议外完成。

21. 升级与兼容性

每次升级 Pi:

read changelogs
→ type/build checks
→ skill static tests
→ extension contract tests
→ session fixture replay
→ SDK/RPC bridge tests
→ golden eval comparison
→ canary
→ rollback ready

重点观察:

  • Agent/Session Event 顺序;
  • Tool Result Schema;
  • Extension Handler 错误语义;
  • Reload/Replacement;
  • Provider Model Metadata;
  • RPC Union;
  • Session Entry/Compaction;
  • Node Runtime 要求。

TypeScript 编译通过不代表行为兼容。

22. 发布门禁

门禁 必须通过
安全 越权/注入/审批替换测试
确定性 Unit、Faux、Replay
生命周期 Abort、Reload、Dispose、Crash
并发 Parallel Tool、多 Session、多窗口
质量 Golden Evals 不低于基线
成本 Token/Latency 在预算内
兼容 Pi/Node/Worker/Extension Matrix
运维 Log、Diagnostic、Canary、Rollback

安全关键失败不能用“已知问题”静默放行。

23. 综合验收测试

发布前完成以下 12 项:

  1. Text + Thinking Streaming;
  2. 模型切换与能力降级;
  3. Read Tool 与 Parallel Tool;
  4. Write Approval 通过、拒绝、超时和窗口关闭;
  5. Agent Run、独立 Bash、Compaction、Branch Summary 分别取消,并传播到对应底层进程;
  6. Steering/Follow-up Queue;
  7. Session Save/Resume/Branch;
  8. Compaction 后历史仍可回看;
  9. Skill 触发、读取、遵循与缺文件诊断;
  10. Extension Reload、冲突、失败与 Cleanup;
  11. Worker Crash/Rehydrate/Indeterminate Tool;
  12. 两 Session 两窗口无状态或审批串线。

每项都要有自动化证据;真实 Provider 只补充少量端到端 Smoke。

24. 常见误区

“有 Approval 就不用 Sandbox”

错误。Approval 可能被误路由,参数也可能在批准后改变。

“Abort Promise 返回就代表副作用停止”

错误。底层 Driver 必须观察 Signal,已发生动作还要对账。

“Skill 是 Markdown,不需要测试”

错误。它会改变模型的 Tool 选择和执行流程。

“Faux Provider 不能测试真实 Agent”

错误。它非常适合测试控制流与不变量;真实 Provider 用于补行为评估。

“版本固定后就不需要兼容测试”

错误。升级、Session 恢复与 Extension 包仍会跨版本发生。

25. 本章重点总结

  • 生产安全是 Prompt、Extension、Approval、Broker、Sandbox 与 Audit 的分层组合;
  • Timeout 必须触发 Abort,Abort 必须传到底层,Cleanup 必须幂等;
  • 副作用使用 Idempotency Key,并把未知结果标为 Indeterminate;
  • Unit/Faux/Replay 测确定控制流,Golden Eval 测模型效果;
  • Skill 测发现、读取、遵循与安全,Extension 测错误、生命周期、冲突与并发;
  • Session Replay 只重建状态,绝不重新执行 Tool;
  • Diagnostics 来自多个层,不是一个统一数组;
  • 打包固定 Worker/依赖,升级运行契约测试并准备 Canary/Rollback。

26. 练习

  1. 为什么 Approval Capability 要绑定 argsHashtoolCallId
  2. Promise.race() 超时为什么可能留下后台副作用?
  3. 哪些测试最适合 Faux Provider,哪些必须用真实模型?
  4. Skill 的静态测试与 Golden Task 测试分别发现什么问题?
  5. Worker 崩溃后 Tool 状态为什么可能是 Indeterminate?
  6. Session Replay 为什么不能调用 Tool?
  7. 设计一份包含两个并行 Session 的隔离测试。
  8. 当前 RPC 没有版本握手时,桌面应用怎样固定兼容版本?