让AI自主开发跑起来:先写验收,再拆任务

最近我在尝试把一批开发需求交给多个 AI 执行。这批需求的一部分来自会议录音转写:里面有产品边界、技术方案、人员分工,也有口误、同音字和讨论到一半又被推翻的想法。最直接的做法是把转写总结成任务列表喂给 AI,等它写完再补测试和自测。
实际跑下来,这条路有两个绕不过去的问题。一是需求本身不可靠:根据一个同音字直接换 Owner,任务 DAG 会跟着变,最后很难说清楚这个决定是谁做的;一句“先这样做”也可能只是为了继续讨论,不是结论。
二是验收不可靠:测试标准也是实现者自己写的,只要实现遇到困难,它就可能下意识选择更容易通过的验证方式。最后代码和测试都绿了,最初的需求有没有满足反而说不清楚。
还有一个更隐蔽的误区是重提示词、轻验收。提示词只能让 AI 更了解我要什么,不能自动回答“它做完了吗”。任务背景和注意事项写得再完整,完成判断仍然需要一个外部标准。
这篇文章记录的是后来调整出来的完整顺序:先冻结验收,再拆任务,用能失败的门禁拦住违规,Skill 先跑通纵切再拆细。四件事在同一条链上,这里按顺序做个记录。
先写验收:把需求变成一条可回溯的链
会议内容不能直接变成任务,我先给它分三类:
- 已确认:在会议后半段被重复确认、也和此前决定一致,可以进入正式需求。
- 工作假设:方向大致明确但技术上还没有证据,比如某项服务端能力“应该可以”搬到本地。这类进 Spike,写清楚退出条件,不能写成既成事实。
- 待确认:人名转写不清、仓库归属没定、时间点只有口头承诺。这类进决策台账,不生成唯一负责人和硬依赖。
这一步看起来慢,实际避免了后面更大的返工。
分类之后还要处理模糊词。比如“离线运行”,有人理解为不依赖业务平台,有人理解为完全断网,还有人只是想表达客户端可以在本机执行部分流程。
最后我把它改成一个操作性定义:停止业务平台以后,本地流程仍能从输入运行到最终产物;模型、素材和网络等外部依赖单独列出;测试时记录平台请求数,而不是凭感觉判断“好像没调用”。一个词不能被观察,就还不适合进入验收标准。
分类和定义之后,才轮到那条真正的 Spec 链:需求先分配稳定的 Requirement ID;需求下面写可观察的 Acceptance Criteria;每条验收再由 Test ID 覆盖;测试写清楚输入、程序、Pass/Fail oracle 和证据位置;最后才生成可以领取的原子任务。
任务完成以后,还要留下实际 diff、执行命令、退出码、fixture 版本和结果文件,这样才能从一段代码反向追到它在实现哪条需求、用什么标准验收、证据在哪里。中间缺一层,AI 就有解释空间;在“是否完成”这个问题上,解释空间越大,结果越不可靠。
这里我走过一个弯路。一开始提 tests-first,第一反应是每个任务都要先写失败的单元测试,后来发现实际项目里很多工作并不适合这样处理。
数据结构和 API 可以先写兼容 fixture 或 Schema validator;UI 可以先冻结 loading、empty、error、offline 等状态矩阵和截图程序;安全功能先准备越权、重放和恶意输入;发布任务先定义升级、失败回退和数据兼容程序。
测试先行的重点不是测试文件必须先出现,而是成功和失败的边界必须先出现。“页面看起来正常”无法自主验收,“所有规定状态都有截图,键盘焦点可达,阻断级可访问性问题为零”才接近可执行标准。
“升级没有问题”也不够,需要明确候选版本在哪些兼容单元格通过,失败后能否回到当前版本。
测试标准冻结以后,马上会遇到一个反对意见:如果测试本身写错了,岂不是把 AI 锁在错误方向上?这个问题不是假问题,真实代码经常会推翻设计阶段的假设,过早冻结一个错误 oracle,可能比没有 oracle 更浪费时间。
所以任务状态里必须有一条正规退路:当代码事实否定冻结合同,或者现有测试根本无法区分正确和错误时,任务进入 NEEDS_SPEC_UPDATE。实现者留下反例,说明哪条需求、接口或者测试假设与代码事实冲突,由负责人修改权威规格、重新冻结,再把任务放回 READY。
没有这个状态,AI 为了完成任务,最容易做的事情就是改测试、过滤退出码、加一个 mock,或者把失败路径排除掉。表面上进度更快,实际上只是把问题藏起来。
拆任务阶段,很多测试还没有真正实现,只能先标成 planned。这个状态有价值:它允许团队先冻结“以后要怎么证明”,不用假装测试已经存在。如果完全不允许 planned,另一种风险是大家先写实现、结束时再补测试合同——那时的测试很容易按现有实现设计,只覆盖已经能跑通的路径。
问题不在 planned 本身,而在它会不会被错误地向上汇报。我把测试状态分成四层:
- 测试合同已经定义:有 Test ID、覆盖的验收项、程序、输入和 Pass/Fail oracle。
- 测试程序已经实现:命令真实存在,正例和负例都能执行,失败返回非零。
- 测试在指定环境运行过:执行记录包含 commit、环境、fixture 版本、退出码和 artifact。
- 证据足以支持当前范围的结论:mock transport 通过只能说明适配层合同成立,不能说明真实在线服务可用。
前两层解决“准备怎么证明”,后两层才解决“这次是否真的证明了”。把 planned 当 passed 报,积累的其实是证据债务:只跑成功路径不补约定的负例、外部账号不可用就拿 mock 标 DONE、执行了测试但没保存环境和 fixture 版本,都是常见的积累方式。
还有一种更隐蔽的:结构 validator 通过以后被当成业务质量通过。结构门禁只能发现缺失 ID、坏链接、依赖环和未覆盖的 AC,它不知道页面是否好用,也不知道生成内容是否正确。
“以前通过过”也是一个容易遗漏的状态。测试在旧代码、旧模型或者旧 fixture 上通过,不代表当前版本仍然通过。合同变化以后,原来的 passed 应该变成需要重跑,而不是永久绿色。
每个任务都保存命令、环境、fixture、退出码和 artifact,会不会把开发拖进流程负担?这个担心成立,我的处理是让证据和风险匹配:纯文本和静态规则保存简短 validator 输出,接口和 Schema 保存版本化 fixture,并发、恢复和发布必须保留真实环境记录。重点是最小充分证据,不是日志越多越好。
这套做法不会自动让 AI 写出更好的代码,也还没有数据证明它能提升多少效率。它先解决的是另一个更基础的问题:当 AI 说“完成了”时,我们终于知道应该去哪里检查。
再拆任务:工作包和原子任务不是一回事
验收冻结之后才轮到拆任务。这一步我也修正过一次判断:一开始我把目标、背景、实施步骤、验收清单都写进一个工作包,觉得信息够全就可以整个交给 AI,后来重新检查才发现,问题恰恰出在“信息很全”——它描述的是一个领域结果,不是一次可以独立完成的改动。
以一个持久任务运行模块为例,工作包里写了并发领取、租约续期、进程被杀后的恢复、取消竞争、有限重试、失败产物清理。人看完会觉得信息很全,AI 也确实能马上开始写代码。
但每个小点都有自己的失败方式:并发领取需要数据库事务测试,租约需要可控时钟,进程恢复需要故障注入,重试需要区分临时错误和确定性错误。一次性交给 AI 会出现两个问题:一是范围自然膨胀,它为了让主流程跑通,可能顺手修改数据结构、接口和测试基础设施,代码很多,但很难确认每处修改是否必要;二是没有单一的完成信号,测试通过了一部分算不算完成、某个恢复场景失败是继续修还是改设计,任务本身回答不了。
后来我把任务改成两层。工作包继续保留,负责说明共同目标、输入事实、整体风险和跨模块关系,是给负责人和评审者看的;没有这一层,拆细以后 AI 很容易只顾局部测试,不知道为什么要做。
工作包下面再拆原子任务。一个原子任务只追求一个主要可观察结果,写清楚允许修改和禁止修改的文件、前置依赖及其完成证据、可观察的验收结果、测试程序和 Pass/Fail 标准、失败时应该停止还是回到规格修改、完成后给下游留下什么。这样 AI 领取的任务,从“把这个模块做好”变成了“让这一条不变量在指定测试中成立”。
目前的判断比较朴素:一个任务要有一个主要结果,能独立验证和回滚,预计一次连续工作可以完成;跨数据库、API、客户端和发布的需求,按合同、生产者、消费者、接线和端到端验收拆开。
两层模型引入了一个我没想到的问题。我分别检查了工作包 DAG 和原子任务 DAG,两张图都没有环,按直觉调度不会有问题;把两层依赖放在一起展开,居然出现了死锁。
最小例子是这样:工作包图规定 B 必须等 A 完成,只有 A 指向 B,没有环。原子任务里,A 有任务 A2,B 有任务 B1,后来为了复用 B1 产出的一个接口,给 A2 加了一条依赖:A2 等 B1。原子图单独看也没有环。
但工作包 A 只有在 A2 完成后才算完成,B1 又因为父包 B 的启动条件,必须等整个 A 完成。实际关系变成:A2 等 B1,B1 又等 A2。两张独立 DAG 都通过了,组合出一个两节点环,只分别检查两份 manifest 的调度器会把任务永远留在等待状态。
这不是理论边角:任务拆解持续调整时,很容易为了精确复用某个下游产物,加一条跨工作包的原子依赖,刚好反向穿过父包完成门。
正确做法是展开父包语义:B start_after A 意味着 B 中每个受父包门限制的原子任务,都隐含等待 A 的全部原子任务。展开这些隐式边,加上原子任务自己的 start_after,再对组合图做拓扑排序或强连通分量检查。检测到环时还要输出能看懂的最短路径,类似 A2 → B1 → A2,开发者才能判断该删原子反向依赖、下沉父包依赖,还是把共享合同提前拆出来。
最直接的反对意见是:两层这么复杂,为什么不把所有任务放进一张平面 DAG?如果任务不多、由同一个小团队管理,我赞成用一张图。但节点接近两百以后,负责人要的是工作包级风险和交付结果,Agent 要的是文件、测试和精确前置,压在一张图里人类评审会很困难。
两层模型不是免费午餐:选择了它,就必须承担组合校验和父包完成聚合的复杂度;没有能力维护这些门禁,宁可回到单层,也不要让两层依赖只存在于文档约定里。
依赖图只解决时间顺序,不解决文件所有权。两个任务可以在 DAG 上完全独立,却同时修改同一个入口文件、Schema 或构建配置,Git 冲突只是最后看到的现象。AI 写代码更快以后,这个问题被放大——多个 Agent 可以在几分钟内各自完成一套合理实现,然后在同一个文件里撞车。
所以原子任务里声明的 allowed_paths 不只是代码审查字段,也是并发控制的一部分。调度器在任务启动前就可以检查:两个并行任务是否写同一目录,是否有人要改共享 Schema,是否越过自己的仓库根。
发现重叠后,可以调整任务边界、增加依赖,或者只允许其中一个任务写共享文件——解决的是“应该由谁改”,不用等到事后研究“怎么把两份代码拼起来”。
一个具体修正:我最初有一个“实现完整客户端状态”的任务,允许修改页面、Adapter、Schema 和测试,看起来目标完整,实际上谁领取都会和其他任务冲突。后来拆成四段:合同 Owner 先冻结状态 Schema;Adapter 任务只生产归一化事件;页面任务只消费 view model;最后一个接线任务负责集成测试。
四段各拥有不同路径,只有 Schema 和 fixture 通过显式依赖串起来。拆完以后,allowed paths 还能反过来验证任务是否原子——如果一个小任务仍需要同时写四个所有权域,大概率说明它还该继续拆。
路径还必须相对于正确的根。这次遇到一个隐蔽问题:任务里写了 docs/** 或 src/**,但客户端位于一个开源项目的自有 Overlay 中,不说明任务根,Agent 很容易把相对路径理解成仓库根,直接改到上游同名目录。我们把 client 任务路径统一解释为相对于 Overlay 根,并把上游管理区列为全局禁止路径。
路径隔离也解决不了语义冲突。两个 Agent 写不同文件,仍然可能对同一个字段做出相反理解:一个把 completed 当作媒体已生成,另一个当作人工审核完成,Git 不会冲突,产品却已经冲突了。所以路径隔离还要配合合同管理:一个合同只有一个最终 Owner,共享 Schema 先冻结再实现,生产者和消费者使用同一 fixture。路径管物理并发边界,合同管语义并发边界。
把任务切得这么细,会不会把开发变成工单流水线,让 AI 看不到完整系统?这个担心成立。如果把一个业务流程拆成几十个只有文件名和一句话的小任务,AI 每次都要重新恢复上下文,局部任务都通过了,系统仍然可能接不起来。所以我没有删除工作包,也没有追求越细越好:原子任务解决执行问题,上下文靠工作包保留,两层怎么接靠端到端测试兜底,缺一个都不行。
门禁先证明自己会失败
任务拆完、边界划清,还需要最后一道东西保证规则真的生效。以前做项目门禁时,我最关心的是正常代码能不能通过,命令返回 0、报告显示绿色,看起来规则就已经生效了。但一个永远返回 0 的脚本,同样可以让正常代码“通过”——如果没有故意制造违规输入,我们其实不知道它会不会拦截任何东西。
所以门禁的第一项验收不是正常代码能通过,而是违规输入一定失败。
这个习惯不是从门禁开始的。之前审查一个 AI 项目时,按需求文档逐项检查,很容易得出“功能基本都有”的结论:页面有了,接口有了,流程能跑,测试也写了。但换一个方向,专门去找能够推翻这些结论的证据,结果完全不同:页面虽然存在但没有接入真实生产链路,流程虽然能运行但输入数据来自 fixture,测试虽然不少但验证的是 mock,异步任务虽然提交成功但进程退出以后无法恢复。
从那以后,审查一个问题时我会先问:有什么证据可以证明,我们现在的判断是错的?门禁就是这个问题在 CI 里的形态。
证明门禁会失败,需要负向 fixture。比如制造一个未跟踪文件绕过普通 diff、把锁文件的 commit 和 tree 改错、让子检查失败但总脚本错误地吞掉退出码。每个 fixture 都应该触发预期错误;只报“检查失败”也不够,还要能定位是哪条合同被破坏,否则 Agent 很可能通过删除检查或放宽匹配来让它重新变绿。
这些负例要当成 validator 的回归测试来维护:以后门禁规则变化,先看旧负例是否仍然失败,再看批准基线是否通过。这和安全测试很像——没有攻击样本,只证明了正常用户能登录,不能证明未授权用户进不来。
前面说的组合 DAG 检查也是一样。我专门加过一个负例,把一条跨包依赖反向接回父包完成门,确认 validator 非零退出,才敢相信这个检查真的在拦东西。
门禁背后其实是一个更普遍的习惯:工程证据有强弱之分。需求文档里写了、代码文件存在,这类证据只能证明“设计过”或者“实现过”;fixture 或 mock 环境能跑通进了一步,但不能外推到真实服务;最接近完成的证据是长时间生产运行仍然稳定。
用弱证据宣称强完成度,中间的差距就是项目风险。负向 fixture 的意义也在这里:它把门禁的证据从“脚本存在”这种弱层级,提升到“违规一定失败”这种可以重复检验的层级。
反证也不是写一份问题报告就结束。一个被确认的问题,至少要转化成下面某一种东西:一个自动化测试、一条发布门禁、一条接口契约、一个明确的非目标、一个需要延期的需求,或者一个有负责人和截止时间的风险项。只存在于评审记录里的问题,很快就会被遗忘。
真正困难的情况是业务需求确实需要越过当前红线。最省事的做法是在脚本里加一个文件 allowlist,或者临时跳过某个检查,一次两次以后,门禁会变成“默认禁止,但历史上这些文件都可以例外”。更干净的处理是进入规格变更:说明现有接口为什么不够、长期维护成本是什么,例外是产品和成本决策,不应伪装成普通修复。
门禁也可能过度。保护的边界必须来自真实架构目标,不能为了“严格”而严格:对低风险格式规则可以自动修复,对上游源码、密钥、发布制品和数据迁移则应该严格阻断。判断标准看失败后的代价,不看规则数量。
同样的思路也适用于审核环节,这里有一条我从多 Agent 协作里借来的规则:审核者不能沿用生成者的上下文。让同一个模型在同一个上下文里检查自己的方案,它大概率只会补充细节,很难真正怀疑最开始的方向。
所以方案和初始需求文档必须放到不同上下文中交叉审核——可以让不同的模型分别审核,也可以创建相互独立的 subagent。关键不在数量,在于审核者不继承生成者的默认假设。
Skill 先跑通完整纵切,再按证据拆细
链的最外层是 Skill 的组织方式。刚开始设计视频生产 Skill 时,很容易列出一长串目录:脚本 Skill、planning Skill、storyboard Skill、image Skill、audio Skill、render Skill、校验 Skill。每个名字都很清楚,看上去也很模块化。
但真的把流程串起来以后才发现,Skill 拆得越细,Agent 需要在越多边界之间传递上下文、状态和确认结果。最终并没有更清晰,反而更容易重复调用和丢状态。
这里有一个容易混淆的地方:API 和 Tool 适合做细,image.generate、artifact.validate 都可以有明确输入输出,方便单独测试;Skill 的粒度应该围绕用户目标。用户说“根据这段脚本做一个视频”,他并不想分别管理八个技术步骤。
如果用户明确说“只重做这张图”,才直接进入对应的功能 Skill。此时导演不能再同时启动一次完整流程,否则同一个意图会产生两组副作用。Tool 按稳定 capability 拆,Skill 按可独立验收的用户目标拆,两者不能因为名字相似就使用同一粒度。
第一版可以只有一个导演 Skill 和一个粗粒度本地创作 Skill,把流程从 intake 一直跑到 validation。这个纵切的意义在于尽早暴露跨阶段问题:输入结构能否传递,产物能否登记,某一步失败后从哪里恢复,Agent Session 删除后项目是否还存在。
如果连完整链路的状态真相都没有搞清楚,提前拆十个 Skill,只会把问题分散到十个目录里。
粗粒度只粗在用户目标上,不等于把所有实现写进一个文件:Skill 仍然只负责编排,下面的 typed tool、capability package、Manifest 和脚本要保持明确边界。
纵切还必须允许失败。可以故意拿走字体、让媒体工具返回非零退出码,或者在中间阶段终止进程,然后检查系统有没有留下可解释的 checkpoint。只跑一次顺利流程,最多证明演示脚本写对了;能在边界失败并恢复,才说明 Skill、Tool 和 Manifest 的职责真的分开了。
纵切跑通以后,再根据真实证据决定拆不拆。原来列过不少判据,实际用下来真正起作用的主要是两条:用户是否会明确单独发起这个目标;独立以后能否减少重跑,而不是增加路由冲突。例如“只修改一张图片”是独立用户目标,适合单独成 Skill;某个步骤只是内部 JSON 转换,就更适合留在 Tool 里。
有一种常见意见是模块越小越容易复用,先拆细才符合软件工程。这对纯函数和 Tool 经常成立,对带上下文和副作用的 Agent Skill 不一定成立:每多一个 Skill 边界,就多一次路由判断、上下文裁剪、权限检查、失败传递和幂等处理,合同没稳定之前,所谓组合只是把不确定性组合在一起。
也要防反方向的问题:不能因为一个 Skill 已经能做完全部事情就永远不拆。出现独立权限、独立确认或高频局部修改时,继续维持一个万能 Skill,路由和测试会越来越重。粗粒度是验证顺序,不是最终教条。
目前这个纵切证明到哪里,也值得说清楚:确定性工程纵切已经可以把 planning、image、audio、render 串起来,产出可解码视频,并验证 Manifest、checkpoint 和 Session 删除后的恢复。这说明“一个导演加粗粒度纵切”的验证方式可行。
但确定性图片和音调不代表真实 Provider 的内容质量,自动探测也不代替人工播放;服务端 Promote 和 live Local/Server parity 尚未完全证明,所以不能把这个纵切写成已经达到生产同品质。它的作用是验证架构和测试边界,不是提前宣布最终产品完成。
下一步
目前这批 Spec 通过了 Requirement、任务、测试追踪和组合依赖图的结构检查,近两百项原子任务能被机器读取,但绝大多数业务测试仍处于 planned——结构校验证明的是任务体系能被检查,不代表业务已经完成。
接下来我打算先盯三个数:planned 还剩多少、已实现但未执行的有多少、证据已过期的有多少;一个阶段长期有大量 planned 测试,项目状态就不该显示绿色。等原子任务真正跑起来,再看首次验收通过率、返工次数和并行冲突。现在能确认的只有一件事:把一个没冻结验收的工作包原样交给 AI,风险大概率比我们想象的高。