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.
- an Anthropic API key (
ANTHROPIC_API_KEY) +pip install anthropic
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.
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 type | Role |
|---|---|
| Assistant agent | An LLM-backed agent — reasons, responds, can call tools. The workhorse. |
| User proxy / human | Represents the human — can auto-reply, execute code, or ask a real person (human-in-the-loop). |
| Team | A group of agents plus a pattern (round-robin, selector) that decides who speaks next. |
| Termination condition | The rule that ends the conversation — a keyword, a max-message count, or a custom check. |
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
APPROVEDappears. 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
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())
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.
- The
importlines 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. model = AnthropicChatCompletionClient(...)is the shared brain — both agents use the same Claude model; only their system messages (their instructions) differ.- The two
AssistantAgent(...)lines create the players. The coder is told to write and revise; the reviewer is told to reply exactlyAPPROVEDwhen the code is correct — that exact word is what will end the chat. TextMentionTermination("APPROVED")is the stop rule: end the conversation as soon as the textAPPROVEDappears.RoundRobinGroupChat([coder, reviewer], ...)puts both agents in a team that alternates turns.team.run(task=...)kicks it off with a real job. It'sasync(noteawaitandasyncio.run(main())) because agent calls happen over the network —awaitjust 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.
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 pattern | How the next speaker is chosen | Maps to (L1) |
|---|---|---|
| Round-robin | Fixed rotation — each agent in turn | Simple sequential collaboration |
| Selector | A model picks the best next speaker for the moment | Supervisor / dynamic routing |
| Swarm / handoff | Agents explicitly hand off to a named peer | Network with directed edges |
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)
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.
- Three agents are defined — a planner (breaks the task into steps), a coder (implements a step), and a reviewer (says
DONEwhen complete). Distinct jobs matter here: the selector can only choose well if the roles are clearly different. TextMentionTermination("DONE") | MaxMessageTermination(20)combines two stop rules with|(meaning "either one ends it"): stop when someone saysDONE, or after 20 messages no matter what. The second is a safety cap for when the agents get stuck.SelectorGroupChat([...], model_client=model, ...)builds the team. Unlike round-robin, it passes amodel_clientbecause 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.
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."
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)
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.
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.AssistantAgent("analyst", ..., tools=[get_stock], ...)hands that function to the agent as a tool. Now instead of guessing a price, the analyst can callget_stockand use the real result — the system message even forbids guessing.UserProxyAgent("human", input_func=input)adds a real person to the chat.inputis 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.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.
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
| Framework | Metaphor | Best at | Watch out for |
|---|---|---|---|
| CrewAI (M1) | Roles on an assembly line | Quick, readable role-based teams | Less exact control |
| AutoGen (this) | Agents in conversation | Open-ended collaboration, code-gen loops, research chat | Runaway chatter without firm termination; API churn |
| LangGraph (L4/L5) | Explicit state machine | Control, persistence, human-in-the-loop, audit | More upfront wiring |
Common pitfalls advanced
| Pitfall | Fix |
|---|---|
| No termination condition | Always set one; combine a semantic stop with a max-message cap |
Copying old pyautogen tutorials | Use the modern autogen-agentchat API; verify imports |
| Agents that flatter instead of finishing | Give a concrete, checkable done-signal ("reply 'APPROVED'") |
| Auto-executing code with no sandbox | Sandbox the executor; gate destructive actions (T1) |
| Selector team with vague agent roles | Distinct system messages so the selector can choose well |
| Using conversation when you need control | Use 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.
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.
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.
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.
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.
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.
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 aMaxMessageTermination(cap) - Show the team being run on a task
- Note the modern
autogen-agentchatAPI differs from olderpyautogen; 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.
Knowledge check check yourself
AutoGen models multi-agent work as a conversation; why is a termination condition described as the load-bearing design decision?
Show answer
How does a SelectorGroupChat choose the next speaker differently from a RoundRobinGroupChat, and what does each map to in L1's topologies?