M14. 生产化清单:可恢复、可观测、可约束#
一个 Agent 能够完成任务,只能说明模型和工具连接成功。要进入长期运行的应用,还需要回答四个问题:它做过什么?失败后能否恢复?危险动作由谁批准?上下文变长后会不会悄悄退化?
Pi 的好处是这些问题没有被藏在一层不可替换的服务里。坏处是你必须自己把边界拼完整。本章给出一套最小的生产化检查框架。
1. 用四条线组织系统#
用户输入
│
▼
AgentSession ── subscribe ──▶ 事件投影 / UI / 指标
│
├── Agent Loop ──▶ 模型 + 工具
│ │
│ └── 扩展策略:校验、阻断、命令、动态能力
│
└── SessionManager ──▶ JSONL 会话树 / 压缩 / 分支四条线分别解决:运行、控制、观察和恢复。它们可以在一个进程里实现,但不应该共享所有状态。比如事件投影可以丢失并重建,Session Tree 则要保证 entry 的父子关系不被随意改写。
2. 工具层:把副作用变成契约#
生产工具至少要有以下字段和行为:
- 输入 schema:拒绝缺字段、非法枚举和超长内容。
- 明确 description:让模型知道何时应该调用,不要把权限政策藏在描述里。
- 可取消执行:网络、文件和子进程都要接收
AbortSignal。 - 稳定结果:给模型返回可行动的文本,给程序返回结构化
details。 - 幂等键:重复消息或重试不会重复扣费、发信或写入外部系统。
- 审计信息:记录 tool name、参数摘要、结果状态和操作者,不要记录密钥。
扩展的 tool_call 钩子可以做一次策略阻断,但不能替代 OS 权限和服务端授权。模型提示词、工具描述和用户确认窗口都不是最终安全边界。
3. 会话层:append-only 不等于永不整理#
Pi 的 session file 使用 JSONL:首行是 session header,其余每行是带 id、parentId 和时间戳的 entry。消息、压缩、分支摘要、标签、模型变化等都可以成为不同类型的 entry。
这个结构提供两个重要性质:
- 追加新节点不会修改旧节点,回退只移动当前 leaf。
- 从旧 entry 继续工作会产生新分支,原始尝试仍然可追踪。
但文件会增长,消息历史也会超过模型窗口,所以恢复系统还需要 compaction。压缩不是删除历史,而是追加一条带摘要、保留起点和 token 统计的压缩 entry,然后在重建上下文时按路径选择可见内容。
import { SessionManager } from "@earendil-works/pi-coding-agent";
const session = SessionManager.open("/path/to/session.jsonl");
const tree = session.getTree();
const currentPath = session.getPath();
const leaf = session.getLeafEntry();
if (leaf) {
console.log({
leaf: leaf.id,
pathLength: currentPath.length,
childCount: session.getChildren(leaf.id).length,
treeSnapshotBytes: JSON.stringify(tree).length,
});
}上面的读取用于诊断和导航。不要通过手写 JSONL 来改变 leaf;让 SessionManager 的 branch、label 和 session 操作维护不变量。
4. 恢复流程要区分三类状态#
一次运行至少有三类状态:
| 状态 | 例子 | 恢复方式 |
|---|---|---|
| 已提交事实 | 完成的 user/assistant/toolResult 消息 | 从 session file 重建 |
| 运行中状态 | 当前流、活动工具、取消信号 | 结束旧运行,按策略重启 |
| 外部副作用 | 已发出的请求、写入的文件、扣费 | 依赖幂等键和外部系统查询 |
不要把“进程崩溃前最后一个 delta”当作已提交事实;也不要重放所有工具调用来猜测外部系统状态。服务启动时,应先打开 session,检查最后一个完整 entry,再根据工具幂等记录决定是否继续。
5. 观测层:既看结果,也看过程#
只记录最终答案无法定位 Agent 为什么失败。至少应保留以下四类指标:
- 生命周期:一次 prompt 的开始、结束、取消、重试和耗时。
- 工具:每个工具的调用次数、成功率、耗时、错误类型和输出大小。
- 上下文:压缩次数、压缩前 token 估算、保留路径长度和摘要失败次数。
- 成本与质量:模型、thinking level、token 使用、用户重试率和验收通过率。
原始输入可能包含源代码、隐私或密钥。事件上报前应做字段级脱敏,尤其是工具参数和工具输出。不要因为 Pi 的事件很透明,就把完整上下文无条件复制到第三方日志服务。
6. 扩展与版本管理#
扩展是运行时代码,不是静态 prompt。上线时要像管理插件一样管理它:
- 锁定 Pi 包版本和扩展版本。
- 启动时记录扩展清单与校验和。
- 让动态工具启用有明确的租期或撤销操作。
- 为
tool_call、session_start、message_end等关键钩子写回归测试。 - 升级 Pi 时检查事件联合类型、session entry schema 和 compaction 行为。
尤其不要只测试“扩展能加载”。加载成功不代表工具描述正确、阻断分支可达或热重载后不会留下旧订阅。
7. 上线前失败演练#
一套有效的验收不应该只执行成功案例。建议至少演练:
| 故障 | 预期结果 |
|---|---|
| 模型请求超时 | 运行可取消,session 不写入半截 assistant 消息 |
| 工具参数非法 | 调用被 schema 拒绝,模型收到可纠正的错误 |
| 工具执行异常 | 结果标记为错误,Agent 有机会决定重试或停止 |
| 用户拒绝危险动作 | 工具不执行,拒绝原因可追踪 |
| 压缩调用失败 | 不覆盖旧历史,发出明确的 compaction 错误 |
| 进程在工具后崩溃 | 重启后能识别最后完整 entry,不盲目重复副作用 |
| 事件消费者断开 | Agent 与展示层解耦,重新连接可从 session 恢复 |
| 扩展版本不匹配 | 启动失败或降级,而不是静默使用错误工具 |
8. 最小上线清单#
- 每个工具有 schema、超时、取消、错误和幂等策略。
- 危险工具有宿主权限检查;容器或 OS 边界已配置。
- session file 的目录、权限、备份和清理策略已确定。
- compaction 和 branch summarization 有失败测试。
- 事件投影与持久化消息分开,敏感字段已脱敏。
- prompt、tool result、扩展日志和业务数据库都有大小上限。
- 可以从 session id 定位一次运行的模型、工具、版本和错误。
- Pi 升级有 schema 检查和一组 golden session 回放。
9. 结语#
Pi 的生产化不是给一个“大 Agent”再套一层服务,而是把运行循环、能力扩展、事件投影和会话恢复分别做成可验证的边界。这样,当模型换了、工具加了、上下文压缩策略变了,系统仍然知道哪些事实可以恢复、哪些副作用不能重放、哪些状态必须由人决定。
资料
本文依据 Pi SDK 文档、SessionManager 源码、Compaction 文档和 Pi Extensions 文档重写。上线前请再次核对当前版本的接口和 entry schema。