Multi-Agent Workflows Explained: Assigning Roles to AI Teams
How to architect multi-agent systems with specialized roles — Researcher, Writer, Code Reviewer, Tester — comparing hierarchical vs. peer-to-peer patterns with LangGraph and CrewAI code.
AgenticMedia Team
Content Creator

TL;DR / Key Takeaways
Multi-agent systems fail most often not because the LLM is weak, but because the communication topology doesn’t match the task. Before writing a single agent prompt, decide how agents talk to each other:
- Hierarchical (manager-delegate) patterns fit tasks with a clear decomposition — a manager agent breaks work into subtasks and routes them to specialists, then synthesizes results. Best when subtasks are largely independent.
- Peer-to-peer (round-robin/group chat) patterns fit tasks requiring iterative refinement — a Writer and a Critic going back and forth until quality converges. Best when the “right” output depends on multiple rounds of feedback, not a single pass.
- Pipeline patterns (a strict Researcher → Writer → Reviewer → Tester sequence) are the simplest and most debuggable topology, and are the right default unless you have a specific reason to need dynamic routing or iterative back-and-forth.
- More agents is not automatically better. Every additional agent in the loop is an additional LLM round-trip, additional failure surface, and additional context to keep synchronized — a well-scoped 3-agent pipeline reliably outperforms a poorly-scoped 6-agent system in both cost and output quality.
- Role specificity beats role quantity. A single agent with a well-written, narrow role prompt (“You are a Python code reviewer who checks only for security vulnerabilities”) outperforms a vague “helpful assistant” role split across multiple agents.
Core Concept: Three Communication Topologies
1. Pipeline (Sequential, Fixed Order)
Each agent’s output becomes the next agent’s input, in a fixed, predetermined sequence. This is the simplest topology to build, debug, and reason about — there’s no dynamic routing decision to get wrong.
Researcher → Writer → Code Reviewer → Tester → Final Output
Best for: tasks with a natural linear decomposition — content pipelines, code generation-then-review workflows, data processing chains.
2. Hierarchical (Manager-Delegate)
A manager agent receives the overall task, decides which specialist agent(s) should handle which subtask, and can route work dynamically based on the task’s actual shape rather than a fixed sequence — including skipping agents that aren’t needed for a given input.
┌─→ Researcher ─┐
Manager Agent ───┼─→ Code Reviewer ─┼──→ Manager synthesizes → Output
└─→ Tester ──────┘
Best for: tasks where the right subset of specialists varies by input — e.g., a customer support triage system where only some tickets need a technical specialist agent.
3. Peer-to-Peer (Group Chat / Iterative)
Agents participate in a shared conversation, critiquing and revising each other’s output across multiple turns, with a termination condition (approval, iteration cap, or convergence check) ending the loop.
Writer ⟷ Critic ⟷ Writer ⟷ Critic → (Critic approves) → Output
Best for: tasks that benefit from genuine back-and-forth refinement — code review cycles, editorial revision, negotiation-style tasks where a single pass rarely produces the final-quality output.
Practical Tutorial: Building a Researcher → Writer → Code Reviewer → Tester Pipeline
Pattern A: Hierarchical, via LangGraph Conditional Routing
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-6")
class TeamState(TypedDict):
task: str
research_notes: str
draft_code: str
review_feedback: str
test_results: str
needs_revision: bool
def manager_route(state: TeamState) -> Literal["researcher", "code_reviewer", "end"]:
"""Manager decides the next specialist based on current state."""
if not state.get("research_notes"):
return "researcher"
if state.get("needs_revision"):
return "code_reviewer" # send back for another review pass
return "end"
def researcher_node(state: TeamState) -> TeamState:
response = llm.invoke(f"Research technical context needed for: {state['task']}")
return {"research_notes": response.content}
def writer_node(state: TeamState) -> TeamState:
response = llm.invoke(
f"Write Python code for: {state['task']}\nContext: {state['research_notes']}"
)
return {"draft_code": response.content}
def code_reviewer_node(state: TeamState) -> TeamState:
response = llm.invoke(
f"Review this code for correctness and security issues:\n{state['draft_code']}\n"
f"Respond with 'APPROVED' or specific revision feedback."
)
approved = "APPROVED" in response.content
return {"review_feedback": response.content, "needs_revision": not approved}
def tester_node(state: TeamState) -> TeamState:
response = llm.invoke(f"Write and describe unit tests for:\n{state['draft_code']}")
return {"test_results": response.content}
graph = StateGraph(TeamState)
graph.add_node("researcher", researcher_node)
graph.add_node("writer", writer_node)
graph.add_node("code_reviewer", code_reviewer_node)
graph.add_node("tester", tester_node)
graph.set_entry_point("researcher")
graph.add_edge("researcher", "writer")
graph.add_edge("writer", "code_reviewer")
graph.add_conditional_edges(
"code_reviewer",
lambda state: "writer" if state["needs_revision"] else "tester",
{"writer": "writer", "tester": "tester"},
)
graph.add_edge("tester", END)
app = graph.compile()
result = app.invoke({"task": "a rate limiter using the token bucket algorithm"})
The key architectural decision here is add_conditional_edges() on the reviewer node — if the reviewer doesn’t approve, the graph routes back to the writer for revision rather than proceeding, and this loop is explicit and bounded by the graph structure itself, not by hoping an LLM “remembers” to loop back.
Pattern B: Hierarchical, via CrewAI’s Manager Process
from crewai import Agent, Task, Crew, Process
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-6")
researcher = Agent(
role="Technical Researcher",
goal="Gather accurate technical context for implementation tasks",
backstory="You research best practices and prior art before any code is written.",
llm=llm,
)
writer = Agent(
role="Python Developer",
goal="Write clean, well-documented Python code",
backstory="You implement based on research context, following PEP 8 strictly.",
llm=llm,
)
reviewer = Agent(
role="Code Reviewer",
goal="Catch correctness and security issues before code ships",
backstory="You are a strict reviewer who never approves code with unhandled edge cases.",
llm=llm,
)
tester = Agent(
role="QA Engineer",
goal="Write comprehensive unit tests",
backstory="You write tests that cover edge cases the developer likely missed.",
llm=llm,
)
research_task = Task(
description="Research implementation approaches for a token bucket rate limiter.",
expected_output="A summary of the algorithm and key implementation considerations.",
agent=researcher,
)
write_task = Task(
description="Implement a token bucket rate limiter in Python.",
expected_output="Complete, documented Python code.",
agent=writer,
context=[research_task],
)
review_task = Task(
description="Review the implementation for correctness and security issues.",
expected_output="Either 'APPROVED' or specific revision feedback.",
agent=reviewer,
context=[write_task],
)
test_task = Task(
description="Write unit tests covering normal operation and edge cases.",
expected_output="A complete test suite.",
agent=tester,
context=[write_task, review_task],
)
crew = Crew(
agents=[researcher, writer, reviewer, tester],
tasks=[research_task, write_task, review_task, test_task],
process=Process.hierarchical,
manager_llm=llm,
)
result = crew.kickoff()
CrewAI’s Process.hierarchical introduces an implicit manager LLM that oversees task delegation — this is more convenient to set up than LangGraph’s explicit conditional routing, but the revision loop (reviewer sending work back to the writer) is not native to this structure the way it is in the LangGraph version above; CrewAI’s hierarchical mode is better suited to delegation of independent subtasks than to iterative revision loops.
Pattern C: Peer-to-Peer, via AutoGen Group Chat (Writer/Critic Loop)
from autogen import AssistantAgent, GroupChat, GroupChatManager
llm_config = {"model": "claude-sonnet-4-6", "api_key": "your-key"}
writer = AssistantAgent(
name="Writer",
system_message="You write Python code based on the task. Revise based on Critic feedback.",
llm_config=llm_config,
)
critic = AssistantAgent(
name="Critic",
system_message=(
"You review the Writer's code strictly. If it has issues, explain them clearly. "
"If it meets all requirements, respond with exactly 'APPROVED'."
),
llm_config=llm_config,
)
group_chat = GroupChat(
agents=[writer, critic],
messages=[],
max_round=6, # hard cap to prevent runaway conversations
speaker_selection_method="round_robin",
)
manager = GroupChatManager(groupchat=group_chat, llm_config=llm_config)
writer.initiate_chat(
manager,
message="Write a token bucket rate limiter in Python.",
)
max_round=6 is the critical safety valve here — peer-to-peer conversational patterns can, without a hard cap, continue exchanging feedback indefinitely if the termination condition (APPROVED) is ambiguous or the critic is overly strict; always bound conversational loops explicitly rather than trusting the conversation to self-terminate.
Tool / Solution Comparison Table
| Topology | Best Fit | Framework Best Suited | Key Risk |
|---|---|---|---|
| Pipeline (sequential) | Linear task decomposition, easiest to debug | LangGraph (linear edges) or CrewAI (Process.sequential) |
Rigid — can’t adapt if a step needs to be skipped or repeated dynamically |
| Hierarchical (manager-delegate) | Tasks where the right specialist subset varies by input | CrewAI (Process.hierarchical) or LangGraph with conditional edges |
Manager LLM’s delegation reasoning can misroute on ambiguous tasks |
| Peer-to-peer (group chat) | Iterative refinement, critique/revise cycles | AutoGen (GroupChat) | Unbounded conversations without an explicit round cap or clear termination signal |
Actionable Checklist / Next Steps
- Identify whether your task is linear, input-dependent, or iterative-refinement in shape before picking a topology — this decision matters more than which framework you use.
- Default to a pipeline unless you have a specific reason to need dynamic routing or back-and-forth conversation.
- Write narrow, specific role prompts for each agent — avoid vague “helpful assistant” framing even when using a dedicated role-based framework.
- Always bound peer-to-peer/conversational loops with an explicit max-round cap, not just a hoped-for termination phrase.
- Keep your agent count as small as the task genuinely requires — each additional agent adds cost, latency, and a new failure surface.
- Test your revision/feedback loop with intentionally bad input to confirm it actually loops back and terminates correctly, not just on the happy path.
Next in this series: Pixar-Style Visual Animation Workflows: From Image Prompt to Render — Day 12 shifts to AI media production pipelines.
AgenticMedia Team
Content Creator • @agenticmedia
Writer and technology enthusiast sharing engineering playbooks and digital optimization guides.
