By Sagar Shankaran, Founder of CallSphere
Build an autonomous research agent that searches the web, reads documents, synthesizes findings, and produces structured reports. Covers architecture, tool integration, source verification, and iterative deepening strategies.
Key takeaways
A research agent autonomously investigates a topic by searching for information, reading sources, evaluating credibility, and synthesizing findings into a coherent report. Unlike a simple search-and-summarize pipeline, a research agent iterates: it reads initial sources, identifies gaps or follow-up questions, searches again, and progressively deepens its understanding.
This is one of the most practical and immediately valuable applications of the Claude API. Analysts, journalists, product managers, and investors spend hours manually doing what a well-built research agent can accomplish in minutes.
User Query
|
v
[Query Planner] -- Decompose into sub-questions
|
v
[Search Agent] -- Find relevant sources (loop)
|
v
[Reader Agent] -- Extract key information from each source
|
v
[Evaluator Agent] -- Assess source credibility and consistency
|
v
[Synthesizer Agent] -- Produce final report with citations
The first step transforms a broad query into specific, searchable sub-questions:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
USER(["User message"])
LOOP{"messages.create<br/>agent loop"}
THINK["Extended thinking<br/>optional"]
TOOL{"stop_reason<br/>tool_use?"}
EXEC["Execute tool<br/>append tool_result"]
DONE(["stop_reason<br/>end_turn"])
USER --> LOOP --> THINK --> TOOL
TOOL -->|Yes| EXEC --> LOOP
TOOL -->|No| DONE
style LOOP fill:#4f46e5,stroke:#4338ca,color:#fff
style THINK fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style DONE fill:#059669,stroke:#047857,color:#fff
from anthropic import Anthropic
client = Anthropic()
PLANNER_PROMPT = """You are a research planning agent. Given a research query:
1. Identify the key aspects that need investigation
2. Generate 3-5 specific sub-questions that together would provide a comprehensive answer
3. For each sub-question, suggest search queries that would find relevant information
4. Prioritize sub-questions by importance
Return JSON with this structure:
{
"main_topic": "...",
"sub_questions": [
{
"question": "...",
"search_queries": ["...", "..."],
"priority": 1
}
]
}"""
def plan_research(query: str) -> dict:
response = client.messages.create(
model="claude-sonnet-4-5-20250514",
max_tokens=2048,
system=PLANNER_PROMPT,
messages=[{"role": "user", "content": query}],
)
return parse_json(response.content[0].text)
Connect the agent to a search API. Here we use a generic search function that you would implement with your preferred search provider (Brave, Google, Bing, or Tavily):
import httpx
from dataclasses import dataclass
@dataclass
class SearchResult:
title: str
url: str
snippet: str
source: str
async def search_web(query: str, num_results: int = 5) -> list[SearchResult]:
"""Search the web using your preferred search API."""
# Example with a generic search API
async with httpx.AsyncClient() as http:
response = await http.get(
"https://api.search-provider.com/search",
params={"q": query, "count": num_results},
headers={"Authorization": f"Bearer {SEARCH_API_KEY}"},
)
data = response.json()
return [
SearchResult(
title=r["title"],
url=r["url"],
snippet=r["snippet"],
source=extract_domain(r["url"]),
)
for r in data["results"]
]
For each search result, fetch the page content and extract the relevant information:
from bs4 import BeautifulSoup
import httpx
async def fetch_and_extract(url: str) -> str:
"""Fetch a URL and extract clean text content."""
try:
async with httpx.AsyncClient(follow_redirects=True, timeout=10.0) as http:
response = await http.get(url)
response.raise_for_status()
except (httpx.HTTPError, httpx.TimeoutException):
return ""
soup = BeautifulSoup(response.text, "html.parser")
# Remove scripts, styles, nav, footer
for tag in soup(["script", "style", "nav", "footer", "header", "aside"]):
tag.decompose()
text = soup.get_text(separator="\n", strip=True)
# Truncate to avoid exceeding context limits
max_chars = 10_000
if len(text) > max_chars:
text = text[:max_chars] + "\n[Content truncated]"
return text
READER_PROMPT = """You are a research reader agent. Given a source document and a
specific question, extract all relevant information that helps answer the question.
Rules:
- Only extract information that is directly relevant
- Note specific facts, statistics, dates, and quotes
- Identify the author and publication if available
- Rate the source credibility (1-5): 1=unverified blog, 5=peer-reviewed/official
- Flag any claims that seem unsupported or contradictory
Return JSON:
{
"relevant_facts": ["...", "..."],
"key_quotes": ["...", "..."],
"credibility_score": 4,
"credibility_notes": "...",
"gaps": ["Questions this source does not answer"]
}"""
async def read_source(url: str, question: str) -> dict:
content = await fetch_and_extract(url)
if not content:
return {"relevant_facts": [], "credibility_score": 0}
response = client.messages.create(
model="claude-haiku-4-5-20250514", # Haiku is sufficient for extraction
max_tokens=1024,
system=READER_PROMPT,
messages=[{
"role": "user",
"content": f"Question: {question}\n\nSource URL: {url}\n\nContent:\n{content}"
}],
)
return parse_json(response.content[0].text)
The key differentiator of a research agent versus a simple search pipeline is iteration. After the first round of research, the agent identifies gaps and searches again:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
async def research_loop(
query: str,
max_iterations: int = 3,
min_sources: int = 5,
) -> dict:
"""Iterative research loop that deepens understanding."""
plan = plan_research(query)
all_findings = []
searched_urls = set()
iteration = 0
for sub_q in plan["sub_questions"]:
for search_query in sub_q["search_queries"]:
results = await search_web(search_query)
for result in results:
if result.url in searched_urls:
continue
searched_urls.add(result.url)
findings = await read_source(result.url, sub_q["question"])
findings["url"] = result.url
findings["title"] = result.title
findings["question"] = sub_q["question"]
all_findings.append(findings)
iteration += 1
if iteration >= max_iterations:
break
# Check for gaps and do follow-up searches
gaps = identify_gaps(all_findings, plan)
if gaps and iteration < max_iterations:
for gap in gaps[:3]: # Limit follow-up searches
follow_up_results = await search_web(gap)
for result in follow_up_results:
if result.url not in searched_urls:
searched_urls.add(result.url)
findings = await read_source(result.url, gap)
findings["url"] = result.url
findings["title"] = result.title
findings["question"] = gap
all_findings.append(findings)
return {
"plan": plan,
"findings": all_findings,
"sources_consulted": len(searched_urls),
"iterations": iteration,
}
def identify_gaps(findings: list[dict], plan: dict) -> list[str]:
"""Identify unanswered questions from the research so far."""
all_gaps = []
for finding in findings:
all_gaps.extend(finding.get("gaps", []))
return list(set(all_gaps))[:5] # Deduplicate and limit
The final step synthesizes all findings into a coherent, cited report:
SYNTHESIZER_PROMPT = """You are a research synthesis agent. Given a collection of
findings from multiple sources, produce a comprehensive research report.
Report requirements:
1. Start with an executive summary (2-3 sentences)
2. Organize findings by theme, not by source
3. Cite sources using [Source N] notation
4. Highlight areas of consensus and disagreement between sources
5. Note limitations and areas where more research is needed
6. Include a source bibliography at the end
Quality standards:
- Every factual claim must have a citation
- Clearly distinguish between well-established facts and uncertain claims
- Present multiple perspectives when sources disagree
- Use precise language and avoid hedging unless genuinely uncertain"""
async def synthesize_report(research_data: dict) -> str:
findings_text = ""
for i, finding in enumerate(research_data["findings"]):
findings_text += f"""
Source [{i+1}]: {finding.get('title', 'Unknown')}
URL: {finding['url']}
Credibility: {finding.get('credibility_score', 'N/A')}/5
Question investigated: {finding['question']}
Key facts: {json.dumps(finding.get('relevant_facts', []))}
Key quotes: {json.dumps(finding.get('key_quotes', []))}
---"""
response = client.messages.create(
model="claude-sonnet-4-5-20250514",
max_tokens=8192,
system=SYNTHESIZER_PROMPT,
messages=[{
"role": "user",
"content": f"""Research topic: {research_data['plan']['main_topic']}
Sources consulted: {research_data['sources_consulted']}
Findings:
{findings_text}
Produce a comprehensive research report."""
}],
)
return response.content[0].text
async def run_research(query: str) -> str:
"""Run the complete research pipeline."""
print(f"Researching: {query}")
# Phase 1: Plan
print("Planning research...")
research_data = await research_loop(query, max_iterations=3, min_sources=5)
print(f"Consulted {research_data['sources_consulted']} sources")
# Phase 2: Synthesize
print("Synthesizing report...")
report = await synthesize_report(research_data)
return report
# Usage
import asyncio
report = asyncio.run(run_research(
"What are the current best practices for deploying LLM applications in production?"
))
print(report)
For a typical research task consulting 10 sources:
| Component | Model | Calls | Avg Tokens | Cost |
|---|---|---|---|---|
| Query planner | Sonnet | 1 | 1,500 | $0.03 |
| Source readers | Haiku | 10 | 3,000 each | $0.04 |
| Gap analysis | Sonnet | 1 | 2,000 | $0.04 |
| Report synthesis | Sonnet | 1 | 8,000 | $0.15 |
| Total | 13 | ~43,000 | $0.26 |
A comprehensive research report for under $0.30 -- compared to 2-4 hours of manual research at analyst rates.

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
A founder's guide to the personal AI assistant market: best AI assistant apps, business-grade options, and how CallSphere's voice agent fits in.
A founder's guide to free AI agents, low-code AI agent builders, and how to know when you should pay for a real platform like CallSphere.
Graphiti is the open-source temporal knowledge graph for AI agents in 2026. Learn how bi-temporal memory beats vector RAG for voice agents and long-running LLMs.
Chatbot app vs ChatGPT in 2026: a founder's clear take on the difference, when to use which, and how a real AI chatbot app development works.
How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.
May 2026's biggest agent-architecture shift: planning, tool selection, and self-correction move inside the model. Framework code shrinks. Here is what changes.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco