用 C# + Blazor 开发智能体(Agent)教程 – known

用 C# + Blazor 开发智能体(Agent)教程 – known


Applicable people: Developers who are familiar with C#/ASP.NET Core and want to make AI applications that “can really work”.
Technology stack: .NET 10 (.NET 9 is also available) + Blazor Web App (Interactive Server) + Microsoft.Extensions.AI.
The final result: an agent that can talk in the browser, call its own tools (check orders, do math, check the time) and stream the reasoning process.

1. Let’s be clear first: the agent is not a “chat box”

Many people call “connecting to OpenAI’s chat page” an agent, but in fact, only 20% of the process is completed. real agent = Model+Tool+Loop+State。

ability General chat application agent
Call large model ✅ ✅
Remember multiple rounds of context Optional ✅
Call external systems (database, API) ❌ ✅ Through tools (Function Calling)
Make your own decision on “what to do next” ❌ ✅ Powered by loops
Self-correction (retry after the tool reports an error) ❌ ✅ Backfill error results to the model

The core loop looks like this:

          ┌────────────────────────────────────────────┐
          │                                            │
用户提问 ──► LLM 推理 ──► 输出「文字 + 工具调用请求」──┐   │
                          │                          │   │
                          ├─ 没有工具调用 → 直接回答 ──┘   │
                          │                              │
                          └─ 有工具调用 → 执行工具 ────────┘
                                          │
                                    结果回填给 LLM
                                    (下一轮推理)

Remember this sentence:The “intelligence” of the agent comes from the model, but the “ability to work” comes from your code. The large model is only responsible for decision-making. What actually queries orders, writes databases, and sends emails are the tools and methods you registered. This is why C# backend developers have a natural advantage in building intelligent agents – your business code is inherently a tool.


2. Technology selection: Blazor three modes and model access layer

2.1 How to choose among the three hosting modes of Blazor

model Code running location Is the API Key exposed? First screen speed Long task support Recommended scenarios
Interactive Server Server (SignalR long connection) No✅ quick Good✅ The first choice for intelligent agents
WebAssembly Browser will be exposed ⚠️ Slow (need to download wasm) Restricted by browser Pure front-end/offline
Auto Server first, then WASM Exposed after downgrade medium generally mixed site

Conclusion: Agent applications should always use Interactive Server first. There are three reasons:

  1. The API Key remains on the server forever and will not be packaged into the browser;
  2. Streaming output goes directly to the server and is not restricted by browser CORS;
  3. Tools can read and write databases, so naturally they should be executed on the backend.

If you really want to do a pure front-end, you must also set up your own BFF (Backend for Frontend) relay, and do not directly connect the model in WASM.

2.2 Model access layer: choose one from three

plan advantage shortcoming when to use
Native HttpClient Completely controllable, zero dependence You need to write your own SSE parsing and tool calling protocols Learning protocol principles
Microsoft.Extensions.AI unified IChatClient Abstraction, built-in tool calling and streaming, switching vendors only change one line abstraction thinner Choose it for this tutorial and also recommend it for business projects
Semantic Kernel Complete plug-in, planner, and multi-Agent framework Many concepts, fast version iteration Requires complex orchestration or existing SK assets

Microsoft.Extensions.AI There are only four core objects, let’s get familiar with them first:

type effect
IChatClient The only way to talk to models
ChatMessage a message (ChatRole.System/User/Assistant/Tool)
ChatOptions Temperature, tool list, maximum token, etc.
AIFunction / AITool A method that can be called by the model

3. Environment preparation

# 1) 检查 SDK(需要 .NET 9 或 10)
dotnet --list-sdks

# 2) 准备一个模型 API Key(OpenAI / Azure OpenAI / DeepSeek / 通义 / 本地 Ollama 均可)
#    本教程所有示例都走「OpenAI 兼容协议」,大多数厂商都支持

Also required:

  • Editor: Visual Studio 2026 (or 2022 17.12+) / VS Code + C# Dev Kit;
  • One can get through Key.

Local models also work: Execute after installing Ollama ollama pull qwen2.5:14bthe interface address is used . Note that model tool calls below 7B are often unstable. It is recommended to use at least 14B for tool-type agents.


4. Create project

dotnet new blazor -n AgentStudio -int Server
cd AgentStudio

dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI

