Grab 用 AI Agent 提升生产力:详细讲解及代码示例
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 广泛的读权限、有限的写权限,并始终让审查员在有重大后果的操作的最后一步。