AI EngineeringZero to ProductionHome·About·Contact
Multi-Agent Orchestration · Chapter M2

Multi-Agent Systems with Microsoft AutoGen

CrewAI models a team as roles on an assembly line. AutoGen models it as a conversation: agents that talk to each other, in patterns you choose. It leans into the network topology from L1 — powerful for open-ended collaboration, and a sharp lesson in why free-form agent chatter needs firm termination rules.

⏱️ ~55 min🧪 3 labs🎯 Intermediate→Advanced
⚙️ To run this for realThe code here is complete and correct as written. To actually execute it you'll need:
  • an Anthropic API key (ANTHROPIC_API_KEY) + pip install anthropic
Reading and learning works without any of this — run when you're ready. IDs/ARNs in examples are placeholders; swap in your own.

Learning objectives

  • Explain AutoGen's conversation-based model and its core agent types.
  • Build a two-agent conversation with a clear termination condition.
  • Use a team pattern (round-robin / selector) for more than two agents.
  • Add tools and a human-in-the-loop participant.
  • Contrast AutoGen with CrewAI and LangGraph and pick deliberately.
Builds on L1 & M1AutoGen is the clearest expression of L1's network topology (peers conversing) and also supports supervisor-style routing. Read it against M1's CrewAI: same goal — coordinate specialists — different metaphor (conversation vs assembly line).
AutoGen moves fast — learn the shapes, verify the APIAutoGen was substantially redesigned (the modern autogen-agentchat line differs from older autogen/pyautogen examples), and class names/imports shift across versions. The concepts here — conversation, termination, teams, human proxy — are stable; treat exact imports as illustrative and check them against the version you install.

The model: agents in conversation essential

In AutoGen, work happens through messages between agents. You define agents, put them in a team with a conversation pattern, give it a task, and the agents talk until a termination condition fires. The core participants:

Agent typeRole
Assistant agentAn LLM-backed agent — reasons, responds, can call tools. The workhorse.
User proxy / humanRepresents the human — can auto-reply, execute code, or ask a real person (human-in-the-loop).
TeamA group of agents plus a pattern (round-robin, selector) that decides who speaks next.
Termination conditionThe rule that ends the conversation — a keyword, a max-message count, or a custom check.
Coder Reviewer "here's the code" "fix line 3, then it's APPROVED" conversation ends when a termination condition matches (e.g. "APPROVED") Collaboration as dialogue. Agents exchange messages; each reads the shared conversation and responds. The catch: nothing stops the chatter unless you define when it's done. Termination is not optional — it's the load-bearing design decision.
🗺️ How to read this diagram

This picture is the whole idea of AutoGen in one image: instead of a fixed assembly line, two agents talk to each other. Each one reads the shared conversation and writes the next message, back and forth, until a rule says "stop".

  • The left box (Coder) and the right box (Reviewer) are two agents — each is an LLM given a job by its system message.
  • The top arrow is the Coder sending its work ("here's the code"); the bottom arrow is the Reviewer answering ("fix line 3, then it's APPROVED"). They take turns — this is the conversation.
  • The red line at the bottom is the load-bearing part: the chat only ends when a termination condition matches — here, when the word APPROVED appears. Without such a rule the two would keep messaging forever.

In short: AutoGen coordinates agents by letting them converse. Your one non-negotiable job as the designer is to decide when the conversation is finished — everything else flows from that.

Lab M2.1 · A two-agent conversation essential

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Lab M2.1
shellpip install -U "autogen-agentchat" "autogen-ext[anthropic]"
pair.pyimport asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.conditions import TextMentionTermination
from autogen_ext.models.anthropic import AnthropicChatCompletionClient

model = AnthropicChatCompletionClient(model="claude-opus-4-8")

coder = AssistantAgent("coder", model_client=model,
    system_message="Write Python. Revise based on review feedback.")
reviewer = AssistantAgent("reviewer", model_client=model,
    system_message="Review the code. When it's correct, reply exactly 'APPROVED'.")

