Skip to content
IRC-CodingIRC-Coding
agent-orchestrationmulti-agent-systemsbest-practicesagent-patternsai-agents

Agent Orchestration: Multi-Agent Systems Best Practices

Advanced techniques for orchestrating AI agents. Patterns, anti-patterns, and best practices for reliable agent systems.

I

IRC-Coding Team

8 min read
Agent Orchestration: Multi-Agent Systems Best Practices

Agent Orchestration: Best Practices for Multi-Agent Systems

Agent orchestration is the art of coordinating multiple AI agents so they solve complex tasks together—reliably, efficiently, and transparently. Individual agents are rarely the bottleneck. The challenge is coordination: who does what, in what order, and what happens when things break? How do you keep costs under control?

TL;DR — Agent Orchestration in 90 Seconds

Agent orchestration is coordinating multiple AI agents: task delegation, communication, control flow, error handling, and resource management.

---

Four main patterns: Hub-and-Spoke (centralized), Pipeline (sequential), DAG (parallel), Event-Driven (reactive).

Three critical best practices: Clear roles (Single Responsibility Principle), explicit typed state, retry logic with max_retries.

Three major anti-patterns: God Agent, unbounded loops, blindly trusting agent outputs.

End of summary!

What Is Agent Orchestration?

Agent orchestration coordinates multiple AI agents across six dimensions:

  • Task delegation: Who handles what? Which parts require specific expertise?
  • Communication: How do agents exchange information? Through shared state, messages, or events?
  • Control flow: Who decides the next step? A central orchestrator or conditional routing logic?
  • Error handling: What happens when things fail? Retry, fallback, or human intervention?
  • Resource optimization: How do you minimize LLM calls? Caching, model selection, token budgets?
  • Observability: How do you track what’s happening? Logs, traces, and metrics?

Single-Agent vs. Multi-Agent — When Do You Need Orchestration?

Single-agent works for: linear tasks, a single domain of expertise, one-shot solutions that fit within a single LLM call.

Multi-agent becomes necessary when: you need multiple specialties (research plus writing plus review), iterative feedback loops (writer ↔ reviewer), parallel execution, tasks exceeding context windows, varied tooling, or human-in-the-loop approval.

Orchestration Patterns — Deep Dive

1. Hub-and-Spoke (Central Orchestrator)

A central orchestrator agent decides which agents run and when.

         [Orchestrator]
        /     |      \
[Agent A] [Agent B] [Agent C]

Strengths: Clear control flow, straightforward error handling, fully auditable. Weaknesses: Single point of failure, orchestrator becomes a bottleneck, extra LLM cost per decision.

def orchestrator(state):
    task = state["task"]
    # LLM decides which agent to use
    response = llm.invoke(f"Which agent for: {task}? (research/code/write)")
    task_type = response.content.strip().lower()
    return {"task_type": task_type}

def route_to_agent(state):
    return state["task_type"]

# In LangGraph:
workflow.add_conditional_edges("orchestrator", route_to_agent, {
    "research": "research_agent",
    "code": "code_agent",
    "write": "writer_agent"
})

2. Pipeline (Sequential)

Agents work one after another, each passing results to the next. CrewAI’s Process.sequential follows this pattern by default.

[Agent A] → [Agent B] → [Agent C] → [Output]

Strengths: Simple, predictable, each agent receives all prior results. Weaknesses: No parallelism, one failure blocks everything downstream.

When to use: Linear workflows without branching (research → write → review).

3. DAG (Directed Acyclic Graph)

Agents execute according to dependencies, with parallelism wherever possible. A DAG lets independent tasks run simultaneously while coordinating dependent ones.

[Agent A] ──→ [Agent C] ──→ [Agent E] (Merge)
     └──→ [Agent B] ──→ [Agent D] ──┘

Strengths: Parallelism—independent tasks run concurrently. Efficient—total time equals the longest path. Weaknesses: Complex to debug, dependencies must be explicit, potential race conditions with shared state.

Example with LangGraph (parallel researchers):

class ResearchState(TypedDict):
    topic: str
    general_research: str
    technical_research: str
    combined_report: str

def general_researcher(state: ResearchState) -> dict:
    response = llm.invoke(f"General information on: {state['topic']}")
    return {"general_research": response.content}

def technical_researcher(state: ResearchState) -> dict:
    response = llm.invoke(f"Technical details on: {state['topic']}")
    return {"technical_research": response.content}

def merge_results(state: ResearchState) -> dict:
    combined = f"General:\n{state['general_research']}\n\nTechnical:\n{state['technical_research']}"
    return {"combined_report": combined}

# Graph with parallel nodes
workflow = StateGraph(ResearchState)
workflow.add_node("general", general_researcher)
workflow.add_node("technical", technical_researcher)
workflow.add_node("merge", merge_results)

workflow.set_entry_point("general")
workflow.add_edge("general", "technical")  # Both run in parallel
workflow.add_edge("general", "merge")
workflow.add_edge("technical", "merge")
workflow.add_edge("merge", END)

