Multi-Agent Systems

This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page’s examples target Python 3.12 with LangChain 1.4.2 / langchain-core 1.6.5 / LangGraph 1.2.12, langgraph-swarm 0.1.0 and langchain-ollama 1.1.0, Java 21 with LangChain4j 1.20.2 (langchain4j-agentic 1.20.2-beta30), and Spring Boot 4.1 with Spring AI 2.0.1 plus the community spring-ai-agent-utils 0.12.0, all using DocsAssistant's Ollama llama3.1:8b chat model, as checked on 2026-09-28 (see the AI Agents baseline). The ai-catalog excerpt below is pinned to commit 58fb709 (58fb70954404f9d539031a02ac6d4413f03745be), which was still the tip of its main branch on 2026-09-28.

A multi-agent system splits one job across several agents, each with its own instructions, tools and — usually — its own context window, coordinated by a topology. The agents talk to each other through messages, shared state or tool calls; across processes they talk through a protocol such as A2A (see Agent-to-Agent Protocol (A2A)).

When to add agents

Adding agents is an architectural decision with a real cost — more model calls, more tokens, more latency, and failure modes that only appear when agents interact — so start with one agent and add more only for a reason:

  • Context isolation. One agent’s work would flood another’s context (raw search results, long documents). A sub-agent returns a condensed result instead (see sub-agent isolation).

  • Too many tools. Past a few dozen tools, selection accuracy drops; splitting them across specialists keeps each tool list short.

  • Conflicting instructions. A reviewer that must be sceptical of a draft should not share a system prompt with the drafter.

  • Parallelism. Independent sub-tasks can run at the same time.

  • Separation of privilege. Only one agent holds the write tool, and it only receives vetted input.

Do not add agents to make a single agent’s task "feel" more organised: a single agent with good tools beats a committee on most tasks, and a well-structured workflow (see Workflow Patterns) is often the better answer to "several steps with different prompts".

How many agents? As few as the reasons above justify. Coordination overhead grows with the number of agents — with agents that may all talk to each other there are possible channels, so five peer-to-peer agents already have ten — which is why topologies with a coordinator ( channels) dominate in practice. Two to five specialised agents under one supervisor covers most real systems.

Topologies

Supervisor (manager)

One supervisor agent receives the task, decides which worker to call, reads the worker’s result, and decides again, until it can answer. Workers never talk to each other. It is the orchestrator-workers pattern with the workers promoted to full agents, and the default topology for most systems.

flowchart TD U[User] --> S[Supervisor] S -->|ask| R[Researcher] S -->|ask| V[Reviewer] R -->|result| S V -->|verdict| S S --> A([Answer])

Hierarchical

Supervisors of supervisors: a top-level manager delegates to team leads, each of which supervises its own workers. It scales the supervisor topology to many agents while keeping each coordinator’s choice small. The cost is latency, since every level adds model calls, and lost detail, since results are summarised on the way up.

flowchart TD M[Manager] --> L1[Docs team lead] M --> L2[Code team lead] L1 --> W1[Researcher] L1 --> W2[Reviewer] L2 --> W3[Test writer] L2 --> W4[Coder]

Handoffs and swarms

In a handoff topology there is no permanent coordinator: the active agent handles the conversation until it decides another agent should, then transfers control — and the conversation — to it. A swarm is a set of agents that can all hand off to each other. Handoffs suit customer-facing flows where different phases need different specialists and the user should talk to one at a time; the system remembers which agent is active per conversation.

flowchart LR U[User] --> D[DocsAssistant] D -->|transfer_to_reviewer| V[Reviewer] V -->|transfer_to_docs_assistant| D

Democratic

Several peer agents work on the same question and the result is decided collectively — by voting, or by a debate in which agents read each other’s answers and revise for a few rounds before a vote. It improves robustness on judgement calls (is this page actually wrong?) at a multiple of the cost, and it only helps when the agents' errors are not all the same — use different prompts, tools or models.

flowchart LR Q[Is the page wrong?] --> A1[Agent 1] Q --> A2[Agent 2] Q --> A3[Agent 3] A1 --> V{Majority vote} A2 --> V A3 --> V

