karthik.dev
Back to blogMulti-Agent

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:

  1. Understand what information you're looking for (not just the literal question)
  2. Search for it in the right places
  3. Evaluate what you found for relevance and credibility
  4. 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.