为什么跑通端到端才算完成,且每次会话都要留干净状态
单测过≠任务完成,完成判定必须外部化跑通 E2E,并以五维清洁状态收尾对抗熵增。
一个被反复宣告的「完成」
你大概率经历过这一幕:agent 写完代码,跑了单测,绿灯,然后郑重宣布「功能已实现,全部通过」。你信了,合并了,上线了——结果生产环境第一个真实请求就炸了。回头一查,单测里 mock 掉的那个下游服务,接口字段名对不上。
这不是某个 agent 偶尔犯的错,而是 agent 系统的结构性过度自信。它对「完成」的判断停留在它能直接观测的层面:语法没报错、单测变绿。但「语法对」和「单测过」与「任务真的做完了」之间,隔着两道它跨不过去的鸿沟。
本讲是 Module C 的收官讲。这一讲收口在输出侧的两件事:怎么判定一个任务真的完成,以及会话结束时怎么把状态留干净。这两件事都指向同一个底层事实:不能信 agent 对自己工作的主观感受,要靠 harness 用确定性的手段去外部验证。
框架一:三层终止校验,每跳一层都丢信息
「完成」不是一个布尔值,而是一条要逐层爬升的阶梯。每一层验证的对象不同,能发现的问题也不同:
| 层级 | 验证什么 | 典型手段 | 能抓到的问题 | 抓不到的问题 |
|---|---|---|---|---|
| L1 语法静态 | 代码合不合法 | 编译 / lint / 类型检查 | 拼写、类型、未导入 | 逻辑错、跑起来行为不对 |
| L2 运行时行为 | 单个组件跑对没 | 单元测试 / 函数级断言 | 单函数逻辑缺陷 | 组件之间怎么交互 |
| L3 系统级 E2E | 整个系统作为整体对外表现对没 | 真实启动、真实输入、观察真实输出 | 接口不匹配、状态传播、资源泄漏、环境依赖 | (这是最后一道,逃不掉) |
关键洞察在最后一列:每往上跳一层,下层验证持有的信息就会丢失一部分。L1 知道每个 token 合法,但不知道函数逻辑对不对;L2 知道每个函数单独跑对,但因为它把依赖 mock 掉了,恰恰不知道这些函数拼在一起会发生什么。agent 的过度自信,本质就是拿低层级的通过,去冒充高层级的完成——它停在 L2 绿灯就宣布胜利,而真实世界的缺陷大量藏在 L3。
框架二:单测为什么有系统性盲区
不是单测没用,而是单测的隔离性——它最大的优点——同时是它最大的盲区来源。为了让单测快、稳、可重复,我们把被测组件从真实环境里抠出来,用 mock 把它周围的世界都换成假的。于是下面这四类问题被结构性地挡在了单测视野之外:
| 盲区类型 | 单测为什么看不见 | 只有 E2E 才暴露的现场 |
|---|---|---|
| 接口不匹配 | 两端各自的单测都用了自己想象的接口形状 | 真实拼接时字段名/类型/可空性对不上 |
| 状态传播 | 单测里每个组件状态是手工摆好的 | 上游真实输出喂进下游,中间状态被污染 |
| 资源泄漏 | 单测进程短命,跑完就退 | 长时间运行后连接/句柄/内存耗尽 |
| 环境依赖 | 隔离环境把配置、时区、路径都假设成理想值 | 真实环境缺变量、缺权限、路径不存在 |
更深一层的价值是:E2E 不只是事后检测缺陷,它会反过来塑造 agent 的编码策略。一个知道自己最终要过 E2E 的 agent,在写代码时就会主动考虑组件怎么对接、状态怎么流动、资源怎么释放——因为它清楚 mock 蒙混不过去那一关。验收标准前置,行为就前移。这就是为什么 E2E 闸门要在开工时就让 agent 知道,而不是写完才亮出来。
框架三:完成判定外部化 + 可观测性是架构属性
既然 agent 对自己的判断不可信,结论就很直接:完成判定权必须从 agent 手里拿走,交给 harness。把它落成职责分离:规划 agent 定义「做完」长什么样(验收标准、E2E 场景)→ 生成 agent 写代码满足它 → 评估 agent / harness 独立跑验证,只认客观信号。三者分离后,缺陷率能从很高降到接近 0——因为没有任何一个角色能用「我觉得做完了」蒙混。
当 E2E 红了,下一个问题是「为什么红」。如果你得靠加 print、重跑、猜,那么会话时间的 30%–50% 会耗在重复诊断上。可观测性不能是「出事再补日志」,它得是 harness 设计时就长进去的结构,分两层:运行时可观测(系统做了什么:每步输入输出、状态迁移、错误现场)+ 过程可观测(为什么该接受/拒绝这次结果:验收标准、评分标准、判定依据)。两层都在,诊断才从「猜」变成「读」。
框架四:清洁状态五维,会话结束的必备清单
任务通过了还没完。会话结束时留下的状态,决定了下一次能不能干净接手。熵增是默认,而「以后再清理」几乎等于「永久放弃」。所以会话收尾要过这张五维清单:
| 维度 | 验收信号 | 不做的代价 |
|---|---|---|
| 构建通过 | 干净 checkout 能 build 成功 | 下次开局就在修别人的半成品 |
| 测试通过 | 全套测试绿,含 E2E | 不知道哪些是新引入的红 |
| 进度已记录 | 进度文件写明做到哪、下次入口 | 上下文全丢,靠考古重建 |
| 临时工件清理 | 调试产物/scratch 文件/死代码清掉 | 仓库被噪声淹没,信噪比崩塌 |
| 启动路径可用 | 文档里的「如何跑起来」当前就能跑通 | README 撒谎,新人/新 agent 卡在第一步 |
case:本课程站自己的 verify 纪律
完成判定被外部化成一段脚本,而不是一句宣告。 项目里 scripts/verify.sh 是端到端验证脚本,跑 6 项检查:① build 真的产出 site/index.html;② 首页含双模块和侧边栏;③ 讲义页生成、且 fixture 里列的每个期望标题都命中;④ 表格真的渲染成了 <table>;⑤ 全站 grep 无 emoji;⑥ 导航链接和样式引用就位。注意这些检查全是对最终产物(site/ 下的 HTML)做的——不是问 build 脚本「你成功了吗」,而是去构建出来的页面里验真。这正是 L3 系统级 E2E。
passing 是一道由 E2E 把守的闸门。 features.json 里每个 feature 的状态是 pending → in_progress → passing。规则写死在 CLAUDE.md 的 verify 纪律里:草稿写完只能到 in_progress;只有 build 产出正确 HTML 且过完整检查清单,才允许改成 passing。「写完了」和「通过了」是两个状态,中间隔着 verify.sh 的退出码。
fixture 先于代码,是验收标准前置的具体形态。 第 1 讲的 markdown 不是「随便写的样例」,它是 build 脚本的 fixture,先于脚本存在;fixtures/first-lecture/expected-headings.txt 里列的标题,就是 verify 第 ③ 项要逐条命中的验收标准。脚本还没写,验收标准已经在那等着了——agent 写 build 脚本时就知道终点长什么样。
收尾必更新进度,是清洁状态的强制动作。 M1/PROGRESS.md 倒序记着每个 session 做了什么、踩了什么坑、核查结果,顶部 active_feature/slice/更新日期一眼可见下次入口。CLAUDE.md 第一条入会顺序就是「先读 STATUS」——这套记录不是写给档案馆的,是写给下一次开局的自己的。
一个值得点名的诚实细节:某次浏览器截图预览被扩展权限挡住了,项目没有假装「肉眼核查通过」,而是在进度里明确写下「客观验证以 verify.sh 为准」。这正是不信主观感受、只认确定性信号的纪律。
可操作做法
- 写 verify 脚本时,把断言打在最终产物上,别打在中间过程上。 去成品里 grep / 启动 / 发真实请求验真。退出码就是完成判定。
- 让「写完」和「通过」是两个不同状态。 用单一事实源记 status,规定只有 E2E 脚本绿了才能进 passing。
- 验收标准前置,最好做成 fixture。 开工前就把期望输出落成文件,让 agent 从第一行代码起就知道要过哪一关。
- 可观测性当架构设计,不当事后补救。 运行时 + 过程两层都要。
- 会话收尾过五维清单,一项不漏。 把进度写进确定性文件,并在文件开头标好「下次入口」。
- 遇到验证被环境挡住,诚实记录退路,不要伪造通过。
收口
代码会骗你,单测会骗你,agent 对自己的感觉更会骗你;只有一个干净环境里跑通的端到端,和一份下次能照着开局的清洁状态,不会骗你。
完成不是 agent 说了算,是 harness 验出来的;交接不是「我尽力了」,是别人接得住。熵增是默认,所以每一次「以后再清理」,都是一次永久的放弃。