Table of Contents

Build an Agent with Web Search and Live Data Access

Give your agent real-time access to the internet. LM-Kit.NET's WebSearchTool lets an agent search the web, read the results, and synthesize an answer, all running locally on your hardware. This guide walks through every provider option, event hook, and production configuration so you can ship a grounded, up-to-date AI agent.


TL;DR

Minimal working agent with web search in under 20 lines of setup. Uses DuckDuckGo, no API key required:

using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

using LM model = LM.LoadFromModelID("qwen3:8b");

var agent = Agent.CreateBuilder(model)
    .WithPersona("Research Assistant")
    .WithInstruction("Search the web to answer questions with current information. Cite your sources.")
    .WithPlanning(PlanningStrategy.ReAct)
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.WebSearch);
    })
    .Build();

var result = await agent.RunAsync("What were the biggest tech announcements this week?");
Console.WriteLine(result.Content);

Read on for premium providers, event hooks, page fetching, and production hardening.


Why This Matters

Two problems that web search solves for AI agents:

  1. Stale training data. Every model has a knowledge cutoff. An agent without web access cannot answer questions about recent events, updated documentation, current prices, or breaking news. Web search closes that gap by grounding the agent's responses in live data.
  2. Research automation. Many workflows require gathering information from multiple sources, comparing results, and synthesizing a summary. A search-enabled agent can do this autonomously: it formulates queries, reads snippets, refines its search, and produces a coherent answer.

Prerequisites

Requirement Minimum
.NET SDK 8.0+
VRAM 6+ GB (for Qwen 3 8B with tool-calling support)
Internet Required for web search queries
API Key Optional (DuckDuckGo works without one)

You need a model that supports tool calling. Recommended: qwen3:8b. Verify with model.HasToolCalls after loading.


Step 1: Create a Basic Search Agent

This example uses DuckDuckGo as the search provider. It requires no API key and no account setup.

using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

// 1. Load a tool-calling model
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });

Console.WriteLine($"\n  Tool calling supported: {model.HasToolCalls}\n");

// 2. Build the agent with ReAct planning and web search
var agent = Agent.CreateBuilder(model)
    .WithPersona("Research Assistant")
    .WithInstruction("You help users find current information by searching the web. " +
                     "Always cite the source URL when presenting search results. " +
                     "If the first search does not answer the question, refine your query and search again.")
    .WithPlanning(PlanningStrategy.ReAct)
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.WebSearch);  // DuckDuckGo, no API key
    })
    .WithMaxIterations(10)
    .Build();

// 3. Interactive loop
Console.WriteLine("Search agent ready. Ask anything (or 'quit' to exit):\n");

while (true)
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.Write("You: ");
    Console.ResetColor();

    string? input = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(input) || input.Equals("quit", StringComparison.OrdinalIgnoreCase))
        break;

    var result = await agent.RunAsync(input);

    if (result.IsSuccess)
    {
        Console.ForegroundColor = ConsoleColor.Cyan;
        Console.WriteLine($"\nAssistant: {result.Content}");
        Console.ResetColor();

        if (result.ToolCalls.Count > 0)
        {
            Console.ForegroundColor = ConsoleColor.DarkGray;
            Console.WriteLine($"  Tools used: {string.Join(", ", result.ToolCalls.Select(tc => tc.ToolName))}");
            Console.WriteLine($"  Duration: {result.Duration.TotalSeconds:F1}s");
            Console.ResetColor();
        }
    }
    else
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine($"\nError: {result.Error?.Message}");
        Console.ResetColor();
    }

    Console.WriteLine();
}

How it works: The ReAct planning strategy drives a Thought/Action/Observation loop. When the agent decides it needs current data, it calls web_search with a query string and an optional maxResults count (1 to 20, default 5). Each result contains a title, snippet, and url. The agent reads the snippets, reasons about them, and either searches again or produces a final answer.


Step 2: Switch to a Premium Provider

DuckDuckGo is great for getting started, but premium providers offer higher-quality results and better rate limits.

Free tier provides 2,000 queries per month. High-quality results with rich snippets.

using LMKit.Agents.Tools.BuiltIn;
using LMKit.Agents.Tools.BuiltIn.Net;

