在让AI实现功能前先用ADR(架构决策记录)和SDD(方案设计文档)锁定上下文,强制AI先读真实代码再做决策,避免隐性设计漂移。
问题不在于 AI 写出了错误的代码,而在于它在没有足够上下文的情况下自行做决定
让 AI Agent"完成 X 任务"看起来很简单,直到你想起大多数任务是怎么描述的:一个标题、两行描述、也许一个泛泛的验收标准。这些信息对已经熟悉系统的人来说足够了——他们可以用隐性上下文填补空白。但对 AI 来说不够,AI 填补空白的方式恰好也是猜,而且不会告诉你它在猜。
症状不是代码格式错误。是在实现过程中悄然做出的设计决策,没有人意识到这个决策已经被做出了。一旦发生这种情况,bug 不会出现在 PR 里,而是出现在生产环境,或者更糟——根本不出现:它就躺在那里休眠,直到某一天 AI 所忽略的那条路径恰好是一个真实客户会走的路径。
实践中整理这个问题的方法是反转顺序:与 Agent 一起生成 ADR(架构决策记录)和 SDD(解决方案设计文档),在任何一行实现代码之前,迫使 AI 在决定任何事情之前先读取系统的真实代码,只有在那之后才释放实现。TDD 作为第三层加入,几乎是必然的:如果已经存在一条已记录的决策(ADR)和一幅如何实现它的图纸(SDD),下一步自然的就是把它转换成测试——这些测试先因为正确的原因而失败,然后在代码让它通过之前就证明决策是站得住脚的。
这个流程被命名为"Plan first, then implement":ADR → SDD → TDD,作为开发者与 AI 之间的上下文契约,在每一步都可review,在任何 commit 之前就完成。
为什么是三层,不是一层
每个文档回答的问题各不相同,试图用一个单一制品解决三个问题,就是"文档"总是被看过烂文档的人忽略的原因:
ADR——为什么。已经做出的决策,记录了被拒绝的备选方案和已承担的后果。它不是开放讨论的地方(那是 RFC 的职能);它是团队已经决定的事情的记录,这样以后就没人需要重建推理过程了。
SDD——怎么做。具体的图纸:哪些文件会改动、输入/输出的契约是什么、哪些现有的消费者会受影响。正是在这里,"在两个数据源之间做 fallback"变成了"这两个展示类访问的是固定字段,所以不做格式翻译的 fallback 会产生一个几乎为空的响应"。
TDD——验证什么。可验证的 RED-GREEN 循环,证明 ADR 的决策和 SDD 的图纸在实际案例面前真的站得住脚,包括那些只有 SDD 才揭示出来的边界情况。
顺序很重要:跳过 SDD 直接从 ADR 到代码,意味着那些映射决策——它们不够架构级别而进不了 ADR,但又太大而不能隐含在代码里——没有地方存放。它们最终活在实现者的脑子里,这恰恰是这个方法所要避免的问题。
真实案例:两个外部查询 API 之间的双向 fallback
原始任务要求一个 fallback:如果主查询 API 查询外部注册信息失败,系统应该在放弃之前尝试第二个 API。这样描述的话,看起来就是一个 try/catch 里放着第二个调用。ADR → SDD → TDD 循环揭示出它不是这么回事。
ADR 确定了什么:哪个 API 是主的、为什么顺序重要(SLA、响应格式、数据覆盖),以及——这个决策只在文档的第二次迭代中才出现的——fallback 只应在抛出异常时触发,绝不在业务错误响应时触发。这个决策的存在是有原因的:两个 API 以不对称的方式发出错误信号:一个总是抛异常,包括"未找到"的情况;另一个返回 HTTP 成功,但在响应体里带一个错误指示器。不把这个区别明确记录下来,一个"朴素"的 fallback 会把第二个 API 的业务错误响应当作成功。
SDD 揭示了 ADR 没有捕获到的东西:
字段翻译是前置条件,不是可选步骤。消费两个响应的展示类各自访问的是固定字段名,而每个 API 有自己的格式。一个只换数据源而不规范化响应格式的 fallback,会在系统其余部分期望的格式下产生一个几乎为空的对象,fallback"成功"了——在返回成功这个意义上——但在真正重要的意义上失败了。
现有业务规则中的一个真实 blocker。一个低位注册流程的校验实现了一条阻止活跃记录做低位的规则。这条规则依赖一个字段,而没有第一项的翻译,那个字段根本传不过来,这意味着业务规则会在 fallback 路径上悄然停止执行,没有任何可见的错误。
死代码作为副作用被揭示出来。同一个校验流程中一个状态字段的检查从未对主 API 的响应为真过,因为该 API 从不产生这个字段。它不在任务范围内,却被发现了——因为 SDD 的要求,把两种响应格式并排映射,暴露了不一致。
这三个发现没有一个在原始任务里。直接对 Agent 说"实现 fallback",没有一个是显而易见的。三个都出现了,是因为写 SDD 的过程迫使了对两种响应格式和这些数据的真实消费者的对比阅读,不是因为有人主动去排查 bug。
五个 TDD 循环,为什么第四个最有趣
ADR 和 SDD 确定之后,TDD 不再是抽象地"先写测试再写代码",而变成了一张按 SDD 揭示顺序排列的待证明行为清单:字段翻译、只在异常时触发 fallback、保持低位业务规则、以及对已识别死代码的处理。
最具教学意义的循环不是技术最复杂的,而是处理死代码的那个。其中的教训是:RED 也可以意味着"这不应该再存在",而不仅仅是"这还不存在"。传统的 TDD 循环证明一个新行为有效;这个特定循环用一个测试证明了——如果死检查仍然可以对主 API 数据触达,它就会失败——逻辑实际上从未执行过,把 SDD 的发现记录在测试中,而不是简单删掉那行代码然后依赖看过 SDD 的人的记忆力。
这在实践中改变了什么
收益不是为文档而文档,而是时机。"在实现之后"记录"只在异常时触发 fallback"的决策什么都防止不了;代码已经按照另一种假设被写出来了。方法的价值完全在于把这些发现提前到第一次 commit 之前——那时候只是一句话的 SDD 代价,而不是生产环境的 rollback。
还需要指出的是,文档不是在第一次尝试时就完整产生的。这个案例的 ADR 第一版没有关于异常触发的决策,也没有字段映射表,两者都是在后续review中出现的——在检查真实代码后发现初始假设被推翻了。这不是流程的失败;正是流程在起作用:每一层都暴露了前一层没有明确的东西,文档在变成代码之前就更新了,不是在之后。
一个实际的副作用:PR、QA 描述和验收标准都从 ADR/SDD/TDD 派生,而不是在交付时才基于假设产生。任务 → ADR → SDD → TDD → PR 的可追溯性闭合了一个通常断裂的循环——通常只有任务和 PR 存在,中间发生的一切都只存在实现者的脑子里。
一个模式,不是固定规则
同样的流程、以同样的形式,在范围小得多的任务上重复了——比如同一个注册数据两种校验流程之间的奇偶性修正,那里的"发现"不是业务 blocker,而是发现新校验中客户类别删除与遗留流程已验证的行为不一致。问题的规模变了;在生产环境动手之前生成三份文档的纪律没变。
这说明方法的价值不与任务复杂度挂钩,在于永远不让 AI(或者在截止日期压力下的开发者自己)隐式决定任何本应在代码存在之前就明确记录的事情。
本质上,ADR、SDD 和 TDD 不是为流程辩护的官僚主义,而是把"信任 AI"变成可验证之事的方式,而不是一种信仰行为。不是关于不信任 Agent;而是不把本应明确的决策外包给一个本质上倾向于沉默填补空白而不是承认自己不知道的过程。
反直觉的一点是:这不会让工作更慢,只是让第一步更慢,而让之后的一切更快——因为那些通常几周后才出现、以生产环境 bug 面目示人的返工,已经在文档的一句话里解决了,在存在需要重写的代码之前就解决了。Plan first, then implement 不是为了纪律而纪律。它是"AI 在不通知你的情况下替你决定"和"你与 AI 一起、睁着眼睛做决定"之间的差别。