# stop when 'APPROVED' appears OR after 8 messages — always bound it two ways
term = TextMentionTermination("APPROVED")
team = RoundRobinGroupChat([coder, reviewer], termination_condition=term)

async def main():
    await team.run(task="Write a function that returns the nth Fibonacci number.")

asyncio.run(main())
▶ How this works

This is the smallest complete AutoGen program: two agents that talk until one says the magic word. A coder writes code and a reviewer checks it; they trade messages until the reviewer is satisfied. It shows every moving part you need — agents, a team, and a termination rule.

  1. The import lines pull in the pieces: AssistantAgent (an LLM-backed agent), RoundRobinGroupChat (a team where agents speak in a fixed rotation), a termination condition, and the Anthropic model client that connects to Claude.
  2. model = AnthropicChatCompletionClient(...) is the shared brain — both agents use the same Claude model; only their system messages (their instructions) differ.
  3. The two AssistantAgent(...) lines create the players. The coder is told to write and revise; the reviewer is told to reply exactly APPROVED when the code is correct — that exact word is what will end the chat.
  4. TextMentionTermination("APPROVED") is the stop rule: end the conversation as soon as the text APPROVED appears. RoundRobinGroupChat([coder, reviewer], ...) puts both agents in a team that alternates turns.
  5. team.run(task=...) kicks it off with a real job. It's async (note await and asyncio.run(main())) because agent calls happen over the network — await just means "wait here for the reply, don't freeze the program".

What the output means: Behind the scenes the coder writes a Fibonacci function, the reviewer critiques it, the coder revises, and the reviewer eventually replies APPROVED — which trips the termination condition and the run returns.

Try this: Change the reviewer's done-word to something the coder would never accidentally type, and give the pair a harder task. Watch how many back-and-forth turns it takes before APPROVED shows up.

A conversation with no termination is a runaway billTwo agents will happily thank each other forever. Always set a termination condition — and back it with a max-message cap as a second brake, exactly like the recursion limit on a LangGraph cycle (L5). The "APPROVED" keyword is the primary stop; the message cap is the safety net.

Lab M2.2 · Teams of three+ (who speaks next?) essential

Beyond two agents, the key question is speaker selection: who talks next? AutoGen gives you patterns for this — the difference between L1's network (free-for-all) and supervisor (a chooser) topologies.

Team patternHow the next speaker is chosenMaps to (L1)
Round-robinFixed rotation — each agent in turnSimple sequential collaboration
SelectorA model picks the best next speaker for the momentSupervisor / dynamic routing
Swarm / handoffAgents explicitly hand off to a named peerNetwork with directed edges
Lab M2.2

Requires: pip install autogen-agentchat

selector.pyfrom autogen_agentchat.teams import SelectorGroupChat
from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination

planner  = AssistantAgent("planner",  model_client=model, system_message="Break the task into steps.")
coder    = AssistantAgent("coder",    model_client=model, system_message="Implement the current step.")
reviewer = AssistantAgent("reviewer", model_client=model, system_message="Review; say 'DONE' when complete.")

# selector uses a model to choose the next speaker each turn (supervisor-style)
term = TextMentionTermination("DONE") | MaxMessageTermination(20)   # combine conditions
team = SelectorGroupChat([planner, coder, reviewer], model_client=model,
                         termination_condition=term)
▶ How this works

With more than two agents the big question becomes who talks next? A round-robin just rotates, but a selector team asks a model to pick the best next speaker for the moment — like a supervisor handing the floor to whoever is most useful right now.

  1. Three agents are defined — a planner (breaks the task into steps), a coder (implements a step), and a reviewer (says DONE when complete). Distinct jobs matter here: the selector can only choose well if the roles are clearly different.
  2. TextMentionTermination("DONE") | MaxMessageTermination(20) combines two stop rules with | (meaning "either one ends it"): stop when someone says DONE, or after 20 messages no matter what. The second is a safety cap for when the agents get stuck.
  3. SelectorGroupChat([...], model_client=model, ...) builds the team. Unlike round-robin, it passes a model_client because a model does the choosing — reading the conversation and deciding who should speak next each turn.

