Lesson 19 of 46 ~25 min
Course progress
0%

Building Research Pipelines

Build BrowseComp-level research pipelines — multi-source information retrieval, synthesis, fact-checking, and citation generation with Opus 4.6.

Opus 4.6 scored #1 on BrowseComp — the benchmark for complex, multi-source research tasks. This lesson teaches you to build research pipelines that exploit that capability: gathering information from multiple sources, synthesizing findings, verifying facts, and generating properly cited outputs.

Research Pipeline Architecture

┌──────────┐    ┌──────────────┐    ┌───────────┐    ┌──────────┐
│  Query   │───▶│  Source       │───▶│ Synthesis │───▶│  Output  │
│  Planning│    │  Retrieval    │    │ & Verify  │    │  + Cites │
└──────────┘    └──────────────┘    └───────────┘    └──────────┘
     │               │                    │                │
     ▼               ▼                    ▼                ▼
  Decompose     Search APIs          Cross-ref        Markdown/
  question      Documents            Sources          PDF/JSON
  into sub-     Web pages            Flag gaps
  queries       Databases

Step 1: Query Decomposition

Complex research questions need to be broken into searchable sub-queries:

from anthropic import Anthropic
import json

client = Anthropic()

def decompose_research_question(question: str) -> list[dict]:
    """Break a complex question into searchable sub-queries."""
    response = client.messages.create(
        model="claude-opus-4-6-20260205",
        max_tokens=2048,
        thinking={"type": "adaptive"},
        system="""You decompose complex research questions into specific,
searchable sub-queries. Output JSON array with:
[{"sub_query": "...", "purpose": "...", "source_type": "academic|web|database|expert"}]

Rules:
- Each sub-query should be independently searchable
- Cover all aspects of the original question
- Order from foundational to advanced
- Include at least one verification query""",
        messages=[{"role": "user", "content": question}]
    )

    text = next(b.text for b in response.content if b.type == "text")
    return json.loads(text)

# Example
sub_queries = decompose_research_question(
    "What are the most effective techniques for reducing hallucination "
    "in large language models, and how do they compare in terms of "
    "computational cost and accuracy improvement?"
)

Step 2: Multi-Source Retrieval

from dataclasses import dataclass

@dataclass
class Source:
    title: str
    content: str
    url: str
    source_type: str  # "academic", "web", "database"
    reliability: float  # 0.0-1.0

class SourceRetriever:
    """Retrieve information from multiple source types."""

    def __init__(self):
        self.client = Anthropic()
        self.sources: list[Source] = []

    def add_document(self, title: str, content: str,
                     url: str = "", source_type: str = "document",
                     reliability: float = 0.8):
        """Add a document source to the retriever."""
        self.sources.append(Source(
            title=title, content=content, url=url,
            source_type=source_type, reliability=reliability
        ))

    def retrieve_for_query(self, query: str,
                           max_sources: int = 10) -> list[Source]:
        """Find the most relevant sources for a query."""
        if not self.sources:
            return []

        source_descriptions = "\n".join(
            f"[{i}] {s.title} ({s.source_type}, reliability: {s.reliability})"
            for i, s in enumerate(self.sources)
        )

        response = self.client.messages.create(
            model="claude-opus-4-6-20260205",
            max_tokens=1024,
            thinking={"type": "adaptive"},
            messages=[{
                "role": "user",
                "content": f"""Given this research query: "{query}"

Which of these sources are most relevant? Return a JSON array of
indices (0-based), ordered by relevance. Maximum {max_sources} sources.

Available sources:
{source_descriptions}"""
            }]
        )

        text = next(b.text for b in response.content if b.type == "text")
        indices = json.loads(text)
        return [self.sources[i] for i in indices if i < len(self.sources)]

Step 3: Synthesis with Cross-Referencing

class ResearchSynthesizer:
    """Synthesize findings from multiple sources with cross-referencing."""

    SYSTEM_PROMPT = """You are a research analyst. When synthesizing
information from multiple sources:

1. Cross-reference claims across sources
2. Note agreement and disagreement between sources
3. Flag claims that appear in only one source
4. Rate confidence for each finding (HIGH/MEDIUM/LOW)
5. Identify gaps where no source provides information
6. Use inline citations: [Source Title, Section]

Output structure:
## Key Findings
(numbered findings with confidence and citations)

## Agreements Across Sources
(claims confirmed by multiple sources)

## Contradictions
(where sources disagree, with both positions cited)

## Gaps
(questions not answered by available sources)

## Recommended Next Steps
(what additional research is needed)"""

    def __init__(self):
        self.client = Anthropic()

    def synthesize(self, question: str,
                   sources: list[Source]) -> str:
        source_content = ""
        for i, src in enumerate(sources):
            source_content += f"""
### Source {i+1}: {src.title}
Type: {src.source_type} | Reliability: {src.reliability}
URL: {src.url}

{src.content[:5000]}  # Truncate very long sources

---
"""
        response = self.client.messages.create(
            model="claude-opus-4-6-20260205",
            max_tokens=8192,
            thinking={"type": "adaptive", "effort": "maximum"},
            system=self.SYSTEM_PROMPT,
            messages=[{
                "role": "user",
                "content": f"""Research question: {question}

Sources:
{source_content}

Synthesize findings, cross-reference claims, and identify gaps."""
            }]
        )

        return next(b.text for b in response.content if b.type == "text")

