Net+AI智能体进阶5:Agent智能体
2025-11-01 19:31:17一、第一个智能体
1. 什么是 MAF
Microsoft Agent Framework (MAF) 是微软推出的企业级 AI Agent 开发框架,构建在 Microsoft.Extensions.AI (MEAI) 之上,提供了构建生产级 AI Agent 所需的完整能力。
Agent vs ChatClient - 什么时候用 Agent?
| 特性 | IChatClient | AIAgent |
|---|---|---|
| 定位 | 底层 AI 调用抽象 | 高级智能体封装 |
| 状态管理 | 无状态,每次调用独立 | 内置对话线程 (AgentThread) |
| 身份定义 | 需要手动在每次调用中传入 System Message | 固定的 Instructions 和 Name |
| 工具管理 | 需要手动配置 ChatOptions.Tools | Agent 级别统一管理工具 |
| 使用场景 | 构建自定义 AI 功能,单次对话场景 | 企业级对话系统,多轮交互场景 |
简单来说:
- ChatClient 就像一个纯函数:给定输入,返回输出,不保留状态
- Agent 就像一个有记忆的助手: 有固定身份、能记住上下文、能使用工具
2. MAF 的核心概念
MAF 围绕以下核心概念构建:
AIAgent - 智能代理:Agent 是具有特定身份和能力的智能实体,包含:
Name (名称):Agent 的唯一标识
Instructions (指令):Agent 的系统提示词,定义其行为和角色
Tools (工具):Agent 可以调用的函数工具集合
AgentThread - 对话线程:每个 Agent 可以有多个对话线程,每个线程维护独立的对话历史:
自动管理消息历史
支持序列化/反序列化 (持久化)
线程隔离,互不干扰
AgentRun - 执行实例:每次调用 Agent 都会创建一个 Run:
同步调用:RunAsync() → 返回完整响应
流式调用:RunStreamingAsync() → 返回增量更新流
3. 快速开始
让我们通过创建一个口语教练 Agent 来快速入门 MAF。
// 步骤 1: 获取底层获取 ChatClient
var chatClient = ChatClientFactory.GetQwenClient();
// 步骤 2: 创建口语教练 Agent
// 使用 `CreateAIAgent()` 扩展方法将 ChatClient 转换为 Agent。
// 核心参数:
// - instructions: Agent 的系统提示词, 定义角色和行为
// - name: Agent 的名称(可选, 用于日志和调试)
AIAgent spokenEnglishCoach = chatClient.CreateAIAgent(
instructions: "你是一位专业的英语口语教练。你的任务是帮助学生提升英语口语能力,包括发音、流利度和自然表达。请始终保持鼓励和友好的态度。",
name: "SpokenEnglishCoach"
);
Console.WriteLine("Agent 定义完成");
// 步骤 3:调用 Agent
// 场景 1: 学生请教如何提升口语
var userMessage = "我想提高我的英语口语能力,但不知道从哪里开始。你能给我一些建议吗?";
Console.WriteLine($"用户: {userMessage}");
Console.WriteLine("\n正在请求 Agent...\n");
// 调用 Agent (同步模式)
var response = await spokenEnglishCoach.RunAsync(userMessage);
Console.WriteLine($"{spokenEnglishCoach.Name}: {response}");
// 场景 2: 学生说一句英语并请求纠错
var userMessage2 = "我刚才说了这句话: 'I want to going to the park tomorrow.' 这样说对吗?";
Console.WriteLine($"用户: {userMessage2}");
Console.WriteLine("\n正在请求 Agent...\n");
var response2 = await spokenEnglishCoach.RunAsync(userMessage2);
Console.WriteLine($"{spokenEnglishCoach.Name}: {response2}");
// 场景 3: 流式调用,逐字显示响应
var userMessage3 = "请给我讲解一下如何练习英语的连读(linking)技巧,并给出几个例子。";
Console.WriteLine($"用户: {userMessage3}");
Console.WriteLine($"\n{spokenEnglishCoach.Name} (流式响应): \n");
// 流式调用 Agent
await foreach (var chunk in spokenEnglishCoach.RunStreamingAsync(userMessage3))
{
Console.Write(chunk); // 逐块输出,不换行
}
Console.WriteLine("\n\n流式响应完成");
二、会话线程
在上面的示例中,每次调用 RunAsync() 都是独立的单次对话,Agent 不会记住之前的内容。
原因分析:每次调用 RunAsync() 时,如果不传入 thread 参数,MAF 会临时创建一个新的线程,调用结束后立即丢弃。因此,每次调用都是全新的对话,没有任何历史记忆。
1. 显示创建并复用 Thread
要实现多轮对话记忆,需要:
- 显式创建 AgentThread
- 在所有相关的调用中复用同一个 Thread
- Thread 会自动管理消息历史
AgentThread 的核心特性
| 特性 | 说明 |
|---|---|
| 线程隔离 | 每个 Thread 维护独立的对话历史,互不干扰 |
| 自动管理 | Thread 自动存储所有消息 (用户消息 + Agent 响应) |
| 可复用 | 同一个 Thread 可以在多次调用中复用 |
| 可序列化 | Thread 可以序列化保存,实现持久化 |
| 唯一标识 | 每个 Thread 有唯一的 ThreadId |
2. 快速开始
// 步骤 1: 创建一个个人助理 Agent
AIAgent personalAssistant = chatClient.CreateAIAgent(
instructions: "你是一位专业的个人助理。你需要记住用户告诉你的所有信息,包括姓名、偏好、需求等,并在后续对话中使用这些信息提供个性化服务。请始终保持礼貌和专业。",
name: "PersonalAssistant"
);
Console.WriteLine("Agent 创建完成");
// 步骤 2: 创建新的对话线程
AgentThread thread = personalAssistant.GetNewThread();
// 步骤 3:多轮对话模式
// 第一轮:自我介绍
var message1 = "你好!我叫李明,是一名软件工程师,喜欢喝咖啡。";
Console.WriteLine($"用户: {message1}");
Console.WriteLine("\n正在请求 Agent...\n");
// 调用 Agent,传入 thread 参数
var response1 = await personalAssistant.RunAsync(message1, thread);
Console.WriteLine($"{personalAssistant.Name}: {response1}");
// 第二轮:测试记忆
var message2 = "你还记得我叫什么名字吗?";
Console.WriteLine($"用户: {message2}");
Console.WriteLine("\n正在请求 Agent...\n");
// 使用同一个 thread
var response2 = await personalAssistant.RunAsync(message2, thread);
Console.WriteLine($"{personalAssistant.Name}: {response2}");
Console.WriteLine("\nAgent 成功记住了姓名!");
// 第三轮:咨询偏好
var message3 = "我喜欢喝什么饮料?";
Console.WriteLine($"用户: {message3}");
Console.WriteLine("\n正在请求 Agent...\n");
// 继续使用同一个 thread
var response3 = await personalAssistant.RunAsync(message3, thread);
Console.WriteLine($"{personalAssistant.Name}: {response3}");
Console.WriteLine("\nAgent 成功记住了偏好信息!");
// 第四轮:复杂上下文引用
var message4 = "根据我的职业和爱好,你能推荐一些适合我的书籍吗?";
Console.WriteLine($"用户: {message4}");
Console.WriteLine("\n正在请求 Agent...\n");
// 继续使用同一个 thread
var response4 = await personalAssistant.RunAsync(message4, thread);
Console.WriteLine($"{personalAssistant.Name}: {response4}");
Console.WriteLine("\nAgent 能够综合使用多条历史信息!");
3. 流式多轮对话
我们将创建一个故事创作助手,通过多轮对话逐步完善故事。
// 步骤 1:创建故事创作助手 Agent
AIAgent storyWriter = chatClient.CreateAIAgent(
instructions: "你是一位创意写作助手。你会根据用户的想法逐步完善故事情节。请记住之前讨论的所有故事元素,并在后续创作中保持一致性。",
name: "StoryWriter"
);
// 创建新的对话线程
AgentThread storyThread = storyWriter.GetNewThread();
Console.WriteLine("故事创作 Agent 和 Thread 创建完成");
// 步骤 2:第一轮流水对话 - 设定故事背景
var storyMessage1 = "我想写一个科幻故事,背景是2150年的火星殖民地,主角叫阿尔法,是一名工程师。";
Console.WriteLine($"用户: {storyMessage1}");
Console.WriteLine($"\n{storyWriter.Name} (流式响应): \n");
// 流式调用,传入 thread
await foreach (var chunk in storyWriter.RunStreamingAsync(storyMessage1, storyThread))
{
Console.Write(chunk);
}
Console.WriteLine("\n\n第一轮对话完成");
// 步骤 3:第二轮流水对话 - 引入冲突
var storyMessage2 = "现在给故事增加一些冲突:殖民地的生命支持系统出现了故障,阿尔法必须在24小时内修复它。请继续写下去。";
Console.WriteLine($"用户: {storyMessage2}");
Console.WriteLine($"\n{storyWriter.Name} (流式响应): \n");
// 继续使用同一个 thread
await foreach (var chunk in storyWriter.RunStreamingAsync(storyMessage2, storyThread))
{
Console.Write(chunk);
}
Console.WriteLine("\n\n第二轮对话完成");
// 步骤 4:第三轮流水对话 - 添加转折
var storyMessage3 = "在修复过程中,阿尔法发现系统故障不是意外,而是有人故意破坏。请写出这个转折点。";
Console.WriteLine($"用户: {storyMessage3}");
Console.WriteLine($"\n{storyWriter.Name} (流式响应): \n");
// 继续使用同一个 thread
await foreach (var chunk in storyWriter.RunStreamingAsync(storyMessage3, storyThread))
{
Console.Write(chunk);
}
Console.WriteLine("\n\n第三轮对话完成");
Console.WriteLine("\nAgent 完美地保持了故事的连贯性,记住了所有设定!");
4. 线程隔离机制
AgentThread 的一个重要特性是线程隔离:不同的 Thread 之间完全独立,互不干扰。
这对于多用户场景非常重要:每个用户应该有自己独立的对话线程。
// 创建客服助手 Agent
AIAgent customerService = chatClient.CreateAIAgent(
instructions: "你是一位电商平台的客服助手。请记住每位客户的信息和需求,提供个性化服务。",
name: "CustomerService"
);
// 创建两个独立的线程,分别代表两个用户
AgentThread threadZhangSan = customerService.GetNewThread();
AgentThread threadLiSi = customerService.GetNewThread();
Console.WriteLine("客服 Agent 和两个独立 Thread 创建完成");
// 张三的第一次对话
var zhangsan1 = await customerService.RunAsync("你好,我叫张三,我想买一台笔记本电脑,预算8000元。", threadZhangSan);
Console.WriteLine($"张三: 你好,我叫张三,我想买一台笔记本电脑,预算8000元。");
Console.WriteLine($"客服: {zhangsan1}\n");
// 张三的第二次对话
var zhangsan2 = await customerService.RunAsync("我主要用来做软件开发。", threadZhangSan);
Console.WriteLine($"张三: 我主要用来做软件开发。");
Console.WriteLine($"客服: {zhangsan2}\n");
// 李四的第一次对话
var lisi1 = await customerService.RunAsync("我是李四,想买一个游戏鼠标,有什么推荐吗?", threadLiSi);
Console.WriteLine($"李四: 我是李四,想买一个游戏鼠标,有什么推荐吗?");
Console.WriteLine($"客服: {lisi1}\n");
// 李四的第二次对话
var lisi2 = await customerService.RunAsync("价格在300元以内。", threadLiSi);
Console.WriteLine($"李四: 价格在300元以内。");
Console.WriteLine($"客服: {lisi2}\n");
// 张三询问自己的需求
var zhangsan3 = await customerService.RunAsync("你还记得我的预算和用途吗?", threadZhangSan);
Console.WriteLine($"张三: 你还记得我的预算和用途吗?");
Console.WriteLine($"客服: {zhangsan3}\n");
// 李四询问自己的需求
var lisi3 = await customerService.RunAsync("我刚才想买什么?", threadLiSi);
Console.WriteLine($"李四: 我刚才想买什么?");
Console.WriteLine($"客服: {lisi3}\n");
Console.WriteLine("验证成功: 两个线程完全隔离,Agent 分别记住了两个用户的信息!");
5. 访问 Thread 历史消息
Thread 内部维护了一个消息历史列表,我们可以通过 :
- GetService<IList
>() 方法获取。 - GetService
()方法获取。
实战演示:查看个人助理的对话历史
// 获取之前创建的个人助理 Thread 的历史消息
IList<ChatMessage>? chatHistory = thread.GetService<IList<ChatMessage>>();
if (chatHistory != null)
{
Console.WriteLine($"当前 Thread 的对话统计:\n");
Console.WriteLine($"总消息数: {chatHistory.Count} 条");
// 统计不同角色的消息数
var userMessages = chatHistory.Where(m => m.Role == ChatRole.User).Count();
var assistantMessages = chatHistory.Where(m => m.Role == ChatRole.Assistant).Count();
var systemMessages = chatHistory.Where(m => m.Role == ChatRole.System).Count();
new
{
用户消息 = userMessages,
Agent响应 = assistantMessages,
系统消息 = systemMessages,
总计 = chatHistory.Count
}.Display();
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
Console.WriteLine("完整对话历史:\n");
// 遍历并显示所有历史消息
for (int i = 0; i < chatHistory.Count; i++)
{
var message = chatHistory[i];
var roleIcon = message.Role.ToString() switch
{
"user" => "用户",
"assistant" => "助理消息",
"system" => "系统消息",
_ => "其他消息"
};
Console.WriteLine($"{i + 1}. {roleIcon} [{message.Role}]:");
Console.WriteLine($" {message.Text}\n");
}
Console.WriteLine("历史消息访问成功!");
}
else
{
Console.WriteLine("无法获取历史消息");
}
// 获取会话线程消息存储
var msgStore = thread.GetService<ChatMessageStore>();
msgStore.Display();
var msgs = await msgStore!.GetMessagesAsync();
msgs.Display();
实战演示:监控多用户的对话活跃度
// 对比两个用户的对话活跃度
var zhangSanHistory = threadZhangSan.GetService<IList<ChatMessage>>();
var liSiHistory = threadLiSi.GetService<IList<ChatMessage>>();
Console.WriteLine(" 多用户对话活跃度统计:\n");
if (zhangSanHistory != null && liSiHistory != null)
{
new
{
用户 = "张三",
ThreadId = threadZhangSan.ThreadId.Substring(0, 8) + "...",
总消息数 = zhangSanHistory.Count,
用户消息数 = zhangSanHistory.Where(m => m.Role == ChatRole.User).Count()
}.Display();
new
{
用户 = "李四",
ThreadId = threadLiSi.ThreadId.Substring(0, 8) + "...",
总消息数 = liSiHistory.Count,
用户消息数 = liSiHistory.Where(m => m.Role == ChatRole.User).Count()
}.Display();
Console.WriteLine("\n可以清晰地看到每个用户的对话活跃度!");
}
6. 历史消息管理的注意事项
适合的场景:
统计分析对话数据
调试和日志记录
导出对话记录
实现自定义的消息过滤
注意事项:
只读访问:虽然返回的是 IList
,但不建议直接修改历史消息,这可能导致不可预测的行为。 Token 管理:随着对话轮数增加,历史消息会越来越长,需要注意 Token 限制。解决方案:
使用 IChatReducer 自动裁剪历史
定期创建新的 Thread
实现消息摘要机制
内存占用:长时间运行的对话会占用较多内存,考虑:
序列化旧的 Thread 到存储
实现 Thread 生命周期管理
设置最大消息数限制
示例: 检查历史长度
var history = thread.GetService<IList<ChatMessage>>();
if (history != null && history.Count > 50)
{
Console.WriteLine("对话历史过长,建议创建新的 Thread 或使用 ChatReducer");
}
7. 最佳实践
推荐做法
- 为每个用户会话创建独立的 Thread
// 用户登录时
var userThread = agent.GetNewThread();
_userThreads[userId] = userThread; // 保存到字典
- 在整个对话过程中复用同一个 Thread
// 所有对话使用同一个 thread
var thread = _userThreads[userId];
await agent.RunAsync(message, thread);
- 流式场景优先使用 Thread
// 流式多轮对话
await foreach (var chunk in agent.RunStreamingAsync(message, thread))
{
// 处理增量响应
}
- 为 Thread 添加元数据 (可选)
thread.Metadata["UserId"] = userId;
thread.Metadata["SessionStartTime"] = DateTime.UtcNow;
常见错误
- 忘记传入 Thread 参数
// 错误: 每次都创建新线程
await agent.RunAsync("第一句话");
await agent.RunAsync("第二句话"); // 忘记了第一句话
- 为每次调用创建新的 Thread
// 错误: 每次都创建新线程
var thread1 = agent.GetNewThread();
await agent.RunAsync("第一句话", thread1);
var thread2 = agent.GetNewThread(); // 不应该创建新的!
await agent.RunAsync("第二句话", thread2);
- 多个用户共用同一个 Thread
// 错误: 会导致用户对话混乱
var sharedThread = agent.GetNewThread();
await agent.RunAsync("用户A的消息", sharedThread);
await agent.RunAsync("用户B的消息", sharedThread); // B能看到A的历史!
三、消息存储
1. ChatMessageStore 抽象基类
ChatMessageStore 抽象基类,定义了消息存储的核心接口。
AgentThread 内部使用 ChatMessageStore 来管理所有的对话消息。
// 获取消息存储
ChatMessageStore? messageStore = thread.GetService<ChatMessageStore>();
核心方法:
// ChatMessageStore 的核心接口 (简化版)
public abstract class ChatMessageStore
{
// 获取所有消息 (按时间顺序,最早的在前)
public abstract Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default);
// 添加新消息
public abstract Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default);
// 序列化状态 (用于持久化)
public abstract JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null);
}
2. InMemoryChatMessageStore
InMemoryChatMessageStore 是 MAF 提供的默认内存存储实现。
关键特性:
| 特性 | 说明 |
|---|---|
| 内存存储 | 所有消息存储在 List |
| 实现 IList | 支持集合操作 (Add, Remove, Clear 等) |
| 高性能 | 读写速度极快,无 I/O 开销 |
| 支持 ChatReducer | 可配置消息裁剪策略 |
| 非持久化 | 程序重启后数据丢失 |
何时创建?
自动创建:当调用 agent.GetNewThread() 时,MAF 会自动创建一个 InMemoryChatMessageStore 实例。
// 内部流程 (简化)
AgentThread GetNewThread()
{
// 1. 创建默认的内存存储
var messageStore = new InMemoryChatMessageStore();
// 2. 创建 Thread 并关联存储
var thread = new AgentThread(messageStore);
return thread;
}
3. 演示:访问 ChatMessageStore
// 创建 Agent
AIAgent agent = chatClient.CreateAIAgent(
instructions: "你是一位友好的助手。",
name: "FriendlyAssistant"
);
// 创建 Thread
AgentThread thread = agent.GetNewThread();
Console.WriteLine("Agent 和 Thread 创建完成");
// 创建几轮对话
// 第一轮对话
Console.WriteLine("用户: 你好!");
var response1 = await agent.RunAsync("你好!", thread);
Console.WriteLine($"Agent: {response1}\n");
// 第二轮对话
Console.WriteLine("用户: 今天天气怎么样?");
var response2 = await agent.RunAsync("今天天气怎么样?", thread);
Console.WriteLine($"Agent: {response2}\n");
// 第三轮对话
Console.WriteLine("用户: 我刚才说了什么?");
var response3 = await agent.RunAsync("我刚才说了什么?", thread);
Console.WriteLine($"Agent: {response3}\n");
Console.WriteLine("三轮对话完成");
// 方法一:获取所有消息
// 从 Thread 获取消息存储
ChatMessageStore? messageStore = thread.GetService<ChatMessageStore>();
if (messageStore != null)
{
Console.WriteLine("成功获取 ChatMessageStore");
// 显示类型信息
new
{
类型 = messageStore.GetType().Name,
全名 = messageStore.GetType().FullName,
是否为内存存储 = messageStore is InMemoryChatMessageStore
}.Display();
}
else
{
Console.WriteLine("无法获取 ChatMessageStore");
}
if (messageStore != null)
{
var messages = await messageStore.GetMessagesAsync();
var messageList = messages.ToList();
Console.WriteLine($"ChatMessageStore 中的消息统计:\n");
new
{
总消息数 = messageList.Count,
System消息 = messageList.Count(m => m.Role == ChatRole.System),
User消息 = messageList.Count(m => m.Role == ChatRole.User),
Assistant消息 = messageList.Count(m => m.Role == ChatRole.Assistant)
}.Display();
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
Console.WriteLine("完整消息列表:\n");
for (int i = 0; i < messageList.Count; i++)
{
var message = messageList[i];
var roleIcon = message.Role.ToString() switch
{
"user" => "用户",
"assistant" => "助理消息",
"system" => "系统消息",
_ => "其他消息"
};
Console.WriteLine($"{i + 1}. {roleIcon} [{message.Role}]:");
// 截断过长的内容
var content = message.Text ?? "";
if (content.Length > 100)
{
content = content.Substring(0, 100) + "...";
}
Console.WriteLine($" {content}\n");
}
Console.WriteLine("消息存储查看完成!");
}
// 方法二:直接操作 InMemoryChatMessageStore (高级)
// 因为 InMemoryChatMessageStore 实现了 IList<ChatMessage>,我们也可以通过 IList 接口直接访问:
IList<ChatMessage>? messageList2 = thread.GetService<IList<ChatMessage>>();
if (messageList2 != null)
{
Console.WriteLine("成功通过 IList<ChatMessage> 接口访问消息存储\n");
Console.WriteLine($"消息统计 (通过 IList 接口):\n");
new
{
总数 = messageList2.Count,
是否只读 = messageList2.IsReadOnly,
第一条消息角色 = messageList2[0].Role.ToString(),
最后一条消息角色 = messageList2[messageList2.Count - 1].Role.ToString()
}.Display();
Console.WriteLine("\n提示:");
Console.WriteLine(" - 通过 GetService<ChatMessageStore>() 获取的是存储抽象接口");
Console.WriteLine(" - 通过 GetService<IList<ChatMessage>>() 获取的是直接的消息列表");
Console.WriteLine(" - 两者指向同一个底层存储,只是访问方式不同");
}
4. 持久化
MAF 提供了两个核心 API 来实现对话持久化:
| API | 作用 | 返回类型 | 使用时机 |
|---|---|---|---|
| thread.Serialize() | 将 Thread 序列化为 JSON | JsonElement | 需要保存对话状态时 |
| agent.DeserializeThread(jsonElement) | 从 JSON 恢复 Thread | AgentThread | 需要恢复对话状态时 |
- 创建模拟对话
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 场景:智能客服机器人 → 转接人工客服 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 1. 创建智能客服 AI Agent
var agent = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "CustomerServiceBot",
Instructions = @"你是某电商平台的智能客服机器人,专门处理售后咨询。
擅长领域:
- 退货退款政策解答
- 订单状态查询
- 物流跟踪
- 常见问题快速解答
回答风格:
- 友好、耐心、专业
- 使用简洁明了的语言
- 提供具体的操作步骤
- 如果问题复杂或涉及特殊情况,建议转人工处理
重要规则:
- 如果涉及订单异常、金额争议、特殊退款等复杂情况,必须明确告知需要转人工客服
- 记录用户问题的关键信息,方便人工客服快速接手"
});
Console.WriteLine("智能客服机器人 Agent 创建完成\n");
// 2. 创建用户咨询会话
var thread = agent.GetNewThread();
Console.WriteLine("用户咨询会话已开启\n");
// 3. 用户第一次咨询 - 退货问题
Console.WriteLine("━━━━ 用户与AI客服对话(第1轮)━━━━\n");
Console.WriteLine("用户:你好,我想退货,订单号是 DD20250109001。");
Console.WriteLine(" 我3天前买的一件羽绒服,收到后发现尺码不合适。\n");
var response1 = await agent.RunAsync(
@"你好,我想退货,订单号是 DD20250109001。
我3天前买的一件羽绒服,收到后发现尺码不合适,想退货退款。",
thread);
Console.WriteLine("AI客服:");
Console.WriteLine(response1.Text);
Console.WriteLine();
// 4. 用户继续咨询 - 发现订单状态异常
Console.WriteLine("━━━━ 用户与AI客服对话(第2轮)━━━━\n");
Console.WriteLine("用户:但是我看订单状态显示'已签收',可是我明明没有签收啊!");
Console.WriteLine(" 而且物流信息显示是昨天送到的,但我根本没收到货。\n");
var response2 = await agent.RunAsync(
@"但是我看订单状态显示'已签收',可是我明明没有签收啊!
而且物流信息显示是昨天送到的,但我根本没收到货,这是怎么回事?",
thread);
Console.WriteLine("AI客服:");
Console.WriteLine(response2.Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" AI客服判断:订单状态异常,需要转人工处理 ");
Console.WriteLine(" 对话上下文:用户信息、订单号、问题描述 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
- 序列化对话状态,关键 API:thread.Serialize()
- 返回 JsonElement 类型
- 包含完整的对话历史(用户消息 + Agent 响应)
- 可以序列化为字符串保存到任何存储介质
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" AI客服判断:需要转人工 → 保存对话上下文 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
Console.WriteLine("AI客服:您好,我发现您的订单存在异常状态(显示已签收但您未收到货),");
Console.WriteLine(" 这种情况需要人工客服介入核实物流信息和订单状态。");
Console.WriteLine(" 正在为您转接人工客服,请稍候...\n");
// 1. 序列化 Thread(保存完整对话历史)
JsonElement serializedThread = thread.Serialize();
Console.WriteLine("Thread 已序列化为 JsonElement\n");
// 2. 将 JsonElement 转换为字符串
string jsonString = JsonSerializer.Serialize(serializedThread);
Console.WriteLine($"序列化后的对话数据大小:{jsonString.Length} 字符\n");
// 3. 保存到临时文件(模拟客服系统的会话存储)
string tempFilePath = Path.GetTempFileName();
await File.WriteAllTextAsync(tempFilePath, jsonString);
Console.WriteLine($"对话历史已保存到客服系统:");
Console.WriteLine($" {tempFilePath}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 关键信息已保存: ");
Console.WriteLine(" - 用户咨询的完整对话历史");
Console.WriteLine(" - 订单号:DD20250109001");
Console.WriteLine(" - 问题:订单状态异常(已签收但未收到货)");
Console.WriteLine(" 模拟场景:正在排队等待人工客服接入... ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
- 反序列化恢复对话,关键 API:agent.DeserializeThread(jsonElement)
- 接受 JsonElement 参数
- 返回完全恢复的 AgentThread 对象
- 包含所有历史消息和上下文
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("人工客服接入 → 加载完整对话历史 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
Console.WriteLine("模拟场景:人工客服 '张小美' 接入工单...\n");
// 1. 从客服系统加载序列化的对话数据
string loadedJsonString = await File.ReadAllTextAsync(tempFilePath);
Console.WriteLine($"从客服系统加载了 {loadedJsonString.Length} 字符的对话数据\n");
// 2. 反序列化为 JsonElement
JsonElement reloadedSerializedThread = JsonSerializer.Deserialize<JsonElement>(loadedJsonString);
Console.WriteLine("JSON 数据已解析为 JsonElement\n");
// 3:使用 Agent 反序列化恢复 Thread
AgentThread resumedThread = agent.DeserializeThread(reloadedSerializedThread);
Console.WriteLine("Thread 已完全恢复,人工客服可以查看完整对话历史\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 人工客服工作台显示: ");
Console.WriteLine(" 用户之前与AI的完整对话");
Console.WriteLine(" 订单号:DD20250109001");
Console.WriteLine(" 问题类型:订单状态异常");
Console.WriteLine(" 用户诉求:未收到货但显示已签收");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
- 使用恢复的 Thread 继续对话
Console.WriteLine("\n━━━━ 人工客服与用户对话 ━━━━\n");
Console.WriteLine("人工客服(张小美):您好,我是人工客服张小美。");
Console.WriteLine(" 我已经看到您刚才反馈的订单 DD20250109001 的问题了。");
Console.WriteLine(" 系统显示已签收但您没收到货,我马上帮您核实。\n");
// 使用恢复的 Thread 继续对话
// 人工客服基于对话历史,给出专业的处理方案
var response3 = await agent.RunAsync(
@"[人工客服身份]
我已经查看了您的订单 DD20250109001 和物流信息。
经核实:
1. 快递员昨天17:30 在您的小区门口进行了签收操作
2. 但签收人并非您本人
3. 快递员备注:'放在小区菜鸟驿站'
请问您是否有去菜鸟驿站查看?如果驿站也没有,我立即为您启动丢件赔付流程。",
resumedThread);
Console.WriteLine("AI客服(代表人工客服回复):");
Console.WriteLine(response3.Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 成功!人工客服基于完整对话历史解决问题 ");
Console.WriteLine(" 关键价值体现:");
Console.WriteLine(" 1. 无需让用户重复描述问题(最大痛点解决!)");
Console.WriteLine(" 2. 人工客服直接看到订单号和问题详情");
Console.WriteLine(" 3. 快速定位问题,提升客户满意度");
Console.WriteLine(" 4. 节省沟通时间,提高客服效率");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// 清理临时文件
File.Delete(tempFilePath);
四、智能压缩
1. 对话历史管理
- 与 MEAI 的关系
flowchart LR
A[MEAI: IChatReducer 基础] --> B[MAF: Agent 集成]
B --> C[InMemoryChatMessageStore]
B --> D[ChatMessageStoreFactory]
C --> E[自动历史裁剪]
D --> E
style A fill:#e1f5ff
style B fill:#fff4e6
style E fill:#e8f5e9
| 特性 | MEAI 直接使用 | MAF Agent 集成 |
|---|---|---|
| 使用方式 | ChatClientBuilder.UseChatReducer() | ChatMessageStoreFactory 配置 |
| 应用时机 | 每次 CompleteAsync() 前 | 每次 RunAsync() 前自动应用 |
| 状态管理 | 手动管理 List |
agentThread 自动管理 |
| 持久化 | 需自行实现 | 可通过 serialize() 保存状态 |
| 适用场景 | 直接调用 ChatClient | Agent 多轮对话系统 |
MAF 中的重要约束,Chat Reducer 仅适用于本地存储模式:
OpenAI Chat Completion - 聊天历史在客户端管理,可使用 Reducer
Azure AI Foundry Agents - 聊天历史由服务端管理,无法使用 Reducer
其他服务端存储方案 - 需服务端自行实现历史管理
2. MAF Agent 集成 MessageCountingChatReducer
创建一个笑话 Agent,使用 MessageCountingChatReducer 限制历史消息数量,防止上下文爆炸。
// 1. 创建带 Reducer 的 Agent
var options = new ChatClientAgentOptions
{
Instructions = "你是一个擅长讲笑话的幽默助手。",
Name = "笑话大师",
// 关键配置:通过工厂方法创建带 Reducer 的消息存储
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(
#pragma warning disable MEAI001
chatReducer: new MessageCountingChatReducer(targetCount: 2), // 保留最近 2 条消息
serializedStoreState: ctx.SerializedState,
jsonSerializerOptions: ctx.JsonSerializerOptions
)
};
// 创建 Agent,配置 ChatMessageStoreFactory
var agent = chatClient.CreateAIAgent(options);
Console.WriteLine("Agent 已创建(集成 MessageCountingChatReducer)");
Console.WriteLine(" - 策略: 保留最近 2 条非系统消息");
Console.WriteLine(" - 存储: InMemoryChatMessageStore\n");
// 2. 创建 Thread 并进行多轮对话
var thread = agent.GetNewThread();
Console.WriteLine("═══════════════════════════════════════");
Console.WriteLine("第 1 轮对话");
Console.WriteLine("═══════════════════════════════════════\n");
var response1 = await agent.RunAsync("讲一个关于海盗的笑话", thread);
// 获取 Thread 底层的消息列表(用于观察历史记录)
var chatHistory = thread.GetService<IList<ChatMessage>>();
Console.WriteLine($"{response1.Text}\n");
Console.WriteLine($"当前历史消息数: {chatHistory?.Count} 条\n");
Console.WriteLine("═══════════════════════════════════════");
Console.WriteLine("第 2 轮对话");
Console.WriteLine("═══════════════════════════════════════\n");
var response2 = await agent.RunAsync("讲一个关于机器人的笑话", thread);
Console.WriteLine($"{response2.Text}\n");
Console.WriteLine($"当前历史消息数: {chatHistory?.Count} 条\n");
Console.WriteLine("═══════════════════════════════════════");
Console.WriteLine("第 3 轮对话");
Console.WriteLine("═══════════════════════════════════════\n");
var response3 = await agent.RunAsync("讲一个关于狐猴的笑话", thread);
Console.WriteLine($"{response3.Text}\n");
Console.WriteLine($"当前历史消息数: {chatHistory?.Count} 条\n");
// 3. 验证裁剪效果
Console.WriteLine("═══════════════════════════════════════");
Console.WriteLine("测试:引用被裁剪的历史消息");
Console.WriteLine("═══════════════════════════════════════\n");
var response4 = await agent.RunAsync(
"把刚才关于海盗的笑话再讲一遍,但加上表情符号,用鹦鹉的口吻",
thread
);
Console.WriteLine($"{response4.Text}\n");
Console.WriteLine($"当前历史消息数: {chatHistory?.Count} 条\n");
Console.WriteLine(" 注意:由于海盗笑话已被裁剪,Agent 无法准确回忆!");
// 4. 查看实际的历史消息内容
Console.WriteLine("当前 Thread 中的完整消息列表:\n");
if (chatHistory != null)
{
for (int i = 0; i < chatHistory.Count; i++)
{
var msg = chatHistory[i];
var preview = msg.Text?.Length > 50
? msg.Text.Substring(0, 50) + "..."
: msg.Text;
Console.WriteLine($"{i + 1}. [{msg.Role}] {preview}");
}
Console.WriteLine($"\n总消息数: {chatHistory.Count} 条");
Console.WriteLine("符合 Reducer 配置: 系统消息 + 最近 2 条用户/助手消息");
}
else
{
Console.WriteLine("无法访问 ChatHistory");
}
3. 持久化下的 Reducer 应用
// 1. 序列化带 Reducer 的 Thread
// 序列化当前 Thread(包含裁剪后的历史)
var serializedThread = thread.Serialize();
Console.WriteLine("Thread 已序列化\n");
Console.WriteLine("序列化内容预览:");
Console.WriteLine(serializedThread.ToString().Substring(0, Math.Min(500, serializedThread.ToString().Length)));
Console.WriteLine("\n...(已省略)\n");
// 在实际应用中,这里可以保存到数据库或文件
Console.WriteLine("在生产环境中,可将此序列化数据存储到:");
Console.WriteLine(" - 数据库 (SQL Server, Cosmos DB)");
Console.WriteLine(" - 缓存 (Redis)");
Console.WriteLine(" - 文件系统\n");
// 2. 反序列化并恢复对话
// 模拟从存储中读取序列化数据后恢复 Thread
var restoredThread = agent.DeserializeThread(serializedThread);
Console.WriteLine("Thread 已从序列化数据恢复\n");
// 继续对话
var response5 = await agent.RunAsync("讲一个关于程序员的笑话", restoredThread);
Console.WriteLine($"{response5.Text}\n");
var restoredHistory = restoredThread.GetService<IList<ChatMessage>>();
Console.WriteLine($"恢复后的历史消息数: {restoredHistory?.Count} 条");
Console.WriteLine("Reducer 仍然生效,自动裁剪超出限制的消息");
4. 自定义 Reducer 集成到 MAF
场景说明:创建一个自定义 Reducer,保留系统消息和包含特定关键词的消息。
- 实现自定义 IChatReducer
/// <summary>
/// 自定义 Reducer:保留包含特定关键词的重要消息
/// </summary>
public class KeywordBasedChatReducer : IChatReducer
{
private readonly string[] _keywords;
private readonly int _maxMessages;
public KeywordBasedChatReducer(string[] keywords, int maxMessages = 5)
{
_keywords = keywords;
_maxMessages = maxMessages;
}
public Task<IEnumerable<ChatMessage>> ReduceAsync(
IEnumerable<ChatMessage> messages,
CancellationToken cancellationToken)
{
var messageList = messages.ToList();
// 1. 保留所有系统消息
var systemMessages = messageList.Where(m => m.Role == ChatRole.System).ToList();
// 2. 保留包含关键词的消息
var keywordMessages = messageList
.Where(m => m.Role != ChatRole.System &&
_keywords.Any(kw => m.Text?.Contains(kw, StringComparison.OrdinalIgnoreCase) == true))
.ToList();
// 3. 保留最近的消息(排除已包含的关键词消息)
var recentMessages = messageList
.Where(m => m.Role != ChatRole.System && !keywordMessages.Contains(m))
.TakeLast(_maxMessages - keywordMessages.Count)
.ToList();
// 4. 合并并按原始顺序排序
var reducedMessages = systemMessages
.Concat(keywordMessages)
.Concat(recentMessages)
.OrderBy(m => messageList.IndexOf(m))
.ToList();
return Task.FromResult<IEnumerable<ChatMessage>>(reducedMessages);
}
}
- 集成自定义 Reducer 到 Agent
var customReducer = new KeywordBasedChatReducer(
keywords: new[] { "重要", "订单", "支付" },
maxMessages: 3
);
var agentWithCustomReducer = chatClient.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "你是一个电商客服助手。",
Name = "智能客服",
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(
chatReducer: customReducer,
serializedStoreState: ctx.SerializedState,
jsonSerializerOptions: ctx.JsonSerializerOptions
)
});
Console.WriteLine("Agent 已创建(集成自定义 Reducer)");
Console.WriteLine(" - 关键词: 重要、订单、支付");
Console.WriteLine(" - 最大消息数: 3 条\n");
- 测试自定义 Reducer 效果
var customThread = agentWithCustomReducer.GetNewThread();
IList<ChatMessage> customHistory = null!;
// 模拟多轮对话
var queries = new[]
{
"你好,我想咨询一下",
"我的重要订单号是 ORD12345",
"发货了吗?",
"运费是多少?",
"支付方式有哪些?",
"可以开发票吗?"
};
foreach (var query in queries)
{
var resp = await agentWithCustomReducer.RunAsync(query, customThread);
if(customHistory is null)
customHistory = customThread.GetService<IList<ChatMessage>>()!;
Console.WriteLine($"用户: {query}");
Console.WriteLine($"客服: {resp.Text}");
Console.WriteLine($"历史消息数: {customHistory?.Count} 条\n");
}
Console.WriteLine("═══════════════════════════════════════");
Console.WriteLine("最终保留的消息:");
Console.WriteLine("═══════════════════════════════════════\n");
if (customHistory != null)
{
foreach (var msg in customHistory)
{
var preview = msg.Text?.Length > 60 ? msg.Text.Substring(0, 60) + "..." : msg.Text;
Console.WriteLine($"[{msg.Role}] {preview}");
}
Console.WriteLine("\n包含'重要'和'支付'关键词的消息被优先保留");
}
5. MAF Chat Reducer 最佳实践
- 选择合适的 Reducer 策略
| 场景 | 推荐 Reducer | MAF 配置要点 |
|---|---|---|
| 短对话系统 | MessageCountingChatReducer(2-5) | 保留最近对话即可 |
| 客服机器人 | MessageCountingChatReducer(10) | 保留足够上下文 |
| 医疗咨询 | SummarizingChatReducer | 保留完整病史语义 |
| 关键信息追踪 | 自定义 Reducer(关键词过滤) | 保留重要业务消息 |
- 与持久化结合的注意事项
// 正确:序列化时自动包含裁剪后的历史
var serialized = thread.Serialize();
// 保存到数据库...
// 正确:反序列化时 Reducer 仍然生效
var restored = agent.DeserializeThread(serialized);
- 多 Agent 场景的 Reducer 策略
// 不同 Agent 可使用不同 Reducer 策略
var summaryAgent = chatClient.CreateAIAgent(new ChatClientAgentOptions
{
Name = "总结助手",
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(
reducer: new MessageCountingChatReducer(20), // 保留更多上下文
serializedState: ctx.SerializedState,
jsonSerializerOptions: ctx.JsonSerializerOptions
)
});
var quickAgent = chatClient.CreateAIAgent(new ChatClientAgentOptions
{
Name = "快速问答",
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(
reducer: new MessageCountingChatReducer(3), // 仅保留最近对话
serializedState: ctx.SerializedState,
jsonSerializerOptions: ctx.JsonSerializerOptions
)
});
- 性能优化建议
// 推荐:使用 MessageCountingChatReducer(零延迟)
new MessageCountingChatReducer(targetCount: 10);
// 谨慎:SummarizingChatReducer 会额外调用 LLM
new SummarizingChatReducer(
chatClient: lightweightClient, // 使用更小的模型(如 GPT-3.5)
targetCount: 5,
threshold: 2
);
- 避免的常见错误
// 错误:在 Azure AI Foundry Agent 中使用 Reducer(无效)
var foundryAgent = azureFoundryClient.CreateAIAgent(new ChatClientAgentOptions
{
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(
reducer: new MessageCountingChatReducer(5), // 服务端存储不支持
serializedState: ctx.SerializedState,
jsonSerializerOptions: ctx.JsonSerializerOptions
)
});
// 正确:仅在本地存储模式下使用
var openAIAgent = openAIClient.GetChatClient("gpt-4o").CreateAIAgent(...);
6. MAF vs MEAI Reducer 对比
| 维度 | MEAI 直接使用 | MAF Agent 集成 |
|---|---|---|
| 配置方式 | ChatClientBuilder.UseChatReducer() | ChatMessageStoreFactory |
| 状态管理 | 手动维护 List |
AgentThread 自动管理 |
| 持久化 | 需自行序列化 | Serialize()/DeserializeThread() |
| 多轮对话 | 需手动传递历史 | RunAsync(message, thread) 自动处理 |
| 适用场景 | 直接调用 ChatClient | Agent 多轮对话系统 |
| 复杂度 | 较低 | 较高(封装更完善) |
选择建议:
- 使用 MEAI 直接方式:简单的单次调用、自定义对话流程
- 使用 MAF 集成方式:复杂的 Agent 系统、需要持久化、多轮对话
五、工具调用
1. MAF 中的 Function Calling 特点
- 自动调用:Agent 自动判断何时调用工具,无需手动干预
- 多轮调用:一次对话可以调用多个工具
- 与 Thread 集成:工具调用记录自动保存到 Thread
- 类型安全:使用 C# 方法,编译时类型检查
- 描述驱动:通过 [Description] 让 AI 理解工具用途
2. Agent 的工具调用
- 定义函数工具
// 定义天气查询函数
[Description("查询指定城市的当前天气信息,包括天气状况和温度")]
string GetWeather([Description("要查询天气的城市名称,例如: 北京、上海、深圳")] string city)
{
// 模拟天气查询 (实际应用中应该调用真实的天气 API)
var weatherData = new Dictionary<string, (string condition, int temperature)>
{
["北京"] = ("晴天", 15),
["上海"] = ("多云", 20),
["深圳"] = ("阴天", 25),
["广州"] = ("小雨", 22),
["杭州"] = ("晴天", 18)
};
if (weatherData.TryGetValue(city, out var weather))
{
return $"{city}的天气: {weather.condition}, 温度: {weather.temperature}°C";
}
return $"抱歉,暂时无法获取{city}的天气信息";
}
Console.WriteLine("天气查询函数定义完成");
- 创建 Agent 并注册工具
// 使用 AIFunctionFactory 创建工具
var weatherTool = AIFunctionFactory.Create(GetWeather);
// 创建 Agent 并注册工具
AIAgent weatherAssistant = chatClient.CreateAIAgent(
instructions: "你是一位专业的天气助手。当用户询问某个城市的天气时,你应该使用 GetWeather 工具查询实时天气信息,然后用友好的语言告诉用户。",
name: "WeatherAssistant",
tools: [weatherTool] // 注册工具
);
Console.WriteLine("天气助手 Agent 创建完成");
- 测试工具调用 (同步模式)
// 测试 1: 明确的天气查询
var query1 = "上海的天气怎么样?";
Console.WriteLine($"用户: {query1}");
Console.WriteLine("\nAgent 处理中...");
Console.WriteLine("[观察] Agent 会自动调用 GetWeather(\"上海\") 工具\n");
var response1 = await weatherAssistant.RunAsync(query1);
Console.WriteLine($"{weatherAssistant.Name}: {response1}");
Console.WriteLine("\n工具调用成功! Agent 自动识别了天气查询意图并调用了工具。");
// 测试 2: 多城市对比查询
var query2 = "北京和深圳哪个城市天气更好?";
Console.WriteLine($"用户: {query2}");
Console.WriteLine("\nAgent 处理中...");
Console.WriteLine("[观察] Agent 可能会调用两次 GetWeather 工具\n");
var response2 = await weatherAssistant.RunAsync(query2);
Console.WriteLine($"{weatherAssistant.Name}: {response2}");
Console.WriteLine("\nAgent 能够智能地多次调用工具来完成任务!");
// 测试 3: 不需要工具的查询
var query3 = "你好,你是谁?";
Console.WriteLine($"用户: {query3}");
Console.WriteLine("\nAgent 处理中...");
Console.WriteLine("[观察] Agent 会判断不需要调用工具,直接回答\n");
var response3 = await weatherAssistant.RunAsync(query3);
Console.WriteLine($"{weatherAssistant.Name}: {response3}");
Console.WriteLine("\nAgent 能够智能判断何时需要工具,何时直接回答!");
- 流式调用 + 调用工具
var query4 = "杭州今天适合户外活动吗?";
Console.WriteLine($"用户: {query4}");
Console.WriteLine($"\n{weatherAssistant.Name} (流式响应):\n");
// 流式调用
await foreach (var chunk in weatherAssistant.RunStreamingAsync(query4))
{
Console.Write(chunk);
}
Console.WriteLine("\n\n流式工具调用完成!");
3. 函数工具 + AgentThread
函数工具与 AgentThread 完美集成,工具调用的历史也会被记录到 Thread 中。
我们将创建一个旅行助手,结合多个工具和多轮对话。
- 定义多个工具函数
// 工具 1: 景点推荐
[Description("推荐指定城市的热门旅游景点")]
string GetAttractions([Description("城市名称")] string city)
{
var attractions = new Dictionary<string, string>
{
["北京"] = "故宫、长城、天坛、颐和园",
["上海"] = "外滩、东方明珠、豫园、南京路",
["深圳"] = "世界之窗、欢乐谷、大梅沙、莲花山公园",
["杭州"] = "西湖、灵隐寺、雷峰塔、宋城"
};
return attractions.TryGetValue(city, out var value)
? $"{city}的热门景点: {value}"
: $"暂无{city}的景点信息";
}
// 工具 2: 美食推荐
[Description("推荐指定城市的特色美食")]
string GetLocalFood([Description("城市名称")] string city)
{
var foods = new Dictionary<string, string>
{
["北京"] = "北京烤鸭、炸酱面、涮羊肉、豆汁",
["上海"] = "小笼包、生煎包、本帮菜、糖醋小排",
["深圳"] = "潮汕火锅、早茶点心、海鲜、客家菜",
["杭州"] = "西湖醋鱼、东坡肉、龙井虾仁、叫化鸡"
};
return foods.TryGetValue(city, out var value)
? $"{city}的特色美食: {value}"
: $"暂无{city}的美食信息";
}
// 工具 3: 酒店价格查询
[Description("查询指定城市的酒店平均价格")]
string GetHotelPrice([Description("城市名称")] string city)
{
var prices = new Dictionary<string, string>
{
["北京"] = "经济型 200-400元, 舒适型 400-800元, 豪华型 800元以上",
["上海"] = "经济型 250-450元, 舒适型 450-900元, 豪华型 900元以上",
["深圳"] = "经济型 230-430元, 舒适型 430-850元, 豪华型 850元以上",
["杭州"] = "经济型 180-380元, 舒适型 380-750元, 豪华型 750元以上"
};
return prices.TryGetValue(city, out var value)
? $"{city}的酒店价格: {value}"
: $"暂无{city}的酒店价格信息";
}
- 创建带多工具的 Agent
// 创建旅行助手 Agent,注册所有工具
AIAgent travelAssistant = chatClient.CreateAIAgent(
instructions: "你是一位专业的旅行顾问。你可以使用工具查询景点、美食和酒店信息,为用户提供全面的旅行建议。",
name: "TravelAssistant",
tools: [
AIFunctionFactory.Create(GetWeather),
AIFunctionFactory.Create(GetAttractions),
AIFunctionFactory.Create(GetLocalFood),
AIFunctionFactory.Create(GetHotelPrice)
]
);
// 创建对话线程
AgentThread travelThread = travelAssistant.GetNewThread();
Console.WriteLine("旅行助手 Agent 和 Thread 创建完成");
- 多轮对话测试
// 第一轮: 初步咨询
var travel1 = "我想去杭州旅游,能给我一些建议吗?";
Console.WriteLine($"用户: {travel1}");
Console.WriteLine("\nAgent 处理中...\n");
var travelResponse1 = await travelAssistant.RunAsync(travel1, travelThread);
Console.WriteLine($"{travelAssistant.Name}: {travelResponse1}");
// 第二轮: 深入询问
var travel2 = "那边有什么好吃的?";
Console.WriteLine($"用户: {travel2}");
Console.WriteLine("\nAgent 处理中...\n");
// Agent 会记住上一轮说的是杭州
var travelResponse2 = await travelAssistant.RunAsync(travel2, travelThread);
Console.WriteLine($"{travelAssistant.Name}: {travelResponse2}");
Console.WriteLine("\nAgent 记住了之前讨论的城市!");
// 第三轮: 综合决策
var travel3 = "酒店价格怎么样?我预算不高。";
Console.WriteLine($"用户: {travel3}");
Console.WriteLine("\nAgent 处理中...\n");
var travelResponse3 = await travelAssistant.RunAsync(travel3, travelThread);
Console.WriteLine($"{travelAssistant.Name}: {travelResponse3}");
Console.WriteLine("\nAgent 综合使用了多个工具和上下文!");
4. 查看工具调用历史
// 查看 Thread 中的历史消息,包括工具调用记录
var travelHistory = travelThread.GetService<IList<ChatMessage>>();
if (travelHistory != null)
{
Console.WriteLine("对话和工具调用历史:\n");
for (int i = 0; i < travelHistory.Count; i++)
{
var message = travelHistory[i];
var roleIcon = message.Role.Value switch
{
"user" => "用户",
"assistant" => "助理消息",
"tool" => "工具消息",
_ => "其他消息"
};
Console.WriteLine($"{i + 1}. {roleIcon} [{message.Role}]");
// 检查是否包含函数调用
var functionCalls = message.Contents.OfType<FunctionCallContent>();
if (functionCalls.Any())
{
foreach (var call in functionCalls)
{
Console.WriteLine($" 调用工具: {call.Name}");
Console.WriteLine($" 参数: {JsonSerializer.Serialize(call.Arguments)}");
}
}
else if (message.Role == ChatRole.Tool)
{
// 工具返回结果
Console.WriteLine($" 工具返回: {message.Text?.Substring(0, Math.Min(50, message.Text.Length))}...");
}
else
{
// 普通消息
var preview = message.Text?.Substring(0, Math.Min(60, message.Text.Length));
Console.WriteLine($" {preview}...");
}
Console.WriteLine();
}
Console.WriteLine("可以看到完整的工具调用流程!");
}
5. ChatToolMode - 控制工具调用行为
ChatToolMode 让你可以精确控制 Agent 何时以及如何使用工具。
三种模式:
| 模式 | 说明 | 使用场景 |
|---|---|---|
| Auto | 自动模式 (默认):Agent 自行判断是否需要调用工具 | 大多数场景 |
| RequiredAny | 强制模式:Agent 必须至少调用一次工具 | 确保获取实时数据 |
| None | 禁用模式Agent:不能调用任何工具 | 只需要对话,不需要工具 |
配置方式:通过 ChatOptions 的 ToolMode 属性配置
var options = new ChatClientAgentOptions(instructions: "...", name :"....", tools: [...])
{
ChatOptions = new ChatOptions
{
ToolMode = ChatToolMode.RequiredAny // 强制使用工具
}
};
实战对比:不同模式的效果
- 模式 1: Auto (自动模式) - 默认行为
// Auto 模式: Agent 自行判断
var agentAuto = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions(instructions: "你是天气助手", tools: [AIFunctionFactory.Create(GetWeather)])
{
Name = "WeatherAgent_Auto",
ChatOptions = new ChatOptions
{
ToolMode = ChatToolMode.Auto // 自动模式
}
}
);
Console.WriteLine("模式: Auto (自动)\n");
// 测试 1: 需要工具
var autoResponse1 = await agentAuto.RunAsync("北京天气如何?");
Console.WriteLine($"需要工具的问题: 北京天气如何?");
Console.WriteLine($"响应: {autoResponse1}\n");
// 测试 2: 不需要工具
var autoResponse2 = await agentAuto.RunAsync("你好");
Console.WriteLine($"不需要工具的问题: 你好");
Console.WriteLine($"响应: {autoResponse2}\n");
Console.WriteLine("Auto 模式: Agent 智能判断是否使用工具");
- 模式 2: Required (强制模式)
// Required 模式: 必须使用工具
var agentRequired = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions(instructions: "你是天气助手,必须使用工具查询实时天气", tools: [AIFunctionFactory.Create(GetWeather)])
{
Name = "WeatherAgent_Required",
ChatOptions = new ChatOptions
{
ToolMode = ChatToolMode.RequireAny // 强制模式
}
}
);
Console.WriteLine("模式: Required (强制)\n");
// 测试: 即使是简单问候,也会尝试使用工具
var requiredResponse = await agentRequired.RunAsync("深圳怎么样?");
Console.WriteLine($"问题: 深圳怎么样?");
Console.WriteLine($"响应: {requiredResponse}\n");
Console.WriteLine("Required 模式: Agent 必须调用工具");
6. 最佳实践
函数定义最佳实践
- 清晰的 Description
// 好的描述: 清晰、具体
[Description("查询指定城市的实时天气信息,包括天气状况、温度和湿度")]
string GetWeather([Description("城市名称,例如: 北京、上海")] string city)
// 不好的描述: 模糊、不清楚
[Description("获取天气")]
string GetWeather(string city)
- 参数类型选择
// 推荐: 使用基本类型 (string, int, bool, enum)
string GetWeather(string city, bool includeHumidity = false)
// 复杂类型: 需要确保能正确序列化
string GetWeather(WeatherQuery query)
- 返回值规范
// 推荐: 返回 string (AI 容易理解)
string GetWeather(string city) => $"天气: 晴, 温度: 25°C";
// 也可以: 返回对象 (会自动序列化为 JSON)
WeatherInfo GetWeather(string city) => new WeatherInfo { ... };
- 错误处理
string GetWeather(string city)
{
try
{
// 调用外部 API
return CallWeatherAPI(city);
}
catch (Exception ex)
{
// 返回友好的错误消息,而不是抛出异常
return $"无法获取{city}的天气信息: {ex.Message}";
}
}
Agent 配置最佳实践
- Instructions 中说明工具使用
// 明确告知 Agent 何时使用工具
instructions: "你是天气助手。当用户询问天气时,使用 GetWeather 工具查询实时信息。"
// 不够明确
instructions: "你是助手。"
- 工具命名规范
// 使用动词开头的清晰命名
AIFunctionFactory.Create(GetWeather, name: "GetWeather")
AIFunctionFactory.Create(SearchProducts, name: "SearchProducts")
// 模糊的命名
AIFunctionFactory.Create(Query, name: "Query")
- 选择合适的 ToolMode
// 大多数情况使用 Auto
ToolMode = ChatToolMode.Auto
// 需要确保获取实时数据时使用 Required
ToolMode = ChatToolMode.Required
// 纯对话场景使用 None
ToolMode = ChatToolMode.None
常见错误
- 忘记添加 Description
// 错误: AI 无法理解工具用途
string GetWeather(string city) => ...;
// 正确: 添加描述
[Description("查询城市天气")]
string GetWeather([Description("城市名称")] string city) => ...;
- 工具执行时间过长
// 错误: 长时间阻塞
string GetData(string id)
{
Thread.Sleep(30000); // 30秒
return ...;
}
// 正确: 异步操作或超时控制
async Task<string> GetDataAsync(string id)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
return await FetchDataAsync(id, cts.Token);
}
- 工具返回过多数据
// 错误: 返回大量数据
string GetAllProducts() => JsonSerializer.Serialize(GetAll()); // 10000条记录
// 正确: 返回摘要或分页
string GetProductSummary(int page = 1, int pageSize = 10) => ...;
六、插件系统
1. MAF 中的插件设计理念
在 Microsoft Agent Framework 中,插件系统基于以下设计原则:
依赖注入优先
插件依赖通过构造函数注入
使用 IServiceProvider 解析服务
支持复杂的依赖关系
选择性暴露
通过 AsAITools() 方法显式声明公开的工具
内部方法不会自动暴露给 Agent
精确控制插件能力边界
接口标准化
定义统一的插件接口
便于插件管理和动态加载
提供一致的开发体验
插件 vs 直接函数调用
- 直接函数调用的问题
// 所有工具都直接定义
var tools = new[]
{
AIFunctionFactory.Create(GetWeather),
AIFunctionFactory.Create(GetTime),
AIFunctionFactory.Create(BookMeetingRoom),
AIFunctionFactory.Create(QueryExpense),
// ... 越来越多的函数
};
// 问题:
// 1. 代码臃肿,难以维护
// 2. 依赖关系复杂
// 3. 无法按模块管理
// 4. 测试困难
- 使用插件系统
// 按功能模块组织
services.AddSingleton<WeatherPlugin>();
services.AddSingleton<TimePlugin>();
services.AddSingleton<OfficePlugin>();
services.AddSingleton<FinancePlugin>();
// 插件自动管理依赖和工具暴露
var tools = serviceProvider
.GetServices<IAgentPlugin>()
.SelectMany(p => p.AsAITools());
// 优势:
// 1. 模块化清晰
// 2. 依赖自动注入
// 3. 易于测试和维护
// 4. 支持动态加载
2. 创建简单插件类
- 创建一个天气查询插件(WeatherPlugin)。
/// <summary>
/// 天气查询插件
/// </summary>
public sealed class WeatherPlugin
{
/// <summary>
/// 查询指定城市的天气信息
/// </summary>
[Description("查询指定城市的天气信息,返回温度、天气状况和空气质量")]
public string GetWeather(
[Description("城市名称,例如:北京、上海、深圳")] string location)
{
// 模拟天气数据(实际应用中应调用天气 API)
var weatherData = location switch
{
"北京" => "晴朗,温度 15°C,空气质量良好,适合出行",
"上海" => "多云,温度 18°C,空气质量优,早晚温差大",
"深圳" => "阴天,温度 22°C,湿度较高,注意防潮",
"西雅图" => "小雨,温度 10°C,建议携带雨具",
_ => $"暂无 {location} 的天气数据"
};
return $"{location} 天气情况:{weatherData}";
}
}
- 注册插件
// 1. 创建插件实例
var weatherPlugin = new WeatherPlugin();
// 2. 使用 AIFunctionFactory 转换插件方法
var tools = new[]
{
AIFunctionFactory.Create(weatherPlugin.GetWeather)
};
// 3. 注册到 Agent
var agent = chatClient.CreateAIAgent(
instructions: "你是企业智能工作助手",
name: "WorkAssistant",
tools: tools
);
- 测试
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 企业智能工作助手 - 天气查询测试 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
try
{
// 1. 创建插件实例
var weatherPlugin = new WeatherPlugin();
Console.WriteLine("天气插件实例创建成功\n");
// 2. 转换为 AI 工具
var tools = new[]
{
AIFunctionFactory.Create(weatherPlugin.GetWeather)
};
Console.WriteLine("插件工具注册完成\n");
// 3. 创建 ChatClient
var chatClient = AIClientHelper.GetDefaultChatClient();
// 4. 创建 Agent
var agent = chatClient.CreateAIAgent(
instructions: "你是一个企业智能工作助手,帮助员工查询工作相关信息。回答要简洁专业。",
name: "WorkAssistant",
tools: tools
);
Console.WriteLine("Agent 创建完成\n");
// 5. 测试插件功能
var employeeRequests = new[]
{
"北京明天天气怎么样?我要去出差",
"帮我查询一下上海的天气情况"
};
foreach (var request in employeeRequests)
{
Console.WriteLine($"员工请求: {request}");
var response = await agent.RunAsync(request);
Console.WriteLine($"助手回复: {response.Text}\n");
}
Console.WriteLine("测试完成!插件系统运行正常。");
}
catch (Exception ex)
{
Console.WriteLine("测试失败:");
new {
错误类型 = ex.GetType().Name,
错误消息 = ex.Message
}.Display();
}
3. 依赖注入管理
在实际企业应用中,插件通常需要依赖其他服务(如数据库、API 客户端、配置服务等)。
所以我们要为插件实现依赖注入
- 创建 WeatherProvider 服务类,负责实际的天气查询逻辑:
/// <summary>
/// 天气数据提供者服务
/// 负责实际的天气查询逻辑
/// </summary>
public sealed class WeatherProvider
{
/// <summary>
/// 查询指定位置的天气信息
/// </summary>
/// <remarks>
/// 在实际应用中,这里应该调用真实的天气 API
/// 例如:OpenWeatherMap、和风天气等
/// </remarks>
public string GetWeather(string location)
{
// 模拟数据库或 API 查询
// 实际应用中应该:
// 1. 调用天气 API
// 2. 处理响应数据
// 3. 进行缓存优化
// 4. 处理异常情况
var weatherData = location switch
{
"北京" => "晴朗,温度 15°C,空气质量良好,适合出行",
"上海" => "多云,温度 18°C,空气质量优,早晚温差大",
"深圳" => "阴天,温度 22°C,湿度较高,注意防潮",
"西雅图" => "小雨,温度 10°C,建议携带雨具",
"纽约" => "晴转多云,温度 5°C,体感较冷",
_ => $"暂无 {location} 的天气数据"
};
return $"{location} 天气情况:{weatherData}";
}
}
Console.WriteLine("WeatherProvider 服务定义完成");
- 使用构造函数注入
/// <summary>
/// 天气插件 - 使用依赖注入
/// </summary>
public sealed class WeatherPlugin
{
private readonly WeatherProvider _weatherProvider;
/// <summary>
/// 构造函数:通过依赖注入接收 WeatherProvider
/// </summary>
public WeatherPlugin(WeatherProvider weatherProvider)
{
_weatherProvider = weatherProvider ?? throw new ArgumentNullException(nameof(weatherProvider));
}
/// <summary>
/// 查询指定城市的天气信息
/// </summary>
[Description("查询指定城市的天气信息,返回温度、天气状况和空气质量")]
public string GetWeather([Description("城市名称,例如:北京、上海、深圳")] string location)
{
// 委托给注入的 WeatherProvider 处理
return _weatherProvider.GetWeather(location);
}
}
Console.WriteLine("WeatherPlugin 定义完成(使用依赖注入)");
- 注册服务
// 1. 创建服务容器
var services = new ServiceCollection();
// 2. 注册依赖服务
services.AddSingleton<WeatherProvider>();
// 3. 注册插件
services.AddSingleton<WeatherPlugin>();
// 4. 构建服务提供者
var serviceProvider = services.BuildServiceProvider();
// 5. 解析插件(依赖会自动注入)
var plugin = serviceProvider.GetRequiredService<WeatherPlugin>();
// 6. Agent 需要 ServiceProvider 来解析工具依赖
var agent = chatClient.CreateAIAgent(
// ... 配置
services: serviceProvider // 传递 ServiceProvider
);
- 测试
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 企业智能工作助手 - 依赖注入版本测试 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
try
{
// 1. 创建服务容器
var services = new ServiceCollection();
Console.WriteLine("服务容器创建成功");
// 2. 注册依赖服务
services.AddSingleton<WeatherProvider>();
Console.WriteLine("WeatherProvider 服务注册完成");
// 3. 注册插件(构造函数依赖会自动注入)
services.AddSingleton<WeatherPluginV2>();
Console.WriteLine("WeatherPluginV2 插件注册完成");
// 4. 构建服务提供者
var serviceProvider = services.BuildServiceProvider();
Console.WriteLine("ServiceProvider 构建完成\n");
// 5. 解析插件实例(依赖已自动注入)
var weatherPlugin = serviceProvider.GetRequiredService<WeatherPluginV2>();
Console.WriteLine("插件实例解析成功(依赖已注入)\n");
// 6. 转换为 AI 工具
var tools = new[]
{
AIFunctionFactory.Create(weatherPlugin.GetWeather)
};
// 7. 创建 Agent(传递 ServiceProvider)
var agent = chatClient.CreateAIAgent(
instructions: "你是一个企业智能工作助手,帮助员工查询工作相关信息。回答要简洁专业。",
name: "WorkAssistant",
tools: tools,
services: serviceProvider // 传递 ServiceProvider 以支持工具中的依赖解析
);
Console.WriteLine("Agent 创建完成(已注入 ServiceProvider)\n");
// 8. 测试插件功能
var employeeRequests = new[]
{
"纽约现在天气怎么样?",
"我要去深圳出差,帮我查一下天气"
};
foreach (var request in employeeRequests)
{
Console.WriteLine($"员工请求: {request}");
var response = await agent.RunAsync(request);
Console.WriteLine($"助手回复: {response.Text}\n");
}
Console.WriteLine("测试完成!依赖注入工作正常。");
}
catch (Exception ex)
{
Console.WriteLine("测试失败:");
new {
错误类型 = ex.GetType().Name,
错误消息 = ex.Message,
堆栈跟踪 = ex.StackTrace?.Split('\n').Take(3).ToArray()
}.Display();
}
4. 选择性暴露工具
在实际开发中,每个插件都需要实现 AsAITools() 方法来选择性暴露工具,这会导致大量重复代码。让我们通过抽象基类来简化插件的创建。
- 使用抽象基类
/// <summary>
/// Agent 插件抽象基类
/// 提供统一的插件实现模式,减少重复代码
/// </summary>
public abstract class AgentPluginBase
{
/// <summary>
/// 获取插件名称(用于日志和监控)
/// </summary>
public virtual string PluginName => GetType().Name;
/// <summary>
/// 获取插件描述
/// </summary>
public virtual string PluginDescription => string.Empty;
/// <summary>
/// 子类重写此方法,返回需要暴露给 AI 的工具方法
/// </summary>
/// <returns>工具方法的委托列表</returns>
protected abstract IEnumerable<Delegate> GetToolMethods();
/// <summary>
/// 将插件方法转换为 AI 工具
/// 这个方法由基类统一实现,子类无需重复
/// </summary>
public IEnumerable<AITool> AsAITools()
{
var methods = GetToolMethods();
foreach (var method in methods)
{
if (method == null)
{
Console.WriteLine($"警告:{PluginName} 中存在空的工具方法");
continue;
}
yield return AIFunctionFactory.Create(method);
}
}
/// <summary>
/// 获取插件信息(用于监控和调试)
/// </summary>
public virtual object GetPluginInfo()
{
var toolMethods = GetToolMethods().ToList();
return new
{
插件名称 = PluginName,
插件描述 = PluginDescription,
工具数量 = toolMethods.Count,
工具列表 = toolMethods.Select(m => m.Method.Name).ToArray()
};
}
}
Console.WriteLine("AgentPluginBase 抽象基类定义完成");
- 使用抽象基类创建天气插件
现在使用抽象基类重构我们的天气插件,展示如何简化插件开发。
/// <summary>
/// 天气插件 - 使用抽象基类
/// </summary>
public sealed class WeatherPlugin : AgentPluginBase
{
private readonly WeatherProvider _weatherProvider;
private Dictionary<string, string> _cache = new();
public override string PluginName => "天气查询插件";
public override string PluginDescription => "提供城市天气信息查询功能,支持缓存";
public WeatherPlugin(WeatherProvider weatherProvider)
{
_weatherProvider = weatherProvider ?? throw new ArgumentNullException(nameof(weatherProvider));
}
/// <summary>
/// 查询天气 - 暴露给 AI
/// </summary>
[Description("查询指定城市的天气信息,返回温度、天气状况和空气质量")]
public string GetWeather(
[Description("城市名称,例如:北京、上海、深圳")] string location)
{
// 先验证位置
if (!ValidateLocation(location))
{
return "城市名称无效,请提供有效的城市名称";
}
// 检查缓存
if (_cache.TryGetValue(location, out var cached))
{
return $"[缓存] {cached}";
}
// 查询并缓存
var result = _weatherProvider.GetWeather(location);
_cache[location] = result;
return result;
}
/// <summary>
/// 更新缓存 - 内部管理方法,不暴露给 AI
/// </summary>
public void UpdateWeatherCache()
{
Console.WriteLine("清理天气缓存...");
_cache.Clear();
Console.WriteLine("缓存已清理");
}
/// <summary>
/// 设置 API 密钥 - 敏感操作,不暴露给 AI
/// </summary>
public void SetApiKey(string apiKey)
{
Console.WriteLine($"设置 API 密钥: {apiKey}");
}
/// <summary>
/// 验证城市名称 - 私有辅助方法
/// </summary>
private bool ValidateLocation(string location)
{
return !string.IsNullOrWhiteSpace(location) && location.Length >= 2;
}
/// <summary>
/// 只需要声明要暴露的方法 - 基类会自动处理 AsAITools()
/// </summary>
protected override IEnumerable<Delegate> GetToolMethods()
{
// 只暴露天气查询功能
yield return this.GetWeather;
// UpdateWeatherCache 和 SetApiKey 不在这里,所以不会被暴露
}
}
Console.WriteLine("WeatherPlugin 定义完成(使用抽象基类)");
- 测试天气插件
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 企业智能工作助手 - 抽象基类插件测试 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
try
{
// 1. 创建服务容器并注册依赖
var services = new ServiceCollection();
services.AddSingleton<WeatherProvider>();
services.AddSingleton<WeatherPlugin>();
var serviceProvider = services.BuildServiceProvider();
Console.WriteLine("服务容器创建并注册完成\n");
// 2. 解析插件实例
var weatherPlugin = serviceProvider.GetRequiredService<WeatherPlugin>();
Console.WriteLine("插件实例解析成功(依赖已自动注入)\n");
// 3. 显示插件信息
Console.WriteLine("插件信息:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
weatherPlugin.GetPluginInfo().Display();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 4. 基类自动处理 AsAITools()
var tools = weatherPlugin.AsAITools().ToArray();
Console.WriteLine($"通过基类自动收集到 {tools.Length} 个工具");
Console.WriteLine(" 只暴露了 GetWeather 方法");
Console.WriteLine(" 未暴露 UpdateWeatherCache 和 SetApiKey\n");
// 5. 创建 Agent
var chatClient = AIClientHelper.GetDefaultChatClient();
var agent = chatClient.CreateAIAgent(
instructions: "你是一个企业智能工作助手。回答要简洁专业。",
name: "WorkAssistant",
tools: tools,
services: serviceProvider
);
Console.WriteLine("Agent 创建完成\n");
// 6. 测试天气查询(第一次查询)
Console.WriteLine("【测试 1】第一次查询北京天气:");
var response1 = await agent.RunAsync("北京今天天气怎么样?");
Console.WriteLine($"助手回复: {response1.Text}\n");
// 7. 再次查询相同城市(测试缓存)
Console.WriteLine("【测试 2】第二次查询北京天气(应该命中缓存):");
var response2 = await agent.RunAsync("再查一次北京的天气");
Console.WriteLine($"助手回复: {response2.Text}\n");
// 8. 查询不同城市
Console.WriteLine("【测试 3】查询上海天气:");
var response3 = await agent.RunAsync("上海天气如何?");
Console.WriteLine($"助手回复: {response3.Text}\n");
// 9. 手动调用内部方法(演示)
Console.WriteLine("【演示】手动调用内部管理方法:");
weatherPlugin.UpdateWeatherCache();
Console.WriteLine(" 注意:这个方法没有暴露给 AI,只能由代码直接调用\n");
// 10. 验证 AI 无法调用未暴露的方法
Console.WriteLine("【验证】AI 无法调用未暴露的方法:");
Console.WriteLine(" UpdateWeatherCache:不在工具列表中");
Console.WriteLine(" SetApiKey:不在工具列表中");
Console.WriteLine(" 只有 GetWeather 可被 AI 调用\n");
Console.WriteLine("测试完成!抽象基类版本运行正常。");
}
catch (Exception ex)
{
Console.WriteLine("测试失败:");
new {
错误类型 = ex.GetType().Name,
错误消息 = ex.Message,
堆栈跟踪 = ex.StackTrace?.Split('\n').Take(3).ToArray()
}.Display();
}
5. 多插件集成
- 创建时间插件
/// <summary>
/// 时间提供者服务
/// 负责时区转换和时间查询
/// </summary>
public sealed class CurrentTimeProvider
{
/// <summary>
/// 获取指定位置的当前时间
/// </summary>
public DateTimeOffset GetCurrentTime(string location)
{
// 根据位置获取时区(实际应用中应使用时区数据库)
var timeZoneId = location switch
{
"北京" or "上海" or "深圳" => "China Standard Time",
"西雅图" => "Pacific Standard Time",
"纽约" => "Eastern Standard Time",
"伦敦" => "GMT Standard Time",
"东京" => "Tokyo Standard Time",
_ => "China Standard Time" // 默认使用中国时区
};
try
{
var timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
var utcNow = DateTimeOffset.UtcNow;
var localTime = TimeZoneInfo.ConvertTime(utcNow, timeZone);
return localTime;
}
catch
{
// 如果时区查找失败,返回 UTC 时间
return DateTimeOffset.UtcNow;
}
}
}
/// <summary>
/// 时间查询插件 - 使用抽象基类
/// </summary>
public sealed class TimePlugin : AgentPluginBase
{
private readonly CurrentTimeProvider _timeProvider;
public override string PluginName => "时间查询插件";
public override string PluginDescription => "提供不同时区的时间查询功能";
public TimePlugin(CurrentTimeProvider timeProvider)
{
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}
/// <summary>
/// 查询指定位置的当前时间
/// </summary>
[Description("查询指定城市或地区的当前时间,支持不同时区")]
public string GetCurrentTime(
[Description("城市或地区名称,例如:北京、纽约、伦敦")] string location)
{
var currentTime = _timeProvider.GetCurrentTime(location);
var formattedTime = currentTime.ToString("yyyy-MM-dd HH:mm:ss");
var dayOfWeek = currentTime.ToString("dddd", new System.Globalization.CultureInfo("zh-CN"));
return $"{location} 当前时间:{formattedTime} ({dayOfWeek})";
}
/// <summary>
/// 内部方法:格式化时间(不暴露给 AI)
/// </summary>
private string FormatTimeWithZone(DateTimeOffset time, string location)
{
return $"{time:yyyy-MM-dd HH:mm:ss} ({location})";
}
/// <summary>
/// 只需要声明要暴露的方法 - 基类会自动处理 AsAITools()
/// </summary>
protected override IEnumerable<Delegate> GetToolMethods()
{
yield return this.GetCurrentTime;
}
}
Console.WriteLine("TimePlugin 和 CurrentTimeProvider 定义完成(使用抽象基类)");
- 集成多个插件
// 1. 注册所有服务和插件
services.AddSingleton<WeatherProvider>();
services.AddSingleton<CurrentTimeProvider>();
services.AddSingleton<WeatherPlugin>(); // 继承 AgentPluginBase
services.AddSingleton<TimePlugin>(); // 继承 AgentPluginBase
// 2. 解析所有插件
var weatherPlugin = sp.GetRequiredService<WeatherPlugin>();
var timePlugin = sp.GetRequiredService<TimePlugin>();
// 3. 基类自动收集所有工具
var tools = weatherPlugin.AsAITools()
.Concat(timePlugin.AsAITools())
.ToArray();
- 测试多插件集成
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 企业智能工作助手 - 多插件集成测试 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
try
{
// 1. 创建服务容器并注册所有依赖
var services = new ServiceCollection();
// 注册服务
services.AddSingleton<WeatherProvider>();
services.AddSingleton<CurrentTimeProvider>();
Console.WriteLine("服务提供者注册完成");
// 注册插件(都继承 AgentPluginBase)
services.AddSingleton<WeatherPlugin>();
services.AddSingleton<TimePlugin>();
Console.WriteLine("插件注册完成");
// 构建服务提供者
var serviceProvider = services.BuildServiceProvider();
Console.WriteLine("ServiceProvider 构建完成\n");
// 2. 解析所有插件
var weatherPlugin = serviceProvider.GetRequiredService<WeatherPlugin>();
var timePlugin = serviceProvider.GetRequiredService<TimePlugin>();
Console.WriteLine("所有插件实例解析成功\n");
// 3. 显示插件信息
Console.WriteLine("插件信息:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
weatherPlugin.GetPluginInfo().Display();
Console.WriteLine();
timePlugin.GetPluginInfo().Display();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 4. 基类自动收集所有工具
var tools = weatherPlugin.AsAITools()
.Concat(timePlugin.AsAITools())
.ToArray();
Console.WriteLine($"通过基类自动收集到 {tools.Length} 个工具:");
Console.WriteLine(" - GetWeather (天气查询)");
Console.WriteLine(" - GetCurrentTime (时间查询)\n");
// 5. 创建 Agent
var chatClient = AIClientHelper.GetDefaultChatClient();
var agent = chatClient.CreateAIAgent(
instructions: @"你是一个企业智能工作助手,帮助员工查询工作相关信息。
你可以查询天气和时间信息。回答要简洁专业。",
name: "WorkAssistant",
tools: tools,
services: serviceProvider
);
Console.WriteLine("多功能 Agent 创建完成\n");
// 6. 测试不同类型的请求
var employeeRequests = new[]
{
"北京明天天气怎么样?",
"现在纽约几点了?",
"我要去西雅图出差,帮我查一下那边的天气和当前时间"
};
foreach (var request in employeeRequests)
{
Console.WriteLine($"员工请求: {request}");
var response = await agent.RunAsync(request);
Console.WriteLine($"助手回复: {response.Text}");
Console.WriteLine();
}
Console.WriteLine("测试完成!多插件系统运行正常。");
Console.WriteLine("\n系统统计:");
Console.WriteLine($" • 已加载插件数:2");
Console.WriteLine($" • 可用工具数:{tools.Length}");
Console.WriteLine($" • 处理请求数:{employeeRequests.Length}");
}
catch (Exception ex)
{
Console.WriteLine("测试失败:");
new {
错误类型 = ex.GetType().Name,
错误消息 = ex.Message,
堆栈跟踪 = ex.StackTrace?.Split('\n').Take(3).ToArray()
}.Display();
}
6. 企业级插件接口设计
为了进一步标准化插件开发,我们可以定义一个插件接口(IAgentPlugin),配合抽象基类使用。
- 接口 + 抽象基类的架构
/// <summary>
/// Agent 插件接口
/// 定义所有插件必须实现的契约
/// </summary>
public interface IAgentPlugin
{
/// <summary>
/// 插件名称
/// </summary>
string PluginName { get; }
/// <summary>
/// 插件描述
/// </summary>
string PluginDescription { get; }
/// <summary>
/// 将插件方法转换为 AI 工具
/// </summary>
IEnumerable<AITool> AsAITools();
/// <summary>
/// 获取插件信息
/// </summary>
object GetPluginInfo();
}
/// <summary>
/// Agent 插件抽象基类 - 实现 IAgentPlugin 接口
/// 提供统一的插件实现模式
/// </summary>
public abstract class StandardAgentPluginBase : IAgentPlugin
{
/// <summary>
/// 插件名称
/// </summary>
public abstract string PluginName { get; }
/// <summary>
/// 插件描述
/// </summary>
public virtual string PluginDescription => string.Empty;
/// <summary>
/// 子类重写此方法,返回需要暴露给 AI 的工具方法
/// </summary>
protected abstract IEnumerable<Delegate> GetToolMethods();
/// <summary>
/// 将插件方法转换为 AI 工具
/// </summary>
public IEnumerable<AITool> AsAITools()
{
var methods = GetToolMethods();
foreach (var method in methods)
{
if (method == null)
{
Console.WriteLine($"警告:{PluginName} 中存在空的工具方法");
continue;
}
yield return AIFunctionFactory.Create(method);
}
}
/// <summary>
/// 获取插件信息
/// </summary>
public virtual object GetPluginInfo()
{
var toolMethods = GetToolMethods().ToList();
return new
{
插件名称 = PluginName,
插件描述 = PluginDescription,
工具数量 = toolMethods.Count,
工具列表 = toolMethods.Select(m => m.Method.Name).ToArray()
};
}
}
Console.WriteLine("IAgentPlugin 接口和 StandardAgentPluginBase 基类定义完成");
- 使用接口重构插件
/// <summary>
/// 标准版天气插件 - 实现 IAgentPlugin 接口
/// </summary>
public sealed class StandardWeatherPlugin : StandardAgentPluginBase
{
private readonly WeatherProvider _weatherProvider;
public override string PluginName => "标准天气查询插件";
public override string PluginDescription => "基于接口和基类的标准实现";
public StandardWeatherPlugin(WeatherProvider weatherProvider)
{
_weatherProvider = weatherProvider ?? throw new ArgumentNullException(nameof(weatherProvider));
}
[Description("查询指定城市的天气信息")]
public string GetWeather(
[Description("城市名称")] string location)
{
return _weatherProvider.GetWeather(location);
}
protected override IEnumerable<Delegate> GetToolMethods()
{
yield return this.GetWeather;
}
}
/// <summary>
/// 标准版时间插件 - 实现 IAgentPlugin 接口
/// </summary>
public sealed class StandardTimePlugin : StandardAgentPluginBase
{
private readonly CurrentTimeProvider _timeProvider;
public override string PluginName => "标准时间查询插件";
public override string PluginDescription => "基于接口和基类的标准实现";
public StandardTimePlugin(CurrentTimeProvider timeProvider)
{
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}
[Description("查询指定城市的当前时间")]
public string GetCurrentTime(
[Description("城市名称")] string location)
{
var currentTime = _timeProvider.GetCurrentTime(location);
return $"{location} 当前时间:{currentTime:yyyy-MM-dd HH:mm:ss}";
}
protected override IEnumerable<Delegate> GetToolMethods()
{
yield return this.GetCurrentTime;
}
}
Console.WriteLine("标准插件定义完成(实现 IAgentPlugin 接口)");
- 注册
// 通过接口统一注册
services.AddSingleton<IAgentPlugin, StandardWeatherPlugin>();
services.AddSingleton<IAgentPlugin, StandardTimePlugin>();
// 通过接口统一获取
var plugins = serviceProvider.GetServices<IAgentPlugin>();
// 自动收集所有工具
var tools = plugins.SelectMany(p => p.AsAITools()).ToArray();
- 测试企业级插件架构
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 企业智能工作助手 - 企业级插件架构测试 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
try
{
// 1. 创建服务容器并注册所有依赖
var services = new ServiceCollection();
// 注册服务
services.AddSingleton<WeatherProvider>();
services.AddSingleton<CurrentTimeProvider>();
Console.WriteLine("服务提供者注册完成");
// 通过接口注册插件(企业级标准做法)
services.AddSingleton<IAgentPlugin, StandardWeatherPlugin>();
services.AddSingleton<IAgentPlugin, StandardTimePlugin>();
Console.WriteLine("插件通过 IAgentPlugin 接口注册完成");
// 构建服务提供者
var serviceProvider = services.BuildServiceProvider();
Console.WriteLine("ServiceProvider 构建完成\n");
// 2. 通过接口统一解析所有插件
var plugins = serviceProvider.GetServices<IAgentPlugin>().ToArray();
Console.WriteLine($"自动发现 {plugins.Length} 个插件:");
foreach (var plugin in plugins)
{
Console.WriteLine($" • {plugin.PluginName}");
}
Console.WriteLine();
// 3. 显示所有插件信息
Console.WriteLine("插件详细信息:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
foreach (var plugin in plugins)
{
plugin.GetPluginInfo().Display();
Console.WriteLine();
}
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 4. 自动收集所有工具(通过接口统一处理)
var tools = plugins
.SelectMany(p => p.AsAITools())
.ToArray();
Console.WriteLine($"通过接口自动收集到 {tools.Length} 个工具\n");
// 5. 创建 Agent
var chatClient = AIClientHelper.GetDefaultChatClient();
var agent = chatClient.CreateAIAgent(
instructions: @"你是一个企业智能工作助手,帮助员工查询工作相关信息。
你可以查询天气和时间信息。回答要简洁专业。",
name: "EnterpriseWorkAssistant",
tools: tools,
services: serviceProvider
);
Console.WriteLine("企业级 Agent 创建完成\n");
// 6. 测试功能
var employeeRequests = new[]
{
"我要去深圳出差,帮我查一下当地天气和时间",
"纽约现在是几点?天气怎么样?"
};
foreach (var request in employeeRequests)
{
Console.WriteLine($"员工请求: {request}");
var response = await agent.RunAsync(request);
Console.WriteLine($"助手回复: {response.Text}");
Console.WriteLine();
}
Console.WriteLine("测试完成!企业级插件架构运行正常。\n");
// 7. 架构演进总结
Console.WriteLine("架构演进总结:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
new
{
阶段1_直接函数 = new
{
代码特点 = "直接定义函数",
优点 = "简单直接",
缺点 = "难以维护,不可扩展"
},
阶段2_插件类 = new
{
代码特点 = "封装为插件类",
优点 = "模块化清晰",
缺点 = "AsAITools() 重复实现"
},
阶段3_抽象基类 = new
{
代码特点 = "继承抽象基类",
优点 = "减少重复代码",
缺点 = "缺少统一接口"
},
阶段4_接口加基类 = new
{
代码特点 = "接口 + 抽象基类",
优点 = "完全标准化,易于扩展",
缺点 = "无明显缺点"
},
最佳实践 = new
{
推荐架构 = "IAgentPlugin 接口 + 抽象基类",
核心价值 = "标准化、可扩展、易维护",
适用场景 = "企业级应用、大型项目",
学习成本 = "中等(一次投入,长期受益)"
}
}.Display();
}
catch (Exception ex)
{
Console.WriteLine("测试失败:");
new {
错误类型 = ex.GetType().Name,
错误消息 = ex.Message,
堆栈跟踪 = ex.StackTrace?.Split('\n').Take(3).ToArray()
}.Display();
}
七、人机协助
1. 快速开始
- 定于需要审批的工具
// 定义转账函数 (敏感操作)
[Description("执行银行转账操作,将指定金额从当前账户转账到目标账户")]
string TransferMoney(
[Description("收款人账户号码")] string targetAccount,
[Description("转账金额 (元)")] decimal amount,
[Description("转账备注")] string note = "")
{
// 模拟转账操作 (实际应用中应该调用真实的银行 API)
return $"转账成功!\n" +
$" 收款账户: {targetAccount}\n" +
$" 转账金额: ¥{amount:F2}\n" +
$" 备注: {note}\n" +
$" 时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
}
Console.WriteLine("转账函数定义完成");
使用 ApprovalRequiredAIFunction 包装(关键步骤),工作原理:
ApprovalRequiredAIFunction 是一个装饰器 (Decorator)
它包装了原始的 AIFunction
当 Agent 尝试调用工具时,会先发出审批请求
只有在获得批准后才会真正执行内部函数
// 步骤 1: 使用 AIFunctionFactory 创建基础工具
var transferTool = AIFunctionFactory.Create(TransferMoney);
// 步骤 2: 用 ApprovalRequiredAIFunction 包装,添加审批环节
var approvalRequiredTransferTool = new ApprovalRequiredAIFunction(transferTool);
// 步骤 3: 创建 Agent 并注册包装后的工具
AIAgent bankAgent = chatClient.CreateAIAgent(
instructions: "你是一位专业的银行助手。你可以帮助用户进行转账操作,但在执行转账前,你必须请求用户确认。",
name: "BankAssistant",
tools: [approvalRequiredTransferTool] // 注册需要审批的工具
);
Console.WriteLine("银行助手 Agent 创建完成");
处理审批流程,这是最核心的部分,需要:
- 调用 Agent
- 检查是否有审批请求 (UserInputRequests)
- 获取 FunctionApprovalRequestContent
- 请求用户确认
- 使用 CreateResponse() 返回审批结果
- 继续 Agent 处理
关键 API:
response.UserInputRequests - 获取所有待处理的用户输入请求
FunctionApprovalRequestContent - 函数审批请求内容
CreateResponse(bool approved) - 创建审批响应
// 步骤 1: 测试场景,用户请求转账
// 创建对话线程
AgentThread bankThread = bankAgent.GetNewThread();
// 用户请求
var userRequest = "帮我给账户 6222000012345678 转账 1000 元,备注: 还款";
Console.WriteLine($"用户: {userRequest}");
Console.WriteLine("\nAgent 处理中...\n");
// 第一次调用 Agent
var response = await bankAgent.RunAsync(userRequest, bankThread);
// 检查是否有审批请求
var userInputRequests = response.UserInputRequests.ToList();
Console.WriteLine($"Agent 返回了 {userInputRequests.Count} 个待处理请求");
if (userInputRequests.Count > 0)
{
Console.WriteLine("检测到需要审批的操作!\n");
}
// 步骤 2:处理审批请求
// 处理审批请求
if (userInputRequests.Count > 0)
{
// 提取函数审批请求
var approvalRequests = userInputRequests
.OfType<FunctionApprovalRequestContent>()
.ToList();
Console.WriteLine($"发现 {approvalRequests.Count} 个函数调用需要审批:\n");
// 创建用户响应列表
var userResponses = new List<ChatMessage>();
foreach (var approvalRequest in approvalRequests)
{
// 显示审批信息
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("审批请求");
Console.WriteLine($" 函数名称: {approvalRequest.FunctionCall.Name}");
Console.WriteLine($" 调用参数: {approvalRequest.FunctionCall.Arguments}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 模拟用户审批决策
Console.WriteLine("请审批此操作:");
Console.WriteLine(" [Y] 批准");
Console.WriteLine(" [N] 拒绝");
Console.Write("\n你的决定: ");
// 我们模拟用户批准,实际应用中应该使用 Console.ReadLine() 或 UI 交互
var userDecision = "Y"; // 模拟用户输入 "Y"
Console.WriteLine($"{userDecision}\n");
bool approved = userDecision.Equals("Y", StringComparison.OrdinalIgnoreCase);
if (approved)
{
Console.WriteLine("用户批准了此操作\n");
}
else
{
Console.WriteLine("用户拒绝了此操作\n");
}
// 创建审批响应
var approvalResponse = approvalRequest.CreateResponse(approved);
// 将响应包装为 ChatMessage
var responseMessage = new ChatMessage(ChatRole.User, [approvalResponse]);
userResponses.Add(responseMessage);
}
Console.WriteLine("将审批结果返回给 Agent...\n");
// 将审批结果发送回 Agent 继续处理
response = await bankAgent.RunAsync(userResponses, bankThread);
Console.WriteLine($"{bankAgent.Name}: {response}\n");
Console.WriteLine("审批流程完成!");
}
else
{
// 没有审批请求,直接显示响应
Console.WriteLine($"{bankAgent.Name}: {response}");
}
2. 流式调用
- 流式审批,流式审批的特点
- 可以实时看到 Agent 的思考过程
- 审批请求会在流式响应中累积
- 需要使用 ToListAsync() 收集所有更新
// 1. 流式调用并收集所有更新
var updates = await agent.RunStreamingAsync(message, thread).ToListAsync();
// 2. 从所有更新中提取审批请求
var userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList();
// 3. 处理审批...
// 4. 继续流式处理
updates = await agent.RunStreamingAsync(userResponses, thread).ToListAsync();
- 测试流水审批
// 创建新线程
AgentThread streamThread = bankAgent.GetNewThread();
var streamRequest = "帮我给 6222000099998888 转账 2000 元,备注工资";
Console.WriteLine($"用户: {streamRequest}");
Console.WriteLine($"\n{bankAgent.Name} (流式处理):\n");
// 流式调用并收集所有更新
var streamUpdates = await bankAgent.RunStreamingAsync(streamRequest, streamThread).ToListAsync();
// 实时显示流式内容 (模拟)
foreach (var update in streamUpdates.Where(u => !string.IsNullOrEmpty(u.Text)))
{
Console.Write(update.Text);
}
Console.WriteLine("\n");
// 从所有更新中提取审批请求
var streamRequests = streamUpdates.SelectMany(x => x.UserInputRequests).ToList();
if (streamRequests.Count > 0)
{
Console.WriteLine($"检测到 {streamRequests.Count} 个审批请求\n");
var approvalRequests = streamRequests
.OfType<FunctionApprovalRequestContent>()
.ToList();
var userResponses = new List<ChatMessage>();
foreach (var approvalRequest in approvalRequests)
{
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("审批请求");
Console.WriteLine($" 函数: {approvalRequest.FunctionCall.Name}");
Console.WriteLine($" 参数: {approvalRequest.FunctionCall.Arguments}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var decision = "Y";
Console.WriteLine($"用户决定: {decision} (批准)\n");
bool approved = decision.Equals("Y", StringComparison.OrdinalIgnoreCase);
var approvalResponse = approvalRequest.CreateResponse(approved);
userResponses.Add(new ChatMessage(ChatRole.User, [approvalResponse]));
}
// 继续流式处理
Console.WriteLine("返回审批结果,继续流式处理:\n");
var finalUpdates = await bankAgent.RunStreamingAsync(userResponses, streamThread).ToListAsync();
foreach (var update in finalUpdates.Where(u => !string.IsNullOrEmpty(u.Text)))
{
Console.Write(update.Text);
}
Console.WriteLine("\n\n流式审批流程完成!");
}
3. 企业场景 - 混合工具 (部分需要审批)
在实际应用中,Agent 通常有多个工具,其中只有部分需要审批。
- 定义混合工具集
// ===== 低风险工具 (不需要审批) =====
[Description("查询服务器的运行状态")]
string GetServerStatus([Description("服务器名称")] string serverName)
{
return $"服务器 {serverName} 状态: 运行中, CPU: 45%, 内存: 60%, 运行时间: 15天";
}
[Description("查询系统日志")]
string GetSystemLogs([Description("日志类型: error/info/warning")] string logType)
{
return $"最近的 {logType} 日志: [2025-01-08 10:30:15] 系统正常运行";
}
// ===== 高风险工具 (需要审批) =====
[Description("重启指定的服务器")]
string RestartServer([Description("服务器名称")] string serverName)
{
return $"服务器 {serverName} 已重启完成";
}
[Description("删除指定用户的账户")]
string DeleteUser([Description("用户ID")] string userId)
{
return $"用户 {userId} 的账户已删除";
}
Console.WriteLine("混合工具集定义完成");
- 创建混合工具 Agent
// 创建 IT 助手 Agent
AIAgent itAssistant = chatClient.CreateAIAgent(
instructions: "你是企业 IT 助手。你可以查询服务器状态和日志 (无需审批),但重启服务器或删除用户账户需要管理员确认。",
name: "ITAssistant",
tools: [
// 安全工具 - 不需要审批
AIFunctionFactory.Create(GetServerStatus),
AIFunctionFactory.Create(GetSystemLogs),
// 危险工具 - 需要审批
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(RestartServer)),
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(DeleteUser))
]
);
Console.WriteLine("IT 助手 Agent 创建完成");
- 测试场景1:只查询(不需要审批)
AgentThread itThread1 = itAssistant.GetNewThread();
var query1 = "帮我查看 WebServer01 的状态";
Console.WriteLine($"用户: {query1}");
Console.WriteLine("\nAgent 处理中...\n");
var itResponse1 = await itAssistant.RunAsync(query1, itThread1);
var itRequests1 = itResponse1.UserInputRequests.ToList();
if (itRequests1.Count == 0)
{
Console.WriteLine($"{itAssistant.Name}: {itResponse1}");
Console.WriteLine("\n查询操作无需审批,直接返回结果!");
}
- 测试场景2:重启服务器(需要审批)
AgentThread itThread2 = itAssistant.GetNewThread();
var query2 = "WebServer01 响应很慢,帮我重启一下";
Console.WriteLine($"用户: {query2}");
Console.WriteLine("\nAgent 处理中...\n");
var itResponse2 = await itAssistant.RunAsync(query2, itThread2);
var itRequests2 = itResponse2.UserInputRequests.ToList();
// 处理审批
while (itRequests2.Count > 0)
{
var approvalRequests = itRequests2
.OfType<FunctionApprovalRequestContent>()
.ToList();
if (approvalRequests.Count > 0)
{
Console.WriteLine("检测到危险操作,需要管理员审批:\n");
var userResponses = new List<ChatMessage>();
foreach (var approvalRequest in approvalRequests)
{
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"审批请求: {approvalRequest.FunctionCall.Name}");
Console.WriteLine($" 参数: {approvalRequest.FunctionCall.Arguments}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var decision = "Y";
Console.WriteLine($"管理员决定: {decision} (批准)\n");
bool approved = decision.Equals("Y", StringComparison.OrdinalIgnoreCase);
var approvalResponse = approvalRequest.CreateResponse(approved);
userResponses.Add(new ChatMessage(ChatRole.User, [approvalResponse]));
}
itResponse2 = await itAssistant.RunAsync(userResponses, itThread2);
itRequests2 = itResponse2.UserInputRequests.ToList();
}
}
Console.WriteLine($"{itAssistant.Name}: {itResponse2}");
Console.WriteLine("\n危险操作已完成审批并执行!");
4. 最佳实践
工具分类策略
- 明确工具风险等级
// 低风险: 直接注册
AIFunctionFactory.Create(QueryData)
// 中风险: 根据场景决定
AIFunctionFactory.Create(SendEmail) // 或包装为审批工具
// 高风险: 必须审批
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(DeleteData))
风险评估清单
必须审批:资金操作、数据删除、权限变更
建议审批:对外通信、批量操作、配置修改
可选审批:敏感查询、导出数据
无需审批:只读查询、计算、搜索
审批流程设计
- 使用循环处理所有审批
// 正确: 使用 while 循环
while (userInputRequests.Count > 0)
{
// 处理审批...
response = await agent.RunAsync(userResponses, thread);
userInputRequests = response.UserInputRequests.ToList();
}
// 错误: 只处理一次
if (userInputRequests.Count > 0) { /* 处理 */ }
- 提供详细的审批信息
// 好的实践: 显示完整信息
Console.WriteLine($"函数: {request.FunctionCall.Name}");
Console.WriteLine($"参数: {request.FunctionCall.Arguments}");
Console.WriteLine($"风险等级: 高");
Console.WriteLine($"影响范围: 将删除用户数据");
// 不够详细
Console.WriteLine("需要审批");
- 记录审批历史
// 记录审批决策
var auditLog = new
{
Timestamp = DateTime.Now,
Function = approvalRequest.FunctionCall.Name,
Parameters = approvalRequest.FunctionCall.Arguments,
Approved = approved,
Approver = currentUser.Name
};
SaveAuditLog(auditLog);
结合持久化 (重要!)
在生产环境中,审批流程通常需要时间,用户可能不会立即响应。此时需要:
- 序列化 Thread 等待审批
// 保存当前状态
var serializedThread = thread.Serialize();
await SaveToDatabase(userId, serializedThread);
// 等待用户审批 (可能数小时/数天)...
// 恢复状态
var restoredThread = agent.DeserializeThread(serializedThread);
- 持久化审批请求
// 保存审批请求到数据库
var approvalRecord = new
{
UserId = userId,
RequestId = Guid.NewGuid(),
FunctionName = approvalRequest.FunctionCall.Name,
Arguments = approvalRequest.FunctionCall.Arguments,
Status = "Pending",
CreatedAt = DateTime.Now
};
await SaveApprovalRequest(approvalRecord);
常见错误
- 忘记循环处理
// 错误: 只处理第一个审批请求
if (userInputRequests.Count > 0)
{
var response = await agent.RunAsync(...);
// 如果还有更多审批请求,会被忽略!
}
- 流式调用忘记 ToListAsync()
// 错误: 无法获取审批请求
await foreach (var update in agent.RunStreamingAsync(...))
{
var requests = update.UserInputRequests; // 不完整!
}
// 正确: 收集所有更新
var updates = await agent.RunStreamingAsync(...).ToListAsync();
var requests = updates.SelectMany(x => x.UserInputRequests).ToList();
- 没有区分工具风险等级
// 错误: 所有工具都需要审批
tools: tools.Select(t => new ApprovalRequiredAIFunction(t)).ToArray()
// 正确: 只对高风险工具审批
tools: [
lowRiskTool, // 不包装
new ApprovalRequiredAIFunction(highRiskTool) // 包装
]
八、结构化输出
1. 简单类型的结构化输出
- 使用 MEAI 中完全一样的模型
/// <summary>
/// 个人信息数据模型
/// </summary>
public class PersonInfo
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("age")]
public int? Age { get; set; }
[JsonPropertyName("occupation")]
public string? Occupation { get; set; }
[JsonPropertyName("location")]
public string? Location { get; set; }
}
- 创建支持结构化输出的 Agent
// 1. 生成 JSON Schema
JsonElement personSchema = AIJsonUtilities.CreateJsonSchema(typeof(PersonInfo));
Console.WriteLine("生成的 JSON Schema:");
personSchema.Display();
// 2. 配置 ChatOptions
// 关键步骤 通过 ChatOptions.ResponseFormat 配置结构化输出
var chatOptions = new ChatOptions
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: personSchema,
schemaName: "PersonInfo",
schemaDescription: "包含姓名、年龄、职业和工作地点的个人信息")
};
Console.WriteLine("ChatOptions 配置完成(已指定结构化输出格式)");
// 3. 创建 Agent(传入 chatOptions)
var agent = chatClient.CreateAIAgent(options: new ChatClientAgentOptions()
{
Name = "PersonInfoExtractor",
Instructions = "你是一个信息提取助手,负责从文本中提取个人信息。",
ChatOptions = chatOptions // 传入配置好的 ChatOptions
});
Console.WriteLine("Agent 创建完成(已配置结构化输出)");
- 使用 RunAsync() 获取结构化输出。MAF 提供了两种方式获取结构化数据,重要区别:
- 方式 1(非泛型):需要预先在 Agent 创建时配置 ChatOptions.ResponseFormat
- 方式 2(泛型):无需预先配置,泛型方法会自动处理 JSON Schema 和反序列化
// 方式 1:使用 Deserialize() 手动反序列化(需预先配置 ChatOptions)
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 方式 1:RunAsync() + Deserialize<T>() ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var thread = agent.GetNewThread();
var response = await agent.RunAsync("请提取:张伟是一名35岁的软件工程师,目前在北京工作。", thread);
Console.WriteLine("原始 JSON 响应:");
Console.WriteLine(response.Text);
// 使用 Deserialize 方法手动反序列化
var personInfo = response.Deserialize<PersonInfo>(JsonSerializerOptions.Web);
Console.WriteLine("\n反序列化成功\n");
Console.WriteLine("提取的个人信息:");
personInfo.Display();
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// 方式 2:MAF 提供了泛型版本的 RunAsync<T>(),类似于 MEAI 的 GetResponseAsync<T>()
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 方式 2:RunAsync<T>() 泛型方法(推荐) ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 注意:泛型方法不需要预先配置 ChatOptions.ResponseFormat
// 创建一个没有配置结构化输出的 Agent
var simpleAgent = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "SimpleExtractor",
Instructions = "你是一个信息提取助手,负责从文本中提取个人信息。"
// 注意:这里没有配置 ChatOptions
});
var thread2 = simpleAgent.GetNewThread();
// 使用泛型方法直接获取强类型对象(无需预先配置)
var response2 = await simpleAgent.RunAsync<PersonInfo>("请提取:李娜是一名29岁的产品经理,在上海工作。", thread2);
Console.WriteLine("原始 JSON 响应:");
Console.WriteLine(response2.Text);
Console.WriteLine("\n自动反序列化成功\n");
Console.WriteLine("提取的个人信息(直接从 Result 属性获取):");
response2.Result.Display();
Console.WriteLine("\n说明:RunAsync<T>() 自动生成 JSON Schema、配置 ResponseFormat、并反序列化为强类型对象。");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
- 流式结构化输出。MAF 同样支持流式获取结构化输出,需要等待流式响应完成后手动反序列化。
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 流式结构化输出示例 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var thread3 = agent.GetNewThread();
Console.Write("正在接收流式响应: ");
// 使用 RunStreamingAsync 获取流式响应
var streamingUpdates = agent.RunStreamingAsync("请提取:王芳是一名28岁的设计师,在深圳工作。", thread3);
// 等待流式响应完成
var streamResponse = await streamingUpdates.ToAgentRunResponseAsync();
Console.WriteLine(" 完成\n");
Console.WriteLine("原始 JSON 响应:");
Console.WriteLine(streamResponse.Text);
// 流式响应完成后,手动反序列化
var streamPersonInfo = streamResponse.Deserialize<PersonInfo>(JsonSerializerOptions.Web);
Console.WriteLine("\n流式响应反序列化成功\n");
Console.WriteLine("提取的个人信息:");
streamPersonInfo.Display();
Console.WriteLine("\n 注意:流式输出需要等待完成后再反序列化,不支持 RunAsync<T>() 泛型方法。");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2. 嵌套对象的结构化输出
- 定义复杂类型
/// <summary>
/// 工单类别
/// </summary>
public enum TicketCategory
{
TechnicalSupport,
Billing,
FeatureRequest,
Bug,
Other
}
/// <summary>
/// 工单优先级
/// </summary>
public enum TicketPriority
{
Low,
Medium,
High,
Critical
}
/// <summary>
/// 客户信息
/// </summary>
public class CustomerInfo
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("email")]
public string? Email { get; set; }
[JsonPropertyName("account_id")]
public string? AccountId { get; set; }
}
/// <summary>
/// 工单分析结果
/// </summary>
public class TicketAnalysis
{
[JsonPropertyName("ticket_id")]
public string? TicketId { get; set; }
[JsonPropertyName("category")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public TicketCategory Category { get; set; }
[JsonPropertyName("priority")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public TicketPriority Priority { get; set; }
[JsonPropertyName("customer")]
public CustomerInfo? Customer { get; set; }
[JsonPropertyName("summary")]
public string? Summary { get; set; }
[JsonPropertyName("action_items")]
public List<string>? ActionItems { get; set; }
[JsonPropertyName("estimated_resolution_hours")]
public int EstimatedResolutionHours { get; set; }
}
- 创建工单分析 Agent
// 生成 JSON Schema
JsonElement ticketSchema = AIJsonUtilities.CreateJsonSchema(typeof(TicketAnalysis));
// 配置 ChatOptions
var ticketChatOptions = new ChatOptions
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: ticketSchema,
schemaName: "TicketAnalysis",
schemaDescription: "客户服务工单的完整分析结果")
};
// 创建 Agent
var ticketAgent = chatClient.CreateAIAgent(options: new ChatClientAgentOptions()
{
Name = "TicketAnalyzer",
Instructions = @"
你是一个专业的客户服务工单分析助手。请分析工单内容并提取以下信息:
1. 工单类别(TechnicalSupport, Billing, FeatureRequest, Bug, Other)
2. 优先级(Low, Medium, High, Critical)
3. 客户信息(姓名、邮箱、账户ID)
4. 问题摘要(不超过100字)
5. 需要采取的行动项(列表)
6. 预计解决时间(小时数)
",
ChatOptions = ticketChatOptions
});
Console.WriteLine("工单分析 Agent 创建完成");
- 分析真实工单
var ticketContent = @"
工单 #12345
客户信息:
姓名:李明
邮箱:liming@example.com
账户ID:CUST-98765
问题描述:
我们的生产环境在今天凌晨3点突然无法访问,所有用户都报告无法登录系统。
这已经持续了2个小时,严重影响了我们的业务运营。我们的客户也在投诉。
请尽快解决这个紧急问题!另外,我们发现过去一周系统响应速度也明显变慢。
";
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 企业级客户工单分析系统 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
Console.WriteLine("正在分析工单 #12345...\n");
var ticketThread = ticketAgent.GetNewThread();
// 使用泛型方法直接获取强类型对象
var ticketResponse = await ticketAgent.RunAsync<TicketAnalysis>($"请分析以下工单:\n\n{ticketContent}", ticketThread);
Console.WriteLine("工单分析完成\n");
Console.WriteLine("分析结果:");
ticketResponse.Result.Display();
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"工单已自动分类为:{ticketResponse.Result?.Category}");
Console.WriteLine($" 优先级:{ticketResponse.Result?.Priority}");
Console.WriteLine($" 预计解决时间:{ticketResponse.Result?.EstimatedResolutionHours} 小时");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
3. 国内模型
国内模型(Qwen、DeepSeek 等)不支持 JSON Schema,在使用泛型方法时需要特殊处理。对于不支持 JSON Schema 的模型,也有两种配置方式
- 非泛型方法 + ChatResponseFormat.Json
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 方式 1:非泛型方法 + ChatResponseFormat.Json ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 简化写法:使用泛型扩展方法
var qwenkChatOptions1 = new ChatOptions
{
// 推荐:使用泛型扩展方法,自动生成 Schema
ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>()
};
Console.WriteLine("配置方式 1 - 使用 ForJsonSchema<T>() 泛型方法");
Console.WriteLine(" 等同于:ChatResponseFormatJson.ForJsonSchema(");
Console.WriteLine(" schema: AIJsonUtilities.CreateJsonSchema(typeof(PersonInfo)),");
Console.WriteLine(" schemaName: \"PersonInfo\")");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 对于国产模型,上述配置会失败,需要使用纯 JSON 模式
var qwenChatOptions2 = new ChatOptions
{
// 国产模型:使用 ChatResponseFormat.Json(无 Schema)
ResponseFormat = ChatResponseFormat.Json
};
Console.WriteLine("配置方式 2 - 国产模型使用 ChatResponseFormat.Json");
Console.WriteLine(" 无 JSON Schema,依赖 Instructions 提示词约束格式");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
- 使用泛型方法 + 国产模型参数,关键参数:useJsonSchemaResponseFormat
// 创建 Agent(需要详细的 Instructions)
var qwenAgent = qwenChatClient.CreateAIAgent(options: new ChatClientAgentOptions()
{
Name = "QwenExtractor",
// 关键:国产模型需要在 Instructions 中详细描述 JSON 格式
Instructions = @"
你是一个信息提取助手。请严格按照以下 JSON 格式返回结果:
{
""name"": ""姓名(字符串)"",
""age"": ""年龄(整数)"",
""occupation"": ""职业(字符串)"",
""location"": ""工作地点(字符串)""
}
重要:只返回 JSON,不要添加任何其他文本。"
// 注意:这里不配置 ChatOptions
});
Console.WriteLine("Qwen Agent 创建完成");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 使用泛型方法 + useJsonSchemaResponseFormat ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var qwenThread = qwenAgent.GetNewThread();
try
{
// 关键:对于不支持 JSON Schema 的模型
// 必须设置 useJsonSchemaResponseFormat: false
var qwenResponse = await qwenAgent.RunAsync<PersonInfo>(
message: "请提取:刘洋是一名42岁的数据科学家,目前在深圳工作。",
thread: qwenThread,
useJsonSchemaResponseFormat: false // 国产模型必须设为 false
);
Console.WriteLine("Qwen 响应完成\n");
Console.WriteLine("原始 JSON 响应:");
Console.WriteLine(qwenResponse.Text);
Console.WriteLine("\n泛型方法自动反序列化成功\n");
Console.WriteLine("提取的个人信息:");
qwenResponse.Result.Display();
Console.WriteLine("\n说明:");
Console.WriteLine(" - useJsonSchemaResponseFormat: false 告诉框架不使用 JSON Schema");
Console.WriteLine(" - 框架会自动配置 ChatResponseFormat.Json");
Console.WriteLine(" - 依赖 Instructions 中的格式描述来约束输出");
}
catch (Exception ex)
{
Console.WriteLine($"错误:{ex.Message}");
}
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
4. MAF vs MEAI:结构化输出对比
核心差异
| 特性 | MEAI (IChatClient) | MAF (AIAgent) |
|---|---|---|
| 配置位置 | 每次请求时传入 ChatOptions | Agent 创建时传入 ChatOptions |
| 泛型方法 | GetResponseAsync |
RunAsync |
| 手动反序列化 | JsonSerializer.Deserialize |
response.Deserialize |
| 流式输出 | GetStreamingResponseAsync() | RunStreamingAsync() |
| 上下文管理 | 手动传入 messages 列表 | 自动管理 AgentThread |
| 适用场景 | 通用聊天、无状态调用 | 企业 Agent、有状态对话 |
代码对比
// ━━━━━ MEAI 方式 ━━━━━
var messages = new[] {
new ChatMessage(ChatRole.System, "..."),
new ChatMessage(ChatRole.User, "...")
};
var options = new ChatOptions {
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(...)
};
// 每次请求都需要配置
var response = await chatClient.GetResponseAsync<PersonInfo>(messages, options);
// ━━━━━ MAF 方式 ━━━━━
var chatOptions = new ChatOptions {
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(...)
};
// 创建时配置一次,后续请求自动使用
var agent = chatClient.CreateAIAgent(new ChatClientAgentOptions {
Instructions = "...",
ChatOptions = chatOptions
});
var thread = agent.GetNewThread();
var response = await agent.RunAsync<PersonInfo>("...", thread);
优势对比
MEAI 的优势:
更灵活,每次请求可以使用不同的 ChatOptions
更轻量,无需创建 Agent 和 Thread
适合一次性、无状态的调用
MAF 的优势:
配置复用:一次配置,多次使用
自动上下文管理:AgentThread 自动管理对话历史
状态持久化:支持对话序列化和恢复
企业功能:支持工具调用、审批流程、中间件等
适合长期运行的 Agent 应用
5. 最佳实践总结
Agent 中的结构化输出配置
// ━━━━━ 方式 1:使用非泛型方法(需预先配置)━━━━━
var chatOptions = new ChatOptions
{
// 简化写法:使用泛型扩展方法
ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>()
// 等同于:
// ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
// schema: AIJsonUtilities.CreateJsonSchema(typeof(PersonInfo)),
// schemaName: "PersonInfo",
// schemaDescription: "详细描述")
};
var agent = chatClient.CreateAIAgent(new ChatClientAgentOptions {
Name = "ExtractorAgent",
Instructions = "你是一个信息提取助手。",
ChatOptions = chatOptions // 配置一次,所有请求自动使用
});
// 使用非泛型方法
var response = await agent.RunAsync("...", thread);
var result = response.Deserialize<PersonInfo>();
// ━━━━━ 方式 2:使用泛型方法(推荐,无需预先配置)━━━━━
var agent2 = chatClient.CreateAIAgent(new ChatClientAgentOptions {
Name = "ExtractorAgent",
Instructions = "你是一个信息提取助手。"
// 注意:不需要配置 ChatOptions
});
// 泛型方法自动处理一切
var response2 = await agent2.RunAsync<PersonInfo>("...", thread);
var result2 = response2.Result;
获取结构化数据的推荐方式
| 场景 | 推荐方式 | 预先配置 |
|---|---|---|
| 非流式输出 | RunAsync |
无需配置 |
| 流式输出 | RunStreamingAsync()+Deserialize() |
需要配置 |
数据模型设计建议
与 MEAI 完全一致:
- 使用 [JsonPropertyName] 特性明确字段名
- 使用 [JsonConverter(typeof(JsonStringEnumConverter))] 处理枚举
- 对可选字段使用可空类型(string?, int?)
- 为复杂类型添加 XML 注释说明
国产模型适配(Qwen/DeepSeek)
// 推荐方式:使用泛型方法 + 设置参数
var qwenAgent = qwenClient.CreateAIAgent(new ChatClientAgentOptions {
Name = "Extractor",
// 重要:在 Instructions 中详细描述 JSON 格式
Instructions = @"
你是一个信息提取助手。请严格按照以下 JSON 格式返回:
{
""name"": ""姓名"",
""age"": 年龄数字,
""occupation"": ""职业"",
""location"": ""地点""
}
只返回 JSON,不要添加其他文本。"
});
// 关键:设置 useJsonSchemaResponseFormat: false
var response = await qwenAgent.RunAsync<PersonInfo>(
"...",
thread,
useJsonSchemaResponseFormat: false // 国产模型必须设为 false
);
Instructions 提示词技巧
// OpenAI / Azure OpenAI(有 JSON Schema 支持)
Instructions = "你是一个专业的信息提取助手。请分析内容并提取信息。"
// Qwen / DeepSeek(无 JSON Schema 支持)
Instructions = @"
你是一个专业的信息提取助手。请严格按照以下要求提取信息:
1. category: 必须是以下之一(TechnicalSupport, Billing, FeatureRequest, Bug, Other)
2. priority: 根据紧急程度分类(Critical - 立即处理,High - 2小时内,Medium - 24小时内,Low - 3天内)
3. action_items: 提取 3-5 个具体的行动项,每项不超过30字
4. estimated_resolution_hours: 预计解决时间,必须是整数
输出格式示例:
{
""category"": ""TechnicalSupport"",
""priority"": ""High"",
""action_items"": [""行动项1"", ""行动项2""],
""estimated_resolution_hours"": 4
}
重要:只返回 JSON,不要添加任何其他文本或说明。
"
何时使用 MAF 结构化输出?
推荐使用 MAF 的场景:
- 需要多轮对话收集信息
- Agent 长期运行,配置复用
- 需要对话状态持久化
- 结合工具调用、审批流程等企业功能
- 需要统一的 Instructions 和输出格式
推荐使用 MEAI 的场景:
一次性、无状态的数据提取
需要动态切换不同的输出格式
简单的聊天场景
常见错误与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 返回纯文本而非 JSON | 未配置 ChatOptions(非泛型方法) | 使用泛型方法 RunAsync |
| DeepSeek 返回带说明文本 | 未设置 useJsonSchemaResponseFormat: false | 添加参数并在 Instructions 中强调"只返回 JSON" |
| 反序列化失败 | 数据模型不匹配 | 检查 JsonPropertyName 和字段类型定义 |
| 枚举值解析错误 | 未使用 JsonStringEnumConverter | 为枚举属性添加 [JsonConverter] 特性 |
| Result 属性为 null | 使用了非泛型方法 | 使用 RunAsync |
九、AgentOptions
1. ChatClientAgentOptions 概览
ChatClientAgentOptions 是创建 AI Agent 时的配置类,定义了 Agent 的身份、行为和依赖。它是 CreateAIAgent() 方法的核心参数。
| 配置项 | 类型 | 作用 |
|---|---|---|
| Id | string? | Agent 的唯一标识 ID |
| Name | string? | Agent 的显示名称,用于日志、追踪和多 Agent 场景 |
| Instructions | string? | Agent 的系统提示词(System Prompt),定义行为和角色 |
| Description | string? | Agent 的描述信息 |
| ChatOptions | ChatOptions? | AI 调用的全局配置(工具、温度、Token 限制等) |
| ChatMessageStoreFactory | Func<...>? | 聊天消息存储工厂,自定义消息持久化策略 |
| AIContextProviderFactory | Func<...>? | 上下文提供者工厂,动态注入运行时上下文信息 |
| UseProvidedChatClientAsIs | bool | 是否直接使用原始 IChatClient(禁用默认装饰器) |
重要特性:
- 所有配置项都是可选的,可以根据需要灵活组合
- 配置项在 Agent 创建时一次性设置,后续调用自动应用
- ChatOptions 支持运行时扩展(通过 ChatClientAgentRunOptions)
// 运行时可以扩展 ChatOptions(而非完全覆盖)
var response = await agent.RunAsync(
"消息",
thread,
options: new ChatClientAgentRunOptions
{
ChatOptions = new ChatOptions
{
Temperature = 0.1f,
Tools = [AdditionalTool] // 会与默认工具合并(union)
}
});
2. 配置项1:Name - Agent 名称
Name 是 Agent 的唯一标识符,主要用于:
- 日志追踪:在日志中区分不同 Agent 的行为
- 多 Agent 协作:标识发送消息的 Agent 身份
- 调试诊断:快速定位问题来源
- 监控统计:按 Agent 维度统计调用量和性能
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 多 Agent 场景中的 Name 作用 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 创建三个不同角色的 Agent
var translatorAgent = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "TranslatorAgent", // 翻译助手
Instructions = "你是一个专业的翻译助手,负责中英互译。"
});
var proofreaderAgent = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "ProofreaderAgent", // 校对助手
Instructions = "你是一个专业的校对助手,负责检查语法和拼写错误。"
});
var summarizerAgent = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "SummarizerAgent", // 摘要助手
Instructions = "你是一个摘要助手,负责提取文本的关键信息。"
});
Console.WriteLine("创建了 3 个不同角色的 Agent:\n");
var agents = new[] { translatorAgent, proofreaderAgent, summarizerAgent };
foreach (var agent in agents)
{
Console.WriteLine($" {agent.Name}");
}
Console.WriteLine("\n在日志和追踪系统中,可以通过 Name 区分不同 Agent 的行为");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
3. 配置项2:Instructions - 系统提示词
Instructions 是 Agent 的"灵魂",定义了 Agent 的:
- 角色定位:你是谁?(客服、教练、分析师等)
- 行为规范:如何回复?(友好、专业、简洁等)
- 能力边界:能做什么?不能做什么?
- 输出格式:如何组织答案?
与 MEAI 的 System Message 对比:
| 特性 | MEAI System Message | MAF Instructions |
|---|---|---|
| 配置时机 | 每次请求时传入 | Agent 创建时配置一次 |
| 生命周期 | 单次调用 | Agent 整个生命周期 |
| 复用性 | 需要手动复用 | 自动应用到所有请求 |
| 适用场景 | 灵活的单次对话 | 长期运行的 Agent |
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 对比有无 Instructions 的 Agent ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 没有 Instructions 的 Agent
var agentNoInstructions = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "BasicAgent"
});
var threadA = agentNoInstructions.GetNewThread();
var responseA = await agentNoInstructions.RunAsync("你是谁?", threadA);
Console.WriteLine("没有 Instructions 的 Agent:");
Console.WriteLine($" {responseA.Text}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 有 Instructions 的 Agent
var agentWithInstructions = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "EnglishCoach",
// 详细的角色和行为定义
Instructions = @"
你是一名专业的英语口语教练,名字叫 Alex。
你的职责:
- 帮助用户提升英语口语能力
- 纠正发音和语法错误
- 提供实用的口语表达建议
你的风格:
- 友好、鼓励、耐心
- 使用简单易懂的语言
- 每次回复不超过 100 字
你的回复格式:
1. 先肯定用户的努力
2. 指出需要改进的地方
3. 给出正确的表达方式
4. 提供一个相关的例句
"
});
var threadB = agentWithInstructions.GetNewThread();
var responseB = await agentWithInstructions.RunAsync("你是谁?", threadB);
Console.WriteLine("有 Instructions 的 Agent:");
Console.WriteLine($" {responseB.Text}");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("说明:Instructions 决定了 Agent 的角色、行为和回复风格");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Instructions 最佳实践模板
// 完整的 Instructions 示例
var bestPracticeInstructions = @"
# 你是谁(角色定位)
你是一名资深的软件架构师,名字叫李明,专注于 .NET 技术栈。
# 你的专业领域
- .NET Core / .NET 8+ 架构设计
- 微服务和分布式系统
- 云原生应用开发(Azure / AWS)
- 性能优化和可扩展性设计
# 你的职责
1. 回答用户关于 .NET 架构设计的问题
2. 提供代码审查和重构建议
3. 推荐最佳实践和设计模式
4. 警告常见的架构陷阱
# 你的回复风格
- 专业但易懂,避免过度技术术语
- 优先提供实用的代码示例
- 对复杂问题给出循序渐进的解决方案
- 每次回复控制在 300 字以内
# 你的限制
- 不讨论非 .NET 技术栈(如 Java、Python)
- 不提供完整项目代码,只给关键代码片段
- 不参与公司内部技术选型决策
# 特殊处理
- 如果问题超出你的专业领域,诚实告知并建议咨询相关专家
- 如果问题过于宽泛,引导用户提供更具体的场景
";
好的 Instructions 包含的要素:
角色定位(你是谁)
明确 Agent 的身份和专业背景
给 Agent 一个名字更有亲和力
专业领域(你懂什么)
列出核心专长和技能范围
帮助 AI 聚焦到特定领域
职责范围(你做什么)
明确 Agent 应该执行的任务
用数字列表清晰呈现
回复风格(如何回复)
定义语气和表达方式
控制回复的长度和详细程度
能力边界(不做什么)
明确拒绝处理的内容
防止超出能力范围的请求
特殊处理(异常情况)
如何处理边界情况
如何引导用户优化提问
4. 配置项3:ChatOptions - AI 调用配置
ChatOptions 是 AI 调用的全局配置,控制底层 AI 模型的行为,包括:
| 配置项 | 作用 | 典型值 |
|---|---|---|
| Tools | Function Calling 工具列表 | [AIFunctionFactory.Create(...)] |
| ToolMode | 工具调用模式 | Auto / Required / None |
| Temperature | 创造性程度 | 0.0 (精确) ~ 1.0 (创意) |
| MaxOutputTokens | 最大输出 Token 数 | 1000, 2000, 4096 |
| TopP | 核采样参数 | 0.9, 0.95, 1.0 |
| ResponseFormat | 输出格式 | Text / Json / JsonSchema |
| StopSequences | 停止序列 | ["END", "---"] |
| FrequencyPenalty | 词频惩罚 | -2.0 ~ 2.0 |
| PresencePenalty | 存在惩罚 | -2.0 ~ 2.0 |
重要特性:
- ChatOptions 在 Agent 创建时配置,应用到所有后续请求
- 也可以在运行时通过 ChatClientAgentRunOptions 扩展配置
- 运行时的配置会与默认配置合并(merge):
- 集合类型(如 Tools):会合并(union),不是替换
- 标量值(如 Temperature):运行时值优先(覆盖)
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 使用 ChatOptions 配置 Agent ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 配置 ChatOptions
var chatOptions = new ChatOptions
{
Temperature = 0.3f, // 较低的温度,输出更稳定
MaxOutputTokens = 500, // 限制输出长度
TopP = 0.9f, // 核采样参数
FrequencyPenalty = 0.5f, // 减少重复词汇
PresencePenalty = 0.2f // 鼓励话题多样性
};
var agentWithOptions = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "PreciseAssistant",
Instructions = "你是一个精确、简洁的技术助手。",
ChatOptions = chatOptions // 传入 ChatOptions
});
var thread = agentWithOptions.GetNewThread();
var response = await agentWithOptions.RunAsync(
"请用一句话解释什么是依赖注入?",
thread);
Console.WriteLine("Agent 的回复:");
Console.WriteLine($" {response.Text}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 在 ChatOptions 中配置 Function Calling ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 1. 定义工具函数
[Description("获取指定城市的天气信息")]
string GetWeather([Description("城市名称")] string city)
{
return city switch
{
"北京" => "晴天,气温 15-25°C,空气质量良好",
"上海" => "多云,气温 18-26°C,湿度较高",
"深圳" => "小雨,气温 20-28°C,建议携带雨具",
_ => $"{city}:暂无天气数据"
};
}
[Description("查询股票实时价格")]
string GetStockPrice([Description("股票代码")] string symbol)
{
return symbol.ToUpper() switch
{
"MSFT" => "Microsoft: $412.56 (↑ 2.3%)",
"AAPL" => "Apple: $189.78 (↓ 0.8%)",
"GOOGL" => "Google: $142.33 (↑ 1.5%)",
_ => $"{symbol}: 股票代码无效"
};
}
// 2. 配置 ChatOptions,添加工具
var chatOptionsWithTools = new ChatOptions
{
// 使用 AIFunctionFactory 创建工具
Tools =
[
AIFunctionFactory.Create(GetWeather),
AIFunctionFactory.Create(GetStockPrice)
],
ToolMode = ChatToolMode.Auto // 自动决定是否调用工具
};
// 3. 创建 Agent
var agentWithTools = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "InfoAssistant",
Instructions = "你是一个信息查询助手,可以查询天气和股票价格。",
ChatOptions = chatOptionsWithTools // 传入配置好的 ChatOptions
});
Console.WriteLine("Agent 创建完成,已配置 2 个工具:");
Console.WriteLine(" GetWeather - 查询天气");
Console.WriteLine(" GetStockPrice - 查询股票价格\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 4. 测试工具调用
var toolThread = agentWithTools.GetNewThread();
Console.WriteLine("测试 1:查询天气");
var weatherResponse = await agentWithTools.RunAsync("北京今天天气怎么样?", toolThread);
Console.WriteLine($" {weatherResponse.Text}\n");
Console.WriteLine("测试 2:查询股票");
var stockResponse = await agentWithTools.RunAsync("帮我查一下微软的股价", toolThread);
Console.WriteLine($" {stockResponse.Text}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 运行时扩展配置(ChatClientAgentRunOptions) ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 创建一个默认配置的 Agent
var defaultAgent = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "FlexibleAgent",
Instructions = "你是一个灵活的助手。",
ChatOptions = new ChatOptions
{
Temperature = 0.7f // 默认:中等创造性
}
});
var flexThread = defaultAgent.GetNewThread();
// 1. 使用默认配置
Console.WriteLine("使用默认配置 (Temperature = 0.7):");
var response1 = await defaultAgent.RunAsync("用一句话描述 AI", flexThread);
Console.WriteLine($" {response1.Text}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 2. 运行时扩展:降低温度(更精确)
Console.WriteLine("运行时扩展 (Temperature = 0.1 - 覆盖默认值):");
var response2 = await defaultAgent.RunAsync(
"用一句话描述 AI",
flexThread,
options: new ChatClientAgentRunOptions // 使用 ChatClientAgentRunOptions
{
ChatOptions = new ChatOptions { Temperature = 0.1f }
});
Console.WriteLine($" {response2.Text}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 3. 运行时扩展:提高温度(更有创意)
Console.WriteLine("运行时扩展 (Temperature = 1.0 - 覆盖默认值):");
var response3 = await defaultAgent.RunAsync(
"用一句话描述 AI",
flexThread,
options: new ChatClientAgentRunOptions // 使用 ChatClientAgentRunOptions
{
ChatOptions = new ChatOptions { Temperature = 1.0f }
});
Console.WriteLine($" {response3.Text}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("说明:");
Console.WriteLine(" - 运行时使用 ChatClientAgentRunOptions 包装 ChatOptions");
Console.WriteLine(" - 临时扩展不会影响 Agent 的默认配置");
Console.WriteLine(" - 标量值(如 Temperature)运行时值会覆盖默认值");
Console.WriteLine(" - 集合值(如 Tools)运行时值会与默认值合并");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
运行时扩展 Tools 列表(重要特性):运行时可以 扩展(合并) 工具列表,这在动态场景中非常有用。
var chatClient = AIClientHelper.GetQwenChatClient().AsBuilder()
.Use(getResponseFunc: async (messages, options, innerClient, token)=>{
Console.WriteLine($"tools list: { string.Join(",", options.Tools.Select(t=>t.Name))}");
var response = await innerClient.GetResponseAsync(messages, options, token);
return response;
},getStreamingResponseFunc: null)
.Build();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 运行时扩展 Tools 列表 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 定义额外的工具函数
[Description("计算两个数字的和")]
int Calculate([Description("第一个数字")] int a, [Description("第二个数字")] int b)
{
return a + b;
}
// 创建一个带天气工具的 Agent
var weatherOnlyAgent = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "WeatherAgent",
Instructions = "你是一个天气查询助手。",
ChatOptions = new ChatOptions
{
Tools = [AIFunctionFactory.Create(GetWeather)], // 默认只有天气工具
ToolMode = ChatToolMode.Auto
}
});
var toolOverrideThread = weatherOnlyAgent.GetNewThread();
// 1. 使用默认工具(只有 GetWeather)
Console.WriteLine("使用默认工具(GetWeather):");
var weatherResp = await weatherOnlyAgent.RunAsync("北京天气怎么样?", toolOverrideThread);
Console.WriteLine($" {weatherResp.Text}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 2. 运行时扩展工具列表(添加 Calculate)
Console.WriteLine("运行时扩展工具列表(添加 Calculate - 与默认工具合并):");
var calcResp = await weatherOnlyAgent.RunAsync(
"请帮我计算 123 + 456 等于多少?",
toolOverrideThread,
options: new ChatClientAgentRunOptions
{
ChatOptions = new ChatOptions
{
// 添加新工具(会与默认的 GetWeather 合并)
Tools = [AIFunctionFactory.Create(Calculate)],
ToolMode = ChatToolMode.Auto
}
});
Console.WriteLine($" {calcResp.Text}\n");
Console.WriteLine("运行时 Tools 会与默认 Tools 合并,现在 Agent 同时拥有:");
Console.WriteLine(" - GetWeather(默认)");
Console.WriteLine(" - Calculate(运行时添加)\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 3. 运行时添加多个工具(继续扩展)
Console.WriteLine("运行时添加多个工具(GetStockPrice + Calculate):");
var multiResp = await weatherOnlyAgent.RunAsync(
"北京天气如何?微软股价多少?123+456等于多少?",
toolOverrideThread,
options: new ChatClientAgentRunOptions
{
ChatOptions = new ChatOptions
{
// 提供多个工具(会与默认 GetWeather 合并)
Tools =
[
AIFunctionFactory.Create(GetStockPrice),
AIFunctionFactory.Create(Calculate)
],
ToolMode = ChatToolMode.Auto
}
});
Console.WriteLine($" {multiResp.Text}\n");
Console.WriteLine("现在 Agent 同时拥有三个工具:");
Console.WriteLine(" - GetWeather(默认)");
Console.WriteLine(" - GetStockPrice(运行时添加)");
Console.WriteLine(" - Calculate(运行时添加)\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
5. 配置项 4:ChatMessageStoreFactory - 消息存储工厂
ChatMessageStoreFactory 是一个工厂函数,用于创建自定义的 消息存储策略(ChatMessageStore)。
核心概念:
- 默认情况下,Agent 的对话消息存储在内存中
- 通过 ChatMessageStoreFactory,可以实现自定义持久化策略
- 每个 AgentThread 会调用工厂创建独立的存储实例
适用场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| 持久化存储 | 将对话历史保存到数据库、文件系统、云存储 | 用户关闭应用后可恢复对话 |
| 分布式存储 | 在多服务器环境下共享对话状态 | Redis、Azure Cosmos DB |
| 审计与合规 | 记录完整对话日志用于审计 | 金融、医疗行业的合规要求 |
| 多租户隔离 | 为不同租户使用独立存储 | SaaS 应用的数据隔离 |
| 消息过滤 | 在存储前处理敏感信息 | 过滤个人身份信息(PII) |
var agent = chatClient.CreateAIAgent(options: new ChatClientAgentOptions()
{
Name = "PersistentAgent",
ChatMessageStoreFactory = (context) =>
{
// 从 context.SerializedState 恢复状态(如果有)
// 返回自定义的 ChatMessageStore 实现
return new DatabaseChatMessageStore(connectionString);
}
});
注意事项:
- 工厂函数在每次创建 新 Thread 时调用
- 需要自己实现 ChatMessageStore 的抽象类
- 状态恢复通过 SerializedState 实现(用于断点续传)
6. 配置项 5:AIContextProviderFactory - 上下文提供者工厂
AIContextProviderFactory 是一个工厂函数,用于创建 动态上下文提供者(AIContextProvider)。
核心概念:
- 在 Agent 运行时,动态注入额外的上下文信息到对话中
- 每个 AgentThread 会调用工厂创建独立的上下文提供者实例
- 上下文信息会自动添加到每次 AI 调用前
适用场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| 动态用户信息 | 根据当前用户注入个性化信息 | 用户偏好、历史订单、会员等级 |
| 实时环境数据 | 注入系统状态、时间、位置等信息 | 当前时间、天气、系统负载 |
| 会话上下文 | 根据对话进展动态调整上下文 | 多轮对话中的状态跟踪 |
| 权限控制 | 根据用户权限注入可访问的数据范围 | 企业级应用的数据隔离 |
| A/B 测试 | 注入实验配置,动态切换 Agent 行为 | 不同版本的 Instructions |
var agent = chatClient.CreateAIAgent(options: new ChatClientAgentOptions()
{
Name = "ContextAwareAgent",
AIContextProviderFactory = (context) =>
{
// 从 context.SerializedState 恢复状态(如果有)
// 返回自定义的 AIContextProvider 实现
return new UserContextProvider(userId: currentUserId);
}
});
上下文提供者的工作流程:
sequenceDiagram
participant User
participant Agent
participant ContextProvider
participant AI Model
User->>Agent: 发送消息
Agent->>ContextProvider: 请求当前上下文
ContextProvider-->>Agent: 返回上下文信息
Agent->>AI Model: 消息 + 上下文
AI Model-->>Agent: 响应
Agent-->>User: 返回结果
与 Instructions 的区别:
| 特性 | Instructions | AIContextProvider |
|---|---|---|
| 配置时机 | Agent 创建时固定 | 每次运行时动态生成 |
| 内容 | 静态的角色和规则 | 动态的运行时信息 |
| 适用场景 | 通用行为定义 | 个性化、实时数据 |
| 示例 | "你是客服助手" | "当前用户:张三,VIP会员" |
注意事项:
- 工厂函数在每次创建 新 Thread 时调用
- 需要自己实现 AIContextProvider 的抽象类
- 上下文信息会自动注入到每次 AI 请求中
7. 配置项 6:UseProvidedChatClientAsIs - 禁用默认装饰器
UseProvidedChatClientAsIs 是一个布尔值配置,控制是否禁用 Agent 的默认装饰器。
核心概念:
- 默认情况下(false),MAF 会自动为 IChatClient 添加默认装饰器
- 设置为 true 时,Agent 会直接使用传入的 IChatClient,不添加任何装饰器
- 这个配置适用于需要完全自定义 ChatClient 管道的高级场景
默认装饰器的作用
| 默认装饰器 | 作用 | 影响 |
|---|---|---|
| 自动函数调用 | 自动执行 Function Calling | 处理工具调用和结果注入 |
| 消息格式化 | 标准化消息格式 | 确保与 Agent 框架兼容 |
| 错误处理 | 统一异常处理 | 捕获并转换底层错误 |
默认行为(推荐)
// UseProvidedChatClientAsIs = false(默认)
var agent = chatClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
Name = "MyAgent",
// MAF 会自动添加装饰器(自动函数调用、消息格式化等)
});
// 等价于:
var decoratedClient = chatClient
.AsBuilder()
.UseFunctionInvocation() // 自动函数调用
.UseMessageFormatter() // 消息格式化
.UseErrorHandling() // 错误处理
.Build();
何时使用 UseProvidedChatClientAsIs = true?
| 场景 | 说明 | 示例 |
|---|---|---|
| 自定义中间件管道 | 已经在 IChatClient 上配置了完整的中间件链 | 自定义日志、限流、缓存策略 |
| 替换默认行为 | 不希望使用 MAF 的默认装饰器 | 自定义函数调用逻辑 |
| 性能优化 | 减少不必要的装饰器层级 | 高性能场景下的精简配置 |
| 测试和调试 | 需要完全控制 ChatClient 行为 | 单元测试、集成测试 |
// 场景:已经自定义了完整的中间件管道
var customChatClient = baseChatClient
.AsBuilder()
.Use(new CustomLoggingMiddleware()) // 自定义日志
.Use(new CustomRateLimitingMiddleware()) // 自定义限流
.Use(new CustomCachingMiddleware()) // 自定义缓存
.UseFunctionInvocation() // 手动添加函数调用
.Build();
// 禁用默认装饰器,直接使用自定义管道
var agent = customChatClient.CreateAIAgent(options: new ChatClientAgentOptions()
{
Name = "CustomPipelineAgent",
UseProvidedChatClientAsIs = true, // 直接使用传入的 IChatClient
ChatOptions = new ChatOptions
{
Tools = [AIFunctionFactory.Create(MyTool)]
}
});
注意事项:使用 UseProvidedChatClientAsIs = true 时需要注意:
函数调用不会自动执行
必须手动添加 .UseFunctionInvocation()
否则工具调用不会生效
消息格式可能不兼容:需要确保 IChatClient 返回的消息格式符合 MAF 要求
错误处理需要自己实现:MAF 不会自动捕获和转换异常
推荐做法
// 正确:完整的自定义管道
var customClient = baseChatClient
.AsBuilder()
.Use(new MyLoggingMiddleware())
.Use(new MyRateLimitingMiddleware())
.UseFunctionInvocation() // 必须手动添加
.Build();
var agent = customClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
UseProvidedChatClientAsIs = true,
ChatOptions = new ChatOptions
{
Tools = [...], // 工具可以正常工作
ToolMode = ChatToolMode.Auto
}
});
// 错误:缺少 UseFunctionInvocation
var incorrectClient = baseChatClient
.AsBuilder()
.Use(new MyLoggingMiddleware())
.Build(); // 没有 UseFunctionInvocation
var brokenAgent = incorrectClient.CreateAIAgent(
options: new ChatClientAgentOptions()
{
UseProvidedChatClientAsIs = true,
ChatOptions = new ChatOptions
{
Tools = [...], // 工具不会被调用!
}
});
8. 配置方式对比与选择
| 配置方式 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| 仅配置 Name | 简单的对话 Agent | 简洁,易于追踪 | 功能有限 |
| 配置 Instructions | 需要特定角色和行为的 Agent | 定义清晰的身份和职责 | 无法控制模型参数 |
| 配置 ChatOptions | 需要工具调用或精细控制 AI 行为 | 强大的功能扩展 | 配置复杂 |
| 配置 ChatMessageStoreFactory | 需要持久化对话历史 | 灵活的存储策略 | 需要实现自定义 Store |
| 配置 AIContextProviderFactory | 需要动态注入运行时上下文 | 个性化、实时数据注入 | 需要实现自定义 Provider |
| 配置 UseProvidedChatClientAsIs | 需要完全自定义中间件管道 | 完全控制 ChatClient 行为 | 需要手动配置所有装饰器 |
