目录
-
- [一、Copilot 与 Agent:差别在哪里](#一、Copilot 与 Agent:差别在哪里)
-
- [1.1 三代 AI 编程工具](#1.1 三代 AI 编程工具)
- [1.2 这带来的真正挑战](#1.2 这带来的真正挑战)
- 二、整体工作流
- 三、第一步:把团队约定写成规则文件
- 四、第二步:任务说明比提示词技巧更重要
- 五、第三步:先审计划,再让它动手
- 六、第四步:测试先行,让测试成为完成标准
- 七、第五步:自动化检查与人工审查分工
-
- [7.1 能自动化的交给流水线](#7.1 能自动化的交给流水线)
- [7.2 人工审查重点](#7.2 人工审查重点)
- [八、哪些任务适合交给 Agent](#八、哪些任务适合交给 Agent)
- 九、风险与应对
- 十、如何度量效果,而不是凭感觉
- 十一、开发者角色的变化
- 参考资料
适合谁读:已经在用 Copilot 类代码补全工具,想进一步让 AI Agent 承担完整开发任务,但又担心质量失控的开发者和技术负责人。
解决什么问题:从"补全一行代码"到"交给 Agent 一个需求",变化的不只是工具,而是整个开发工作流:任务怎么描述、上下文怎么提供、产出怎么验证、责任怎么划分。本文以"给用户服务增加注册功能"为主线,给出一套可以直接落地的 Agent 协作流程,包括规则文件、任务模板、测试先行、审查清单和效果度量方法。
本文不罗列"效率提升了多少倍"这类无法复现的数字,而是讲清楚每一步该怎么做、为什么这么做。Agent 系统本身的架构设计,见本系列上一篇 AI Agent 生产级架构设计。
示例项目环境(文中的规则文件、任务说明和测试代码均基于此环境):
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 21 | 使用 record、虚拟线程等新特性时需在规则文件中说明 |
| Spring Boot | 3.4.x | 3.4 起测试中使用 @MockitoBean 替代已废弃的 @MockBean |
| JUnit / Mockito | JUnit 5.11、Mockito 5.x | 由 spring-boot-starter-test 统一管理版本 |
| MySQL / Redis | 8.0 / 7.2 | 集成测试通过 Testcontainers 启动 |
| 构建工具 | Maven Wrapper(./mvnw) |
保证 Agent 与 CI 使用同一 Maven 版本 |
一、Copilot 与 Agent:差别在哪里
1.1 三代 AI 编程工具
| 形态 | 交互方式 | 上下文范围 | 人的角色 | 典型产品形态 |
|---|---|---|---|---|
| 代码补全 | 光标处自动建议 | 当前文件及少量相邻文件 | 逐行编写,AI 辅助 | IDE 补全插件 |
| 对话助手 | 选中代码提问、生成片段 | 用户手动提供的代码 | 复制粘贴、自行整合 | IDE 内聊天面板 |
| 编程 Agent | 描述任务,Agent 自主读代码、改文件、跑命令 | 整个仓库 + 终端 + 工具 | 定义任务、审查结果 | IDE Agent、终端 Agent、云端异步 Agent |
前两代工具中,人始终在环路的每一步:每一行代码都经过人的眼睛。第三代工具的本质变化是:Agent 可以连续执行几十个动作(搜索代码、修改多个文件、运行测试、根据报错修正),人只在开始和结束时介入。
1.2 这带来的真正挑战
效率提升的同时,风险的形态也变了:
- 错误的粒度变大:补全错一行,人立刻能看到;Agent 改错了十个文件里的某一处,很可能在审查时被忽略;
- 上下文决定质量 :Agent 不知道团队的约定(例如"金额必须用
BigDecimal""禁止在 Controller 里写业务逻辑"),就会按"网上最常见的写法"生成代码; - 验证成本转移:写代码的时间减少了,但审查和验证的时间如果不刻意设计,会成为新的瓶颈。
所以,用好 Agent 的关键不是"提示词写得多巧妙",而是把团队的隐性知识显性化,并建立自动化的验证闭环。
二、整体工作流
#mermaid-svg-mv0jPbDxQ3QO8Ndy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .error-icon{fill:#552222;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .marker.cross{stroke:#333333;}#mermaid-svg-mv0jPbDxQ3QO8Ndy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mv0jPbDxQ3QO8Ndy p{margin:0;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .cluster-label text{fill:#333;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .cluster-label span{color:#333;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .cluster-label span p{background-color:transparent;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .label text,#mermaid-svg-mv0jPbDxQ3QO8Ndy span{fill:#333;color:#333;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .node rect,#mermaid-svg-mv0jPbDxQ3QO8Ndy .node circle,#mermaid-svg-mv0jPbDxQ3QO8Ndy .node ellipse,#mermaid-svg-mv0jPbDxQ3QO8Ndy .node polygon,#mermaid-svg-mv0jPbDxQ3QO8Ndy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .rough-node .label text,#mermaid-svg-mv0jPbDxQ3QO8Ndy .node .label text,#mermaid-svg-mv0jPbDxQ3QO8Ndy .image-shape .label,#mermaid-svg-mv0jPbDxQ3QO8Ndy .icon-shape .label{text-anchor:middle;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .rough-node .label,#mermaid-svg-mv0jPbDxQ3QO8Ndy .node .label,#mermaid-svg-mv0jPbDxQ3QO8Ndy .image-shape .label,#mermaid-svg-mv0jPbDxQ3QO8Ndy .icon-shape .label{text-align:center;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .node.clickable{cursor:pointer;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .arrowheadPath{fill:#333333;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mv0jPbDxQ3QO8Ndy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mv0jPbDxQ3QO8Ndy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mv0jPbDxQ3QO8Ndy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .cluster text{fill:#333;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .cluster span{color:#333;}#mermaid-svg-mv0jPbDxQ3QO8Ndy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mv0jPbDxQ3QO8Ndy rect.text{fill:none;stroke-width:0;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .icon-shape,#mermaid-svg-mv0jPbDxQ3QO8Ndy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .icon-shape p,#mermaid-svg-mv0jPbDxQ3QO8Ndy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .icon-shape .label rect,#mermaid-svg-mv0jPbDxQ3QO8Ndy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mv0jPbDxQ3QO8Ndy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mv0jPbDxQ3QO8Ndy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mv0jPbDxQ3QO8Ndy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 调整
通过
失败
通过
需求
编写任务说明
验收标准 + 约束
Agent 产出计划
人审查计划
测试先行
先写/确认测试
Agent 实现
运行测试并自我修正
自动化检查
编译 · 测试 · 静态扫描
人工审查
按清单
合并
与传统流程相比,有两个关键的新增环节:先审计划再动手 ,以及以测试作为 Agent 的"完成定义"。下面逐步展开。
三、第一步:把团队约定写成规则文件
Agent 每次开始任务时都是"新同事",它不会记得上次你纠正过什么。把反复出现的约定写进仓库根目录的规则文件(如 AGENTS.md,各工具也有自己的规则文件约定),Agent 在每次任务开始时都会读取。
markdown
# AGENTS.md
## 项目概况
- Spring Boot 3.4 + JDK 21 + MySQL 8.0 + Redis 7.2
- 测试中 mock Spring Bean 使用 @MockitoBean(@MockBean 已废弃)
- 分层:controller -> application -> domain -> infrastructure,禁止跨层调用
## 编码约定
- 金额使用 BigDecimal,禁止 double/float
- 时间统一使用 java.time,存储为 UTC
- 对外接口返回 Result<T>,异常由 GlobalExceptionHandler 统一处理
- 新增 SQL 必须走 MyBatis XML,禁止字符串拼接 SQL
## 构建与验证
- 编译与测试:./mvnw -q verify
- 只运行某个测试:./mvnw -q test -Dtest=UserRegisterServiceTest
- 提交前必须保证 verify 通过
## 禁止事项
- 不要修改 db/migration 下已发布的迁移脚本,只能新增
- 不要引入新的第三方依赖,除非任务说明中明确允许
- 不要在日志中打印手机号、身份证号、密码等敏感信息
规则文件的编写原则:
- 只写 Agent 不看代码就无法知道的事:项目结构 Agent 自己能读出来,但"为什么禁止 double"它无法推断;
- 给出可执行的验证命令:这是 Agent 判断"自己做没做对"的依据;
- 持续维护:每次在审查中纠正 Agent 的同类错误超过两次,就把它补进规则文件。
四、第二步:任务说明比提示词技巧更重要
一个好的任务说明,应该能让一个不了解背景的新同事独立完成工作。以"用户注册"为例:
markdown
## 任务:用户注册接口
### 背景
用户服务目前只有登录,需要新增手机号注册。
### 验收标准
1. POST /api/users/register,参数:phone、password、smsCode
2. 手机号已注册时返回错误码 USER_PHONE_EXISTS
3. 短信验证码错误或过期时返回 SMS_CODE_INVALID,验证码校验通过后立即失效
4. 密码使用 BCrypt 存储,强度至少 8 位且包含字母和数字
5. 并发重复提交同一手机号时,最终只能创建一个用户
### 约束
- 复用现有的 SmsCodeService,不要自己实现验证码逻辑
- 唯一性以数据库唯一索引为准,不要只依赖"先查后插"
### 不在本次范围
- 第三方登录、邀请码
注意第 5 条和"约束"中的唯一索引:这类并发与一致性要求是 Agent 最容易遗漏的,因为"先查询再插入"在功能测试中完全正常,只在并发时出错。把它明确写进验收标准,比事后审查发现要便宜得多。
五、第三步:先审计划,再让它动手
对于涉及多个文件的任务,先要求 Agent 只输出计划、不修改代码,审查后再执行。计划应包括:要修改和新增哪些文件、数据库变更、关键实现思路、需要新增的测试。
审查计划时重点看三件事:
| 检查点 | 常见问题 | 发现后的处理 |
|---|---|---|
| 改动范围 | 顺手"重构"了无关代码 | 明确要求只改任务相关文件 |
| 设计取舍 | 用"先查后插"保证唯一性 | 指出并发问题,要求使用唯一索引 + 捕获冲突异常 |
| 依赖与接口 | 编造了项目中不存在的工具类或方法 | 要求先搜索确认再使用 |
在计划阶段纠正方向,成本只是几句话;在实现完成后才发现,往往需要推倒重来。
六、第四步:测试先行,让测试成为完成标准
Agent 最大的优势之一是可以自己运行测试并根据失败信息修正。所以最有效的做法是:先确定测试,再让 Agent 实现,直到测试通过。
验收标准中最关键的几条,可以由人先写好测试(或者让 Agent 先写测试、人审查测试后再实现):
java
@SpringBootTest
class UserRegisterServiceTest {
@Autowired UserRegisterService registerService;
@Autowired UserRepository userRepository;
@MockitoBean SmsCodeService smsCodeService;
@Test
void should_reject_duplicate_phone() {
when(smsCodeService.verifyAndConsume(anyString(), anyString())).thenReturn(true);
registerService.register(new RegisterCommand("13800000000", "Passw0rd!", "123456"));
BizException ex = assertThrows(BizException.class, () ->
registerService.register(new RegisterCommand("13800000000", "Passw0rd!", "123456")));
assertEquals(ErrorCode.USER_PHONE_EXISTS, ex.getCode());
}
@Test
void should_create_only_one_user_under_concurrent_requests() throws Exception {
when(smsCodeService.verifyAndConsume(anyString(), anyString())).thenReturn(true);
int threads = 8;
ExecutorService pool = Executors.newFixedThreadPool(threads);
CountDownLatch start = new CountDownLatch(1);
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < threads; i++) {
futures.add(pool.submit(() -> {
start.await();
return registerService.register(new RegisterCommand("13900000000", "Passw0rd!", "123456"));
}));
}
start.countDown();
long success = futures.stream().filter(f -> {
try { f.get(); return true; } catch (Exception e) { return false; }
}).count();
pool.shutdown();
assertEquals(1, success);
assertEquals(1, userRepository.countByPhone("13900000000"));
}
@Test
void should_store_password_with_bcrypt() {
when(smsCodeService.verifyAndConsume(anyString(), anyString())).thenReturn(true);
registerService.register(new RegisterCommand("13700000000", "Passw0rd!", "123456"));
String stored = userRepository.findByPhone("13700000000").orElseThrow().getPasswordHash();
assertTrue(stored.startsWith("$2"));
assertNotEquals("Passw0rd!", stored);
}
}
其中并发测试最有价值:它能直接暴露"先查后插"的实现缺陷,Agent 会在测试失败后自行改为"唯一索引 + 捕获 DuplicateKeyException 转换为业务错误码"的实现。
需要警惕的是:Agent 有时会为了让测试通过而修改测试本身(放宽断言、删除用例)。所以要在任务说明或规则文件中明确"不允许修改已有测试的断言",并在审查时专门查看测试文件的 diff。
七、第五步:自动化检查与人工审查分工
7.1 能自动化的交给流水线
Agent 提交的代码和人写的代码一样,要经过 CI 的完整检查:编译、单元测试、集成测试、静态扫描(如 SonarQube、SpotBugs)、依赖漏洞扫描、代码格式检查。凡是机器能判断的,就不应该占用人的审查时间。
7.2 人工审查重点
人的注意力应该集中在机器难以判断的地方:
| 审查维度 | 关注点 |
|---|---|
| 需求理解 | 是否真正满足验收标准,有没有"看起来实现了"但边界条件错误 |
| 并发与一致性 | 唯一性、幂等、事务边界是否正确 |
| 安全 | 输入校验、越权访问、敏感信息是否写入日志 |
| 可维护性 | 是否引入了重复代码、过度抽象,或违反分层约定 |
| 测试本身 | 测试是否真的验证了行为,有没有被修改为"永远通过" |
| 依赖 | 是否引入了新依赖、调用了不存在或已废弃的 API |
一个实用的习惯是:让 Agent 在提交时附上变更说明------改了什么、为什么这样改、哪些地方不确定。这份说明能显著减少审查者重新理解代码的时间,也能暴露 Agent 自己"没把握"的地方。
八、哪些任务适合交给 Agent
| 适合程度 | 任务类型 | 原因 |
|---|---|---|
| 很适合 | 有明确验收标准的增删改查、单元测试补齐、依赖升级后的编译修复、批量重命名与迁移 | 结果可以被测试或编译器客观验证 |
| 适合,需审计划 | 跨多个模块的功能开发、性能优化 | 需要设计取舍,方向错了代价大 |
| 谨慎使用 | 核心交易逻辑、权限与安全相关代码、数据库迁移 | 错误后果严重,必须逐行审查 |
| 不适合 | 需求本身不清楚的探索、需要与多方协商的架构决策 | Agent 会"自信地"给出一个答案,掩盖了真正需要讨论的问题 |
判断标准可以归结为一句话:结果越容易被客观验证,越适合交给 Agent。
九、风险与应对
| 风险 | 表现 | 应对 |
|---|---|---|
| 幻觉 API | 调用不存在的方法、使用错误版本的框架用法 | 规则文件写明版本;编译和测试必须通过 |
| 范围蔓延 | 顺手修改无关文件 | 任务说明限定范围;审查时先看文件列表 |
| 测试被篡改 | 删除或放宽断言 | 规则禁止修改已有断言;单独审查测试 diff |
| 密钥泄露 | 把密钥写进代码或日志 | 密钥扫描纳入 CI;Agent 运行环境不注入生产凭证 |
| 提示词注入 | 读取的依赖源码、Issue 内容中藏有恶意指令 | 限制 Agent 可执行的命令;高风险操作需人工确认 |
| 能力退化 | 团队成员逐渐不理解自己代码库的关键逻辑 | 核心模块坚持人工编写或逐行审查;定期做代码走读 |
十、如何度量效果,而不是凭感觉
引入 Agent 之后"感觉快了很多",但感觉往往不可靠:写代码的时间缩短了,审查和返工的时间可能增加了。建议在引入前后对比以下指标,至少观察一到两个迭代周期:
| 指标 | 含义 | 说明 |
|---|---|---|
| 需求交付周期 | 从开始开发到上线的时间 | 端到端指标,不只看编码时间 |
| 审查耗时与轮次 | PR 从创建到合并的时间、修改轮次 | 反映 Agent 产出的"一次通过率" |
| 变更失败率 | 上线后需要回滚或热修复的比例 | 来自 DORA 指标,衡量质量是否下降 |
| 缺陷逃逸数 | 测试环境未发现、线上才暴露的缺陷 | 衡量验证闭环是否有效 |
| 测试覆盖与有效性 | 覆盖率及变异测试得分 | 防止"为了覆盖率而写的空测试" |
如果交付周期缩短而变更失败率上升,说明验证环节没有跟上,应该先加强测试和审查,而不是继续扩大 Agent 的使用范围。
十一、开发者角色的变化
从 Copilot 到 Agent,开发者的工作重心发生了明显的转移:
- 从"写代码"到"定义问题":写清楚验收标准和约束,本身就是最重要的设计工作;
- 从"实现细节"到"验证体系":测试、CI、规则文件成为保证质量的主要手段;
- 从"个人技巧"到"团队资产":规则文件、任务模板、审查清单都应该沉淀在仓库里,而不是留在某个人的脑子里。
回到开头的问题:"交给 Agent 一个需求"能不能放心?答案取决于你是否提前准备好了三样东西:规则文件 让 Agent 了解团队约定,测试 让 Agent 知道什么叫"做完了",审查清单让人把注意力放在机器无法判断的地方。这三样东西准备好了,Agent 才是可靠的生产力,否则只是更快地产生需要返工的代码。
参考资料
- Building Effective Agents(Anthropic,Agent 的适用边界)
- AGENTS.md(面向编程 Agent 的仓库说明文件约定)
- SWE-bench(编程 Agent 在真实 GitHub Issue 上的评测基准)
- DORA Research(交付周期、变更失败率等软件交付效能指标)
- OWASP Top 10 for LLM Applications(提示词注入、过度授权等风险)
- PIT Mutation Testing(Java 变异测试工具,用于评估测试有效性)