为什么 Agent 越界又做不完:WIP=1 与 feature list 原语

同时开多任务必全败,靠 WIP=1 加可执行验证的 feature 原语锁住边界。

Module C · 第 5 讲

一个 agent 同时做五个功能的下场

给 agent 一份待办:实现登录、加购物车、写订单接口、接支付、补单元测试。一小时后回来看,五个分支都改了,五处都「快好了」,没有一处能端到端跑通。登录差一个回调,购物车少了库存校验,订单接口返回 500,支付还在调 SDK,测试因为前面没跑通而全红。

这不是 agent 偷懒,是它太勤快——它倾向于「同时启动所有任务」,因为每个任务单独看都不难。问题出在注意力的分配方式上。

把 agent 在一个工作周期里能投入的有效注意力记为容量 C。当它同时推进 k 个任务时,每个任务平均只分到 C/k。每个任务都有一个「能把它真正做完」的最低注意力阈值 T。

k(同时任务数)每任务分到与阈值 T 关系结果
1CC ≥ T1 个做完
2C/2可能 < T0~1 个做完
5C/5几乎必 < T0 个做完

注意这里的非线性:不是「做完的数量按比例下降」,而是一旦每份注意力跌破 T,所有任务一起失败。同时做五件事的产出不是「五件各完成 20%」,而是「五件各停在 80%,零件可交付」。半成品的价值约等于零——没跑通的代码不能上线,不能被下一步依赖,还占着你后面调试它的时间。

所以这一讲的核心结论先抛出来:「少做但做完」永远优于「多做但做半」。 而要让 agent 真正照做,光靠在 prompt 里写一句「请专注」是没用的——必须把约束做成它绕不过去的数据结构。这就是 feature list 原语。

框架一:WIP=1 的两条规则

WIP(Work In Progress)= 在制品。WIP=1 是一条调度纪律:

任意时刻,只能有一个任务处于 active 状态。

它配一个量化闸门——VCR(Verified Completion Rate,验证完成率):当前已被验证通过的任务占已开启任务的比例。

概念定义在 harness 里的作用
WIP同时处于 active 的任务数限制 = 1
VCR已 passing 任务数 / 已开启任务数< 1.0 时禁止开新任务
active正在做、尚未验证通过全局至多一个

两条规则:① 同一时刻只有一个 active 任务——把注意力数学里的 k 强行锁死为 1,保证每个任务拿到完整的 C。② VCR < 1.0 时不准开新任务——只要还有任务开了头没收尾,就不许碰下一个。这条直接堵死「越界又做不完」:agent 想开第二个任务时,闸门会拦住它,逼它先把手上这个验证到 passing。

这正是本项目 M1/AGENTS.md 里「选 feature 算法」的内核:优先做 status=failing 且依赖已全 passing 的最低编号 feature,做完一个再选下一个,同一 slice 内做完才进下一 slice。整个算法的输出永远是「下一件该干的事」,单数。

框架二:feature list 是原语,不是文档

WIP=1 要能执行,得先有个可被机器读取的任务清单。这就是 feature list——它是 harness 的基础数据结构(原语),不是给人看的需求文档。为什么强调「原语」?因为 harness 的三个核心组件全都直接依赖它:

组件依赖 feature list 的什么干什么
调度器status 字段算出唯一的下一个 active 任务
验证器verify 命令跑命令判定能否升 passing
交接器三元组全量新会话据此 10 分钟接班

一个 feature 是一个三元组

字段含义例子
行为描述这个功能做什么(用户/外部可观测)「创建订单接口返回 201 并落库」
验证命令怎么算它真的做完(可执行)curl -X POST .../orders → 201
当前状态它现在在状态机的哪一格failing

关键洞察:文档可以被忽略,原语不能被绕过。 这就像数据库里的注释 vs 触发器约束——注释写「请不要插入负数库存」谁都能无视;而 CHECK(stock >= 0) 约束会让违规的 INSERT 直接失败。feature list 要起作用,必须是后者:agent 不读它就无法知道下一步,不更新它就无法宣称完成。把任务清单写进 README 是文档,写进调度器和验证器读取的结构化文件才是原语。

