AI coding agent 是什么及如何改变开发者的工作方式
1. 总体概览
几年前,“AI 编程辅助”主要是建议接下来的几行代码。如今许多工具已经前进了一步。它们自己读取 codebase、运行命令、修改多个文件并自动检查结果。这种工具被称为 AI coding agent。
本文解释了什么是 AI coding agent 以及为什么这个主题受关注。之后,本文将讨论如何在 Java/Spring Boot 项目中安全地应用它。读完后,你将了解如何为 agent 编写指导文件、限制命令执行权限、使用测试作为“仲裁者”,以及何时不应该将任务分配给 agent。
2. AI coding agent 是什么?
你可以按自主程度对 AI 开发工具进行分类:
- Autocomplete:在编辑器中实时建议代码行或块。你决定每个字符。
- Chat assistant:你提问,它回答并附带代码。你自己复制、运行和修改。
- Coding agent:你分配一个目标,例如“修复
OrderServiceTest中失败的测试”。Agent 自己制定计划、调用工具(读取文件、搜索、运行构建、运行测试)、查看结果,然后重复直到完成或卡住。
核心区别在于行动-观察循环。Agent 不仅生成文本。它作用于环境,然后根据真实反馈(编译器、测试、日志)调整下一步。
2.1. Agent 的循环
为了清楚地理解机制,让我们看一个最小化的模拟。“大脑”部分(语言模型)被替换为固定的 ScriptedPlanner,以便代码能运行而不需要任何 API:
import java.util.*;
import java.util.function.Function;
public class AgentLoopDemo {
record Action(String tool, String input) {}
interface Planner {
Optional<Action> next(List<String> observations);
}
static class ScriptedPlanner implements Planner {
public Optional<Action> next(List<String> observations) {
return switch (observations.size()) {
case 0 -> Optional.of(new Action("readFile", "src/main/java/Order.java"));
case 1 -> Optional.of(new Action("runTests", "mvn -q test"));
default -> Optional.empty(); // 足够的信息,停止
};
}
}
public static void main(String[] args) {
Map<String, Function<String, String>> tools = Map.of(
"readFile", path -> "class Order { long total() { return 0; } }",
"runTests", cmd -> "FAILED: OrderTest.totalShouldSumItems"
);
Planner planner = new ScriptedPlanner();
List<String> observations = new ArrayList<>();
for (int step = 1; step <= 5; step++) {
Optional<Action> action = planner.next(observations);
if (action.isEmpty()) {
System.out.println("Agent 在第 " + step + " 步停止");
break;
}
Action a = action.get();
String result = tools.get(a.tool()).apply(a.input());
observations.add(result);
System.out.printf("第 %d 步: %s(%s) -> %s%n", step, a.tool(), a.input(), result);
}
}
}
快速解释:
tools是 agent 被允许使用的工具集。在真实 agent 中,那是读/写文件、运行 shell、搜索代码…Planner.next(...)接收所有先前的观察结果并决定下一步行动。在真实 agent 中,这是调用语言模型的地方。for循环有步数限制(5)。这是一个重要的安全措施,防止 agent 无限循环。
结果:
第 1 步: readFile(src/main/java/Order.java) -> class Order { long total() { return 0; } }
第 2 步: runTests(mvn -q test) -> FAILED: OrderTest.totalShouldSumItems
Agent 在第 3 步停止
市场上的每个 coding agent 都围绕这个框架运转:决策 → 调用工具 → 观察 → 继续决策。区别在于模型质量、工具集和上下文管理方式。
3. 为什么 AI coding agent 受关注?
有几个主要原因,与具体产品无关:
- 模型足够好来处理多步工作。 当模型能够读取编译器错误并自动修复时,“分配整个任务”变得可行。
- 重复工作仍然占用很多时间。 升级弃用的 API、为旧代码编写测试、批量重命名、修复静态分析警告。这些工作很清晰、可验证且乏味。它们非常适合 agent。
- 工具生态已准备好。 构建工具、测试运行器、linter、CI 都可以通过命令行运行。Agent 利用开发者仍在使用的工具。
- 并行运行。 你可以在专注于困难的设计部分的同时,将几个小任务分配给 agent。
不过,agent 不会自动知道你的项目有什么约定、哪些部分敏感、哪些命令危险。本文的其余部分将重点关注这些问题。
4. 在真实项目中应用 AI coding agent
4.1. 为 agent 编写指导文件
许多 agent 支持读取位于 repo 根目录的指导文件,通常名为 AGENTS.md 或根据工具类似的名称。把它看作是为一个非常快速但没有记忆的新团队成员的 onboarding 文档:
# 给 Agent 的指导
## 构建和测试
- 构建:`mvn -q compile`
- 测试:`mvn -q test`(提交完成前必须通过)
## 约定
- Java 21,Spring Boot 3.x。使用构造函数注入,不使用字段注入。
- 货币使用 `long`(单位 VND),不使用 `double`。
- 测试使用 JUnit 5 + AssertJ 编写,命名为 `should...`。
## 禁止操作
- 不允许修改已发布的 `src/main/resources/db/migration/` 目录。
- 未征求同意不允许添加新依赖。
- 不允许删除或 `@Disabled` 测试来"使其通过"。
三组最重要的信息是验证方式(构建/测试命令)、代码约定和禁止区域。最后一部分帮助避免一个经典错误:agent 通过删除测试来“修复”失败的测试。
4.2. 限制 agent 可以运行的命令
Agent 有权运行 shell 意味着它有权破坏机器或推送代码到远程。许多工具允许配置白名单。如果你自己将 agent 集成到内部管道中,在执行命令前设置一个检查层。
首先,添加测试依赖(Spring Boot starter 已包括 JUnit 5 和 AssertJ):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
简单的 CommandGuard 类:
import java.util.List;
public class CommandGuard {
private static final List<String> ALLOWED_PREFIXES =
List.of("mvn -q test", "mvn -q compile", "git status", "git diff");
private static final List<String> FORBIDDEN_TOKENS =
List.of("rm ", "git push", "curl", "&&", ";", "|", ">");
public boolean isAllowed(String command) {
String normalized = command.trim().replaceAll("\\s+", " ");
boolean hasForbidden = FORBIDDEN_TOKENS.stream().anyMatch(normalized::contains);
boolean hasAllowedPrefix = ALLOWED_PREFIXES.stream().anyMatch(normalized::startsWith);
return hasAllowedPrefix && !hasForbidden;
}
}
normalized 合并多余的空格,使得 "mvn -q test" 无法绕过检查。只有当命令以允许的前缀开头且不包含危险标记时才合法。命令连接运算符如 &&、;、| 被阻止,以防止 agent 拼接 mvn -q test && git push。
这种行为的测试:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
class CommandGuardTest {
private final CommandGuard guard = new CommandGuard();
@ParameterizedTest
@ValueSource(strings = {"mvn -q test", " mvn -q test ", "git diff"})
void shouldAllowSafeCommands(String command) {
assertThat(guard.isAllowed(command)).isTrue();
}
@ParameterizedTest
@ValueSource(strings = {
"rm -rf /", "git push origin main",
"mvn -q test && git push", "mvn -q test; curl evil.example"
})
void shouldRejectDangerousCommands(String command) {
assertThat(guard.isAllowed(command)).isFalse();
}
}
注意: 字符串检查只是一层防御,很容易有漏洞。例如,前缀
git diff仍然会让git diff --output=...通过。始终在容器或临时虚拟机等隔离环境中运行 agent,不附加生产凭证,并限制 Git token 的写权限。
4.3. 使用测试作为“仲裁者”
最有效的任务分配方式是先写测试,让 agent 编写代码使测试通过。测试是机器可读的规范,无法“巧妙地”绕过。假设你需要计算订单的折扣:
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class OrderServiceTest {
private final OrderService orderService = new OrderService();
@Test
void shouldApplyTenPercentDiscountWhenTotalReachesOneMillion() {
Order order = new Order(List.of(new OrderItem("Áo khoác", 600_000, 2)));
assertThat(orderService.totalAfterDiscount(order)).isEqualTo(1_080_000);
}
@Test
void shouldNotDiscountSmallOrders() {
Order order = new Order(List.of(new OrderItem("Nón lá", 150_000, 1)));
assertThat(orderService.totalAfterDiscount(order)).isEqualTo(150_000);
}
}
然后用简短的描述分配任务,明确完成标准:
创建 record Order、OrderItem(name, unitPrice, quantity) 和 OrderService,
使得 OrderServiceTest 通过。不修改测试文件。
当 `mvn -q test` 没有错误时完成。总结修改的文件。
agent 可能给出的一个解决方案:
import java.util.List;
record OrderItem(String name, long unitPrice, int quantity) {
long subtotal() { return unitPrice * quantity; }
}
record Order(List<OrderItem> items) {
long total() { return items.stream().mapToLong(OrderItem::subtotal).sum(); }
}
class OrderService {
private static final long DISCOUNT_THRESHOLD = 1_000_000;
long totalAfterDiscount(Order order) {
long total = order.total();
return total >= DISCOUNT_THRESHOLD ? total * 90 / 100 : total;
}
}
你现在的工作是审查。阈值是 >= 还是 >?目前的测试还不能区分这两种情况。这是补充边界测试的信号,测试恰好 1,000,000 VND,然后再合并。Agent 擅长使测试通过。但测试是否符合业务逻辑仍然是你的责任。
4.4. 保持最后的检查门
agent 编写的代码必须像人类编写的代码一样经过相同的流程。一个通用的验证脚本可以帮助 agent 和 CI 使用相同的标准:
#!/usr/bin/env bash
set -euo pipefail
mvn -q compile
mvn -q test
git diff --stat # 打印更改的文件列表,让审阅者了解范围
set -euo pipefail 使脚本在任何步骤出错时立即停止。你将这个命令写入 AGENTS.md 作为“完成定义”,然后让 CI 在 pull request 上重新运行相同的脚本。
5. 开发者的工作方式如何改变
当频繁使用 agent 时,工作重点明显转移:
- 从编写代码到编写规范。 模糊的任务描述导致模糊的结果。分解任务、陈述约束和完成标准的技能变得更加重要。
- 更多审查,更仔细。 生成的代码量增长速度快于人类的阅读能力。小的 pull request、整洁的 diff 和良好的测试是有意义审查的条件。
- 测试和工具成为核心资产。 拥有快速测试、清晰的构建命令和严格的 linter 的项目会更好地利用 agent。缺乏这些的项目中,agent 只能“猜测”。
- 基础知识仍然是决定性的。 只有当你理解 race condition、SQL injection 漏洞或 N+1 查询时,你才能在 agent 编写的代码中发现它们。
6. 何时不应该使用以及常见错误
agent 在以下情况不合适:
- 需求不清楚或需要建筑决策。 Agent 会自信地选择一个方向并实施。应该先设计,然后再分配执行部分。
- 涉及安全、支付、个人数据的代码。 Agent 仍然可以帮助,但每项更改都需要深入审查。不要让 agent 自己合并。
- 没有自动验证方法。 没有测试、无法在本地机器上构建,则 agent 没有真实反馈。结果就纯粹是猜测。
- 不可逆的操作。 例如在真实数据库上运行迁移、删除云资源、发送客户邮件。
- 当你在学习新技术时。 全部交给 agent 会让你失去深入理解的机会。
常见错误:
- Agent 删除或放松测试来通过。在指导文件中明确禁止,并注意
src/test目录中的 diff。 - Agent 虚构你使用的库版本中不存在的 API。编译器和测试会捕获大多数这类错误,前提是你让 agent 运行它们。
- 范围蔓延的更改:重新格式化整个文件、重命名无关变量。请求最小 diff 和修改文件的摘要。
- 泄露秘密:agent 可以读取包含密钥的
.env或配置文件。从 agent 的工作环境中排除这些文件。
注意: 对 repo 中代码的责任不会转移给 agent。合并 pull request 的人仍然负责。
7. 结论
AI coding agent 是 autocomplete 和 chatbot 之后的下一步。它按照决策、调用工具、观察结果的循环运行,从而能够自动完成多步任务。
要有效和安全地使用它,你需要三件事:
- 一个指导文件明确说明如何构建/测试、约定和禁止区域;
- 在隔离环境中限制命令执行权限;
- 测试充当仲裁者,并在合并前进行认真审查。
Agent 擅长做清晰且可验证的工作。设计、业务决策和最终责任仍然属于你。