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

> Discover how Vane integrates LLMs and web search. Its Researcher agent iteratively uses SearxNG for tool-calling, returning cited results for enhanced reasoning.

- Repository: [Kushagra Srivastava/Vane](https://github.com/ItzCrazyKns/Vane)
- Tags: architecture
- Published: 2026-03-11

---

**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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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.

```typescript
// 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`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/searxng.ts). This function retrieves the endpoint URL via `getSearxngURL` from [`src/lib/config/serverRegistry.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/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.

```typescript
// 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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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

- **Agentic Control**: The Researcher agent in [`src/lib/agents/search/researcher/index.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/agents/search/researcher/index.ts) lets the LLM decide when to search rather than using static retrieval.
- **SearxNG Backend**: All web queries route through the configurable SearxNG endpoint defined in [`src/lib/searxng.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/searxng.ts) and [`src/lib/config/serverRegistry.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/config/serverRegistry.ts).
- **Parallel Execution**: The `web_search` action processes up to three queries simultaneously via [`src/lib/agents/search/researcher/actions/webSearch.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/agents/search/researcher/actions/webSearch.ts).
- **Context Building**: Search results are injected as `Chunk` objects into the research block, enabling iterative, citation-aware reasoning across multiple turns.
- **Provider Agnostic**: The `BaseLLM` interface in [`src/lib/models/base/llm.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/models/base/llm.ts) supports diverse backends while maintaining the same tool-calling contract.

## Frequently Asked Questions

### How does Vane decide when to search the web?

According to the [`src/lib/agents/search/researcher/index.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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.