框架三:状态机——通过验证是唯一的升级路径

每个 feature 在一个不可逆的状态机里流转:

not_started → active → blocked ⇄ active → passing
feature 状态机:not_started→active⇄blocked,active 经 verify 通过才升 passing(终态·不可逆)
feature 状态机:not_started→active⇄blocked,active 经 verify 通过才升 passing(终态·不可逆)

两条铁律:通过验证是升到 passing 的唯一路径——agent 不能自己把状态改成 passing,必须由验证器跑完 verify 命令、拿到可执行的成功证据才升级;passing 不可逆——一旦通过就锁定,避免「做完了又被改回去」的反复横跳。这条状态机就是 WIP=1 的执行机制:只要存在 active 任务且它没到 passing,调度器就不会产出新的 active。

完成的证据必须可执行:「代码看起来没问题」不是证据,「curl 返回 201」才是证据。判定标准是——换个人、换台机器、原样跑一遍,结果稳定。

case:本项目的 features.json 怎么把这四件事落地

状态枚举 = 状态机。 约定里写死 status ∈ {pending, in_progress, failing, passing},并配规则「build 产出正确 HTML 且过检查清单才能改 passing;草稿写完只到 in_progress」。多出的 in_progress 正好卡在「看起来做完了」和「验证通过了」之间,防止 agent 把「我写完了」误当成「它通过了」。

verifier 硬闸门 = 原语不可绕过。 约定里最硬的一条:「verify 字段为空 = 不准开工。」这把「可执行证据」前置成开工条件——一个功能如果连「怎么算做完」都说不清,就根本不许开始。看某个 feature 的 verify:

test -f assets/orangebook.css && ! grep -lP '[\x{1F300}-\x{1FAFF}]' assets/orangebook.css

文件存在 + 全文无 emoji,一条命令跑出真假,没有「看起来达标」的余地。

线性切片 slices = WIP=1 的宏观版。 features 被切成 S1→S2→S3→S4 四片,每片有明确的 exit_criteria。规矩是「同一 slice 内做完再进下一 slice,不跳」。这是把「同时只做一个任务」从单 feature 层级抬到 slice 层级:不允许 S1 还没端到端跑通就开 S2。

三段式关联 = 原语自带边界。 每个 feature 带 related / affected / out_of_scope 三段。out_of_scope 明确告诉 agent「这些不是你这个任务的事」,从结构上压制它顺手多做的冲动——它既是上下文路由,也是反越界的护栏。

把这四点连起来看:作者并没有在 prompt 里反复叮嘱「请专注、别越界」。他把这些话全翻译成了 features.json 里的字段约束和 AGENTS.md 里的选取算法。约束是触发器,不是注释。

可操作做法:给你自己的项目装上这套闸门

  1. 建单一事实源文件。 把任务清单挪进一个结构化文件,它是调度器和验证器唯一读取的真相源。
  2. 每个 feature 写满三元组。 行为描述 + 验证命令 + 当前状态。验证命令为空的,不准进清单、不准开工。
  3. 状态只允许这几格,且 passing 由验证器盖章。 agent 写完代码最多只能标 in_progress,必须跑通 verify 才升 passing
  4. 执行 WIP=1。 开工前先看清单:有没有 active 任务还没到 passing?有就先把它做完。VCR 不到 1.0 不开新任务。
  5. 可执行证据落成命令。 把「做完了吗」写成一行能跑的命令——curlpytestgreptest
  6. 三段式标边界。 out_of_scope 是反越界的护栏,related 是上下文路由。
  7. 任务多到一屏放不下时,切片。 分成线性 slice,每片定 exit_criteria,做完一片再进下一片。

收口

Agent 越界又做不完,不是因为它不够聪明,而是因为它的注意力被你默许地摊薄了。修复方式从来不是更长的叮嘱,而是更硬的结构。

同时做五个功能的产出,往往是零个能跑的功能。少做一件,做完它,胜过开五件、扔五件。