← Blog

How to Build a Claude-Powered Research Assistant

2026-11-03 · 5 min read · SubToAPI Team

A Claude-powered research assistant is an application that takes a question, gathers information from external sources (search, documents, APIs), and returns a synthesized, cited answer using Claude as the reasoning engine. It's not a single prompt — it's a small pipeline: retrieval, tool calls, synthesis, and often a follow-up loop where Claude decides it needs more information before answering.

This guide walks through the actual architecture: what tools to wire up, how the request/response loop works, how to stream long answers, and how to keep citations attached to claims so the output is trustworthy rather than a confident-sounding guess.

The core architecture

A research assistant needs four pieces working together:

  1. A query planner — Claude decides what to search for based on the user's question
  2. A retrieval layer — web search, a vector database, or an internal document API that the model can call as a tool
  3. A synthesis step — Claude reads the retrieved content and writes an answer, ideally with inline citations
  4. A loop controller — your code that keeps calling Claude until it stops requesting tools and produces a final answer

The key design decision is that Claude doesn't fetch anything itself. It emits a tool call with arguments, your backend executes it (hits a search API, queries a database), and you feed the result back into the conversation. This round trip is what makes it a research assistant instead of a chatbot with a knowledge cutoff.

Step 1: Define the search tool

Start with a single tool: a web or document search function. Keep the schema tight so Claude doesn't over- or under-specify queries.

{
  "name": "search_sources",
  "description": "Search the web or internal knowledge base for information relevant to a query.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": { "type": "string", "description": "The search query" },
      "max_results": { "type": "integer", "default": 5 }
    },
    "required": ["query"]
  }
}

If you're routing requests through SubToAPI, tool definitions are passed in the same request body as the message. See /docs/tools for the full schema reference.

Step 2: Send the initial request

curl https://api.subtoapi.app/v1/messages \
  -H "Authorization: Bearer $SUBTOAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4.5",
    "max_tokens": 1024,
    "tools": [{
      "name": "search_sources",
      "description": "Search the web for relevant information.",
      "input_schema": {
        "type": "object",
        "properties": {
          "query": {"type": "string"},
          "max_results": {"type": "integer", "default": 5}
        },
        "required": ["query"]
      }
    }],
    "messages": [
      {"role": "user", "content": "What are the main approaches to vector database sharding, and what are the tradeoffs?"}
    ]
  }'

Claude's first response will typically contain a tool_use block instead of a final answer — it's asking you to run a search before it commits to anything.

Step 3: Execute the tool call and loop

Your backend pulls the query argument out of the tool call, hits your search provider, and sends the results back as a tool_result message:

async function runResearchLoop(question, searchFn) {
  let messages = [{ role: "user", content: question }];
  const tools = [/* search_sources schema */];

  while (true) {
    const res = await fetch("https://api.subtoapi.app/v1/messages", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        model: "claude-sonnet-4.5",
        max_tokens: 1536,
        tools,
        messages
      })
    });

    const data = await res.json();
    messages.push({ role: "assistant", content: data.content });

    const toolCall = data.content.find(b => b.type === "tool_use");
    if (!toolCall) return data; // final answer, no more tool calls

    const results = await searchFn(toolCall.input.query, toolCall.input.max_results);

    messages.push({
      role: "user",
      content: [{
        type: "tool_result",
        tool_use_id: toolCall.id,
        content: JSON.stringify(results)
      }]
    });
  }
}

This loop can run multiple times for complex questions — Claude might search once, read the results, decide it needs a more specific follow-up query, and search again before synthesizing. That's normal and is exactly why you keep looping instead of expecting one search to be enough. For the full request/response shape, check /docs/messages.

Step 4: Keep citations attached

Ask Claude explicitly to cite sources inline, and pass each search result with a stable identifier (URL or document ID) so it can reference them:

When you use information from a search result, cite it inline like [1], [2], etc., 
matching the order of the sources provided. List the sources at the end.

Feeding results as structured JSON (title, url, snippet) rather than raw text makes this much more reliable — Claude can match claims to the right source index instead of guessing.

Step 5: Stream the final answer

Research answers tend to be long. Once Claude has stopped calling tools and is writing the final synthesis, switch to streaming so the user sees output immediately instead of waiting for the full response:

curl https://api.subtoapi.app/v1/messages \
  -H "Authorization: Bearer $SUBTOAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4.5",
    "stream": true,
    "max_tokens": 2048,
    "messages": [...]
  }'

See /docs/streaming for the event format if you're parsing server-sent events on the frontend.

Production considerations

If you're prototyping this as a side project or internal tool, you don't need to manage Claude API credentials, rate limits, and billing separately — /signup gets you an API key and a free trial, and /pricing covers Solo, Team, and Scale tiers if you're building this for a team rather than yourself. The /docs/quickstart page has a minimal working example if you want to skip straight to a working request.

Questions

Does Claude have built-in web search? No — Claude's API doesn't fetch URLs or run searches on its own. You provide a search tool (your own API, a search provider, or a vector database), and Claude emits structured tool calls that your backend executes and feeds back in.

How many search rounds does a typical research query need? Most questions resolve in one to three rounds: an initial broad search, then one or two refinement searches if the first results are too generic or Claude needs a specific fact to continue. Cap it at a hard limit to avoid open-ended loops.

Can I use a smaller/cheaper Claude model for the search-query planning step? Yes — many teams use a faster model to generate search queries and a stronger model only for the final synthesis, since query planning is a simpler task than reasoning over retrieved content.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →