Net+AI智能体进阶7:自定义Agent
2025-11-15 18:12:48一、自定义 Agent 的实现
1. 场景分析
在大多数场景下,使用 chatClient.CreateAIAgent() 创建的标准 Agent 已经足够强大。但在某些特殊场景下,自定义 Agent 实现能带来更大的价值:
场景 1:规则引擎替代 AI(成本优化)
问题:客服系统每天处理数万次重复性问题("营业时间?","退货流程?"),每次调用 AI 模型都会产生成本。
解决方案:自定义 Agent 使用 FAQ 知识库进行关键词匹配,只在无法匹配时才调用 AI。
收益:
- 成本降低 70-90%(高频简单问题零成本)
响应速度提升 10 倍(无需等待 AI 推理)
答案一致性更高(预定义标准答案)
场景 2:遗留系统集成(ERP/CRM/工作流引擎)
问题:企业内部有成熟的审批工作流引擎,需要将其包装为 Agent 供统一调度。
解决方案:自定义 Agent 作为适配器,将工作流引擎的 API 转换为 Agent 接口。
收益:
无缝集成现有系统(无需重构)
复用企业级规则引擎(审批、权限、流程)
数据安全可控(不发送敏感数据到外部 AI)
场景 3:测试模拟(Mock Agent)
- 问题:开发和测试环境中,不希望调用真实 AI 模型(成本、稳定性、可预测性)。
- 解决方案:自定义 Agent 返回固定或可配置的测试数据。
- 收益:
- 单元测试更可靠(确定性输出)
- 开发环境零成本
- CI/CD 管道更快(无需等待 AI 响应)
场景 4:混合模式(规则 + AI)
- 问题:希望结合规则引擎的确定性和 AI 的灵活性。
- 解决方案:自定义 Agent 先尝试规则匹配,失败后转发给 AI Agent。
- 收益:
- 平衡成本与效果
- 灵活的分流策略(按优先级、置信度)
- 逐步优化规则库(分析 AI 处理的高频问题)
对比: 标准 Agent vs 自定义 Agent
| 特性 | ChatClientAgent(标准) | 自定义 Agent | 说明 |
|---|---|---|---|
| 创建方式 | chatClient.CreateAIAgent() | 继承 AIAgent 抽象类 | 标准方式更简单 |
| 开发复杂度 | 低 | 高 | 自定义需实现所有核心方法 |
| 灵活性 | 受限于 IChatClient 能力 | 完全可控 | 自定义可实现任意逻辑 |
| 成本 | 按 Token 计费 | 固定成本(计算资源) | 高频场景自定义更省钱 |
| 响应速度 | 取决于 AI 模型 | 毫秒级(无网络调用) | 规则引擎极快 |
| 确定性 | 不确定(AI 输出有随机性) | 完全确定 | 审批、计算等需要确定性 |
| 学习能力 | 有(通过 Prompt) | 无(纯代码逻辑) | AI 能处理未知场景 |
| 适用场景 | 复杂推理、开放式对话 | 规则匹配、系统集成、成本优化 | 各有所长 |
| 互操作性 | 完全兼容 MAF 生态 | 完全兼容 MAF 生态 | 都实现 AIAgent 接口 |
关键结论:自定义 Agent 不是替代标准 Agent,而是在特定场景下的补充方案。两者可以无缝组合使用。
2. AIAgent 抽象类核心接口详解
AIAgent 是 MAF 框架中所有 Agent 的基类,无论是标准的 ChatClientAgent 还是自定义 Agent,都必须实现此抽象类定义的核心接口。
必须实现的核心成员
public abstract class AIAgent
{
// 1. Agent 名称(用于标识和日志)
public abstract string? Name { get; }
// 2. 创建新的对话线程
public abstract AgentThread GetNewThread();
// 3. 从序列化数据恢复线程(支持持久化)
public abstract AgentThread DeserializeThread(
JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null);
// 4. 同步调用(核心执行逻辑)
public abstract Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
// 5. 流式调用(支持实时输出)
public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
}
可选成员(继承自基类)
- Id (string):Agent 唯一标识符(默认自动生成)
- DisplayName (string):显示名称(默认使用 Name)
- Description (string?):Agent 描述
- NotifyThreadOfNewMessagesAsync():通知线程有新消息(关键方法)
3. 自定义 Agent 流程
sequenceDiagram
participant User as 用户代码
participant Agent as 自定义 Agent
participant Thread as AgentThread
participant Logic as 自定义逻辑
User->>Agent: RunAsync(messages)
alt Thread 为 null
Agent->>Agent: GetNewThread()
Agent-->>Thread: 创建新线程
end
Agent->>Logic: 执行自定义逻辑
Note over Logic: 可能是:<br/>- 规则匹配<br/>- 数据库查询<br/>- API 调用<br/>- AI 推理
Logic->>Agent: 生成响应消息
Agent->>Agent: 设置消息属性
Note over Agent: Role = Assistant<br/>AuthorName = Agent.Name<br/>MessageId = Guid
Agent->>Thread: NotifyThreadOfNewMessagesAsync()
Note over Thread: 保存输入+输出消息<br/>到线程历史
Agent->>User: 返回 AgentRunResponse
关键要点
- 必须创建或使用提供的 Thread
- 响应消息必须设置正确的 Role、AuthorName、MessageId
- 必须调用 NotifyThreadOfNewMessagesAsync() 更新线程历史
- 返回的 AgentRunResponse 包含所有响应消息
4. 关键设计原则
消息转换原则:输入的 ChatMessage 通常是用户消息 (Role = User),Agent 需要:
- 创建新的响应消息(或克隆输入消息后修改)
设置 Role = ChatRole.Assistant
设置 AuthorName = this.DisplayName (标识消息来源)
设置 MessageId = Guid.NewGuid().ToString("N") (唯一标识)
线程通知原则:为什么必须调用 NotifyThreadOfNewMessagesAsync()?
维护对话历史(多轮对话的基础)
支持持久化(保存完整上下文)
可观测性(追踪消息流)
调用时机:在生成响应消息后,返回结果前。
传入的消息:输入消息 + 输出消息(使用 messages.Concat(responseMessages))
响应构造原则
// AgentRunResponse 结构
new AgentRunResponse
{
AgentId = this.Id, // Agent 标识
ResponseId = Guid.NewGuid().ToString("N"), // 本次响应标识
Messages = responseMessages // 响应消息列表
};
// AgentRunResponseUpdate 结构(流式)
new AgentRunResponseUpdate
{
AgentId = this.Id,
ResponseId = responseId, // 同一响应的多个 update 共享 ID
MessageId = messageId, // 同一消息的多个 update 共享 ID
AuthorName = this.DisplayName,
Role = ChatRole.Assistant,
Contents = [new TextContent(text)] // 增量内容
};
5. 最小化 Agent 骨架代码
internal sealed class CustomAgentThread : InMemoryAgentThread
{
internal CustomAgentThread() { }
internal CustomAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
: base(serializedThreadState, jsonSerializerOptions) { }
}
/// <summary>
/// 最小化的自定义 Agent 骨架
/// 功能:简单地将用户输入原样返回(Echo Agent)
/// </summary>
public class MinimalAgent : AIAgent
{
// 1. Agent 名称
public override string? Name => "MinimalAgent";
// 2. 创建新线程
public override AgentThread GetNewThread()
=> new CustomAgentThread(); // 使用内置的内存线程
// 3. 反序列化线程
public override AgentThread DeserializeThread(
JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null)
=> new CustomAgentThread(serializedThread, jsonSerializerOptions);
// 4. 同步执行
public override async Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
// 步骤 1: 确保有线程
thread ??= GetNewThread();
// 步骤 2: 提取最后一条用户消息
var lastUserMessage = messages.LastOrDefault();
var userText = lastUserMessage?.Text ?? "(空消息)";
// 步骤 3: 生成响应消息
var responseMessage = new ChatMessage(
ChatRole.Assistant,
$"Echo: {userText}"
)
{
AuthorName = this.DisplayName,
MessageId = Guid.NewGuid().ToString("N")
};
var responseMessages = new[] { responseMessage };
// 步骤 4: 通知线程(关键!)
await NotifyThreadOfNewMessagesAsync(
thread,
messages.Concat(responseMessages),
cancellationToken);
// 步骤 5: 返回响应
return new AgentRunResponse
{
AgentId = this.Id,
ResponseId = Guid.NewGuid().ToString("N"),
Messages = responseMessages
};
}
// 5. 流式执行
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
thread ??= GetNewThread();
var lastUserMessage = messages.LastOrDefault();
var userText = lastUserMessage?.Text ?? "(空消息)";
var echoText = $"Echo: {userText}";
var responseMessage = new ChatMessage(ChatRole.Assistant, echoText)
{
AuthorName = this.DisplayName,
MessageId = Guid.NewGuid().ToString("N")
};
await NotifyThreadOfNewMessagesAsync(
thread,
messages.Concat(new[] { responseMessage }),
cancellationToken);
// 流式输出:逐字符返回
var responseId = Guid.NewGuid().ToString("N");
var messageId = responseMessage.MessageId;
foreach (var character in echoText)
{
yield return new AgentRunResponseUpdate
{
AgentId = this.Id,
ResponseId = responseId,
MessageId = messageId,
AuthorName = this.DisplayName,
Role = ChatRole.Assistant,
Contents = [new TextContent(character.ToString())]
};
await Task.Delay(50, cancellationToken); // 模拟流式延迟
}
}
}
Console.WriteLine("MinimalAgent 定义完成");
测试
var minimalAgent = new MinimalAgent();
// 测试 1: 同步调用
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: 同步调用");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var response1 = await minimalAgent.RunAsync("你好,世界!");
new {
AgentId = response1.AgentId,
ResponseId = response1.ResponseId,
消息数量 = response1.Messages.Count,
响应内容 = response1.Messages.First().Text
}.Display();
// 测试 2: 流式调用
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 流式调用");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.Write("响应: ");
await foreach (var update in minimalAgent.RunStreamingAsync("流式测试"))
{
var text = update.Contents.OfType<TextContent>().FirstOrDefault()?.Text;
Console.Write(text);
}
Console.WriteLine("\n\n测试完成");
// 测试 3: 多轮对话(验证线程管理)
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 3: 多轮对话");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var thread = minimalAgent.GetNewThread();
// 第 1 轮
var response3_1 = await minimalAgent.RunAsync("第一条消息", thread);
Console.WriteLine($"第 1 轮: {response3_1.Messages.First().Text}");
// 第 2 轮
var response3_2 = await minimalAgent.RunAsync("第二条消息", thread);
Console.WriteLine($"第 2 轮: {response3_2.Messages.First().Text}");
var msgStore = thread.GetService<ChatMessageStore>();
var msgs = await msgStore.GetMessagesAsync();
// 检查线程中的消息历史
Console.WriteLine($"\n线程中的消息总数: {msgs.Count()}");
Console.WriteLine("\n消息历史:");
foreach (var msg in msgs)
{
Console.WriteLine($"[{msg.Role}] {msg.AuthorName}: {msg.Text}");
}
Console.WriteLine("\n多轮对话测试完成");
6. 核心要点总结
通过 MinimalAgent 示例,我们学到了:
- 必须实现的 5 个核心方法
- Name:返回 Agent 名称
- GetNewThread():创建新线程(通常使用 InMemoryAgentThread)
- DeserializeThread():从序列化数据恢复线程
- RunAsync():同步执行逻辑
- RunStreamingAsync():流式执行逻辑
- RunAsync 实现的 5 个步骤
flowchart LR
A[1. 确保线程存在] --> B[2. 提取用户消息]
B --> C[3. 生成响应消息]
C --> D[4. 设置消息属性]
D --> E[5. 通知线程]
E --> F[6. 返回响应]
style A fill:#e3f2fd
style E fill:#fff3e0
style F fill:#e8f5e9
消息属性检查清单
- Role = ChatRole.Assistant
- AuthorName = this.DisplayName
- MessageId = Guid.NewGuid().ToString("N")
- 调用 NotifyThreadOfNewMessagesAsync()
流式调用要点
使用 yield return 逐步返回 AgentRunResponseUpdate
同一响应的所有 update 共享同一个 ResponseId
同一消息的所有 update 共享同一个 MessageId
支持 [EnumeratorCancellation] 特性处理取消
二、实战:FAQ 自动回复 Agent
1. 场景描述:
企业客服系统每天收到数万次重复性咨询:
- "营业时间是什么?"
- "如何退货?"
- "配送需要多久?"
- "支持哪些支付方式?"
这些高频问题每次都调用 AI 模型不仅成本高昂,而且响应速度慢。我们的解决方案:使用自定义 Agent 实现 FAQ 知识库匹配,零成本、毫秒级响应!
实现目标
- 定义 FAQ 知识库(字典结构)
- 实现关键词匹配逻辑(简单但实用)
- 支持同步和流式调用
- 支持多轮对话
- 匹配失败时返回友好提示
2. 定义 FAQ 知识库
/// <summary>
/// FAQ 自动回复 Agent
/// 功能:通过关键词匹配返回预定义答案,零成本快速响应
/// </summary>
public class FaqAgent : AIAgent
{
// FAQ 知识库
private readonly Dictionary<string, string> _faqDatabase = new()
{
// 营业相关
["营业时间"] = "我们的营业时间是:\n- 周一至周五: 9:00-18:00\n- 周六日: 10:00-17:00\n- 节假日: 休息",
["工作时间"] = "我们的营业时间是:\n- 周一至周五: 9:00-18:00\n- 周六日: 10:00-17:00\n- 节假日: 休息",
// 退货相关
["退货"] = "退货流程:\n1. 登录账户,进入订单详情\n2. 点击\"申请退货\"\n3. 选择退货原因并上传照片\n4. 等待审核通过\n5. 打包商品并寄回\n6. 收到商品后 3-5 个工作日退款",
["退款"] = "退货流程:\n1. 登录账户,进入订单详情\n2. 点击\"申请退货\"\n3. 选择退货原因并上传照片\n4. 等待审核通过\n5. 打包商品并寄回\n6. 收到商品后 3-5 个工作日退款",
// 配送相关
["配送"] = "配送时效:\n- 同城: 24 小时内送达\n- 省内: 2-3 个工作日\n- 跨省: 3-5 个工作日\n- 偏远地区: 5-7 个工作日",
["快递"] = "配送时效:\n- 同城: 24 小时内送达\n- 省内: 2-3 个工作日\n- 跨省: 3-5 个工作日\n- 偏远地区: 5-7 个工作日",
["物流"] = "配送时效:\n- 同城: 24 小时内送达\n- 省内: 2-3 个工作日\n- 跨省: 3-5 个工作日\n- 偏远地区: 5-7 个工作日",
// 支付相关
["支付"] = "支持的支付方式:\n 微信支付\n 支付宝\n 银行卡\n 信用卡\n 货到付款(部分地区)",
["付款"] = "支持的支付方式:\n 微信支付\n 支付宝\n 银行卡\n 信用卡\n 货到付款(部分地区)",
// 联系方式
["联系"] = "联系我们:\n客服热线: 400-123-4567\n邮箱: service@example.com\n在线客服: 网站右下角聊天窗口",
["电话"] = "联系我们:\n客服热线: 400-123-4567\n邮箱: service@example.com\n💬 在线客服: 网站右下角聊天窗口",
["客服"] = "联系我们:\n客服热线: 400-123-4567\n邮箱: service@example.com\n💬 在线客服: 网站右下角聊天窗口"
};
public override string? Name => "FaqAgent";
public override AgentThread GetNewThread()
=> new CustomAgentThread();
public override AgentThread DeserializeThread(
JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null)
=> new CustomAgentThread(serializedThread, jsonSerializerOptions);
// 关键词匹配逻辑
private string? MatchFaq(string userInput)
{
// 遍历 FAQ 知识库,查找包含关键词的问题
foreach (var kvp in _faqDatabase)
{
if (userInput.Contains(kvp.Key, StringComparison.OrdinalIgnoreCase))
{
return kvp.Value;
}
}
return null; // 未匹配到
}
public override async Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
thread ??= GetNewThread();
// 提取最后一条用户消息
var lastUserMessage = messages.LastOrDefault();
var userText = lastUserMessage?.Text ?? "";
// 尝试匹配 FAQ
var answer = MatchFaq(userText);
// 构造响应内容
var responseText = answer ??
"抱歉,我没有找到相关的 FAQ。\n\n您可以:\n" +
"1.换个方式描述您的问题\n" +
"2.拨打客服热线: 400-123-4567\n" +
"3.发送邮件: service@example.com";
// 创建响应消息
var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
{
AuthorName = this.DisplayName,
MessageId = Guid.NewGuid().ToString("N")
};
var responseMessages = new[] { responseMessage };
// 通知线程
await NotifyThreadOfNewMessagesAsync(
thread,
messages.Concat(responseMessages),
cancellationToken);
return new AgentRunResponse
{
AgentId = this.Id,
ResponseId = Guid.NewGuid().ToString("N"),
Messages = responseMessages
};
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
thread ??= GetNewThread();
var lastUserMessage = messages.LastOrDefault();
var userText = lastUserMessage?.Text ?? "";
var answer = MatchFaq(userText);
var responseText = answer ??
"抱歉,我没有找到相关的 FAQ。\n\n您可以:\n" +
"1.换个方式描述您的问题\n" +
"2.拨打客服热线: 400-123-4567\n" +
"3.发送邮件: service@example.com";
var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
{
AuthorName = this.DisplayName,
MessageId = Guid.NewGuid().ToString("N")
};
await NotifyThreadOfNewMessagesAsync(
thread,
messages.Concat(new[] { responseMessage }),
cancellationToken);
// 流式输出:逐字返回(模拟打字效果)
var responseId = Guid.NewGuid().ToString("N");
var messageId = responseMessage.MessageId;
foreach (var character in responseText)
{
yield return new AgentRunResponseUpdate
{
AgentId = this.Id,
ResponseId = responseId,
MessageId = messageId,
AuthorName = this.DisplayName,
Role = ChatRole.Assistant,
Contents = [new TextContent(character.ToString())]
};
await Task.Delay(20, cancellationToken); // 快速流式输出
}
}
}
Console.WriteLine("FaqAgent 定义完成");
3. 关键设计点解析
FAQ 知识库 (Dictionary)
使用 Dictionary<string, string> 存储关键词和答案
一个答案可以对应多个关键词(如"退货"和"退款")
易于维护和扩展
匹配逻辑 (MatchFaq)
userInput.Contains(kvp.Key, StringComparison.OrdinalIgnoreCase)- 简单的关键词包含匹配
- 不区分大小写
- 返回第一个匹配结果
友好的失败提示
匹配失败时不返回空响应
提供替代联系方式
引导用户换个方式提问
流式输出优化
Task.Delay(20) 快速流式输出
模拟真实打字效果
提升用户体验
4. 测试 FaqAgent
var faqAgent = new FaqAgent();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: 匹配成功 - 营业时间");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var response1 = await faqAgent.RunAsync("请问你们的营业时间是什么?");
Console.WriteLine(response1.Messages.First().Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 匹配成功 - 退货流程");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var response2 = await faqAgent.RunAsync("我想退货,怎么操作?");
Console.WriteLine(response2.Messages.First().Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 3: 匹配失败 - 未知问题");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var response3 = await faqAgent.RunAsync("你们有宠物用品吗?");
Console.WriteLine(response3.Messages.First().Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 4: 流式输出");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.Write("响应: ");
await foreach (var update in faqAgent.RunStreamingAsync("快递需要多久?"))
{
var text = update.Contents.OfType<TextContent>().FirstOrDefault()?.Text;
Console.Write(text);
}
Console.WriteLine("\n\n所有测试完成");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 5: 多轮对话");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var thread = faqAgent.GetNewThread();
// 第 1 轮:咨询支付
var r1 = await faqAgent.RunAsync("支持哪些支付方式?", thread);
Console.WriteLine($"第 1 轮:\nQ: 支持哪些支付方式?\nA: {r1.Messages.First().Text}\n");
// 第 2 轮:咨询配送
var r2 = await faqAgent.RunAsync("配送要多久?", thread);
Console.WriteLine($"第 2 轮:\nQ: 配送要多久?\nA: {r2.Messages.First().Text}\n");
// 第 3 轮:咨询联系方式
var r3 = await faqAgent.RunAsync("怎么联系客服?", thread);
Console.WriteLine($"第 3 轮:\nQ: 怎么联系客服?\nA: {r3.Messages.First().Text}\n");
var msgStore = thread.GetService<ChatMessageStore>();
var msgs = await msgStore.GetMessagesAsync();
Console.WriteLine($"线程中的消息总数: {msgs.Count()}");
Console.WriteLine("多轮对话测试完成");
5. FaqAgent 价值分析
让我们对比传统 AI Agent 和 FaqAgent 的成本与性能:
| 指标 | 传统 AI Agent | FaqAgent | 改进幅度 |
|---|---|---|---|
| 响应时间 | 1-3 秒 | < 50 毫秒 | 快 60 倍 |
| 单次成本 | ¥0.001-0.01 | ¥0 | 节省 100% |
| 日处理 10 万次成本 | ¥100-1000 | ¥0 | 节省 100% |
| 答案一致性 | 80-95%(有随机性) | 100%(完全确定) | 提升 5-20% |
| 高并发能力 | 受限于 API 限流 | 仅受服务器性能限制 | 提升 10+ 倍 |
实际场景计算
假设客服系统每天 10 万次咨询
其中 60% (6 万次) 是 FAQ 能解决的
使用 FaqAgent:
- 成本节省: 6 万 × ¥0.005 = ¥300/天 = ¥10 万/年
- 响应速度提升: 从 2 秒 → 0.05 秒
- 用户体验显著改善
6. 优化建议
- 更智能的匹配算法
// 当前:简单的包含匹配
userInput.Contains(keyword)
// 优化:模糊匹配 + 相似度计算
- 使用 Levenshtein 距离计算相似度
- 支持同义词("购买" = "买" = "下单")
- 支持拼音匹配(容错)
- 缓存热门问题统计
// 记录每个 FAQ 的访问次数
// 优先匹配高频问题
// 定期分析未匹配问题,扩充知识库
- 多轮对话引导
// 当匹配模糊时,提供选项
"您是想问以下哪个问题?
1.退货流程
2.退款到账时间
3.退货运费"
- 与 AI Agent 混合
// FaqAgent 匹配失败时
// 自动转发给 AI Agent 处理
三、实战:审批工作流 Agent
1. 场景描述
企业审批系统需要处理各种类型的审批申请:
- 请假申请:根据天数和职级自动审批或转人工
- 报销申请:根据金额和部门自动审批或转人工
- 采购申请:根据金额和物品类型判断审批流程
这些审批流程有明确的规则引擎,不需要 AI 推理,但需要:
- 多轮对话收集信息
- 状态管理(记住已收集的信息)
- 规则匹配(自动判断审批结果)
- 状态持久化(支持中断恢复)
使用自定义 Agent 包装现有规则引擎,实现智能审批助手!
实现目标
- 定义审批规则引擎
- 实现多轮对话状态机
- 自定义 Thread 存储对话状态
- 支持序列化/反序列化(中断恢复)
- 规则匹配并返回审批结果
2. 目标实现
- 步骤1:定义审批规则和状态
/// <summary>
/// 审批类型
/// </summary>
public enum ApprovalType
{
未知,
请假,
报销,
采购
}
/// <summary>
/// 审批状态
/// </summary>
public enum ApprovalStatus
{
收集信息中,
自动通过,
需要主管审批,
需要总监审批,
自动拒绝
}
/// <summary>
/// 审批规则
/// </summary>
public class ApprovalRule
{
public ApprovalType Type { get; set; }
public decimal MaxAmount { get; set; } // 最大金额(报销/采购)
public int MaxDays { get; set; } // 最大天数(请假)
public ApprovalStatus Result { get; set; }
public string Reason { get; set; } = "";
}
/// <summary>
/// 对话状态(存储在 Thread 中)
/// </summary>
public class ApprovalConversationState
{
public ApprovalType Type { get; set; } = ApprovalType.未知;
public decimal Amount { get; set; } // 金额
public int Days { get; set; } // 天数
public string Description { get; set; } = ""; // 申请描述
public bool HasType { get; set; }
public bool HasAmount { get; set; }
public bool HasDays { get; set; }
public bool HasDescription { get; set; }
}
- 步骤2:实现自定义 Thread 存储状态
/// <summary>
/// 审批 Agent 的自定义 Thread
/// 扩展 InMemoryAgentThread,添加对话状态存储
/// </summary>
public class ApprovalAgentThread : InMemoryAgentThread
{
// 对话状态
public ApprovalConversationState State { get; set; } = new();
public ApprovalAgentThread() : base() { }
// 反序列化构造函数
public ApprovalAgentThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
: base(serializedThread, jsonSerializerOptions)
{
// 从序列化数据中恢复状态
if (serializedThread.TryGetProperty("state", out var stateElement))
{
State = JsonSerializer.Deserialize<ApprovalConversationState>(
stateElement.GetRawText(),
jsonSerializerOptions) ?? new();
}
}
// 重写序列化方法,包含状态
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
var baseJson = base.Serialize(jsonSerializerOptions);
var dict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(baseJson.GetRawText())
?? new Dictionary<string, JsonElement>();
// 添加状态到序列化结果
var stateJson = JsonSerializer.SerializeToElement(State, jsonSerializerOptions);
dict["state"] = stateJson;
return JsonSerializer.SerializeToElement(dict, jsonSerializerOptions);
}
}
- 步骤3:实现审批 Agent 核心逻辑
/// <summary>
/// 审批工作流 Agent
/// 功能:多轮对话收集信息,基于规则引擎自动审批
/// </summary>
public class ApprovalAgent : AIAgent
{
// 审批规则库
private readonly List<ApprovalRule> _rules = new()
{
// 请假规则
new() { Type = ApprovalType.请假, MaxDays = 3, Result = ApprovalStatus.自动通过, Reason = "3天以内请假自动通过" },
new() { Type = ApprovalType.请假, MaxDays = 7, Result = ApprovalStatus.需要主管审批, Reason = "3-7天请假需要主管审批" },
new() { Type = ApprovalType.请假, MaxDays = int.MaxValue, Result = ApprovalStatus.需要总监审批, Reason = "超过7天请假需要总监审批" },
// 报销规则
new() { Type = ApprovalType.报销, MaxAmount = 1000, Result = ApprovalStatus.自动通过, Reason = "1000元以内报销自动通过" },
new() { Type = ApprovalType.报销, MaxAmount = 5000, Result = ApprovalStatus.需要主管审批, Reason = "1000-5000元报销需要主管审批" },
new() { Type = ApprovalType.报销, MaxAmount = decimal.MaxValue, Result = ApprovalStatus.需要总监审批, Reason = "超过5000元报销需要总监审批" },
// 采购规则
new() { Type = ApprovalType.采购, MaxAmount = 3000, Result = ApprovalStatus.自动通过, Reason = "3000元以内采购自动通过" },
new() { Type = ApprovalType.采购, MaxAmount = 10000, Result = ApprovalStatus.需要主管审批, Reason = "3000-10000元采购需要主管审批" },
new() { Type = ApprovalType.采购, MaxAmount = decimal.MaxValue, Result = ApprovalStatus.需要总监审批, Reason = "超过10000元采购需要总监审批" }
};
public override string? Name => "ApprovalAgent";
public override AgentThread GetNewThread() => new ApprovalAgentThread();
public override AgentThread DeserializeThread(
JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null)
=> new ApprovalAgentThread(serializedThread, jsonSerializerOptions);
// 从用户输入中提取审批类型
private ApprovalType ExtractType(string input)
{
if (input.Contains("请假", StringComparison.OrdinalIgnoreCase)) return ApprovalType.请假;
if (input.Contains("报销", StringComparison.OrdinalIgnoreCase)) return ApprovalType.报销;
if (input.Contains("采购", StringComparison.OrdinalIgnoreCase)) return ApprovalType.采购;
return ApprovalType.未知;
}
// 从用户输入中提取数字(金额或天数)
private decimal ExtractNumber(string input)
{
var match = System.Text.RegularExpressions.Regex.Match(input, @"\d+(\.\d+)?");
return match.Success ? decimal.Parse(match.Value) : 0;
}
// 根据规则匹配审批结果
private ApprovalRule? MatchRule(ApprovalType type, decimal amount, int days)
{
var applicableRules = _rules.Where(r => r.Type == type).OrderBy(r =>
type == ApprovalType.请假 ? r.MaxDays : r.MaxAmount);
foreach (var rule in applicableRules)
{
if (type == ApprovalType.请假 && days <= rule.MaxDays)
return rule;
if (type != ApprovalType.请假 && amount <= rule.MaxAmount)
return rule;
}
return null;
}
// 处理对话逻辑
private string ProcessConversation(ApprovalAgentThread thread, string userInput)
{
var state = thread.State;
// 步骤 1: 识别审批类型
if (!state.HasType)
{
var type = ExtractType(userInput);
if (type != ApprovalType.未知)
{
state.Type = type;
state.HasType = true;
// 根据类型询问相应信息
return type switch
{
ApprovalType.请假 => "好的,请问您要请假几天?",
ApprovalType.报销 => "好的,请问报销金额是多少元?",
ApprovalType.采购 => "好的,请问采购金额是多少元?",
_ => "抱歉,我没理解您的申请类型。请说明是【请假】、【报销】还是【采购】?"
};
}
else
{
return "您好!我是审批助手。\n\n请告诉我您要申请什么类型的审批:\n请假\n报销\n采购";
}
}
// 步骤 2: 收集金额或天数
if (state.Type == ApprovalType.请假 && !state.HasDays)
{
var days = (int)ExtractNumber(userInput);
if (days > 0)
{
state.Days = days;
state.HasDays = true;
return "请简单描述一下请假事由:";
}
else
{
return "请输入请假天数(例如:3天)";
}
}
if ((state.Type == ApprovalType.报销 || state.Type == ApprovalType.采购) && !state.HasAmount)
{
var amount = ExtractNumber(userInput);
if (amount > 0)
{
state.Amount = amount;
state.HasAmount = true;
return $"请简单描述一下{(state.Type == ApprovalType.报销 ? "报销" : "采购")}事由:";
}
else
{
return "请输入金额(例如:500元 或 500)";
}
}
// 步骤 3: 收集描述
if (!state.HasDescription)
{
state.Description = userInput;
state.HasDescription = true;
// 继续到审批逻辑
}
// 步骤 4: 执行审批规则匹配
var rule = MatchRule(state.Type, state.Amount, state.Days);
if (rule == null)
{
return "审批规则匹配失败,请联系管理员。";
}
// 构造审批结果
var result = $"**审批申请摘要**\n\n";
result += $"- 类型: {state.Type}\n";
if (state.Type == ApprovalType.请假)
result += $"- 天数: {state.Days} 天\n";
else
result += $"- 金额: ¥{state.Amount:N2}\n";
result += $"- 事由: {state.Description}\n\n";
result += $"**审批结果**: {rule.Result}\n";
result += $"**理由**: {rule.Reason}\n\n";
result += rule.Result switch
{
ApprovalStatus.自动通过 => "您的申请已自动通过!",
ApprovalStatus.需要主管审批 => "您的申请已提交,等待主管审批...",
ApprovalStatus.需要总监审批 => "您的申请已提交,等待总监审批...",
_ => "您的申请未通过。"
};
return result;
}
public override async Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
var approvalThread = (thread as ApprovalAgentThread) ?? (ApprovalAgentThread)GetNewThread();
var lastUserMessage = messages.LastOrDefault();
var userText = lastUserMessage?.Text ?? "";
// 处理对话
var responseText = ProcessConversation(approvalThread, userText);
var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
{
AuthorName = this.DisplayName,
MessageId = Guid.NewGuid().ToString("N")
};
var responseMessages = new[] { responseMessage };
await NotifyThreadOfNewMessagesAsync(
approvalThread,
messages.Concat(responseMessages),
cancellationToken);
return new AgentRunResponse
{
AgentId = this.Id,
ResponseId = Guid.NewGuid().ToString("N"),
Messages = responseMessages
};
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var approvalThread = (thread as ApprovalAgentThread) ?? (ApprovalAgentThread)GetNewThread();
var lastUserMessage = messages.LastOrDefault();
var userText = lastUserMessage?.Text ?? "";
var responseText = ProcessConversation(approvalThread, userText);
var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
{
AuthorName = this.DisplayName,
MessageId = Guid.NewGuid().ToString("N")
};
await NotifyThreadOfNewMessagesAsync(
approvalThread,
messages.Concat(new[] { responseMessage }),
cancellationToken);
var responseId = Guid.NewGuid().ToString("N");
var messageId = responseMessage.MessageId;
foreach (var character in responseText)
{
yield return new AgentRunResponseUpdate
{
AgentId = this.Id,
ResponseId = responseId,
MessageId = messageId,
AuthorName = this.DisplayName,
Role = ChatRole.Assistant,
Contents = [new TextContent(character.ToString())]
};
await Task.Delay(15, cancellationToken);
}
}
}
3. 关键设计点解析
自定义 Thread 存储状态
public class ApprovalAgentThread : InMemoryAgentThread { public ApprovalConversationState State { get; set; } = new(); // 重写 Serialize() 包含状态 }扩展 InMemoryAgentThread 添加自定义状态
实现序列化/反序列化支持持久化
多轮对话状态机
未知类型 → 识别类型 → 收集数字 → 收集描述 → 规则匹配 → 返回结果使用布尔标志跟踪每个步骤完成情况
根据当前状态决定下一步询问什么
规则引擎
private ApprovalRule? MatchRule(ApprovalType type, decimal amount, int days)预定义规则列表
按金额/天数排序后匹配第一个符合条件的规则
企业可以动态加载规则(从数据库、配置文件等)
信息提取
ExtractType() // 关键词匹配 ExtractNumber() // 正则表达式提取数字- 简单但实用的 NLU(自然语言理解)
- 生产环境可以使用更复杂的 NLP 库
4. 测试 ApprovalAgent
var approvalAgent = new ApprovalAgent();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: 请假申请(2天 - 自动通过)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var thread1 = approvalAgent.GetNewThread();
// 第 1 轮:说明申请类型
var r1_1 = await approvalAgent.RunAsync("我要请假", thread1);
Console.WriteLine($"Agent: {r1_1.Messages.First().Text}\n");
// 第 2 轮:提供天数
var r1_2 = await approvalAgent.RunAsync("2天", thread1);
Console.WriteLine($"Agent: {r1_2.Messages.First().Text}\n");
// 第 3 轮:提供事由
var r1_3 = await approvalAgent.RunAsync("家里有事", thread1);
Console.WriteLine($"Agent:\n{r1_3.Messages.First().Text}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 报销申请(3500元 - 需要主管审批)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var thread2 = approvalAgent.GetNewThread();
var r2_1 = await approvalAgent.RunAsync("申请报销", thread2);
Console.WriteLine($"Agent: {r2_1.Messages.First().Text}\n");
var r2_2 = await approvalAgent.RunAsync("3500元", thread2);
Console.WriteLine($"Agent: {r2_2.Messages.First().Text}\n");
var r2_3 = await approvalAgent.RunAsync("出差住宿费", thread2);
Console.WriteLine($"Agent:\n{r2_3.Messages.First().Text}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 3: 采购申请(15000元 - 需要总监审批)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var thread3 = approvalAgent.GetNewThread();
var r3_1 = await approvalAgent.RunAsync("我要采购", thread3);
Console.WriteLine($"Agent: {r3_1.Messages.First().Text}\n");
var r3_2 = await approvalAgent.RunAsync("15000", thread3);
Console.WriteLine($"Agent: {r3_2.Messages.First().Text}\n");
var r3_3 = await approvalAgent.RunAsync("办公设备升级", thread3);
Console.WriteLine($"Agent:\n{r3_3.Messages.First().Text}\n");
测试序列化与恢复
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 4: 序列化与恢复");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 开始新对话
var thread4 = approvalAgent.GetNewThread();
var r4_1 = await approvalAgent.RunAsync("我要报销", thread4);
Console.WriteLine($"第 1 轮 - Agent: {r4_1.Messages.First().Text}\n");
var r4_2 = await approvalAgent.RunAsync("800元", thread4);
Console.WriteLine($"第 2 轮 - Agent: {r4_2.Messages.First().Text}\n");
// 模拟中断:序列化状态
Console.WriteLine("模拟对话中断,序列化状态...");
var serialized = thread4.Serialize();
Console.WriteLine($"序列化数据大小: {serialized.GetRawText().Length} 字符\n");
// 模拟恢复:反序列化
Console.WriteLine("恢复对话...");
var restoredThread = approvalAgent.DeserializeThread(serialized) as ApprovalAgentThread;
Console.WriteLine($"恢复的状态:");
new {
类型 = restoredThread!.State.Type,
金额 = restoredThread.State.Amount,
已收集类型 = restoredThread.State.HasType,
已收集金额 = restoredThread.State.HasAmount
}.Display();
// 继续对话
var r4_3 = await approvalAgent.RunAsync("团队聚餐", restoredThread);
Console.WriteLine($"\n第 3 轮(恢复后)- Agent:\n{r4_3.Messages.First().Text}\n");
Console.WriteLine("序列化与恢复测试成功!");
5. ApprovalAgent 技术要点总结
核心技术点
- 自定义 Thread 扩展
- 在 Thread 中存储自定义业务状态
- 支持序列化/反序列化实现对话恢复
- 无需外部状态存储(简化架构)
public class ApprovalAgentThread : InMemoryAgentThread
{
public ApprovalConversationState State { get; set; }
public override JsonElement Serialize() { /* 包含状态 */ }
}
- 多轮对话状态机
- 结构化的对话流程
- 可扩展到更复杂的工作流
- 每个状态明确的输入/输出
stateDiagram-v2
[*] --> 询问类型
询问类型 --> 询问数字: 识别到类型
询问数字 --> 询问事由: 提取到数字
询问事由 --> 规则匹配: 收到描述
规则匹配 --> [*]: 返回结果
- 规则引擎集成
- 确定性的审批结果(无 AI 随机性)
- 规则可外部化(数据库、配置文件)
- 支持复杂的企业审批逻辑
private readonly List<ApprovalRule> _rules = new() { /* 规则列表 */ };
private ApprovalRule? MatchRule(ApprovalType type, decimal amount, int days)
- 序列化与持久化
- 支持对话中断恢复
- 可保存到数据库、Redis、文件等
- 无状态服务架构的基础
var serialized = thread.Serialize();
var restored = agent.DeserializeThread(serialized);
对比: 传统工作流 vs Agent 工作流
| 特性 | 传统工作流系统 | Agent 工作流 | 优势 |
|---|---|---|---|
| 交互方式 | 表单填写 | 自然对话 | 用户体验更好 |
| 集成难度 | 需要 UI 集成 | 只需接入对话接口 | 开发成本低 |
| 灵活性 | 固定流程 | 可中断/恢复 | 更人性化 |
| 扩展性 | 修改表单和逻辑 | 调整对话流程 | 更易维护 |
| 多渠道 | 需要多套 UI | 统一对话接口 | 跨平台一致性 |
生产环境优化建议
- 规则外部化
// 从数据库加载规则
_rules = await LoadRulesFromDatabase();
// 从配置文件加载
_rules = LoadRulesFromConfig("approval-rules.json");
- 更智能的 NLU
// 使用 NLP 库提取实体
var (type, amount, days) = await NlpService.ExtractEntities(userInput);
// 支持更自然的表达
"我想请3天假" → 一次性提取类型和天数
- 审批流程追踪
// 记录审批历史
await AuditLog.RecordApproval(userId, type, amount, result);
// 通知相关人员
await NotificationService.NotifyApprover(approverEmail, request);
- 与现有系统集成
// 调用 ERP API 提交审批
var approvalId = await ErpClient.SubmitApproval(request);
// 轮询审批状态
var status = await ErpClient.GetApprovalStatus(approvalId);
四、实战3 - 数据查询 Agent
1. 场景描述
企业数据报表系统需要响应各种数据查询请求:
- 销售数据: "今天的销售额是多少?"
- 库存数据: "还有多少库存?"
- 用户数据: "当前有多少活跃用户?"
- 财务数据: "本月收入是多少?"
传统做法是让 AI 生成 SQL,但这存在风险:
- SQL 注入风险
- 生成的 SQL 可能有误
- 每次查询都消耗 AI Token
- 响应速度慢
使用自定义 Agent 预定义安全的查询命令,零成本、高安全!
2. 实现目标
- 步骤 1: 定义数据源和查询命令
/// <summary>
/// 企业数据源模拟(生产环境中会连接真实数据库)
/// </summary>
public class EnterpriseDataSource
{
// 销售数据
private readonly List<(DateTime Date, decimal Amount)> _salesData = new()
{
(DateTime.Today, 125680.50m),
(DateTime.Today.AddDays(-1), 98500.30m),
(DateTime.Today.AddDays(-2), 110200.00m)
};
// 库存数据
private readonly Dictionary<string, int> _inventoryData = new()
{
["手机"] = 350,
["笔记本电脑"] = 120,
["平板"] = 200,
["耳机"] = 850,
["充电器"] = 1200
};
// 用户数据
private int _totalUsers = 125680;
private int _activeUsers = 45230;
private int _newUsersToday = 1250;
// 财务数据
private decimal _monthlyRevenue = 3850000.00m;
private decimal _monthlyExpense = 1520000.00m;
// 查询方法
public decimal GetTodaySales() => _salesData.First(s => s.Date == DateTime.Today).Amount;
public decimal GetYesterdaySales() => _salesData.First(s => s.Date == DateTime.Today.AddDays(-1)).Amount;
public decimal GetWeeklySales() => _salesData.Sum(s => s.Amount);
public int GetInventory(string product)
=> _inventoryData.TryGetValue(product, out var count) ? count : 0;
public Dictionary<string, int> GetAllInventory() => _inventoryData;
public int GetTotalUsers() => _totalUsers;
public int GetActiveUsers() => _activeUsers;
public int GetNewUsersToday() => _newUsersToday;
public decimal GetMonthlyRevenue() => _monthlyRevenue;
public decimal GetMonthlyExpense() => _monthlyExpense;
public decimal GetMonthlyProfit() => _monthlyRevenue - _monthlyExpense;
}
- 步骤 2: 实现数据查询 Agent
/// <summary>
/// 数据查询 Agent
/// 功能:将自然语言查询映射到预定义的安全查询命令
/// </summary>
public class DataQueryAgent : AIAgent
{
private readonly EnterpriseDataSource _dataSource = new();
// 查询命令映射(关键词 → 查询方法)
private readonly Dictionary<string, Func<string>> _queryCommands;
public DataQueryAgent()
{
_queryCommands = new()
{
// 销售相关
["今天销售"] = () => FormatSalesResult("今日销售额", _dataSource.GetTodaySales()),
["今日销售"] = () => FormatSalesResult("今日销售额", _dataSource.GetTodaySales()),
["昨天销售"] = () => FormatSalesResult("昨日销售额", _dataSource.GetYesterdaySales()),
["本周销售"] = () => FormatSalesResult("本周销售额", _dataSource.GetWeeklySales()),
// 库存相关
["库存"] = () => FormatInventoryResult(_dataSource.GetAllInventory()),
// 用户相关
["总用户"] = () => FormatUserResult("总用户数", _dataSource.GetTotalUsers()),
["用户总数"] = () => FormatUserResult("总用户数", _dataSource.GetTotalUsers()),
["活跃用户"] = () => FormatUserResult("活跃用户数", _dataSource.GetActiveUsers()),
["新增用户"] = () => FormatUserResult("今日新增用户", _dataSource.GetNewUsersToday()),
// 财务相关
["本月收入"] = () => FormatFinanceResult("本月收入", _dataSource.GetMonthlyRevenue()),
["本月支出"] = () => FormatFinanceResult("本月支出", _dataSource.GetMonthlyExpense()),
["本月利润"] = () => FormatFinanceResult("本月利润", _dataSource.GetMonthlyProfit())
};
}
public override string? Name => "DataQueryAgent";
public override AgentThread GetNewThread() => new CustomAgentThread();
public override AgentThread DeserializeThread(
JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null)
=> new CustomAgentThread(serializedThread, jsonSerializerOptions);
// 意图识别:匹配查询命令
private string? MatchQuery(string userInput)
{
foreach (var kvp in _queryCommands)
{
if (userInput.Contains(kvp.Key, StringComparison.OrdinalIgnoreCase))
{
return kvp.Value();
}
}
return null;
}
// 格式化销售结果
private string FormatSalesResult(string label, decimal amount)
{
return $"**{label}**\n\n" +
$"金额: ¥{amount:N2}\n" +
$"趋势: {(amount > 100000 ? "增长" : "下降")}\n" +
$"查询时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
}
// 格式化库存结果
private string FormatInventoryResult(Dictionary<string, int> inventory)
{
var result = "**当前库存情况**\n\n";
foreach (var item in inventory.OrderByDescending(i => i.Value))
{
var status = item.Value > 500 ? "充足" :
item.Value > 200 ? "正常" : "偏低";
result += $"- {item.Key}: {item.Value} 件 {status}\n";
}
result += $"\n查询时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
return result;
}
// 格式化用户结果
private string FormatUserResult(string label, int count)
{
return $"**{label}**\n\n" +
$"数量: {count:N0}\n" +
$"查询时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
}
// 格式化财务结果
private string FormatFinanceResult(string label, decimal amount)
{
var emoji = label.Contains("利润") ? "💵" :
label.Contains("收入") ? "💰" : "💸";
return $"{emoji} **{label}**\n\n" +
$"金额: ¥{amount:N2}\n" +
$"查询时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
}
public override async Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
thread ??= GetNewThread();
var lastUserMessage = messages.LastOrDefault();
var userText = lastUserMessage?.Text ?? "";
// 尝试匹配查询
var result = MatchQuery(userText);
var responseText = result ??
"未识别的查询命令。\n\n" +
"我支持以下查询:\n\n" +
"**销售数据**\n" +
"- 今天/昨天/本周销售额\n\n" +
"**库存数据**\n" +
"- 库存情况\n\n" +
"**用户数据**\n" +
"- 总用户数\n" +
"- 活跃用户\n" +
"- 新增用户\n\n" +
"**财务数据**\n" +
"- 本月收入/支出/利润";
var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
{
AuthorName = this.DisplayName,
MessageId = Guid.NewGuid().ToString("N")
};
var responseMessages = new[] { responseMessage };
await NotifyThreadOfNewMessagesAsync(
thread,
messages.Concat(responseMessages),
cancellationToken);
return new AgentRunResponse
{
AgentId = this.Id,
ResponseId = Guid.NewGuid().ToString("N"),
Messages = responseMessages
};
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
thread ??= GetNewThread();
var lastUserMessage = messages.LastOrDefault();
var userText = lastUserMessage?.Text ?? "";
var result = MatchQuery(userText);
var responseText = result ??
"未识别的查询命令。\n\n请尝试查询:销售额、库存、用户数、财务数据等。";
var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
{
AuthorName = this.DisplayName,
MessageId = Guid.NewGuid().ToString("N")
};
await NotifyThreadOfNewMessagesAsync(
thread,
messages.Concat(new[] { responseMessage }),
cancellationToken);
var responseId = Guid.NewGuid().ToString("N");
var messageId = responseMessage.MessageId;
foreach (var character in responseText)
{
yield return new AgentRunResponseUpdate
{
AgentId = this.Id,
ResponseId = responseId,
MessageId = messageId,
AuthorName = this.DisplayName,
Role = ChatRole.Assistant,
Contents = [new TextContent(character.ToString())]
};
await Task.Delay(10, cancellationToken);
}
}
}
3. 关键设计点解析
- 预定义查询命令
- 使用委托存储查询逻辑
- 关键词映射到具体查询方法
- 完全可控,无 SQL 注入风险
private readonly Dictionary<string, Func<string>> _queryCommands;
- 数据源抽象
- 封装数据访问逻辑
- 易于替换为真实数据库连接
- 支持缓存、连接池等优化
public class EnterpriseDataSource
{
// 模拟数据(生产中连接真实数据库)
}
- 结果格式化
- 统一的数据展示格式
- 包含 Emoji 和结构化信息
- 提升用户体验
FormatSalesResult()
FormatInventoryResult()
FormatUserResult()
FormatFinanceResult()
安全性保障
- 无 AI 生成 SQL(避免注入)
- 预定义白名单查询
- 无法执行危险操作(UPDATE/DELETE)
- 数据访问权限可控
4. 测试 DataQueryAgent
var dataQueryAgent = new DataQueryAgent();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: 查询今日销售额");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response1 = await dataQueryAgent.RunAsync("今天销售额是多少?");
Console.WriteLine(response1.Messages.First().Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 查询库存情况");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response2 = await dataQueryAgent.RunAsync("库存情况如何?");
Console.WriteLine(response2.Messages.First().Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 3: 查询活跃用户");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response3 = await dataQueryAgent.RunAsync("当前有多少活跃用户?");
Console.WriteLine(response3.Messages.First().Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 4: 查询本月利润");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response4 = await dataQueryAgent.RunAsync("本月利润是多少?");
Console.WriteLine(response4.Messages.First().Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 5: 未识别的查询");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response5 = await dataQueryAgent.RunAsync("今天天气怎么样?");
Console.WriteLine(response5.Messages.First().Text);
Console.WriteLine("\n所有测试完成");
5. DataQueryAgent vs AI 生成 SQL
让我们对比两种方案的优劣:
| 特性 | AI 生成 SQL | DataQueryAgent | 说明 |
|---|---|---|---|
| 安全性 | 有 SQL 注入风险 | 完全安全 | 预定义查询无注入风险 |
| 准确性 | 可能生成错误 SQL | 100% 准确 | 预测试的查询逻辑 |
| 响应速度 | 2-5 秒 | < 100 毫秒 | 无需 AI 推理 |
| 成本 | 每次查询消耗 Token | 零成本 | 仅计算资源 |
| 灵活性 | 可处理任意查询 | 仅限预定义查询 | AI 更灵活 |
| 可控性 | 难以审计 | 完全可控 | 明确的权限管理 |
| 复杂查询 | 支持复杂 JOIN | 需预先实现 | AI 能生成复杂 SQL |
| 数据权限 | 可能越权查询 | 严格权限控制 | 角色级别控制 |
最佳实践:
- 高频简单查询: 使用 DataQueryAgent(成本、安全、速度)
- 低频复杂查询: 使用 AI 生成 SQL(灵活性)
- 混合模式: 简单查询用 Agent,复杂查询转 AI(Part 6 会讲解)
6. 生产环境扩展建议
- 连接真实数据库
public class EnterpriseDataSource
{
private readonly SqlConnection _connection;
public decimal GetTodaySales()
{
using var cmd = new SqlCommand(
"SELECT SUM(Amount) FROM Sales WHERE Date = @Date",
_connection);
cmd.Parameters.AddWithValue("@Date", DateTime.Today);
return (decimal)cmd.ExecuteScalar();
}
}
- 添加缓存机制
private readonly MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
public decimal GetTodaySales()
{
return _cache.GetOrCreate("TodaySales", entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return _dataSource.GetTodaySales();
});
}
- 支持参数化查询
// 用户: "查询手机库存"
// 提取产品名 "手机"
var product = ExtractProduct(userInput);
var count = _dataSource.GetInventory(product);
- 权限控制
public class SecureDataQueryAgent : DataQueryAgent
{
private readonly IAuthorizationService _authService;
public override async Task<AgentRunResponse> RunAsync(...)
{
// 检查用户权限
if (!await _authService.CanAccessData(userId, queryType))
{
return new AgentRunResponse {
Messages = new[] {
new ChatMessage(ChatRole.Assistant, "无权限查询此数据")
}
};
}
return await base.RunAsync(messages, thread, options, cancellationToken);
}
}
- 审计日志
private async Task LogQuery(string userId, string query, string result)
{
await _auditLog.LogAsync(new AuditEntry
{
UserId = userId,
Action = "DataQuery",
Query = query,
Result = result,
Timestamp = DateTime.UtcNow
});
}
五、实战4 - 混合模式 Agent(规则 + AI)
1. 场景描述
前面我们实现了三种专用 Agent:
- FaqAgent:快速响应常见问题(零成本)
- DataQueryAgent:安全的数据查询(高安全)
- ApprovalAgent:规则驱动的审批(确定性)
但企业实际场景更复杂:
- 60% 的问题可以用规则解决
- 30% 的问题需要数据查询
- 10% 的问题需要 AI 推理
传统做法:所有问题都发给 AI → 成本高、速度慢
智能做法:根据问题类型智能分流 → 成本低、速度快、体验好
构建混合模式 Agent,实现智能分流和降级策略!
2. 架构设计
混合模式 Agent 的分流逻辑:
flowchart TD
A[用户输入] --> B{意图识别}
B -->|FAQ 类问题| C[FaqAgent]
B -->|数据查询| D[DataQueryAgent]
B -->|复杂推理| E[AI Agent]
C -->|匹配成功| F[返回结果]
C -->|匹配失败| G{降级策略}
D -->|查询成功| F
D -->|查询失败| G
G -->|转发到 AI| E
E --> F
style C fill:#fff3e0
style D fill:#e3f2fd
style E fill:#f3e5f5
style F fill:#e8f5e9
关键设计点:
- 优先级:FAQ → 数据查询 → AI(成本递增)
- 降级策略:规则失败时自动转 AI
- 统计分析:记录各 Agent 使用率
- 可配置:灵活调整分流策略
3. 代码实现
/// <summary>
/// 混合模式 Agent
/// 功能:智能路由到不同的 Agent,实现成本优化和性能提升
/// </summary>
public class HybridAgent : AIAgent
{
private readonly FaqAgent _faqAgent = new();
private readonly DataQueryAgent _dataQueryAgent = new();
private readonly IChatClient _aiChatClient;
// 统计信息
public int FaqHits { get; private set; } = 0;
public int DataQueryHits { get; private set; } = 0;
public int AIHits { get; private set; } = 0;
public HybridAgent(IChatClient aiChatClient)
{
_aiChatClient = aiChatClient;
}
public override string? Name => "HybridAgent";
public override AgentThread GetNewThread() => new CustomAgentThread();
public override AgentThread DeserializeThread(
JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null)
=> new CustomAgentThread(serializedThread, jsonSerializerOptions);
// 意图识别:判断问题类型
private string ClassifyIntent(string userInput)
{
// 检查是否是 FAQ 类问题
var faqKeywords = new[] { "营业时间", "工作时间", "退货", "退款", "配送", "快递", "物流", "支付", "付款", "联系", "电话", "客服" };
if (faqKeywords.Any(k => userInput.Contains(k, StringComparison.OrdinalIgnoreCase)))
return "faq";
// 检查是否是数据查询问题
var dataKeywords = new[] { "销售", "库存", "用户", "活跃", "收入", "支出", "利润", "财务", "数据" };
if (dataKeywords.Any(k => userInput.Contains(k, StringComparison.OrdinalIgnoreCase)))
return "data";
// 默认:复杂问题,需要 AI
return "ai";
}
// 检查 Agent 响应是否成功
private bool IsSuccessfulResponse(string response)
{
// 如果响应包含错误提示,说明匹配失败
return !response.Contains("抱歉") &&
!response.Contains("未识别") &&
!response.Contains("没有找到");
}
public override async Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
thread ??= GetNewThread();
var lastUserMessage = messages.LastOrDefault();
var userText = lastUserMessage?.Text ?? "";
string responseText;
string source;
// 步骤 1: 意图识别
var intent = ClassifyIntent(userText);
// 步骤 2: 根据意图路由到相应 Agent
if (intent == "faq")
{
// 尝试 FaqAgent
var faqResponse = await _faqAgent.RunAsync(userText);
var faqText = faqResponse.Messages.First().Text;
if (IsSuccessfulResponse(faqText))
{
responseText = faqText;
source = "FaqAgent";
FaqHits++;
}
else
{
// 降级到 AI
responseText = await CallAI(userText);
source = "AI (降级)";
AIHits++;
}
}
else if (intent == "data")
{
// 尝试 DataQueryAgent
var dataResponse = await _dataQueryAgent.RunAsync(userText);
var dataText = dataResponse.Messages.First().Text;
if (IsSuccessfulResponse(dataText))
{
responseText = dataText;
source = "DataQueryAgent";
DataQueryHits++;
}
else
{
// 降级到 AI
responseText = await CallAI(userText);
source = "AI (降级)";
AIHits++;
}
}
else
{
// 直接使用 AI
responseText = await CallAI(userText);
source = "AI";
AIHits++;
}
// 添加来源标识(调试用)
responseText += $"\n\n---\n*处理方式: {source}*";
var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
{
AuthorName = this.DisplayName,
MessageId = Guid.NewGuid().ToString("N")
};
var responseMessages = new[] { responseMessage };
await NotifyThreadOfNewMessagesAsync(
thread,
messages.Concat(responseMessages),
cancellationToken);
return new AgentRunResponse
{
AgentId = this.Id,
ResponseId = Guid.NewGuid().ToString("N"),
Messages = responseMessages
};
}
// 调用 AI Agent
private async Task<string> CallAI(string userInput)
{
try
{
var response = await _aiChatClient.GetResponseAsync(userInput);
return response.Text ?? "AI 未返回有效响应";
}
catch (Exception ex)
{
return $"AI 调用失败: {ex.Message}";
}
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// 简化版:直接调用 RunAsync 再流式输出
var response = await RunAsync(messages, thread, options, cancellationToken);
var responseText = response.Messages.First().Text;
var responseId = Guid.NewGuid().ToString("N");
var messageId = Guid.NewGuid().ToString("N");
foreach (var character in responseText)
{
yield return new AgentRunResponseUpdate
{
AgentId = this.Id,
ResponseId = responseId,
MessageId = messageId,
AuthorName = this.DisplayName,
Role = ChatRole.Assistant,
Contents = [new TextContent(character.ToString())]
};
await Task.Delay(10, cancellationToken);
}
}
// 获取统计信息
public string GetStatistics()
{
var total = FaqHits + DataQueryHits + AIHits;
if (total == 0) return "暂无统计数据";
return $"**使用统计**\n\n" +
$"- FaqAgent: {FaqHits} 次 ({FaqHits * 100.0 / total:F1}%)\n" +
$"- DataQueryAgent: {DataQueryHits} 次 ({DataQueryHits * 100.0 / total:F1}%)\n" +
$"- AI Agent: {AIHits} 次 ({AIHits * 100.0 / total:F1}%)\n" +
$"- 总计: {total} 次\n\n" +
$"**成本优化**: {(FaqHits + DataQueryHits) * 100.0 / total:F1}% 的请求避免了 AI 调用";
}
}
4. 关键设计点解析
- 意图识别(ClassifyIntent)
- 简单但有效的分类逻辑
- 可扩展为更复杂的 NLP 模型
private string ClassifyIntent(string userInput)
{
// 关键词匹配识别问题类型
if (faqKeywords.Any(...)) return "faq";
if (dataKeywords.Any(...)) return "data";
return "ai"; // 默认:复杂问题
}
- 智能路由(RunAsync)
flowchart LR
A[意图识别] --> B{问题类型}
B -->|FAQ| C[FaqAgent]
B -->|Data| D[DataQueryAgent]
B -->|AI| E[AI Agent]
C -->|失败| E
D -->|失败| E
style E fill:#f3e5f5
- 降级策略(IsSuccessfulResponse)
- 规则 Agent 失败时自动降级到 AI
- 保证用户体验(总能得到答案)
private bool IsSuccessfulResponse(string response)
{
// 检查响应是否包含错误提示
return !response.Contains("抱歉") && !response.Contains("未识别");
}
统计分析(GetStatistics)
- 记录各 Agent 使用次数
- 计算成本优化比例
- 支持性能监控和优化决策
5. 测试 HybridAgent
// 创建 HybridAgent(需要 AI ChatClient)
var chatClient = AIClientHelper.GetDefaultChatClient();
var hybridAgent = new HybridAgent(chatClient);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: FAQ 类问题(路由到 FaqAgent)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response1 = await hybridAgent.RunAsync("请问你们的营业时间?");
Console.WriteLine(response1.Messages.First().Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 数据查询问题(路由到 DataQueryAgent)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response2 = await hybridAgent.RunAsync("今天销售额是多少?");
Console.WriteLine(response2.Messages.First().Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 3: 复杂问题(路由到 AI Agent)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response3 = await hybridAgent.RunAsync("如何提升客户满意度?");
Console.WriteLine(response3.Messages.First().Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 4: 降级场景(规则失败 → AI)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 这个问题会被识别为数据查询,但 DataQueryAgent 无法处理,会降级到 AI
var response4 = await hybridAgent.RunAsync("去年的销售数据趋势如何?");
Console.WriteLine(response4.Messages.First().Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("查看使用统计");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
Console.WriteLine(hybridAgent.GetStatistics());
Console.WriteLine("\n所有测试完成");
6. 混合模式的价值分析
成本对比
假设企业客服系统每天处理 10万次 对话:
| 模式 | 成本计算 | 每日成本 | 每年成本 | 节省 |
|---|---|---|---|---|
| 纯 AI 模式 | 10万 × ¥0.005 | ¥500 | ¥18万 | - |
| 混合模式 | 1万(AI) × ¥0.005 | ¥50 | ¥1.8万 | 90% |
假设分流比例:
60% FAQ (6万次) → ¥0
30% 数据查询 (3万次) → ¥0
10% AI (1万次) → ¥50/天
- 年度节省:¥18万 - ¥1.8万 = ¥16.2万
性能对比
| 指标 | 纯 AI 模式 | 混合模式 | 提升 |
|---|---|---|---|
| 平均响应时间 | 2 秒 | 0.5 秒 | 4倍 |
| P95 响应时间 | 5 秒 | 1 秒 | 5倍 |
| 并发能力 | 100 QPS | 1000+ QPS | 10倍+ |
| 可用性 | 99.9% | 99.99%+ | 更高 |
用户体验提升
- 响应更快:90% 的问题 < 100ms 响应
- 答案更准:规则 Agent 100% 准确
- 高可用:规则 Agent 无外部依赖
- 降级保障:AI 作为兜底,保证总能得到答案
7. 生产环境优化策略
- 更智能的意图识别
// 使用 ML.NET 或其他 NLP 模型
public class MLIntentClassifier
{
private readonly PredictionEngine<Input, Output> _model;
public string Classify(string text)
{
var prediction = _model.Predict(new Input { Text = text });
return prediction.Intent; // "faq" / "data" / "ai"
}
}
- 动态分流策略
public class RoutingStrategy
{
// 根据系统负载动态调整
public string Route(string intent, double systemLoad)
{
if (systemLoad > 0.8 && intent == "ai")
{
// 高负载时,尽量用规则 Agent
return "try_rule_first";
}
return "normal";
}
}
- A/B 测试支持
public class HybridAgentWithAB : HybridAgent
{
private readonly IAbTestingService _abTest;
public override async Task<AgentRunResponse> RunAsync(...)
{
var variant = _abTest.GetVariant(userId);
if (variant == "pure_ai")
{
// 对照组:纯 AI
return await CallAI(userText);
}
else
{
// 实验组:混合模式
return await base.RunAsync(messages, thread, options, cancellationToken);
}
}
}
- 智能学习与优化
// 记录用户反馈
public void RecordFeedback(string query, string source, bool isHelpful)
{
// 存储到数据库
_feedbackRepo.Add(new Feedback
{
Query = query,
Source = source,
IsHelpful = isHelpful,
Timestamp = DateTime.UtcNow
});
// 分析哪些问题应该加入规则库
if (source == "AI" && isHelpful && CountSimilarQueries(query) > 100)
{
SuggestAddToRuleBase(query);
}
}
- 监控与告警
// 监控各 Agent 使用情况
public class AgentMonitor
{
public void Track(string agentName, TimeSpan duration, bool success)
{
_metrics.RecordHistogram($"agent.{agentName}.duration", duration.TotalMilliseconds);
_metrics.IncrementCounter($"agent.{agentName}.{(success ? "success" : "failure")}");
// AI 使用率过高告警
if (CalculateAIUsageRate() > 0.5)
{
_alertService.SendAlert("AI 使用率超过 50%,建议优化规则库");
}
}
}
六、序列化与互操作性
1. 序列化最佳实践
自定义 Agent 的状态持久化是生产环境的关键需求。我们已经在工作流实战中实现了 ApprovalAgent 的序列化,现在总结最佳实践:
序列化检查清单
- 必须序列化的内容:
- 消息历史(base.Serialize() 已包含)
- 自定义状态(需显式序列化)
- Thread ID 和元数据
- 时间戳信息
- 可选序列化的内容:
- 统计信息(如 HybridAgent 的使用统计)
- 临时缓存(可重建的数据)
- UI 状态(特定于客户端)
// 回顾 ApprovalAgentThread 的序列化实现
public class ApprovalAgentThread : InMemoryAgentThread
{
public ApprovalState State { get; set; } = new();
public override string Serialize()
{
// 1. 序列化基础消息历史
var baseJson = base.Serialize();
var baseDoc = JsonDocument.Parse(baseJson);
// 2. 添加自定义状态
var root = JsonSerializer.SerializeToElement(new
{
messages = baseDoc.RootElement.GetProperty("messages"),
customState = State // 自定义状态
});
return JsonSerializer.Serialize(root, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
}
}
2. 跨平台互操作性
自定义 Agent 可以通过以下方式集成到 MAF 生态系统:
- 导出为 AI Function:任何 AIAgent 都可以通过 AsAIFunction() 方法导出为 Function Calling 工具
// 演示: 将 FaqAgent 导出为工具函数
var faqAgent = new FaqAgent();
var faqFunction = faqAgent.AsAIFunction();
Console.WriteLine(" 导出的 Function 信息:");
faqFunction.Display();
Console.WriteLine("\nAgent 导出为 Function 完成");
Console.WriteLine("现在可以在其他 Agent 或 ChatOptions 中使用此工具");
- 嵌套使用 Agent:自定义 Agent 可以作为其他 Agent 的工具
// 演示: 在 ChatCompletionAgent 中使用自定义 Agent
var dataAgent = new DataQueryAgent();
var masterAgent = chatClient.CreateAIAgent(
name: "MasterAgent",
instructions : "你是一个主 Agent,可以调用多个专业工具回答用户问题",
tools: new[]
{
faqAgent.AsAIFunction(),
dataAgent.AsAIFunction()
}
);
// 测试嵌套调用
var testThread = masterAgent.GetNewThread();
var nestedResponse = await masterAgent.RunAsync("今日销售额如何?", testThread);
Console.WriteLine("嵌套调用结果:");
Console.WriteLine(nestedResponse.Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("嵌套 Agent 测试完成");
- MCP 集成:自定义 Agent 可以通过 MCP 协议暴露给外部系统
flowchart LR
A[MCP Client] -->|MCP Protocol| B[MCP Server]
B --> C[Custom Agent]
C --> D[Agent Logic]
D --> E[Response]
E --> B --> A
// 在 MCP Server 中注册 Agent 工具
server.AddTool(
name: "faq_agent",
description: "查询常见问题知识库",
async (string query) =>
{
var thread = new CustomAgentThread();
thread.AddUserMessage(query);
var response = await faqAgent.InvokeAsync(thread);
return response.Text;
}
);
//在 MCP Client 中调用
var mcpClient = new McpClient(transport);
var result = await mcpClient.CallToolAsync("faq_agent", new { query = "如何修改密码?" });
优势:自定义 Agent 可以跨语言、跨平台使用
3. 互操作性对比
| 集成方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| AsAIFunction() | 在同一应用内嵌套 | 零开销、类型安全 | 仅限 .NET |
| MCP 集成 | 跨语言/跨进程 | 语言无关、松耦合 | 序列化开销 |
| HTTP API | 微服务架构 | 标准化、可扩展 | 网络延迟 |
| 消息队列 | 异步处理 | 高吞吐、解耦 | 复杂度高 |
七、最佳实践与总结
1. 何时使用自定义 Agent?
决策树
flowchart TD
Start[需要创建 Agent] --> Q1{需求是否简单?}
Q1 -->|是<br>单轮对话| ChatCompletionAgent
Q1 -->|否| Q2{是否需要状态管理?}
Q2 -->|否<br>无状态逻辑| Q3{是否需要工具调用?}
Q3 -->|否| SimpleAgent[FaqAgent 模式]
Q3 -->|是| ToolAgent[DataQueryAgent 模式]
Q2 -->|是<br>多轮对话| Q4{状态复杂度}
Q4 -->|简单<br>基础字段| InMemory[扩展 InMemoryAgentThread]
Q4 -->|复杂<br>嵌套状态| Custom[ApprovalAgent 模式]
Q2 --> Q5{需要智能路由?}
Q5 -->|是| Hybrid[HybridAgent 模式]
Q5 -->|否| Direct[直接实现 AIAgent]
style ChatCompletionAgent fill:#90EE90
style SimpleAgent fill:#87CEEB
style ToolAgent fill:#87CEEB
style InMemory fill:#FFD700
style Custom fill:#FFA07A
style Hybrid fill:#FF6B6B
2. 实现检查清单
必须实现的 5 个方法
| 方法 | 作用 | 实现难度 | 注意事项 |
|---|---|---|---|
| InvokeStreamingAsync() | 流式响应 | ⭐⭐⭐ | 必须使用 yield return 语句 |
| InvokeAsync() | 同步响应 | ⭐⭐ | 通常调用 Streaming 版本后聚合结果 |
| CreateThread() | 创建对话线程 | ⭐ | 返回 InMemoryAgentThread 或自定义子类 |
| Serialize() | 序列化状态 | ⭐⭐⭐⭐ | 需同时序列化消息历史和自定义状态 |
| DeserializeThread() | 反序列化 | ⭐⭐⭐⭐ | 必须还原完整状态,包括自定义字段 |
3. 核心设计原则
单一职责原则 (SRP)
FaqAgent 只处理 FAQ
ApprovalAgent 只处理审批流程
DataQueryAgent 只处理数据查询
避免一个 Agent 做太多事情
状态管理原则
能用 InMemoryAgentThread 就不要自定义
自定义 Thread 时必须实现完整序列化
状态类使用 record 类型(不可变性)
错误处理原则
try
{
// Agent 逻辑
yield return new ChatMessageContent(AgentRole.Assistant, result);
}
catch (Exception ex)
{
// 必须返回错误消息,而不是抛出异常
yield return new ChatMessageContent(
AgentRole.Assistant,
$"处理失败: {ex.Message}"
);
}
性能优化原则
- 使用 async/await 避免阻塞
- 缓存可重用的数据(如 FAQ 字典)
- 使用 IAsyncEnumerable 流式传输大响应
- 避免在 Agent 内部做大量计算
4. 安全性考虑
- 输入验证
public override async IAsyncEnumerable<ChatMessageContent> InvokeStreamingAsync(
AgentThread thread)
{
var lastMessage = thread.GetLastUserMessage();
// 1. 长度检查
if (string.IsNullOrWhiteSpace(lastMessage) || lastMessage.Length > 10000)
{
yield return new ChatMessageContent(
AgentRole.Assistant,
"输入无效或过长"
);
yield break;
}
// 2. SQL 注入防护(DataQueryAgent)
if (lastMessage.Contains(";") || lastMessage.Contains("--"))
{
yield return new ChatMessageContent(
AgentRole.Assistant,
"检测到非法字符"
);
yield break;
}
// 3. 敏感信息过滤
var sanitizedInput = RemoveSensitiveInfo(lastMessage);
// 继续处理...
}
- 权限控制
// 在 AgentThread 中存储用户权限
public class SecureAgentThread : InMemoryAgentThread
{
public string UserId { get; set; } = "";
public List<string> Roles { get; set; } = new();
public bool HasPermission(string requiredRole) =>
Roles.Contains(requiredRole);
}
// 在 Agent 中检查权限
if (secureThread.HasPermission("Admin"))
{
// 执行管理员操作
}
else
{
yield return new ChatMessageContent(
AgentRole.Assistant,
"权限不足"
);
yield break;
}
- 审计日志
// 记录所有 Agent 操作
protected async Task LogOperationAsync(string action, string details)
{
await _auditLogger.LogAsync(new AuditLog
{
AgentName = Name,
Action = action,
Details = details,
Timestamp = DateTime.UtcNow,
UserId = GetCurrentUserId()
});
}
5. 性能监控
关键指标
public class AgentMetrics
{
public int TotalRequests { get; set; }
public TimeSpan AverageLatency { get; set; }
public int ErrorCount { get; set; }
public Dictionary<string, int> RouteStatistics { get; set; } = new();
}
// 在 Agent 中集成监控
private readonly AgentMetrics _metrics = new();
public override async IAsyncEnumerable<ChatMessageContent> InvokeStreamingAsync(
AgentThread thread)
{
var sw = Stopwatch.StartNew();
_metrics.TotalRequests++;
try
{
// Agent 逻辑...
yield return result;
}
catch (Exception ex)
{
_metrics.ErrorCount++;
throw;
}
finally
{
sw.Stop();
_metrics.AverageLatency = CalculateAverageLatency(sw.Elapsed);
}
}
监控仪表盘数据
| 指标 | 目标值 | 告警阈值 | 说明 |
|---|---|---|---|
| 平均延迟 | < 2s | > 5s | 用户体验关键指标 |
| 错误率 | < 1% | > 5% | 稳定性指标 |
| 并发连接 | < 1000 | > 5000 | 资源使用指标 |
| Token 使用 | < 1M/天 | > 10M/天 | 成本控制指标 |
