1. 为什么要重构开发工作流:传统TDD的困境与AI-TDD的破局点
这半年我一直在琢磨一个事:在AI大模型已经能顺畅生成单元测试、甚至能对着一段需求描述直接写出完整实现的今天,我们团队原来那套“人肉TDD”工作流,到底还有多少环节值得保留?
先说结论:TDD的核心思想不但没有过时,反而成了大模型时代最稀缺的“安全绳”。但传统TDD的操作方式——人写测试、人跑测试、人分析失败、人写最小实现——在大模型介入后,完全可以被压缩成一个人机协作闭环。我管这套新流程叫Behavior-Driven AI-TDD,它的核心不是让AI替代程序员,而是让AI把TDD中那些重复、机械、容易偷懒的环节接过去,让人专注于“行为定义”和“结果评审”这两件机器很难做好的事。
适合谁来看这篇文章?如果你正在做AI辅助编程工具的落地实践,或者你在团队里尝试过让大模型写代码但发现“生成一时爽、维护火葬场”,又或者你只是好奇BDD和TDD在大模型时代怎么嫁接起来,这篇文章里都有你用得上的东西。我会把一个真实项目里的流程重构过程、工具选型、提示词设计、踩坑记录全部拆开讲,不藏着掖着。
1.1 传统TDD的最大痛点
传统TDD的链条是这样的:先写一个失败测试,再写最小实现让测试通过,然后重构,循环往复。理论上很完美,但我在团队里推行TDD这么多年,遇到的最大阻力不是理念,而是“人”的惰性和成本。
第一个痛点是“先写测试”的门槛高。大部分开发者的本能是先写实现,因为实现有即时反馈,测试则要额外设计输入、期望输出和边界条件。尤其是需求不明确时,测试根本无从下手。第二个痛点是测试代码也是代码,也会腐烂。时间一长,测试用例变成了一堆没人敢动的“历史遗留”,红绿状态不再反映真实质量,而是反映“有没有人记得更新它”。
第三个痛点是反馈循环太长。写好测试、跑出红、写实现、跑绿,这个循环如果完全靠人手动衔接,一次迭代至少半小时。一旦上下文切换,人脑记住的“当前要解决的问题”就丢了,TDD的节奏感也荡然无存。
1.2 大模型给TDD带来的三个关键变化
大模型介入之后,我发现TDD的这几个痛点都有了对症解法。
第一个变化是“从行为到测试”的翻译成本大幅下降。BDD里那些Given-When-Then的验收场景,本身就是一种非常接近自然语言的“伪代码”。大模型对这种结构化文本的理解能力非常强,你只需要给它一个用户故事和对应的Feature文件,它就能生成一整套带断言的测试骨架。这件事人做要花半小时,AI做只要几十秒,而且生成的质量不差。
第二个变化是“红-绿-重构”循环中的反馈可以被AI消化。过去测试失败后,程序员要盯着堆栈信息猜哪里写错了。现在完全可以把失败的测试输出、覆盖率报告、静态检查报错一股脑丢给大模型,让它结合已有的代码上下文给出实现建议或补丁。换句话说,AI不仅是代码生成器,还是“失败分析器”。
第三个变化是TDD的执行边界从“人写人测”变成了“人定义机器验证”。人的核心工作变成了把需求转化为可验证的行为描述,也就是写Feature文件、写验收标准、做代码评审的决策;而AI负责把行为描述展开成测试代码,再把测试逼出实现。这个变化听着不大,实际执行起来整个研发节奏都不一样了。
1.3 Behavior-Driven AI-TDD 的核心闭环逻辑
我把这套流程画成一条闭环(这里没法贴流程图,你就脑补一个圆环):行为定义 -> 测试生成 -> 执行红态 -> 实现补全 -> 执行绿态 -> 人工评审 -> 回归验证 -> 反馈回行为定义。
这个闭环和传统TDD最本质的区别在于,行为定义是唯一的“真实来源”(Single Source of Truth)。测试代码和实现代码都是从这个行为定义里“派生”出来的,而不是人脑里随机蹦出来的。这样一来,需求变更不再需要同步改一大堆散落的测试用例,只需要改行为描述,再让AI重新生成测试、重新实现即可。
我用一个生活化的类比:传统TDD像你自己下厨,一边看菜谱一边尝味道,全靠手感;Behavior-Driven AI-TDD像你先把菜品标准写成SOP(标准作业流程),再让一个非常听话、但是偶尔会发挥过头的助手按SOP做菜。你不需要亲自颠勺,但你必须把SOP写清楚,并且在出锅时认真把关。
2. 重构工作流的整体设计与环节拆解
真正落地这套流程,不能一上来就让AI接管所有事情。我把工作流拆成了四个环节:需求行为化、测试行为化、实现驱动化、闭环反馈化。每个环节都有明确输入、输出和人工介入点。
2.1 需求行为化:用Gherkin语言做需求基线
我以前对BDD有个误解,以为它就是开会时在Wiki上写两句用户故事。后来才明白,BDD真正的价值在于用结构化的自然语言把需求固化成机器可读的规格说明。
Gherkin语法是最常见的载体。一个典型的Feature文件长这样:
Feature: 用户登录 作为一个已注册用户 我希望能够通过正确的邮箱和密码登录 以便进入系统使用完整功能 Scenario: 使用正确凭证成功登录 Given 系统中有已注册用户 And 该用户的邮箱是 "user@example.com" And 该用户设置的密码为 "correct-password" When 用户提交邮箱 "user@example.com" 和密码 "correct-password" Then 系统返回登录成功状态 And 系统创建一个有效会话这段文本既能让产品经理看懂,也能让大模型直接理解。关键是要避免模糊词汇,比如“有效会话”到底是指什么——是会话有效期超过30分钟?还是返回了sessionId?必须在Feature的后置注释或场景大纲里给出可度量的定义。
在这一步,我的经验是:不要追求一次把Feature写到完美。大模型时代的好处是可以快速迭代。你可以先写一版粗场景,让AI生成测试,再通过测试执行的失败反推需求遗漏的边界条件,回填到Feature里。需求是被“逼”出来的,而不是憋出来的。
2.2 测试行为化:从Scenario直接生成测试骨架
Feature文件写完后,接下来轮到AI登场。我们团队用的是Python技术栈,测试框架是pytest,再加上pytest-bdd做BDD绑定。但你用什么语言、什么框架都不重要,因为AI能适应大部分测试框架,关键是上下文怎么组织。
我会这样设计提示词模板:
你是资深测试工程师。下面是一个BDD Feature文件: {feature_content} 请根据该Feature生成pytest测试代码,要求: 1. 每个Scenario对应一个测试函数。 2. 使用pytest fixtures管理前置数据,不要写死具体ID。 3. Given/When/Then步骤要拆分成清晰的辅助函数。 4. 对未定义的行为边界,使用明显的TODO标记,不要擅自猜测。 5. 测试断言必须来自Scenario的Then子句,不得额外发挥。这段提示词里,最重要的一句话是“不要擅自猜测”。因为大模型非常喜欢“脑补”行为,尤其当Feature文件里的边界条件不够清楚时,它会在测试里默默加上一条“用户名为空时返回400”之类的断言,而这个断言可能根本不是需求方想要的。所以在提示词里强制加一条“TODO标记”的规则,让AI把不确定的地方交给人来决策,而不是自作主张。
生成的测试代码通常是这样的:
import pytest from app.services.auth import login def test_successful_login_with_valid_credentials(user_fixture): user = user_fixture.create(email="user@example.com", password="correct-password") result = login(email="user@example.com", password="correct-password") assert result.status == "success" assert result.session_id is not None看到没有,AI生成的测试代码已经具备了“可执行规格”的样子。但这里有个细节:它引用的user_fixture和app.services.auth都是它自己猜出来的模块路径,这样的测试第一次跑大概率是import失败。这是正常的,因为我们的目标是先得到“红态”,而这个红态不仅能证明测试存在,还能暴露“模块设计还没成形”这个事实。
2.3 实现驱动化:AI补全实现与人工复盘的边界
测试骨架生成后,我们需要运行pytest,此时一定是红灯。接着,把失败的测试代码和错误堆栈一起交给另一个Agent,让它生成最小实现。
这里我特别强调“最小实现”。大模型生成代码的通病是“过度工程”——它会在实现里顺手加一个抽象工厂、一个装饰器、三个工具函数,甚至把日志、配置、异常处理全写上。看起来功能很全,实际上严重破坏了TDD的节奏:你没法判断到底是哪个行为驱动了这些代码,也没法在后面的重构中获得清晰的块状结构。
所以第二个Agent的提示词我会写得更“克制”:
下面是当前失败的测试代码和运行输出: {test_code} {failure_output} 请基于现有项目结构,生成能让测试通过的最小实现代码。 约束: - 只实现测试中调用的函数和类,不要增加额外抽象。 - 使用项目已有的基础库,禁止引入新依赖。 - 保持代码风格与项目现有模块一致。 - 如果测试中存在多个失败,优先处理第一个,不要一次重写所有文件。这种设计让AI在局部范围内做修补,而不是一次性生成一个“新世界”。当测试通过变绿后,真正的人工环节才出现:开发者需要阅读这段AI生成的实现,检查它是否真的符合业务规则、是否有明显性能隐患、是否遵守团队编码规范。这一步是绝对不可省略的。
我经常跟团队说:AI可以替你写代码,但不能替你对代码负责。你review时要带着“挑刺”的心态,而不是“AI写的应该没问题”的心态。项目里最严重的一次线上问题,就是AI生成的查询逻辑漏了索引条件,测试全绿,但数据量一上来直接慢查询拖垮了接口。
2.4 闭环反馈化:测试-重构-回归的自动流转
当实现代码让测试变绿之后,真正的价值才刚刚开始:我们有了一个“行为-测试-实现”三者严格对应的临时稳定点。接下来可以做两件事。
第一件事是自动回归。把Feature文件中的每个Scenario映射到测试用例,跑一次全量测试,任何被改动的行为都会以红绿变化的方式反馈出来。以前这个步骤靠人肉检查,现在完全由CI执行,AI可以顺手把失败的用例分析结果写到PR评论里。
第二件事是AI辅助重构。重构在传统TDD里是最考验经验的环节,现在我们可以把“保持行为不变”作为约束,让AI提出重构建议。比如把重复的Given步骤收敛成fixture,把长函数拆分成私有方法,把魔法数字提取为常量。AI生成重构建议后,同样需要跑测试来保证行为不变。这个“行为不变”的验证就是闭环的自我校验机制。
我在这里最深刻的体会是:闭环是个好东西,但它需要“停车位”。你的流程里必须有一个明确的停止信号,不能无限循环下去。我们团队定的规则是:当所有测试通过、代码review通过、覆盖率不低于基准线时,进入合并;否则继续循环。这个信号让AI的生成行为有了终点,也让开发者的时间有了边界。
3. 核心工具选型与工程化配置
聊完流程,再说工具。这套闭环要用到几个关键组件:大模型本身、Agent框架、提示词管理工具,还有CI/CD集成。工具不是越贵越好,也不是越智能越好,关键是和你团队的现状匹配。
3.1 LLM选型:通用模型 vs 代码专用模型
我用过好几个大模型,既有通用对话模型,也有专门针对代码训练的模型。一个很明显的感受是:代码专用模型在单文件、局部代码生成场景下表现强,但通用模型在处理“跨文件的业务关系”时反而更灵活。
比如在生成Gherkin场景对应的测试代码时,代码模型能给出更规范的pytest写法;但当测试失败信息需要结合一段模糊的需求描述来推断“到底哪个行为错了”时,通用模型的语义理解能力更占优。所以我的建议不是只选一个模型,而是按环节区分:测试生成和实现补全阶段用代码模型;失败分析和行为边界提取阶段用通用模型。
如果你是个人开发者,不想接一堆API,也可以直接用一个本地部署的开源模型,结合RAG把项目结构和历史提交记录喂给它。我们团队为了数据安全,核心代码没法出内网,所以最终是在内网部署了一个私有化模型。这里不多说部署细节,因为不同硬件和框架差异太大,但记住一点:模型参数规模不是唯一指标,推理速度和上下文长度同样重要。几十B的模型在代码理解上的效果,很多时候已经接近商用模型。
3.2 Agent框架:从单一Prompt到多角色协作
你当然可以把所有事情塞进一个超长Prompt里让模型一口气做完,但效果一定不好。原因很简单:模型会在长上下文里迷失,尤其是多个环节的需求互相干扰时。
我推荐的做法是引入多Agent协作模式。不需要引入多复杂的框架,简单一点用三个Agent就够了:
- 需求Agent:负责解析用户故事和Feature文件,提取行为清单和边界条件。它往往还需要调用API查询现有代码里是否有类似的实现。
- 测试Agent:只负责生成测试代码,输入是Feature文件和项目结构树,输出是测试文件。
- 实现Agent:只负责让测试变绿,输入是失败测试和堆栈,输出是补丁。
这三个Agent之间通过一个简单的事件总线通信:需求Agent生成行为清单后,测试Agent订阅完成事件;测试Agent生成测试后,实现Agent订阅;实现Agent产出后,触发CI。这套机制不需要上Kubernetes级别的编排,用Celery或者最简单的协程队列就能实现。
自研Agent框架的好处是可以控制每一个环节的提示词行为,坏处是需要投入人力维护。如果你不想从零写,也可以直接用市面上的Agent框架,但要先确认它支持工具调用(比如执行pytest、读取文件、修改文件)以及人工审批节点。没有审批节点的Agent框架等于开着一辆没有刹车的车,前期爽,后期慌。
3.3 提示词模板:行为描述到测试代码的转换规则
提示词工程在这套流程中的重要性,怎么强调都不过分。我把团队摸索出来的几组模板规则公布一下,你直接抄作业就行。
第一组规则是强制“输入-输出”结构。在提示词开头明确指定输入字段和输出格式,比如:
输入: - Feature文件内容 - 项目文件树(截断到3层) - 测试框架版本 输出: - 测试文件名 - 测试文件完整代码 - 依赖的fixtures列表结构化输入能大幅降低模型的自由发挥概率。
第二组规则是负面约束。除了“要做什么”,还要告诉它“不要做什么”。我见过最典型的反面案例是AI在生成测试时偷偷把本地数据库连上,导致CI跑测试依赖外部环境。所以模板里必须明确:
禁止事项: - 测试中禁止连接真实数据库或外部服务,一律使用mock。 - 测试中禁止生成随机数据,必须使用固定可复现的数据。 - 禁止修改Feature文件本身。第三组规则是上下文注入。不要只给Feature文件,最好把相邻模块的公共函数签名也放进去。比如被测函数调用了get_current_user(),AI不知道这个函数的返回结构,就会猜一个,结果测试写出来就挂。你可以用一个脚本自动提取被测文件相邻的import和函数定义,拼到Prompt里。
3.4 在CI/CD里接入AI-TDD的几种方式
把AI-TDD接入CI,不是简单跑一个脚本,而是要设计好“人在环上”的位置。我们的做法分三种模式,对应不同场景。
模式一:离线批量生成。每天晚上定时把当天合并的分支产生的新Feature文件,批量喂给AI生成测试和实现,然后生成报告发到团队群。这种方式不阻塞开发,适合让AI先熟悉项目代码库。
模式二:PR触发式。每次开发者提交PR时,CI自动跑测试,如果覆盖不到新新增的Feature场景,就触发AI生成补充测试,并附在PR评论里。这个模式最有价值,因为它在代码进入主干之前补上了行为层面的校验。
模式三:配对实时模式。开发者在本地用IDE插件,写完Feature文件后一键让AI生成测试和实现,跑完红绿再推送。这个模式对工具链要求最高,但对开发者体验最友好。我们最终是先在PR触发式上跑稳定了,再推广到本地模式。
接入CI时还要注意一个事:控制AI调用的成本。大模型API不是免费的,如果每次PR都跑完整流程,一个月下来费用惊人。我们后来加了两个规则:只有Feature文件变动的PR才触发AI;测试失败时,只有失败数量小于5个才允许调用实现Agent,否则只生成失败分析摘要。
4. 实操过程:一个用户故事从需求到上线的完整案例
理论讲多了容易虚,我拿一个真实做过的功能来走一遍完整流程。这个功能是:用户注册后需要验证邮箱,系统发送验证邮件,用户点击邮件链接后完成邮箱验证。
4.1 从用户故事到Gherkin场景
项目初期,我们和产品经理聊完需求,先写了一版Gherkin文件,放在features/email_verification.feature:
Feature: 邮箱验证 作为刚注册的用户 我希望在收到验证邮件后点击链接完成验证 以便我的账号可以正常使用 Scenario: 新注册用户收到验证邮件 Given 用户刚刚完成注册 When 系统触发邮箱验证流程 Then 系统向用户注册邮箱发送一封验证邮件 And 邮件包含指向 "/verify?token=..." 的链接 Scenario: 点击有效验证链接后账号变为已验证 Given 用户邮箱验证链接的有效期为24小时 And 该链接尚未使用 When 用户点击验证链接 Then 系统将用户账号状态更新为 "verified" Scenario: 验证链接超过有效期 Given 用户邮箱验证链接的有效期为24小时 And 该链接生成时间已经超过24小时 When 用户点击验证链接 Then 系统返回链接失效提示 And 用户账号状态保持为 "unverified"这版Feature其实是有问题的。第一个Scenario里“邮件包含指向/verify?token=...的链接”这个表述,AI无法知道token是放在query参数还是path里,也不清楚token格式。我们当时没有讨论清楚,导致AI生成测试时自创了一个“token必须为32位hex”的断言,跑起来也确实通过,但这个断言本身是噪音。
后来我们的解决办法是,在Feature文件顶部加了一个“字段定义”区域:
@field-definitions 验证链接的格式为:站点域名 + "/verify?token=" + 32位随机十六进制字符串 验证链接的有效期:24小时,从邮件发送时开始计算这种额外的约束是应对“AI自由发挥”的有效手段。Behavior-Driven AI-TDD要求行为描述必须精确到“机器可执行”,所以任何人类觉得理所当然、但机器不知道的语义,都要写清楚。
4.2 AI生成测试代码的上下文组织
接下来我打开我们自研的命令行工具,输入:
ai-tdd generate-tests --feature features/email_verification.feature --framework pytest --output tests/test_email_verification.py这个命令会做几件事:读取Feature文件,扫描项目结构,抓取相关模块的接口定义,拼装Prompt,调用模型API,最后写入测试文件。
生成出来的测试代码大致如下:
import pytest from datetime import datetime, timedelta from app.services.email_verification import ( send_verification_email, verify_email_token, is_token_expired, ) from app.models import User class TestEmailVerification: def test_new_user_receives_verification_email(self, db, mail_server): user = User(email="new@example.com", status="pending") db.add(user); db.commit() send_verification_email(user) assert len(mail_server.outbox) == 1 mail = mail_server.outbox[0] assert mail.to == "new@example.com" assert "/verify?token=" in mail.body def test_click_valid_link_marks_user_verified(self, db): user = User(email="new@example.com", status="pending") token = "a" * 32 # 32位hex db.add(user); db.commit() result = verify_email_token(user, token) assert result is True assert user.status == "verified" def test_expired_link_rejected(self, db): user = User(email="new@example.com", status="pending") expired_token = "b" * 32 db.add(user); db.commit() expired_at = datetime.utcnow() - timedelta(hours=25) is_expired = is_token_expired(expired_token, expired_at) assert is_expired is True assert user.status == "unverified"看到没,第一版测试代码里已经暴露出好几个设计问题:token = "a" * 32太随意;is_token_expired的参数设计很生硬;user.status的取值是字符串还是枚举也没定。这些问题的出现不是坏事,它们正是测试作为“探针”的价值所在。
我不建议你要求AI第一次就生成完美的测试,也不要因为测试看起来“不太优雅”就否定整个流程。记住,这些测试之后会和实现代码一起被review和重构,当时存在设计问题,说明实现设计还没跟上,这是TDD的正常节奏。
4.3 校验与反馈循环:让AI“看懂”红绿测试结果
有了测试文件,执行:
pytest tests/test_email_verification.py -v --tb=short此时肯定红灯,因为app.services.email_verification模块根本不存在。这是预期内的。
然后我调用实现Agent,用的命令是:
ai-tdd implement --test-file tests/test_email_verification.py --source-dir app/services实现Agent首先读取测试文件代码和上面的pytest输出,然后去扫描app/services/email_verification是否已有文件,没有就新建,有就在现有基础上补充。它生成的实现第一版通常很奇怪,会尝试把is_token_expired变成独立函数,然后在verify_email_token里调用它。
这个过程中,我要特别强调一个“反馈循环”的设计。普通AI生成代码是“一次性生成”,而我们的系统会执行测试、捕获失败信息、把失败信息回传给AI,让AI自己修正,最多循环3次。这个循环不需要人工介入,但每轮循环都会生成一个日志文件,记录“模型看到了什么失败、它决定怎么改”,方便你事后复盘。
有一次循环里出现了很经典的“无效修复”:AI在第一次实现时发现send_verification_email里需要访问数据库里的用户邮箱,它没查清User模型字段名,直接写了user.email_address,结果测试报错AttributeError。第二次循环它看到这个错误,没有去查模型定义,反而在函数里加了一个getattr(user, "email_address", user.email)的兼容写法。测试过了,代码却极其丑陋。
后来我在实现Agent的Prompt里加了一句:“遇到AttributeError时,必须读取模型类定义文件,而不是使用getattr规避。”这次教训让我明白:AI的试错循环需要引导,不然它会在错误的路径上越走越远。
4.4 生成实现代码与人工Review清单
经过两三轮循环后,测试全绿。此时实现代码已经生成,大概是这样的(简化版):
import secrets from datetime import datetime, timedelta def send_verification_email(user): token = secrets.token_hex(16) user.verification_token = token user.token_created_at = datetime.utcnow() send_email(user.email, f"/verify?token={token}") def verify_email_token(user, token): if is_token_expired(user.verification_token, user.token_created_at): return False if user.verification_token != token: return False user.status = "verified" return True def is_token_expired(stored_token, created_at): return datetime.utcnow() - created_at > timedelta(hours=24)第一眼看上去是能工作的,但我发现一个严重问题:is_token_expired函数根本没有使用传入的stored_token参数,只用了created_at。这就是典型的“测试驱动出无关参数”的产物。测试是因为这个函数签名才写的,实现也是照签名写的,但两者都对“为什么需要这个参数”毫无反应。
这里就需要人工Review了。我每次做Review都带一张清单,现在分享给你:
- 测试名称是否真正反映了Feature里的行为?
- 测试断言的粒度是否过于宽松?(比如只判断返回了True,却没验证用户状态)
- 实现代码中是否有“为了过测试而硬编码”的部分?
- 是否有未经过测试的额外逻辑被顺手加进了实现?
- 函数参数是否都有实际用途?
- 边界条件(空值、超时、非法token)是测试覆盖的还是人工脑补的?
- 生成代码是否引入了不必要的外部依赖或内部状态?
这张清单是我们团队在无数次review之后总结出来的,你直接拿去用也行。记住一点:Review不是走过场,而是闭环里唯一的“防呆”机制。AI负责效率和发散,人负责质量和收敛。
5. 踩坑实录:Behavior-Driven AI-TDD 的典型问题与排查方法
这套流程跑了几个月,踩过的坑比想象中多。我挑几个最有代表性的问题,连同排查思路一起写在下面,当作一份问题速查表。
5.1 AI生成的测试覆盖了错误行为
这是最常见、也是最隐蔽的问题。AI在生成测试时,会把“它认为应该发生的行为”写进去,而不是“Feature里描述的行为”。比如前面提到的邮箱验证功能,AI曾在测试里擅自添加“用户点击无效token后应返回404页面”,这个断言根本不在Feature里。测试全绿跑着,但它验证的是AI的想象,不是产品的需求。
排查方法很简单:把生成的测试代码和Feature文件逐句对照,凡是测试里出现Feature中没有明确提到的“预期结果”,都要打上问号。如果你发现这类问题比例比较高,就要回头检查提示词里的“不要擅自猜测”约束是不是被弱化了。我们后来把这条约束提到了Prompt的最前部,并在输出格式中增加了一个“假设清单(Assumptions)”字段,凡是模型做了推测,强制列出。这样至少能让推测可见,再决定是否采纳。
5.2 测试代码膨胀与“假绿”
AI生成代码有一个通病:为了覆盖更多场景,它会源源不断地生成测试,哪怕很多测试是重复的。有一次CI的pytest耗时从2分钟暴涨到15分钟,排查后发现是AI给同一个函数生成了三套功能相同的测试,只是命名不同。
另一个更可怕的问题是“假绿”。AI为了通过测试,可能在实现里写if True: return ...或者偷偷mock掉关键依赖。我们遇到过AI在测试代码里用autouse=True的fixture替换了所有数据库查询为返回固定对象,导致整个测试套件看着全绿,实际什么都没测到。
解决方案有两条:一是严格控制测试生成数量。在Agent的Prompt里写明“每个Scenario最多生成2个测试函数,优先覆盖核心行为,不要拆分过多helper函数”。二是把“覆盖率变化”作为CI准入门槛。如果新Feature提交后,相关模块的覆盖率反而下降,则亮红灯。覆盖率不是万能,但能挡住一部分“假绿”。
5.3 大模型上下文窗口限制与分片策略
我们早期的实现Agent经常写到一半就“失忆”——它读过整个测试文件后,生成实现到第三个函数时,开始重复第一个函数的逻辑。根因就是上下文窗口被占满了,尤其当你让模型“先阅读业务关联的5个模块”时。
后来我们给Agent加了一个“文件级分片”策略:只要项目文件超过一定大小,就先让模型输出一个“实现计划”列表,计划只包含函数名和一句话描述;然后按照计划逐函数生成,每次只给模型看“一个函数对应的测试代码 + 依赖函数签名”,生成完一个函数再流式地生成下一个。这样虽然增加了交互次数,但每个环节的上下文更聚焦,生成质量明显提升。
这里有个小技巧:分片后,每次生成的补丁会因为没有全局视角而产生“接口不一致”的问题。解决办法是在分片Prompt里固定注入一个“公共类型定义片段”,把项目中已有枚举、DTO、异常类型提前贴出来,相当于给模型一张“地图”。
5.4 重构阶段AI改坏既有测试
当AI辅助重构时,最头疼的是它在改进实现的同时,偷偷改了测试来迎合新代码。我们管这个叫“让测试适应代码”,完全违反了TDD精神。
排查记录里有一个典型例子:AI在重构时发现某个测试里频繁构造一个复杂对象,觉得浪费时间,就顺手把测试中的构造调用替换成了一个提前定义好的fixture,顺便把断言里的期望值改了。表面上看重构后测试全绿,但一个关键业务规则(对象A的状态必须是“初始态”)被静默抹掉了。
为了防住这个问题,我们给重构Agent加了两个硬性约束:
- 禁止修改已通过的测试文件。
- 如果必须修改测试,先输出DB-level报告说明修改原因,并请求人工审批。
在Agent框架里实现这个约束,只需要在重构Agent的工具调用权限上做一个开关:它只能写“实现目录”或“生成新增测试文件”,不能覆盖既有测试文件。这个权限控制是必须做的。
5.5 团队协作节奏与代码评审的新问题
AI-TDD重构了工作流,也重构了团队协作方式。我一开始以为最难的环节是技术,结果发现最难的是“人的不信任感”。有些老员工认为AI生成的测试不可信,坚持自己重新手写一遍测试。有些新员工则过于信任AI,测试红了也不看原因,直接让AI“修一下”,结果越修越糟。
我们后来调整了协作节奏,把原来的“每天下班前push”变成了“每个行为闭环完成后push”。也就是说,一个Feature从生成到测试到实现到review通过后,才形成一个最小的可合并单元。这个“行为闭环单元”的粒度,比过去的“文件”或“功能点”更清晰,也更适合AI辅助评审。
代码评审也随之变了味。以前review是看代码逻辑,现在review有一半时间是在看“AI的假设是否正确”。我在PR模版里加了一个“AI行为摘要”区,要求提交者把本次AI生成过程中出现的假设、违规项、人工修正点都写进去,评审者只需要重点核对这块,效率高了不少。
6. 一些个人体会和扩展方向
如果要把这套流程推广到别的团队,我不建议直接照搬。每个团队的技术栈、产品形态、数据安全要求都不一样。但有一个推进顺序是可以参考的,我先写在这里。
6.1 我建议的工作流落地顺序
第一步,先在非核心模块做小范围试点。找一个内部工具或低风险功能,用Feature文件描述清楚行为,让AI生成测试,人工补齐实现,跑通完整闭环。先不要急着写Agent框架,可以用命令行脚本模拟,哪怕每次手动复制粘贴Prompt也行,目的是验证“行为定义到测试到实现”的翻译质量。
第二步,提炼你团队自己的提示词模板。把试点阶段发现的各种“AI自由发挥”案例整理成负面约束,把“可复用的项目上下文”整理成固定注入片段,慢慢形成一套专属模板库。
第三步,再把Agent框架化。当提示词稳定后,你会发现把这些模板包进一个工具里是顺理成章的事,因为接口清楚了。此时再考虑引入多Agent、权限控制、CI联动。
最后一步,才去优化模型选型和微调。如果你发现通用模型在特定框架上的测试生成经常出错,可以考虑收集一批历史Featurefile和对应测试代码,做一个小范围微调。这一步成本较高,但收益也最持久。不过微调前一定要问自己:是不是模板没写好?多数时候,模板的ROI远远高于微调。
6.2 这个闭环未来还能往哪走
我自己比较看好的扩展方向是“行为回放”。既然Feature文件是行为基线,测试是行为快照,那就可以在每次发布前,把生产环境的请求日志抽象成新的Behaviour文件,自动比对现有测试是否覆盖了线上的真实行为。这相当于把APM(应用性能监控)升级成了行为监控,尚处在很早期的探索阶段。
另一个方向是“自动生成行为特征树”。大模型可以把一个复杂的用户故事分解成树状行为节点,每个叶子节点对应一个场景,然后自动检查测试覆盖是否完备。我和团队尝试过在调研阶段用这个思路来评估遗留项目的测试缺口,效果很直观。
我想强调的是,Behavior-Driven AI-TDD不是银弹,它不会让软件研发变得“全自动”。它更像一条重新校准的生产线:把人类从繁琐的测试编码中解放出来,要求人类用更严谨的REQUIREMENTS去约束AI的发挥。我个人体会最深的一点是:过去我担心AI会让开发者的水平下降,现在我发现,当开发者被要求把行为定义到机器可执行的精度时,他们对业务的理解反而更深了。这大概就是大模型时代工程师的新基本功:用清晰的规格喂养机器,用严格的review守护系统。