Introduction to Building a Documentation Generator with AutoGen
Documentation is often the most neglected part of the software development lifecycle. Developers write code, ship features, and move on — leaving behind stale, incomplete, or missing docs. Microsoft's AutoGen framework, a multi-agent conversation system, offers a powerful way to automate this process by orchestrating specialized AI agents that read, analyze, and produce high-quality documentation from your codebase.
In this guide, you'll learn what an AutoGen-based documentation generator is, why it matters, how to build one from scratch, and the best practices to follow when deploying it in real-world projects.
What Is AutoGen?
AutoGen is an open-source framework by Microsoft Research that enables the creation of multi-agent applications where Large Language Models (LLMs) converse, collaborate, and solve complex tasks. Instead of relying on a single prompt-response cycle, AutoGen lets you define multiple agents — each with a specific role, persona, and skill set — that exchange messages until a task is complete.
A documentation generator built on AutoGen typically involves several agents working together:
- Code Reader Agent — parses source files and extracts structure.
- Analyzer Agent — understands logic, dependencies, and intent.
- Writer Agent — produces Markdown or reStructuredText documentation.
- Reviewer Agent — validates accuracy, tone, and completeness.
Why Use AutoGen for Documentation Generation?
Traditional documentation tools like JSDoc, Sphinx, or Javadoc extract comments and signatures mechanically. They produce reference docs, but they don't explain the code. AutoGen-based generators add a layer of semantic understanding:
- Context-aware explanations — agents reason about why code exists, not just what it does.
- Multi-file understanding — agents can trace function calls across modules.
- Natural language output — docs read like they were written by a human technical writer.
- Iterative refinement — the reviewer agent catches errors before output is finalized.
- Customizable tone — you can tune agents for beginner-friendly, API-reference, or internal-engineering audiences.
This matters because poor documentation slows onboarding, increases support burden, and hides technical debt. Automating high-quality docs reduces these costs dramatically.
Prerequisites and Setup
Before building the generator, ensure you have the following:
- Python 3.9 or higher
- An OpenAI API key (or Azure OpenAI endpoint)
- A codebase you want to document
Install AutoGen and supporting libraries:
pip install "pyautogen>=0.2" python-dotenv pathlib
Create a .env file in your project root:
OPENAI_API_KEY=sk-your-key-here
MODEL_NAME=gpt-4o-mini
Project Structure
Organize your project as follows:
docgen/
├── .env
├── main.py
├── agents.py
├── file_utils.py
├── config.py
└── sample_code/
└── calculator.py
Step 1: Configuration
Start by centralizing your LLM configuration. This keeps agent definitions clean and makes it easy to swap models later.
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
LLM_CONFIG = {
"config_list": [
{
"model": os.getenv("MODEL_NAME", "gpt-4o-mini"),
"api_key": os.getenv("OPENAI_API_KEY"),
}
],
"temperature": 0.3,
"timeout": 120,
}
A low temperature (0.3) is ideal for documentation tasks because you want factual, deterministic output rather than creative variation.
Step 2: File Utilities
You need helpers to read source files and write the generated documentation. Keep these separate from agent logic for testability.
# file_utils.py
from pathlib import Path
SUPPORTED_EXTENSIONS = {".py", ".js", ".ts", ".java", ".go", ".rs"}
def collect_source_files(root_dir: str) -> list[Path]:
"""Walk a directory and return supported source files."""
root = Path(root_dir)
files = []
for path in root.rglob("*"):
if path.is_file() and path.suffix in SUPPORTED_EXTENSIONS:
files.append(path)
return files
def read_file(path: Path) -> str:
"""Read file contents with UTF-8 encoding."""
return path.read_text(encoding="utf-8")
def write_docs(output_dir: str, filename: str, content: str) -> Path:
"""Write generated documentation to disk."""
out = Path(output_dir)
out.mkdir(parents=True, exist_ok=True)
target = out / f"{filename}.md"
target.write_text(content, encoding="utf-8")
return target
Step 3: Defining the Agents
This is the heart of the system. You'll define four agents and wire them into a group chat. Each agent has a system prompt that constrains its behavior.
# agents.py
import autogen
from config import LLM_CONFIG
def build_agents():
# Agent 1: Code Reader
code_reader = autogen.AssistantAgent(
name="CodeReader",
llm_config=LLM_CONFIG,
system_message=(
"You are a Code Reader. Given a source file, you extract "
"its structure: classes, functions, signatures, imports, "
"and notable logic. Output a concise structured summary. "
"Do NOT write documentation yet."
),
)
# Agent 2: Analyzer
analyzer = autogen.AssistantAgent(
name="Analyzer",
llm_config=LLM_CONFIG,
system_message=(
"You are a Code Analyzer. You receive a structured summary "
"and explain the purpose, behavior, edge cases, and "
"dependencies of the code. Highlight anything a maintainer "
"should know. Be precise and technical."
),
)
# Agent 3: Writer
writer = autogen.AssistantAgent(
name="Writer",
llm_config=LLM_CONFIG,
system_message=(
"You are a Technical Writer. You produce clean Markdown "
"documentation based on the analysis. Include a title, "
"overview, function/class reference tables, usage examples, "
"and notes. Use fenced code blocks for examples. "
"Output ONLY the Markdown document."
),
)
# Agent 4: Reviewer
reviewer = autogen.AssistantAgent(
name="Reviewer",
llm_config=LLM_CONFIG,
system_message=(
"You are a Documentation Reviewer. You check the Markdown "
"for accuracy, clarity, missing sections, and tone. "
"If issues exist, list them concisely. If the doc is good, "
"respond with exactly: APPROVED"
),
)
return code_reader, analyzer, writer, reviewer
Step 4: Orchestrating the Group Chat
AutoGen's GroupChat lets multiple agents collaborate in a shared conversation. You control the flow with a GroupChatManager and a custom speaker selection function.
# main.py (part 1)
import autogen
from config import LLM_CONFIG
from agents import build_agents
from file_utils import collect_source_files, read_file, write_docs
MAX_ROUNDS = 12
def make_group_chat():
reader, analyzer, writer, reviewer = build_agents()
def speaker_selection(last_speaker, groupchat):
messages = groupchat.messages
if not messages:
return reader
last_msg = messages[-1]["content"].strip().upper()
if last_speaker is reader:
return analyzer
if last_speaker is analyzer:
return writer
if last_speaker is writer:
return reviewer
if last_speaker is reviewer:
if "APPROVED" in last_msg:
return None # end conversation
return writer # revise
return reader
group_chat = autogen.GroupChat(
agents=[reader, analyzer, writer, reviewer],
messages=[],
max_round=MAX_ROUNDS,
speaker_selection_method=speaker_selection,
)
manager = autogen.GroupChatManager(
groupchat=group_chat,
llm_config=LLM_CONFIG,
)
return manager, writer
The speaker_selection function enforces a pipeline: read → analyze → write → review. If the reviewer rejects, the writer revises. If approved, the conversation terminates.
Step 5: Running the Generator
Now wire everything together. You'll use a UserProxyAgent to kick off the conversation with the file contents, then extract the final Markdown from the writer's last message.
# main.py (part 2)
import re
def extract_markdown(messages, writer_name="Writer"):
"""Pull the last Markdown block produced by the Writer agent."""
for msg in reversed(messages):
if msg.get("name") == writer_name:
content = msg["content"]
# If wrapped in code fences, strip them
match = re.search(r"(?:markdown)?\n(.*?)", content, re.DOTALL)
return match.group(1).strip() if match else content.strip()
return ""
def generate_for_file(file_path, manager, user_proxy):
source = read_file(file_path)
prompt = (
f"Please document the following source file ({file_path.name}).\n\n"
f"--- BEGIN SOURCE ---\n{source}\n--- END SOURCE ---"
)
user_proxy.initiate_chat(manager, message=prompt, clear_history=True)
return extract_markdown(manager.groupchat.messages)
def main():
source_dir = "sample_code"
output_dir = "docs_output"
manager, _ = make_group_chat()
user_proxy = autogen.UserProxyAgent(
name="User",
human_input_mode="NEVER",
max_consecutive_auto_reply=0,
code_execution_config=False,
)
files = collect_source_files(source_dir)
print(f"Found {len(files)} source file(s).")
for f in files:
print(f"Generating docs for: {f.name}")
markdown = generate_for_file(f, manager, user_proxy)
if markdown:
path = write_docs(output_dir, f.stem, markdown)
print(f" -> wrote {path}")
else:
print(f" -> no documentation produced for {f.name}")
if __name__ == "__main__":
main()
Step 6: Sample Input and Expected Output
Create a sample file to test the generator:
# sample_code/calculator.py
class Calculator:
def __init__(self):
self.history = []
def add(self, a, b):
result = a + b
self.history.append(("add", a, b, result))
return result
def divide(self, a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
result = a / b
self.history.append(("divide", a, b, result))
return result
def clear_history(self):
self.history.clear()
When you run python main.py, the agents collaborate and produce a Markdown file at docs_output/calculator.md resembling:
# Calculator
## Overview
The `Calculator` class provides basic arithmetic operations while
maintaining an internal history of every computation.
## Class Reference
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| `__init__` | none | void | Initializes an empty history list |
| `add` | `a: int/float`, `b: int/float` | `int/float` | Returns the sum and logs the operation |
| `divide` | `a: int/float`, `b: int/float` | `int/float` | Returns the quotient; raises `ValueError` if `b` is zero |
| `clear_history` | none | void | Empties the history list |
## Usage
python
calc = Calculator()
print(calc.add(2, 3)) # 5
print(calc.divide(10, 2)) # 5.0
calc.clear_history()
## Notes
- `divide` raises `ValueError` when the divisor is zero.
- History grows unbounded; call `clear_history` periodically in long-running processes.
Best Practices
1. Keep Agents Focused
Each agent should have a single responsibility. Resist the temptation to make one agent both analyze and write. Separation of concerns improves output quality and makes debugging easier.
2. Constrain Output Formats
Use explicit instructions like "Output ONLY the Markdown document" or "Respond with exactly: APPROVED". This prevents agents from adding conversational filler that breaks downstream parsing.
3. Limit Conversation Rounds
Set a reasonable max_round value (10–15). Without a cap, a reviewer-writer loop can spiral indefinitely if the reviewer keeps finding minor issues.
4. Cache Expensive Calls
LLM calls are costly. Cache the structured summary and analysis per file hash so re-running the generator on unchanged files skips redundant work. A simple approach:
import hashlib, json, os
def file_hash(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def load_cache(cache_path):
if os.path.exists(cache_path):
return json.loads(Path(cache_path).read_text())
return {}
def save_cache(cache, cache_path):
Path(cache_path).write_text(json.dumps(cache, indent=2))
5. Validate Generated Docs
Don't blindly trust LLM output. Add a post-processing step that checks for required sections (Overview, Reference, Usage) and verifies code examples are syntactically valid using ast.parse for Python files.
6. Handle Large Files
LLMs have context limits. For files exceeding ~8,000 tokens, split them by class or function before sending to the CodeReader agent. Merge the resulting docs afterward.
7. Use Cheap Models for Review
The reviewer's job is pattern matching, not deep reasoning. You can configure it with a cheaper model like gpt-4o-mini while reserving gpt-4o for the analyzer and writer.
8. Version Your Prompts
Treat system prompts as code. Store them in separate files or a YAML config so you can iterate on wording without touching application logic.
Extending the Generator
Once the core pipeline works, consider these enhancements:
- Cross-file linking — after generating per-file docs, run a fifth agent that inserts links between related functions and classes.
- Diagram generation — add an agent that emits Mermaid sequence or class diagrams from the analyzer's output.
- Changelog integration — feed git diffs to a specialized agent that updates only the sections affected by recent commits.
- Multi-language support — extend
SUPPORTED_EXTENSIONSand add language-specific reader prompts.
Conclusion
Building a documentation generator with AutoGen transforms a tedious manual chore into an automated, intelligent workflow. By decomposing the task into focused agents — reader, analyzer, writer, and reviewer — you get documentation that explains intent, not just signatures. The architecture is modular, so you can swap models, add agents, or integrate with existing CI pipelines without rewriting the core. Start with the pipeline in this guide, apply the best practices around caching, validation, and round limits, and you'll have a documentation system that keeps your docs as fresh as your code.