var braveSearch = BuiltInTools.CreateWebSearch(
    WebSearchTool.Provider.Brave,
    "your-brave-api-key"
);

var agent = Agent.CreateBuilder(model)
    .WithPlanning(PlanningStrategy.ReAct)
    .WithTools(tools =>
    {
        tools.Register(braveSearch);
    })
    .Build();

Tavily (Optimized for AI and RAG)

Tavily is designed specifically for AI agents and retrieval-augmented generation workloads. It returns clean, structured results that models can consume efficiently.

var tavilySearch = BuiltInTools.CreateWebSearch(
    WebSearchTool.Provider.Tavily,
    Environment.GetEnvironmentVariable("TAVILY_API_KEY")
);

Serper (Google Results via API)

Access Google search results programmatically through the Serper API.

var serperSearch = BuiltInTools.CreateWebSearch(
    WebSearchTool.Provider.Serper,
    Environment.GetEnvironmentVariable("SERPER_API_KEY")
);

All providers use the same WebSearchTool interface, so switching providers requires changing only the factory call. Your agent code stays identical.


Step 3: Combine Search with Page Fetching

Search results return snippets, but sometimes the agent needs the full content of a page. Register both WebSearchTool and HttpTool so the agent can search, find a relevant URL, then fetch the complete page.

using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;

using LM model = LM.LoadFromModelID("qwen3:8b");

var agent = Agent.CreateBuilder(model)
    .WithPersona("Deep Research Assistant")
    .WithInstruction(
        "You are a thorough research assistant. " +
        "First, search the web to find relevant pages. " +
        "Then, use the HTTP tool to fetch the full content of the most promising URLs. " +
        "Synthesize your findings into a comprehensive answer with source citations.")
    .WithPlanning(PlanningStrategy.ReAct)
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.WebSearch);  // Find relevant URLs
        tools.Register(BuiltInTools.Http);       // Fetch full page content
    })
    .WithMaxIterations(15)
    .Build();

var result = await agent.RunAsync(
    "Compare the latest features of .NET 9 and .NET 10 Preview. " +
    "What are the key differences?"
);

Console.WriteLine(result.Content);

The agent's ReAct loop looks like this:

Thought: I need to find information about .NET 9 and .NET 10 features.
Action: web_search(query: ".NET 10 preview new features")
Observation: [snippets with URLs]
Thought: The Microsoft blog post looks most relevant. Let me read the full page.
Action: http_get(url: "https://devblogs.microsoft.com/...")
Observation: [full page content]
Thought: Now I have enough information to compare. Let me compile the answer.
Final Answer: ...

Step 4: Filter and Monitor Results

The WebSearchTool fires two events that give you visibility into the raw search data and control over which results the agent sees.

Log Raw Responses

The RawContentReceived event fires when the raw HTML or JSON response arrives from the search provider. Use it for logging, debugging, or analytics.

using LMKit.Agents.Tools.BuiltIn;
using LMKit.Agents.Tools.BuiltIn.Net;

var search = BuiltInTools.CreateWebSearch(new WebSearchTool.Options
{
    SearchProvider = WebSearchTool.Provider.Brave,
    ApiKey = "your-brave-api-key"
});

search.RawContentReceived += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.DarkGray;
    Console.WriteLine($"  [Search Log] Provider: {e.SearchProvider}");
    Console.WriteLine($"  [Search Log] URL: {e.RequestUrl}");
    Console.WriteLine($"  [Search Log] Response length: {e.RawContent.Length} chars");
    Console.ResetColor();
};

Filter Out Unwanted Domains

The ResultProcessing event fires before each individual result is added to the response. Set Cancel = true to exclude results from specific domains or containing unwanted content.

// Block results from specific domains
var blockedDomains = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
    "pinterest.com",
    "quora.com"
};

search.ResultProcessing += (sender, e) =>
{
    // Check if the result URL matches a blocked domain
    if (Uri.TryCreate(e.ProcessedResult.Url, UriKind.Absolute, out var uri))
    {
        string host = uri.Host.TrimStart("www.".ToCharArray());

        if (blockedDomains.Any(d => host.EndsWith(d, StringComparison.OrdinalIgnoreCase)))
        {
            e.Cancel = true;  // Exclude this result
            Console.WriteLine($"  [Filtered] Blocked result from {uri.Host}");
        }
    }
};

