Multi-Agent Research Assistant: Planner, Search, Synthesis, and Writer
2026-06-20 · 5 min read
Why Four Agents?
Deep research on any topic requires more than one LLM call. You need to:
- Understand what information you're looking for (not just the literal question)
- Search for it in the right places
- Evaluate what you found for relevance and credibility
- Synthesize it into a coherent, cited narrative
Each step requires different cognitive work. A single LLM call handles none of them well because it has no real-time web access and no mechanism for iterative refinement.
The four-agent pipeline assigns one specialist to each step.
The Research Context Object
Every agent in the pipeline reads from and writes to a shared ResearchContext:
// src/types/research.ts
interface ResearchContext {
originalQuery: string;
subQueries: string[]; // Planner output
searchResults: SearchResult[]; // Search Agent output
extractedClaims: Claim[]; // Synthesis Agent output
report: Report | null; // Writer output
metadata: {
startedAt: Date;
completedSteps: AgentStep[];
totalSources: number;
totalTokensUsed: number;
};
}
interface Claim {
statement: string;
evidence: string;
sourceUrl: string;
sourceDomain: string;
confidence: "high" | "medium" | "low";
}
Typed handoffs are the key to preventing context drift. Each agent validates the context it receives before processing.
Agent 1: The Planner
// src/agents/planner.ts
export class PlannerAgent {
async plan(query: string): Promise<string[]> {
const prompt = `
You are a research planner. Break this research question into 3-5 targeted sub-queries.
Each sub-query should target a specific aspect of the main question.
Main question: ${query}
Good sub-queries are:
- Specific enough to return focused results
- Collectively comprehensive (together they cover the main question)
- Searchable (phrased as a real web search query)
Return JSON: { subQueries: string[] }
`;
const response = await this.openai.chat.completions.create({
model: "gpt-4o-mini", // small model for decomposition — it's a structured task
messages: [{ role: "user", content: prompt }],
response_format: { type: "json_object" },
});
const { subQueries } = JSON.parse(response.choices[0].message.content!);
return subQueries;
}
}
The Planner runs on gpt-4o-mini — decomposition is a simple structured task that doesn't require the full power of GPT-4o.
Agent 2: The Search Agent
// src/agents/search.ts
export class SearchAgent {
async search(subQueries: string[]): Promise<SearchResult[]> {
const results = await Promise.allSettled(
subQueries.map(query => this.tavilySearch(query))
);
return results
.filter((r): r is PromiseFulfilledResult<SearchResult[]> => r.status === "fulfilled")
.flatMap(r => r.value)
.filter(r => this.isCredibleSource(r.url));
}
private async tavilySearch(query: string): Promise<SearchResult[]> {
const response = await fetch("https://api.tavily.com/search", {
method: "POST",
headers: {
"Content-Type": "application/json",
"api-key": process.env.TAVILY_API_KEY!,
},
body: JSON.stringify({
query,
search_depth: "advanced",
max_results: 5,
include_raw_content: false,
}),
});
const data = await response.json();
return data.results.map(r => ({
url: r.url,
title: r.title,
content: r.content,
score: r.score,
}));
}
private isCredibleSource(url: string): boolean {
// Filter out low-credibility sources
const blocklist = ["reddit.com", "quora.com", "answers.yahoo.com"];
return !blocklist.some(domain => url.includes(domain));
}
}
All sub-queries run in parallel with Promise.allSettled() — failed searches don't block the others.
Agent 3: The Synthesis Agent
// src/agents/synthesis.ts
export class SynthesisAgent {
async synthesize(
query: string,
searchResults: SearchResult[],
): Promise<Claim[]> {
const prompt = `
You are a research analyst. Extract key claims and supporting evidence from these search results.
Original research question: ${query}
Search Results:
${searchResults.map((r, i) => `[${i+1}] ${r.url}\n${r.content}`).join("\n\n")}
For each key finding:
- State the claim clearly
- Quote or paraphrase the specific evidence
- Note the source URL
- Rate confidence (high/medium/low) based on source quality and corroboration
Only include claims directly relevant to the research question.
If a claim appears in multiple sources, note the corroboration.
Return JSON: { claims: [{ statement, evidence, sourceUrl, confidence }] }
`;
const response = await this.openai.chat.completions.create({
model: "gpt-4o", // full model for extraction — quality matters here
messages: [{ role: "user", content: prompt }],
response_format: { type: "json_object" },
});
const { claims } = JSON.parse(response.choices[0].message.content!);
return claims;
}
}
Agent 4: The Writer
// src/agents/writer.ts
export class WriterAgent {
async write(query: string, claims: Claim[]): Promise<Report> {
const prompt = `
You are a research report writer. Write a well-structured research report.
Research Question: ${query}
Verified Claims (with sources):
${claims.map((c, i) => `[${i+1}] ${c.statement}\nEvidence: ${c.evidence}\nSource: ${c.sourceUrl}`).join("\n\n")}
Write a report with:
- Executive Summary (2-3 sentences)
- Key Findings (organized thematically, each citing sources like [1], [2])
- Conclusion
- References (numbered list)
Use only the provided claims and evidence. Do not add information not in the claims.
Every factual statement must have a citation.
Return JSON: { summary, sections: [{ title, content }], references: [{ number, url }] }
`;
const response = await this.openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: prompt }],
response_format: { type: "json_object" },
});
return JSON.parse(response.choices[0].message.content!);
}
}
Cost Optimization: Right Model for Each Agent
| Agent | Model | Reason |
|---|---|---|
| Planner | gpt-4o-mini | Simple structured decomposition |
| Search | N/A (Tavily API) | Real-time web, not LLM |
| Synthesis | gpt-4o | Critical extraction step — quality matters |
| Writer | gpt-4o | Final output quality matters |
Running the Planner on gpt-4o-mini cuts its cost by ~6x vs gpt-4o with no quality loss.
Results
A research question that takes a human researcher 30-60 minutes produces a citation-backed report in under 2 minutes, covering 4-5 sub-queries with 15-25 sources evaluated and the top 8-10 claims extracted and synthesized.