DEV Community

Said Olano
Said Olano

Posted on

Agentic AI and Multi-Agent Systems: From Single Prompts to Coordinated Teams

Agentic AI and Multi-Agent Systems: From Single Prompts to Coordinated Teams

Most LLM applications today still follow a simple pattern: a user sends a prompt, the model returns a response, and the interaction ends. That works well for summarizing a document or drafting an email. It breaks down quickly when the task requires planning, looking up information from several sources, calling APIs, checking intermediate results, and recovering from errors.

This is where agentic AI comes in. An agentic system pursues a goal through a loop of reasoning, action, and observation. Instead of producing one answer, it decides what to do next, uses tools, inspects the results, and adjusts its plan until the goal is met or it determines it cannot proceed.

When several such agents collaborate, each with a focused responsibility, we get a multi-agent system. In this article we will look at the core ideas, the trade-offs, and how a Java team can build these systems with clean, testable boundaries.

Why Single-Prompt Applications Hit a Ceiling

A single large prompt that tries to do everything has several structural problems:

  • Mixed responsibilities. Planning, retrieval, formatting, and validation are all bundled into one instruction set, which makes them hard to test and improve independently.
  • No intermediate verification. If step two produces wrong data, step three builds on it without any check.
  • Brittle tool integration. Tool calls are often parsed from free text, which is fragile and hard to audit.
  • Poor failure isolation. When something goes wrong, the whole chain fails, and the logs rarely explain why.

Agentic design addresses these issues by making the steps explicit: who plans, who acts, who checks, and what state is shared between them.

The Agent Loop

At its core, an agent is a function that repeatedly does the following:

  1. Observe the current state: the goal, the conversation so far, and the results of previous actions.
  2. Decide the next action: answer directly, call a tool, or delegate to another agent.
  3. Act by executing that action.
  4. Record the observation and update the state.
  5. Stop when the goal is satisfied, a step limit is reached, or the agent escalates.

The stopping conditions matter as much as the loop itself. Without a step limit and clear success criteria, an agent can spend tokens and money going in circles.

Here is a minimal, framework-agnostic version of that loop in Java:

public interface Tool {
    String name();
    String execute(String input);
}

public record AgentStep(String thought, String toolName, String toolInput) {}

public class SingleAgent {

    private final LlmClient llm;
    private final Map<String, Tool> tools;
    private final int maxSteps;

    public SingleAgent(LlmClient llm, List<Tool> tools, int maxSteps) {
        this.llm = llm;
        this.tools = tools.stream().collect(Collectors.toMap(Tool::name, t -> t));
        this.maxSteps = maxSteps;
    }