Actor-critic

An actor agent produces, a critic agent evaluates, and the loop repeats until the critic approves. It is the evaluator-optimizer workflow with both roles as full agents with their own tools — the critic can, for example, fetch the official source itself rather than trusting the actor’s citation. DocsAssistant’s researcher and reviewer below form an actor-critic pair inside a supervisor. Automating the design of such systems — letting a meta-agent search over agent architectures — is an active research direction (ADAS).

flowchart LR T[Task] --> AC[Actor: draft fix] AC --> CR[Critic: verify against sources] CR -->|REJECT + reasons| AC CR -->|APPROVE| O([Proceed])

Shared vs. isolated state

  • Shared state — all agents read and write one state object (a LangGraph graph state, LangChain4j’s AgenticScope, a blackboard). Coordination is easy and nothing is lost in translation, but every agent’s context can fill with every other agent’s work, and one confused agent can corrupt what the rest rely on.

  • Isolated state — each agent has its own context and state and receives only an explicit brief; it returns only an explicit result. This is what makes sub-agents a context-management tool, and it is also a security boundary: the reviewer below never sees the user’s raw chat, so an injection in it cannot reach the reviewer directly.

Most systems mix the two: a small shared state for the facts every agent needs (the task, decisions, the final artifacts), and isolated working contexts for everything else. Whatever is shared must be designed like an API — named fields, clear ownership of who writes what.

Distributed agents: brokers, actors and workflow engines

Everything above can run in one process. Once agents run on different machines, scale independently, or have to survive restarts mid-task, the coordination moves into infrastructure this site already covers in other sections:

  • Message brokers (Kafka, RabbitMQ) decouple agents in time and space: an agent publishes a task or an event, another consumes it. Delivery guarantees and idempotent consumers matter as much for agents as for any service — a redelivered "open an issue" message must not open two issues. See the Messaging Reference.

  • Actor frameworks (Ray, Microsoft Orleans, Akka) model each agent as an actor with private state and a mailbox, giving location transparency, supervision and restarts. They fit large numbers of long-lived, stateful agents.

  • Workflow engines with durable execution persist each step so a multi-hour agent run resumes where it stopped after a crash or a redeploy. LangGraph’s checkpointer (see Memory) provides the same property within a graph.

  • Agent protocols — A2A for agent-to-agent calls across organisational and framework boundaries (see Agent-to-Agent Protocol (A2A)), and MCP for agent-to-tool calls.

Deployment itself is out of scope here; the LLMOps & Evaluation sub-section of this AI section covers running agents in production.

LangGraph: supervisor and handoffs

LangChain’s current recommendation for a supervisor is the tool-based pattern: each worker is a create_agent agent wrapped in a tool, and the supervisor is a create_agent agent that calls those tools. That gives full control over what each worker receives (its brief) and returns (its result) — the context engineering the multi-agent design depends on. The earlier langgraph-supervisor library still works, but its own README now points new code to the tool-based pattern.

DocsAssistant as a supervisor with a researcher and a reviewer
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain.tools import tool
from langchain_ollama import ChatOllama
from langgraph.checkpoint.memory import InMemorySaver

llm = ChatOllama(model="llama3.1:8b", temperature=0)

# Worker 1: answers from the docs.
researcher = create_agent(
    llm, tools=[docs_search, docs_get_section], name="researcher",
    system_prompt="Answer documentation questions from the docs only. Cite page paths.",
)

# Worker 2: the reviewer. It sees only the proposed fix and the sources -- not the user chat.
reviewer = create_agent(
    llm, tools=[docs_get_section], name="reviewer",
    system_prompt="You review a proposed documentation fix. Check each claim against the cited "
                  "official source. Reply APPROVE or REJECT with one line per problem.",
)


@tool
def ask_researcher(question: str) -> str:
    """Ask the research agent a documentation question; returns a cited answer."""
    return researcher.invoke({"messages": [("user", question)]})["messages"][-1].content