Try this: Give the three agents vague, overlapping system messages and then very sharp, distinct ones. The selector's choices get noticeably smarter with clear roles — that's the whole point of letting a model pick the speaker.

Termination conditions composeNotice TextMentionTermination(...) | MaxMessageTermination(20) — conditions combine with |. Ship at least two: a semantic stop ("DONE") for the happy path and a hard cap for when the agents get stuck. This is the belt-and-suspenders pattern from every loop in this course.

Lab M2.3 · Tools & a human in the loop intermediate

AutoGen agents take tools (Python functions), and a human participant can join the conversation — the human-in-the-loop gate from L5, expressed as "a person is one of the speakers."

Lab M2.3

Requires: pip install autogen-agentchat

tools_human.pyfrom autogen_agentchat.agents import AssistantAgent, UserProxyAgent

def get_stock(ticker: str) -> str:
    """Get the latest price for a ticker."""
    return f"{ticker}: $123.45"

analyst = AssistantAgent("analyst", model_client=model,
    tools=[get_stock],                       # a Python function becomes a tool
    system_message="Use tools to get real data; never guess prices.")

# the human joins as a participant — asked to weigh in before finalizing
human = UserProxyAgent("human", input_func=input)   # prompts a real person at the console

team = RoundRobinGroupChat([analyst, human], termination_condition=term)
▶ How this works

Two upgrades in one lab: give an agent a tool (a plain Python function it can call for real data) and add a human as a participant in the conversation. Together these turn a chatty demo into something that can act and be supervised.

  1. get_stock(ticker) is an ordinary Python function — it just returns a price string here. The docstring ("""Get the latest price...""") is important: the model reads it to understand when to call the tool.
  2. AssistantAgent("analyst", ..., tools=[get_stock], ...) hands that function to the agent as a tool. Now instead of guessing a price, the analyst can call get_stock and use the real result — the system message even forbids guessing.
  3. UserProxyAgent("human", input_func=input) adds a real person to the chat. input is Python's built-in "ask at the keyboard" function, so when it's this agent's turn, the program pauses and waits for you to type — a human-in-the-loop gate.
  4. RoundRobinGroupChat([analyst, human], ...) puts the analyst and the human in a team that alternates: the analyst proposes, the human responds, and so on until the shared termination condition fires.

What the output means: At runtime the analyst calls get_stock to fetch a price instead of inventing one, then the console pauses for the human to weigh in before anything is finalized.

Try this: Add a second tool (say a function that looks up a company name) and watch the analyst decide which tool to call. This is the same tool-calling idea from the agents chapter, now inside a multi-agent conversation.

Two flavors of "user proxy"A UserProxyAgent can be a real human (prompts for input) or an automated stand-in that auto-replies or executes code. That dual nature is why AutoGen became known for code-writing agents: an assistant proposes code, the proxy executes it and reports results, and they iterate — a reflection loop (L1) between two participants. Same safety rule as always: gate anything that executes code or takes irreversible action.

AutoGen vs CrewAI vs LangGraph intermediate

FrameworkMetaphorBest atWatch out for
CrewAI (M1)Roles on an assembly lineQuick, readable role-based teamsLess exact control
AutoGen (this)Agents in conversationOpen-ended collaboration, code-gen loops, research chatRunaway chatter without firm termination; API churn
LangGraph (L4/L5)Explicit state machineControl, persistence, human-in-the-loop, auditMore upfront wiring
Pick by how much you trust free-form chatReach for AutoGen when the problem genuinely benefits from agents conversing to a solution (brainstorming, iterative code, debate). Reach for LangGraph when you need to guarantee the path and gate risky steps. CrewAI sits between for role pipelines. All three can call the same Claude models and the same tools — the difference is the coordination model, not the intelligence.

Common pitfalls advanced

