n4nAI

Designing a multi-step research loop with Claude Opus 4.5

Build a multi-step research loop with Claude Opus 4.5 using the Anthropic SDK. Hands-on tutorial with planner, tool-use researcher, and synthesis.

n4n Team2 min read486 words

Audio narration

Coming soon — every post will get a voice note here.

Building a multi-step research loop claude system means chaining planning, retrieval, and synthesis into one autonomous cycle. This tutorial implements that pattern with Claude Opus 4.5 using the Anthropic SDK and a stubbed search backend. You get runnable code and expected outputs at each stage.

Prerequisites

  • Python 3.10 or newer
  • anthropic package: pip install anthropic
  • An Anthropic API key exported as ANTHROPIC_API_KEY
  • (Optional) An OpenAI-compatible gateway such as n4n.ai if you want automatic fallback across providers; the native SDK code below swaps cleanly to a base URL change.

Architecture of the loop

A research agent that tries to do everything in one prompt loses attribution and hits context limits. Splitting the work into three roles keeps each step debuggable:

  • Planner decomposes the user query into answerable subquestions.
  • Researcher executes those subquestions, calling a search tool as needed.
  • Synthesizer merges the collected findings into a cited brief.

Why not a single prompt

Claude Opus 4.5 handles long context, but a 50-page research dump in one message forces the model to both retrieve and write simultaneously. Separation lets you cache the planner’s output, retry the researcher on tool failure, and swap the retriever without rewriting the writing logic.

State shape

from dataclasses import dataclass, field

@dataclass
class ResearchState:
    query: str
    subquestions: list[str] = field(default_factory=list)
    findings: dict[str, str] = field(default_factory=dict)
    report: str = ""

Step 1: The planner

We force structured output by offering a single tool and locking tool_choice. This avoids fragile JSON parsing from free text.

import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
MODEL = "claude-opus-4-5"

def plan_subquestions(query: str) -> list[str]:
    resp = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        tools=[{
            "name": "emit_plan",
            "description": "Return research subquestions",
            "input_schema": {
                "type": "object",
                "properties": {
                    "subquestions": {
                        "type": "array",
                        "items": {"type": "string"}
                    }
                },
                "required": ["subquestions"]
            }
        }],
        tool_choice={"type": "tool", "name": "emit_plan"},
        messages=[{
            "role": "user",
            "content": f"Decompose this research task into 3-5 specific subquestions: {query}"
        }]
    )
    return resp.content[0].input["subquestions"]

Expected output for query = "What are the tradeoffs of MVCC in distributed databases?":

[
  "How does MVCC work in single-node vs distributed systems?",
  "What consistency anomalies can arise under MVCC in distributed settings?",
  "Which production databases use MVCC and how do they differ?"
]

Step 2: The researcher with tool use

The researcher calls a web_search tool. We run a stub, return the result, and loop until Claude emits text instead of a tool call.

def web_search(query: str) -> str:
    # Stub: replace with Bing, SerpAPI, or an internal vector index.
    return f"Stub result for '{query}': MVCC avoids reader-writer locks but requires version cleanup."

def research_subquestion(sub: str) -> str:
    messages = [{"role": "user", "content": sub}]
    while True:
        resp = client.messages.create(
            model=MODEL,
            max_tokens=1024,
            tools=[{
                "name": "web_search",
                "description": "Search the web for factual context",
                "input_schema": {
                    "type": "object",
                    "properties": {"query": {"type": "string"}},
                    "required": ["query"]
                }
            }],
            messages=messages
        )
        if resp.stop_reason == "tool_use":
            tool = resp.content[-1]
            result = web_search(tool.input["query"])
            messages.append({"role": "assistant", "content": resp.content})
            messages.append({
                "role": "user",
                "content": [{
                    "type": "tool_result",
                    "tool_use_id": tool.id,
                    "content": result
                }]
            })
        else:
            return "".join(block.text for block in resp.content if block.type == "text")

Checkpoint output inside the loop (abridged):

> assistant requests tool_use: web_search("MVCC distributed consistency anomalies")
< tool_result: Stub result for 'MVCC distributed consistency anomalies'...
> final text: MVCC in distributed systems can exhibit write skew unless guarded by serializable snapshot isolation...

Step 3: Synthesis

The synthesizer receives only the gathered findings, not the raw tool chatter. That keeps the final brief focused.

def synthesize(state: ResearchState) -> str:
    context = "\n\n".join(
        f"Q: {q}\nA: {state.findings[q]}" for q in state.subquestions
    )
    resp = client.messages.create(
        model=MODEL,
        max_tokens=2048,
        messages=[{
            "role": "user",
            "content": f"Write a concise research brief using only these findings:\n{context}"
        }]
    )
    return "".join(b.text for b in resp.content if b.type == "text")

Step 4: Wiring it together

def run_research(query: str) -> ResearchState:
    state = ResearchState(query=query)
    state.subquestions = plan_subquestions(query)
    for sub in state.subquestions:
        state.findings[sub] = research_subquestion(sub)
    state.report = synthesize(state)
    return state

if __name__ == "__main__":
    result = run_research("What are the tradeoffs of MVCC in distributed databases?")
    print(result.report)

Running the loop

Execute python research_loop.py. A typical final brief opens like:

# Research Brief: MVCC in Distributed Databases

Multi-version concurrency control (MVCC) decouples readers from writers by
snapshotting row versions. In distributed deployments, this avoids coarse
locks but introduces version garbage and cross-node visibility gaps...

Sources:
- How does MVCC work in single-node vs distributed systems?
- What consistency anomalies can arise under MVCC in distributed settings?
- Which production databases use MVCC and how do they differ?

The multi-step research loop claude design keeps each phase isolated, so you can replace the stub search with a real retriever or add a critique pass without disturbing the planner.

Operational notes

Tool loops burn tokens fast. Set max_tokens per call and cap researcher iterations at something like five to avoid runaway cost. Log the raw tool inputs and outputs; when the researcher calls web_search with a malformed query, you want that visible in logs.

Idempotency matters. Key findings by subquestion so a crashed run can resume from the synthesizer instead of re-paying for search. If you route through an OpenAI-compatible gateway that honors client routing directives, forward Anthropic’s cache_control hints on the system prompt to reuse prompt prefixes across the planner and researcher. n4n.ai forwards those hints and meters per-token usage, which simplifies debugging a long-running multi-step research loop claude job.

Extending the loop

The pattern recurses naturally. If a subquestion returns thin findings, feed it back to plan_subquestions and run a deeper layer. Store depth in ResearchState and bail at a max depth. That turns a linear brief into a tree-backed research agent without changing the core calls.

Tagsclaude-opusresearch-agentagentic-searchtutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All deep research & agentic search posts →