从Copilot到Agent:AI编程工作流的重构与落地实践

目录

适合谁读:已经在用 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 下已发布的迁移脚本,只能新增
- 不要引入新的第三方依赖,除非任务说明中明确允许
- 不要在日志中打印手机号、身份证号、密码等敏感信息

规则文件的编写原则:

  1. 只写 Agent 不看代码就无法知道的事:项目结构 Agent 自己能读出来,但"为什么禁止 double"它无法推断;
  2. 给出可执行的验证命令:这是 Agent 判断"自己做没做对"的依据;
  3. 持续维护:每次在审查中纠正 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 才是可靠的生产力,否则只是更快地产生需要返工的代码。

参考资料

相关推荐
rolt1 小时前
向窃·格瓦拉学习本体建模基础知识-第7赛季第22轮
软件工程·uml·面向对象·数理逻辑
Sammyyyyy2 小时前
jev-ultrafast 实战:浏览器操作代理加本地 AI 网关,把云控制台配置做成自动化流水线
运维·人工智能·自动化·ai编程·servbay
啵啵啵鱼2 小时前
对数据的增删改查
数据库·mysql·ai·idea·ai编程·navicat·obsidian
AINative软件工程3 小时前
LLM 异步任务的 Webhook 工程实践:回调可靠投递、重试与幂等消费的生产设计
后端·ai编程
小蠢驴打代码15 小时前
给 Codex Plan 模式加一本决策账本:问过什么、选了什么,都进仓库
开源·openai·ai编程
就叫飞六吧16 小时前
nat打洞和frp区别
ai编程
桃西西呀17 小时前
LangGraph4j之一:客服请求分诊既要循环又要分流,我用 LangGraph4j 把控制流抽成了数据
人工智能·llm·ai编程
长路 ㅤ   17 小时前
阿里云 AgentCore 深度调研:企业级 Agent 构建与治理平台的能力与边界
ai agent·ai智能体·云端agent·本地agent·agent基础设施
奈落2418 小时前
独立的AI编程助手正在消失?从文心快码并入百度搭子、Qwen Code转向CLI,看编程工具正被吸收进更大的智能体平台
百度·agent·ai编程·ai 编程