Register the configured search tool with the agent as usual:

var agent = Agent.CreateBuilder(model)
    .WithPlanning(PlanningStrategy.ReAct)
    .WithTools(tools =>
    {
        tools.Register(search);
    })
    .Build();

Step 5: Configure for Production

Use the WebSearchTool.Options class for fine-grained control over search behavior.

using LMKit.Agents.Tools.BuiltIn;
using LMKit.Agents.Tools.BuiltIn.Net;

var productionSearch = BuiltInTools.CreateWebSearch(new WebSearchTool.Options
{
    // Provider and authentication
    SearchProvider = WebSearchTool.Provider.Brave,
    ApiKey = Environment.GetEnvironmentVariable("BRAVE_API_KEY"),

    // Result limits
    DefaultMaxResults = 8,          // Return up to 8 results per query (default: 5)
    MaxSnippetLength = 1024,        // Truncate snippets to 1024 chars (default: 2048)

    // Network settings
    Timeout = TimeSpan.FromSeconds(10),  // Fail fast on slow connections (default: 15s)
    UserAgent = "MyApp/1.0 ResearchBot"  // Custom user agent string
});

Configuration Reference

Property Default Description
SearchProvider DuckDuckGo Which search engine to use
ApiKey null API key for Brave, Tavily, or Serper
BaseUrl null Custom endpoint for SearXNG or Custom providers
Timeout 15 seconds Maximum time to wait for search response
DefaultMaxResults 5 Number of results returned per search (1 to 20)
MaxSnippetLength 2048 Maximum characters per snippet before truncation
UserAgent null Custom HTTP User-Agent header

Results are automatically HTML-decoded, emoji-stripped, and whitespace-normalized before the agent sees them.


Step 6: Use a Self-Hosted Search Engine

SearXNG is an open-source, self-hosted meta-search engine. It queries multiple search engines simultaneously and aggregates the results. No API key is needed, but you must provide the base URL of your SearXNG instance.

using LMKit.Agents.Tools.BuiltIn;
using LMKit.Agents.Tools.BuiltIn.Net;

var searxSearch = BuiltInTools.CreateWebSearch(new WebSearchTool.Options
{
    SearchProvider = WebSearchTool.Provider.SearXNG,
    BaseUrl = "https://searxng.mycompany.com",   // Your self-hosted instance
    DefaultMaxResults = 10,
    Timeout = TimeSpan.FromSeconds(20)
});

var agent = Agent.CreateBuilder(model)
    .WithPersona("Internal Research Assistant")
    .WithInstruction("Search using the company's internal search engine. " +
                     "Summarize findings clearly and cite sources.")
    .WithPlanning(PlanningStrategy.ReAct)
    .WithTools(tools =>
    {
        tools.Register(searxSearch);
    })
    .Build();

This is ideal for air-gapped or privacy-sensitive environments where queries must not leave your network.

Custom Provider

For complete control, use the Custom provider with URL placeholders:

var customSearch = BuiltInTools.CreateWebSearch(new WebSearchTool.Options
{
    SearchProvider = WebSearchTool.Provider.Custom,
    BaseUrl = "https://search.internal.corp/api?q={query}&limit={maxResults}",
    Timeout = TimeSpan.FromSeconds(10)
});

The {query} and {maxResults} placeholders are replaced at runtime with the agent's search parameters.


Complete Example

A full research agent that searches the web, fetches page content, and synthesizes a detailed answer with citations.

using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;
using LMKit.Agents.Tools.BuiltIn.Net;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

// ──────────────────────────────────────
// 1. Load model
// ──────────────────────────────────────
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

// ──────────────────────────────────────
// 2. Configure search with monitoring
// ──────────────────────────────────────
var search = BuiltInTools.CreateWebSearch(new WebSearchTool.Options
{
    SearchProvider = WebSearchTool.Provider.DuckDuckGo,
    DefaultMaxResults = 8,
    MaxSnippetLength = 1024,
    Timeout = TimeSpan.FromSeconds(15)
});

// Log each search query
search.RawContentReceived += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.DarkGray;
    Console.WriteLine($"\n  [Search] Query sent to {e.SearchProvider}: {e.RequestUrl}");
    Console.ResetColor();
};