When to use: When independent subtasks can run in parallel (for example, three separate research streams that merge afterward).

4. Event-Driven (Reactive)

Agents respond to events rather than direct calls. An event bus distributes events to all interested subscribers.

[Event Bus]
  ├── [Agent A] (listens for "research.done")
  ├── [Agent B] (listens for "code.reviewed")
  └── [Agent C] (listens for "test.failed")

Strengths: Loose coupling, scales horizontally, agents can be added dynamically. Weaknesses: Hard to trace, race conditions possible, debugging becomes complex.

from collections import defaultdict

class EventBus:
    def __init__(self):
        self.subscribers = defaultdict(list)
    
    def subscribe(self, event_type: str, handler):
        self.subscribers[event_type].append(handler)
    
    def publish(self, event_type: str, data: dict):
        results = []
        for handler in self.subscribers[event_type]:
            results.append(handler(data))
        return results

bus = EventBus()
bus.subscribe("research.done", writer_agent)
bus.subscribe("research.done", fact_checker_agent)
bus.subscribe("code.reviewed", refactor_agent)
bus.publish("research.done", {"topic": "AI 2026", "result": "..."})

When to use: Loosely coupled systems where agents are added dynamically and execution order isn’t predetermined.

Pattern Comparison

PatternComplexityParallelismFlexibilityDebuggingBest For
PipelineLowLowEasyLinear workflows
Hub-and-SpokeMediumMediumModerateDynamic routing
DAGHighHighDifficultParallel tasks
Event-DrivenHighVery highVery difficultLoosely coupled systems

Best Practices — In Detail

1. Define Clear Roles (Single Responsibility Principle)

Each agent should have exactly one responsibility. An agent that simultaneously researches, writes, and tests is difficult to debug, expensive to run, and produces worse results than three specialized agents.

Poor:

agent = Agent(
    role="Researcher, Writer and Tester",
    goal="Research, write and test everything"
)

Good:

researcher = Agent(role="Researcher", goal="Find information")
writer = Agent(role="Writer", goal="Write content based on research")
tester = Agent(role="Tester", goal="Test functionality and report bugs")

2. Make State Explicit

Use typed states instead of implicit communication. This makes the workflow traceable and resilient to errors.

from typing import TypedDict, Optional, List, Annotated
from operator import add

class WorkflowState(TypedDict):
    input: str
    task_type: str
    research_result: Optional[str]
    draft: Optional[str]
    review_feedback: Optional[str]
    final_output: Optional[str]
    error: Optional[str]
    retry_count: int
    messages: Annotated[List[str], add]  # Appended, not overwritten

3. Error Handling and Retry Logic

LLMs are unreliable — rate limits, timeouts, hallucinations. Every agent needs error handling:

import logging

logger = logging.getLogger("agent_orchestrator")

def agent_with_retry(agent_func, state, max_retries=3, fallback=None):
    for attempt in range(max_retries):
        try:
            logger.info(f"Agent {agent_func.__name__} - Attempt {attempt + 1}")
            result = agent_func(state)
            if validate_result(result):
                return result
            else:
                logger.warning(f"Agent {agent_func.__name__} - invalid result")
                state["error"] = "Invalid result"
        except Exception as e:
            logger.error(f"Agent {agent_func.__name__} - Error: {str(e)}")
            state["error"] = str(e)
            state["retry_count"] = attempt + 1
    
    if fallback:
        return fallback(state)
    return {**state, "error": f"Agent failed after {max_retries} retries"}

def validate_result(result):
    if not result:
        return False
    if "error" in result and result["error"]:
        return False
    return True

4. Cost Control

LLM calls add up fast. With 5 agents each making 3 revisions, that’s 15+ LLM calls. Using GPT-4o, a single run can cost $5–20.

class CostTracker:
    def __init__(self, max_budget=10.0):
        self.costs = {}
        self.max_budget = max_budget
    
    def track(self, agent_name, tokens, model="gpt-4o"):
        cost_per_1k = {
            "gpt-4o": 0.005,
            "gpt-4o-mini": 0.0003,
            "claude-3.5-sonnet": 0.003,
        }
        rate = cost_per_1k.get(model, 0.005)
        cost = (tokens / 1000) * rate
        self.costs[agent_name] = self.costs.get(agent_name, 0) + cost
        
        if self.total() > self.max_budget:
            raise Exception(f"Budget exceeded: ${self.total():.2f}")
    
    def total(self):
        return sum(self.costs.values())
    
    def report(self):
        lines = [f"Cost Report (Total: ${self.total():.4f})"]
        for agent, cost in sorted(self.costs.items()):
            lines.append(f"  {agent}: ${cost:.4f}")
        return "\n".join(lines)

Cost Optimization Strategies:

  • Caching: Don’t run the same prompt twice
  • Model Selection: Use GPT-4o-mini ($0.30/M) for simple tasks, GPT-4o ($5.00/M) for complex ones
  • Early Stopping: Stop when the result is “good enough”
  • Token Limits: Set max_tokens per agent
  • Local Models: Use Ollama for development ($0)

