Spiga

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 需要恢复对话状态时
  1. 创建模拟对话
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("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
  1. 序列化对话状态,关键 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("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
  1. 反序列化恢复对话,关键 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("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
  1. 使用恢复的 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() 或配置 ChatOptions.ResponseFormat
DeepSeek 返回带说明文本 未设置 useJsonSchemaResponseFormat: false 添加参数并在 Instructions 中强调"只返回 JSON"
反序列化失败 数据模型不匹配 检查 JsonPropertyName 和字段类型定义
枚举值解析错误 未使用 JsonStringEnumConverter 为枚举属性添加 [JsonConverter] 特性
Result 属性为 null 使用了非泛型方法 使用 RunAsync() 而不是 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 行为 需要手动配置所有装饰器