Grab 用 AI Agent 提升生产力:详细讲解及代码示例

本文有一个短视频:TikTok

1. 概览

许多像 Grab 这样的大型科技公司正在从“使用聊天机器人问答”转变为“使用 AI Agent 自动执行操作”以提升技术生产力:自动代码审查、自动总结拉取请求、自动事件分类。本文解释了 AI Agent 的本质与单纯 LLM 调用的区别、常见的 Planner-Tool-Executor 架构,以及如何用 Java 21 + Spring AI 构建类似的 agent。阅读完后,你将能够自己编写一个小型 agent 来调用真实工具(读取文件、调用内部 API),以及了解何时不应该将 agent 引入生产环境。

2. AI Agent 与普通 LLM 调用的区别

单纯的 LLM 调用接收 prompt,返回文本——没有状态,没有操作。AI Agent 则有三个特征:

  • 自动规划(planning): 将大的请求分解为较小的步骤。
  • 调用工具(tool calling / function calling): agent 可以调用 API、读取文件、执行查询——不只是生成文本。
  • 观察-行动循环(observe-act loop): agent 观察工具返回的结果,决定下一步,重复直到完成任务。

大型工程团队中的实际例子:agent 读取拉取请求的 diff,调用工具“获取团队的编码规范”,调用工具“运行静态分析”,然后综合为审查注释——一切都不需要人坐等每一步。

注意: 如果你的工作只是“总结文本”或“翻译语言”,单纯的 LLM 调用就足够了。不要把“agent”的标签贴在所有调用 OpenAI API 的东西上——不必要的复杂化会使系统难以调试。

3. Agent 基础架构:Planner-Tool-Executor

最常见的模式包含三个部分:

用户请求
   │
   ▼
[Planner]  -- 生成需要执行的步骤列表,或选择下一个工具
   │
   ▼
[Tool Executor] -- 调用真实工具:API、数据库、文件系统
   │
   ▼
[Observation] -- 工具结果被返回给 Planner
   │
   └── 重复直到 Planner 报告"完成"

Planner 通常是一个 LLM,被授予“工具菜单”(名称、描述、参数 schema)。LLM 不会自己运行代码——它只是决定调用哪个工具及其参数;你的代码才是真正的执行者。

3.1. 所需依赖

Spring AI 为这个模式提供了相当简洁的抽象:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini

4. 实战示例:代码审查 Agent

假设你想要一个 agent 接收 Order 服务的 diff,自动检查规范并提出修改建议。首先,声明 agent 被允许调用的工具——这里是从内部文件读取编码规范的工具。

@Component
public class ConventionTool {

    @Tool(description = "获取团队当前针对某种语言的编码规范")
    public String getConvention(String language) {
        return switch (language.toLowerCase()) {
            case "java" -> "使用 record 作为 DTO;不要 @Autowired 字段;使用 SLF4J 进行日志记录。";
            default -> "此语言还没有专门的编码规范。";
        };
    }
}
  • @Tool 是 Spring AI 的注解,标记此方法为 LLM 可调用的函数。
  • description 正是 LLM 读取以决定“是否应该调用此工具”的内容——写得清楚,LLM 才能选择正确。

接下来是服务编排——Planner + Executor 的角色所在:

@Service
public class CodeReviewAgent {

    private final ChatClient chatClient;

    public CodeReviewAgent(ChatClient.Builder builder, ConventionTool conventionTool) {
        this.chatClient = builder
                .defaultTools(conventionTool)
                .build();
    }

    public String review(String diff) {
        return chatClient.prompt()
                .system("你是一个审查员。在发表意见之前,如果需要,请使用工具获取编码规范。")
                .user("请审查以下 diff:\n" + diff)
                .call()
                .content();
    }
}
  • defaultTools(conventionTool) 为 chatClient 的所有请求注册工具。
  • 当调用 review(diff) 时,Spring AI 自动将“工具菜单”和 prompt 发送给模型。如果模型决定调用 getConvention("java"),框架会自动执行对应的 Java 方法,获得结果,将其发送回给模型以生成最终答案——整个循环发生在单个 call() 调用内。

尝试调用:

@RestController
@RequestMapping("/api/review")
public class ReviewController {

    private final CodeReviewAgent agent;

    public ReviewController(CodeReviewAgent agent) {
        this.agent = agent;
    }

    @PostMapping
    public String review(@RequestBody String diff) {
        return agent.review(diff);
    }
}

典型输出:

团队编码规范要求使用 record 作为 DTO,使用 SLF4J 进行日志记录。
在此 diff 中,OrderDto 类是普通类,有 getter/setter——
应该改为 record。第 42 行使用 System.out.println 而不是 logger,
需要根据规范进行修改。

关键点是:你不需要编写“如果看到普通类就建议使用 record”的逻辑——模型基于从工具获得的规范自动推理。这就是 agent “提升生产力”的原因:审查不再依赖于人工记忆规范。

5. Agent 调用多个工具:PR 总结 Agent

