在管理系统中引入 AI,最常见的需求就是"自然语言查询数据、执行操作"。本文以 EOMS 系统的 AI Agent 模块为样本,完整解析如何在 .NET 10 项目中集成大模型,从底层 ChatClient 配置到 Agent 编排器、工具系统、会话管理,再到前端 SignalR 流式对话的全链路实现。

一、架构全景
整个 AI Agent 模块分为四层:
前端 Chat.vue ──→ REST API / SignalR AgentHub
│
AgentOrchestrator(编排器)
│
┌────┴────┐
ChatClient IAgentToolProvider
(OpenAI/DeepSeek) ├─ SystemTools
├─ QtzTools
└─ HelloTools
项目通过 COM.HS.Agent 类库封装了所有 Agent 相关示例逻辑。
COM.HS.Agent/
├── Agent/
│ └── AgentOrchestrator.cs # Agent 编排器
├── Extensions/
│ ├── AgentServiceExtension.cs # DI 注册
│ └── ChatClientFactory.cs # AI 客户端工厂
├── Session/
│ ├── AgentSession.cs # 会话实体
│ └── AgentSessionManager.cs # 会话管理器
├── Tools/
│ ├── HelloAgentTools.cs # 验证工具
│ ├── SystemAgentTools.cs # 系统管理工具
│ └── QtzAgentTools.cs # 定时任务工具
└── IAgentToolProvider.cs # 工具提供者接口
二、AI 客户端:支持 DeepSeek / OpenAI 兼容协议
系统需要对接不同的大模型供应商——在实际项目中,可能使用 DeepSeek、通义千问、智谱等国产模型。这些模型大多数兼容 OpenAI 的协议格式。
2.1 ChatClientFactory
ChatClientFactory 支持 OpenAI 原生 SDK 的 ChatClient,支持完整工具调用(Function Calling):
public static class ChatClientFactory
{
public static ChatClient CreateNativeChatClient(IConfigurationSection aiConfig)
{
var endpoint = aiConfig["Endpoint"];
var apiKey = aiConfig["ApiKey"];
var model = aiConfig["Model"];
var credential = new ApiKeyCredential(apiKey);
var client = string.IsNullOrWhiteSpace(endpoint)
? new OpenAIClient(credential) // 默认 OpenAI
: new OpenAIClient(credential, new OpenAIClientOptions
{
Endpoint = new Uri(endpoint.TrimEnd('/') + "/") // 自定义端点
});
return client.GetChatClient(model);
}
}
如果 endpoint 为空,连接 OpenAI 官方 API;如果配置了 endpoint(如 https://api.deepseek.com/),则连接 DeepSeek 或其他兼容协议的服务。一套代码,多模型兼容。
同时还会创建一个 IChatClient 实例(MEAI 抽象层),供通用场景使用:
services.TryAddSingleton<IChatClient>(sp =>
{
var native = sp.GetRequiredService<ChatClient>();
return native.AsIChatClient(); // OpenAI SDK → MEAI 标准接口
});
2.2 配置
在 appsettings.json 中只需要几行配置:
"AI": {
"Provider": "DeepSeek",
"Endpoint": "https://api.deepseek.com/",
"ApiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Model": "deepseek-v4-flash"
}
更换模型供应商时,只需改 Endpoint 和 Model,代码不需要任何修改。
三、Agent 编排器(AgentOrchestrator)
编排器是整个 AI Agent 的核心,封装了 AIAgent 的创建、工具注入和会话上下文管理。
3.1 核心逻辑
public sealed class AgentOrchestrator
{
private readonly IEnumerable<IAgentToolProvider> _toolProviders;
private readonly ChatSessionManager _sessionManager;
private readonly IChatClient _chatClient;
// 缓存 AIAgent 实例(单例即可,不含会话状态)
private AIAgent? _cachedAgent;
private readonly object _lock = new();
// ...
}
四个关键依赖:工具提供者集合、会话管理器、AI 客户端、缓存 Agent 实例。
3.2 线程安全的 Agent 创建
private AIAgent GetAgent()
{
if (_cachedAgent != null) return _cachedAgent;
lock (_lock) // 双重检查锁定,确保线程安全
{
if (_cachedAgent != null) return _cachedAgent;
var tools = _toolProviders
.SelectMany(p => p.GetTools()) // 合并所有工具
.ToArray();
_cachedAgent = _chatClient.AsAIAgent(
instructions: """
你是企业运维管理平台 EOMS 的 AI 智能助手。
能力范围:
- 系统管理:查询用户、角色、部门、API 接口
- 人力资源管理:请假申请、查询请假记录
- 财务管理:报销查询、提交报销
- 工作流引擎:查询待审批任务、审批通过/驳回、查询流程进度
- 定时任务:查询任务状态、运行/停止任务
回答规则:
- 有可用的工具时,必须优先使用工具获取数据,不要编造信息
- 对于不确定的问题,坦诚表示无法回答
- 回答简洁专业
- 请使用中文回复
""",
name: "EOMSAgent",
tools: tools); // 工具注入
return _cachedAgent;
}
}
这部分有几个关键设计点:
- 双重检查锁定(Double-Checked Locking):
AIAgent实例本身不持有会话状态,可以全局共享,因此用lock做线程安全的懒加载 SelectMany 合并工具:所有实现了IAgentToolProvider的类,它们的工具被合并到一个数组注入到 Agent 中。新增一个工具类,自动注册、自动合并- System Prompt(instructions):定义了 AI 的角色定位、能力范围和回答规则。"有可用的工具时,必须优先使用工具获取数据,不要编造信息"——这句规则正是避免 LLM 幻觉的关键
3.3 非流式对话(REST API)
public async Task<string> ChatAsync(string sessionId, string message, string userId)
{
var session = await _sessionManager.GetOrCreateAsync(sessionId, userId);
var mafSession = await GetMafSessionAsync(session); // 获取 MAF 会话
var agent = GetAgent();
var response = await agent.RunAsync(message, mafSession);
return response.Text;
}
3.4 流式对话(SignalR)
public async IAsyncEnumerable<string> ChatStreamingAsync(string sessionId, string message, string userId)
{
var session = await _sessionManager.GetOrCreateAsync(sessionId, userId);
var mafSession = await GetMafSessionAsync(session);
var agent = GetAgent();
await foreach (var update in agent.RunStreamingAsync(message, mafSession))
{
yield return update.Text ?? string.Empty; // 逐块返回
}
}
四、工具系统(IAgentToolProvider)
这是 Agent 与业务系统对接的核心接口。
4.1 接口定义
public interface IAgentToolProvider
{
/// <summary>获取工具定义集合</summary>
IEnumerable<AIFunction> GetTools();
}
接口极其精简——只要求实现 GetTools() 方法,返回 AIFunction 集合。AIFunction 是 MEAI(Microsoft.Extensions.AI)标准类型,通过 AIFunctionFactory.Create 从方法引用自动生成。
4.2 自动发现与注册
工具提供者的注册使用了 Scrutor(一个程序集扫描库):
// AgentServiceExtension.cs
services.Scan(scan => scan
.FromAssembliesOf(typeof(IAgentToolProvider))
.AddClasses(classes => classes.AssignableTo(typeof(IAgentToolProvider)))
.As<IAgentToolProvider>()
.WithScopedLifetime());
这意味着:所有实现了 IAgentToolProvider 的类,都会被自动扫描并注册到 DI 容器。新增一个工具类,只需要在新的文件中实现接口,不需要在注册代码中显式添加一行。
4.3 SystemAgentTools:系统管理工具
public sealed class SystemAgentTools : IAgentToolProvider
{
private readonly IUserService _userService;
private readonly IRoleService _roleService;
private readonly IDeptService _deptService;
private readonly IApiService _apiService;
// ...
public IEnumerable<AIFunction> GetTools()
{
yield return AIFunctionFactory.Create(
SearchUsersAsync,
name: "SearchUsers",
description: "搜索用户列表,支持按关键词、部门、启用状态筛选");
yield return AIFunctionFactory.Create(
GetUserDetailAsync,
name: "GetUserDetail",
description: "根据用户ID获取用户详细信息");
yield return AIFunctionFactory.Create(
GetRolesAsync,
name: "GetRoles",
description: "获取角色列表,可按关键词搜索");
yield return AIFunctionFactory.Create(
GetDeptTreeAsync,
name: "GetDeptTree",
description: "获取部门列表");
yield return AIFunctionFactory.Create(
GetApisAsync,
name: "GetApis",
description: "获取API接口列表,可按关键词搜索");
}
// ...
}
AIFunctionFactory.Create 通过反射自动解析方法的参数和返回值,构建 OpenAI Function Calling 所需的 JSON Schema。参数的 [Description] 特性会被自动提取,作为 Schema 的描述信息。
4.4 QtzAgentTools:定时任务工具
另一个工具提供者,管理 Quartz 定时任务的增删启停:
public sealed class QtzAgentTools : IAgentToolProvider
{
public IEnumerable<AIFunction> GetTools()
{
yield return AIFunctionFactory.Create(
GetTaskListAsync, name: "GetTaskList",
description: "查询定时任务列表,可按关键词搜索");
yield return AIFunctionFactory.Create(
RunTaskAsync, name: "RunTask",
description: "运行/启动一个定时任务");
yield return AIFunctionFactory.Create(
StopTaskAsync, name: "StopTask",
description: "停止一个正在运行的定时任务");
yield return AIFunctionFactory.Create(
RunTaskNowAsync, name: "RunTaskNow",
description: "立即执行一次定时任务,不修改调度计划");
}
}
以 RunTaskAsync 为例,AI 调用它的流程是:
用户问:帮我启动日志清理任务
↓
AI 分析意图 → 匹配 RunTask 工具
↓
提取参数 taskId → 从问题中推断
↓
调用 RunTaskAsync(taskId)
↓
后端启动 Quartz 触发器 → 更新数据库状态 → 返回结果
↓
AI 组织自然语言回复:已成功启动「日志清理」任务
4.5 HelloAgentTools:验证工具
一个轻量工具,用于验证 AI 链路是否正常联通:
public sealed class HelloAgentTools : IAgentToolProvider
{
public IEnumerable<AIFunction> GetTools()
{
yield return AIFunctionFactory.Create(
GetCurrentUser, name: "GetCurrentUser",
description: "获取当前登录用户的信息");
yield return AIFunctionFactory.Create(
GetCurrentTime, name: "GetCurrentTime",
description: "获取当前服务器时间");
}
[Description("返回当前登录用户的 UserId 和基本信息")]
public string GetCurrentUser()
{
return $"当前用户: {_user.UserId}, 姓名: {_user.UserName}";
}
[Description("返回当前服务器时间")]
public string GetCurrentTime()
{
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
}
这两个工具不需要依赖任何业务服务,在开发调试期用于快速定位问题:是 AI 连接不通还是工具执行报错。
五、会话管理(ChatSessionManager)
多轮对话需要上下文记忆。Agent 使用了 MAF(Microsoft.Agents.Framework)的 AgentSession 来维护对话历史。
5.1 会话实体
public class ChatSession
{
public string SessionId { get; set; }
public string UserId { get; set; }
public string? Title { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime LastActivityAt { get; set; }
public bool IsArchived { get; set; }
/// <summary>MAF 会话对象(持有对话历史)</summary>
public AgentSession? MafSession { get; set; }
}
5.2 会话管理器(内存实现)
public sealed class ChatSessionManager
{
private readonly IMemoryCache _cache;
private readonly TimeSpan _sessionTtl = TimeSpan.FromHours(2);
public Task<ChatSession> GetOrCreateAsync(string sessionId, string userId)
{
var cacheKey = $"agent_session_{sessionId}";
if (_cache.TryGetValue<ChatSession>(cacheKey, out var session) && session != null)
{
session.LastActivityAt = DateTime.UtcNow; // 续期
return Task.FromResult(session);
}
var newSession = new ChatSession { ... };
_cache.Set(cacheKey, newSession, _sessionTtl); // 2 小时过期
return Task.FromResult(newSession);
}
}
当前使用 IMemoryCache 存储会话,优点是零配置、无侵入。但注意注释提到"后续可扩展为 DB 持久化"——当会话数据量增大或需要跨服务共享时,只需将 IMemoryCache 替换为 Redis 或其他分布式缓存。
5.3 MAF 会话的懒加载
private async Task<AgentSession> GetMafSessionAsync(ChatSession session)
{
if (session.MafSession != null) // 已有 MAF 会话,直接复用
return session.MafSession;
var agent = GetAgent();
var mafSession = await agent.CreateSessionAsync(); // 创建新会话
session.MafSession = mafSession; // 缓存
return mafSession;
}
这个懒加载设计确保了:每个 ChatSession 在第一次调用时才创建对应的 MAF AgentSession。并且一旦创建,在整个会话生命周期内复用同一个 MAF 会话实例,从而保持对话上下文的连续性。
六、消息投递:REST API + SignalR 双通道
Agent 提供了两种交互方式:REST API 非流式 和 SignalR 流式。
6.1 REST API:AgentController
适用于"发送消息,一次性获取完整回复"的场景:
[Route("api/agent")]
[ApiController]
[Authorize]
public class AgentController : ControllerBase
{
[Route("chat")]
[HttpPost]
public async Task<ResultModel<string>> Chat([FromBody] AgentChatRequest request)
{
var sessionId = request.SessionId ?? Guid.NewGuid().ToString("N");
var result = await _orchestrator.ChatAsync(
sessionId, request.Message, _user.UserId);
return new ResultModel<string>(true, result);
}
[Route("session")]
[HttpGet]
public ResultModel<string> NewSession()
{
return new ResultModel<string>(true, Guid.NewGuid().ToString("N"));
}
}
两个接口:/api/agent/chat 发起对话,/api/agent/session 获取新会话 ID。
6.2 SignalR 流式:AgentHub
适用于"逐字显示 AI 回复"的实时体验:
[Authorize]
public class AgentHub : Hub
{
public async Task Chat(string sessionId, string message)
{
var userId = Context.UserIdentifier ?? "anonymous";
try
{
await foreach (var chunk in _orchestrator.ChatStreamingAsync(sessionId, message, userId))
{
await Clients.Caller.SendAsync("AgentChunk", new
{
sessionId,
content = chunk,
isComplete = false
});
}
// 发送完成标记
await Clients.Caller.SendAsync("AgentChunk", new
{
sessionId,
content = string.Empty,
isComplete = true
});
}
catch (Exception ex)
{
await Clients.Caller.SendAsync("AgentChunk", new
{
sessionId,
content = $"错误: {ex.Message}",
isComplete = true,
isError = true
});
}
}
}
流式对话的实现使用了 C# 8 的 IAsyncEnumerable 配合 await foreach。AgentOrchestrator.ChatStreamingAsync 每次 yield return 一个文本块,AgentHub 立即通过 SignalR 推送给前端。
注意这里的前端调用方是 Clients.Caller——只推送给发起对话的当前用户,而不是广播给所有人。
七、前端 Chat.vue:完整对话界面
前端 AI 对话页面使用 Vue3 + SignalR(流式)+ REST(非流式)实现。
7.1 消息结构
// 消息对象结构
{
role: 'user' | 'assistant',
content: string, // 消息内容
isLoading: boolean, // 是否正在加载(流式时显示打字动画)
timestamp: number, // 时间戳
}
7.2 发送流程
async function handleSend() {
const text = inputMessage.value.trim();
if (!text || isLoading.value) return;
inputMessage.value = '';
if (!currentSessionId.value) await initSession();
// 1. 添加用户消息
messages.value.push({ role: 'user', content: text, timestamp: Date.now() });
// 2. 添加空的助手消息(显示打字动画)
const assistantMsg = { role: 'assistant', content: '', isLoading: true, timestamp: Date.now() };
messages.value.push(assistantMsg);
scrollToBottom();
// 3. 调用 REST API(非流式)
isLoading.value = true;
try {
const res = await chat({
message: text,
sessionId: currentSessionId.value,
});
assistantMsg.content = res.data?.data || '暂无回复';
} catch (err) {
assistantMsg.content = '网络请求异常:' + (err.message || '');
} finally {
assistantMsg.isLoading = false; // 移除打字动画
isLoading.value = false;
scrollToBottom();
}
}
当前版本使用的是 REST API(非流式),但界面已经为流式做好了准备——isLoading 控制打字动画,content 逐块追加即可转流式。
7.3 Markdown 渲染
AI 回复的内容是 Markdown 格式,通过 marked 库渲染为 HTML:
import { marked } from 'marked';
marked.setOptions({ breaks: true, gfm: true });
function renderMarkdown(content) {
if (!content) return '';
try {
return marked.parse(content);
} catch {
return content;
}
}
在模板中使用 v-html 渲染:
<div class="message-text markdown-body" v-html="renderMarkdown(msg.content)"></div>
页面 CSS 专门为 Markdown 输出做了全面适配,覆盖了 h1-h6、代码块(暗色背景)、表格、引用、列表、任务清单等几乎所有 Markdown 语法。代码块使用了深色背景 #1e293b,配合高亮配色。
7.4 快捷提问
用户首次打开对话时,页面展示 4 个快捷问题卡片:
<div class="quick-card" @click="sendQuick('查询定时任务列表')">
<span class="quick-label">查询定时任务</span>
</div>
<div class="quick-card" @click="sendQuick('列出所有角色')">
<span class="quick-label">列出所有角色</span>
</div>
<div class="quick-card" @click="sendQuick('列出所有部门')">
<span class="quick-label">列出所有部门</span>
</div>
<div class="quick-card" @click="sendQuick('现在几点了')">
<span class="quick-label">现在几点了</span>
</div>
其中"现在几点了"对应 HelloAgentTools.GetCurrentTime,"查询定时任务列表"对应 QtzAgentTools.GetTaskListAsync——这些快捷问题覆盖了不同工具提供者,方便测试验证。
八、DI 注册全景
全部基础设施在 AgentServiceExtension.AddAgent() 中完成:
public static IServiceCollection AddAgent(this IServices)
{
// 1. OpenAI SDK 原生 ChatClient(工具调用完整支持)
services.TryAddSingleton<ChatClient>(sp => {
var config = sp.GetRequiredService<IConfiguration>();
return ChatClientFactory.CreateNativeChatClient(config.GetSection("AI"));
});
// 2. MEAI IChatClient(通用层,基于原生 ChatClient 包装)
services.TryAddSingleton<IChatClient>(sp => {
var native = sp.GetRequiredService<ChatClient>();
return native.AsIChatClient();
});
// 3. AgentOrchestrator (Scoped)
services.TryAddScoped<AgentOrchestrator>();
// 4. 会话管理 (Scoped)
services.TryAddScoped<ChatSessionManager>();
// 5. 自动扫描注册所有 IAgentToolProvider
services.Scan(scan => scan
.FromAssembliesOf(typeof(IAgentToolProvider))
.AddClasses(classes => classes.AssignableTo(typeof(IAgentToolProvider)))
.As<IAgentToolProvider>()
.WithScopedLifetime());
return services;
}
在 Program.cs 中一行启用:
/* Agent */
builder.Services.AddAgent();
DI 的生命周期选择:
ChatClient/IChatClient:Singleton(全局单例,有连接池)AgentOrchestrator:Scoped(每个请求一个实例,内部的 Agent 缓存是线程安全的)ChatSessionManager:Scoped(注入 IMemoryCache,它是 Singleton,所以 Scoped 不影响缓存效果)IAgentToolProvider 实现:Scoped(它们注入的 Service 是 Scoped)
九、扩展一个新工具只需两步
以新加一个"人力资源管理"工具为例:
第一步:创建工具类
public sealed class HrAgentTools : IAgentToolProvider
{
private readonly ILeaveService _leaveService;
public IEnumerable<AIFunction> GetTools()
{
yield return AIFunctionFactory.Create(
GetLeaveBalanceAsync,
name: "GetLeaveBalance",
description: "查询当前员工的请假余额");
}
public async Task<string> GetLeaveBalanceAsync(string userId)
{
var balance = await _leaveService.GetBalanceAsync(userId);
return JsonSerializer.Serialize(balance);
}
}
第二步:注册到第一个工具提供者集合
由于 services.Scan 自动扫描了所有 IAgentToolProvider 的实现,新增的 HrAgentTools 会被自动发现和注册。不需要修改任何现有代码。
然后 AIAgent 在下次重建时(当前使用缓存,重启即生效)就会自动包含新工具。这就是依赖注入 + 程序集扫描带来的扩展性。
十、技术栈与依赖
Agent 模块的依赖清单:
<PackageReference Include="Microsoft.Agents.AI" Version="1.9.0" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.9.0" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.7.0" />
<PackageReference Include="OpenAI" Version="2.11.0" />
<PackageReference Include="Scrutor" Version="6.0.1" />
- Microsoft.Agents.AI:MAF 框架,提供
AIAgent、AgentSession等核心抽象 - Microsoft.Extensions.AI.OpenAI:MEAI 标准接口的 OpenAI 实现
- OpenAI SDK:底层的 OpenAI API 客户端(支持 Function Calling 的完整协议)
- Scrutor:程序集扫描,自动注册所有
IAgentToolProvider
十一、小结
| 设计维度 | 实现方案 | 核心价值 |
|---|---|---|
| AI 供应商 | ChatClientFactory 工厂 + appsettings.json 配置 | 切换模型只需改配置 |
| Agent 编排 | AgentOrchestrator + AIAgent | 统一管理工具注入 |
| System Prompt | instructions 字符串 | 定义角色、能力、回答规则 |
| 工具系统 | IAgentToolProvider + Scrutor 自动扫描 | 新增工具零代码侵入 |
| 工具注册 | AIFunctionFactory.Create 从方法生成 | 反射自动解析 JSON Schema |
| 会话管理 | ChatSessionManager + MAF AgentSession | 多轮上下文记忆 |
| 投递方式 | REST API + SignalR 双通道 | 流式/非流式按需切换 |
| 前端渲染 | marked Markdown 渲染 + 打字动画 | 用户体验流畅 |
| 自动发现 | Scrutor 程序集扫描 | 扩展工具实现即可 |
整个 Agent 集成的设计核心是接口标准化 + 自动发现:所有 AI 供应商通过统一的 ChatClient 接口接入,所有工具通过统一的 IAgentToolProvider 接口暴露,模块之间通过依赖注入组合。这使得添加一个新的 AI 能力,往往只需要"实现一个接口 + 写一个工具方法"两步。
欢迎关注 天草设计 公众号,领取 源代码 福利礼包!
