How Vane Combines LLMs and Web Search: Inside the Researcher Agent Architecture

Vane combines LLMs and web search through an iterative tool-calling loop where the Researcher agent lets the model decide when to invoke the web_search tool, executes queries via SearxNG, and feeds results back for citation-rich reasoning.

The open-source repository ItzCrazyKns/Vane implements a unique agentic architecture that moves beyond static retrieval-augmented generation. By allowing large language models to dynamically control their own search queries through a continuous feedback loop, Vane delivers up-to-date, verifiable answers while keeping the entire pipeline private and self-hosted.

The Core Architecture: LLM-Driven Search Decisions

The Researcher Agent Orchestration

The orchestration logic resides in src/lib/agents/search/researcher/index.ts, where the Researcher agent manages a streaming conversation with the LLM. When a user submits a query, Vane initializes a research block and starts a streaming LLM call using a system prompt generated by getResearcherPrompt from src/lib/prompts/search/researcher.ts. This prompt contains structured descriptions of all available tools, including the web_search capability, effectively teaching the model how to request external data.

Tool Calling and Decision Parsing

During the streaming response, the LLM can emit special tool-call chunks indicating its desire to search. The Researcher parses these chunks between lines 68-77 of src/lib/agents/search/researcher/index.ts, recording each planned action in the research block before handing execution to the Action Registry. This design delegates search timing and query formulation to the model itself, rather than using pre-defined retrieval steps.

Executing Web Searches with SearxNG

The web_search Action Implementation

When the LLM decides to search, the web_search action defined in src/lib/agents/search/researcher/actions/webSearch.ts processes the request. The action accepts up to three query strings per invocation—enforced via input.queries.slice(0, 3)—and logs each searching step before executing queries in parallel.

// src/lib/agents/search/researcher/actions/webSearch.ts
const webSearchAction: ResearchAction<typeof actionSchema> = {
  name: 'web_search',
  schema: actionSchema,
  enabled: cfg => cfg.sources.includes('web') && !cfg.classification.skipSearch,
  async execute(input, ctx) {
    // limit to 3 queries
    input.queries = input.queries.slice(0, 3);
    const results: Chunk[] = [];

    // run each query against SearxNG
    await Promise.all(
      input.queries.map(async q => {
        const { results: r } = await searchSearxng(q);
        results.push(...r.map(item => ({
          content: item.content || item.title,
          metadata: { title: item.title, url: item.url },
        })));
      })
    );

    return { type: 'search_results', results };
  },
};

SearxNG Integration and Configuration

The actual HTTP requests are handled by searchSearxng in src/lib/searxng.ts. This function retrieves the endpoint URL via getSearxngURL from src/lib/config/serverRegistry.ts, which reads the config.search.searxngURL setting configured by the user. It constructs requests to the SearxNG /search?format=json endpoint with a 10-second timeout, returning structured results containing titles, URLs, and content snippets.

// src/lib/searxng.ts
export const searchSearxng = async (query, opts) => {
  const base = getSearxngURL();               // ← config.search.searxngURL
  const url = new URL(`${base}/search?format=json`);
  url.searchParams.append('q', query);
  // optional params (categories, engines, …)
  if (opts) Object.entries(opts).forEach(([k, v]) =>
    Array.isArray(v)
      ? url.searchParams.append(k, v.join(','))
      : url.searchParams.append(k, v as string)
  );

  const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
  const { results, suggestions } = await res.json();
  return { results, suggestions };
};

Result Injection and Iterative Reasoning

Search results are transformed into Chunk objects and appended to the research block as a search_results sub-step between lines 35-55 of webSearch.ts. This real-time update allows the LLM to reference fresh citations in subsequent iterations. The model can issue additional web_search calls or invoke other tools until it determines the answer is complete—signaled by emitting a done tool call—or until the iteration budget is exhausted. The final answer is rendered alongside collected sources extracted from the research block at lines 184-214 of src/lib/agents/search/researcher/index.ts.

Cross-Provider LLM Support

Vane abstracts LLM interactions through the BaseLLM interface defined in src/lib/models/base/llm.ts. Each provider implementation—including OpenAI, Anthropic, Ollama, and Groq—exposes streamText for the Researcher's iterative loop and generateObject for structured generation tasks. This abstraction ensures that Vane combines LLMs and web search consistently regardless of which backend hosts the model.

Summary

Frequently Asked Questions

How does Vane decide when to search the web?

According to the src/lib/agents/search/researcher/index.ts source code, the LLM itself decides when to search by emitting a web_search tool call during its streaming response. The system prompt generated by getResearcherPrompt instructs the model on available tools, and the Researcher agent parses these calls in real-time, executing them only when the model explicitly requests external data.

What search engine does Vane use for web queries?

Vane uses SearxNG as its search backend. The searchSearxng function in src/lib/searxng.ts constructs HTTP requests to a user-configured SearxNG endpoint specified via config.search.searxngURL in the server configuration. This allows Vane to aggregate results from multiple search engines while maintaining user privacy.

Is Vane compatible with local LLMs?

Yes. Vane supports local inference through providers like Ollama and LM Studio, implemented via the BaseLLM interface in src/lib/models/base/llm.ts. As long as the local endpoint supports the expected streamText and generateObject methods, it can power the same iterative search-and-reasoning loop used by cloud-based providers.

How many queries can Vane run in a single search step?

The web_search action in src/lib/agents/search/researcher/actions/webSearch.ts limits each tool invocation to three queries, enforced by slicing the input array with input.queries.slice(0, 3). However, the LLM can invoke the web_search tool multiple times across different iterations of the research loop if it requires additional information.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →