把 AI Agent 关进流程的笼子里
把 AI Agent 关进流程的笼子里#
面向团队工程师的实践分享:如何让 Claude Code 在遗留项目上稳定产出高质量代码——从背景、设计原理、实现细节、日常实践,到诚实的局限
一、背景:为什么直接让 AI 写代码,在遗留项目上会翻车#
如果你只用 AI Agent 写过新项目的原型,你会觉得它无所不能;但只要让它改过一个有五年历史的生产代码库,你大概率经历过下面这些时刻。
你让它加一个导出筛选功能,它顺手优化了旁边一段看不惯的旧代码。那段代码确实丑,但丑陋之下承载着三个早已无人记得的隐性约定,测试没有覆盖,问题直到上线才暴露。
或者,会话开始时它还清楚地记得不要触碰 billing 模块,两小时后,上下文里塞满了探索记录和报错日志,它开始遗忘规则、混淆文件名,重复犯下已经被纠正过的错误。会话越长,它越不像最初那个可靠的协作者。
还有一种失败更隐蔽:实现写不下去时,它回头削弱了测试断言,像是一个精确的相等断言悄悄退化成非空检查。测试全绿,CI 通过,功能却是错的。它并非有意作弊,只是在忠实地优化让测试通过这个目标,而我们从未把测试不可修改变成一条硬约束。
最后是理解层面的偏差,你用一句话描述需求,它自行补全了所有模糊之处的细节,然后以极高的效率,把一个偏离了十度的理解实现得完整而精致。返工的成本,往往比自己从头写还高。
这四类失败指向同一个根因:Agent 缺的不是能力,而是约束结构。人类工程师在遗留项目上的谨慎,来自踩过的雷、承担的责任和盯着的评审;Agent 没有这些,它拥有的只是你给它的上下文与规则,而纯提示词形式的规则,在长会话与目标压力之下,是会被遗忘和变通的。
因此我们的思路不是打磨更精巧的提示词,而是搭建一套流程,让正确的行为成为结构上的必然,让错误的行为在物理上不可达。这套流程由两个经典方法论衔接而成:SDD(Spec-Driven Development,规格驱动开发)负责前半程,把需求转化为可执行的规格;TDD(Test-Driven Development,测试驱动开发)负责后半程,把规格转化为受测试保护的代码;再借助 Claude Code 的原生机制——CLAUDE.md、skills、subagents、hooks——将其固化为一条自动化流水线。
二、流程总览:六个阶段,两道防线#
前三个阶段产出三份文档,存放于仓库的 docs/specs/<需求名>/ 目录下,随代码一起提交。
需求文档采用 EARS 语法书写验收标准:每一条形如 WHEN〈触发条件〉, THE SYSTEM SHALL〈可观测行为〉,逐条编号。这种句式的价值在于强制翻译,诸如让体验更好一点之类的模糊表述,在 EARS 面前无处遁形,而写不出 EARS 的需求,本质上是还没想清楚的需求。
设计文档做两件事,现状分析与变更点清单。变更点精确到文件与函数,每一项都标注它所服务的需求编号。这一步最重要的纪律是反向检查,即如果存在某个设计点对应不到任何需求编号,那就是范围蔓延,直接砍掉。
任务文档把设计拆解为不超过半天粒度的原子任务,每个任务携带涉及文件、需求编号与完成标准。排序遵循硬规则:为将被修改的遗留模块补齐特征测试的任务排在最前,每个功能的测试任务先于其实现任务。
后三个阶段按任务循环执行经典的 TDD 三步:先写一个必然失败的测试,再写让它通过的最小实现,最后在测试的保护下整理代码结构,每个任务收尾时提交一次。
两道防线贯穿全程:软的一道是质量门,每个阶段有一份检查清单,由独立的审查子代理执行,不通过则打回;硬的一道是 hook,在两个最关键的位置做物理拦截,下文详述。
三、设计原理:这套流程为什么有效#
表面上看,这不过是一个文档偏多的开发流程。但每一个设计选择,都在针对性地对抗第一节中的某个具体失败模式。
规格文档是 Agent 的外置持久记忆。 Agent 没有跨会话记忆,上下文窗口就是它的全部世界。三份文档入库之后,任何新会话、任何新的 Agent 实例,只要读完文档就能获得完整且一致的任务认知。这直接化解了长会话退化与中断后失忆两个问题。换句话说,文档不是流程的副产品,文档本身就是流程的状态机。
每个任务一个全新上下文,把上下文卫生从自觉变成结构。 TDD 循环采用编排器与执行器分离的架构。主会话中的 /tdd-next 命令扮演编排器,自己不写一行代码,只负责从任务清单领取任务、派发给一个全新的 task-executor 子代理、验收结果、汇报进度、再领取下一个。执行子代理每次都是冷启动,从文档中读取全部所需信息,完成一个任务后即告消亡。于是理想情况下,第十七个任务与第一个任务的执行质量完全一致,因为它们的起点一模一样。
验收只认证据,不认汇报:Agent 声称任务完成,有时属实,有时只是它的美好愿望。编排器的验收因此定为四查:提交历史中是否存在带任务编号的 commit;任务清单是否已更新勾选;修改范围内的测试是否真正全绿;diff 是否仅包含任务声明的文件。任何一项对不上,循环立即停止并如实报告,而不是自行圆场。
关键规则用 hook 物理执法,不依赖提示词自觉:这是整套流程中最重要的一个设计决策。提示词约束在目标压力下是软的,而 hook 返回的 exit 2 是硬的。我们只在两个最致命的位置设置了物理关卡。其一拦截提交:执行 git commit 之前强制运行修改范围内的全量测试,不通过则阻断——未经测试验证的代码在物理上无法进入版本历史。其二保护测试:TDD 一旦进入最小实现阶段,任何对测试文件的修改都会被拦截。测试在编写阶段定稿,实现阶段只读,削弱断言以求通过的那条路被彻底焊死。如果测试本身确实写错了,唯一的正规出口是停下来向人说明,由人确认后手动解锁。
特征测试是遗留项目的专属安全网:对将被修改的旧代码,先编写特征测试(characterization tests),把现有行为原样固化为断言——无论这行为看上去多么怪异,先锁住再说。此后的任何改动若无意间破坏了旧行为,特征测试会第一时间变红。这是对改动引发连锁故障的直接解药,也是整套流程中针对遗留项目最关键的一条纪律:先锁行为,再动代码。
流程自我进化:每个需求完结后执行一次复盘命令,从提交历史、质量门审查记录、hook 拦截记录中提炼一到两条针对本项目的具体规则——例如修改某个目录前必须先运行冒烟测试——经人确认后写回 CLAUDE.md。跑过三五个需求之后,这套规则集就不再是通用模板,而是被这个项目的真实事故反复调教过的定制宪法。
为什么必须是两者的组合:与单独使用 SDD 或 TDD 的对比#
有同事会问:这两个方法论各自都足够成熟,为什么不单独用其中一个?答案是,它们各自的短板恰好在对方的长处上,而这一点在 Agent 主导开发时会被急剧放大。
单独使用 TDD 时,测试驱动实现,但没有人回答测试本身从何而来。面对一句模糊的需求描述,Agent 会自行脑补验收标准再据此写测试,于是测试忠实地编码了一个错误的理解,此后的红绿循环越严谨,就把这个误解执行得越彻底。TDD 保证的是实现忠于测试,却无法保证测试忠于意图;它能确保把事情做对,不能确保做的是对的事情。此外,缺少任务拆解的 TDD 在 Agent 手里容易滑向一次性写一大片测试再一次性实现的大爆炸模式,而缺少持久化文档意味着会话一断,所有上下文归零。
单独使用 SDD 则是镜像的问题。规格写得再严谨,它也只是一份静态的承诺,而代码是否真的兑现了规格,靠什么验证?靠 Agent 的自我汇报,或者靠人逐行读代码,前者不可信,后者不可扩展。规格与实现之间的鸿沟没有任何自动化的桥,遗留代码的既有行为也没有任何保护。更微妙的是,纯 SDD 的文档没有执行者,写完即开始老化,这正是下一小节要谈的腐化问题的根源。
组合之后,两者互为对方缺失的那半环:SDD 给 TDD 提供了测试的事实来源,每条 EARS 验收标准最终落成具体的测试断言,测试不再来自 Agent 的脑补;TDD 给 SDD 提供了执行引擎,规格的每次违背都表现为一次测试变红,承诺变成了可被机器验证的断言。串起这两半的是一条完整的可追溯编号链:需求编号映射到设计点,设计点映射到任务编号,任务编号出现在测试名与提交信息里。链上任何一环断裂,审查子代理和验收环节都能机械地检测出来。这条链才是组合的真正价值,缺了任何一端,它都串不起来。
简易 SDD 如何对抗文档腐化#
规格驱动最常见的死法不是写不出文档,而是文档腐化:代码改了文档没改,三个月后没人敢信文档,于是没人再读,于是更没人更新,恶性循环一旦启动,规格体系名存实亡。我们把这套 SDD 刻意做得很轻,是为了减少 Token 成本和流转成本,但是我们也是围绕防腐化特意做了些设计的。
第一条也是最根本的一条:让文档承重。传统 wiki 式文档的致命弱点在于它不承担任何执行职责,过期了也不会有任何东西报错。而在这套流程里,每个任务的执行子代理都是冷启动,它对任务的全部认知来自现场读取的三份文档,文档一旦与代码脱节,下一个任务会立刻做错方向或者验收失败,腐化在发生的当天就被暴露,而不是沉默地积累三个月。文档从描述系统的旁观者,变成了驱动系统的零件;零件坏了,机器会停。
第二条是变更方向单一。宪法规定需求变更必须先改文档、再改代码:回到受影响的最早阶段更新规格,然后向下重新推进。配合提交拦截,代码不存在绕开文档独自演进的路径,同步不再依赖任何人的自觉。
第三条是控制文档的范围与寿命。腐化的速度大致正比于文档的覆盖范围乘以它被要求保鲜的时长,最容易烂掉的恰恰是那种试图永远描述整个系统现状的全局大规格。我们反其道而行:文档按需求切分,一个需求三份短文档,需求完结后它们自然转为历史决策记录,不再承担描述系统现状的义务。每份文档只需要在自己几天到几周的生命周期内保持准确,这个要求低到流程本身就能兜住。同时,快车道的存在也是防腐化手段的一部分,如果三行代码的修复也要走全套文档,人一定会绕过流程,而每一次绕过都是一次代码与文档的脱钩,我们给小改动一条合规的近路,比事后追讨文档欠账便宜得多。
第四条是给规则集配备减法机制。复盘命令在向宪法追加新规则时,会同时检查已有规则是否与之重复或已经过时,提议合并与删除。文档体系的健康不仅取决于写入了什么,同样取决于能不能删掉不再成立的东西:只增不减的规则集,本身就是另一种形式的腐化。
四、实现:Claude Code 四层机制的分工#
整套方案是一个可以直接复制进项目根目录的配置包,四层结构各司其职。
第一层是 CLAUDE.md,可以理解为宪法。Claude Code 在每次会话中自动加载它,因此硬性规则写在这里便等同于常驻内存:没有失败的测试不写实现代码;改遗留代码前先锁行为;实现阶段测试文件只读;只写最小实现、禁止范围蔓延;每个任务收尾必须提交。文件底部预留了一个由复盘持续追加的项目特有规则小节。
第二层是 skills,即阶段命令。.claude/skills/ 目录下共七条命令,每条对应一个 SKILL.md,内置该阶段的执行步骤、质量门与停止条件:
/spec-new <需求描述> 阶段一 需求澄清 → requirements.md
/spec-design <需求名> 阶段二 方案设计 → design.md
/spec-tasks <需求名> 阶段三 任务拆解 → tasks.md
/tdd-next <需求名> 阶段四至六 编排器,自动循环至任务清单清空
/spec-lite <需求描述> 小改动快车道(不超过三个文件、半天工作量)
/spec-status <需求名> 进度与状态体检
/spec-retro <需求名> 复盘,沉淀规则回 CLAUDE.md第三层是 subagents,负责独立上下文之间的分工。task-executor 是执行者,携带完整的 TDD 操作规程,每个任务对应一个全新实例,并内置防死循环铁律:同一错误连续碰壁三次必须求助,而非继续硬试。gate-reviewer 是审查者,只配只读工具,运行在 haiku 模型上(质量门审查本质是清单核对,不需要最强模型,成本可以省下一大截),立场设定为存疑即不通过。审查者与执行者的上下文彼此隔离,执行者的自我辩护无法污染审查判断,这也正是我们在人类流程中让评审独立于开发的原因。
第四层是 hooks,物理执法层。两个 bash 脚本挂载在 PreToolUse 事件上。其中阶段状态的实现值得一提:执行器进入每个 TDD 步骤时,将阶段名写入 .claude/state/<需求名>.phase,测试保护脚本读取该文件决定是否拦截。状态文件按需求隔离——需求 A 中断在实现阶段时切换去处理需求 B,B 的整个生命周期不会触碰 A 的状态;A 残留的测试保护会在下次冲突时被显式暴露(拦截信息会指明持锁者),而不是被静默清除。残留状态被暴露,永远好过被掩盖。
一个必须坦白的实现细节:hook 从输入 JSON 中提取信息采用的是文本匹配而非严格解析(为了不引入 jq 依赖),对已知格式有效,但 Claude Code 迭代很快。首次使用时务必亲眼验证拦截确实触发,尤其要确认项目级 hook 对子代理的工具调用同样生效——这是整套防线成立的前提。
五、实践:日常如何使用#
这套流程中,工程师的角色被压缩为两件事:三次确认,加上异常仲裁。
三次确认对应三份规格文档的人工评审。这是全流程中投入产出比最高的十五分钟——规格错了,后面的自动化越高效,错得越彻底。三次确认各有侧重:评审需求文档时,逐条检查验收标准是否可测、排除项是否符合预期;评审设计文档时,重点看变更面是否最小、有没有顺手夹带的改动;评审任务清单时,确认特征测试任务排在最前、没有藏着超过半天粒度的大任务。
异常仲裁对应循环主动停下的时刻。停止条件被刻意设计得较为敏感:子代理求助、验收不符、同一任务重派两次仍失败,以及出现与当前任务无关的测试失败——最后这条在遗留项目上尤其重要,它大概率意味着触碰了某个隐性依赖,需要人先看清现场再做决定。循环停下来不是流程失败,恰恰是流程在正确地工作。
一个常规需求的完整生命周期如下:
/spec-new 订单导出增加按取消原因筛选 # Agent 提问,你答疑,确认需求文档
/spec-design order-export-filter # 评审设计(建议以 Plan Mode 只读运行)
/spec-tasks order-export-filter # 确认任务清单
/tdd-next order-export-filter # 此后只需关注进度行:✅ T3 完成|剩余 4
/spec-retro order-export-filter # 完结复盘,规则进化几个高频场景也有各自的标准动作。小改动走快车道命令:一页规格、一次确认、直达 TDD 循环,但特征测试、测试先行、提交拦截三条底线不因流程简化而豁免。被绕过的流程等于没有流程,所以我们为小需求修了一条合规的近路,而不是逼人翻墙。中断恢复只需重新运行编排命令,它会自行检查状态文件与 git 现场,你不需要记得上次进行到哪里——文档和状态文件替你记得。多个需求可以随意交错切换,状态按需求隔离、互不污染;但同一工作目录内不要真正并行——共享的 git index 与测试套件是物理限制而非流程限制,真并行的正确方式是 git worktree,目录隔离之后,代码、状态与 hook 全部天然隔离。
六、局限:哪些问题它不解决,哪些账要算清#
讲透彻就必须讲局限。以下是推广之前,团队应当知道的全部实话。
规格阶段有真实成本,且成本前置: 一个需求要经过三份文档与三次确认才开始产出代码。对中大型需求,这笔投资稳赚;对小需求,快车道压缩了它;但对探索性工作——你自己都尚不确定要什么、需要边写边看的原型验证——这套流程是负资产。探索阶段就应该自由地快速试错,方向确定之后再回到流程中来。用错场景不是流程的错。
质量上限由测试质量决定,而测试出自 Agent 之手: TDD 的保护强度取决于测试的刁钻程度。Agent 编写的测试覆盖正常路径通常没有问题,但对边界条件与异常路径的想象力,仍然弱于有领域经验的工程师。流程没有为测试用例单设一道人工确认,是因为逐条评审测试会让流程重到无人使用,我们接受了这个权衡,其代价是:验收标准写得越具体,测试质量越有保障;EARS 写得含糊,测试先行阶段就形同虚设。
特征测试救不了不可测试的代码:部分遗留代码没有任何可以注入测试的缝隙,比如静态耦合、全局状态、构造函数里直连数据库等。对这类代码,先锁行为在操作上无法成立,需要资深工程师先做最小限度的解耦(提取接口、注入依赖),撬开一道测试缝隙,Agent 才能接手。这是流程覆盖不到、必须由人来判断的部分。
hook 防线并非绝对:文本匹配提取路径存在理论上的绕过空间;版本更新可能改变 hook 的输入格式或子代理行为;在跳过权限确认的运行模式下,防线形态也会不同。它的定位是挡住 Agent 在目标压力下的变通,而不是防御一个蓄意的攻击者。整套配置基于 2026 年年中的 Claude Code 机制编写并验证,该产品迭代极快,行为不符时以官方文档为准。
它不能修复糟糕的架构,只能阻止其继续恶化:流程保证每次改动最小、可回滚、行为受保护,但项目的结构性债务不会因此减少。重构阶段做的是任务级的小步整理,而非架构级的重塑。架构演进依然需要人来规划,它也可以作为独立需求走这套流程执行,但方向盘必须握在人手里。
token 成本显著高于放任自流: 每个任务冷启动意味着重复读取文档,审查子代理是额外开销,特征测试是额外的编写量。尽管做了一些优化,比如审查降级到 haiku、代码探索委托给专门的子代理,总成本仍明显高于一把梭的写法。这笔账的另一侧是返工成本与生产事故成本,在遗留项目上并不难算,但每个人心里都应该有这个数。
人工确认是最后一道、也是最脆弱的防线:流程把人的介入压缩到了三次确认与异常仲裁,这意味着这几个节点上的注意力质量直接决定最终结果。如果确认退化为扫一眼就通过,整套自动化就是在高效地放大一份没人认真读过的规格。工具能约束 Agent,约束不了敷衍。
七、结语#
这套流程的本质,是把我们对良好工程实践的共识——想清楚再动手、测试先行、小步提交、行为受保护、评审不缺席——从依赖个人自觉的软规范,编译成了 Agent 无法绕开的硬结构。Agent 不需要被说服,它只需要一个让正确行为成为最短路径的环境。
落地路径建议:挑选一个中等规模、非紧急的真实需求完整跑一遍,重点验证两道 hook 确实在拦截;跑完立即复盘,沉淀第一批项目特有规则;三个需求之后,再讨论是否全面推开。过程中如果遇到莫名其妙的拦截,或者认为某道质量门纯属形式,请直接提出——流程本身也身处这个复盘与进化的循环之中,它应当被持续修订,而不是被供奉起来。
配置包与详细的 README 在团队仓库里。装好之后的第一件事,建议用快车道命令试一个小需求,亲手体会一次被 hook 拦下的瞬间,那就是这套流程与提示词工程之间的全部区别。