← Blog

How to Build a Claude-Powered Search Assistant

2026-10-28 · 5 min read · SubToAPI Team

A Claude-powered search assistant is an application that lets Claude decide when to query an external search source (a web search API, your own vector index, or a documentation site), read the results, and synthesize an answer with citations. Unlike a plain chatbot, it doesn't rely on what Claude already knows — it fetches fresh, specific information and reasons over it in real time.

Building one comes down to four parts: a model that supports tool use, a search backend Claude can call, a loop that handles the back-and-forth between "Claude wants to search" and "here are the results," and a way to stream the final answer to the user. Below is a practical walkthrough you can adapt regardless of which search backend you use.

What You're Actually Building

At a high level, the assistant works like this:

  1. The user asks a question.
  2. Claude evaluates whether it needs external information and, if so, calls a search tool with a query.
  3. Your backend runs that query against a real search provider and returns structured results.
  4. Claude reads the results and either searches again or writes a final answer, ideally with source references.
  5. The answer streams back to the user token by token.

This is a tool-use pattern, not a fine-tuning or embeddings problem — you don't need to retrain anything. You need Claude to know it has a search tool and reliably use it.

Step 1: Set Up API Access

You need a Claude API key and a way to call the Messages endpoint with tool definitions and streaming enabled. If you're already running Claude through SubToAPI, you get this with an application key (sub_live_...) rather than managing raw provider credentials — useful if you want usage metadata and team seats on top of the same Messages API. Sign up at /signup, check /pricing for plan limits, and see /docs/quickstart to get your first key issued.

Step 2: Define the Search Tool

Claude needs a tool schema describing what "search" means, what parameters it takes, and what it returns. Keep the description specific — vague tool descriptions lead to Claude either over-calling or ignoring the tool.

{
  "name": "web_search",
  "description": "Search the web for current information. Use this when the user asks about recent events, specific facts, prices, or anything that may have changed since your training data.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "The search query, written as a user would type it into a search engine."
      }
    },
    "required": ["query"]
  }
}

If you're searching your own documents instead of the open web, swap the description to match (e.g., "Search the internal knowledge base for relevant articles") and point the backend implementation at your vector store or Elasticsearch index instead of a search API.

Step 3: Wire Up the Tool-Use Loop

This is the part people get wrong: tool use isn't a single request. Claude returns a tool_use block, your code executes the actual search, and you send the result back as a tool_result in a follow-up message. Repeat until Claude stops requesting tools.

async function askWithSearch(question) {
  let messages = [{ role: "user", content: question }];

  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: 1024,
        tools: [webSearchTool],
        messages
      })
    });

    const data = await res.json();
    const toolUse = data.content.find(b => b.type === "tool_use");

    if (!toolUse) {
      return data.content.find(b => b.type === "text").text;
    }

    const results = await runSearch(toolUse.input.query);
    messages.push({ role: "assistant", content: data.content });
    messages.push({
      role: "user",
      content: [{
        type: "tool_result",
        tool_use_id: toolUse.id,
        content: JSON.stringify(results)
      }]
    });
  }
}

The implementation of runSearch() is where you plug in your actual provider — a search API, a scraped index, or a retrieval layer over your own documents. Full tool schema and request/response shapes are documented at /docs/tools, and general request formatting is covered at /docs/messages.

Step 4: Stream the Final Answer

Once Claude has search results and starts writing the actual answer, you want that streamed to the user rather than waiting for the full response — especially if the answer is long or the user is watching a loading state. Switch stream: true on the final request in the loop (the search-calling turns don't need streaming, only the answer-writing turn benefits from it).

const stream = 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: 1024,
    stream: true,
    messages
  })
});

Details on handling server-sent events and partial token deltas are in /docs/streaming.

Step 5: Track Citations

A search assistant is only as trustworthy as its sourcing. Instruct Claude in your system prompt to cite sources inline (e.g., [1], [2]) and have your search results include URLs or document IDs so you can render a footnote list after the response. This also makes debugging much easier — when the assistant gives a wrong answer, you can check whether the search results were bad or Claude misread good results.

Production Considerations

A few things that matter once this moves past a prototype:

Questions

Do I need embeddings or a vector database to build this? No. Tool use works with any search backend — a web search API, a keyword search index, or a vector store. Embeddings help if you're searching your own unstructured documents, but they're not required for the pattern itself.

How many tool calls does a typical search assistant make per question? Most well-scoped questions resolve in one or two tool calls. If Claude is consistently calling search three or more times, your tool description or search result quality usually needs improvement.

Can I use this pattern for internal documentation instead of the public web? Yes — swap the search tool's backend to query your internal docs, wiki, or support tickets. The tool-use loop and streaming logic stay identical; only the runSearch() implementation changes.

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 →