Step 4: Fact-Checking Pipeline

class FactChecker:
    """Verify claims extracted from research synthesis."""

    def __init__(self):
        self.client = Anthropic()

    def extract_claims(self, synthesis: str) -> list[dict]:
        """Extract verifiable claims from a research synthesis."""
        response = self.client.messages.create(
            model="claude-opus-4-6-20260205",
            max_tokens=4096,
            messages=[{
                "role": "user",
                "content": f"""Extract all factual claims from this text.
For each claim, output JSON:
[{{"claim": "...", "category": "statistical|causal|comparative|temporal",
   "verifiable": true/false, "cited_source": "..."}}]

Text:
{synthesis}"""
            }]
        )

        text = next(b.text for b in response.content if b.type == "text")
        return json.loads(text)

    def verify_claim(self, claim: dict,
                     available_sources: list[Source]) -> dict:
        """Verify a single claim against available sources."""
        source_text = "\n\n".join(
            f"[{s.title}]: {s.content[:2000]}"
            for s in available_sources
        )

        response = self.client.messages.create(
            model="claude-opus-4-6-20260205",
            max_tokens=1024,
            thinking={"type": "adaptive", "effort": "deep"},
            messages=[{
                "role": "user",
                "content": f"""Verify this claim against the provided sources.

Claim: {claim['claim']}
Category: {claim['category']}

Sources:
{source_text}

Output JSON:
{{"verdict": "CONFIRMED|PARTIALLY_CONFIRMED|UNVERIFIABLE|CONTRADICTED",
  "confidence": 0.0-1.0,
  "supporting_evidence": "...",
  "contradicting_evidence": "...",
  "notes": "..."}}"""
            }]
        )

        text = next(b.text for b in response.content if b.type == "text")
        return json.loads(text)

    def verify_all(self, synthesis: str,
                   sources: list[Source]) -> list[dict]:
        """Extract and verify all claims in a synthesis."""
        claims = self.extract_claims(synthesis)
        results = []
        for claim in claims:
            if claim.get("verifiable"):
                result = self.verify_claim(claim, sources)
                results.append({**claim, **result})
        return results

Step 5: Citation Generation

class CitationFormatter:
    """Generate properly formatted citations."""

    def format_citations(self, sources: list[Source],
                         style: str = "APA") -> str:
        """Generate formatted citation list."""
        response = Anthropic().messages.create(
            model="claude-opus-4-6-20260205",
            max_tokens=2048,
            messages=[{
                "role": "user",
                "content": f"""Format these sources as {style} citations:

{json.dumps([{
    "title": s.title,
    "url": s.url,
    "type": s.source_type
} for s in sources], indent=2)}

Output a numbered reference list in proper {style} format."""
            }]
        )

        return next(b.text for b in response.content if b.type == "text")

Complete Pipeline Orchestration

class ResearchPipeline:
    """End-to-end research pipeline."""

    def __init__(self):
        self.retriever = SourceRetriever()
        self.synthesizer = ResearchSynthesizer()
        self.fact_checker = FactChecker()
        self.citation_formatter = CitationFormatter()

    def research(self, question: str) -> dict:
        # Step 1: Decompose
        sub_queries = decompose_research_question(question)

        # Step 2: Retrieve sources for each sub-query
        all_sources = []
        for sq in sub_queries:
            sources = self.retriever.retrieve_for_query(sq["sub_query"])
            all_sources.extend(sources)

        # Deduplicate
        seen = set()
        unique_sources = []
        for s in all_sources:
            if s.title not in seen:
                seen.add(s.title)
                unique_sources.append(s)

        # Step 3: Synthesize
        synthesis = self.synthesizer.synthesize(question, unique_sources)

        # Step 4: Verify
        verification = self.fact_checker.verify_all(synthesis, unique_sources)

        # Step 5: Citations
        citations = self.citation_formatter.format_citations(unique_sources)

        return {
            "question": question,
            "sub_queries": sub_queries,
            "sources_used": len(unique_sources),
            "synthesis": synthesis,
            "verification": verification,
            "citations": citations,
            "confidence": self._overall_confidence(verification),
        }

    def _overall_confidence(self, verification: list[dict]) -> float:
        if not verification:
            return 0.0
        confidences = [v.get("confidence", 0.5) for v in verification]
        return sum(confidences) / len(confidences)

The research pipeline pattern scales from simple literature reviews to complex multi-source investigations. In the next lesson, you will learn how to combine this with RAG patterns for even more powerful information retrieval.