Grab uses AI Agent to boost productivity: detailed explanation with examples

This article has a short video:TikTok

1. Overview

Many large tech companies like Grab are shifting from “using Q&A chatbots” to “letting AI Agents take autonomous action” to boost engineering productivity: auto-reviewing code, auto-summarizing pull requests, auto-triaging incidents. This article explains what makes an AI Agent fundamentally different from a plain LLM call, the common Planner – Tool – Executor architecture, and how you can build a similar agent yourself using Java 21 + Spring AI. After reading, you’ll be able to write a small agent that calls real tools (read files, call internal APIs) and know when not to put an agent in production.

2. How is an AI Agent different from a regular LLM call?

A plain LLM call takes a prompt and returns text — no state, no action. An AI Agent has three key characteristics:

  • Self-planning: breaking a large request into smaller steps.
  • Tool calling (function calling): an agent can invoke APIs, read files, run queries — not just generate text.
  • Observe–act loop: the agent observes the tool result, decides the next step, and repeats until the task is complete.

Real-world example from large engineering teams: an agent reads a pull request diff, calls a tool to “fetch the team’s coding conventions”, calls a tool to “run static analysis”, then synthesizes the results into a review comment — all without anyone waiting for each step.

Note: If your task is just “summarize text” or “translate language”, a plain LLM call is enough. Don’t slap the “agent” label on everything that calls OpenAI API — unnecessary complexity makes your system harder to debug.

3. Basic agent architecture: Planner – Tool – Executor

The most common model has three parts:

User request
   │
   ▼
[Planner]  -- generates a list of steps to take, or selects the next tool
   │
   ▼
[Tool Executor] -- invokes real tools: APIs, databases, file system
   │
   ▼
[Observation] -- tool result is fed back to the Planner
   │
   └── repeat until Planner says "done"

The Planner is typically an LLM given a “menu” of available tools (name, description, parameter schema). The LLM doesn’t run code itself — it only decides which tool to call with what parameters; your code is the one who actually executes it.

3.1. Required dependencies

Spring AI provides a pretty clean abstraction for this pattern:

<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. Hands-on example: Code Review Agent

Suppose you want an agent that receives a diff from an Order service, checks conventions automatically, and suggests fixes. First, declare the tools the agent is allowed to call — here, reading coding conventions from an internal file.

@Component
public class ConventionTool {

    @Tool(description = "Fetch the current coding convention for the team for a given language")
    public String getConvention(String language) {
        return switch (language.toLowerCase()) {
            case "java" -> "Use records for DTOs; no @Autowired fields; log using SLF4J.";
            default -> "No specific convention yet for this language.";
        };
    }
}
  • @Tool is a Spring AI annotation marking this method as a function the LLM can call.
  • description is what the LLM reads to decide “should I call this tool?” — write clearly, the LLM will pick correctly.

Next is the service orchestration — the 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("You are a reviewer. Use tools if needed to check conventions before commenting.")
                .user("Review the following diff:\n" + diff)
                .call()
                .content();
    }
}
  • defaultTools(conventionTool) registers the tool for every request from chatClient.
  • When you call review(diff), Spring AI automatically sends the “tool menu” along with the prompt to the model. If the model decides to call getConvention("java"), the framework automatically executes the corresponding Java method, gets the result, sends it back to the model so the model generates a final answer — this entire loop happens in one call().

Try it out:

@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);
    }
}

Typical output:

Team conventions require using records for DTOs and SLF4J for logging.
In this diff, the OrderDto class is a regular class with getters/setters —
it should be converted to a record. Line 42 uses System.out.println instead of a logger,
needs to be fixed according to conventions.

The key point: you don’t write logic “if we see a regular class then suggest a record” — the model reasons it out based on the convention fetched from the tool. That’s why an agent “boosts productivity”: review no longer depends on people remembering conventions manually.

5. Agent calling multiple tools: PR Summary Agent

In reality, large teams often need an agent to call multiple tools in sequence — for example: fetch the list of changed files, fetch test coverage, then summarize the PR for reviewers.

@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 = "Get the list of changed files in a PR by id")
    public List<String> listChangedFiles(long prId) {
        return gitService.changedFiles(prId);
    }

    @Tool(description = "Get the test coverage percentage for a file")
    public double getCoverage(String filePath) {
        return coverageService.coverageOf(filePath);
    }
}
  • Two independent tools, each does one small thing, easy to test individually.
  • The model will decide the call order on its own: usually calls listChangedFiles first, then calls getCoverage for each returned file — this is exactly the “observe – act” loop mentioned in section 3.
@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("Summarize the PR with: number of files changed, which files have coverage below 70% need attention.")
                .user("Summarize PR id " + prId)
                .call()
                .content();
    }
}

This kind of agent solves the real productivity problem: reviewers don’t need to open each file and check coverage themselves — the agent has already compiled a summary for them.

6. Testing agents

The hardest part about deploying agents in production isn’t calling LLMs, but ensuring tools are called correctly as inputs change. So you should separate unit tests for tools (deterministic) and integration tests for agents (can 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("No specific convention yet for this language.");
    }
}
  • Testing tools is like testing a regular method, no LLM mocking needed — this is the part you control 100%.
  • For agents calling real LLMs, write tests at the contract level: assert that the response contains mandatory keywords (e.g., “record”, “coverage”), avoid asserting exact strings since LLM output isn’t 100% stable across runs.
@Test
void reviewShouldMentionConventionKeyword() {
    String diff = "class OrderDto { private String id; }";

    String result = codeReviewAgent.review(diff);

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

Note: testing with real LLMs costs money and is slow — separate this into a test suite (e.g., tag @Tag("llm-integration")) and only run in a dedicated pipeline, not on every commit.

7. Common pitfalls when deploying agents

  • Vague tool descriptions: if a tool description isn’t clear, the model picks the wrong tool or doesn’t call when it should. Write descriptions like Javadoc for someone reading it for the first time.
  • No iteration limit: an agent can get stuck in a loop repeatedly calling tools if the end condition isn’t clear. Always set maxIterations or a timeout at the orchestration layer.
  • Giving agents permission to write sensitive data: don’t let an agent have a tool to “deploy production” or “delete data” without a human confirmation step. Agents should have broad read permissions but narrow write permissions.
  • Not logging the tool-calling flow: when an agent gives a wrong answer, you need to know which tools it called, with what parameters, to debug. Missing this log makes tracing errors nearly impossible.
  • Using an agent for something a simple rule can solve: if the logic “if X then Y” is fixed, writing regular code is faster, cheaper, and easier to test than an agent.

8. When you shouldn’t use an AI Agent

Agents work well for tasks with many variable steps, needing to gather info from multiple sources, and accepting small errors (review suggestions, summaries). Agents don’t work well for:

  • Tasks needing absolute accuracy (billing, financial transactions).
  • Tasks with fixed workflows that rarely change — a rule engine or workflow engine (Camunda, Temporal) is more stable and easier to audit.
  • Systems needing low latency, since each tool-calling loop costs another round-trip to the LLM.

9. Conclusion

AI Agents deliver clear productivity value when applied correctly: repetitive engineering tasks, multiple steps, needing info synthesis — code review, PR summaries, error triage. Instead of building an agent that “knows everything”, start small with a few clear tools with their own tests, then let the Planner (LLM) decide how to coordinate them. Most important: separate permissions — give agents broad read access, narrow write access, and always keep humans in the loop for high-impact decisions.

AI AgentSpring AIProductivityLLMAutomation