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:
- 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.
- 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.
Brave Search
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
- Create an AI Agent with Tools: covers the full agent architecture, custom tools via
ITool, and attribute-based tools. - Build a RAG Pipeline: combine web search with local document retrieval for grounded answers.
- Samples: Research Assistant Agent: complete demo showcasing ReAct planning with web search.
- Connect to MCP Servers: integrate external tool servers via the Model Context Protocol.