Microsoft.Extensions.AI If it is still a preview version, add --prerelease。

In the generated directory, delete the ones that come with the template first. Counter、Weather Example (optional), then add new files as shown below:

AgentStudio/
├── Program.cs                     ← 注册服务、配置模型
├── appsettings.json               ← 非机密配置
├── Models/
│   └── AgentChunk.cs              ← 流式输出的数据块
├── Services/
│   ├── OrderRepository.cs         ← 模拟业务数据(换成你的 DbContext 即可)
│   ├── AgentTools.cs              ← 工具集合
│   └── AgentService.cs            ← 智能体主循环
├── Components/
│   ├── App.razor
│   ├── Routes.razor
│   ├── Layout/
│   │   └── NavMenu.razor          ← 加一个「智能体」菜单项
│   └── Pages/
│       └── Chat.razor             ← 对话页面
└── wwwroot/
    └── app.css                    ← 追加聊天样式

5. Configure model access

5.1 Configuration file (without key)

appsettings.json:

{
  "AI": {
    "Endpoint": "
    "ModelId": "gpt-4o-mini",
    "MaxIterations": 6,
    "TimeoutSeconds": 180
  }
}

key away user-secretsnever enter the code base (SVN also needs to add ignore):

dotnet user-secrets init
dotnet user-secrets set "AI:ApiKey" "sk-你的真实密钥"

Comparison of configurations from different manufacturers:

Serve Endpoint ModelId example ApiKey
OpenAI gpt-4o-mini Official Key
DeepSeek deepseek-chat Official Key
Tongyi Qianwen (compatibility mode) qwen-plus Ali Key
Ollama (local) qwen2.5:14b Fill in whatever you want, such as ollama

5.2 Program.cs

using AgentStudio.Services;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;

var builder = WebApplication.CreateBuilder(args);

// Blazor 交互式服务端
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

// ---- 模型客户端(OpenAI 兼容协议)----
var aiSection = builder.Configuration.GetSection("AI");
var apiKey = aiSection["ApiKey"]
    ?? throw new InvalidOperationException("缺少 AI:ApiKey,请用 user-secrets 配置");
var endpoint = aiSection["Endpoint"] ?? "";
var modelId = aiSection["ModelId"] ?? "gpt-4o-mini";

builder.Services.AddChatClient(_ =>
{
    var client = new OpenAIClient(
        new ApiKeyCredential(apiKey),
        new OpenAIClientOptions { Endpoint = new Uri(endpoint) });

    return client.GetChatClient(modelId).AsIChatClient();
});

// ---- 业务服务 ----
builder.Services.AddSingleton();
builder.Services.AddSingleton();   // 工具无状态,单例即可
builder.Services.AddScoped();    // 每个用户连接一个实例

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();                       // .NET 9+;.NET 8 用 app.UseStaticFiles()
app.MapRazorComponents()
    .AddInteractiveServerRenderMode();

app.Run();

How to write Azure OpenAI: Replace with new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey)).GetChatClient(deploymentName).AsIChatClient(),ModelId Fill in the “deployment name” instead of the model name. This is the easiest pitfall for novices.

Notice:AddChatClient Registered isstatelessclient. Conversation memory should be in AgentService / Maintain the components yourself, don’t expect the client to remember them for you.


6. Define tools (make the model hands-on)

Tools are just plain C# methods.AIFunctionFactory will read itMethod name, parameter name, parameter type, Descriptionautomatically generate JSON Schema and hand it to the model.

6.1 Simulate business data

Services/OrderRepository.cs:

namespace AgentStudio.Services;

public sealed record Order(
    string Id,
    string Customer,
    string Status,
    decimal Amount,
    DateTimeOffset CreatedAt);

public sealed class OrderRepository
{
    private readonly Dictionary _orders = new(StringComparer.OrdinalIgnoreCase)
    {
        ["A1001"] = new("A1001", "张三", "已发货", 299.00m, DateTimeOffset.Now.AddDays(-2)),
        ["A1002"] = new("A1002", "李四", "待付款", 88.50m, DateTimeOffset.Now.AddHours(-3)),
        ["B2001"] = new("B2001", "王五", "已退款", 1299.00m, DateTimeOffset.Now.AddDays(-15)),
    };