5. Observability and Logging

Without observability, you’re flying blind — you don’t know how long each agent takes or where failures occur.

import logging
import time
from functools import wraps

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("agent_orchestrator")

def observed_agent(agent_func):
    @wraps(agent_func)
    def wrapper(state):
        name = agent_func.__name__
        start = time.time()
        logger.info(f"[{name}] START - Input keys: {list(state.keys())}")
        try:
            result = agent_func(state)
            duration = time.time() - start
            logger.info(f"[{name}] DONE - {duration:.2f}s")
            return result
        except Exception as e:
            duration = time.time() - start
            logger.error(f"[{name}] ERROR - {duration:.2f}s - {str(e)}")
            raise
    return wrapper

Additionally: Use LangSmith for detailed tracing — visualizes every LLM call, state transition, and token calculation:

import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "ls-..."

6. Human-in-the-Loop

For critical decisions, a human should approve. LangGraph supports this natively with interrupt_before:

app = workflow.compile(
    checkpointer=memory,
    interrupt_before=["publisher"]  # Pauses before publisher
)

# First run: executes up to publisher
result = app.invoke(initial_state, config={"configurable": {"thread_id": "1"}})

# Human reviews and approves
approval = input("Approve? (y/n): ")
result = app.invoke(
    {"approved": approval == "y"},
    config={"configurable": {"thread_id": "1"}}
)

When to use Human-in-the-Loop: Publishing content, production code changes, decisions with financial impact, low agent confidence.

Anti-Patterns — What to Avoid

1. God Agent

One agent doing everything. The prompt becomes massive → LLM loses focus → poor quality. Hard to debug and expensive to operate. Solution: Split into 3–5 specialized agents.

2. Infinite Loops

Agents calling each other without a stopping condition. Solution: Set maximum iterations and implement an emergency brake:

MAX_ITERATIONS = 5

def route_after_review(state):
    if state.get("approved"):
        return "publish"
    if state.get("iteration_count", 0) >= MAX_ITERATIONS:
        return "human_review"  # Emergency brake
    return "writer"

3. No Validation

Blindly trusting agent output. LLMs hallucinate and invent sources. Solution: Validate every output:

def validate_research(result: str) -> bool:
    if not result or len(result) < 100:
        return False
    if "http" not in result:  # At least one source
        return False
    return True

4. Too Many Agents

More agents means more coordination overhead, higher costs, and more failure points. Rule of thumb: 3–5 agents. If you need more, split into multiple independent crews.

5. No Cost Control

Without cost tracking, multi-agent systems can easily cost hundreds of dollars per run. Solution: Use CostTracker with a budget limit (see above).

Framework Comparison for Orchestration

FeatureLangGraphCrewAICustom
Pipeline✅ (sequential)
Hub-and-Spoke✅ (conditional edges)✅ (hierarchical)
DAG/Parallel✅ (native)✅ (asyncio)
Event-Driven❌ (not native)✅ (custom bus)
Cycles✅ (conditional edges)Limited
Human-in-the-Loop✅ (interrupt)Manual
Checkpointing✅ (native)✅ (custom)

Exam Essentials

  • Orchestration: Coordinating multiple agents through task delegation, communication, control flow, error handling, and resource management
  • 4 Patterns: Pipeline (sequential), Hub-and-Spoke (centralized), DAG (parallel), Event-Driven (reactive)
  • Best Practices: Clear roles (SRP), explicit typed states, retry logic with max_retries, cost control via CostTracker, observability through logging and LangSmith, Human-in-the-Loop for critical decisions
  • Anti-Patterns: God Agent, unbounded loops, missing validation, too many agents, no cost control
  • Framework Selection: LangGraph for complex graphs, CrewAI for rapid prototyping, custom solutions for Event-Driven patterns

FAQ

When do I need multi-agent over single-agent? When the task demands multiple specialized skills, iteration is required (Writer ↔ Reviewer), parallelism is possible, or the problem is too complex for a single prompt to handle.

What’s the optimal number of agents? Three to five agents covers most use cases. Beyond eight, coordination becomes unwieldy and costs spike. For complex requirements, split the work into multiple independent crews or graphs instead.

Which orchestration pattern should I use? Pipeline for straightforward linear tasks, Hub-and-Spoke for dynamic routing, DAG for parallel work, Event-Driven for loosely coupled systems. Most production systems blend multiple patterns.

How do I prevent infinite loops? Always set a maximum iteration count. Implement an emergency stop that triggers Human-in-the-Loop or END status when iterations exceed the threshold.

How do I control costs in multi-agent systems?

  1. CostTracker with a budget limit. 2) Cheaper models for simple tasks. 3) Caching for repeated prompts. 4) Early stopping. 5) Local models during development.

Do I need LangSmith in production? Highly recommended. Without tracing, you’re flying blind—you won’t know which agent takes how long or where failures occur. LangSmith offers a free tier for small projects.

Keine Bücher für Kategorie "ki-agenten" gefunden.

Back to Blog
Share:

Related Posts