@tool
def ask_reviewer(page_path: str, proposed_fix: str, evidence: str) -> str:
    """Ask the reviewer agent to check a proposed page fix before any issue is filed."""
    brief = f"Page: {page_path}\nProposed fix: {proposed_fix}\nEvidence: {evidence}"
    return reviewer.invoke({"messages": [("user", brief)]})["messages"][-1].content


supervisor = create_agent(
    llm,
    tools=[ask_researcher, ask_reviewer, github_open_issue],
    system_prompt=(
        "You are DocsAssistant's supervisor. Route questions to ask_researcher. When the user "
        "reports a wrong page, gather evidence with ask_researcher, get ask_reviewer's APPROVE, "
        "and only then call github_open_issue."
    ),
    middleware=[HumanInTheLoopMiddleware(interrupt_on={
        "github_open_issue": {"allowed_decisions": ["approve", "edit", "reject"]},
        "ask_researcher": False,
        "ask_reviewer": False,
    })],
    checkpointer=InMemorySaver(),
)

The reviewer receives only a three-field brief, never the user’s conversation, and the supervisor’s prompt makes the reviewer’s APPROVE a precondition for the write tool. Only the supervisor holds github_open_issue, and its HumanInTheLoopMiddleware plus checkpointer pause the run before that tool executes, exactly as on Human-in-the-Loop — a reviewer agent is a check, not a substitute for the human. docs_search, docs_get_section and github_open_issue are the tools from Tool Design and Context Engineering for Agents. See LangChain — Multi-agent.

For handoffs, langgraph-swarm gives agents create_handoff_tool(…​) tools and tracks the active agent in the graph state:

DocsAssistant and the reviewer as a two-agent swarm
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain_ollama import ChatOllama
from langgraph.checkpoint.memory import InMemorySaver
from langgraph_swarm import create_handoff_tool, create_swarm

llm = ChatOllama(model="llama3.1:8b", temperature=0)

docs_assistant = create_agent(
    llm, name="docs_assistant",
    tools=[docs_search, github_open_issue, create_handoff_tool(
        agent_name="reviewer",
        description="Transfer to the reviewer when the user reports that a page is wrong.")],
    system_prompt="You are DocsAssistant. Answer from the docs; hand off wrong-page reports, and "
                  "call github_open_issue only after the reviewer hands back an APPROVE.",
    middleware=[HumanInTheLoopMiddleware(interrupt_on={
        "github_open_issue": {"allowed_decisions": ["approve", "edit", "reject"]},
        "docs_search": False,
    })],
)
reviewer = create_agent(
    llm, name="reviewer",
    tools=[docs_get_section, create_handoff_tool(
        agent_name="docs_assistant",
        description="Transfer back to DocsAssistant with APPROVE or REJECT and the evidence.")],
    system_prompt="You verify reported documentation errors. Reply APPROVE or REJECT with the "
                  "evidence, then transfer back to DocsAssistant.",
)

swarm = create_swarm([docs_assistant, reviewer], default_active_agent="docs_assistant")
app = swarm.compile(checkpointer=InMemorySaver())     # active agent per thread, and paused approvals

When DocsAssistant calls transfer_to_reviewer, the swarm routes the rest of the turn — and, because the checkpointer stores active_agent, the next user message in the same thread — to the reviewer. The reviewer has no write tool: it can only hand a verdict back, and github_open_issue stays with DocsAssistant behind the same HumanInTheLoopMiddleware gate. The agents are subgraphs of the swarm, so the swarm’s checkpointer is the one that saves the interrupt, and the approval resumes with Command(resume=…​) on the same thread_id. See langgraph-swarm and langgraph-supervisor.

LangChain4j supervisor agent

LangChain4j’s agentic module ships the supervisor topology as a built-in agent. The supervisor’s planner model reads each sub-agent’s @Agent description, generates a sequence of agent invocations, runs them, and decides when the task is done; responseStrategy chooses whether the caller gets the last agent’s output (LAST, the default), a summary of the whole run (SUMMARY), or the better-scored of the two (SCORED).

LangChain4j: DocsAssistant’s supervisor
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;