实际上,大型团队通常需要 agent 依次调用多个工具——例如:获取已改更文件列表、获取测试覆盖率,然后为审查员总结 PR。

@Component
public class PullRequestTools {

    private final GitService gitService;
    private final CoverageService coverageService;

    public PullRequestTools(GitService gitService, CoverageService coverageService) {
        this.gitService = gitService;
        this.coverageService = coverageService;
    }

    @Tool(description = "根据 PR id 获取已改更的文件列表")
    public List<String> listChangedFiles(long prId) {
        return gitService.changedFiles(prId);
    }

    @Tool(description = "获取某个文件的测试覆盖率百分比")
    public double getCoverage(String filePath) {
        return coverageService.coverageOf(filePath);
    }
}
  • 两个工具是独立的,每个工具做一件小事情,易于单独测试。
  • 模型会自动决定调用顺序:通常先调用 listChangedFiles,然后为每个返回的文件调用 getCoverage——这正是第 3 部分提到的“observe-act”循环。
@Service
public class PullRequestSummaryAgent {

    private final ChatClient chatClient;

    public PullRequestSummaryAgent(ChatClient.Builder builder, PullRequestTools tools) {
        this.chatClient = builder.defaultTools(tools).build();
    }

    public String summarize(long prId) {
        return chatClient.prompt()
                .system("总结 PR:包括改更文件数、哪些文件覆盖率低于 70% 需要关注。")
                .user("总结 PR id " + prId)
                .call()
                .content();
    }
}

这种 agent 正确解决了生产力问题:审查员不需要自己打开每个文件检查覆盖率——agent 已经在一份总结中综合好了。

6. Agent 测试

将 agent 投入实际使用时,最难的部分不是能够调用 LLM,而是确保工具被正确调用当输入改变时。因此应该分别对工具进行单元测试(确定性的)和对 agent 进行集成测试(可以 mock ChatClient)。

class ConventionToolTest {

    private final ConventionTool tool = new ConventionTool();

    @Test
    void shouldReturnJavaConvention() {
        String result = tool.getConvention("Java");

        assertThat(result)
                .contains("record")
                .contains("SLF4J");
    }

    @Test
    void shouldReturnDefaultMessageForUnknownLanguage() {
        String result = tool.getConvention("cobol");

        assertThat(result).isEqualTo("此语言还没有专门的编码规范。");
    }
}
  • 测试工具类似于测试纯方法,不需要 mock LLM——这是你能 100% 控制的部分。
  • 对于调用真实 LLM 的 agent 部分,应该在“契约”级别写测试:assert response 包含必需的关键词(例如“record”、“coverage”),避免 assert 精确字符串,因为 LLM 输出在调用之间不是绝对稳定的。
@Test
void reviewShouldMentionConventionKeyword() {
    String diff = "class OrderDto { private String id; }";

    String result = codeReviewAgent.review(diff);

    assertThat(result.toLowerCase()).containsAnyOf("record", "convention");
}

注意: 测试调用真实 LLM 很费钱且很慢——应该分离出一个测试套件(例如标签 @Tag("llm-integration"))并仅在专门的 pipeline 中运行,不要在每次 commit 时运行。

7. 部署 Agent 时的常见错误

  • 工具描述模糊: 如果工具描述不清楚,模型可能选错工具或在需要时不调用工具。写描述要像为陌生人第一次阅读写 Javadoc。
  • 没有限制循环次数: 如果终止逻辑不清楚,agent 可能陷入持续调用工具的循环。在编排层始终设置 maxIterations 或超时。
  • 授予 agent 敏感数据写入权限: 不要让 agent 有“部署到生产”或“删除数据”的工具而不经过人工确认步骤。Agent 应该有广泛的读权限,有限的写权限。
  • 不记录工具调用过程: 当 agent 给出错误答案时,你需要知道它调用了哪些工具、使用了什么参数来调试。缺少这样的日志会使错误追踪几乎不可能。
  • 对可以通过简单规则解决的问题使用 agent: 如果逻辑“如果 X 则 Y”是固定的,编写代码通常比 agent 更快、更便宜、更易测试。

8. 何时不应该使用 AI Agent

Agent 适合于有许多不固定步骤、需要从多个来源综合信息并接受小误差(建议审查、总结)的工作。Agent不适合:

  • 需要绝对准确性的任务(计费、处理金融交易)。
  • 有固定流程、很少变化的任务——规则引擎或工作流引擎(Camunda、Temporal)仍然更稳定且更易审计。
  • 系统需要低延迟,因为每个工具调用循环都需要多一次往返 LLM。

9. 总结

当 AI Agent 被正确应用到合适的地方时,能带来明显的生产力价值:重复的工程任务、多步骤、需要综合分散信息的工作——代码审查、PR 总结、问题分类。与其写“无所不能”的 agent,不如从少数几个小的、清晰的、有独立测试的工具开始,然后让 Planner(LLM)决定如何协调。最重要的是权限分离:赋予 agent 广泛的读权限、有限的写权限,并始终让审查员在有重大后果的操作的最后一步。

AI AgentSpring AIProductivityLLMAutomation