Create an AI Agent with Tools
An AI agent goes beyond simple text generation: it can reason about a problem, decide which tools to call, interpret the results, and iterate until it has a complete answer. This tutorial builds an agent that uses web search, a calculator, and a custom tool you write yourself.
What You Will Build
A research assistant that can:
- Search the web for current information (using DuckDuckGo, no API key needed).
- Perform calculations when the user's question involves math.
- Call a custom tool you define, demonstrating the
IToolinterface.
Prerequisites
| Requirement | Minimum |
|---|---|
| .NET SDK | 8.0+ |
| VRAM | 4+ GB (for a 4B model with tool-calling support) |
You need a model that supports tool calling. Check model.HasToolCalls after loading. Recommended: qwen3:4b or gemma3:4b.
Step 1: Create the Project
dotnet new console -n AgentQuickstart
cd AgentQuickstart
dotnet add package LM-Kit.NET
Step 2: Understand the Agent Architecture
┌──────────────────────────────────────────────────────┐
│ Agent │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Identity │ │ Planning │ │ Tools │ │
│ │ (persona) │ │ (ReAct) │ │ (registry) │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────┬───────────────────────────────┘
│ Run("user query")
▼
┌──────────────────┐
│ Thought-Action │ ◄── ReAct loop
│ Observation │
│ ... repeat ... │
│ Final Answer │
└──────────────────┘
| Component | Purpose |
|---|---|
Agent.CreateBuilder() |
Fluent API to configure the agent |
PlanningStrategy |
How the agent reasons (None, ReAct, ChainOfThought, etc.) |
ToolRegistry |
Collection of tools the agent can call |
BuiltInTools |
Pre-built tools: WebSearch, Calculator, DateTime, Http, and 50+ more |
ITool |
Interface for custom tools |
AgentExecutionResult |
The agent's response, including tool call history |
Step 3: Agent with Built-In Tools
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:4b",
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
var agent = Agent.CreateBuilder(model)
.WithPersona("Research Assistant")
.WithInstruction("You help users find information and answer questions accurately. " +
"Use tools when you need current data or calculations. " +
"Cite your sources when using web search.")
.WithPlanning(PlanningStrategy.ReAct)
.WithTools(tools =>
{
tools.Register(BuiltInTools.WebSearch); // DuckDuckGo, no API key
tools.Register(BuiltInTools.Calculator);
tools.Register(BuiltInTools.DateTime);
})
.WithMaxIterations(10)
.Build();
// 3. Interactive loop
Console.WriteLine("Agent ready. Type 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;
var result = await agent.RunAsync(input);
if (result.IsSuccess)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"\nAssistant: {result.Content}");
Console.ResetColor();
// Show tool usage
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, Inferences: {result.InferenceCount}");
Console.ResetColor();
}
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"\nError: {result.Error?.Message}");
Console.ResetColor();
}
Console.WriteLine();
}
Step 4: Add a Custom Tool
Implement the ITool interface to give the agent any capability you want. Here is a tool that looks up stock-like information (simulated):
using System.Text.Json;
using LMKit.Agents.Tools;
public class CompanyLookupTool : ITool
{
public string Name => "lookup_company";
public string Description => "Look up key information about a company by ticker symbol";
public string InputSchema => """
{
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Stock ticker symbol (e.g. MSFT, AAPL)"
}
},
"required": ["ticker"]
}
""";
public Task<string> InvokeAsync(string arguments, CancellationToken cancellationToken = default)
{
var args = JsonDocument.Parse(arguments);
string ticker = args.RootElement.GetProperty("ticker").GetString()!.ToUpperInvariant();
// In a real app, call a financial data API here
var data = ticker switch
{
"MSFT" => new { name = "Microsoft Corp", sector = "Technology", employees = 228000 },
"AAPL" => new { name = "Apple Inc", sector = "Technology", employees = 164000 },
_ => new { name = "Unknown", sector = "N/A", employees = 0 }
};
return Task.FromResult(JsonSerializer.Serialize(data));
}
}
Register it alongside built-in tools:
.WithTools(tools =>
{
tools.Register(BuiltInTools.WebSearch);
tools.Register(BuiltInTools.Calculator);
tools.Register(new CompanyLookupTool());
})
The agent will now call lookup_company when a question involves company information, and fall back to web search for everything else.
Step 5: Alternative: Attribute-Based Tools
For simpler tools, use LMFunctionAttribute instead of implementing ITool:
using System.Text.Json;
using LMKit.Agents.Tools;
public class UtilityTools
{
[LMFunction("convert_temperature", "Convert temperature between Celsius and Fahrenheit")]
public string ConvertTemperature(double value, string from_unit)
{
double result = from_unit.ToLower() switch
{
"celsius" => value * 9.0 / 5.0 + 32,
"fahrenheit" => (value - 32) * 5.0 / 9.0,
_ => throw new ArgumentException($"Unknown unit: {from_unit}")
};
string toUnit = from_unit.ToLower() == "celsius" ? "fahrenheit" : "celsius";
return JsonSerializer.Serialize(new { result, unit = toUnit });
}
[LMFunction("word_count", "Count words in a text")]
public int WordCount(string text) => text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}
Register the class (all annotated methods are discovered automatically):
.WithTools(tools =>
{
tools.Register(new UtilityTools()); // registers both tools
})
Planning Strategies
The planning strategy controls how the agent reasons between tool calls.
| Strategy | Behavior | Best For |
|---|---|---|
None |
Direct response, no explicit reasoning | Simple Q&A, fast responses |
ReAct |
Thought → Action → Observation loop | Tool-using agents (recommended default) |
ChainOfThought |
Step-by-step reasoning before answering | Math, logic, multi-step analysis |
PlanAndExecute |
Generate full plan, then execute steps | Complex multi-step tasks |
Reflection |
Generate, self-critique, refine | Tasks requiring accuracy |
TreeOfThought |
Explore multiple reasoning paths | Problems with multiple solution approaches |
Start with ReAct for any agent that uses tools. It provides the reasoning/action loop that tool-calling agents need.
Configuring Web Search Providers
The default BuiltInTools.WebSearch uses DuckDuckGo (free, no API key). For higher-quality results:
using LMKit.Agents.Tools.BuiltIn.Net;
// Brave Search (free tier: 2,000 queries/month)
var braveSearch = BuiltInTools.CreateWebSearch(
WebSearchTool.Provider.Brave,
"your-brave-api-key"
);
// Tavily (optimized for AI/RAG workloads)
var tavilySearch = BuiltInTools.CreateWebSearch(
WebSearchTool.Provider.Tavily,
Environment.GetEnvironmentVariable("TAVILY_API_KEY")
);
// Custom options
var customSearch = BuiltInTools.CreateWebSearch(new WebSearchTool.Options
{
SearchProvider = WebSearchTool.Provider.Brave,
ApiKey = "your-key",
DefaultMaxResults = 10,
Timeout = TimeSpan.FromSeconds(30)
});
Controlling Agent Behavior
MaxIterations
Caps how many reasoning + tool-call cycles the agent can perform before returning:
.WithMaxIterations(15) // default: 10
If the agent hits the limit, it returns whatever partial answer it has.
Inspecting the Reasoning Trace
var result = await agent.RunAsync("What is the population of France divided by 3?");
// See the agent's internal reasoning (Thought/Action/Observation steps)
if (!string.IsNullOrEmpty(result.ReasoningTrace))
Console.WriteLine($"Reasoning:\n{result.ReasoningTrace}");
// See each tool call
foreach (var call in result.ToolCalls)
Console.WriteLine($" Tool: {call.ToolName}, Status: {call.Type}, Result: {call.ResultJson}");
Common Issues
| Problem | Cause | Fix |
|---|---|---|
| Agent never calls tools | Model doesn't support tool calling | Check model.HasToolCalls. Use qwen3:4b or gemma3:4b |
| Agent loops without making progress | Planning strategy mismatch | Use ReAct for tool-using agents. Increase MaxIterations if task is complex |
| Web search returns empty | DuckDuckGo rate-limiting | Switch to Brave or Tavily provider, or add a delay between queries |
| Custom tool errors silently | InvokeAsync throws exception |
The agent sees the error and may retry. Log exceptions inside your tool |
Next Steps
- Extract Structured Data from Unstructured Text: schema-driven data extraction.
- Samples: Research Assistant Agent: full ReAct agent with web search.
- Samples: Tool Calling Assistant: custom tool implementation demo.
- Samples: Smart Task Router: multi-agent delegation with SupervisorOrchestrator.