public interface Researcher {
    @UserMessage("Answer from the documentation only, citing page paths: {{question}}")
    @Agent("Answers documentation questions with citations")
    String research(@V("question") String question);
}

public interface Reviewer {
    @UserMessage("Check this proposed fix against its evidence. Reply APPROVE or REJECT with reasons. "
            + "Page: {{pagePath}} Fix: {{fix}} Evidence: {{evidence}}")
    @Agent("Reviews a proposed documentation fix against official sources")
    String review(@V("pagePath") String pagePath, @V("fix") String fix, @V("evidence") String evidence);
}

Researcher researcher = AgenticServices.agentBuilder(Researcher.class)
        .chatModel(chatModel).tools(new DocsTools()).build();
Reviewer reviewer = AgenticServices.agentBuilder(Reviewer.class)
        .chatModel(chatModel).tools(new DocsTools()).build();

SupervisorAgent docsSupervisor = AgenticServices.supervisorBuilder()
        .chatModel(plannerModel)                       // a stronger model plans
        .subAgents(researcher, reviewer)
        .supervisorContext("Never propose filing an issue unless the reviewer replied APPROVE.")
        .responseStrategy(SupervisorResponseStrategy.SUMMARY)
        .maxAgentsInvocations(6)
        .build();

String result = docsSupervisor.invoke(
        "The memory page says MessageWindowChatMemory keeps 10 messages -- is it wrong?");

supervisorContext adds standing instructions to the planner, and maxAgentsInvocations bounds the loop. See LangChain4j — Agents and Agentic AI (pure agentic AI, supervisor design and customization).

Spring AI subagents

Spring AI’s core has no multi-agent abstraction — a supervisor is a ChatClient whose tools call other ChatClient instances, exactly as in the LangGraph example. The community spring-ai-agent-utils library adds a Claude-Code-style subagent system: TaskTool gives a main ChatClient a task tool that launches a named subagent, defined in a Markdown file with front matter, in its own context and returns its result; its spring-ai-agent-utils-a2a module lets a subagent be a remote A2A agent instead of a local one. The wiring is shown on Context Engineering for Agents. See spring-ai-agent-utils and Spring AI — Tool calling.

Worked example: orchestrator/worker isolation in a coding-agent catalog

This account’s ai-catalog repository is a real multi-agent system for coding agents (Claude Code skills and subagents), and its agents overview page, docs/modules/ROOT/pages/agents/overview.adoc at the pinned commit, states the design principle this page keeps returning to. In paraphrase: an agent there is a sub-instance of the coding assistant with its own separate context window, spawned either to search a codebase without derailing the current conversation or to isolate a sub-task — a verbose report, a whole multi-step skill run, a batch of independent summaries — so that only a distilled result returns to the caller’s context. The catalog defines three custom agents for shapes that recurred often enough to centralise (a gate runner that reports only compact check results, an isolated skill executor, a change summariser) and relies on a read-only built-in search agent for exploration, so an exploratory pass cannot make an accidental edit.

What it demonstrates is the supervisor topology with strictly isolated state and separation of privilege: the orchestrating skill never reads the raw build log or the raw diff, it reads a worker’s summary; and the read-only worker cannot write at all. Generalised to DocsAssistant: the researcher’s raw chunks and the reviewer’s source fetches stay in their own windows, the supervisor sees two short results, and only the supervisor can reach the approval-gated write tool. See Agents and Orchestration Patterns for the full pipeline.

In other frameworks

Framework Multi-agent support

OpenAI Agents SDK

Handoffs (handoffs=[…​] on an Agent, or handoff(…​)) transfer control; agents as tools (agent.as_tool(…​)) give the supervisor pattern.

Claude Agent SDK

Subagents declared with AgentDefinition in ClaudeAgentOptions.agents, each with its own prompt, tools and model, invoked by the main agent in isolated contexts.

Microsoft Agent Framework

Orchestrations for sequential, concurrent, handoff, group-chat and Magentic (a manager with a task ledger) topologies, plus A2A for remote agents.

CrewAI

A Crew of Agent objects with Process.sequential or Process.hierarchical (a manager agent delegates); agents can delegate to each other when allow_delegation is enabled.