跳转到内容

阶段 8:工程化与可信交付

本阶段的位置:前七阶段解决“系统怎样工作”。这一阶段解决“改了以后,怎么知道没有改坏,而且以后还敢继续改”。 源码目录:$PI_SRC。核心材料:根目录 AGENTS.md、CONTRIBUTING.md、test.sh、package.json、scripts/、.husky/pre-commit。 一句话主线:模型输出可以不确定,但测试、边界和发布流程必须尽量确定。

一、为什么 Agent 项目的工程化更难

跳转到“一、为什么 Agent 项目的工程化更难”

普通函数很容易测试:

输入 2 + 3 → 输出一定是 5

Agent 不一样:

  • 模型每次措辞可能不同;
  • 同一个问题可能走不同的工具路径;
  • 网络、限流和上下文长度会改变结果;
  • 工具可能修改真实文件;
  • 一个“看起来回答对了”的结果,内部可能没有执行该执行的步骤。

所以不能只测试“最后那段文字像不像”。更可靠的做法是测试结构和不变量:

  • 工具调用有没有发生;
  • 参数有没有经过校验;
  • 工具结果有没有回到会话;
  • 事件顺序对不对;
  • 失败时有没有正确中止;
  • 压缩后原文有没有保留;
  • 分支切换后活动路径是否正确。

这一阶段的核心不是背命令,而是建立一种工程判断:

哪些部分可以完全确定地测,哪些部分只能做少量真实集成验证?


二、先读两份“合同”

跳转到“二、先读两份“合同””

它主要告诉开发者和 Agent:

  • 代码风格和 TypeScript 约束;
  • 修改后运行什么检查;
  • 哪些命令不要随便跑;
  • 测试必须用假 provider;
  • Git 操作有哪些禁区;
  • changelog 怎么写;
  • 依赖和 lockfile 怎样处理。

这份文件不是背景资料,而是仓库内开发的行为合同。用 Agent 改 Pi 时,应让它在仓库根目录工作,才能自动读到这些规则。

2.2 CONTRIBUTING.md:外部贡献门槛

跳转到“2.2 CONTRIBUTING.md:外部贡献门槛”

它强调的是:

  • core is minimal:不属于核心的功能更应做成扩展;
  • 你必须理解自己的代码:可以用 AI 写,但解释不清的 PR 会被关闭;
  • 新贡献者的 issue/PR 有自动关闭和人工放行机制;
  • 提交 PR 前必须通过 npm run check 和 ./test.sh;
  • 上游 PR 不自行修改 CHANGELOG.md,由维护者统一处理。

两份文件的共同点:

工程化不是“多跑几条命令”,而是让别人能判断你的改动为什么存在、影响了哪里、怎样验证。


三、测试金字塔:不要所有东西都用真模型

跳转到“三、测试金字塔:不要所有东西都用真模型”
e2e / 真实 provider
少、慢、贵、受网络影响
▲
集成测试:循环 + 会话 + faux provider
快、稳定、能覆盖完整请求链路
▲
单元测试:纯函数和局部模块
最多、最快、最容易定位

适合测:

  • 截断函数在不同行列限制下的行为;
  • 路径解析和权限判断;
  • 消息转换;
  • 树的分支回溯;
  • 参数 schema;
  • 成本计算。

这些测试不需要模型,也不应该访问网络。

适合测:

  • 一次完整的“用户消息 → 模型工具调用 → 工具结果 → 下一轮 → 最终回答”;
  • 事件顺序;
  • 会话落盘和恢复;
  • 压缩后投影;
  • 中止和重试;
  • 扩展钩子的拦截。

集成测试会运行真实循环和会话,但模型用假的。

真实模型只适合少量验证:

  • 某个 provider 的请求格式是否还能工作;
  • 流式解析是否与真实服务兼容;
  • 认证和端点是否可用。

它不适合作为大多数逻辑的回归保障,因为:

  • 花钱;
  • 依赖网络;
  • 输出不确定;
  • 故障可能来自模型服务,而不是你的代码。

四、faux provider:为什么测试里到处都是“假模型”

跳转到“四、faux provider:为什么测试里到处都是“假模型””

faux provider 是一个内存里的假模型。测试可以预先安排它依次返回:

第一条:一段文字
第二条:一个工具调用
第三条:基于工具结果的最终回答

于是测试可以确定地验证:

  • 循环是否发了第二次请求;
  • 工具参数是否正确;
  • 结果是否进入下一轮;
  • 最终是否停止;
  • 事件顺序是否符合预期。

它没有网络、没有费用,也不会因为模型今天“心情不同”而改变路径。

4.2 为什么不能只用 mock 替换 prompt()

跳转到“4.2 为什么不能只用 mock 替换 prompt()”