PitfallFix
No termination conditionAlways set one; combine a semantic stop with a max-message cap
Copying old pyautogen tutorialsUse the modern autogen-agentchat API; verify imports
Agents that flatter instead of finishingGive a concrete, checkable done-signal ("reply 'APPROVED'")
Auto-executing code with no sandboxSandbox the executor; gate destructive actions (T1)
Selector team with vague agent rolesDistinct system messages so the selector can choose well
Using conversation when you need controlUse LangGraph for guaranteed paths & audit

Exercises advanced

Exercise M2.1 — Coder + reviewer loop

Context: Removing the termination condition and watching the team run to the cap is the fastest way to feel why termination is load-bearing.

Your task: Build a coder + reviewer pair, give it a real task, confirm it terminates on "APPROVED", then remove the termination condition and watch it run to the message cap.

Requirements:

  • A coder and a reviewer agent on a real task
  • Confirm it terminates on the agreed stop word
  • Remove the termination condition and observe it running to the cap
  • Note why termination is load-bearing

💡 Hint: With no stop word the only thing that ends the run is the hard message cap — which is the backstop, not the intended exit.

Exercise M2.2 — Selector team

Context: A selector team only picks well when the roles are distinct — comparing it to round-robin shows what clear roles buy you.

Your task: Build a three-agent selector team (planner, coder, reviewer), read the transcript to judge whether the selector picks sensible speakers, and compare to a round-robin version.

Requirements:

  • Three agents with distinct roles
  • A selector picks the next speaker by content
  • Read the transcript and judge the choices
  • Tighten the system messages until routing is sensible
  • Compare to a round-robin version of the same three

💡 Hint: Round-robin is predictable but wastes turns; the selector adapts only as well as the roles are distinguished — vague roles make it choose poorly.

Show what to look for

