---
title: "Building Multi-Agent Systems with LLMs"
description: "A practical guide to choosing an orchestration pattern, defining agent contracts, and keeping multi-agent systems observable when tasks fail."
date: "2026-02-07T00:00:00.000Z"
author: "Carlos Garavito"
tags: ["ai", "agents", "llm", "architecture"]
canonical_url: "https://cgaravito.dev/en/blog/building-multi-agent-systems"
last_updated: "2026-02-07T00:00:00.000Z"
locale: "en"
---

A multi-agent system adds coordination, state, and more failure modes. I use one when the work has boundaries that are clearer than a single agent's prompt, such as planning, implementation, review, and testing in a software workflow.

Each agent needs a focused system prompt, a limited tool set, and an explicit input and output contract. The architecture depends on how the work moves between those agents.

## Use an orchestrator when the path can change

An orchestrator owns the plan, selects a specialized agent for each step, and keeps the shared context. This fits tasks where a result can change what should happen next.

This simplified sketch leaves the `planAgent` and shared `context` wiring out of the example.

```typescript
interface Agent {
  name: string;
  systemPrompt: string;
  tools: Tool[];
  execute(input: string): Promise<AgentResult>;
}

class Orchestrator {
  private agents: Map<string, Agent>;

  async run(task: string): Promise<string> {
    const plan = await this.planAgent.execute(task);

    for (const step of plan.steps) {
      const agent = this.agents.get(step.agentName);
      const result = await agent.execute(step.input);
      this.context.addResult(step.id, result);
    }

    return this.synthesize();
  }
}
```

The coordinator is also the place to record which agent ran, which input it received, and which result entered the shared context. Without that trace, a bad final answer is difficult to diagnose.

## Use a pipeline when the order is fixed

A pipeline is simpler when every task follows the same sequence and each output becomes the next input.

```typescript
const pipeline = createPipeline([
  researchAgent,
  analysisAgent,
  writingAgent,
  reviewAgent,
]);

const result = await pipeline.execute(initialInput);
```

The main failure mode is propagation. A weak research result reaches analysis, writing, and review unless a stage validates its input. I prefer a concrete schema and an acceptance check at each boundary over a larger prompt that asks the next agent to recover whatever it receives.

## Use debate for bounded disagreement

The debate pattern gives several agents the same topic and carries their responses across a fixed number of rounds.

```typescript
async function debate(
  topic: string,
  agents: Agent[],
  rounds: number,
): Promise<string> {
  let context = topic;

  for (let i = 0; i < rounds; i++) {
    for (const agent of agents) {
      const response = await agent.execute(context);
      context += `\n\n${agent.name}: ${response}`;
    }
  }

  return synthesizeDebate(context);
}
```

This can help when different perspectives are part of the task, but every round adds tokens and repeated context. A fixed round count and a defined synthesis rule keep the discussion from becoming an expensive loop.

## Make messages inspectable

Agents still need a shared protocol even when the orchestration is simple. A message bus can make the sender, recipient, message type, and metadata explicit.

```typescript
interface AgentMessage {
  from: string;
  to: string;
  type: "request" | "response" | "broadcast";
  content: string;
  metadata: Record<string, unknown>;
}

class MessageBus {
  private subscribers = new Map<string, (msg: AgentMessage) => void>();

  send(message: AgentMessage): void {
    const handler = this.subscribers.get(message.to);
    handler?.(message);
  }

  subscribe(agentId: string, handler: (msg: AgentMessage) => void): void {
    this.subscribers.set(agentId, handler);
  }
}
```

The message contract gives logging and retries a stable unit to work with. I want every interaction to be traceable, failures to be visible, and retries or fallbacks to happen at a known boundary.

Start with two agents and one coordination pattern. Add another agent when it owns a distinct task, tools, and result that you can evaluate. The useful system is the smallest one whose handoffs remain clear when an agent returns a wrong answer or no answer at all.