    public Order? Find(string orderId) =>
        _orders.TryGetValue(orderId.Trim(), out var order) ? order : null;
}

6.2 Tool Collection

Services/AgentTools.cs:

using Microsoft.Extensions.AI;
using System.ComponentModel;

namespace AgentStudio.Services;

public sealed class AgentTools(OrderRepository orders)
{
    /// 一次性构建全部工具,避免每次请求重复反射。
    public IList CreateTools() =>
    [
        AIFunctionFactory.Create(
            GetCurrentTime,
            "get_current_time",
            "获取服务器当前的日期、时间与时区。当用户问“现在几点”“今天几号”时调用。"),

        AIFunctionFactory.Create(
            QueryOrder,
            "query_order",
            "根据订单号查询订单的当前状态、金额与下单时间。当用户提供订单号并询问进度、发货、退款时调用。"),

        AIFunctionFactory.Create(
            Calculate,
            "calculate",
            "对两个数字执行四则运算。参数 op 只能是 + - * / 之一。"),
    ];

    [Description("获取当前时间,返回 yyyy-MM-dd HH:mm:ss 格式字符串。")]
    private string GetCurrentTime() =>
        DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz");

    [Description("按订单号查询订单。找不到时返回未找到提示,此时不要编造结果。")]
    private string QueryOrder(
        [Description("订单号,例如 A1001")] string orderId)
    {
        var order = orders.Find(orderId);
        return order is null
            ? $"未找到订单 {orderId}。请向用户确认单号是否正确。"
            : $"订单 {order.Id}:客户={order.Customer},状态={order.Status}," +
              $"金额={order.Amount:0.00} 元,下单于 {order.CreatedAt:yyyy-MM-dd HH:mm}";
    }

    [Description("执行四则运算并返回结果。")]
    private double Calculate(
        [Description("第一个数字")] double a,
        [Description("第二个数字")] double b,
        [Description("运算符,只能是 + - * /")] string op) => op switch
        {
            "+" => a + b,
            "-" => a - b,
            "*" => a * b,
            "/" when b != 0 => a / b,
            _ => double.NaN,
        };
}

6.3 Four rigid requirements for writing tools

Require Counterexample Positive example
The name should be semantic DoQuery query_order
The description clearly states “when to call” "查询" "当用户提供订单号并询问进度、发货、退款时调用"
The return value should be short Return the entire entity object, more than ten KB JSON Only return fields required by the model, controlled within 1 KB
Errors are data too Throw an exception and make the request 500 return "未找到订单 X" Let the model correct itself

The last one is especially important. Exceptions thrown by the tool will not be seen by the model and will only interrupt your request. The correct approach is to wrap the error into a string backfill, and the model can often change the question or prompt the user for confirmation.


7. Implement the main loop of the agent

This is the core of the entire tutorial.Microsoft.Extensions.AI provided .UseFunctionInvocation() Middleware can automatically run a loop in one line; butIt is recommended to write it by hand firstotherwise you have no idea what happened when something goes wrong.

7.1 Streaming data blocks

Models/AgentChunk.cs:

namespace AgentStudio.Models;

public enum AgentChunkKind
{
    /// 模型输出的正文增量。
    Text,
    /// 模型发起了一次工具调用。
    ToolCall,
    /// 工具执行完成并返回了结果。
    ToolResult,
}

/// 传给 UI 的最小单位,Text/Assistant 正文走流式,工具事件走侧栏。
public readonly record struct AgentChunk(AgentChunkKind Kind, string Value)
{
    public static AgentChunk Text(string text) => new(AgentChunkKind.Text, text);
    public static AgentChunk ToolCall(string name) => new(AgentChunkKind.ToolCall, name);
    public static AgentChunk ToolResult(string text) => new(AgentChunkKind.ToolResult, text);
}

7.2 AgentService

Services/AgentService.cs:

using AgentStudio.Models;
using Microsoft.Extensions.AI;
using System.Runtime.CompilerServices;

namespace AgentStudio.Services;