    public String run(String goal) {
        StringBuilder context = new StringBuilder("Goal: " + goal + "\n");

        for (int step = 1; step <= maxSteps; step++) {
            AgentStep next = llm.nextStep(context.toString(), tools.keySet());

            if (next.toolName() == null) {
                return next.thought();          // final answer
            }

            Tool tool = tools.get(next.toolName());
            if (tool == null) {
                context.append("Error: unknown tool ").append(next.toolName()).append("\n");
                continue;
            }

            String observation = tool.execute(next.toolInput());
            context.append("Action: ").append(next.toolName())
                   .append("(").append(next.toolInput()).append(")\n")
                   .append("Observation: ").append(observation).append("\n");
        }

        throw new IllegalStateException("Agent exceeded " + maxSteps + " steps without finishing");
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice what is explicit here: the tool registry, the step limit, and the failure path for unknown tools. Those are the details that make an agent operable in production.

What a Multi-Agent System Adds

A single agent with many tools can work for small problems, but it tends to accumulate responsibilities. Splitting the work across specialized agents brings several advantages:

  • Focused prompts. Each agent has a smaller, clearer instruction set, which is easier to test and tune.
  • Independent evaluation. A reviewer agent can be evaluated on its own accuracy without the noise of the rest of the pipeline.
  • Parallelism. Independent research tasks can run concurrently.
  • Replaceability. You can swap the model or strategy for one agent without touching the others.
  • Clear audit trails. Every message between agents is a discrete, loggable event.

A common set of roles looks like this:

Role Responsibility Typical output
Planner Decompose the goal into ordered sub-tasks A list of steps with dependencies
Researcher Retrieve information from documents, APIs, or search Cited facts and sources
Executor Perform actions such as writing code or calling services Results and side-effect logs
Reviewer Check outputs against criteria and constraints Pass/fail with reasons
Coordinator Route messages, track state, decide when to stop Final aggregated result

Not every system needs all five roles. Many useful designs have only a planner and one or two workers, with the coordinator logic living in ordinary code rather than in a model.

Coordination Patterns

How agents talk to each other is the most important design decision. Three patterns cover most real systems.

1. Orchestrator–Worker

A central coordinator decides which worker runs next and collects the results. This is the easiest pattern to reason about and debug, because control flow is centralized.

public class Orchestrator {

    private final Planner planner;
    private final Map<String, Worker> workers;
    private final Reviewer reviewer;

    public Orchestrator(Planner planner, List<Worker> workers, Reviewer reviewer) {
        this.planner = planner;
        this.workers = workers.stream().collect(Collectors.toMap(Worker::role, w -> w));
        this.reviewer = reviewer;
    }

    public Report execute(String goal) {
        List<Task> plan = planner.plan(goal);
        Map<String, Result> results = new LinkedHashMap<>();

        for (Task task : plan) {
            Worker worker = workers.get(task.assignedRole());
            if (worker == null) {
                throw new IllegalArgumentException("No worker for role " + task.assignedRole());
            }

            Result result = worker.perform(task, results);
            ReviewVerdict verdict = reviewer.review(task, result);

            if (!verdict.approved()) {
                result = worker.retry(task, results, verdict.feedback());
            }
            results.put(task.id(), result);
        }
        return Report.from(goal, results);
    }
}
Enter fullscreen mode Exit fullscreen mode

The reviewer step between worker and result is what turns a pipeline into a feedback loop. It is also where you should apply guardrails: schema validation, policy checks, and grounding checks against retrieved sources.

2. Peer Handoff

Agents pass control directly to each other based on the task. This suits conversational flows, such as a triage agent handing a customer request to a billing agent or a technical agent. The risk is that control flow becomes implicit, so you need strong logging of every handoff and a maximum hop count to prevent loops.

3. Shared Blackboard

Agents read from and write to a shared state store, and a scheduler decides who acts based on what is on the board. This is flexible and works well for research-style tasks where the next step depends on what others have discovered. The cost is concurrency control: you need versioned state, idempotent writes, and clear ownership of each field.

For most enterprise Java systems, the orchestrator–worker pattern is the right starting point. You can always evolve toward handoffs or a blackboard once the requirements justify the added complexity.

Typed Tools Are Non-Negotiable

Agents are only as reliable as the tools they call. The most common production failure is not the model reasoning incorrectly; it is a tool receiving malformed input or returning something the agent cannot interpret.

In Java, this is an opportunity. Give each tool a typed request and response, validate inputs before execution, and return structured errors the agent can reason about.

public record AccountBalanceRequest(String accountId) {
    public AccountBalanceRequest {
        if (accountId == null || !accountId.matches("ACC-[0-9]{8}")) {
            throw new IllegalArgumentException("Invalid account id: " + accountId);
        }
    }
}

public record AccountBalanceResponse(String accountId, BigDecimal balance, String currency) {}

public class AccountBalanceTool implements Tool {

    private final AccountService accounts;
    private final ObjectMapper mapper = new ObjectMapper();

    public AccountBalanceTool(AccountService accounts) {
        this.accounts = accounts;
    }

    @Override
    public String name() {
        return "get_account_balance";
    }

    @Override
    public String execute(String rawInput) {
        try {
            AccountBalanceRequest request = mapper.readValue(rawInput, AccountBalanceRequest.class);
            AccountBalanceResponse response = accounts.balanceOf(request.accountId());
            return mapper.writeValueAsString(response);
        } catch (IllegalArgumentException e) {
            return "{\"error\":\"" + e.getMessage() + "\"}";
        } catch (Exception e) {
            return "{\"error\":\"Tool failed; retry or escalate\"}";
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Returning structured error objects, instead of throwing exceptions into the agent loop, gives the model something concrete to act on. The agent can correct its input, try another tool, or escalate to a human.

Guardrails at Clear Boundaries

Guardrails are often implemented as a long list of instructions inside a single prompt. That approach is hard to verify. A better approach is to place checks at the boundaries between components:

  • Before a tool executes: validate parameters, enforce authorization, and block actions outside an allow-list.
  • Before a result leaves the system: check the output against a schema, scan for sensitive data, and confirm that claims are supported by retrieved sources.
  • Before an action with side effects: require explicit approval for anything irreversible, such as sending money, deleting records, or publishing content.

Because these checks live in ordinary code, you can unit test them, version them, and audit them like any other business rule.

Observability for Agents

Debugging a multi-agent system without good telemetry is painful. At minimum, record for every step:

  • the agent or role that acted,
  • the input and output of each model call and tool call,
  • token usage and latency,
  • the reviewer's verdict and the reason for any retry,
  • the final outcome and the reason the loop stopped.

Correlate all of these with a single trace ID for the entire goal. Tools such as OpenTelemetry work well here, and Spring Boot's observability support can export these traces to the backend you already use. Treat prompts and tool payloads as sensitive data: redact personal information before it reaches logs.

Cost, Latency, and Failure Modes

Multi-agent systems have real costs that should be designed for from the start:

  • Latency compounds. Every additional agent hop adds a model round trip. Parallelize independent branches wherever possible.
  • Token costs multiply. Passing full transcripts between agents is expensive. Pass structured results and summaries instead.
  • Loops happen. Two agents can keep sending work back and forth. Enforce step budgets and maximum retry counts at the orchestrator level.
  • Errors propagate. A wrong fact produced early can be treated as truth downstream. Keep source references with every claim so reviewers can check them.

A useful rule: start with the simplest system that meets the requirement, measure where time and cost go, and add agents only at the boundaries where they clearly reduce errors or enable parallel work.

Where Java Fits Well

Java is not the default language for AI experimentation, but it has strong advantages for building agentic systems that must run reliably:

  • Strong typing makes tool contracts and agent messages explicit.
  • Mature concurrency through virtual threads and structured concurrency helps with parallel agent execution.
  • Spring Boot provides dependency injection, configuration, security, and observability out of the box.
  • Existing services in banking, insurance, retail, and logistics can be exposed as tools without rewriting them.

Each agent can be a bounded Spring component with its own configuration, its own tool set, and its own test suite. The orchestrator becomes an ordinary service that coordinates them, which is familiar territory for Java teams that already operate microservices.

A Practical Starting Point

If you are beginning with multi-agent systems, consider this path:

  1. Pick one workflow that currently requires several manual steps and clear success criteria.
  2. Build a single agent with two or three typed tools and a strict step limit.
  3. Add a reviewer that checks the agent's output against explicit rules.
  4. Split the work into a planner and workers only when a single agent becomes hard to test or tune.
  5. Instrument everything from the first version, so you can see where failures and costs come from.
  6. Evaluate with real cases. Build a small set of representative tasks and track accuracy, latency, and cost over time.

This incremental approach keeps the system understandable and gives you evidence for every added layer of complexity.

Conclusion

Agentic AI moves LLM applications from answering questions to completing goals. Multi-agent systems extend that idea by dividing the work into focused roles with explicit coordination, typed tools, and clear guardrails. The engineering challenges are familiar to anyone who has built distributed systems: boundaries, contracts, observability, and failure handling.

For Java developers, this is a natural extension of the skills already in use. The language's type system, its mature ecosystem, and frameworks like Spring Boot give you the tools to build agent systems that are testable, observable, and safe to operate. Start small, keep control flow explicit, and let the evidence decide when to add more agents.

If you are building agentic systems on the JVM, I would be glad to hear what patterns are working for your team.

Top comments (0)