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 擅长做清晰且可验证的工作。设计、业务决策和最终责任仍然属于你。

aicoding-agenttech-trendstestingdeveloper-productivity