如果直接 mock 掉整个会话,你只能证明“mock 被调用了”,却测不到:

  • 真实循环;
  • 真实投影;
  • 真实工具管线;
  • 真实事件顺序;
  • 真实会话写入。

faux provider 的好处是只替换“模型服务”这一层,其他部分都是真的。

4.3 测试 Agent 时断言什么

跳转到“4.3 测试 Agent 时断言什么”

优先断言:

  • 状态变化;
  • 事件序列;
  • 工具调用次数和参数;
  • 会话树结构;
  • 是否出现错误结果;
  • 最终是否 agent_settled。

不要优先断言:

  • 完整自然语言句子;
  • 模型“应该这样措辞”;
  • 某个不稳定的耗时数字。

语气和措辞可以有人工评测,但不应成为脆弱的单元测试。


五、为什么直接跑全量 vitest 是危险的

跳转到“五、为什么直接跑全量 vitest 是危险的”

仓库里同时有普通测试和 e2e 测试。e2e 可能在检测到 endpoint/auth 环境变量时真实调用模型。

这意味着:

你的机器上刚好有 API Key
→ 全量测试里某些 e2e 被激活
→ 真的发出请求
→ 花钱、依赖网络、结果不稳定

所以正确入口不是随手 npx vitest,而是:

  • 非 e2e 全量:仓库根目录执行 ./test.sh;
  • 单个测试:从对应 package 根目录运行指定的测试文件;
  • 修改了测试文件:必须运行它并迭代到通过。

它不是简单执行 npm test,而是先创建一个隔离世界:

  • 临时 HOME;
  • 临时 TMP;
  • 独立的 npm 配置和缓存;
  • 不读取真实 Git 全局配置;
  • 关闭交互式凭据提示;
  • 只继承平台必要环境变量。

这样测试不会污染你真实的:

  • ~/.pi 设置;
  • 登录凭据;
  • 会话文件;
  • npm 缓存。

测试结束后,脚本只删除自己标记过的临时目录,并有防御性路径检查。

test.sh 是 Bash 脚本。在 PowerShell 中直接运行不行,应使用 Git Bash:

终端窗口
cd $PI_SRC
./test.sh

这一步可能比较慢,适合在准备提交或做较大改动时运行。


六、npm run check:八道自动关卡

跳转到“六、npm run check:八道自动关卡”

根 package.json 的 check 依次执行八类检查:

关卡 在防什么 人话解释
Biome 格式、lint、可自动修复的问题 代码风格不统一、明显错误
pinned deps 直接依赖版本漂移 今天和明天安装出不同东西
runtime deps 运行依赖漏声明 本地碰巧有、发布后没有
TS imports 相对导入约定被破坏 构建产物路径错乱
entry graphs 入口体积和依赖图异常 包越打越大或引入不该有的依赖
install lock 安装锁与生成脚本不一致 用户安装时拿到不同依赖
TypeScript 类型错误 接口对不上、字段用错
browser smoke 浏览器环境基本可用 某些包在浏览器打包时直接坏掉

几个容易踩的细节:

  • Biome 会写文件,所以检查后要再看 git status;
  • 出现 warning/info 也不能忽略,仓库要求修到干净;
  • 文档改动不一定要跑代码检查,但只要改了代码,就要按合同执行;
  • 不要用 --no-verify 绕过提交钩子。

提交前钩子会:

  1. 检查 lockfile 是否被意外修改;
  2. 再跑一次 npm run check;
  3. 在相关文件变更时跑额外的浏览器冒烟检查;
  4. 把格式化修改过的已暂存文件重新加入暂存区。

也就是说,npm run check 通过不代表可以随便提交;pre-commit 还会守住依赖和暂存边界。


七、怎样做一次“小而不坏”的改动

跳转到“七、怎样做一次“小而不坏”的改动”

不要一上来就改实现。先留下一个可重复的证据:

输入是什么
期望是什么
实际是什么
最小复现条件是什么

如果是 bug,最好先写一个会失败的测试。这样你改完以后能证明:

  • 旧代码确实失败;
  • 新代码确实通过;
  • 以后别人不会无意中改回去。

7.2 找根因,不贴表面补丁

跳转到“7.2 找根因,不贴表面补丁”

例如:

现象:大文件读取结果偶尔超过上下文
表面补丁:在模型请求前统一截断
根因可能是:某个工具没有使用统一截断工具

根因修复通常会带来更小的长期复杂度。表面补丁可能让今天不报错,却让别的工具继续漏。

一个好的小改动通常包括:

  • 一处根因修复;
  • 一个能捕获问题的测试;
  • 必要时的类型或文档更新;
  • 按规则添加的 changelog 条目(如果仓库要求)。

