An autogen planner coder critic agent team separates the work of designing a solution, writing the code, and reviewing it into three specialized agents that talk to each other. This tutorial builds that team on AutoGen’s GroupChat, with runnable Python you can point at any OpenAI-compatible model endpoint.
Prerequisites
- Python 3.10 or newer
autogen==0.2.32(the stable 0.2 line; the 0.4 API is different and not used here)- An API key for an OpenAI-compatible LLM endpoint. If you want provider redundancy, point AutoGen’s config at an OpenAI-compatible gateway such as n4n.ai, which fronts 240+ models and fails over automatically when a provider is degraded.
- Basic familiarity with Python dicts and async control flow
Install the framework before continuing:
pip install autogen==0.2.32
Step 1: Import and configure the LLM backend
AutoGen 0.2 uses a plain dict for LLM settings. We define one base config and derive per-agent configs so we can use a cheaper model for planning and a stronger one for implementation.
import autogen
from autogen import AssistantAgent, UserProxyAgent, GroupChat, GroupChatManager
base_llm = {
"seed": 42,
"api_key": "YOUR_KEY",
"base_url": "https://api.openai.com/v1", # swap for your gateway
"temperature": 0.2,
}
planner_llm = {**base_llm, "model": "gpt-4o-mini"}
coder_llm = {**base_llm, "model": "gpt-4o"}
critic_llm = {**base_llm, "model": "gpt-4o"}
A gateway like n4n.ai lets you address 240+ models via one endpoint, so you can change the "model" string without touching base_url or auth.
Step 2: Define the three agent roles
Each agent gets a tight system message. The planner never writes code. The coder writes only code blocks. The critic is terse and decisive.
planner = AssistantAgent(
name="Planner",
llm_config=planner_llm,
system_message=(
"You are a software architect. Given a task, output a concise plan with "
"numbered steps. Do not write code. Stop when the plan is clear."
),
)
coder = AssistantAgent(
name="Coder",
llm_config=coder_llm,
system_message=(
"You are a Python implementer. Given a plan, write the minimal code to "
"fulfill it. Output only code blocks with brief comments. No prose."
),
)
critic = AssistantAgent(
name="Critic",
llm_config=critic_llm,
system_message=(
"You are a senior reviewer. Given code, point out bugs, edge cases, and "
"style issues. If the code is correct, reply with exactly 'LGTM'. Be specific."
),
)
We add a user proxy that never asks a human and terminates the chat when the critic approves.
user = UserProxyAgent(
name="User",
human_input_mode="NEVER",
code_execution_config=False,
default_auto_reply="",
is_termination_msg=lambda m: "LGTM" in m.get("content", ""),
)
Step 3: Wire up the group chat
GroupChat manages the message list and speaker order. round_robin forces Planner → Coder → Critic → Planner… which keeps the review loop deterministic.
group = GroupChat(
agents=[user, planner, coder, critic],
messages=[],
max_round=6,
speaker_selection_method="round_robin",
)
manager = GroupChatManager(group, llm_config=base_llm)
Step 4: Run a simple task
Kick off the conversation with a concrete, small problem.
task = "Write a function that reverses a string without using slicing."
user.initiate_chat(manager, message=task)
Expected output (abridged):
User (to chat_manager):
Write a function that reverses a string without using slicing.
Planner (to chat_manager):
1. Accept a string input.
2. Initialize an empty list.
3. Iterate characters from last to first, append to list.
4. Join list into string and return.
Coder (to chat_manager):
def reverse_string(s: str) -> str:
out = []
for i in range(len(s)-1, -1, -1):
out.append(s[i])
return "".join(out)
Critic (to chat_manager):
LGTM
Because the critic’s message contains LGTM, the user proxy terminates and the process exits cleanly.
Step 5: Force a review cycle with a bug
Real code rarely passes first try. Change the coder prompt indirectly by giving a harder task, or simulate a bug by editing the coder’s system message to allow imperfect drafts. Here we just observe what happens when the coder makes an off-by-one error:
# Suppose Coder emitted:
# def reverse_string(s):
# out = []
# for i in range(len(s), 0, -1):
# out.append(s[i])
# return "".join(out)
The critic should respond:
Critic (to chat_manager):
Bug: range(len(s), 0, -1) starts at len(s), which is out of bounds, and stops before 0.
Fix: use range(len(s)-1, -1, -1).
With max_round=6 the chat continues: Planner stays quiet (plan already given), Coder speaks again with the fix, Critic reviews. If the fix is good, it replies LGTM and terminates.
Step 6: Use automatic speaker selection
Round robin is predictable but rigid. For open-ended tasks, let the manager LLM pick the next speaker:
group = GroupChat(
agents=[planner, coder, critic, user],
messages=[],
max_round=9,
speaker_selection_method="auto",
allow_repeat_speaker=False,
)
auto costs one extra LLM call per transition. It lets the critic ask the planner for clarification instead of blindly approving. Keep allow_repeat_speaker=False to avoid the same agent rambling.
Step 7: Token metering and callbacks
AutoGen invokes registered callbacks after each completion. Use this to track spend locally:
def log_usage(usage):
print(f"prompt={usage.get('prompt_tokens')} completion={usage.get('completion_tokens')}")
base_llm["callbacks"] = [log_usage]
If you routed through a gateway that does per-token metering, the same counts appear in your provider bill without extra instrumentation.
Step 8: Full runnable script
import autogen
from autogen import AssistantAgent, UserProxyAgent, GroupChat, GroupChatManager
base_llm = {
"seed": 42,
"api_key": "YOUR_KEY",
"base_url": "https://api.openai.com/v1",
"temperature": 0.2,
}
planner_llm = {**base_llm, "model": "gpt-4o-mini"}
coder_llm = {**base_llm, "model": "gpt-4o"}
critic_llm = {**base_llm, "model": "gpt-4o"}
planner = AssistantAgent("Planner", llm_config=planner_llm,
system_message="You are a software architect. Output numbered steps. No code.")
coder = AssistantAgent("Coder", llm_config=coder_llm,
system_message="You are a Python implementer. Output only code blocks.")
critic = AssistantAgent("Critic", llm_config=critic_llm,
system_message="You are a senior reviewer. Reply 'LGTM' if correct, else specifics.")
user = UserProxyAgent("User", human_input_mode="NEVER", code_execution_config=False,
default_auto_reply="", is_termination_msg=lambda m: "LGTM" in m.get("content",""))
group = GroupChat([user, planner, coder, critic], messages=[], max_round=6,
speaker_selection_method="round_robin")
manager = GroupChatManager(group, llm_config=base_llm)
user.initiate_chat(manager, message="Write a function that reverses a string without slicing.")
Practical notes
- Role separation is prompt engineering. If the planner starts writing code, tighten its system message. If the critic writes fixes instead of reviews, tell it to never emit code blocks.
- Latency is cumulative. Three agents means three sequential calls per cycle. Use the smallest capable model for the planner.
- Termination is mandatory. Without
is_termination_msg,round_robinwill hitmax_roundand dump a partial state. - Persist messages.
json.dump(group.messages, open("run.json","w"))gives you a replayable transcript for debugging agent behavior.
The autogen planner coder critic agent team pattern turns a single fuzzy prompt into a reviewable pipeline. It is not a replacement for CI or unit tests, but it catches obvious logic errors before the code reaches a human. Swap the roles for docs, test generation, or security review by changing system messages and keeping the same group chat scaffold.