// Filter out unwanted domains
search.ResultProcessing += (sender, e) =>
{
    if (Uri.TryCreate(e.ProcessedResult.Url, UriKind.Absolute, out var uri))
    {
        string host = uri.Host.TrimStart("www.".ToCharArray());
        if (host.EndsWith("pinterest.com", StringComparison.OrdinalIgnoreCase))
        {
            e.Cancel = true;
        }
    }
};

// ──────────────────────────────────────
// 3. Build the research agent
// ──────────────────────────────────────
var agent = Agent.CreateBuilder(model)
    .WithPersona("Senior Research Analyst")
    .WithInstruction(
        "You are a thorough research analyst. Follow these steps for every question:\n" +
        "1. Search the web with a well-crafted query.\n" +
        "2. Review the search snippets and identify the most relevant sources.\n" +
        "3. If snippets are insufficient, use the HTTP tool to fetch full page content.\n" +
        "4. Synthesize your findings into a clear, structured answer.\n" +
        "5. Always include source URLs at the end of your response.")
    .WithPlanning(PlanningStrategy.ReAct)
    .WithTools(tools =>
    {
        tools.Register(search);            // Web search with filtering
        tools.Register(BuiltInTools.Http); // Fetch full pages when needed
    })
    .WithMaxIterations(15)
    .Build();

// ──────────────────────────────────────
// 4. Interactive research session
// ──────────────────────────────────────
Console.WriteLine("Research agent ready. Ask a question (or 'quit' to exit):\n");

while (true)
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.Write("You: ");
    Console.ResetColor();

    string? input = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(input) || input.Equals("quit", StringComparison.OrdinalIgnoreCase))
        break;

    Console.ForegroundColor = ConsoleColor.DarkGray;
    Console.WriteLine("  Researching...\n");
    Console.ResetColor();

    var result = await agent.RunAsync(input);

    if (result.IsSuccess)
    {
        Console.ForegroundColor = ConsoleColor.Cyan;
        Console.WriteLine($"Assistant:\n{result.Content}");
        Console.ResetColor();

        // Summary of tool usage
        if (result.ToolCalls.Count > 0)
        {
            Console.ForegroundColor = ConsoleColor.DarkGray;
            var toolSummary = result.ToolCalls
                .GroupBy(tc => tc.ToolName)
                .Select(g => $"{g.Key} ({g.Count()}x)");
            Console.WriteLine($"\n  Tools: {string.Join(", ", toolSummary)}");
            Console.WriteLine($"  Total duration: {result.Duration.TotalSeconds:F1}s");
            Console.WriteLine($"  Inference steps: {result.InferenceCount}");
            Console.ResetColor();
        }
    }
    else
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine($"Error: {result.Error?.Message}");
        Console.ResetColor();
    }

    Console.WriteLine();
}

Provider Comparison

Provider API Key Required Free Tier Quality Best For
DuckDuckGo No Unlimited (scraping) Good Prototyping, no-cost deployments
Brave Yes 2,000 queries/month High Production apps needing quality results
Tavily Yes Free tier available High AI agents and RAG pipelines
Serper Yes Free tier available High Google-quality results via API
SearXNG No Self-hosted Varies Air-gapped or privacy-sensitive environments
Custom Optional N/A N/A Internal search engines, custom endpoints

Troubleshooting

Problem Cause Fix
Agent never searches Model doesn't support tool calling Check model.HasToolCalls. Use qwen3:8b or another tool-capable model
Empty search results DuckDuckGo rate-limiting or network issue Switch to Brave or Tavily. Verify internet connectivity
Search times out Slow network or provider latency Increase Timeout in WebSearchTool.Options. Default is 15 seconds
Results contain irrelevant content Too many low-quality results Use ResultProcessing event to filter domains. Reduce DefaultMaxResults
Agent loops without answering Too few iterations for a complex query Increase WithMaxIterations(). Ensure the agent instruction encourages concise answers
Snippets are too long for context Large snippets consume model context window Reduce MaxSnippetLength (e.g., 512 or 1024 chars)
API key errors Missing or invalid key Verify the key with the provider's dashboard. Use environment variables instead of hardcoded strings
WebSearchTool not found in GetAll() By design, it must be registered manually Always call tools.Register(BuiltInTools.WebSearch) explicitly

Next Steps