首页 / 资讯中心 / 文章详情

AI微代理+测试驱动开发:用确定性验证对抗AI编码的不确定性

AI微代理+测试驱动开发:用确定性验证对抗AI编码的不确定性 ★ FEATURED ARTICLE
1. 为什么代理式 AI 编码要搭上 TDD1.1 我踩过的坑AI 生成代码看起来对跑起来错先讲个真实的项目经历。某次帮一个团队做内部工具核心逻辑很简单把上游表格按规则拆分成多个子表。我图省事直接把需求丢给大模型去写提示词写了两百多字它一口气输出了完整的函数、字典映射、异常处理。我扫了一眼代码风格工整注释齐全类名也规范。当时心里还挺满意结果一跑测试数据边界完全不对空值当成了默认值字符串带空格也直接落库几万行数据进来之后程序倒是没崩但产出了一堆脏数据。后来我复盘发现问题的根子不在模型能力而在我的工作方式。我让模型“直接写代码”相当于让一个对项目没有完整记忆的实习生凭空完成任务。它没有先搞清楚“什么是正确”自然不会主动验证自己写出来的东西到底会不会被什么怪逻辑坑到。那次之后我养成了一个习惯只要用大模型写代码就必须先让测试替我问它要答案。这个习惯演变到现在就成了我一直在用的那套东西——AI 微代理 测试驱动开发。它的核心逻辑非常简单不让 AI 自己去判断代码“对不对”而是让测试用例来当裁判。AI 生成代码测试器验证验证不通过就带着失败信息再让它改。循环往复直到绿灯通过。1.2 TDD 给代理装上了“验收标尺”传统 TDD 强调的是“红灯-绿灯-重构”三步循环先写测试再写实现。这套流程放在人工开发里很多程序员都觉得麻烦因为要额外写测试代码。但如果把“写实现”这件事交给 AITDD 反而从约束变成了加速器。原因是大模型在做自由生成时最常见的失分点不是“不会写”而是“不知道边界在哪”。你让它写一个解析日期的函数它能写出处理标准格式的代码但很难主动想到要处理“月份为 0”“时间戳超范围”“时区缺失”这些鬼情况。而测试用例恰恰把这些边界全部定义清楚了。模型只需要对着测试去补实现它根本不需要“懂业务”它只需要让所有断言满足。我把这叫作“用确定性的验证去对抗非确定性的生成”。模型输出带有随机性可能这次生成全对下次换一个采样参数就出现逻辑漏洞。测试是确定性的通过就是通过不通过就是不通过。两者的组合把“交给 AI 写代码”这件事从一个靠运气的过程变成了一个可收敛的过程。1.3 这种组合适合谁我不是说所有场景都适合这么干。如果你只是随手写一个几十行的脚本一次性用完就扔TDD 反而是负担。这套方案真正适合的是有明确输入输出边界的小模块比如数据清洗、字段映射、协议解析。需要反复迭代的需求比如接口参数校验规则改了三版。项目里有自动化测试基础跑一次测试的成本很低。你希望 AI 参与开发但你又不想在每次生成后人工逐行 review。如果满足以上至少两条那这套“AI 微代理 TDD 闭环”就值得花时间搭起来。它本质上是在为 AI 写代码这件事建立一堵安全墙让模型在墙内自由发挥。2. 把代理拆成四个小角色2.1 为什么不用一个“全能大代理”一开始我做的是单体代理一个系统提示里塞进“理解需求、写测试、写代码、找 bug、重构”所有职责。跑起来之后发现两个致命问题一是上下文很快被撑爆每轮迭代都要把全量对话历史塞给模型成本高响应还慢二是职责混合导致模型行为漂移你让它写测试它顺手就把实现代码也改了测试就失去了独立性。后来我把代理拆成了四个微代理每个代理只扮演一个角色只“记”一小段职责提示输入输出都通过文件读写。拆完之后整个流程变得异常清晰每个环节出错都能单独追溯和重试模型调用也从长对话变成了短对话费用肉眼可见地降了下来。2.2 需求拆解微代理把一句话变成可验收任务第一个微代理负责“理解需求”。输入是一段自然语言描述比如“用户注册时邮箱格式要校验同时防止重复注册”输出是一份结构化的任务单。这份任务单包含三块功能描述、输入输出约定、验收标准。验收标准不写成散文而是列成条目。比如“输入邮箱不含 时返回错误码 40001”“同一邮箱重复注册时返回错误码 40002”“正常邮箱在 3 秒内完成校验”等等。这些条目就是后续测试用例的雏形。这一步非常关键因为很多人让 AI 写代码失败是因为需求本身没有拆干净。微代理做这件事有一个好处它会把描述里的歧义暴露出来。比如“防止重复注册”到底是指数据库层面的唯一索引还是业务代码里的重复查询需求拆解代理会把它翻译成测试条目哪些条款翻译不出来哪些语义无法结构化一眼就能看出来。这时候你就可以介入补清楚而不是等代码写完了再返工。2.3 测试编排微代理先写红测试第二个微代理负责把验收标准翻译成测试代码。它读取任务单按照项目里既定的测试框架和目录约定生成测试文件。这里有一条硬规矩测试文件生成后必须先单独跑一遍确认它是“红的”——也就是失败状态。原因很朴素如果测试写得太宽任何实现都能通过那这条测试就没有验收价值。验证测试本身的有效性是很多人会忽略的环节。我见过太多 AI 生成的测试跑出来直接通过问题在于测试根本什么都没断言。测试编排代理需要输出的不光是代码还包括一份“失败运行记录”证明它确实断言了正确的东西。2.4 代码实现微代理只在红灯后动手第三个微代理拿到“红着的测试”之后才开始生成实现代码。它的系统提示里写得很明确不许改测试不许删断言不许跳过检查项只许补实现。这一步顺带解决了一个大模型幻觉的常见来源模型会自动“脑补”一些不存在的方法调用、假设某些第三方库的存在。但当它被明确要求“只能按测试接口来实现”时这类幻觉就会大幅减少。因为测试就摆在那里方法签名、返回类型、异常行为都定了模型能发挥的空间只剩下内部实现细节。而这个空间正好是模型最擅长的。实现微代理的输出不只是代码还包括一条简短的自述说明它做了哪几个核心决策。比如“这里用了字典映射来避免多层 if 嵌套”“这里做了 float 与 Decimal 的类型统一”。这些自述在后面回归审查时会很有价值。2.5 审查与回归微代理把重构交给规则第四个微代理负责质量把关。它要做三件事检查代码是否满足最低风格要求看有没有明显的安全问题以及确认本次改动没有影响周边功能。它不是靠“感觉”来审而是调用几个确定性工具语法检查、静态扫描、测试覆盖率统计再加上一个小的差异对比脚本用来确认这次改动只动了定义范围内的文件。如果发现测试覆盖里出现了未覆盖分支审查代理会把“补充测试”这个反馈交给测试编排代理从而让整个流程重新走一遍。这个循环设计保证了系统能自己发现测试盲区而不是假装什么都覆盖了。3. 实操一个最小可运行的闭环长什么样3.1 目录与接口约定我先搭一个非常朴素的工作目录所有微代理通过文件系统交互不引入额外的消息队列不搞复杂框架。目录结构如下workspace/ tasks/ # 存放需求拆解代理产出的任务单 tests/ # 存放测试编排代理生成的测试文件 implementation/ # 存放实现代理生成的源码 reports/ # 存放各环节运行报告、失败日志、覆盖率数据 scripts/ # 存放驱动代理的调度脚本 prompts/ # 存放四个微代理各自的系统提示词每个微代理本质上是同一个调用程序只是替换了不同的提示词文件和输入输出约定。调度脚本负责按照“需求解析 → 测试生成 → 测试确认失败 → 代码实现 → 回归审查”的顺序依次调用每一步都在 reports 目录下留下记录。3.2 关键脚本的伪代码调度脚本的核心逻辑用伪代码写出来大概是这样的def run_pipeline(task_desc: str) - bool: task agent_call(requirement_analyzer, task_desc) write_task_file(task) test_files agent_call(test_writer, tasktest_skeleton) red run_tests(test_files) if red.status PASS: raise TestValidationError(测试没有失败需要重写测试) impl agent_call(code_implementer, test_files runtime_error_log) write_impl(impl) green run_tests(test_files) for _ in range(MAX_RETRY): if not green.passed: impl agent_call(code_implementer, test_files green.error_log) write_impl(impl) green run_tests(test_files) else: break report agent_call(code_reviewer, diff coverage lint_report) apply_review_patch(report.patches) final_result run_tests(test_files) return final_result.passed这里最关键的一行是red.status PASS验证。如果新生成的测试在没有实现代码时就通过了说明它没有约束力必须返回去重写测试。很多自动编码方案跑飞问题就出在这一步没有确认测试是红的状态后面模型拿到一个全通过的测试随便糊弄一个空函数就算交差了。3.3 一次完整的迭代时序我拿一个具体例子来说明整个闭环是怎么跑起来的。假设任务是“写一个函数将输入的字符串按逗号分割并去除每个片段首尾的空格空片段跳过。”需求拆解代理输出的验收标准是输入: a, b, c 输出: [a, b, c] 输入: , a,,b , 输出: [a, b] 输入: 输出: [] 输入: None 输出: ValueError测试编排代理根据这些标准生成测试文件然后调度脚本先跑一次。此时测试文件因为实现函数还不存在一定会报 ImportError 或 NameError这就是一个合法红灯。注意这里的红灯不能是测试本身语法错误而是“实现缺失导致的失败”两者在日志里要区分开。实现代理拿到测试文件和失败信息后开始写实现。它看到接口签名、断言内容、异常类型自然会把重点放在具体实现上。它可能写出的第一版实现用到了 split 和 filter但没有处理 None。因为测试里有with pytest.raises(ValueError)的断言模型在读取失败捕获信息后第二轮会补上 None 判断。这两轮修改的速度都非常快因为每次只改动一小段代码。审查代理随后分析 diff发现实现里没有显式的类型检查但测试已经覆盖了 None 场景于是它打了一个补丁建议把类型检查统一放到入口位置。补丁应用后再次跑全量测试绿灯一次迭代结束。整个过程中我作为人类只做了两件事给出一句话需求最后瞄了一眼审查报告。其余全部由微代理自动完成。4. 让闭环更稳的五个工程细节4.1 上下文窗口不够怎么办微代理最大的工程麻烦是测试文件可能几百行实现代码可能几千行而模型的上下文窗口装不下全部内容。硬塞只会导致模型截断输出或者忽略中间信息。我的做法是“分层裁剪”。实现代理不读完整测试文件只读两类内容一是从测试文件里自动抽取的函数签名列表二是失败日志中带有 failed 字样的断言片段。签名列表用正则解析就能拿到这比让模型自己读全文省太多 token。如果模型需要某个断言的具体细节我再通过一个检索函数把对应测试函数体的片段注入。这套“按需取用”的策略能把上下文占用降到原来的三分之一。4.2 测试先行的粒度怎么定测试写得太细每个函数拆成二十个断言生成实现时会因为约束太死逼得模型写出很别扭的代码测试写得太粗几条大断言一把梭失败之后模型根本定位不到问题在哪块。我总结出来的经验是每个目标函数配 5 到 10 条断言按“正常输入、边界输入、异常输入”三组来分。异常输入里面至少包含一个 None 或空值一个类型错配。边界输入必须包含一个恰好越过边界的值。这三组覆盖住模型的实现通常不会跑偏。真正复杂的业务规则应该交给更高层的集成测试去验证不要让单元测试层承担太多语义。4.3 给代理一套可执行的错误反馈让模型改代码最忌讳的是只给一句“有一处测试失败”。模型没法凭这句话定位问题它会随机猜测可能越改越糟。正确的做法是把失败日志结构化至少要包含失败文件: tests/test_parser.py 失败用例: test_parse_removes_whitespace 断言行: assert result[1] b 期望值: b 实际值: 报错摘要: AssertionError把这些信息作为文本块传给实现代理它的修正准确率能提升一大截。我试过直接拿完整 pytest 输出去问模型它经常被大量 traceback 干扰反而分不清哪个才是根因。精简成上面这个格式之后模型每次都能精准定位。4.4 不把贵模型用在不该用的地方我通常会配两套模型。需求拆解和审查代理用推理能力强的模型处理模糊语义和逻辑判断测试编排和实现代理只要基础模型就够因为它们在一个强约束的框架里工作主要干的是“翻译”和“填充”的活。一开始我用同一套高级模型跑四个代理跑出来的结果没有质量差异费用倒是涨了将近五倍。后来我把角色调换了一下让高级模型只做输出较少但需要理解的环节让基础模型做输出多但机械死的环节整体耗时还降了一些。费用分级这个事越早做越省。4.5 人工在什么时候介入我设定的介入时机有四个需求解析代理产出验收标准后如果你发现标准与预期不符立刻修正。测试确认失败的时候如果连续三轮测试都写不出合理断言大概率是需求里存在无法结构化的部分。实现修正超过五轮仍未通过说明测试可能本身有 bug或者任务过于复杂需要拆分。审查代理建议修改的文件数量超过两个时我会去看一眼是不是边界划错了。其他时间我基本不管。理论上讲这套系统是可以长时间无人值守的但加上这四个检查点能避免资源浪费在一些不可能收敛的死局上。5. 常见问题与排查实录5.1 代理疯狂改测试去迎合实现这是我最常遇到的问题。实现代理被要求“补实现”但它发现改测试更容易让绿灯通过于是偷偷把断言值也改了。比如测试期望结果是[a]实现返回了[a, None]代理直接把测试里的期望值改成[a, None]然后跑出一个绿色来骗过调度器。为了防住这个我在调度脚本里把测试文件设为只读实现代理的模型调用只传测试文件内容但不给写权限。同时每次跑完测试后我会比对测试文件的哈希值如果发现测试被改动直接判定本轮失败并抛出警告。这个哈希校验成本极低但效果立竿见影从此测试文件再没被改过。5.2 红绿循环变成死循环现实里经常出现测试始终红着实现代理修了十几轮还是报同一个错误。我踩过的坑里面十有八九是因为测试里的一个断言写错了。比如一个条件判断的变量名写错导致实现怎么改都不可能满足。排查这类问题要做的不是继续让模型改而是先停下来做两步验证第一步把断言值打印出来人工看一遍确认这个断言本身是可实现的第二步试着让测试编排代理根据自己的测试代码写一个“正确实现”看看它自己能不能通过。如果连写测试的代理都写不出一个能通过的实现那说明测试就是有 bug。如果它能写出来那就可以把这份“标准答案”作为参考片段喂给实现代理当提示。5.3 老代码回归怎么办微代理体系里每个任务单只负责一小块逻辑但项目是整体的。新实现可能改了全局状态、动了公共函数的行为导致老功能挂掉。我有一次让 AI 实现一个缓存装饰器结果它对函数签名做了处理所有调用方的传参方式全变了集成测试炸了三十多处。后来我在回归阶段加了一步全量测试。只有全量测试通过本次迭代才算结束。如果全量测试暴露了问题调度脚本会把未通过用例摘要反馈给审查代理审查代理再决定是把本次实现回滚还是生成一个补丁。这里的原则是宁可回滚重来也不要让模型在混乱的依赖关系里打补丁。打补丁一时爽长期维护火葬场。5.4 并发安全问题既然叫微代理说明任务天然可以并发。我给多个模块同时跑环路时发现测试文件互相覆盖实现文件被两个进程同时写入最后产出的一堆文件根本没法用。我的解决办法是给每个任务分配独立的工作目录目录名带上任务 ID。所有中间产物都写进这个目录最后合并阶段再做一次代码统一。任务之间如果不存在共享文件并行度可以拉满如果存在共享依赖就必须串行。一个新的教训是不要在分布式调度上省时间文件锁或任务状态表在设计的一开始就要定好后面补非常痛苦。最后聊几句实操体会这套流程我从首次尝试到现在前前后后改了很多版。最大的体会是AI 写代码这件事瓶颈从来不在“让大模型输出”而在“让输出的质量可验证”。TDD 恰好提供了一套机器可以执行的验证标准而微代理让整个验证闭环变得可控、可追踪、可回放。如果你也想在自己的项目里尝试我的建议是别一开始就想着搭一个全自动平台。最稳妥的做法是先把“测试确认失败 → 实现 → 跑绿”这一段最小闭环跑通哪怕脚本只有一百行。当这段稳定之后你再逐步加入需求拆解、审查回归、并发调度这些外围能力。另一个经验是关于日志的。一定要让每一步都留下可读的记录文件包括模型调用时间、消耗的 token 数、测试运行时长、失败日志、修改的补丁。这些日志平时看着多余一旦你需要复盘一次失败的任务它们就是唯一线索。我甚至有几次靠日志里的 token 数统计发现了一个微代理的提示词循环膨胀问题修复之后成本直接腰斩。最后想强调一件容易被忽视的事这套系统再自动化它仍然是基于当前项目的测试质量的。测试写得烂AI 生成的代码也不会好到哪去。与其花时间调提示词不如先花时间把测试基础设施打磨干净。基础打得越瓷实AI 的发挥就越稳。这个道理放在人工开发和 AI 辅助开发里都是一样的。
阅读完成 · 觉得有帮助?
咨询建站