Build a Multi-Agent Research Team (Planner, Searcher, Writer)
β‘ Quick Answer
Build a working 3-agent research pipeline with a Planner, Searcher, and Writer using LangChain and AutoGen β complete code with role definitions and output format.
Get more content like this on Telegram!
Daily AI tips, notes & resources β free
Advertisement
Research is the task that convinced me multi-agent systems actually earn their complexity. I tried to do deep research with a single agent for months. The problem was always the same: either the context window filled up before I had enough information, or the agent would summarize prematurely because it was trying to do everything at once.
The fix was separation of concerns. A Planner that thinks about what to find. A Searcher that's purely focused on finding it. A Writer that does nothing but synthesize. Three agents, three roles, dramatically better results.
This tutorial builds a complete three-agent research pipeline from scratch. You'll get full working code, role definitions, and the output format I've landed on after several iterations.
If you want to understand the theory before the code, multi-agent systems explained and AI agents explained cover the fundamentals.
System Architecture
Each agent has a clean interface: defined inputs, defined outputs, and no awareness of agents outside its immediate upstream/downstream. This makes the system modular β you can swap out any agent independently.
Setup
pip install langchain langchain-openai langchain-community tavily-python python-dotenv autogen-agentchat
import os
import json
import asyncio
from typing import Dict, List, Any, Optional
from dataclasses import dataclass, field
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema import HumanMessage, SystemMessage
from langchain_community.tools.tavily_search import TavilySearchResults
load_dotenv()
# Model configs β different models for different roles
PLANNER_MODEL = ChatOpenAI(model="gpt-4o", temperature=0.1)
SEARCHER_MODEL = ChatOpenAI(model="gpt-4o-mini", temperature=0.0)
WRITER_MODEL = ChatOpenAI(model="gpt-4o", temperature=0.3)
# Search tool
search_tool = TavilySearchResults(
max_results=5,
api_key=os.environ["TAVILY_API_KEY"]
)
The model choice here is intentional. The Planner needs the best reasoning (GPT-4o at low temperature). The Searcher needs consistency and speed, not creativity (GPT-4o-mini). The Writer needs quality and some stylistic flexibility (GPT-4o, slightly warmer temperature).
Shared State
The agents communicate through a shared state dictionary. Simple, explicit, easy to debug.
@dataclass
class ResearchState:
topic: str
research_plan: Optional[Dict] = None
search_results: Dict[str, List[Dict]] = field(default_factory=dict)
final_report: Optional[str] = None
errors: List[str] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
def add_search_result(self, sub_question: str, results: List[Dict]):
if sub_question not in self.search_results:
self.search_results[sub_question] = []
self.search_results[sub_question].extend(results)
def to_summary(self) -> str:
"""Compact summary for passing to writer."""
summary = f"Research Topic: {self.topic}\n\n"
for question, results in self.search_results.items():
summary += f"## {question}\n"
for r in results[:3]: # Top 3 per question
summary += f"- **Source:** {r.get('url', 'Unknown')}\n"
summary += f" {r.get('content', '')[:300]}...\n\n"
return summary
The Planner Agent
The Planner's only job is to think about the research question and output a structured plan. It doesn't search for anything.
class PlannerAgent:
def __init__(self, llm: ChatOpenAI):
self.llm = llm
self.system_prompt = """You are a research planning specialist.
Your job is to analyze a research topic and create a structured research plan.
Output a JSON object with this exact structure:
{
"topic": "the research topic",
"objective": "what we want to learn",
"sub_questions": [
{
"id": 1,
"question": "specific sub-question",
"priority": "high|medium|low",
"search_queries": ["query1", "query2"]
}
],
"output_format": "description of expected report structure"
}
Rules:
- Generate 4-6 sub-questions that together cover the full topic
- Each sub-question should have 2-3 specific search queries
- Questions should move from foundational to specific
- Do NOT answer the questions β just plan how to research them
"""
def plan(self, topic: str) -> Dict:
messages = [
SystemMessage(content=self.system_prompt),
HumanMessage(content=f"Create a research plan for: {topic}")
]
response = self.llm.invoke(messages)
# Extract JSON from response
content = response.content
# Handle markdown code blocks if present
if "```json" in content:
content = content.split("```json")[1].split("```")[0].strip()
elif "```" in content:
content = content.split("```")[1].split("```")[0].strip()
plan = json.loads(content)
print(f"[Planner] Generated plan with {len(plan['sub_questions'])} sub-questions")
return plan
The Searcher Agent
The Searcher takes the plan and executes searches for each sub-question. It handles rate limiting, deduplication, and annotates results with source metadata.
class SearcherAgent:
def __init__(self, llm: ChatOpenAI, search_tool: TavilySearchResults):
self.llm = llm
self.search_tool = search_tool
self.system_prompt = """You are a research search specialist.
You receive search results and must:
1. Extract the most relevant information for the given question
2. Note the source URL and date if available
3. Flag any conflicting information between sources
4. Return a structured summary
Output format:
{
"question": "the sub-question",
"key_findings": ["finding 1", "finding 2"],
"sources": [{"url": "...", "relevance": "high|medium"}],
"conflicts": ["any conflicting claims"],
"confidence": "high|medium|low"
}"""
def _search_with_query(self, query: str) -> List[Dict]:
"""Execute a single search query."""
try:
results = self.search_tool.invoke(query)
return results if isinstance(results, list) else []
except Exception as e:
print(f"[Searcher] Search failed for '{query}': {e}")
return []
def _deduplicate_results(self, results: List[Dict]) -> List[Dict]:
"""Remove duplicate URLs from search results."""
seen_urls = set()
unique = []
for r in results:
url = r.get("url", "")
if url and url not in seen_urls:
seen_urls.add(url)
unique.append(r)
return unique
def _synthesize_findings(self, question: str, raw_results: List[Dict]) -> Dict:
"""Use LLM to synthesize raw search results into structured findings."""
results_text = "\n\n".join([
f"Source: {r.get('url', 'unknown')}\nContent: {r.get('content', '')[:500]}"
for r in raw_results[:5]
])
messages = [
SystemMessage(content=self.system_prompt),
HumanMessage(content=f"Question: {question}\n\nSearch Results:\n{results_text}")
]
response = self.llm.invoke(messages)
content = response.content
if "```json" in content:
content = content.split("```json")[1].split("```")[0].strip()
try:
return json.loads(content)
except json.JSONDecodeError:
return {"question": question, "key_findings": [content], "sources": [], "confidence": "low"}
def search(self, research_plan: Dict, state: ResearchState) -> ResearchState:
"""Execute searches for all sub-questions in the plan."""
for sub_q in research_plan["sub_questions"]:
question = sub_q["question"]
queries = sub_q["search_queries"]
priority = sub_q.get("priority", "medium")
print(f"[Searcher] Searching: {question} (priority: {priority})")
# Execute all queries for this sub-question
all_raw_results = []
for query in queries:
raw = self._search_with_query(query)
all_raw_results.extend(raw)
# Deduplicate
unique_results = self._deduplicate_results(all_raw_results)
# Synthesize into structured findings
findings = self._synthesize_findings(question, unique_results)
state.add_search_result(question, [findings])
print(f"[Searcher] Found {len(unique_results)} unique sources for: {question}")
return state
The Writer Agent
The Writer reads the research state and produces a structured, well-cited report.
class WriterAgent:
def __init__(self, llm: ChatOpenAI):
self.llm = llm
self.system_prompt = """You are a research writer who synthesizes information into clear,
well-structured reports.
Your reports must:
1. Start with an executive summary (2-3 sentences)
2. Have clearly labeled sections for each major finding
3. Cite sources with [Source: URL] inline
4. Acknowledge uncertainty where confidence is low
5. End with key conclusions and open questions
6. Use markdown formatting throughout
Style guidelines:
- Write for an informed, professional audience
- Use specific numbers and dates when available
- Avoid vague generalizations
- Distinguish between established facts and current estimates
"""
def write(self, topic: str, state: ResearchState, plan: Dict) -> str:
research_summary = state.to_summary()
messages = [
SystemMessage(content=self.system_prompt),
HumanMessage(content=f"""Write a comprehensive research report on: {topic}
Research Objective: {plan.get('objective', 'Not specified')}
Research Findings:
{research_summary}
Please structure the report according to these sections implied by the research plan.
Include all available sources as citations.""")
]
response = self.llm.invoke(messages)
print(f"[Writer] Report generated: {len(response.content)} characters")
return response.content
The Orchestrator
This ties everything together. It instantiates the agents, manages state, and runs the pipeline.
class ResearchOrchestrator:
def __init__(self):
self.planner = PlannerAgent(PLANNER_MODEL)
self.searcher = SearcherAgent(SEARCHER_MODEL, search_tool)
self.writer = WriterAgent(WRITER_MODEL)
def run(self, topic: str) -> ResearchState:
print(f"\n{'='*60}")
print(f"Starting research: {topic}")
print('='*60)
state = ResearchState(topic=topic)
# Step 1: Plan
print("\n[Phase 1: Planning]")
state.research_plan = self.planner.plan(topic)
# Step 2: Search
print("\n[Phase 2: Searching]")
state = self.searcher.search(state.research_plan, state)
# Step 3: Write
print("\n[Phase 3: Writing]")
state.final_report = self.writer.write(topic, state, state.research_plan)
print("\n[Research Complete]")
return state
def save_report(self, state: ResearchState, output_path: str):
with open(output_path, "w", encoding="utf-8") as f:
f.write(f"# Research Report: {state.topic}\n\n")
f.write(state.final_report)
print(f"Report saved to: {output_path}")
# Usage
if __name__ == "__main__":
orchestrator = ResearchOrchestrator()
result = orchestrator.run(
"The current state of open-source LLM fine-tuning methods in 2026"
)
orchestrator.save_report(result, "research_report.md")
print("\n--- FINAL REPORT PREVIEW ---")
print(result.final_report[:500])
AutoGen Version of the Same Pipeline
If you prefer AutoGen over LangChain, here's the equivalent system:
import autogen
llm_config = {"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}
planner = autogen.AssistantAgent(
name="Planner",
system_message="""Break the research topic into 4-6 specific sub-questions.
For each question, suggest 2-3 search queries.
Output as a numbered list. End with: 'Plan complete.'""",
llm_config=llm_config
)
searcher = autogen.AssistantAgent(
name="Searcher",
system_message="""Given research questions, find relevant information.
For each question, provide a summary of what's known with source URLs.
Be specific. End with: 'Search complete.'""",
llm_config=llm_config
)
writer = autogen.AssistantAgent(
name="Writer",
system_message="""Synthesize research findings into a structured report.
Use headers, bullet points, and inline citations.
End with: 'REPORT COMPLETE'""",
llm_config=llm_config
)
user_proxy = autogen.UserProxyAgent(
name="UserProxy",
human_input_mode="NEVER",
is_termination_msg=lambda m: "REPORT COMPLETE" in m.get("content", ""),
code_execution_config=False
)
groupchat = autogen.GroupChat(
agents=[user_proxy, planner, searcher, writer],
messages=[],
max_round=12,
speaker_selection_method="auto"
)
manager = autogen.GroupChatManager(
groupchat=groupchat,
llm_config=llm_config,
system_message="""Coordinate: Planner goes first, then Searcher, then Writer.
Each agent speaks once per cycle unless revision is needed."""
)
user_proxy.initiate_chat(
manager,
message="Research: How are companies using AI agents in production in 2026?"
)
For deeper AutoGen group chat patterns, autogen multi-agent group chat tutorial has the full setup guide.
Output Format
The final report follows this structure:
# Research Report: [Topic]
## Executive Summary
[2-3 sentence overview of key findings]
## Background
[Foundational context from Planner's first sub-questions]
## Current State
[Main findings with inline citations [Source: URL]]
## Key Developments
[Specific findings, statistics, examples]
## Challenges and Open Questions
[What we don't know, conflicting information]
## Conclusions
[Synthesized takeaways]
## Sources
[Full list of URLs used]
Performance Notes
A typical 6-sub-question research run with this pipeline takes approximately:
- Planning: 5-10 seconds
- Searching: 30-60 seconds (search API latency Γ 6 sub-questions)
- Writing: 15-25 seconds
- Total: 50-95 seconds
Compare that to a human researcher who might take 4-8 hours for equivalent coverage. The quality won't match a professional researcher, but for first-pass research, market overviews, or competitive analysis, this pipeline is genuinely useful.
For an alternative approach using LangChain's RAG pipeline, the LangChain RAG pipeline article shows how to add vector store retrieval for document-heavy research tasks.
The AI research agent build article dives into more advanced patterns including self-evaluation and iterative search refinement.
Conclusion
The Planner-Searcher-Writer pattern is one of the most practical multi-agent architectures for real work. The separation of concerns is clean, the communication via shared state is explicit and debuggable, and the output quality improvement over single-agent research is measurable.
Start with the LangChain version if you want more control over each agent's logic. Start with the AutoGen version if you want to experiment with the group chat dynamics. Both produce good results once you've calibrated the system prompts.
The next step is adding a fact-checker agent that verifies claims against sources before the Writer synthesizes β a pattern covered in multi-agent consensus voting negotiation.
Frequently Asked Questions
What does each agent do in the research pipeline? The Planner breaks the research topic into specific sub-questions. The Searcher finds information for each sub-question using search tools. The Writer synthesizes findings into a structured report. Each role has a distinct system prompt and tool set.
How do agents pass information between each other in this pipeline? The Planner outputs a structured research plan (JSON). The Searcher reads that plan, executes searches, and writes findings to a shared state dict. The Writer reads the state dict and produces the final report. State is passed via a shared dictionary object.
Can I swap out the LLM for different agents? Yes. Each agent is independently configured with its own llm_config. You can use GPT-4o for the Planner (reasoning-heavy), GPT-4o-mini for the Searcher (structured, cheaper), and GPT-4o for the Writer (quality-sensitive). Mix and match for cost-performance optimization.
Advertisement
π¬ DiscussionPowered by GitHub Discussions
Frequently Asked Questions

AiTechWorlds Team
β Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Not sure yet? Ask AI about this article
Get an instant, unbiased AI summary of βBuild a Multi-Agent Research Team (Planner, Searcher, Writer)β.
Advertisement
Related Articles
How to Build a Multi-Agent System with AutoGen (Group Chat)
Step-by-step AutoGen group chat tutorial: build a researcher, coder, and critic agent system with proper termination logic and real working code.
CrewAI Tutorial: Role-Based Multi-Agent Collaboration (2026)
Build a full CrewAI system with Writer, Researcher, and Editor agents. Complete Python code with Agent, Task, and Crew setup, plus honest comparison with AutoGen.
5 Multi-Agent Architecture Patterns (Centralized, Hierarchical, Peer)
Centralized, hierarchical, peer-to-peer β each MAS architecture trades off complexity for scalability. Here's how to pick the right pattern for your project.
7 Communication Protocols in Multi-Agent Systems (Message Passing, Blackboard)
From message passing to publish-subscribe and contract net β here are the 7 communication protocols used in multi-agent systems, with Python examples for each.