不要顺手重构无关代码,也不要把格式化产生的几百行噪声混进修复。

7.4 回归测试要指向真实风险

跳转到“7.4 回归测试要指向真实风险”

如果是修 GitHub issue,测试旁应注明 issue 号。测试最好能回答:

如果把这个修复删掉,测试会不会真的失败?

不会失败的“回归测试”只是装饰。


八、Git 卫生:多人/多 Agent 同时工作的现实

跳转到“八、Git 卫生:多人/多 Agent 同时工作的现实”

Pi 仓库的开发规则明确假设:同一个工作目录可能同时有多个 Agent 在改不同文件。

因此:

  • 只提交你自己改的文件;
  • 用显式路径 git add <path>;
  • 提交前先看 git status;
  • 不要使用 git add -A / git add .;
  • 不要使用 git reset --hard、git clean -fd、git stash;
  • 不要 --no-verify;
  • 提交信息使用约定格式:fix(coding-agent): ...。

这些规则不是“洁癖”。在并行工作区里,一条 git add -A 可能把别人的未完成改动一起提交。


九、发布边界:为什么不能随便重跑发布

跳转到“九、发布边界:为什么不能随便重跑发布”

学习阶段不需要真的发布,但应该理解发布流程为什么严格:

固定依赖和模型目录
→ 升版本
→ 更新 changelog
→ 跑 check 和测试
→ 构建产物
→ 打 tag
→ CI 发布和验证

几个关键点:

  • 已发布版本段落不可修改;
  • tag 一旦推出去,不应重新跑发布脚本覆盖它;
  • CI 失败时重试失败的 job,而不是重做整个发布;
  • 发布产物和源码版本必须对得上。

工程化的最终目的,是让“别人拿到的东西”和“你验证过的东西”一致。


打开 test.sh,用自己的话回答:

  1. 为什么要改 HOME 和 TMP?
  2. 为什么不让 Git 读取全局配置?
  3. 为什么测试目录要带标记文件?
  4. 如果没有隔离,测试可能污染哪些真实数据?

运行:

终端窗口
cd $PI_SRC
npm run check

对照输出,把每一段对应到上面的八类检查。检查结束后运行:

终端窗口
git status

确认格式化有没有改动文件。

从 [1.3.0] 的 changelog 里挑一条小修复,看它的 diff,回答:

  • 问题是什么?
  • 根因在哪里?
  • 加了什么测试?
  • 改动有没有超出问题范围?

练习 4:写一份“验证计划”

跳转到“练习 4:写一份“验证计划””

不必真的改代码,先为一个假想需求写计划:

需求:工具输出超过限制时,结果中必须包含完整输出路径。
单元测试:
集成测试:
需要的事件断言:
不需要真实模型的理由:
失败时怎样复现:

能写出这份计划,比盲目跑一遍测试更接近真正的工程能力。


误区 正确理解
模型输出每次不同,所以 Agent 没法测 测结构、状态、事件和不变量,不测固定措辞
全部用 mock 最快 只 mock 模型层,循环和会话仍要真实运行
e2e 最真实,所以应该多跑 e2e 慢、贵、受网络影响,只适合少量兼容性验证
全量测试就是 npx vitest 仓库有专门的隔离入口 ./test.sh
npm run check 只检查类型 它还管格式、依赖、导入、入口图和安装锁
提交钩子可以临时绕过 不应使用 --no-verify;检查失败要修根因
小改动不需要测试 小改动如果没有回归测试,很容易悄悄回来
改了 bug 就一定要大重构 先做最小根因修复,再另行讨论重构
AI 写的代码能跑就行 贡献者必须能解释代码和影响范围

请回答:

  1. 为什么 Agent 测试更应断言事件和状态,而不是完整自然语言?
  2. 单元、集成、e2e 三层各测什么?
  3. faux provider 为什么比 mock 整个 prompt() 更有价值?
  4. test.sh 的隔离解决了哪些真实风险?
  5. npm run check 的八道关分别防什么?
  6. 为什么不能直接跑全量 vitest?
  7. 一个合格的小修复通常包含哪几部分?
  8. 为什么多 Agent 工作区里不能用 git add -A?

建议产出:

  • 一张测试金字塔,并给每层写两个适合的测试例子;
  • 一份隔离测试环境的说明;
  • 一份 npm run check 八道关的对照表;
  • 一个“先写失败测试,再修根因”的完整演练记录;
  • 一段 3 分钟讲解,说明“模型不确定,工程验证仍然可以确定”。

下一站是阶段 9:把七块重新组装成一个自己的 mini agent,并用“能否脱稿解释”做最终验收。


配套学习路线阶段 8 · 基于 v1.3.0(71023aec)