Round-robin is predictable but wastes turns (the reviewer speaks even when there's nothing to review). Selector adapts but only as well as the roles are distinguished — vague roles make it pick poorly. Clear roles are what make dynamic selection pay off.

Exercise M2.3 — Human approval gate

Context: Both AutoGen's user-proxy and LangGraph's interrupt pause for a human — but one gives a more durable, resumable gate, and knowing which matters.

Your task: Add a UserProxyAgent (a real human) that must approve before an analyst's recommendation is finalized, then compare it to LangGraph's interrupt (L5).

Requirements:

  • A user-proxy requires human approval before finalizing the recommendation
  • Run it and exercise the approval step
  • Compare to LangGraph's interrupt-based gate
  • State which gives a more durable, resumable pause and why

💡 Hint: A conversational approval lives in the running chat; a checkpointed interrupt persists the pause and can resume a different process — that durability is the difference.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Agents in conversationBeginner

Context: AutoGen frames collaboration as a dialogue: work happens as messages between agents, unlike CrewAI's assembly line.

Your task: Model a two-agent exchange: each agent appends a message, run a few turns, and print the transcript.

Requirements:

  • Represent each message with its sender and content
  • Two agents append messages in turn
  • Run a few turns of back-and-forth
  • Print the transcript in order

💡 Hint: A list of message dicts that the agents append to; the collaboration is the dialogue, not a fixed pipeline.

Show solution

Work happens as a message exchange. Runnable stdlib:

def turn(agent_name, msg):
    return {"from": agent_name, "content": msg}

conversation = []
conversation.append(turn("assistant", "Here is a draft plan."))
conversation.append(turn("reviewer",  "Looks good, tighten step 2."))
conversation.append(turn("assistant", "Revised step 2."))

for m in conversation:
    print(f"{m['from']:10}: {m['content']}")

Unlike CrewAI's assembly line, AutoGen frames collaboration as a dialogue — agents talk until a rule stops them.

Exercise 2 · A termination condition (or it never stops)Intermediate

Context: Free-form agent chatter needs a firm stop, or it burns tokens forever — so a conversation ends on a stop word OR a message cap, two independent brakes.

Your task: Model a conversation loop that ends on a keyword ('TERMINATE') or a max-message cap.

Requirements:

  • Stop when the last message contains the stop word
  • Also stop when the message count hits the cap
  • Report which brake ended the conversation
  • Show the stop word ending a run before the cap

💡 Hint: Check both conditions each turn; the cap is the backstop for when the stop word never comes — the same two-brake idea as the L5 cycle.

Show solution

Two brakes, exactly like the L5 cycle. Runnable:

def should_stop(messages, max_messages=6, stop_word="TERMINATE"):
    if len(messages) >= max_messages:
        return "hit message cap"
    if messages and stop_word in messages[-1]["content"]:
        return "stop word seen"
    return None

def run(scripted_replies, max_messages=6):
    msgs = []
    for reply in scripted_replies:
        msgs.append({"content": reply})
        reason = should_stop(msgs, max_messages)
        if reason:
            return msgs, reason
    return msgs, "ran out of scripted turns"

msgs, why = run(["work...", "more work...", "done TERMINATE", "never reached"])
print(len(msgs), "messages; stopped:", why)   # 3 messages; stop word seen

Without a termination condition, a conversation team burns tokens forever — the message cap is the backstop when the stop word never comes.

Exercise 3 · Round-robin team: who speaks nextAdvanced

Context: A team pattern decides speaking order. Round-robin is the simplest: cycle through the agents in turn until termination.

Your task: Model round-robin over N agents, cycling through them until a turn cap.

Requirements:

  • Cycle through the agents in a fixed order
  • Wrap around after the last agent (modulo the team size)
  • Stop at a max-turns cap
  • Show the rotation over more turns than there are agents

💡 Hint: A rotating index (i % len(agents)) gives every agent an equal, predictable turn — the baseline before a smarter selector.

Show solution

Round-robin is a rotating index over the team. Runnable:

def round_robin(agents, max_turns=5):
    transcript = []
    for i in range(max_turns):
        speaker = agents[i % len(agents)]      # cycle through the team
        transcript.append((speaker, f"turn {i+1}"))
    return transcript

for who, what in round_robin(["planner", "coder", "reviewer"]):
    print(who, "->", what)
# planner, coder, reviewer, planner, coder ...

Round-robin gives every agent an equal, predictable turn — the simplest team pattern before you reach for a smarter selector.

Exercise 4 · Selector pattern: pick the next speaker by contentExpert

Context: A selector chooses the next speaker based on the conversation, not a fixed rotation — L1's network topology with brains.

Your task: Model a selector that routes to the agent whose skill the last message needs.

Requirements:

  • Each agent advertises keywords for its skill
  • The selector inspects the last message and picks the matching agent
  • Fall back to a default agent when nothing matches
  • Show messages routing to different agents by content

💡 Hint: Match the last message against each agent's keywords; whoever's skill fits speaks next, enabling open-ended collaboration.

Show solution

The selector routes on need, not order. Runnable:

AGENTS = {"coder": ["code", "bug", "implement"],
          "researcher": ["find", "source", "data"],
          "reviewer": ["review", "check", "approve"]}

def select_next(last_message):
    low = last_message.lower()
    for agent, keywords in AGENTS.items():
        if any(k in low for k in keywords):
            return agent
    return "reviewer"          # default when unsure

print(select_next("please implement the fix"))   # coder
print(select_next("find data on latency"))         # researcher
print(select_next("does this look done?"))          # reviewer

A selector team is L1's network topology with brains: whoever's skill fits the current message speaks next, enabling open-ended collaboration.

Exercise 5 · Add a human-in-the-loop participantProfessional

Context: A user-proxy agent represents the human inside the conversation — auto-replying for routine turns and interrupting for risky ones, the AutoGen version of L5's approval gate.

Your task: Model a proxy that pauses for human approval on risky proposals and auto-approves the rest.

Requirements:

  • Define which actions are risky
  • A risky proposal returns an ask-human decision (with a reason)
  • A safe proposal auto-approves
  • Show a safe action auto-approved and a risky one escalated

💡 Hint: The proxy is just another participant with a gate: branch on whether the proposed action is on the risky list.

Show solution

The human proxy is just another participant with a gate. Runnable:

RISKY = {"deploy", "delete", "spend"}

def user_proxy(proposal, auto_approve_safe=True):
    action = proposal.get("action", "")
    if action in RISKY:
        return {"decision": "ASK_HUMAN", "why": f"'{action}' is risky"}
    if auto_approve_safe:
        return {"decision": "AUTO_APPROVE"}
    return {"decision": "ASK_HUMAN"}

print(user_proxy({"action": "summarize"}))   # AUTO_APPROVE
print(user_proxy({"action": "deploy"}))       # ASK_HUMAN

The user-proxy lets a human sit inside the conversation — auto-replying for routine turns and interrupting for the risky ones, the AutoGen version of L5's approval gate.

Exercise 6 · The real AutoGen team (needs the framework)Industry scenario

Context: The production version uses the modern autogen-agentchat line: two AssistantAgents in a RoundRobinGroupChat with a termination condition — the conversation + brakes you modeled offline.

Your task: Write the real AutoGen team using documented APIs. (Needs the framework installed.)

Requirements:

  • Two AssistantAgents with a model client and system messages
  • Assemble them in a RoundRobinGroupChat
  • Combine two independent brakes: a TextMentionTermination (stop word) OR a MaxMessageTermination (cap)
  • Show the team being run on a task
  • Note the modern autogen-agentchat API differs from older pyautogen; label it as needing the framework + a model client

💡 Hint: The team rotates speakers and stops on the stop word or the message cap — the same two-brake pattern; verify imports against your installed version.

Show solution

Correct modern AutoGen. Needs pip install autogen-agentchat autogen-ext + a model client:

from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.conditions import TextMentionTermination, MaxMessageTermination
# from autogen_ext.models.openai import OpenAIChatCompletionClient  # or another client

# model_client = OpenAIChatCompletionClient(model="...")  # your provider

writer = AssistantAgent("writer", model_client=model_client,
                        system_message="Draft a short answer.")
reviewer = AssistantAgent("reviewer", model_client=model_client,
                          system_message="Critique, then say TERMINATE when good.")

# two independent brakes: a stop word OR a hard message cap
termination = TextMentionTermination("TERMINATE") | MaxMessageTermination(8)
team = RoundRobinGroupChat([writer, reviewer], termination_condition=termination)

# import asyncio; asyncio.run(team.run(task="Explain vLLM in two sentences."))

Same conversation + termination you modeled offline: the team rotates speakers and stops on the stop word or the message cap. The modern autogen-agentchat API differs from older pyautogen — verify imports against your version.

✓ Checkpoint — you can move on when you can…

  • Explain AutoGen's conversation model and its agent types.
  • Build a two-agent team with a termination condition.
  • Choose round-robin vs selector and say what each maps to in L1.
  • Add a tool and a human participant.
  • Choose between AutoGen, CrewAI, and LangGraph for a given problem.
🏗️ Toward the capstoneAn AutoGen coder+reviewer loop is a great way to generate and vet the Terraform the DevOps agent proposes — a debate that catches mistakes before a human ever sees the plan. But the moment that plan touches prod, control moves to the LangGraph safety gate (L5): conversation for ideation, a state machine for the irreversible step. See the safety-gate build →

Knowledge check check yourself

✓ Knowledge check

AutoGen models multi-agent work as a conversation; why is a termination condition described as the load-bearing design decision?

Show answer
Agents exchange messages until a termination condition fires — nothing stops the chatter on its own, so two agents will happily message forever and run up a bill. You must define when the conversation is done, and the guidance is to combine a semantic stop (e.g. the word "APPROVED") with a hard max-message cap as a second brake.
✓ Knowledge check

How does a SelectorGroupChat choose the next speaker differently from a RoundRobinGroupChat, and what does each map to in L1's topologies?

Show answer
Round-robin rotates speakers in a fixed order (simple sequential collaboration). A selector team uses a model to pick the best next speaker each turn (dynamic routing) — but it only chooses well when the agents have clearly distinct roles. Round-robin maps to sequential collaboration; selector maps to the supervisor/dynamic-routing topology.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in