public sealed class AgentService(
    IChatClient chatClient,
    AgentTools tools,
    IConfiguration configuration,
    ILogger logger)
{
    private readonly int _maxIterations = configuration.GetValue("AI:MaxIterations", 6);

    /// 
    /// 跑一轮「模型 ⇄ 工具」循环,边跑边把增量交给 UI。
    /// history 由调用方负责拼接(系统提示 + 历史消息 + 本轮用户消息)。
    /// 
    public async IAsyncEnumerable RunAsync(
        IReadOnlyList history,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        var messages = new List(history);
        var availableTools = tools.CreateTools();
        var options = new ChatOptions
        {
            Temperature = 0.3f,
            Tools = availableTools,
        };

        for (var iteration = 0; iteration < _maxIterations; iteration++)
        {
            var turnContents = new List();  // 本轮模型产出的所有内容
            var pendingCalls = new List();

            // ---- ① 请求模型(流式)----
            await foreach (var update in chatClient.GetStreamingResponseAsync(
                               messages, options, cancellationToken))
            {
                foreach (var content in update.Contents)
                {
                    turnContents.Add(content);

                    switch (content)
                    {
                        case TextContent { Text.Length: > 0 } text:
                            yield return AgentChunk.Text(text.Text);
                            break;

                        case FunctionCallContent call:
                            pendingCalls.Add(call);
                            yield return AgentChunk.ToolCall(call.Name);
                            break;
                    }
                }
            }

            // ---- ② 把模型这一轮的输出写回历史(含工具调用意图)----
            messages.Add(new ChatMessage(ChatRole.Assistant, turnContents));

            if (pendingCalls.Count == 0)
            {
                yield break;   // 模型选择直接回答,本轮结束
            }

            // ---- ③ 执行工具,把结果写回历史 ----
            var toolResults = new List();
            foreach (var call in pendingCalls)
            {
                cancellationToken.ThrowIfCancellationRequested();

                var function = availableTools
                    .OfType()
                    .FirstOrDefault(f => f.Name == call.Name);

                var result = function is null
                    ? $"错误:不存在名为 {call.Name} 的工具,请改用可用工具或直接回答。"
                    : await InvokeSafelyAsync(function, call, cancellationToken);

                toolResults.Add(new FunctionResultContent(call.CallId, result));
                yield return AgentChunk.ToolResult($"{call.Name} → {result}");
            }

            messages.Add(new ChatMessage(ChatRole.Tool, toolResults));

            // 循环回到 ①,模型带着工具结果继续推理
        }

        logger.LogWarning("达到最大工具调用轮数 {Max},强制收尾", _maxIterations);
        yield return AgentChunk.Text("\n\n(本轮工具调用次数已达上限,以上是目前能确定的信息。)");
    }

    private async Task InvokeSafelyAsync(
        AIFunction function,
        FunctionCallContent call,
        CancellationToken cancellationToken)
    {
        try
        {
            var arguments = new AIFunctionArguments(call.Arguments);
            var value = await function.InvokeAsync(arguments, cancellationToken);
            var text = value?.ToString() ?? "";

            // 工具返回值会占用上下文窗口,做一次硬截断
            return text.Length <= 2000 ? text : text[..2000] + "…(已截断)";
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "工具 {Tool} 执行失败", function.Name);
            return $"工具执行失败:{ex.Message},请换一种方式或告知用户稍后再试。";
        }
    }
}

7.3 Why are these three insurances necessary?

Insurance What will happen if you don’t add it? code location
Maximum number of iterations The model calls tools repeatedly, burning money in an endless loop for (var iteration = 0; ...)
CancellationToken The request is still running after the user closes the page, and the server burns the token in vain. Parameters + checks per round
Tools are abnormal A tool reported an error, and the entire conversation was 500 InvokeSafelyAsync

Choose one of two implementation methods: The above is a handwritten loop. If you only want the results, you can Program.cs of AddChatClient(...) chain at the back .UseFunctionInvocation()it will automatically execute the tool for you and try again.Do not use both at the same timeotherwise the tool will be executed twice.


8. Blazor chat interface and streaming output

8.1 Page components

Components/Pages/Chat.razor:

@page "/chat"
@using AgentStudio.Models
@using Microsoft.Extensions.AI
@inject AgentService Agent
@implements IDisposable

智能体对话

@if (_messages.Count == 0) {

试试问我:A1001 这个订单现在什么状态?

} @foreach (var msg in _messages) {
@if (msg.Trace.Count > 0) {
工具调用 @msg.Trace.Count 次
    @foreach (var line in msg.Trace) {
  • @line
  • }
}

@msg.Text

}