The most powerful AI systems being built right now do something different: they put multiple AI agents in a room together, give each one a specific job, and let them work through a problem as a team.
These are really good resources i recommend more about this:
- DeepLearning.AI, AI Agentic Design Patterns with AutoGen
- AutoGen Official Tutorial (0.2.x
- Coursera, AI Agentic Design Patterns with AutoGen
What These Agents Do
This post covers three separate but related builds, each one demonstrating a different way AI agents can collaborate.
The first build puts two agents together: one that represents the goal (get a competitive analysis done) and one that does the work. They exchange messages until the job is complete, then stop automatically.
The second build creates a three-agent team inside a group chat. One agent researches a topic, a second critiques the research and identifies weaknesses, and a third synthesizes everything into a final polished briefing. A manager agent decides who speaks next. The team runs without any human input from start to finish.
The third build is the most powerful. One agent writes Python code. A second agent executes that code on the local machine and sends back the result. If something fails, the first agent reads the error, rewrites the code, and tries again. The loop continues until the output is correct.
All three use the same configuration file. Swap one line and they run on a different AI model entirely.
Why Build This
I kept seeing the same pattern in every conversation I had with founders about AI: they were thinking about AI as a single assistant. One model, one prompt, one answer. That mental model caps what you can build.
The frameworks that matter right now are the ones that let multiple AI agents with different specialties hand work to each other, review each other, and close the loop without waiting for a human to approve every step. AutoGen is one of the clearest implementations of that idea I have worked with. It does not hide the architecture behind abstractions. You define the agents, define the rules of the conversation, and watch it run.
What Actually Happened
What I expected: AutoGen would install cleanly under the name I specified and all three agents would run.
What actually happened: The package name changed. pyautogen is no longer the correct install name on PyPI for the 0.2.x API. The correct name is autogen-agentchat. This cost time and produced a confusing error before the cause became clear.
What broke: agent_v2.py failed on the first run with TypeError: GroupChat.__init__() missing 1 required positional argument: 'messages'. AutoGen 0.2.38 requires an explicit messages=[] parameter when instantiating a GroupChat. The documentation for this version does not make it obvious.
What surprised me: the dependency issue was not in the AutoGen scripts themselves. The scripts compiled and ran once the direct dependencies matched the imports. The unnecessary litellm Python package pin created a dependency conflict even though the code only needed a LiteLLM-compatible HTTP endpoint.
What I would change next: All three builds stop when the agent writes a specific phrase like ANALYSIS COMPLETE in its final message. If the agent forgets to include it, the conversation runs until it hits the round limit or loops indefinitely. A production version would use a structured JSON response with a completion flag instead of relying on the agent to remember an exact string.
Why this matters: Every one of these friction points is invisible until you actually run the build. The gap between reading about AutoGen and shipping something that runs end to end is where most teams give up. These notes exist so you do not have to.
The Architecture: How It Works
AutoGen organizes multi-agent collaboration around a simple idea: agents are participants in a conversation, and conversations have rules.
Every agent in AutoGen has a role defined by its system message, which works like a job description. The agent reads that description before every response and uses it to decide how to behave. A research agent summarizes facts. A critic agent finds weaknesses. A synthesizer agent combines both into a final output. The specialization is entirely in the instructions, not in the code.
The framework handles everything else: who speaks next, when the conversation ends, and how messages are passed between participants.
The Three Patterns
Two-agent conversation. The simplest pattern. One agent holds the goal, the other does the work. They exchange messages until a termination condition is met. In this build, the conversation ends when the assistant includes the phrase “ANALYSIS COMPLETE” in its response. The proxy agent detects that phrase and stops the loop automatically.
Group chat with a manager. Three specialized agents plus a manager. The manager is not a human. It is another AI agent whose job is to read the conversation state and decide which specialist should speak next. This is the GroupChatManager. It runs on the same model as the other agents and makes routing decisions automatically based on what has been said so far.
Code execution loop. Two agents, but one of them can run code. The UserProxyAgent in this pattern has a code_execution_config that points to a working directory on the local machine. When the coder agent writes a Python script inside a code block, the proxy extracts it, runs it, and sends the output back into the conversation. The coder reads the output and decides whether to confirm success or fix the error. The loop runs up to five times before stopping.
The Model-Agnostic Layer
Every agent in all three builds pulls its model configuration from a single .env file through LiteLLM. LiteLLM is a translation layer that sits between your code and any AI provider. Your agent sends a request in one standard format, LiteLLM translates it for whichever provider you have configured, and the response comes back in the same standard format your agent expects.
Switching from DeepSeek to Gemini requires changing one line:
MODEL_NAME=openai/deepseek-v4-pro
can become whatever model name your LiteLLM proxy exposes, such as:
MODEL_NAME=gemini-flash
Nothing else changes. The agent code is identical. In this verification pass, all three builds were tested successfully with gemini-flash through the local LiteLLM proxy.
The Build: Step by Step
Step 1: Install the dependencies
autogen-agentchat==0.2.38
python-dotenv==1.1.1
What This Code Is Actually Doing
autogen-agentchat is the AutoGen 0.2.x framework under its correct PyPI name. The version is pinned because newer AutoGen APIs differ significantly. python-dotenv reads the .env file so credentials never live inside the code.
The LiteLLM Python package is not required here because the scripts do not import litellm. AutoGen sends requests to the LiteLLM-compatible endpoint through the configured base_url.
This was fixed during verification because the previous litellm package pin conflicted with python-dotenv and was not imported by the scripts.
Install with:
py -3.11 -m pip install -r requirements.txt
Step 2: The shared configuration block
Every agent in every file starts with this:
import os
from dotenv import load_dotenv
import autogen
load_dotenv()
llm_config = {
"config_list": [
{
"model": os.environ["MODEL_NAME"],
"api_key": os.environ["LITELLM_API_KEY"],
"base_url": os.environ["LITELLM_BASE_URL"],
}
],
"cache_seed": None,
}
What This Code Is Actually Doing
load_dotenv() reads the .env file and makes its contents available as environment variables. Without this line the script cannot find the model name or API key.
The llm_config dictionary is the connection card every agent carries. It tells AutoGen which model to use, how to authenticate, and where to send requests. base_url points to the LiteLLM proxy running locally on the machine via an SSH tunnel.
cache_seed: None disables AutoGen’s built-in response cache. Caching is useful for development but causes stale responses when you switch providers mid-session. Turning it off ensures every run produces a live response from whichever model is active.
Step 3: Build One, the two-agent conversation (agent_v1.py)
user_proxy = autogen.UserProxyAgent(
name="user_proxy",
human_input_mode="NEVER",
max_consecutive_auto_reply=2,
is_termination_msg=lambda x: "ANALYSIS COMPLETE" in x.get("content", ""),
code_execution_config=False,
)
assistant = autogen.AssistantAgent(
name="assistant",
system_message=(
"You are a strategic business analyst. When asked to analyze a company, produce a structured competitive analysis "
"with three sections: Market Position, Key Risks, and One Strategic Recommendation. End your final response with "
"the exact phrase ANALYSIS COMPLETE."
),
llm_config=llm_config,
)
user_proxy.initiate_chat(
assistant,
message="Produce a competitive analysis for a mid-size SaaS company entering the AI governance market in Latin America.",
)
What This Code Is Actually Doing
Think of user_proxy as a project manager who has been given one task and will not rest until it is done. human_input_mode="NEVER" means the proxy runs fully automated. No keyboard input is needed. It initiates the task, receives the response, and evaluates whether the job is complete.
is_termination_msg is the completion detector. It is a small function that reads every incoming message and checks whether it contains the phrase “ANALYSIS COMPLETE”. When it finds that phrase, it tells AutoGen the conversation is over and the script exits cleanly.
max_consecutive_auto_reply=2 is a safety ceiling. If the termination phrase never appears for some reason, the proxy stops after two replies so the script does not run indefinitely.
The assistant agent has a system message that acts as its job description. It knows it is a business analyst, it knows the output format required, and it knows to end its final message with the exact phrase the proxy is looking for. The two agents are designed to close the loop together.
Step 4: Build Two, the group chat (agent_v2.py)
research_agent = autogen.AssistantAgent(
name="research_agent",
system_message="Gather and summarize factual information on AI governance trends in enterprise settings. Keep responses under 200 words.",
llm_config=llm_config,
)
critic_agent = autogen.AssistantAgent(
name="critic_agent",
system_message="Review the previous research response and identify exactly two gaps or weak claims. Keep responses under 150 words.",
llm_config=llm_config,
)
synthesizer_agent = autogen.AssistantAgent(
name="synthesizer_agent",
system_message="Take the research and the critique and produce a final 250-word AI governance briefing for an enterprise client. End the briefing with the exact phrase BRIEFING COMPLETE.",
llm_config=llm_config,
)
groupchat = autogen.GroupChat(
agents=[user_proxy, research_agent, critic_agent, synthesizer_agent],
messages=[],
max_round=6,
speaker_selection_method="auto",
)
manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=llm_config)
What This Code Is Actually Doing
Each agent is defined by its system message alone. The research agent summarizes. The critic finds exactly two weaknesses. The synthesizer closes. Three different jobs, three different instructions, all running on the same underlying model.
GroupChat is the room where the conversation happens. messages=[] initializes an empty conversation history. This parameter is required in AutoGen 0.2.38 and the build will fail without it.
speaker_selection_method="auto" tells the GroupChatManager to decide who speaks next based on the conversation so far. The manager reads the full history and routes to whichever agent’s role fits the current moment. After the researcher speaks, the manager sends the baton to the critic. After the critic, it goes to the synthesizer. This routing is automatic and requires no additional code.
max_round=6 is the ceiling. Six total exchanges maximum. This prevents the group from running indefinitely if the termination phrase is never produced.
Step 5: Build Three, the code execution loop (agent_v3.py)
executor = autogen.UserProxyAgent(
name="executor",
human_input_mode="NEVER",
max_consecutive_auto_reply=5,
code_execution_config={"work_dir": "coding", "use_docker": False},
is_termination_msg=lambda x: "EXECUTION COMPLETE" in x.get("content", ""),
)
coder = autogen.AssistantAgent(
name="coder",
system_message=(
"You are a Python programmer. When given a data task, write a complete Python script in a single code block. "
"After the executor runs your code and returns output, review the output. If it is correct, respond with a summary "
"of what the script did followed by the exact phrase EXECUTION COMPLETE. If there is an error, diagnose it, "
"rewrite the script, and try again."
),
llm_config=llm_config,
)
What This Code Is Actually Doing
code_execution_config is what makes this build different from the other two. It gives the executor agent the ability to actually run code on your machine. work_dir: "coding" is the folder where the generated scripts are saved and executed. use_docker: False means the code runs directly in your local Python environment rather than inside a sandboxed container.
The coder agent writes a complete Python script inside a code block in its response. AutoGen detects that code block, extracts it, and hands it to the executor. The executor runs the script, captures the output or error, and sends it back into the conversation as the next message. The coder reads that output and makes a decision: confirm success with “EXECUTION COMPLETE” or diagnose the error and rewrite.
max_consecutive_auto_reply=5 gives the coder up to five attempts to produce working code. In this verification pass, the code ran correctly on the first attempt, so the self-correction loop was never triggered. But the architecture is there, and it works.
Errors I Hit During This Build
Error 1: Package not found
ERROR: Could not find a version that satisfies the requirement pyautogen==0.2.38
Cause: Microsoft renamed the PyPI package. pyautogen is no longer the correct install name for the 0.2.x API. The available versions under that name jump from 0.1.14 directly to 0.10.0, which is the new 0.4+ API.
Fix: Change the install name in requirements.txt to autogen-agentchat==0.2.38. The import inside the Python files stays as import autogen. Only the install name changed.
Error 2: GroupChat missing required argument
TypeError: GroupChat.__init__() missing 1 required positional argument: 'messages'
Cause: AutoGen 0.2.38 requires an explicit messages parameter when creating a GroupChat instance. This is not obvious from most documentation examples written for earlier versions.
Fix: Add messages=[] to the GroupChat instantiation. One line, immediate fix.
What I Would Do Differently
The way these three builds know when to stop is simple: they wait for the agent to write a specific phrase like ANALYSIS COMPLETE at the end of its message. That works fine in a controlled test. In a real business pipeline it is risky, because nothing forces the agent to remember that phrase every time. A more reliable design would have the agent return a structured response, like a short JSON object with a field that says ‘done: true’, so the system has a machine-readable signal to check rather than hunting for a word inside a sentence.
The code execution build runs generated code directly on your machine. For a personal project where you control what goes in, that is fine. If this were a customer-facing product where outside users could influence what the agent writes and runs, you would want that code running inside an isolated container, a sealed box that cannot touch the rest of your system. AutoGen supports this natively. One config change is all it takes.
The group chat manager decides who speaks next by reading the conversation and making a judgment call. That works well when the task is straightforward. In a high-stakes workflow, like a compliance review or a financial approval chain, you probably do not want an AI deciding the order of operations. AutoGen has an alternative mode where the speaking order is fixed in advance and rotates predictably, so the sequence is always the same regardless of what the model thinks.
What This Means
Every business that runs on information workflows has tasks that follow the same pattern AutoGen demonstrates here: gather, critique, synthesize. A law firm reviewing contracts. A marketing team developing campaign briefs. A compliance department assessing vendor risk. A product team consolidating customer feedback.
The group chat pattern in Build Two automates that entire workflow. The research agent replaces the junior analyst pulling together information. The critic agent replaces the senior reviewer finding gaps. The synthesizer agent replaces the writer turning notes into a deliverable. The whole cycle runs in under two minutes on a laptop.
The code execution pattern in Build Three goes further. It replaces the back-and-forth between a business analyst who defines the question and a data analyst who writes the query. The coder agent writes the analysis, the executor runs it, and if it fails the coder fixes it. No ticket. No waiting. No handoff.
The three files in this post are working starting points. The model is configurable, the tasks are configurable, and the team structure is configurable. Adapting them to a specific use case takes hours, not weeks of design from scratch. Production deployment will require additional hardening, security review, and testing for your specific environment.
Tools Used
AutoGen (MIT): github.com/microsoft/autogen
LiteLLM (MIT): github.com/BerriAI/litellm
NosisTech Agent Engineering (MIT): github.com/nosistech/agent-engineering
(c) 2026 NosisTech LLC. Licensed under CC BY 4.0. Use freely, just credit us.