Spiga

Net+AI智能体进阶6:Agent进阶扩展

2025-11-08 18:03:58

一、自定义文件消息存储

1. ChatMessageStore 架构概览

classDiagram
    class ChatMessageStore {
        <<abstract>>
        #IChatReducer? ChatReducer
        +AddMessagesAsync(messages)
        +GetMessagesAsync()
        +ClearAsync()
        +Serialize()
        +Deserialize(state)
    }
    
    class InMemoryChatMessageStore {
        -List~ChatMessage~ _messages
        +AddMessagesAsync()
        +GetMessagesAsync()
        +ClearAsync()
    }
    
    class FileChatMessageStore {
        -string _filePath
        -SemaphoreSlim _lock
        +AddMessagesAsync()
        +GetMessagesAsync()
        +ClearAsync()
    }
    
    class RedisChatMessageStore {
        -IConnectionMultiplexer _redis
        +AddMessagesAsync()
        +GetMessagesAsync()
        +ClearAsync()
    }
    
    ChatMessageStore <|-- InMemoryChatMessageStore
    ChatMessageStore <|-- FileChatMessageStore
    ChatMessageStore <|-- RedisChatMessageStore
    
  • ChatMessageStore 抽象类核心方法职责
方法 职责 使用场景
AddMessagesAsync 添加新消息到存储(应用 Reducer) 每次对话后保存消息
GetMessagesAsync 获取当前所有消息 Agent 执行前加载历史
ClearAsync 清空所有消息 重置对话线程
Serialize 序列化当前状态 持久化到数据库/文件
Deserialize 反序列化并恢复状态 从数据库/文件恢复
  • 关键属性
public abstract class ChatMessageStore
{
    // Reducer 集成点:在 AddMessagesAsync 中自动应用
    protected IChatReducer? ChatReducer { get; }
    
    // 基类自动处理 Reducer 应用逻辑
    protected async Task ApplyReducerIfConfiguredAsync()
    {
        if (ChatReducer != null)
        {
            var messages = await GetMessagesAsync();
            var reduced = await ChatReducer.ReduceAsync(messages);
            await ClearAsync();
            await AddMessagesAsync(reduced);
        }
    }
}

2. InMemoryChatMessageStore 工作原理

通过代码分析理解内置内存存储的实现机制,InMemoryChatMessageStore 核心实现:

public class InMemoryChatMessageStore : ChatMessageStore
{
    private readonly List<ChatMessage> _messages = new();
    
    public InMemoryChatMessageStore(
        IChatReducer? chatReducer = null,
        JsonElement? serializedStoreState = null,
        JsonSerializerOptions? jsonSerializerOptions = null)
    {
        ChatReducer = chatReducer;
        
        // 反序列化支持:从持久化状态恢复
        if (serializedStoreState.HasValue)
        {
            var messages = JsonSerializer.Deserialize<List<ChatMessage>>(
                serializedStoreState.Value,
                jsonSerializerOptions);
            if (messages != null) _messages.AddRange(messages);
        }
    }
    
    // 添加消息并应用 Reducer
    public override async Task AddMessagesAsync(
        IEnumerable<ChatMessage> messages,
        CancellationToken cancellationToken = default)
    {
        _messages.AddRange(messages);
        await ApplyReducerIfConfiguredAsync(); // 基类方法自动裁剪
    }
    
    // 获取当前所有消息(只读副本)
    public override Task<IReadOnlyList<ChatMessage>> GetMessagesAsync(
        CancellationToken cancellationToken = default)
    {
        return Task.FromResult<IReadOnlyList<ChatMessage>>(
            _messages.AsReadOnly());
    }
    
    // 清空所有消息
    public override Task ClearAsync(
        CancellationToken cancellationToken = default)
    {
        _messages.Clear();
        return Task.CompletedTask;
    }
}

关键特性

  • 优点:

    • 零 I/O 延迟,性能极佳

    • 实现简单,适合原型开发

    • 自动支持 Reducer 集成

  • 限制:

    • 进程重启后数据丢失

    • 无法跨实例共享数据

    • 内存占用随对话增长

3. 实现 FileChatMessageStore

创建一个自定义存储,将对话历史持久化到 JSON 文件。

  • 实现自定义 ChatMessageStor
/// <summary>
/// 基于文件的消息存储实现
/// </summary>
public class FileChatMessageStore : ChatMessageStore
{
    private readonly string _filePath;
    private readonly JsonSerializerOptions _jsonOptions;
    private readonly SemaphoreSlim _lock = new(1, 1); // 线程安全锁
    
    #pragma warning disable MEAI001

    public IChatReducer? ChatReducer { get; }
    
    public FileChatMessageStore(
        string filePath,
        IChatReducer? chatReducer = null,
        JsonElement? serializedStoreState = null,
        JsonSerializerOptions? jsonSerializerOptions = null)
    {
        _filePath = filePath;
        _jsonOptions = jsonSerializerOptions ?? new JsonSerializerOptions 
        { 
            WriteIndented = true,
            Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
        };
        
        // 继承基类的 Reducer 属性
        ChatReducer = chatReducer;
        
        // 初始化:从序列化状态或现有文件恢复
        if (serializedStoreState.HasValue && 
            serializedStoreState.Value.ValueKind != JsonValueKind.Undefined && 
            serializedStoreState.Value.ValueKind != JsonValueKind.Null)
        {
            var messages = JsonSerializer.Deserialize<List<ChatMessage>>(
                serializedStoreState.Value,
                _jsonOptions);
            if (messages != null)
            {
                // 使用同步方法避免构造函数中的 sync-over-async
                SaveMessagesToFile(messages);
            }
        }
        else if (!File.Exists(_filePath))
        {
            // 创建空文件
            Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
            File.WriteAllText(_filePath, "[]");
        }
    }
    
    /// <summary>
    /// 添加消息并持久化到文件
    /// </summary>
    public override async Task AddMessagesAsync(
        IEnumerable<ChatMessage> messages,
        CancellationToken cancellationToken = default)
    {
        await _lock.WaitAsync(cancellationToken);
        try
        {
            // 1. 加载现有消息
            var existingMessages = await LoadMessagesFromFileAsync(cancellationToken);
            
            // 2. 添加新消息
            var allMessages = new List<ChatMessage>(existingMessages);
            allMessages.AddRange(messages);
            
            // 3. 应用 Reducer(如果配置)
            if (ChatReducer != null)
            {
                var reduced = await ChatReducer.ReduceAsync(allMessages, cancellationToken);
                allMessages = reduced.ToList();
            }
            
            // 4. 保存到文件
            await SaveMessagesToFileAsync(allMessages, cancellationToken);
        }
        finally
        {
            _lock.Release();
        }
    }
    
    /// <summary>
    /// 获取所有消息
    /// </summary>
    public override async Task<IEnumerable<ChatMessage>> GetMessagesAsync(
        CancellationToken cancellationToken = default)
    {
        await _lock.WaitAsync(cancellationToken);
        try
        {
            return await LoadMessagesFromFileAsync(cancellationToken);
        }
        finally
        {
            _lock.Release();
        }
    }

    
    /// <summary>
    /// 清空所有消息
    /// </summary>
    public async Task ClearAsync(CancellationToken cancellationToken = default)
    {
        await _lock.WaitAsync(cancellationToken);
        try
        {
            await File.WriteAllTextAsync(_filePath, "[]", cancellationToken);
        }
        finally
        {
            _lock.Release();
        }
    }

    /// <summary>
    /// 序列化当前存储状态
    /// </summary>
    public override JsonElement Serialize(JsonSerializerOptions? options = null)
    {
        // 优化:使用同步锁和同步 IO 避免 sync-over-async 问题
        _lock.Wait();
        try
        {
            var messages = LoadMessagesFromFile();
            return JsonSerializer.SerializeToElement(messages, options ?? _jsonOptions);
        }
        finally
        {
            _lock.Release();
        }
    }
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 私有辅助方法
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━    
    private List<ChatMessage> LoadMessagesFromFile()
    {
        if (!File.Exists(_filePath))
        {
            return new List<ChatMessage>();
        }
        
        var json = File.ReadAllText(_filePath);
        return JsonSerializer.Deserialize<List<ChatMessage>>(json, _jsonOptions) 
               ?? new List<ChatMessage>();
    }

    private async Task<List<ChatMessage>> LoadMessagesFromFileAsync(
        CancellationToken cancellationToken = default)
    {
        if (!File.Exists(_filePath))
        {
            return new List<ChatMessage>();
        }
        
        var json = await File.ReadAllTextAsync(_filePath, cancellationToken);
        return JsonSerializer.Deserialize<List<ChatMessage>>(json, _jsonOptions) 
               ?? new List<ChatMessage>();
    }
    
    private void SaveMessagesToFile(IEnumerable<ChatMessage> messages)
    {
        var json = JsonSerializer.Serialize(messages, _jsonOptions);
        File.WriteAllText(_filePath, json);
    }

    private async Task SaveMessagesToFileAsync(
        IEnumerable<ChatMessage> messages,
        CancellationToken cancellationToken = default)
    {
        var json = JsonSerializer.Serialize(messages, _jsonOptions);
        await File.WriteAllTextAsync(_filePath, json, cancellationToken);
    }
}
Console.WriteLine("FileChatMessageStore 已定义");
Console.WriteLine(" - 支持 JSON 文件持久化");
Console.WriteLine(" - 集成 IChatReducer 支持");
Console.WriteLine(" - 线程安全 (SemaphoreSlim)");
  • 使用自定义文件存储创建 Agent
#pragma warning disable MEAI001
var storageFilePath = Path.Combine(Path.GetTempPath(), "maf-chat-history.json");

// 清理旧文件
if (File.Exists(storageFilePath))
{
    File.Delete(storageFilePath);
}

var options = new ChatClientAgentOptions
{
    Instructions = "你是一个旅游顾问,帮助用户规划行程。",
    Name = "旅游助手",
    
    // 使用自定义文件存储(不带 Reducer)
    ChatMessageStoreFactory = ctx => new FileChatMessageStore(
        filePath: storageFilePath,
        chatReducer: null, // 暂不使用 Reducer
        serializedStoreState: ctx.SerializedState,
        jsonSerializerOptions: ctx.JsonSerializerOptions
    )
};

var agent = chatClient.CreateAIAgent(options);
Console.WriteLine("Agent 已创建(使用 FileChatMessageStore)");
Console.WriteLine($" 存储路径: {storageFilePath}\n");
  • 多轮对话并验证文件持久化
var thread = agent.GetNewThread();
Console.WriteLine("═══════════════════════════════════════");
Console.WriteLine("第 1 轮对话");
Console.WriteLine("═══════════════════════════════════════\n");
var response1 = await agent.RunAsync("我想去日本旅游,推荐几个城市", thread);
Console.WriteLine($"{response1.Text}\n");
Console.WriteLine($" 存储路径: {storageFilePath}\n");
Console.WriteLine("\n═══════════════════════════════════════");

Console.WriteLine("第 2 轮对话");
Console.WriteLine("═══════════════════════════════════════\n");
var response2 = await agent.RunAsync("东京有哪些必去景点?", thread);
Console.WriteLine($"{response2.Text}\n");
Console.WriteLine($" 存储路径: {storageFilePath}\n");
  • 查看文件内容
Console.WriteLine("文件存储的完整内容:\n");
Console.WriteLine($" 存储路径: {storageFilePath}\n");
var msgStore = thread.GetService<ChatMessageStore>();
msgStore.Display();
var msgs = await msgStore.GetMessagesAsync();
msgs.Display();

4. 文件存储 + Reducer 结合

  • 创建带 Reducer 的 Agent
var storageFilePathWithReducer = Path.Combine(Path.GetTempPath(), "maf-chat-history-reduced.json");
// 清理旧文件
if (File.Exists(storageFilePathWithReducer))
{
    File.Delete(storageFilePathWithReducer);
}

#pragma warning disable MEAI001
var optionsWithReducer = new ChatClientAgentOptions
{
    Instructions = "你是一个美食推荐助手。",
    Name = "美食顾问",    
    // 文件存储 + Reducer 组合
    ChatMessageStoreFactory = ctx => new FileChatMessageStore(
        filePath: storageFilePathWithReducer,
        chatReducer: new MessageCountingChatReducer(targetCount: 3), // 仅保留最近 3 条
        serializedStoreState: ctx.SerializedState,
        jsonSerializerOptions: ctx.JsonSerializerOptions
    )
};

var agentWithReducer = chatClient.CreateAIAgent(optionsWithReducer);
Console.WriteLine("Agent 已创建(FileChatMessageStore + MessageCountingChatReducer)");
Console.WriteLine($" 存储路径: {storageFilePathWithReducer}");
Console.WriteLine(" 裁剪策略: 保留最近 3 条消息\n");
  • 多轮对话观察裁剪效果
var threadWithReducer = agentWithReducer.GetNewThread();
var queries = new[]
{
    "推荐成都的特色美食",
    "火锅哪家好吃?",
    "串串和火锅有什么区别?",
    "推荐一家适合约会的餐厅",
    "有没有素食餐厅?"
};
foreach (var query in queries)
{
    var resp = await agentWithReducer.RunAsync(query, threadWithReducer);
    Console.WriteLine($"用户: {query}");
    Console.WriteLine($"助手: {resp.Text?.Substring(0, Math.Min(60, resp.Text.Length))}...");
}
var msgStore = threadWithReducer.GetService<ChatMessageStore>();
msgStore.Display();
var messagesWithReducer = await msgStore.GetMessagesAsync();
messagesWithReducer.Display();

Console.WriteLine($" 存储路径: {storageFilePathWithReducer}");

5. 模拟进程重启后的恢复

  • 序列化 Thread 状态
// 序列化当前 Thread
var serializedState = threadWithReducer.Serialize();

Console.WriteLine("Thread 已序列化\n");
Console.WriteLine("序列化内容预览:");
var previewLength = Math.Min(300, serializedState.ToString().Length);
Console.WriteLine(serializedState.ToString().Substring(0, previewLength));
Console.WriteLine("\n...(已省略)\n");

Console.WriteLine("在生产环境中,可将序列化数据保存到:");
Console.WriteLine(" - 数据库 (SQL Server, Cosmos DB)");
Console.WriteLine(" - 缓存 (Redis)");
Console.WriteLine(" - 配合文件存储实现双重持久化\n");
  • 模拟重启后恢复
Console.WriteLine("模拟进程重启...\n");

// 重新创建 Agent(模拟新进程)
var newAgent = chatClient.CreateAIAgent(optionsWithReducer);
// 从序列化状态恢复 Thread
var restoredThread = newAgent.DeserializeThread(serializedState);
Console.WriteLine("Thread 已从序列化数据恢复\n");

// 继续对话
var continueResponse = await newAgent.RunAsync(
    "刚才推荐的素食餐厅在哪里?",
    restoredThread
);
Console.WriteLine($"用户: 刚才推荐的素食餐厅在哪里?");
Console.WriteLine($"助手: {continueResponse.Text}\n");
Console.WriteLine("Agent 能够基于恢复的历史继续对话!");
Console.WriteLine("文件存储 + 序列化实现了完整的持久化方案");

6. 自定义 ChatMessageStore 最佳实践

选择合适的存储方案

场景 推荐存储 优势 注意事项
原型开发* InMemoryChatMessageStore 零延迟,实现简单 无持久化
单机部署 FileChatMessageStore 简单持久化,无依赖 I/O 性能限制
分布式系统 RedisChatMessageStore 跨实例共享,高性能 需 Redis 基础设施
企业级应用 DatabaseChatMessageStore 可靠性高,审计完整 需关系型数据库
云原生应用 AzureCosmosChatMessageStore 全球分布,弹性扩展 成本较高

实现 ChatMessageStore 的关键点

public class CustomChatMessageStore : ChatMessageStore
{
    // 必须实现的核心方法
    public override async Task AddMessagesAsync(
        IEnumerable<ChatMessage> messages,
        CancellationToken cancellationToken = default)
    {
        // 1. 保存新消息到存储介质
        await SaveToStorage(messages);
        
        // 2. 应用 Reducer(如果配置)
        if (ChatReducer != null)
        {
            var all = await GetMessagesAsync(cancellationToken);
            var reduced = await ChatReducer.ReduceAsync(all, cancellationToken);
            await ClearAsync(cancellationToken);
            await SaveToStorage(reduced);
        }
    }
    
    // 必须返回只读集合
    public override Task<IEnumerable<ChatMessage>> GetMessagesAsync(...)
    {
        return LoadFromStorage().AsReadOnly();
    }
    
    // 确保线程安全(文件/数据库锁)
    private readonly SemaphoreSlim _lock = new(1, 1);
}

性能优化建议

// 推荐:批量操作减少 I/O
public override async Task AddMessagesAsync(IEnumerable<ChatMessage> messages, ...)
{
    var batch = messages.ToList();
    await _db.BulkInsertAsync(batch); // 批量插入
}

// 推荐:使用异步 I/O
await File.WriteAllTextAsync(...); // 而非 File.WriteAllText(...)

// 推荐:实现缓存层
private List<ChatMessage>? _cachedMessages;
public override async Task<IReadOnlyList<ChatMessage>> GetMessagesAsync(...)
{
    if (_cachedMessages == null)
    {
        _cachedMessages = await LoadFromStorageAsync();
    }
    return _cachedMessages.AsReadOnly();
}

错误处理与容错

public override async Task AddMessagesAsync(...)
{
    try
    {
        await SaveToStorage(messages);
    }
    catch (IOException ex)
    {
        // 降级策略:切换到内存存储
        _logger.LogWarning(ex, "文件存储失败,切换到内存模式");
        _fallbackStore.AddRange(messages);
    }
    catch (UnauthorizedAccessException ex)
    {
        // 权限问题:抛出明确异常
        throw new InvalidOperationException("无权限写入存储文件", ex);
    }
}

生产环境的存储策略

// 企业级方案:数据库 + Redis 缓存
public class HybridChatMessageStore : ChatMessageStore
{
    private readonly IDatabase _redis;
    private readonly DbContext _db;
    
    public override async Task AddMessagesAsync(...)
    {
        // 1. 写入数据库(持久化)
        await _db.Messages.AddRangeAsync(messages);
        await _db.SaveChangesAsync();
        
        // 2. 更新 Redis 缓存(高性能读取)
        var json = JsonSerializer.Serialize(messages);
        await _redis.StringSetAsync($"chat:{threadId}", json, TimeSpan.FromHours(1));
    }
    
    public override async Task<IReadOnlyList<ChatMessage>> GetMessagesAsync(...)
    {
        // 1. 优先从 Redis 读取
        var cached = await _redis.StringGetAsync($"chat:{threadId}");
        if (cached.HasValue)
        {
            return JsonSerializer.Deserialize<List<ChatMessage>>(cached!)!;
        }
        
        // 2. 缓存未命中,从数据库加载
        var messages = await _db.Messages
            .Where(m => m.ThreadId == threadId)
            .OrderBy(m => m.Timestamp)
            .ToListAsync();
        
        // 3. 回写 Redis
        await _redis.StringSetAsync(
            $"chat:{threadId}",
            JsonSerializer.Serialize(messages),
            TimeSpan.FromHours(1)
        );
        
        return messages;
    }
}

二、自定义中间件

1. MAF 三层中间件架构

  • Agent Run 中间件层(最外层)

    • 职责:拦截 Agent 运行时的消息流(用户输入 + Agent 响应)

    • 典型应用:

      • PII 过滤:自动过滤手机号、邮箱、身份证等敏感信息

      • Guardrails:阻止生成有害、违规、敏感内容

      • 内容审计:记录所有对话内容供审计

      • 内容转换:格式化输出、多语言翻译

    • 重要:这是实际执行的最外层,在所有其他中间件之前执行!

  • ChatClient 中间件层(LLM 调用拦截)

    • 职责:拦截每次发送到 LLM 的请求和响应(在 Agent Run 内部,每次 LLM 调用时触发)

    • 典型应用:

      • 全局监控:记录所有 LLM 调用的耗时、Token 使用量
      • 限流控制:控制 API 调用频率,避免超额

      • 缓存策略:缓存重复的请求,减少 API 调用

      • 多模型路由:根据请求类型路由到不同的 LLM

    • 重要:在一次 RunAsync() 中,ChatClient 中间件可能触发多次(每次 LLM 调用)!

  • Function Invocation 中间件层(工具调用拦截)

    • 职责:拦截 Agent 调用工具函数的执行(在工具调用时触发)

    • 典型应用:

      • 调用日志:记录工具名称、参数、结果

      • 权限检查:验证用户是否有权限调用某个工具

      • Mock 数据:测试环境返回模拟数据

      • 性能监控:统计工具执行时间

sequenceDiagram
    participant U as 用户
    participant AR as Agent Run 中间件
    participant CC as ChatClient 中间件
    participant A as Agent 核心
    participant FI as Function 中间件
    participant T as 工具函数
    
    U->>AR: 1. 发送请求(包含 PII)
    AR->>AR: Guardrails 检查
    AR->>AR: 过滤 PII 信息
    AR->>CC: 2. 转发清洗后的请求
    CC->>CC: 记录开始时间
    CC->>A: 3. 调用 LLM
    A->>A: LLM 决策:需要调用工具
    A->>FI: 4. 调用工具
    FI->>FI: 结果覆盖检查
    FI->>FI: 记录工具调用日志
    FI->>T: 5. 执行工具函数
    T-->>FI: 6. 返回结果
    FI-->>A: 7. 返回结果(可能已覆盖)
    A->>CC: 8. 再次调用 LLM(生成最终响应)
    CC->>CC: 统计 Token 和耗时
    CC-->>AR: 9. 返回 Agent 响应
    AR->>AR: 再次检查响应内容
    AR-->>U: 10. 返回最终响应
  • 关键理解
    • 洋葱模型:请求从外向内穿透,响应从内向外返回
    • Agent Run 最外层:Guardrails 和 PII 过滤在整个流程的最外层
    • ChatClient 多次触发:在一次 Run 中可能触发多次(每次 LLM 调用)
    • Function 在工具调用时:只在工具调用时执行
    • 职责分离:每层中间件只关注自己的职责
    • 可组合:可以在同一层添加多个中间件,按顺序执行

2. 观察 Agent 基础行为

首先,我们创建一个简单的智能客服 Agent,不添加任何中间件,作为对比基线。

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   创建基础智能客服 Agent(无中间件)   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 定义工具函数:查询天气
[Description("查询指定城市的天气情况")]
string GetWeather([Description("城市名称")] string city)
{
    // 模拟天气查询
    return city switch
    {
        "北京" => "北京今天多云,气温 15°C",
        "上海" => "上海今天晴天,气温 20°C",
        "深圳" => "深圳今天阴天,气温 22°C",
        _ => $"{city}今天天气未知"
    };
}

// 定义工具函数:查询订单状态
[Description("查询订单的当前状态")]
string QueryOrderStatus([Description("订单号")] string orderId)
{
    // 模拟订单查询
    return orderId switch
    {
        "#DD20250109001" => "您的订单已发货,预计明天送达",
        "#DD20250109002" => "您的订单正在处理中",
        _ => "未找到该订单"
    };
}

var tools =  new List<AIFunction>
{
    AIFunctionFactory.Create(GetWeather),
    AIFunctionFactory.Create(QueryOrderStatus)
};

// 1. 创建基础 Agent(无中间件)
var basicAgent = chatClient.CreateAIAgent(
    instructions: @"你是某电商平台的智能客服机器人,专门处理售后咨询。
擅长领域:
- 订单状态查询
- 天气查询(帮助用户安排收货)
- 退货退款政策解答

回答风格:
- 友好、耐心、专业
- 使用简洁明了的语言
- 提供具体的操作步骤",
    name: "CustomerServiceBot",
    tools: [..tools]
);
Console.WriteLine("基础智能客服 Agent 创建完成(无中间件)\n");

// 2. 测试基础 Agent
// 测试一个正常的咨询场景,观察 Agent 的基础行为。
Console.WriteLine("━━━━ 测试基础 Agent(正常咨询)━━━━\n");
var thread = basicAgent.GetNewThread();
Console.WriteLine("客户:我想查询一下北京明天的天气,我的订单 #DD20250109001 什么时候到货?\n");

var response = await basicAgent.RunAsync(
    "我想查询一下北京明天的天气,我的订单 #DD20250109001 什么时候到货?", 
    thread);
Console.WriteLine("客服 Agent:");
Console.WriteLine(response.Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   观察:基础 Agent 正常工作,但缺少企业级控制   ");
Console.WriteLine("    - 没有 PII 过滤");
Console.WriteLine("    - 没有工具调用日志");
Console.WriteLine("    - 没有监控统计");
Console.WriteLine(" 接下来,我们将通过中间件添加这些能力!");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

3. agent.AsBuilder().Use() 的四种重载方式

在实现中间件之前,让我们先了解 AIAgentBuilder 提供的 .Use() 方法的四种重载方式。

  • Use(Func) - 最简单的包装

    适用场景:

    • 简单地包装内部 Agent,不需要访问 IServiceProvider
    • 适合静态的中间件逻辑

    示例:

.Use(innerAgent => 
{
    // 返回一个包装了 innerAgent 的自定义 Agent
    return new MyCustomAgent(innerAgent);
})
  • Use(Func) - 带依赖注入的包装

    适用场景:

    • 需要从 IServiceProvider 获取依赖服务

    • 适合需要访问日志、配置等服务的中间件

    示例:

.Use((innerAgent, services) => 
{
    var logger = services.GetService<ILogger>();
    return new LoggingAgent(innerAgent, logger);
})
  • Use(sharedFunc) - 共享的前置/后置处理

    适用场景:

    • 只需要在 Agent 执行前后添加逻辑,不需要处理返回值

    • 同一个逻辑同时应用于 RunAsync 和 RunStreamingAsync

    • 适合记录日志、验证输入等场景

    关键特点:

    • 无法访问或修改 Agent 的返回结果

    • 传入的 next 委托返回 Task(无返回值)

    • 主要用于副作用(side effects)

    示例:

.Use(async (messages, thread, options, next, ct) => 
{
    Console.WriteLine("Agent 开始执行(前置)");
    
    // 调用内部 Agent(无法获取返回值)
    await next(messages, thread, options, ct);
    
    Console.WriteLine("Agent 执行完成(后置)");
})
  • Use(runFunc, runStreamingFunc) - 完全控制非流式和流式

    适用场景:

    • 需要完全控制 Agent 的执行和返回结果

    • 可以修改、过滤或替换 Agent 的返回内容

    • 可以分别处理非流式和流式场景

    关键特点:

    • 可以访问和修改 Agent 的返回结果

    • 两个参数可以同时提供,也可以只提供一个(另一个传 null)

    • 如果只提供 runFunc,流式方法会自动基于非流式结果实现(有限流式)

    • 如果只提供 runStreamingFunc,非流式方法会自动聚合流式更新

    示例:

// 1. 只处理非流式(推荐用于大多数中间件)
.Use(
    runFunc: async (messages, thread, options, innerAgent, ct) => 
    {
        Console.WriteLine("Agent 开始执行");
        
        // 调用内部 Agent 并获取返回值
        var response = await innerAgent.RunAsync(messages, thread, options, ct);
        
        // 可以修改返回结果
        Console.WriteLine($"Agent 返回:{response.Messages.Count} 条消息");
        
        return response;
    },
    runStreamingFunc: null  // 不处理流式
)

// 2. 同时处理非流式和流式
.Use(
    runFunc: async (messages, thread, options, innerAgent, ct) => 
    {
        // 处理非流式
        var response = await innerAgent.RunAsync(messages, thread, options, ct);
        return response;
    },
    runStreamingFunc: async (messages, thread, options, innerAgent, ct) => 
    {
        // 处理流式
        await foreach (var update in innerAgent.RunStreamingAsync(messages, thread, options, ct))
        {
            yield return update;
        }
    }
)

4. Agent Run 中间件

Agent Run 中间件:拦截 Agent 运行时的消息流,可以在消息发送给 LLM 之前和 LLM 响应返回之后进行处理。

  • 实现中间件
/// <summary>
/// PII 过滤中间件:自动检测并脱敏个人敏感信息
/// </summary>
async Task<AgentRunResponse> PIIFilterMiddleware(
    IEnumerable<ChatMessage> messages, 
    AgentThread? thread, 
    AgentRunOptions? options, 
    AIAgent innerAgent, 
    CancellationToken cancellationToken)
{
    Console.WriteLine("[PII 过滤] Pre-Run:开始过滤输入消息的 PII 信息");    
    // 步骤 1:过滤输入消息
    var filteredMessages = FilterMessages(messages);    
    // 步骤 2:调用内部 Agent
    var response = await innerAgent.RunAsync(filteredMessages, thread, options, cancellationToken);    
    // 步骤 3:过滤输出消息
    response.Messages = FilterMessages(response.Messages);    
    Console.WriteLine("[PII 过滤] Post-Run:输出消息 PII 过滤完成");    
    return response;
    
    // 辅助方法:过滤消息列表
    static List<ChatMessage> FilterMessages(IEnumerable<ChatMessage> messages)
    {
        return messages.Select(m => new ChatMessage(m.Role, RedactPII(m.Text))).ToList();
    }
    
    // 辅助方法:脱敏 PII 信息
    static string RedactPII(string content)
    {
        if (string.IsNullOrEmpty(content))
            return content;
        
        // 1. 手机号脱敏(中国手机号格式)
        // 匹配:138-0013-8000、13800138000、138 0013 8000
        var phonePattern = @"\b1[3-9]\d[\s\-]?\d{4}[\s\-]?\d{4}\b";
        content = Regex.Replace(content, phonePattern, "[REDACTED: PHONE]");
        
        // 2. 邮箱脱敏
        // 匹配:user@example.com
        var emailPattern = @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b";
        content = Regex.Replace(content, emailPattern, "[REDACTED: EMAIL]");
        
        // 3. 身份证号脱敏(中国身份证)
        // 匹配:18位身份证号
        var idCardPattern = @"\b\d{17}[\dXx]\b";
        content = Regex.Replace(content, idCardPattern, "[REDACTED: ID_CARD]");
        
        // 4. 银行卡号脱敏
        // 匹配:16-19位连续数字
        var bankCardPattern = @"\b\d{16,19}\b";
        content = Regex.Replace(content, bankCardPattern, "[REDACTED: BANK_CARD]");
        
        // 5. 姓名脱敏(简单版,检测"我叫XXX"模式)
        // 生产环境应使用 NER 模型
        var namePattern = @"(?:我叫|我是|姓名[是::])\s*([^\s,。,\.]{2,4})";
        content = Regex.Replace(content, namePattern, match =>
        {
            Console.WriteLine($"   检测到姓名:{match.Groups[1].Value}");
            return match.Value.Replace(match.Groups[1].Value, "[REDACTED: NAME]");
        });        
        return content;
    }
}

/// <summary>
/// Guardrails 中间件:内容合规检查
/// </summary>
async Task<AgentRunResponse> GuardrailsMiddleware(
    IEnumerable<ChatMessage> messages, 
    AgentThread? thread, 
    AgentRunOptions? options, 
    AIAgent innerAgent, 
    CancellationToken cancellationToken)
{
    Console.WriteLine("[Guardrails 中间件] Pre-Run:检查输入消息合规性");    
    // 步骤 1:检查输入消息
    var filteredMessages = FilterMessages(messages);    
    // 步骤 2:调用内部 Agent
    var response = await innerAgent.RunAsync(filteredMessages, thread, options, cancellationToken);    
    // 步骤 3:检查输出消息
    response.Messages = FilterMessages(response.Messages);    
    Console.WriteLine("[Guardrails 中间件] Post-Run:输出消息合规检查完成");    
    return response;
    
    // 辅助方法:过滤消息列表
    static List<ChatMessage> FilterMessages(IEnumerable<ChatMessage> messages)
    {
        return messages.Select(m => new ChatMessage(m.Role, FilterContent(m.Text))).ToList();
    }
    
    // 辅助方法:检查内容合规性
    static string FilterContent(string content)
    {
        if (string.IsNullOrEmpty(content))
            return content;
        
        // 敏感词列表(简化版,生产环境应使用完善的敏感词库)
        string[] forbiddenKeywords = ["harmful", "illegal", "violence", "有害", "违法"];
        
        foreach (var keyword in forbiddenKeywords)
        {
            if (content.Contains(keyword, StringComparison.OrdinalIgnoreCase))
            {
                Console.WriteLine($"   检测到敏感词:{keyword}");
                return "[REDACTED: 检测到违规内容,已被系统拦截]";
            }
        }        
        return content;
    }
}
  • 添加 Agent Run 中间件,使用 .Use() 的重载4(runFunc + runStreamingFunc)添加 Agent Run 中间件:

    • runFunc: 指定非流式处理函数(可以访问和修改返回结果)

    • runStreamingFunc: 设为 null(不处理流式)

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   添加 Agent Run 中间件(PII + Guardrails)   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 使用 .Use() 重载 4 添加 Agent Run 中间件
var agentWithRunMiddleware = basicAgent
    .AsBuilder()
    .Use(PIIFilterMiddleware, null)      // PII 过滤(runFunc, runStreamingFunc)
    .Use(GuardrailsMiddleware, null)      // Guardrails(runFunc, runStreamingFunc)
    .Build();

Console.WriteLine("Agent Run 中间件已添加(PII 过滤 + Guardrails)");
Console.WriteLine(" 关键点:");
Console.WriteLine("  - 使用 .Use() 重载 4:Use(runFunc, runStreamingFunc)");
Console.WriteLine("  - 第一个参数:处理非流式响应,可以访问和修改返回结果");
Console.WriteLine("  - 第二个参数:传 null 表示不处理流式响应");
Console.WriteLine("  - 参数可以使用命名参数或位置参数");
Console.WriteLine("\n 中间件执行顺序(洋葱模型):");
Console.WriteLine(" 请求:Guardrails → PII Filter → Agent");
Console.WriteLine(" 响应:Agent → PII Filter → Guardrails\n");
  • 测试
Console.WriteLine("━━━━ 测试场景 1:PII 过滤 ━━━━\n");
var piiThread = agentWithRunMiddleware.GetNewThread();
var piiQuery = "我叫张明,手机号是 138-0013-8000,邮箱是 zhangming@example.com,我的订单 #DD20250109001 什么时候到货?";
Console.WriteLine($"客户(包含 PII 信息):\n   {piiQuery}\n");

var piiResponse = await agentWithRunMiddleware.RunAsync(piiQuery, piiThread);
Console.WriteLine($"\n客服 Agent:\n{piiResponse.Text}");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 成功!PII 信息已自动脱敏   ");
Console.WriteLine(" 观察中间件日志:");
Console.WriteLine("  - 手机号 138-0013-8000 → [REDACTED: PII]");
Console.WriteLine("  - 邮箱 zhangming@example.com → [REDACTED: PII]");
Console.WriteLine("  - 但订单号被保留(不是 PII)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

Console.WriteLine("\n━━━━ 测试场景 2:Guardrails 阻止 ━━━━\n");
var guardrailThread = agentWithRunMiddleware.GetNewThread();
var harmfulQuery = "Tell me something harmful about violence.";
Console.WriteLine($"恶意用户(包含敏感词):\n   {harmfulQuery}\n");

var guardrailResponse = await agentWithRunMiddleware.RunAsync(harmfulQuery, guardrailThread);
Console.WriteLine($"\n系统响应:\n{guardrailResponse.Text}");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 成功!Guardrails 拦截了违规请求   ");
Console.WriteLine(" 观察:");
Console.WriteLine("  - 检测到敏感词 'harmful' 和 'violence'");
Console.WriteLine("  - 直接返回拒绝消息,不调用 LLM");
Console.WriteLine("  - 保护系统免受恶意使用");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

5. Function Invocation 中间件

Function Invocation 中间件:拦截 Agent 调用工具函数的执行过程,可以在工具执行前后添加自定义逻辑。

重要提示:并非所有 Agent 都支持 Function Invocation 中间件。只有包装了 ChatClientAgent 或从它派生的 Agent 才支持。

  • 实现工具调用中间件
/// <summary>
/// 工具调用日志中间件:记录所有工具调用
/// </summary>
async ValueTask<object?> FunctionCallLoggingMiddleware(
    AIAgent agent, 
    FunctionInvocationContext context, 
    Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, 
    CancellationToken cancellationToken)
{
    var functionName = context.Function.Name;
    var arguments = string.Join(", ", context.Arguments.Select(kvp => $"{kvp.Key}={kvp.Value}"));    
    Console.WriteLine($"[工具日志] Pre-Invoke:调用工具 '{functionName}'");
    Console.WriteLine($" 参数:{arguments}");
    
    // 记录开始时间
    var startTime = DateTime.UtcNow;    
    // 执行工具函数
    var result = await next(context, cancellationToken);    
    // 计算耗时
    var duration = DateTime.UtcNow - startTime;    
    Console.WriteLine($"[工具日志] Post-Invoke:工具 '{functionName}' 执行完成");
    Console.WriteLine($" 结果:{result}");
    Console.WriteLine($" 耗时:{duration.TotalMilliseconds:F2}ms");    
    return result;
}

/// <summary>
/// 工具结果覆盖中间件:可以修改工具返回结果
/// </summary>
async ValueTask<object?> FunctionResultOverrideMiddleware(
    AIAgent agent, 
    FunctionInvocationContext context, 
    Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, 
    CancellationToken cancellationToken)
{
    Console.WriteLine($"[结果覆盖] Pre-Invoke:检查工具 '{context.Function.Name}'");
    
    // 执行工具函数
    var result = await next(context, cancellationToken);    
    // 针对特定工具覆盖结果
    if (context.Function.Name == "GetWeather")
    {
        // 示例:将所有天气结果改为"阳光明媚"(可用于测试或降级)
        var overriddenResult = "今天阳光明媚,气温适宜,适合出门!";        
        Console.WriteLine($"[结果覆盖] Post-Invoke:覆盖 GetWeather 结果");
        Console.WriteLine($" 原始结果:{result}");
        Console.WriteLine($" 覆盖结果:{overriddenResult}");        
        return overriddenResult;
    }    
    Console.WriteLine($"[结果覆盖] Post-Invoke:工具 '{context.Function.Name}' 使用原始结果");     return result;
}
  • 添加 Function Invocation 中间件

    Function Invocation 中间件通过 MAF 的扩展方法添加(不是 AIAgentBuilder 的四种重载之一)。

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   添加 Function Invocation 中间件   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 添加 Function Invocation 中间件(通过扩展方法)
var agentWithFunctionMiddleware = agentWithRunMiddleware
    .AsBuilder()
    .Use(FunctionCallLoggingMiddleware)       // 日志中间件
    .Use(FunctionResultOverrideMiddleware)    // 结果覆盖中间件
    .Build();

Console.WriteLine("Function Invocation 中间件已添加");
Console.WriteLine(" 关键点:");
Console.WriteLine("  - Function 中间件通过扩展方法实现");
Console.WriteLine("  - 不是 AIAgentBuilder 的四种重载之一");
Console.WriteLine("\n 中间件执行顺序(洋葱模型):");
Console.WriteLine(" 请求:结果覆盖 → 日志 → 工具函数");
Console.WriteLine(" 响应:工具函数 → 日志 → 结果覆盖\n");
  • 测试
Console.WriteLine("━━━━ 测试场景 3:工具调用拦截 ━━━━\n");
var functionThread = agentWithFunctionMiddleware.GetNewThread();
var toolQuery = "北京明天的天气怎么样?我的订单 #DD20250109001 什么时候到?";
Console.WriteLine($"客户:{toolQuery}\n");

var toolResponse = await agentWithFunctionMiddleware.RunAsync(toolQuery, functionThread);
Console.WriteLine($"\n客服 Agent:\n{toolResponse.Text}");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 成功!观察完整的工具调用流程   ");
Console.WriteLine(" 关键观察点:");
Console.WriteLine("  1. 日志中间件记录了工具调用的参数和结果");
Console.WriteLine("  2. 覆盖中间件修改了 GetWeather 的结果");
Console.WriteLine("  3. QueryOrderStatus 保持原始结果");
Console.WriteLine("  4. 洋葱模型:覆盖→日志→工具→日志→覆盖");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

6. ChatClient 中间件

ChatClient 中间件是最外层的中间件,拦截所有发送到 LLM 的请求和响应。这是最底层的拦截点,所有 Agent 的 LLM 调用都会经过这一层。

  • 实现全局中间件
/// <summary>
/// ChatClient 全局监控中间件:记录所有 LLM 调用
/// </summary>
async Task<ChatResponse> GlobalMonitoringMiddleware(
    IEnumerable<ChatMessage> messages, 
    ChatOptions? options, 
    IChatClient innerChatClient, 
    CancellationToken cancellationToken)
{
    var messageCount = messages.Count();    
    Console.WriteLine($"[全局监控] Pre-Chat:开始 LLM 调用");
    Console.WriteLine($" 消息数量:{messageCount}");
    
    // 记录开始时间
    var startTime = DateTime.UtcNow;    
    // 调用 LLM
    var response = await innerChatClient.GetResponseAsync(messages, options, cancellationToken);    
    // 计算耗时
    var duration = DateTime.UtcNow - startTime;
    
    // 统计 Token
    var totalTokens = response.Usage?.TotalTokenCount ?? 0;
    var inputTokens = response.Usage?.InputTokenCount ?? 0;
    var outputTokens = response.Usage?.OutputTokenCount ?? 0;    
    Console.WriteLine($"[全局监控] Post-Chat:LLM 调用完成");
    Console.WriteLine($" 耗时:{duration.TotalMilliseconds:F2}ms");
    Console.WriteLine($" Token 使用:总计 {totalTokens} (输入 {inputTokens} + 输出 {outputTokens})");    
    return response;
}
  • 构建完整的三层中间件 Agent

    现在,我们将三层中间件全部组合起来,构建一个企业级的 Agent。

    注意不同中间件的添加方式与实际执行顺序

    • ChatClient 中间件:使用 Microsoft.Extensions.AI 的 IChatClient 扩展方法(在 LLM 调用时触发)

    • Agent Run 中间件:使用 .Use() 重载 4(runFunc:+runStreamingFunc:)- 最外层执行

    • Function 中间件:使用 MAF 提供的扩展方法(在工具调用时触发)

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   构建完整的企业级 Agent(三层中间件)   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 步骤 1:在 ChatClient 层添加中间件(使用 MEAI 的扩展方法)
var monitoredChatClient = chatClient
    .AsBuilder()
    .Use(getResponseFunc:GlobalMonitoringMiddleware, getStreamingResponseFunc: null)  // ChatClient 中间件
    .Build();
Console.WriteLine("步骤 1:ChatClient 层中间件已添加(全局监控)");

// 步骤 2:创建 Agent 并添加所有中间件
var enterpriseAgent = monitoredChatClient.CreateAIAgent(
    instructions: @"你是企业级智能客服机器人,专业、安全、可控。",
    name: "EnterpriseCustomerServiceBot",
    tools:
    [
        AIFunctionFactory.Create(GetWeather),
        AIFunctionFactory.Create(QueryOrderStatus)
    ])
    .AsBuilder()
    // Agent Run 中间件(使用 .Use() 重载 4)
    .Use(GuardrailsMiddleware, null)     // Guardrails(runFunc, runStreamingFunc)
    .Use(PIIFilterMiddleware, null)       // PII 过滤(runFunc, runStreamingFunc)
    // Function Invocation 中间件(使用扩展方法)
    .Use(FunctionCallLoggingMiddleware)                        // 工具日志
    .Use(FunctionResultOverrideMiddleware)                     // 结果覆盖
    .Build();

Console.WriteLine("步骤 2:Agent Run 中间件已添加(Guardrails + PII 过滤)");
Console.WriteLine("步骤 3:Function Invocation 中间件已添加(日志 + 覆盖)");

Console.WriteLine("\n完整的中间件管道(洋葱模型):");
Console.WriteLine(" 实际执行顺序(从外到内):");
Console.WriteLine(" 请求流向:");
Console.WriteLine(" 用户 → Guardrails → PII过滤 → 全局监控(LLM调用) → Agent核心");
Console.WriteLine("            ↓(工具调用时)");
Console.WriteLine("        结果覆盖 → 工具日志 → 工具执行");
Console.WriteLine(" 响应流向:");
Console.WriteLine(" 工具执行 → 工具日志 → 结果覆盖 → Agent核心");
Console.WriteLine("            ↓");
Console.WriteLine(" 全局监控(LLM调用) → PII过滤 → Guardrails → 用户");
Console.WriteLine("\n 中间件添加方式总结:");
Console.WriteLine("    - ChatClient 中间件:MEAI 的 IChatClient 扩展方法");
Console.WriteLine("    - Agent Run 中间件:AIAgentBuilder.Use() 重载 4");
Console.WriteLine("   语法:.Use(runFunc, runStreamingFunc)");
Console.WriteLine("    - Function 中间件:MAF 提供的扩展方法");
Console.WriteLine("\n 关键理解:");
Console.WriteLine("  - Agent Run 层(Guardrails/PII)是最外层,先执行");
Console.WriteLine("  - ChatClient 层(全局监控)在 LLM 调用时触发,可能多次");
Console.WriteLine("  - Function 层(日志/覆盖)在工具调用时触发\n");
  • 完整企业级流程
Console.WriteLine("━━━━ 测试场景 4:完整的企业级流程 ━━━━\n");
var enterpriseThread = enterpriseAgent.GetNewThread();
var enterpriseQuery = "你好,我叫李华,电话 138-8888-8888。我想查询北京天气和我的订单 #DD20250109001";
Console.WriteLine($"客户(包含 PII + 工具调用):\n   {enterpriseQuery}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 开始执行,观察三层中间件的完整执行流程...");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

var enterpriseResponse = await enterpriseAgent.RunAsync(enterpriseQuery, enterpriseThread);
Console.WriteLine($"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"最终响应:\n{enterpriseResponse.Text}");

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 企业级流程执行完成!   ");
Console.WriteLine(" 中间件实际执行顺序总结:");
Console.WriteLine("  1. Guardrails(最外层):检查了内容合规性");
Console.WriteLine("  2. PII 过滤:脱敏了姓名和手机号");
Console.WriteLine("  3. 全局监控:记录了 LLM 调用的 Token 和耗时(触发2次)");
Console.WriteLine("  4. 结果覆盖:修改了 GetWeather 的返回结果");
Console.WriteLine("  5. 工具日志:记录了 GetWeather 和 QueryOrderStatus 调用");
Console.WriteLine(" 所有企业级控制都已生效!");
Console.WriteLine(" 注意:Agent Run 层(Guardrails/PII)在最外层先执行");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

7. 最佳实践

中间件执行顺序的最佳实践

用户请求
  ↓
Guardrails(Agent Run 层)- 最外层,先检查合规,快速拒绝违规请求
  ↓
PII 过滤(Agent Run 层)- 脱敏敏感信息
  ↓
全局监控(ChatClient 层)- 记录 LLM 调用(每次 LLM 调用时触发)
  ↓
Agent 核心逻辑(LLM 调用)
  ↓
结果覆盖(Function 层)- 可能修改工具结果
  ↓
工具日志(Function 层)- 记录实际执行的工具调用
  ↓
工具执行
  • 重要理解:

    • Agent Run 中间件(Guardrails、PII):在整个 RunAsync() 外层,最先执行

    • ChatClient 中间件(全局监控):在每次 LLM 调用时触发,可能触发多次

    • Function 中间件(日志、覆盖):在工具调用前后执行

  • 关键原则:

    • 安全检查在最外层:Agent Run 层的 Guardrails 最先执行,快速拒绝违规请求,节省资源

    • 数据清洗紧随其后:PII 过滤确保敏感信息不泄露

    • 监控在 LLM 调用处:ChatClient 层监控每次 LLM 调用,可能在一次 Run 中触发多次

    • 工具中间件最内层:Function 层中间件在工具调用时执行

常见错误与解决方案

问题 原因 解决方案
Function 中间件不生效 Agent 不支持 Function 中间件 确保使用 ChatClientAgent 或其派生类
中间件执行顺序混乱 添加顺序错误 记住洋葱模型:先添加的在外层
PII 过滤不完整 正则表达式覆盖不全 使用专业的 PII 检测库(如 Presidio)
性能下降 中间件过多或逻辑复杂 使用异步、缓存,避免阻塞
流式响应不支持 中间件未实现流式方法 同时实现 streamingMiddleware 参数

性能优化建议

关键点:

  • 快速失败:违规请求尽早拒绝

  • 并行检查:独立的检查可以并行执行

  • 智能缓存:缓存 PII 检测、Guardrails 结果

  • 异步优先:避免阻塞线程

// 推荐:使用异步和缓存
async Task<AgentRunResponse> OptimizedMiddleware(...)
{
    // 1. 快速路径:对于简单请求,跳过复杂处理
    if (IsSimpleRequest(messages))
    {
        return await innerAgent.RunAsync(messages, thread, options, cancellationToken);
    }
    
    // 2. 并行处理:多个独立的检查可以并行
    var tasks = new[]
    {
        Task.Run(() => CheckPII(messages)),
        Task.Run(() => CheckGuardrails(messages))
    };
    await Task.WhenAll(tasks);
    
    // 3. 缓存:避免重复计算
    var cacheKey = GenerateCacheKey(messages);
    if (_cache.TryGetValue(cacheKey, out var cachedResult))
    {
        return cachedResult;
    }
    
    var result = await innerAgent.RunAsync(messages, thread, options, cancellationToken);
    _cache.Set(cacheKey, result);
    
    return result;
}

中间件组合模式

  • 模式 1:分层中间件包(推荐)
// 定义不同层级的中间件包
public static class MiddlewarePackages
{
    // 安全包:PII + Guardrails
    public static AIAgent AddSecurityMiddleware(this AIAgent agent)
    {
        return agent.AsBuilder()
            .Use(GuardrailsMiddleware, null)
            .Use(PIIFilterMiddleware, null)
            .Build();
    }
    
    // 监控包:日志 + 统计
    public static AIAgent AddMonitoringMiddleware(this AIAgent agent)
    {
        return agent.AsBuilder()
            .Use(FunctionCallLoggingMiddleware)
            .Build();
    }
    
    // 企业级完整包
    public static AIAgent AddEnterpriseMiddleware(this AIAgent agent)
    {
        return agent
            .AddSecurityMiddleware()
            .AddMonitoringMiddleware();
    }
}

// 使用
var agent = basicAgent.AddEnterpriseMiddleware();
  • 模式 2:条件中间件
// 根据环境或配置动态添加中间件
var agent = basicAgent.AsBuilder();

if (config.EnablePIIFiltering)
{
    agent = agent.Use(PIIFilterMiddleware, null);
}

if (config.EnableMonitoring)
{
    agent = agent.Use(GlobalMonitoringMiddleware);
}

return agent.Build();

生产环境建议

  • 必备中间件清单:

    • 全局监控:记录 Token、耗时、错误率

    • PII 过滤:保护用户隐私

    • Guardrails:内容合规检查

    • 错误处理:优雅降级,避免崩溃

    • 审计日志:记录所有工具调用,供审计

  • 可选中间件:

    • 限流控制:防止 API 超额

    • 缓存策略:减少重复调用

    • 多模型路由:根据负载分发

    • A/B 测试:实验性功能灰度发布

三、中间件执行次序

接下里,我们来谈谈 Agent 中间件执行顺序详解,深入理解 ChatClient、Agent Run、Function Invocation 三层中间件的实际执行流程。

1. 环境准备

  • 定义带时间戳和计数器的中间件,为了清晰展示执行顺序,我们为每个中间件添加:

    • 时间戳:记录执行时刻(相对于开始时间)

    • 计数器:统计触发次数

    • 颜色标识:不同层级使用不同颜色

// 全局计数器和计时器
var startTime = DateTime.UtcNow;
var agentRunCounter = 0;
var chatClientCounter = 0;
var functionCounter = 0;

// 辅助方法:获取相对时间戳(毫秒)
double GetTimestamp() => (DateTime.UtcNow - startTime).TotalMilliseconds;

// 辅助方法:格式化时间戳
string FormatTimestamp(double timestamp) => $"[T+{timestamp:F0}ms]";

Console.WriteLine("时间戳和计数器工具定义完成");
  • 实现三层中间件
/// <summary>
/// Agent Run 中间件(最外层)
/// </summary>
async Task<AgentRunResponse> AgentRunMiddleware(
    IEnumerable<ChatMessage> messages, 
    AgentThread? thread, 
    AgentRunOptions? options, 
    AIAgent innerAgent, 
    CancellationToken cancellationToken)
{
    var count = ++agentRunCounter;
    var preTimestamp = GetTimestamp();
    
    Console.WriteLine($"\n{FormatTimestamp(preTimestamp)} [Agent Run #{count}] ═══ Pre-Run 开始 ═══");
    Console.WriteLine($"   消息数量:{messages.Count()}");
    
    // 调用内部 Agent(包含所有后续的中间件和逻辑)
    var response = await innerAgent.RunAsync(messages, thread, options, cancellationToken);
    
    var postTimestamp = GetTimestamp();
    Console.WriteLine($"\n{FormatTimestamp(postTimestamp)} [Agent Run #{count}] ═══ Post-Run 完成 ═══");
    Console.WriteLine($"   响应消息数量:{response.Messages.Count}");
    Console.WriteLine($"   Agent Run 总耗时:{postTimestamp - preTimestamp:F0}ms");
    
    return response;
}

/// <summary>
/// ChatClient 中间件(LLM 调用拦截)
/// </summary>
async Task<ChatResponse> ChatClientMiddleware(
    IEnumerable<ChatMessage> messages, 
    ChatOptions? options, 
    IChatClient innerChatClient, 
    CancellationToken cancellationToken)
{
    var count = ++chatClientCounter;
    var preTimestamp = GetTimestamp();
    
    Console.WriteLine($"\n{FormatTimestamp(preTimestamp)} [ChatClient #{count}] ─── Pre-Chat 开始 ───");
    Console.WriteLine($"   LLM 调用次数:第 {count} 次");
    Console.WriteLine($"   消息数量:{messages.Count()}");
    
    // 调用 LLM
    var response = await innerChatClient.GetResponseAsync(messages, options, cancellationToken);
    
    var postTimestamp = GetTimestamp();
    var tokens = response.Usage?.TotalTokenCount ?? 0;
    
    Console.WriteLine($"\n{FormatTimestamp(postTimestamp)} [ChatClient #{count}] ─── Post-Chat 完成 ───");
    Console.WriteLine($"   Token 使用:{tokens}");
    Console.WriteLine($"   LLM 调用耗时:{postTimestamp - preTimestamp:F0}ms");
    
    return response;
}

/// <summary>
/// Function Invocation 中间件(工具调用拦截)
/// </summary>
async ValueTask<object?> FunctionInvocationMiddleware(
    AIAgent agent, 
    FunctionInvocationContext context, 
    Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, 
    CancellationToken cancellationToken)
{
    var count = ++functionCounter;
    var preTimestamp = GetTimestamp();
    var functionName = context.Function.Name;
    var arguments = string.Join(", ", context.Arguments.Select(kvp => $"{kvp.Key}={kvp.Value}"));
    
    Console.WriteLine($"\n{FormatTimestamp(preTimestamp)} [Function #{count}] ··· Pre-Invoke 开始 ···");
    Console.WriteLine($"   工具名称:{functionName}");
    Console.WriteLine($"   参数:{arguments}");
    
    // 执行工具函数
    var result = await next(context, cancellationToken);
    
    var postTimestamp = GetTimestamp();
    
    Console.WriteLine($"\n{FormatTimestamp(postTimestamp)} [Function #{count}] ··· Post-Invoke 完成 ···");
    Console.WriteLine($"   结果:{result}");
    Console.WriteLine($"   工具执行耗时:{postTimestamp - preTimestamp:F0}ms");
    
    return result;
}
Console.WriteLine("三层中间件定义完成(带时间戳和计数器)");
  • 定于工具函数
[Description("查询指定城市的天气情况")]
string GetWeather([Description("城市名称")] string city)
{
    var timestamp = GetTimestamp();
    Console.WriteLine($"\n{FormatTimestamp(timestamp)} [工具执行] GetWeather(\"{city}\")");
    
    // 模拟耗时操作
    Thread.Sleep(100);
    
    return city switch
    {
        "北京" => "北京今天晴天,气温 15°C",
        "上海" => "上海今天多云,气温 20°C",
        _ => $"{city}今天天气未知"
    };
}

[Description("查询订单的当前状态")]
string QueryOrderStatus([Description("订单号")] string orderId)
{
    var timestamp = GetTimestamp();
    Console.WriteLine($"\n{FormatTimestamp(timestamp)} [工具执行] QueryOrderStatus(\"{orderId}\")");
    
    // 模拟耗时操作
    Thread.Sleep(100);
    
    return orderId switch
    {
        "#DD20250109001" => "您的订单已发货,预计明天送达",
        "#DD20250109002" => "您的订单正在处理中",
        _ => "未找到该订单"
    };
}
Console.WriteLine("工具函数定义完成");
  • 创建携带三层中间件 Agent
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   构建带三层中间件的 Agent   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 步骤 1:获取 ChatClient 并添加 ChatClient 中间件
var chatClient = AIClientHelper.GetDefaultChatClient()
    .AsBuilder()
    .Use(getResponseFunc: ChatClientMiddleware, getStreamingResponseFunc: null)
    .Build();

Console.WriteLine("ChatClient 中间件已添加");

// 步骤 2:创建 Agent 并添加 Agent Run 中间件和 Function 中间件
var agent = chatClient.CreateAIAgent(
    instructions: "你是智能客服机器人,擅长查询天气和订单状态。回答要简洁明了。",
    name: "CustomerServiceBot",
    tools:
    [
        AIFunctionFactory.Create(GetWeather),
        AIFunctionFactory.Create(QueryOrderStatus)
    ])
    .AsBuilder()
    .Use(AgentRunMiddleware, null)           // Agent Run 中间件
    .Use(FunctionInvocationMiddleware)        // Function 中间件
    .Build();

Console.WriteLine("Agent Run 中间件和 Function 中间件已添加");
Console.WriteLine("\n中间件架构:");
Console.WriteLine("   Agent Run 层(最外层)");
Console.WriteLine("    ↓");
Console.WriteLine("   ChatClient 层(每次 LLM 调用时触发)");
Console.WriteLine("    ↓");
Console.WriteLine("   Function 层(每次工具调用时触发)");
Console.WriteLine("\n准备测试...\n");

2. 实验:观察完整的执行流程

测试场景:我们将发送一个需要调用两个工具的请求,完整观察三层中间件的执行顺序和触发次数。

预期行为:

  • Agent Run 触发 1 次(包裹整个流程)
  • ChatClient 触发 2-3 次(理解意图 + 工具调用后生成响应)
  • Function 触发 2 次(GetWeather + QueryOrderStatus)
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   测试:观察三层中间件的执行流程   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 重置计时器和计数器
startTime = DateTime.UtcNow;
agentRunCounter = 0;
chatClientCounter = 0;
functionCounter = 0;

var thread = agent.GetNewThread();
var query = "北京今天天气怎么样?我的订单 #DD20250109001 什么时候到货?";

Console.WriteLine($"用户请求:{query}\n");
Console.WriteLine("═══════════════════════════════════════════════\n");
Console.WriteLine("开始执行,观察中间件执行顺序...\n");
Console.WriteLine("═══════════════════════════════════════════════");

var response = await agent.RunAsync(query, thread);

Console.WriteLine("\n═══════════════════════════════════════════════");
Console.WriteLine($"\n最终响应:\n{response.Text}");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

结果分析

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   中间件触发次数统计   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

var summary = new
{
    Agent_Run_触发次数 = agentRunCounter,
    ChatClient_触发次数 = chatClientCounter,
    Function_触发次数 = functionCounter
};

summary.Display();

Console.WriteLine("\n关键观察:");
Console.WriteLine($" 1. Agent Run 触发了 {agentRunCounter} 次(包裹整个流程)");
Console.WriteLine($" 2. ChatClient 触发了 {chatClientCounter} 次(每次 LLM 调用)");
Console.WriteLine($"    - 第1次:理解用户意图,决定调用工具");
if (chatClientCounter >= 2)
{
    Console.WriteLine($"    - 第2次:基于工具结果生成最终响应");
}
if (chatClientCounter >= 3)
{
    Console.WriteLine($"    - 第3次:可能是额外的推理步骤");
}
Console.WriteLine($" 3. Function 触发了 {functionCounter} 次(每次工具调用)");
Console.WriteLine($"    - GetWeather: 1次");
Console.WriteLine($"    - QueryOrderStatus: 1次");

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

3. 核心结论

  • 问题1:ChatClient 和 Function 中间件是覆盖还是协作关系?

    答案:协作关系,而非覆盖

    • ChatClient 中间件和 Function 中间件在不同的时间点执行

    • ChatClient 在每次 LLM 调用时触发(通常2-3次)

    • Function 在每次工具调用时触发(N次)

    • 它们互不干扰,各司其职

错误理解:ChatClient 会覆盖 Function 的逻辑
错误理解:Function 会覆盖 ChatClient 的逻辑

正确理解:它们是协作关系,在不同阶段执行
  • 问题2:三层中间件的实际执行顺序是什么?

    关键理解:

    • Agent Run:包裹整个流程,只执行1次

    • ChatClient:嵌套在 Agent Run 内,触发多次(通常2-3次)

    • Function:嵌套在 Agent Run 内,触发N次(取决于工具数量)

    • ChatClient 和 Function 不存在嵌套关系,它们是平行的,在不同时间点执行

T0: Agent Run Pre-Run(最外层开始)
↓
T1: ChatClient Pre-Chat(第1次 LLM 调用)
T2: ChatClient Post-Chat
↓ LLM 决策:需要调用工具
T3: Function Pre-Invoke(GetWeather)
T4: 工具执行:GetWeather
T5: Function Post-Invoke
↓
T6: Function Pre-Invoke(QueryOrderStatus)
T7: 工具执行:QueryOrderStatus
T8: Function Post-Invoke
↓ 工具调用完成,需要生成响应
T9: ChatClient Pre-Chat(第2次 LLM 调用)
T10: ChatClient Post-Chat
↓
T11: Agent Run Post-Run(最外层结束)
  • 问题3:为什么 Agent Run 是最外层?

    设计原则:

    • 职责范围:

      • Agent Run 负责整个 Agent 执行流程的控制(PII过滤、Guardrails)

      • ChatClient 只负责 LLM 调用的拦截(监控、限流)

      • Function 只负责工具调用的拦截(日志、权限)

    • 执行时机:

      • Agent Run 包裹整个 RunAsync(),确保所有操作都在其控制之下

      • ChatClient 和 Function 是 Agent 内部的操作,自然在 Agent Run 之内

    • 安全性:

      • 在最外层进行 PII 过滤和 Guardrails 检查,确保不安全的内容不会进入系统

      • 即使内部逻辑有漏洞,Agent Run 层也能兜底

  • 问题4:如何设计合理的中间件架构?

    最佳实践,核心原则:

    • 安全检查在最外层(Agent Run)
    • 全局监控在 LLM 层(ChatClient)
    • 细粒度控制在工具层(Function)
    • 每层职责单一,互不干扰
    Agent Run 层(最外层)
    - PII 过滤(防止敏感信息泄露)
    - Guardrails(内容合规检查)
    - 全局审计(记录所有对话)
    ↓
    ChatClient 层(LLM 调用拦截)
    - Token 统计(成本控制)
    - 限流控制(防止超额)
    - 缓存策略(减少调用)
    - 性能监控(响应时间)
    ↓
    Function 层(工具调用拦截)
    - 调用日志(审计追踪)
    - 权限检查(访问控制)
    - Mock 数据(测试支持)
    - 结果覆盖(降级策略)
    

4. 进阶实验:不同中间件组合效果

  • 实验1:只有 ChatClient 中间件
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   实验 1:只有 ChatClient 中间件   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 重置计数器
startTime = DateTime.UtcNow;
agentRunCounter = 0;
chatClientCounter = 0;
functionCounter = 0;

// 只添加 ChatClient 中间件
var chatClientOnly = AIClientHelper.GetDefaultChatClient()
    .AsBuilder()
    .Use(getResponseFunc: ChatClientMiddleware, getStreamingResponseFunc: null)
    .Build();

var agentWithChatClientOnly = chatClientOnly.CreateAIAgent(
    instructions: "你是智能客服机器人。",
    name: "ChatClientOnlyBot",
    tools: [AIFunctionFactory.Create(GetWeather)]);

var thread1 = agentWithChatClientOnly.GetNewThread();
await agentWithChatClientOnly.RunAsync("北京天气怎么样?", thread1);

Console.WriteLine("\n结果:");
Console.WriteLine($" - Agent Run 触发:{agentRunCounter} 次(未添加)");
Console.WriteLine($" - ChatClient 触发:{chatClientCounter} 次(已添加)");
Console.WriteLine($" - Function 触发:{functionCounter} 次(未添加)");
Console.WriteLine("\n 观察:只有 ChatClient 层被拦截,工具调用没有日志");
  • 实验2:只用 Function 中间件
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   实验 2:只有 Function 中间件   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 重置计数器
startTime = DateTime.UtcNow;
agentRunCounter = 0;
chatClientCounter = 0;
functionCounter = 0;

// 只添加 Function 中间件
var baseChatClient = AIClientHelper.GetDefaultChatClient();

var agentWithFunctionOnly = baseChatClient.CreateAIAgent(
    instructions: "你是智能客服机器人。",
    name: "FunctionOnlyBot",
    tools: [AIFunctionFactory.Create(GetWeather)])
    .AsBuilder()
    .Use(FunctionInvocationMiddleware)
    .Build();

var thread2 = agentWithFunctionOnly.GetNewThread();
await agentWithFunctionOnly.RunAsync("北京天气怎么样?", thread2);

Console.WriteLine("\n结果:");
Console.WriteLine($" - Agent Run 触发:{agentRunCounter} 次(未添加)");
Console.WriteLine($" - ChatClient 触发:{chatClientCounter} 次(未添加)");
Console.WriteLine($" - Function 触发:{functionCounter} 次(已添加)");
Console.WriteLine("\n 观察:只有工具调用被拦截,LLM 调用没有监控");
  • 实验3:只有 Agent Run 中间件
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   实验 3:只有 Agent Run 中间件   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 重置计数器
startTime = DateTime.UtcNow;
agentRunCounter = 0;
chatClientCounter = 0;
functionCounter = 0;

// 只添加 Agent Run 中间件
var agentWithRunOnly = baseChatClient.CreateAIAgent(
    instructions: "你是智能客服机器人。",
    name: "RunOnlyBot",
    tools: [AIFunctionFactory.Create(GetWeather)])
    .AsBuilder()
    .Use(AgentRunMiddleware, null)
    .Build();

var thread3 = agentWithRunOnly.GetNewThread();
await agentWithRunOnly.RunAsync("北京天气怎么样?", thread3);

Console.WriteLine("\n结果:");
Console.WriteLine($" - Agent Run 触发:{agentRunCounter} 次(已添加)");
Console.WriteLine($" - ChatClient 触发:{chatClientCounter} 次(未添加)");
Console.WriteLine($" - Function 触发:{functionCounter} 次(未添加)");
Console.WriteLine("\n 观察:只有 Agent 运行被拦截,LLM 和工具调用都没有日志");

5. 核心结论

  • 三层之间的关系

    • 协作关系,而非覆盖关系
    • 各层在不同时间点执行,互不干扰
    • 每层职责单一,可以独立添加或移除
  • 实际执行顺序

用户请求
↓
Agent Run Pre-Run(1次)
↓
ChatClient(多次,通常2-3次)
↓
Function(N次,取决于工具数量)
↓
Agent Run Post-Run(1次)
↓
用户响应
  • 触发时机:不同层级在不同时刻执行
中间件层级 触发时机 触发次数(一次 RunAsync)
Agent Run 中间件 整个 RunAsync() 的最外层 1次(包裹整个流程)
ChatClient 中间件 每次 LLM 调用时 多次(通常2-3次:理解意图 + 工具调用后生成响应)
Function 中间件 每次工具调用时 N次(取决于需要调用几个工具)

6. 最佳实践

  • 根据职责选择中间件
// 推荐:安全检查在 Agent Run 层(最外层)
.Use(PIIFilterMiddleware, null)      // Agent Run
.Use(GuardrailsMiddleware, null)     // Agent Run

// 推荐:LLM 监控在 ChatClient 层
chatClient.AsBuilder()
    .Use(getResponseFunc: TokenMonitoringMiddleware, getStreamingResponseFunc: null)

// 推荐:工具控制在 Function 层
.Use(FunctionLoggingMiddleware)      // Function
.Use(FunctionPermissionMiddleware)   // Function
  • 考虑性能影响
// 注意:ChatClient 中间件会触发多次,避免重复的昂贵操作
async Task<ChatResponse> ChatClientMiddleware(...)
{
    // 避免:每次 LLM 调用都执行昂贵的检查
    // await ExpensivePIICheck(messages);
    
    // 推荐:昂贵的检查放在 Agent Run 层(只执行1次)
    return await innerChatClient.GetResponseAsync(messages, options, ct);
}
  • 中间件组合模式
// 推荐:使用扩展方法组合中间件
public static class MiddlewareExtensions
{
    public static AIAgent AddEnterpriseMiddleware(this AIAgent agent)
    {
        return agent
            .AsBuilder()
            .Use(GuardrailsMiddleware, null)        // 安全检查
            .Use(PIIFilterMiddleware, null)         // PII 过滤
            .Use(FunctionLoggingMiddleware)         // 工具日志
            .Build();
    }
}

// 使用
var agent = baseChatClient
    .CreateAIAgent(...)
    .AddEnterpriseMiddleware();
  • 进阶建议
    • 使用条件中间件:根据环境或配置动态添加中间件
    • 实现中间件链:将多个相关中间件组合成一个包
    • 监控中间件性能:记录每个中间件的耗时,识别性能瓶颈
    • 测试中间件组合:确保不同组合下的行为符合预期

四、MEAI vs Agent 工具调用中间件

1. 两层工具调用拦截的关系

概念回顾

  • MEAI UseFunctionInvocation

    • 位置:ChatClient 层(MEAI)

    • 职责:自动处理函数调用循环

    • 功能:检测 LLM 返回的函数调用请求、自动执行、结果回传、迭代管理

  • MAF Function Middleware

    • 位置:Agent 层(MAF)

    • 职责:拦截具体的工具执行

    • 功能:日志、权限、Mock、监控

核心问题:两层工具调用拦截的关系?

当我们同时配置:

  • MEAI 的 UseFunctionInvocation (ChatClient 层)
  • MEAI 的 FunctionInvoker (在 UseFunctionInvocation 配置中)
  • MAF 的 Function Middleware (Agent 层)

它们之间是什么关系?会不会冲突?

正确理解:它们是嵌套的协作关系,按顺序执行。

执行链路详解

1. LLM 返回需要调用工具 GetWeather
2. UseFunctionInvocation 检测到 FunctionCallContent
3. UseFunctionInvocation 开始自动处理
4. 调用 FunctionInvoker(如果配置了)
5. FunctionInvoker 内部调用 context.Function.InvokeAsync()
6. 触发 MAF Function Middleware Pre-Invoke
7. 执行实际的工具函数 GetWeather("北京")
8. 触发 MAF Function Middleware Post-Invoke
9. 返回给 FunctionInvoker
10. UseFunctionInvocation 获得结果
11. UseFunctionInvocation 将结果添加到消息历史
12. UseFunctionInvocation 再次调用 LLM(基于工具结果)

关键理解

特性 MEAI UseFunctionInvocation MEAI FunctionInvoker MAF Function Middleware
层级 ChatClient 层 ChatClient 层(内部) Agent 层
职责 自动化循环管理 自定义调用前后逻辑 业务逻辑控制
触发时机 LLM 返回函数调用时 每次调用工具前 InvokeAsync 时
可见范围 整个函数调用流程 单次工具调用 单次工具执行
典型用途 迭代控制、并发、错误重试 监控、预处理 日志、权限、Mock
是否必需 可选(但推荐) 可选 可选

核心要点

  • UseFunctionInvocation 是最外层的自动化框架
  • FunctionInvoker 是 UseFunctionInvocation 内部的钩子
  • MAF Function Middleware 是 Agent 层的拦截器
  • FunctionInvoker 调用 InvokeAsync 时,会触发 MAF Function Middleware

2. 实验

  • 环境准备
// 定义工具函数
[Description("查询指定城市的天气情况")]
string GetWeather([Description("城市名称")] string city)
{
    Console.WriteLine($"    [实际工具执行] GetWeather(\"{city}\") 开始执行");
    // 模拟耗时操作
    Thread.Sleep(100);
    
    var result = city switch
    {
        "北京" => "北京今天晴天,气温 15°C",
        "上海" => "上海今天多云,气温 20°C",
        "深圳" => "深圳今天阴天,气温 22°C",
        _ => $"{city}今天天气未知"
    };    
    Console.WriteLine($"    [实际工具执行] GetWeather(\"{city}\") 执行完成:{result}");
    return result;
}

[Description("查询订单的当前状态")]
string QueryOrderStatus([Description("订单号")] string orderId)
{
    Console.WriteLine($"    [实际工具执行] QueryOrderStatus(\"{orderId}\") 开始执行");
    // 模拟耗时操作
    Thread.Sleep(100);
    
    var result = orderId switch
    {
        "#DD20250109001" => "您的订单已发货,预计明天送达",
        "#DD20250109002" => "您的订单正在处理中",
        _ => "未找到该订单"
    };    
    Console.WriteLine($"    [实际工具执行] QueryOrderStatus(\"{orderId}\") 执行完成:{result}");
    return result;
}

// 初始化计时器和计数器
using System.Threading;

// 全局计数器和计时器
var startTime = DateTime.UtcNow;
var agentRunCounter = 0;
var chatClientCounter = 0;
var useFunctionInvocationCounter = 0;  // UseFunctionInvocation 检测到函数调用的次数
var meaiFunctionInvokerCounter = 0;     // FunctionInvoker 执行次数
var mafFunctionCounter = 0;             // MAF Function Middleware 执行次数

// 辅助方法:获取相对时间戳(毫秒)
double GetTimestamp() => (DateTime.UtcNow - startTime).TotalMilliseconds;
// 辅助方法:格式化时间戳
string FormatTimestamp(double timestamp) => $"[T+{timestamp:F0}ms]";
  • 实验 1:只有 MAF Function Middleware(基线)
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   实验 1:只有 MAF Function Middleware(基线)   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 重置计数器
startTime = DateTime.UtcNow;
mafFunctionCounter = 0;

// MAF Function Middleware
async ValueTask<object?> MAFFunctionMiddleware(
    AIAgent agent, 
    FunctionInvocationContext context, 
    Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, 
    CancellationToken cancellationToken)
{
    var count = ++mafFunctionCounter;
    var timestamp = GetTimestamp();
    
    Console.WriteLine($"\n{FormatTimestamp(timestamp)} [MAF Function #{count}] ═══ Pre-Invoke ═══");
    Console.WriteLine($"   工具名称: {context.Function.Name}");
    Console.WriteLine($"   参数: {string.Join(", ", context.Arguments.Select(kvp => $"{kvp.Key}={kvp.Value}"))}");
    
    // 执行工具函数
    var result = await next(context, cancellationToken);
    
    var endTimestamp = GetTimestamp();
    Console.WriteLine($"\n{FormatTimestamp(endTimestamp)} [MAF Function #{count}] ═══ Post-Invoke ═══");
    Console.WriteLine($"   结果: {result}");
    Console.WriteLine($"   耗时: {endTimestamp - timestamp:F0}ms");
    
    return result;
}

// 创建 Agent(只有 MAF Function Middleware)
var baseChatClient = AIClientHelper.GetDefaultChatClient();

var agentWithMafOnly = baseChatClient.CreateAIAgent(
    instructions: "你是智能客服机器人,擅长查询天气和订单。回答要简洁。",
    name: "MAFOnlyBot",
    tools:
    [
        AIFunctionFactory.Create(GetWeather),
        AIFunctionFactory.Create(QueryOrderStatus)
    ])
    .AsBuilder()
    .Use(MAFFunctionMiddleware)
    .Build();

Console.WriteLine("Agent 创建完成(只有 MAF Function Middleware)");

// 测试
var thread1 = agentWithMafOnly.GetNewThread();
Console.WriteLine("\n用户:北京天气怎么样?\n");
Console.WriteLine("═══════════════════════════════════════════════\n");

var response1 = await agentWithMafOnly.RunAsync("北京天气怎么样?", thread1);

Console.WriteLine("\n═══════════════════════════════════════════════");
Console.WriteLine($"\n响应:{response1.Text}");
Console.WriteLine($"\n统计:MAF Function Middleware 触发了 {mafFunctionCounter} 次");
Console.WriteLine("\n观察:只有 MAF 层拦截工具调用");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
  • 实验 2:UseFunctionInvocation + MAF Function Middleware
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   实验 2:UseFunctionInvocation + MAF Middleware   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 重置计数器
startTime = DateTime.UtcNow;
meaiFunctionInvokerCounter = 0;
mafFunctionCounter = 0;

// 步骤 1:配置 ChatClient 层的 UseFunctionInvocation
var chatClientWithUseFunctionInvocation = baseChatClient
    .AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.AllowConcurrentInvocation = true;
        options.MaximumIterationsPerRequest = 10;
        
        // MEAI 层的 FunctionInvoker
        options.FunctionInvoker = async (context, ct) =>
        {
            var count = ++meaiFunctionInvokerCounter;
            var timestamp = GetTimestamp();
            
            Console.WriteLine($"\n{FormatTimestamp(timestamp)} [MEAI FunctionInvoker #{count}] ─── Pre-Invoke ─────");
            Console.WriteLine($" 工具名称: {context.Function.Name}");
            Console.WriteLine($" 参数: {string.Join(", ", context.Arguments.Select(kvp => $"{kvp.Key}={kvp.Value}"))}");
            Console.WriteLine($" 即将调用 context.Function.InvokeAsync()");
            Console.WriteLine($" 这会触发 MAF Function Middleware...");
            
            // 调用工具(会触发 MAF Function Middleware)
            var result = await context.Function.InvokeAsync(context.Arguments, ct);
            
            var endTimestamp = GetTimestamp();
            Console.WriteLine($"\n{FormatTimestamp(endTimestamp)} [MEAI FunctionInvoker #{count}] ─── Post-Invoke ────");
            Console.WriteLine($" 返回结果: {result}");
            Console.WriteLine($" 耗时: {endTimestamp - timestamp:F0}ms");
            
            return result;
        };
    })
    .Build();

Console.WriteLine("ChatClient 配置完成(包含 UseFunctionInvocation + FunctionInvoker)");

// 步骤 2:配置 Agent 层的 MAF Function Middleware
var agentWithBoth = chatClientWithUseFunctionInvocation.CreateAIAgent(
    instructions: "你是智能客服机器人,擅长查询天气和订单。回答要简洁。",
    name: "IntegratedBot",
    tools:
    [
        AIFunctionFactory.Create(GetWeather),
        AIFunctionFactory.Create(QueryOrderStatus)
    ])
    .AsBuilder()
    .Use(MAFFunctionMiddleware)
    .Build();

Console.WriteLine("Agent 配置完成(包含 MAF Function Middleware)");
Console.WriteLine("\n四层架构:");
Console.WriteLine("   UseFunctionInvocation(自动化框架)");
Console.WriteLine("    ↓ 检测到需要调用工具");
Console.WriteLine("   MEAI FunctionInvoker(自定义钩子)");
Console.WriteLine("    ↓ 调用 InvokeAsync");
Console.WriteLine("   MAF Function Middleware(业务控制)");
Console.WriteLine("    ↓");
Console.WriteLine("   实际工具执行");
Console.WriteLine("\n准备测试...\n");
  • 测试两个工具
// 测试:需要调用两个工具
var thread2 = agentWithBoth.GetNewThread();
var query = "北京今天天气怎么样?我的订单 #DD20250109001 什么时候到货?";
Console.WriteLine($"用户:{query}\n");
Console.WriteLine("═══════════════════════════════════════════════\n");
Console.WriteLine("开始执行,观察四层拦截的协作...\n");
Console.WriteLine("═══════════════════════════════════════════════");

var response2 = await agentWithBoth.RunAsync(query, thread2);
Console.WriteLine("\n═══════════════════════════════════════════════");
Console.WriteLine($"\n最终响应:\n{response2.Text}");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
  • 分析结果
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   四层拦截的触发次数统计   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

var summary = new
{
    MEAI_FunctionInvoker_触发次数 = meaiFunctionInvokerCounter,
    MAF_Function_Middleware_触发次数 = mafFunctionCounter,
    工具调用总次数 = mafFunctionCounter
};
summary.Display();

Console.WriteLine("\n关键观察:");
Console.WriteLine($" 1. MEAI FunctionInvoker 触发了 {meaiFunctionInvokerCounter} 次");
Console.WriteLine($"  - UseFunctionInvocation 检测到 LLM 需要调用工具");
Console.WriteLine($"  - 调用 FunctionInvoker 进行预处理");
Console.WriteLine($" 2. MAF Function Middleware 触发了 {mafFunctionCounter} 次");
Console.WriteLine($"  - 由 FunctionInvoker 调用 InvokeAsync 触发");
Console.WriteLine($"  - MAF 层拦截实际的工具执行");
Console.WriteLine($" 3. 实际工具函数执行了 {mafFunctionCounter} 次");
Console.WriteLine($"  - GetWeather: 1次");
Console.WriteLine($"  - QueryOrderStatus: 1次");

Console.WriteLine("\n完整的执行顺序:");
Console.WriteLine(" T0: LLM 返回:需要调用工具");
Console.WriteLine(" ─── UseFunctionInvocation 开始工作 ───");
Console.WriteLine(" T1: UseFunctionInvocation 检测到 FunctionCallContent");
Console.WriteLine(" T2: MEAI FunctionInvoker Pre-Invoke");
Console.WriteLine("   ↓ 调用 context.Function.InvokeAsync()");
Console.WriteLine(" T3: MAF Function Middleware Pre-Invoke");
Console.WriteLine(" T4: 实际工具执行 GetWeather");
Console.WriteLine(" T5: MAF Function Middleware Post-Invoke");
Console.WriteLine("   ↓ 返回给 MEAI");
Console.WriteLine(" T6: MEAI FunctionInvoker Post-Invoke");
Console.WriteLine(" ─── 重复 T2-T6(QueryOrderStatus)───");
Console.WriteLine(" T7: UseFunctionInvocation 将结果回传给 LLM");
Console.WriteLine(" T8: LLM 基于工具结果生成最终响应");

Console.WriteLine("\n核心结论:");
Console.WriteLine(" - MEAI FunctionInvoker 和 MAF Function Middleware 是嵌套关系");
Console.WriteLine(" - 执行顺序:UseFunctionInvocation → FunctionInvoker → InvokeAsync → MAF Middleware → 工具");
Console.WriteLine(" - 触发次数相同(meaiFunctionInvokerCounter == mafFunctionCounter)");
Console.WriteLine(" - 两者协作,互不冲突!");

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

3. 最佳实践与配置策略

  • 职责分离原则
// 推荐:清晰的职责分离
var chatClient = AIClientHelper.GetDefaultChatClient()
    .AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        // MEAI 层:只负责自动化控制
        options.AllowConcurrentInvocation = true;       // 并发调用
        options.MaximumIterationsPerRequest = 10;       // 迭代控制
        options.MaximumConsecutiveErrorsPerRequest = 3; // 错误重试
        options.IncludeDetailedErrors = false;          // 安全考虑
        
        // 不配置 FunctionInvoker,让 MAF 层统一处理
        // 或者只做简单的监控,不重复 MAF 的逻辑
    })
    .Build();

var agent = chatClient.CreateAIAgent(...)
    .AsBuilder()
    .Use(async (agent, context, next, ct) =>
    {
        // MAF 层:统一的企业级控制        
        // 1. 权限检查
        if (!HasPermission(context.Function.Name))
        {
            throw new UnauthorizedAccessException($"无权调用工具: {context.Function.Name}");
        }        
        // 2. 审计日志
        LogFunctionCall(context.Function.Name, context.Arguments);        
        // 3. 执行工具
        var result = await next(context, ct);        
        // 4. 记录结果
        LogFunctionResult(context.Function.Name, result);        
        return result;
    })
    .Build();
  • 避免重复拦截
// 避免:在两个地方做相同的事
.UseFunctionInvocation(configure: options =>
{
    options.FunctionInvoker = async (context, ct) =>
    {
        // 不要在这里做详细的日志记录
        Console.WriteLine($"MEAI: 调用 {context.Function.Name}");
        return await context.Function.InvokeAsync(context.Arguments, ct);
    };
})
// 然后在 MAF 层又做一次
.Use(async (agent, context, next, ct) =>
{
    // 重复的日志记录
    Console.WriteLine($"MAF: 调用 {context.Function.Name}");
    return await next(context, ct);
})

// 推荐:只在 MAF 层统一处理
.UseFunctionInvocation(configure: options =>
{
    // 只配置自动化参数,不配置 FunctionInvoker
    options.AllowConcurrentInvocation = true;
    options.MaximumIterationsPerRequest = 10;
})
.Use(MAFUnifiedMiddleware)  // 统一的企业级控制
  • 使用场景选择
场景 推荐配置 说明
简单 Agent 不使用 UseFunctionInvocation MAF 自动处理函数调用
需要并发调用 使用 UseFunctionInvocation 启用 AllowConcurrentInvocation
复杂迭代逻辑 使用 UseFunctionInvocation 需要多轮工具调用
需要错误重试 使用 UseFunctionInvocation 配置 MaximumConsecutiveErrorsPerRequest
企业级控制 使用 MAF Function Middleware 权限、审计、Mock
两者结合 UseFunctionInvocation + MAF 自动化 + 业务控制
  • 性能优化建议
// 推荐:在 MEAI FunctionInvoker 中只做轻量级操作
options.FunctionInvoker = async (context, ct) =>
{
    // 只做简单的监控(异步记录,不阻塞)
    _ = Task.Run(() => LogAsync(context.Function.Name));
    // 立即调用工具
    return await context.Function.InvokeAsync(context.Arguments, ct);
};

// 在 MAF 层做详细的控制(可以同步操作)
.Use(async (agent, context, next, ct) =>
{
    // 权限检查(同步)
    ValidatePermission(context.Function.Name);
    // 执行工具
    var result = await next(context, ct);
    // 审计日志(同步)
    await AuditLogAsync(context.Function.Name, result);
    return result;
})

4. 实验3:并发调用场景

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   实验 3:并发调用场景   ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 重置计数器
startTime = DateTime.UtcNow;
meaiFunctionInvokerCounter = 0;
mafFunctionCounter = 0;

// 配置支持并发的 Agent
var concurrentChatClient = baseChatClient
    .AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.AllowConcurrentInvocation = true;  // 启用并发
        options.MaximumIterationsPerRequest = 10;        
        options.FunctionInvoker = async (context, ct) =>
        {
            var count = ++meaiFunctionInvokerCounter;
            var timestamp = GetTimestamp();
            
            Console.WriteLine($"\n{FormatTimestamp(timestamp)} [MEAI Concurrent #{count}] ─── 开始并发调用 ───");
            Console.WriteLine($" 工具: {context.Function.Name}");
            
            var result = await context.Function.InvokeAsync(context.Arguments, ct);
            
            var endTimestamp = GetTimestamp();
            Console.WriteLine($"\n{FormatTimestamp(endTimestamp)} [MEAI Concurrent #{count}] ─── 并发调用完成 ───");
            Console.WriteLine($" 耗时: {endTimestamp - timestamp:F0}ms");
            
            return result;
        };
    })
    .Build();

var concurrentAgent = concurrentChatClient.CreateAIAgent(
    instructions: "你是智能客服机器人。",
    name: "ConcurrentBot",
    tools:
    [
        AIFunctionFactory.Create(GetWeather),
        AIFunctionFactory.Create(QueryOrderStatus)
    ])
    .AsBuilder()
    .Use(MAFFunctionMiddleware)
    .Build();

Console.WriteLine("Agent 配置完成(启用并发调用)\n");

// 测试:同时查询多个信息
var thread3 = concurrentAgent.GetNewThread();
var concurrentQuery = "帮我查询北京和上海的天气,同时查询订单 #DD20250109001 的状态";
Console.WriteLine($"用户:{concurrentQuery}\n");
Console.WriteLine("═══════════════════════════════════════════════\n");
Console.WriteLine("观察并发调用的执行流程...\n");
Console.WriteLine("═══════════════════════════════════════════════");

var startTimestamp = GetTimestamp();
var response3 = await concurrentAgent.RunAsync(concurrentQuery, thread3);
var endTimestamp = GetTimestamp();
Console.WriteLine("\n═══════════════════════════════════════════════");
Console.WriteLine($"\n最终响应:\n{response3.Text}");
Console.WriteLine($"\n总耗时:{endTimestamp - startTimestamp:F0}ms");
Console.WriteLine($"MEAI FunctionInvoker 触发:{meaiFunctionInvokerCounter} 次");
Console.WriteLine($"MAF Function Middleware 触发:{mafFunctionCounter} 次");
Console.WriteLine("\n观察:");
Console.WriteLine(" - 多个工具可能并发调用(取决于 LLM 的返回)");
Console.WriteLine(" - MEAI 层管理并发逻辑");
Console.WriteLine(" - MAF 层依然拦截每个工具调用");
Console.WriteLine(" - 两者协作,实现高效的并发控制");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

五、Agent vs an Funtion

1. Agent 两种复用模式

  • Agent as Function Tool(Agent 作为函数工具)

    • 使用场景:在同一个应用内实现 Agent 嵌套调用

    • 转换方法:agent.AsAIFunction()

    • 调用方式:通过 MEAI 的 AIFunctionFactory 机制

    • 优势:轻量级、高性能、类型安全

  • Agent as MCP Tool(Agent 作为 MCP 工具)

    • 使用场景:跨应用、跨语言、跨平台的 Agent 调用

    • 暴露方式:通过 MCP Server 暴露 Agent

    • 调用方式:任何支持 MCP 的客户端(Claude Desktop、VS Code 等)

    • 优势:标准化协议、跨平台互操作、易于调试

  • 对比表格

特性 AsAIFunction AsMcpToo
适用场景 应用内嵌套调用 跨应用、跨平台调用
性能 高(进程内调用) 中(进程间通信)
互操作性 .NET 限定 支持任何 MCP 客户端
调试工具 无专用工具 MCP Inspector
类型安全 编译时检查 运行时检查
复杂度 简单 中等

2. Agent as Function Tool

  • 创建 WeatherAgent(天气助手)
// 创建天气查询工具
[Description("查询指定城市的实时天气信息")]
string GetRealTimeWeather([Description("城市名称")] string city)
{
    // 模拟 API 调用
    var weatherData = new Dictionary<string, string>
    {
        ["北京"] = "晴天,5-15℃,空气质量良",
        ["上海"] = "多云,10-18℃,微风",
        ["深圳"] = "小雨,20-25℃,湿度大",
        ["成都"] = "阴天,8-16℃,有雾霾"
    };
    
    return weatherData.ContainsKey(city) 
        ? $"{city}:{weatherData[city]}" 
        : $"{city}:暂无天气数据";
}
Console.WriteLine("天气查询工具定义完成");

// 创建 WeatherAgent
var weatherChatClient = AIClientHelper.GetDefaultChatClient();
var weatherAgent = weatherChatClient.CreateAIAgent(
    instructions: "你是一个专业的天气助手,负责查询和解释天气信息。回答要简洁明了,包含温度、天气状况和建议。",
    name: "WeatherAgent",
    description: "专业的天气查询助手,提供实时天气信息和出行建议",
    tools: [
        AIFunctionFactory.Create(GetRealTimeWeather)
    ]
);

weatherAgent.Display();

var functionInvoker = weatherAgent.GetService<FunctionInvokingChatClient>();
functionInvoker.Display();

var registeredTools = functionInvoker?.AdditionalTools ?? [];
Console.WriteLine("WeatherAgent 创建完成");
new { 
    Name = weatherAgent.Name, 
    Description = weatherAgent.Description,
    ToolCount = registeredTools.Count
    }.Display();
  • 测试 WeatherAgent 独立运行
// 测试 WeatherAgent
var weatherThread = weatherAgent.GetNewThread();
var weatherResult = await weatherAgent.RunAsync("北京今天天气怎么样?",weatherThread);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试消息: 北京今天天气怎么样?");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"{weatherAgent.Name}: {weatherResult.Messages.Last().Text}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
  • 使用 AsAIFunction() 转换 Agent,关键方法:agent.AsAIFunction()
    • 作用:将整个 Agent 封装为一个 AIFunction
    • 输入:用户的文本消息
    • 输出:Agent 的响应文本
    • 优势:保留 Agent 的完整能力(工具调用、多轮对话等)
// 将 WeatherAgent 转换为 AIFunction
var weatherFunction = weatherAgent.AsAIFunction();
Console.WriteLine("WeatherAgent 转换为 AIFunction 完成");
weatherFunction.Display();
  • 创建主 Agent 并注册子 Agent
// 创建旅行助手(主 Agent)
var travelChatClient = AIClientHelper.GetDefaultChatClient();
var travelAgent = travelChatClient.CreateAIAgent(
    instructions: @"你是一个专业的旅行助手,帮助用户规划行程。
当用户提到目的地时,你应该:
1. 使用 WeatherAgent 查询目的地天气
2. 根据天气情况给出出行建议(衣物、装备、注意事项)
3. 提供其他旅行小贴士",
    name: "TravelAgent",
    tools: [
        weatherFunction  // 注册 WeatherAgent 作为工具
    ]
);
travelAgent.Display();
Console.WriteLine("TravelAgent 创建完成,已注册 WeatherAgent");
new { 
    Name = travelAgent.Name,
    RegisteredTools = registeredTools?.Select(t => t.Name).ToList()
}.Display();
  • 测试 Agent 嵌套调用
// 测试嵌套调用
var travelThread = travelAgent.GetNewThread();
var travelResult = await travelAgent.RunAsync(
    thread: travelThread,
    message: "我下午要去深圳出差,需要准备什么?"
);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("用户消息: 我下午要去深圳出差,需要准备什么?");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"{travelAgent.Name}: {travelResult.Messages.Last().Text}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
travelResult.Display();

// 显示工具调用详情
var toolCalls = travelResult.Messages
    .Where(m => m.Contents.Any(c => c is FunctionCallContent))
    .SelectMany(m => m.Contents.OfType<FunctionCallContent>())
    .ToList();

if (toolCalls.Any())
{
    Console.WriteLine("\n工具调用记录:");
    foreach (var call in toolCalls)
    {
        Console.WriteLine($" 调用: {call.Name}");
    }
}

3. Agent as MCP Tool

现在我们希望将 WeatherAgent 暴露为 MCP 工具,让任何支持 MCP 协议的客户端都可以调用它。

  • 将 Agent 转换为 MCP 工具

我们先调用 weatherAgent.AsAIFunction() 获取可复用的函数式接口,然后直接通过 McpServerTool.Create(...) 将其注册为 MCP 工具。Agent 的名称与描述会自动沿用到 MCP 工具中。但这里需要额外注意的是,其InputSchema 为:

{
    "type": "object",
    "properties": {
        "query": {
            "description": "Input query to invoke the agent.",
            "type": "string"
        }
    },
    "required": [
        "query"
    ]
}
var weatherAgentFunction = weatherAgent.AsAIFunction();
Console.WriteLine("WeatherAgent 已转换为 AIFunction");

var weatherMcpTool = McpServerTool.Create(weatherAgentFunction);
Console.WriteLine("MCP 工具创建完成");
  • 启动 InMemory MCP Server

为了演示,我们使用 InMemory Transport 创建 Server 和 Client。

var toolList = new List<McpServerTool>
{
    weatherMcpTool
};
// 创建 InMemory MCP Server 和 Client
var (mcpClient, mcpServer) = await McpHelper.CreateInMemoryClientAndServerAsync(
    tools: toolList
);
Console.WriteLine("MCP Server 启动完成");
Console.WriteLine("MCP Client 连接成功");

// 列出可用工具
var availableTools = await mcpClient.ListToolsAsync();
Console.WriteLine($"\nMCP Server 可用工具: {availableTools.Count} 个");
availableTools.ToList().ForEach(t => Console.WriteLine($"  - {t.Name}: {t.Description}"));
availableTools.Display();
  • 通过 MCP Client 调用 Agent

现在我们可以像调用普通 MCP 工具一样调用 WeatherAgent。

// 调用 MCP 工具
var toolResult = await mcpClient.CallToolAsync( 
  toolName: "WeatherAgent",  
  arguments: new Dictionary<string, Object>()
    {
        { "query", "成都天气怎样?" }
    }
);

toolResult.Display();
  • 集成到主 Agent(MCP 方式)

现在我们创建一个新的主 Agent,通过 MCP 协议调用 WeatherAgent。

// 创建主 Agent 并集成 MCP 工具
var mcpTravelChatClient = AIClientHelper.GetDefaultChatClient();

// 将 MCP 工具转换为 AIFunction
var mcpToolFunctions = availableTools.Cast<AIFunction>();
mcpToolFunctions.Display();

var mcpTravelAgent = mcpTravelChatClient.CreateAIAgent(
    instructions: @"你是一个专业的旅行助手。
使用WeatherAgent工具查询天气,并根据天气给出出行建议。",
    name: "TravelAgent_MCP",
    tools: [..mcpToolFunctions]
);
Console.WriteLine("TravelAgent_MCP 创建完成");
mcpTravelAgent.Display();
  • 测试通过 MCP 调用
// 测试通过 MCP 调用
var mcpTravelThread = mcpTravelAgent.GetNewThread();

var mcpTravelResult = await mcpTravelAgent.RunAsync(
    thread: mcpTravelThread,
    message: "我计划去上海旅游,天气如何?"
);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("用户消息: 我计划去上海旅游,天气如何?");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"{mcpTravelAgent.Name}: {mcpTravelResult.Messages.Last().Text}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

4. MCP Inspector 调试

MCP Inspector 是 MCP 官方提供的调试工具,可以:

  • 可视化查看 MCP Server 暴露的工具
  • 手动测试工具调用
  • 查看工具的输入参数和返回值
  • 调试 MCP 协议通信

如何使用

  • 方式1:使用 npx 启动
npx @modelcontextprotocol/inspector <your-mcp-server-command>
  • 方式2:使用 stdio 连接
npx @modelcontextprotocol/inspector dotnet run --project YourMcpServer.csproj

生产环境建议,在生产环境中部署 MCP Server 时:

  • 将 Server 打包为独立可执行文件
  • 配置为 stdio transport
  • 在 Claude Desktop 或其他客户端中注册
  • 使用 MCP Inspector 进行预先测试

配置示例(Claude Desktop)

{
  "mcpServers": {
    "weather-agent": {
      "command": "dotnet",
      "args": ["run", "--project", "/path/to/WeatherAgent.Server"]
    }
  }
}

5. 企业级实战:多 Agent 协作系统

场景描述:构建一个智能客服系统,由多个专项 Agent 协作完成:

  • 主客服 Agent:负责对话流程控制
  • 天气 Agent:提供天气信息
  • 订单 Agent:查询订单状态
  • 优惠券 Agent:查询可用优惠
// 1. 订单 Agent
[Description("查询订单状态和物流信息")]
string CheckOrderStatus([Description("订单号")] string orderId)
{
    var orders = new Dictionary<string, string>
    {
        ["ORD001"] = "已发货,预计明天送达",
        ["ORD002"] = "配送中,今天下午送达",
        ["ORD003"] = "已签收"
    };
    return orders.ContainsKey(orderId) ? orders[orderId] : "订单不存在";
}
var orderChatClient = AIClientHelper.GetDefaultChatClient();
var orderAgent = orderChatClient.CreateAIAgent(
    instructions: "你是订单查询助手,提供订单状态和物流信息。",
    name: "OrderAgent",
    tools: [AIFunctionFactory.Create(CheckOrderStatus)]
);
Console.WriteLine("OrderAgent 创建完成");

// 2. 优惠券 Agent (MCP 方式)
public class CouponAgentMcpTools
{
    [McpServerTool]
    [Description("查询用户的优惠券")]
    public static string QueryCoupons(
        [Description("用户ID")] string userId
    )
    {
        return $"用户 {userId} 有 3 张可用优惠券:满100减20、满200减50、9折券";
    }
}
var couponTools = McpHelper.GetToolsForType<CouponAgentMcpTools>();
var (couponMcpClient, couponMcpServer) = await McpHelper.CreateInMemoryClientAndServerAsync(couponTools);
var mcpTools = await couponMcpClient.ListToolsAsync();
var couponMcpFunctions = mcpTools.Cast<AIFunction>();
Console.WriteLine("CouponAgent (MCP) 创建完成");

// 3. 创建主客服 Agent
var customerServiceChatClient = AIClientHelper.GetDefaultChatClient();
var mainAgent = customerServiceChatClient.CreateAIAgent(
    instructions: @"你是智能客服助手,负责:
1. 回答客户关于天气的咨询(使用 WeatherAgent)
2. 查询订单状态(使用 OrderAgent)
3. 查询优惠券信息(使用 query_coupons)
保持友好、专业的服务态度。",
    name: "CustomerServiceAgent",
    tools: [
        weatherAgent.AsAIFunction(),  // Agent as Function
        orderAgent.AsAIFunction(),    // Agent as Function
        ..couponMcpFunctions          // MCP Tool
    ]
);
var functionChatClient = mainAgent.GetService<FunctionInvokingChatClient>();
var tools = functionChatClient.AdditionalTools;
Console.WriteLine("CustomerServiceAgent 创建完成");
new { 
    Name = mainAgent.Name,
    ToolCount = tools.Count(),
    RegisteredTools = tools.Select(t => t.Name).ToList()
}.Display();

// 4. 测试多 Agent 协作
var csThread = mainAgent.GetNewThread();
var queries = new[]
{
    "北京明天天气怎么样?",
    "查询订单 ORD002 的状态",
    "我的用户ID是 USER123,有什么优惠券吗?"
};
foreach (var query in queries)
{
    Console.WriteLine($"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    Console.WriteLine($"客户: {query}");
    Console.WriteLine($"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    
    var result = await mainAgent.RunAsync(query, csThread);
    Console.WriteLine($"💬 客服: {result.Messages.Last().Text}");
}
Console.WriteLine($"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("多 Agent 协作演示完成!");
Console.WriteLine($"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

6. 两种模式的选择指南

何时使用 AsAIFunction

  • 推荐场景:

    • 应用内的 Agent 组合和嵌套

    • 需要高性能的进程内调用

    • 子 Agent 仅供当前应用使用

    • 需要类型安全和编译时检查

  • 不适合场景:

    • 需要跨应用或跨语言调用

    • Agent 需要独立部署和扩展

    • 需要使用 MCP Inspector 调试

何时使用 AsMcpTool

  • 推荐场景:

    • Agent 需要被多个客户端调用(Claude、VS Code 等)

    • 跨应用、跨语言、跨平台集成

    • Agent 需要独立部署和版本管理

    • 构建标准化的 Agent 生态系统

  • 不适合场景:

    • 对性能要求极高的场景

    • 仅在单一应用内使用

    • 不需要跨进程通信

混合使用策略

在实际项目中,通常会混合使用两种模式:

主 Agent
├─ 内部专项 Agent (AsAIFunction)
│  ├─ 订单处理 Agent
│  └─ 库存查询 Agent
└─ 外部服务 Agent (MCP)
   ├─ 天气服务
   ├─ 物流追踪
   └─ 支付网关

最佳实践

  • Agent 设计原则

    • 单一职责:每个 Agent 负责一个明确的领域

    • 接口清晰:定义明确的输入输出格式

    • 无状态设计:避免依赖外部状态(对 MCP 尤其重要)

    • 错误处理:提供友好的错误消息

  • 性能优化

    • 缓存复用:缓存 Agent 实例,避免重复创建

    • 并行调用:多个子 Agent 调用时使用并行模式

    • 超时控制:设置合理的超时时间

  • 安全考虑

    • 输入验证:验证所有外部输入

    • 权限控制:MCP 工具应实施权限检查

    • 敏感信息:避免在日志中暴露敏感数据

  • 可维护性

    • 文档完善:为每个 Agent 和工具提供清晰的文档

    • 版本管理:为 MCP 工具定义版本号

    • 监控日志:记录 Agent 调用链路和性能指标

  • 测试策略

    • 单元测试:独立测试每个 Agent 的功能

    • 集成测试:测试 Agent 之间的协作

    • MCP 测试:使用 MCP Inspector 进行人工测试

六、自定义上下文提供者

1. 什么是 AIContextProvider

AIContextProvider 是 MAF 提供的抽象基类,用于在 Agent 调用 AI 模型前后注入自定义逻辑。

public abstract class AIContextProvider
{
    // 调用前:注入上下文信息(Instructions、Messages)
    public virtual ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
    {
        return new ValueTask<AIContext>(new AIContext());
    }
    
    // 调用后:提取信息,更新内部状态
    public virtual ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
    {
        return ValueTask.CompletedTask;
    }
    
    // 序列化:保存状态,支持恢复
    public virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
    {
        return default;
    }
}
方法 执行时机 主要用途 返回值 示例场景
InvokingAsync 调用模型 注入 Instructions、Messages AIContext 告诉 AI "用户名是张三"
InvokedAsync 调用模型 提取信息、更新状态 ValueTask 从响应中提取用户年龄
Serialize Thread 序列化时 保存记忆状态 JsonElement 保存用户信息到 JSON

典型应用场景:

  • 场景1:用户信息记忆(下节课重点)

    • InvokingAsync:注入已知的用户姓名、年龄

    • InvokedAsync:从对话中提取新的用户信息

    • Serialize:保存用户信息,支持恢复

  • 场景2:知识库检索(RAG)

    • InvokingAsync:根据问题检索知识库,注入相关文档

    • InvokedAsync:记录检索命中情况

    • Serialize:保存检索历史

  • 场景3:统计分析

    • InvokingAsync:注入用户行为统计

    • InvokedAsync:更新统计数据(提问次数、工具调用次数)

    • Serialize:保存统计信息

  • 场景4:个性化配置

    • InvokingAsync:根据用户偏好调整 Instructions(VIP用户、语言风格)

    • InvokedAsync:学习用户偏好

    • Serialize:保存偏好设置

2. AIContextProvider 生命周期

sequenceDiagram
    participant App as 应用代码
    participant Agent as AIAgent
    participant Provider as AIContextProvider
    participant Model as AI 模型

    App->>Agent: RunAsync("你好", thread)
    
    Note over Agent: 准备请求
    Agent->>Provider: InvokingAsync(context)
    Note over Provider: 根据记忆生成上下文<br/>例如:"用户名是张三"
    Provider-->>Agent: 返回 AIContext<br/>(Instructions + Messages)
    
    Note over Agent: 合并上下文
    Agent->>Model: 发送请求<br/>(原始消息 + 注入的上下文)
    Model-->>Agent: 返回响应<br/>"张三您好!"
    
    Note over Agent: 处理响应
    Agent->>Provider: InvokedAsync(context)
    Note over Provider: 从对话中提取信息<br/>更新内部状态
    Provider-->>Agent: 完成
    
    Agent-->>App: 返回最终响应
    
    App->>Agent: thread.Serialize()
    Agent->>Provider: Serialize()
    Note over Provider: 序列化记忆状态
    Provider-->>Agent: 返回 JsonElement
    Agent-->>App: 返回完整的 Thread 状态
  • InvokingAsync - 调用前准备
    • 执行时机:每次 RunAsync/RunStreamingAsync 调用前
    • 作用:为 AI 模型提供额外的上下文信息
    • 典型操作:
      • 注入用户信息到 Instructions
      • 添加知识库检索结果到 Messages
      • 动态调整系统提示
  • InvokedAsync - 调用后处理
    • 执行时机:AI 模型返回响应后
    • 作用:从对话中提取信息,更新内部状态
    • 典型操作:
      • 提取用户提供的姓名、年龄
      • 更新统计数据
      • 记录工具调用情况
  • Serialize - 状态序列化
    • 执行时机:调用 thread.Serialize() 时
    • 作用:将内部状态转换为可持久化的 JSON
    • 典型操作:
      • 序列化用户信息
      • 保存统计数据
      • 排除不可序列化的服务依赖(如 IChatClient)

重要注意事项:

  • 线程隔离:每个 Thread 有独立的 AIContextProvider 实例
  • 异步执行:虽然返回 ValueTask,但可以同步完成(无 I/O 时)
  • 异常处理:InvokedAsync 中的异常不应影响主流程,需要捕获
  • 性能考虑:InvokingAsync 每次调用都执行,避免耗时操作
  • 序列化限制:序列化数据状态,不序列化服务依赖(如 IChatClient)

3. 核心上下文对象

  • AIContext - 返回给 Agent 的上下文:这是 InvokingAsync 方法返回的对象,用于向 AI 模型注入额外信息。
public class AIContext
{
    // 额外的系统提示(会追加到 Agent 的 Instructions)
    public string? Instructions { get; set; }
    
    // 额外的消息(会添加到对话历史中)
    public IList<ChatMessage>? Messages { get; set; }
}

// 使用示例
return new ValueTask<AIContext>(new AIContext
{
    // 注入用户信息
    Instructions = "The user's name is Alice. The user's age is 25.",
    
    // 注入知识库检索结果
    Messages = new List<ChatMessage>
    {
        new ChatMessage(ChatRole.System, "Relevant documents: ...")
    }
});
  • InvokingContext - 调用前的上下文信息:传递给 InvokingAsync 方法,包含当前调用的相关信息。
public class InvokingContext
{
    // 当前 Thread 的唯一标识
    public string ThreadId { get; }
    
    // 当前请求的消息(用户输入)
    public IReadOnlyList<ChatMessage> Messages { get; }
    
    // Agent 的配置选项
    public ChatClientAgentOptions Options { get; }
    
    // 其他上下文信息...
}

// 典型用法
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
    // 可以根据 ThreadId 加载不同的记忆
    var threadId = context.ThreadId;
    
    // 可以查看用户的输入
    var userMessage = context.Messages.LastOrDefault(m => m.Role == ChatRole.User);
    
    // ...
}
  • InvokedContext - 调用后的上下文信息:传递给 InvokedAsync 方法,包含请求和响应的完整信息。
public class InvokedContext
{
    // 发送给 AI 模型的完整消息(包括注入的上下文)
    public IReadOnlyList<ChatMessage> RequestMessages { get; }
    
    // AI 模型返回的响应消息
    public IReadOnlyList<ChatMessage> ResponseMessages { get; }
    
    // 其他上下文信息...
}

// 典型用法
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
    // 检查是否有用户消息
    if (context.RequestMessages.Any(x => x.Role == ChatRole.User))
    {
        // 从对话中提取用户信息
        var result = await ExtractUserInfo(context.RequestMessages);
        // 更新内部状态
        UpdateMemory(result);
    }
}

三者关系总结

对象 方向 包含内容 用途
InvokingContext 输入 当前请求信息 根据输入决定注入什么上下文
AIContext 输出 要注入的上下文 告诉 Agent 要添加什么信息
InvokedContext 输入 请求+响应完整信息 从交互中学习,更新状态

4. 序列化与恢复机制

  • 序列化流程
sequenceDiagram
    participant App as 应用代码
    participant Thread as AgentThread
    participant Memory as AIContextProvider
    participant Storage as 存储

    rect rgb(200, 220, 240)
        Note over App,Storage: 序列化流程
        App->>Thread: thread.Serialize()
        Thread->>Memory: Serialize()
        Memory-->>Thread: JsonElement<br/>{"UserName":"张三","UserAge":25}
        Thread-->>App: 完整的 JSON 状态<br/>(包括对话历史+记忆)
        App->>Storage: 保存到数据库/文件
    end

    rect rgb(240, 220, 200)
        Note over App,Storage: 反序列化流程
        Storage->>App: 读取 JSON 状态
        App->>Thread: agent.DeserializeThread(json)
        Thread->>Memory: new AIContextProvider(<br/>chatClient, serializedState)
        Memory-->>Thread: 恢复的实例
        Thread-->>App: 恢复的 AgentThread
    end
  • 关键注意事项
方面 说明
只序列化数据状态 序列化 UserInfo,不序列化 _chatClient 等服务依赖
服务依赖重新注入 反序列化时,通过构造函数参数重新注入 IChatClient
两个构造函数 首次创建用无参构造,恢复时用带 JsonElement 的构造
Factory 模式 通过 AIContextProviderFactory 自动调用正确的构造函数
  • 完整的类结构
public sealed class UserInfoMemory : AIContextProvider
{
    private readonly IChatClient _chatClient;  // 不序列化(服务依赖)
    public UserInfo UserInfo { get; set; }     // 序列化(数据状态)
    
    // 构造函数1:用于首次创建
    public UserInfoMemory(IChatClient chatClient, UserInfo? userInfo = null)
    {
        _chatClient = chatClient;
        UserInfo = userInfo ?? new UserInfo();
    }
    
    // 构造函数2:用于反序列化恢复
    public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
    {
        _chatClient = chatClient;
        UserInfo = serializedState.Deserialize<UserInfo>(jsonSerializerOptions)!;
    }
    
    // 序列化:只保存数据状态
    public override JsonElement Serialize(JsonSerializerOptions? options = null)
    {
        return JsonSerializer.SerializeToElement(UserInfo, options);
    }
}

5. 最佳实践

推荐做法

  • 分离数据状态和服务依赖
public sealed class UserInfoMemory : AIContextProvider
{
    // 正确:服务依赖(不序列化)
    private readonly IChatClient _chatClient;

    // 正确:数据状态(序列化)
    public UserInfo UserInfo { get; set; }

    public override JsonElement Serialize(...)
    {
        // 只序列化数据状态
        return JsonSerializer.SerializeToElement(UserInfo);
    }
}
  • 实现两个构造函数
// 构造函数1:首次创建
public UserInfoMemory(IChatClient chatClient, UserInfo? userInfo = null)
{
    _chatClient = chatClient;
    UserInfo = userInfo ?? new UserInfo();
}

// 构造函数2:反序列化恢复
public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? options = null)
{
    _chatClient = chatClient;
    UserInfo = serializedState.Deserialize<UserInfo>(options)!;
}
  • 使用 ??= 运算符更新记忆
// 正确:仅更新未知信息
UserInfo.UserName ??= extractedInfo.UserName;
UserInfo.UserAge ??= extractedInfo.UserAge;

// 错误:会覆盖已知信息
UserInfo.UserName = extractedInfo.UserName;
UserInfo.UserAge = extractedInfo.UserAge;
  • 异常处理不影响主流程
public override async ValueTask InvokedAsync(...)
{
    try
    {
        // 提取信息
        var result = await _chatClient.GetResponseAsync<UserInfo>(...);
        UserInfo.UserName ??= result.UserName;
    }
    catch (Exception ex)
    {
        // 捕获异常,记录日志,不抛出
        Console.WriteLine($"提取失败: {ex.Message}");
    }
}
  • 按用户维度存储记忆
// 正确:按用户 ID 存储
await _db.SetAsync($"user:{userId}:memory", json);
// 错误:按 Thread ID 存储(无法跨 Thread 共享)
await _db.SetAsync($"thread:{threadId}:memory", json);

DON'T - 避免的错误

  • 序列化服务依赖
// 错误:尝试序列化 IChatClient
public override JsonElement Serialize(...)
{
    return JsonSerializer.SerializeToElement(new 
    { 
        UserInfo = UserInfo,
        ChatClient = _chatClient  // 无法序列化
    });
}
  • 忘记实现反序列化构造函数
// 错误:只有一个构造函数
public UserInfoMemory(IChatClient chatClient)
{
    _chatClient = chatClient;
    UserInfo = new UserInfo();
}

// 结果:反序列化时状态丢失
  • InvokingAsync 中执行耗时操作
// 错误:每次调用都查询数据库
public override async ValueTask<AIContext> InvokingAsync(...)
{
    // 避免:InvokingAsync 每次调用都执行
    var userInfo = await _db.GetAsync($"user:{userId}");
    return new AIContext { Instructions = ... };
}

// 正确:在构造函数中加载一次
public UserInfoMemory(IChatClient chatClient, string userId, IDatabase db)
{
    _chatClient = chatClient;
    UserInfo = db.Get($"user:{userId}");  // 只加载一次
}
  • 直接修改 Instructions 属性
// 错误:直接修改 Agent.Instructions(全局影响)
public override ValueTask<AIContext> InvokingAsync(...)
{
    agent.Instructions += $"User name is {UserInfo.UserName}";  // 不要这样做
    return new ValueTask<AIContext>(new AIContext());
}

// 正确:通过 AIContext.Instructions 注入(Thread 级别)
public override ValueTask<AIContext> InvokingAsync(...)
{
    return new ValueTask<AIContext>(new AIContext
    {
        Instructions = $"User name is {UserInfo.UserName}"  // 正确
    });
}
  • 忘记检查 null
// 错误:未检查 GetService 返回值
var memory = thread.GetService<UserInfoMemory>();
memory.UserInfo = userInfo;  // 可能 NullReferenceException

// 正确:检查 null
var memory = thread.GetService<UserInfoMemory>();
if (memory != null)
{
    memory.UserInfo = userInfo;  // 安全
}

七、实践:自定义用户信息上下文提供者

1. 场景说明

我们将创建一个智能助手 Agent,具备以下能力:

  • 自动提取用户信息:从对话中提取姓名和年龄
  • 智能询问:未知信息时主动询问,已知时直接使用
  • 状态持久化:支持序列化和反序列化

2. 实现

  • 步骤1:定义用户信息模型
// 定义用户信息模型
public class UserInfo
{
    // 用户姓名(可空)
    public string? UserName { get; set; }
    
    // 用户年龄(可空)
    public int? UserAge { get; set; }
}

Console.WriteLine("UserInfo 模型定义完成");
Console.WriteLine();

// 显示模型结构
new { 
    类名 = nameof(UserInfo),
    属性 = new[] { "UserName (string?)", "UserAge (int?)" },
    特点 = "可空类型,支持部分信息缺失"
}.Display();
  • 步骤2:实现 InvokingAsync - 注入上下文,这是核心方法之一,在每次调用 AI 模型前执行,用于注入用户信息到上下文中。

    实现逻辑:

    • 如果已知用户姓名 → 告诉 AI "用户名是 XXX"

    • 如果未知用户姓名 → 告诉 AI "请询问用户姓名"

    • 年龄同理

  • 步骤3:实现 InvokedAsync - 提取信息,这是第二个核心方法,在每次 AI 模型返回响应后执行,用于从对话中提取用户信息。

    实现逻辑:

    • 检查是否有用户消息

    • 如果信息不完整,使用结构化输出提取用户信息

    • 仅更新未知的信息(已知的不覆盖)

// 完整的 UserInfoMemory 实现
public sealed class UserInfoMemory : AIContextProvider
{
    private readonly IChatClient _chatClient;
    public UserInfo UserInfo { get; set; }

    // 构造函数1:用于首次创建(无记忆状态)
    public UserInfoMemory(IChatClient chatClient, UserInfo? userInfo = null)
    {
        _chatClient = chatClient;
        UserInfo = userInfo ?? new UserInfo();
    }

    // 构造函数2:用于反序列化恢复(有记忆状态)
    public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
    {
        _chatClient = chatClient;
        UserInfo = serializedState.ValueKind == JsonValueKind.Object ?
            serializedState.Deserialize<UserInfo>(jsonSerializerOptions)! :
            new UserInfo();
    }

    /// <summary>
    /// 在调用 AI 模型前执行,注入用户信息到上下文
    /// </summary>
    public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
    {
        StringBuilder instructions = new();
        
        // 根据记忆状态动态生成指令
        instructions.AppendLine(
            UserInfo.UserName is null ?
                "Ask the user for their name and politely decline to answer any questions until they provide it." :
                $"The user's name is {UserInfo.UserName}.");
        
        instructions.AppendLine(
            UserInfo.UserAge is null ?
                "Ask the user for their age and politely decline to answer any questions until they provide it." :
                $"The user's age is {UserInfo.UserAge}.");

        return new ValueTask<AIContext>(new AIContext
        {
            Instructions = instructions.ToString()
        });
    }

    /// <summary>
    /// 在 AI 模型返回响应后执行,从对话中提取用户信息
    /// </summary>
    public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
    {
        // 仅在以下条件下提取信息:
        // 1. 用户信息不完整(姓名或年龄未知)
        // 2. 有用户消息(避免在系统消息上执行)
        if ((UserInfo.UserName is null || UserInfo.UserAge is null) && 
            context.RequestMessages.Any(x => x.Role == ChatRole.User))
        {
            try
            {
                // 使用结构化输出从对话中提取用户信息
                var result = await _chatClient.GetResponseAsync<UserInfo>(
                    context.RequestMessages,
                    new ChatOptions()
                    {
                        Instructions = "Extract the user's name and age from the message if present. If not present return nulls."
                    },
                    cancellationToken: cancellationToken);

                // 使用 ??= 运算符:仅在当前为 null 时才更新
                UserInfo.UserName ??= result.Result.UserName;
                UserInfo.UserAge ??= result.Result.UserAge;
            }
            catch (Exception ex)
            {
                // 提取失败不应影响主流程,记录日志即可
                Console.WriteLine($"提取用户信息失败: {ex.Message}");
            }
        }
    }

    /// <summary>
    /// 序列化记忆状态,用于持久化
    /// </summary>
    public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
    {
        // 只序列化 UserInfo 数据,不序列化 _chatClient 服务依赖
        return JsonSerializer.SerializeToElement(UserInfo, jsonSerializerOptions);
    }
}

Console.WriteLine("UserInfoMemory 完整实现完成!");
Console.WriteLine();
new { 
    类名 = nameof(UserInfoMemory),
    继承 = nameof(AIContextProvider),
    核心方法 = new[] 
    { 
        "InvokingAsync - 注入上下文", 
        "InvokedAsync - 提取信息", 
        "Serialize - 序列化状态" 
    },
    功能 = "自动记住用户姓名和年龄",
    特性 = new[] 
    { 
        "智能询问缺失信息", 
        "结构化提取用户信息", 
        "支持序列化和恢复" 
    }
}.Display();

3. 测试:基本功能验证

  • 步骤4:使用 AIContextProviderFactory 配置一个带记忆的 Agent
var options = new ChatClientAgentOptions
{
    Instructions = "You are a friendly assistant. Always address the user by their name.",
    AIContextProviderFactory = ctx => new UserInfoMemory(chatClient, ctx.SerializedState, ctx.JsonSerializerOptions)
};

// 创建 Agent(配置记忆组件)
var agent = chatClient.CreateAIAgent(options);

Console.WriteLine("Agent 创建成功");
Console.WriteLine();

new { 
    Agent名称 = agent.Name,
    Instructions = agent.Instructions,
    记忆组件 = nameof(UserInfoMemory),
    初始状态 = "无用户信息(null, null)"
}.Display();
Console.WriteLine();
Console.WriteLine("关键配置说明:");
new { 
    配置点 = "AIContextProviderFactory",
    作用 = "为每个 Thread 创建独立的 UserInfoMemory 实例",
    隔离性 = "不同 Thread 的记忆互不影响"
}.Display();
  • 步骤5:进行 4 轮对话,观察 Agent 的主动询问行为和记忆提取过程。
// 创建新的对话线程
var thread = agent.GetNewThread();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("开始 4 轮对话测试");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();

// 第 1 轮:初次问候(无任何信息)
Console.WriteLine("第 1 轮:初次问候");
Console.WriteLine("用户:你好");
var response1 = await agent.RunAsync("你好", thread);
Console.WriteLine($"Agent:{response1}");
Console.WriteLine();

// 第 2 轮:提供姓名
Console.WriteLine("第 2 轮:提供姓名");
var userNameInput = await PolyglotHelper.ReadConsoleInputAsync<string>(response1.Text);
var response2 = await agent.RunAsync(userNameInput, thread);
Console.WriteLine($"Agent:{response2}");
Console.WriteLine();

// 第 3 轮:提供年龄
Console.WriteLine("第 3 轮:提供年龄");
var userAgeInput = await PolyglotHelper.ReadConsoleInputAsync<string>(response2.Text);
var response3 = await agent.RunAsync(userAgeInput, thread);
Console.WriteLine($"Agent:{response3}");
Console.WriteLine();

// 第 4 轮:提问(验证记忆生效)
Console.WriteLine("第 4 轮:提问(验证记忆)");
var response4 = await agent.RunAsync("天气怎么样?", thread);
Console.WriteLine($"Agent:{response4}");
Console.WriteLine();

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("4 轮对话完成");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
Console.WriteLine("观察要点:");
new { 
    第1轮 = "Agent 会要求提供姓名和年龄",
    第2轮 = "Agent 确认姓名,继续要求年龄",
    第3轮 = "Agent 确认年龄,信息收集完成",
    第4轮 = "Agent 知道用户是谁(记忆生效)"
}.Display();
  • 步骤6:通过 GetService() 获取记忆组件实例,查看提取的用户信息。
// 获取 Thread 中的 UserInfoMemory 实例
var memory = thread.GetService<UserInfoMemory>();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("记忆组件状态");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();

if (memory != null)
{
    Console.WriteLine("记忆组件获取成功");
    Console.WriteLine();    
    new { 
        类型 = memory.GetType().Name,
        用户姓名 = memory.UserInfo.UserName ?? "(未知)",
        用户年龄 = memory.UserInfo.UserAge?.ToString() ?? "(未知)",
        信息完整度 = memory.UserInfo.UserName != null && memory.UserInfo.UserAge != null ? "完整" : "不完整"
    }.Display();
    
    Console.WriteLine();
    Console.WriteLine("提取效果:");
    if (memory.UserInfo.UserName != null && memory.UserInfo.UserAge != null)
    {
        Console.WriteLine($"成功从对话中提取用户信息:姓名={memory.UserInfo.UserName}, 年龄={memory.UserInfo.UserAge}");
    }
    else
    {
        Console.WriteLine("信息未完整提取,可能需要更多轮对话");
    }
}
else
{
    Console.WriteLine("记忆组件未找到");
}
Console.WriteLine();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("验证方法:");
new { 
    获取方式 = "thread.GetService<UserInfoMemory>()",
    返回值 = "UserInfoMemory 实例或 null",
    用途 = "查看、修改记忆组件内部状态"
}.Display();

4. 序列化与恢复机制

  • 步骤7:测试序列化和恢复机制
// 序列化当前 Thread(包括对话历史和记忆)
var serializedThread = thread.Serialize();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("序列化 Thread 状态");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();

// 格式化输出 JSON
var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
var jsonString = JsonSerializer.Serialize(serializedThread, jsonOptions);
Console.WriteLine("序列化结果(JSON):");
Console.WriteLine(jsonString);
Console.WriteLine();

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("JSON 结构分析");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
new { 
    包含内容 = new[] { "对话历史 (Messages)", "记忆状态 (AIContextProviders)" },
    记忆组件位置 = "AIContextProviders -> UserInfoMemory",
    记忆数据 = "UserName + UserAge",
    用途 = "保存到数据库/文件,支持恢复对话"
}.Display();

// 从 JSON 恢复 Thread
var deserializedThread = agent.DeserializeThread(serializedThread);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("反序列化恢复 Thread");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();

// 验证记忆是否恢复
var restoredMemory = deserializedThread.GetService<UserInfoMemory>();
if (restoredMemory != null)
{
    Console.WriteLine("记忆组件恢复成功");
    Console.WriteLine();    
    new { 
        用户姓名 = restoredMemory.UserInfo.UserName ?? "(未知)",
        用户年龄 = restoredMemory.UserInfo.UserAge?.ToString() ?? "(未知)",
        恢复状态 = "完整"
    }.Display();    
    Console.WriteLine();
    Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    Console.WriteLine("验证恢复效果:用恢复的 Thread 继续对话");
    Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    Console.WriteLine();
    
    Console.WriteLine("用户:你知道我的名字吗?");
    var testResponse = await agent.RunAsync("你知道我的名字吗?", deserializedThread);
    Console.WriteLine($"Agent:{testResponse}");
    Console.WriteLine();
    
    Console.WriteLine("预期结果:Agent 应该知道用户名是 '张三'(从恢复的记忆中获取)");
}
else
{
    Console.WriteLine("记忆组件恢复失败");
}
Console.WriteLine();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("反序列化测试完成");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

5. 跨 Thread 共享记忆

  • 步骤8:测试跨 Thread 共享记忆
// 从 JSON 恢复 Thread
var deserializedThread = agent.DeserializeThread(serializedThread);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("反序列化恢复 Thread");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();

// 验证记忆是否恢复
var restoredMemory = deserializedThread.GetService<UserInfoMemory>();
if (restoredMemory != null)
{
    Console.WriteLine("记忆组件恢复成功");
    Console.WriteLine();    
    new { 
        用户姓名 = restoredMemory.UserInfo.UserName ?? "(未知)",
        用户年龄 = restoredMemory.UserInfo.UserAge?.ToString() ?? "(未知)",
        恢复状态 = "完整"
    }.Display();    
    Console.WriteLine();
    Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    Console.WriteLine("验证恢复效果:用恢复的 Thread 继续对话");
    Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    Console.WriteLine();
    
    Console.WriteLine("用户:你知道我的名字吗?");
    var testResponse = await agent.RunAsync("你知道我的名字吗?", deserializedThread);
    Console.WriteLine($"Agent:{testResponse}");
    Console.WriteLine();    
    Console.WriteLine("预期结果:Agent 应该知道用户名是 '张三'(从恢复的记忆中获取)");
}
else
{
    Console.WriteLine("记忆组件恢复失败");
}
Console.WriteLine();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("反序列化测试完成");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

八、实践:智能客服的个性化配置

1. 业务需求

一个电商平台的智能客服系统,需要根据用户特征提供差异化服务

  • VIP 用户

    • 使用尊称("尊敬的张先生/女士")

    • 优先处理请求

    • 推荐高端产品

    • 正式专业的语言风格

  • 普通用户

    • 使用友好称呼("您好")

    • 标准服务流程

    • 常规产品推荐

    • 随和自然的语言风格

  • 新用户

    • 欢迎引导

    • 详细操作说明

    • 热门产品推荐

    • 耐心细致的语言风格

个性化配置维度

配置维度 数据来源 影响内容
会员等级 用户数据库 称呼、服务优先级
语言风格 历史对话分析 正式/随和/简洁
响应长度 用户偏好设置 简洁/标准/详细
专业度 用户画像 术语使用程度
购买历史 订单系统 产品推荐策略

2. 实现

  • 步骤1:定义用户画像模型
/// <summary>
/// 会员等级
/// </summary>
public enum MemberLevel
{
    /// <summary>新用户</summary>
    New = 0,
    /// <summary>普通会员</summary>
    Regular = 1,
    /// <summary>VIP会员</summary>
    VIP = 2
}

/// <summary>
/// 语言风格
/// </summary>
public enum CommunicationStyle
{
    /// <summary>正式风格</summary>
    Formal = 0,
    /// <summary>标准风格</summary>
    Standard = 1,
    /// <summary>随和风格</summary>
    Casual = 2,
    /// <summary>简洁风格</summary>
    Concise = 3
}

/// <summary>
/// 响应详细程度
/// </summary>
public enum DetailLevel
{
    /// <summary>简洁</summary>
    Brief = 0,
    /// <summary>标准</summary>
    Standard = 1,
    /// <summary>详细</summary>
    Detailed = 2
}

/// <summary>
/// 用户画像 - 存储用户的个性化配置信息
/// </summary>
public class UserProfile
{
    /// <summary>用户姓名</summary>
    public string? Name { get; set; }
    
    /// <summary>会员等级</summary>
    public MemberLevel MemberLevel { get; set; } = MemberLevel.Regular;
    
    /// <summary>语言风格偏好</summary>
    public CommunicationStyle CommunicationStyle { get; set; } = CommunicationStyle.Standard;
    
    /// <summary>响应详细程度偏好</summary>
    public DetailLevel ResponseDetailLevel { get; set; } = DetailLevel.Standard;
    
    /// <summary>是否是技术型用户(影响术语使用)</summary>
    public bool IsTechnicalUser { get; set; } = false;
    
    /// <summary>购买历史(产品类别)</summary>
    public List<string> PurchaseHistory { get; set; } = new();
    
    /// <summary>交互次数统计</summary>
    public int InteractionCount { get; set; } = 0;
    
    /// <summary>最后交互时间</summary>
    public DateTime LastInteractionTime { get; set; } = DateTime.Now;
    
    /// <summary>用户消息平均长度(用于判断简洁偏好)</summary>
    public double AverageMessageLength { get; set; } = 0;
}
  • 步骤2:实现基础的 PersonalizationProvider
/// <summary>
/// 个性化配置提供者 - 根据用户画像提供个性化服务
/// </summary>
public sealed class PersonalizationProvider : AIContextProvider
{
    // 不序列化:服务依赖
    private readonly IChatClient _chatClient;
    
    // 序列化:数据状态
    public UserProfile UserProfile { get; set; }
    
    // 构造函数1:用于首次创建
    public PersonalizationProvider(IChatClient chatClient, UserProfile? userProfile = null)
    {
        _chatClient = chatClient;
        UserProfile = userProfile ?? new UserProfile();
        Console.WriteLine($"PersonalizationProvider 创建完成 (会员等级:{UserProfile.MemberLevel})");
    }
    
    // 构造函数2:用于反序列化恢复
    public PersonalizationProvider(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
    {
        _chatClient = chatClient;
        
        // 从 JSON 恢复用户画像
        UserProfile = serializedState.ValueKind == JsonValueKind.Object ?
            serializedState.Deserialize<UserProfile>(jsonSerializerOptions)! :
            new UserProfile();        
        Console.WriteLine($"PersonalizationProvider 从序列化状态恢复 (会员等级:{UserProfile.MemberLevel})");
    }
    
    // InvokingAsync: 调用模型前的钩子(基础版本返回空上下文)
    public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
    {
        return ValueTask.FromResult(new AIContext());
    }
    
    // 序列化:保存用户画像
    public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
    {
        return JsonSerializer.SerializeToElement(UserProfile, jsonSerializerOptions);
    }
}
  • 步骤3:实现 InvokingAsync - 注入个性化 Instructions。这是核心功能,根据用户画像动态生成个性化的系统提示。
/// <summary>
/// 个性化配置提供者 - 完整实现版本
/// </summary>
public sealed class PersonalizationProvider : AIContextProvider
{
    private readonly IChatClient _chatClient;
    public UserProfile UserProfile { get; set; }
    
    // 构造函数1:首次创建
    public PersonalizationProvider(IChatClient chatClient, UserProfile? userProfile = null)
    {
        _chatClient = chatClient;
        UserProfile = userProfile ?? new UserProfile();
    }
    
    // 构造函数2:反序列化恢复
    public PersonalizationProvider(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
    {
        _chatClient = chatClient;
        UserProfile = serializedState.ValueKind == JsonValueKind.Object ?
            serializedState.Deserialize<UserProfile>(jsonSerializerOptions)! :
            new UserProfile();
    }
    
    /// <summary>
    /// 调用模型前:注入个性化 Instructions
    /// </summary>
    public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
    {
        var instructions = BuildPersonalizedInstructions();        
        Console.WriteLine($"\n注入个性化 Instructions (会员等级:{UserProfile.MemberLevel}):{instructions}");
        Console.WriteLine($"Instructions 长度: {instructions.Length} 字符");
        
        return new ValueTask<AIContext>(new AIContext
        {
            Instructions = instructions
        });
    }
    
    /// <summary>
    /// 构建个性化 Instructions
    /// </summary>
    private string BuildPersonalizedInstructions()
    {
        var sb = new StringBuilder();
        sb.AppendLine("# Personalization Context");
        sb.AppendLine();
        
        // 会员等级配置
        sb.AppendLine("## User Profile");
        switch (UserProfile.MemberLevel)
        {
            case MemberLevel.VIP:
                sb.AppendLine($"- User is a **VIP member**{(string.IsNullOrEmpty(UserProfile.Name) ? "" : $" named '{UserProfile.Name}'")}");
                sb.AppendLine($"- Use respectful title: '尊敬的{UserProfile.Name ?? "会员"}'");
                sb.AppendLine("- **Prioritize their requests** and provide premium service");
                sb.AppendLine("- Recommend high-end products when appropriate");
                break;
                
            case MemberLevel.Regular:
                sb.AppendLine($"- User is a **regular member**{(string.IsNullOrEmpty(UserProfile.Name) ? "" : $" named '{UserProfile.Name}'")}");
                sb.AppendLine($"- Use friendly greeting: {UserProfile.Name}, 您好");
                sb.AppendLine("- Provide standard service with enthusiasm");
                break;
                
            case MemberLevel.New:
                sb.AppendLine("- User is a **new customer**");
                sb.AppendLine("- Provide warm welcome and detailed guidance");
                sb.AppendLine("- Explain features and processes clearly");
                sb.AppendLine("- Recommend popular beginner-friendly products");
                break;
        }
        sb.AppendLine();
        
        // 语言风格配置
        sb.AppendLine("## Communication Style");
        switch (UserProfile.CommunicationStyle)
        {
            case CommunicationStyle.Formal:
                sb.AppendLine("- Use **formal and professional** language");
                sb.AppendLine("- Maintain business etiquette");
                sb.AppendLine("- Avoid emojis and casual expressions");
                break;
                
            case CommunicationStyle.Casual:
                sb.AppendLine("- Use **friendly and casual** language");
                sb.AppendLine("- Add appropriate emojis to create warm atmosphere");
                sb.AppendLine("- Be conversational and approachable");
                break;
                
            case CommunicationStyle.Concise:
                sb.AppendLine("- Keep responses **brief and to the point**");
                sb.AppendLine("- Maximum 2-3 sentences unless specifically asked for details");
                sb.AppendLine("- Focus on key information only");
                break;
                
            default: // Standard
                sb.AppendLine("- Use **balanced and natural** language");
                sb.AppendLine("- Adapt tone based on context");
                break;
        }
        sb.AppendLine();
        
        // 响应详细程度配置
        sb.AppendLine("## Response Detail Level");
        switch (UserProfile.ResponseDetailLevel)
        {
            case DetailLevel.Brief:
                sb.AppendLine("- Provide **concise answers**");
                sb.AppendLine("- Skip background information unless essential");
                break;
                
            case DetailLevel.Detailed:
                sb.AppendLine("- Provide **comprehensive explanations**");
                sb.AppendLine("- Include background information and examples");
                sb.AppendLine("- Offer step-by-step guidance when relevant");
                break;
                
            default: // Standard
                sb.AppendLine("- Provide **balanced level of detail**");
                sb.AppendLine("- Adjust based on question complexity");
                break;
        }
        sb.AppendLine();
        
        // 专业度配置
        if (UserProfile.IsTechnicalUser)
        {
            sb.AppendLine("## Technical User");
            sb.AppendLine("- User has technical background");
            sb.AppendLine("- Use industry terminology freely");
            sb.AppendLine("- Provide technical specifications when relevant");
            sb.AppendLine();
        }
        else
        {
            sb.AppendLine("## Non-Technical User");
            sb.AppendLine("- Avoid jargon and technical terms");
            sb.AppendLine("- Use simple, everyday language");
            sb.AppendLine("- Explain concepts clearly");
            sb.AppendLine();
        }
        
        // 购买历史(推荐策略)
        if (UserProfile.PurchaseHistory.Any())
        {
            sb.AppendLine("## Purchase History");
            sb.AppendLine($"- User previously purchased: {string.Join(", ", UserProfile.PurchaseHistory.Take(5))}");
            sb.AppendLine("- Recommend related or complementary products when appropriate");
            sb.AppendLine("- Reference past purchases to build rapport");
            sb.AppendLine();
        }
        
        // 统计信息
        if (UserProfile.InteractionCount > 0)
        {
            sb.AppendLine("## Engagement Stats");
            sb.AppendLine($"- Total interactions: {UserProfile.InteractionCount}");
            sb.AppendLine($"- Last interaction: {UserProfile.LastInteractionTime:yyyy-MM-dd HH:mm}");
            sb.AppendLine();
        }        
        return sb.ToString();
    }
    
    // 序列化
    public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
    {
        return JsonSerializer.SerializeToElement(UserProfile, jsonSerializerOptions);
    }
}
  • 步骤4:创建 Agent 并测试不同用户类型,现在将 PersonalizationProvider 集成到 Agent 中,测试实际对话效果。
/// <summary>
/// 创建智能客服 Agent
/// </summary>
ChatClientAgent CreateCustomerServiceAgent(UserProfile userProfile)
{    
    var options = new ChatClientAgentOptions
    {
        Name = "智能客服助手",
        Instructions = @"
    You are a professional e-commerce customer service assistant.

    Core Responsibilities:
    - Answer product inquiries
    - Handle order status questions
    - Provide shopping guidance
    - Resolve customer concerns

    Important:
    - Always follow the personalization context provided
    - Adjust your tone and detail level according to user preferences
    - Be helpful, patient, and professional
    ",
        AIContextProviderFactory = ctx => new PersonalizationProvider(chatClient, userProfile)
    };

    var agent = chatClient.CreateAIAgent(options);
    Console.WriteLine($"智能客服 Agent 创建完成 (用户:{userProfile.Name ?? "匿名"}, 等级:{userProfile.MemberLevel})");
    return agent;
}

3. 测试

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试场景1: VIP 用户咨询");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var vipProfile = new UserProfile
{
    Name = "王总",
    MemberLevel = MemberLevel.VIP,
    CommunicationStyle = CommunicationStyle.Formal,
    ResponseDetailLevel = DetailLevel.Standard,
    PurchaseHistory = new List<string> { "iPhone 15 Pro", "MacBook Pro", "Apple Watch" },
    InteractionCount = 25
};

var vipAgent = CreateCustomerServiceAgent(vipProfile);
var vipThread = vipAgent.GetNewThread();
Console.WriteLine("\n用户提问: 我的订单什么时候能到?");
await foreach (var response in vipAgent.RunStreamingAsync("我的订单什么时候能到?", vipThread))
{
    Console.Write(response.Text);
}
Console.WriteLine("\n\nVIP 用户测试完成");
Console.WriteLine("注意响应中的称呼和语气");

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试场景2: 普通用户咨询");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var regularProfile = new UserProfile
{
    Name = "小李",
    MemberLevel = MemberLevel.Regular,
    CommunicationStyle = CommunicationStyle.Casual,
    ResponseDetailLevel = DetailLevel.Standard,
    PurchaseHistory = new List<string> { "鼠标", "键盘" },
    InteractionCount = 3
};

var regularAgent = CreateCustomerServiceAgent(regularProfile);
var regularThread = regularAgent.GetNewThread();
Console.WriteLine("\n用户提问: 我的订单什么时候能到?");
await foreach (var response in regularAgent.RunStreamingAsync("我的订单什么时候能到?", regularThread))
{
    Console.Write(response.Text);
}
Console.WriteLine("\n\n普通用户测试完成");
Console.WriteLine("对比 VIP 用户,注意称呼和语气的差异");

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试场景3: 新用户咨询");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var newProfile = new UserProfile
{
    MemberLevel = MemberLevel.New,
    CommunicationStyle = CommunicationStyle.Standard,
    ResponseDetailLevel = DetailLevel.Detailed,
    InteractionCount = 0
};

var newAgent = CreateCustomerServiceAgent(newProfile);
var newThread = newAgent.GetNewThread();
Console.WriteLine("\n用户提问: 怎么查询订单状态?");
await foreach (var response in newAgent.RunStreamingAsync("怎么查询订单状态?", newThread))
{
    Console.Write(response.Text);
}
Console.WriteLine("\n\n新用户测试完成");
Console.WriteLine("注意详细的引导和说明");

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试场景4: 简洁风格用户");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var conciseProfile = new UserProfile
{
    Name = "张工",
    MemberLevel = MemberLevel.Regular,
    CommunicationStyle = CommunicationStyle.Concise,
    ResponseDetailLevel = DetailLevel.Brief,
    IsTechnicalUser = true,
    InteractionCount = 10
};

var conciseAgent = CreateCustomerServiceAgent(conciseProfile);
var conciseThread = conciseAgent.GetNewThread();
Console.WriteLine("\n用户提问: 支持什么支付方式?");
await foreach (var response in conciseAgent.RunStreamingAsync("支持什么支付方式?", conciseThread))
{
    Console.Write(response.Text);
}
Console.WriteLine("\n\n简洁风格测试完成");
Console.WriteLine("注意响应的简洁程度");

4. 实现 InvokedAsync - 动态学习用户偏好

步骤6:通过分析用户消息和响应,自动调整用户画像中的沟通风格、详细程度等偏好。

  • 根据用户消息长度判断是否偏好简洁风格

  • 检测用户反馈(如“太详细”、“能详细说明”)自动调整详细程度

  • 根据交互次数自动升级会员等级

动态学习策略,我们可以在 InvokedAsync 方法中实现以下学习逻辑:

学习维度 检测方法 调整策略
简洁偏好 统计用户消息平均长度 < 50 字符 → 设置为简洁风格
详细程度 检测"太详细"、"能详细说明"等关键词 动态调整 DetailLevel
会员升级 统计交互次数 > 10次 → 升级为 VIP
技术用户 检测技术术语的使用 自动标记为技术用户
/// <summary>
/// 个性化配置提供者 - 完整实现版本(含学习能力)
/// </summary>
public sealed class PersonalizationProviderWithLearning : AIContextProvider
{
    private readonly IChatClient _chatClient;
    public UserProfile UserProfile { get; set; }
    
    // 用于统计用户消息长度
    private readonly List<int> _userMessageLengths = new();
    
    // 构造函数1:首次创建
    public PersonalizationProviderWithLearning(IChatClient chatClient, UserProfile? userProfile = null)
    {
        _chatClient = chatClient;
        UserProfile = userProfile ?? new UserProfile();
    }
    
    // 构造函数2:反序列化恢复
    public PersonalizationProviderWithLearning(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
    {
        _chatClient = chatClient;
        UserProfile = serializedState.ValueKind == JsonValueKind.Object ?
            serializedState.Deserialize<UserProfile>(jsonSerializerOptions)! :
            new UserProfile();
    }
    
    /// <summary>
    /// 调用模型前:注入个性化 Instructions
    /// </summary>
    public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
    {
        var instructions = BuildPersonalizedInstructions();
        return new ValueTask<AIContext>(new AIContext { Instructions = instructions });
    }
    
    /// <summary>
    /// 调用模型后:学习用户偏好
    /// </summary>
    public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            // 更新交互统计
            UserProfile.InteractionCount++;
            UserProfile.LastInteractionTime = DateTime.Now;
            
            // 获取用户消息
            var userMessages = context.RequestMessages
                .Where(m => m.Role == ChatRole.User)
                .ToList();
            
            if (!userMessages.Any())
                return;
            
            // 1. 分析消息长度,判断简洁偏好
            var lastUserMessage = userMessages.Last().Text ?? "";
            var messageLength = lastUserMessage.Length;
            _userMessageLengths.Add(messageLength);
            
            // 保持最近10条消息的统计
            if (_userMessageLengths.Count > 10)
                _userMessageLengths.RemoveAt(0);
            
            // 计算平均长度
            if (_userMessageLengths.Count >= 3)
            {
                UserProfile.AverageMessageLength = _userMessageLengths.Average();
                
                // 如果用户消息持续简短,调整为简洁风格
                if (UserProfile.AverageMessageLength < 50 && 
                    UserProfile.CommunicationStyle != CommunicationStyle.Concise)
                {
                    UserProfile.CommunicationStyle = CommunicationStyle.Concise;
                    Console.WriteLine($"\n检测到用户偏好简洁沟通 (平均消息长度:{UserProfile.AverageMessageLength:F1}字符)");
                }
            }
            
            // 2. 检测用户反馈,调整详细程度
            var userText = lastUserMessage.ToLower();
            
            if (userText.Contains("太详细") || userText.Contains("太长") || userText.Contains("简短"))
            {
                if (UserProfile.ResponseDetailLevel != DetailLevel.Brief)
                {
                    UserProfile.ResponseDetailLevel = DetailLevel.Brief;
                    Console.WriteLine("\n检测到用户反馈:希望更简短的回答");
                }
            }
            else if (userText.Contains("详细说明") || userText.Contains("能详细") || userText.Contains("具体"))
            {
                if (UserProfile.ResponseDetailLevel != DetailLevel.Detailed)
                {
                    UserProfile.ResponseDetailLevel = DetailLevel.Detailed;
                    Console.WriteLine("\n检测到用户反馈:希望更详细的说明");
                }
            }
            
            // 3. 根据交互次数升级会员等级
            if (UserProfile.InteractionCount >= 15 && UserProfile.MemberLevel == MemberLevel.Regular)
            {
                UserProfile.MemberLevel = MemberLevel.VIP;
                Console.WriteLine($"\n恭喜!用户已升级为 VIP 会员 (交互次数:{UserProfile.InteractionCount})");
            }
            else if (UserProfile.InteractionCount >= 5 && UserProfile.MemberLevel == MemberLevel.New)
            {
                UserProfile.MemberLevel = MemberLevel.Regular;
                Console.WriteLine($"\n用户已升级为普通会员 (交互次数:{UserProfile.InteractionCount})");
            }
            
            // 4. 检测技术用户
            var technicalKeywords = new[] { "api", "sdk", "代码", "配置", "技术", "参数", "接口" };
            if (!UserProfile.IsTechnicalUser && 
                technicalKeywords.Any(keyword => userText.Contains(keyword)))
            {
                UserProfile.IsTechnicalUser = true;
                Console.WriteLine("\n检测到技术相关问题,标记为技术用户");
            }
            
            Console.WriteLine($"当前状态: 交互{UserProfile.InteractionCount}次 | 会员等级:{UserProfile.MemberLevel} | 风格:{UserProfile.CommunicationStyle}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"偏好学习失败: {ex.Message}");
        }
    }
    
    /// <summary>
    /// 构建个性化 Instructions (复用之前的实现)
    /// </summary>
    private string BuildPersonalizedInstructions()
    {
        var sb = new StringBuilder();
        sb.AppendLine("# Personalization Context");
        sb.AppendLine();
        
        // 会员等级配置
        sb.AppendLine("## User Profile");
        switch (UserProfile.MemberLevel)
        {
            case MemberLevel.VIP:
                sb.AppendLine($"- User is a **VIP member**{(string.IsNullOrEmpty(UserProfile.Name) ? "" : $" named '{UserProfile.Name}'")}");
                sb.AppendLine($"- Use respectful title: '尊敬的{UserProfile.Name ?? "会员"}'");
                sb.AppendLine("- **Prioritize their requests** and provide premium service");
                break;
            case MemberLevel.Regular:
                sb.AppendLine($"- User is a **regular member**{(string.IsNullOrEmpty(UserProfile.Name) ? "" : $" named '{UserProfile.Name}'")}");
                sb.AppendLine($"- Use friendly greeting: '{UserProfile.Name ?? "您"}好'");
                break;
            case MemberLevel.New:
                sb.AppendLine("- User is a **new customer**");
                sb.AppendLine("- Provide warm welcome and detailed guidance");
                break;
        }
        sb.AppendLine();
        
        // 语言风格配置
        sb.AppendLine("## Communication Style");
        switch (UserProfile.CommunicationStyle)
        {
            case CommunicationStyle.Formal:
                sb.AppendLine("- Use **formal and professional** language");
                sb.AppendLine("- Avoid emojis");
                break;
            case CommunicationStyle.Casual:
                sb.AppendLine("- Use **friendly and casual** language");
                sb.AppendLine("- Add appropriate emojis");
                break;
            case CommunicationStyle.Concise:
                sb.AppendLine("- Keep responses **brief and to the point**");
                sb.AppendLine("- Maximum 2-3 sentences");
                break;
            default:
                sb.AppendLine("- Use **balanced and natural** language");
                break;
        }
        sb.AppendLine();
        
        // 响应详细程度
        sb.AppendLine("## Response Detail Level");
        switch (UserProfile.ResponseDetailLevel)
        {
            case DetailLevel.Brief:
                sb.AppendLine("- Provide **concise answers**");
                break;
            case DetailLevel.Detailed:
                sb.AppendLine("- Provide **comprehensive explanations**");
                sb.AppendLine("- Include examples and step-by-step guidance");
                break;
            default:
                sb.AppendLine("- Provide **balanced level of detail**");
                break;
        }
        sb.AppendLine();
        
        // 专业度配置
        if (UserProfile.IsTechnicalUser)
        {
            sb.AppendLine("## Technical User");
            sb.AppendLine("- Use industry terminology freely");
            sb.AppendLine("- Provide technical specifications");
            sb.AppendLine();
        }
        
        // 购买历史
        if (UserProfile.PurchaseHistory.Any())
        {
            sb.AppendLine("## Purchase History");
            sb.AppendLine($"- User previously purchased: {string.Join(", ", UserProfile.PurchaseHistory.Take(5))}");
            sb.AppendLine("- Recommend related products when appropriate");
            sb.AppendLine();
        }
        
        return sb.ToString();
    }
    
    // 序列化
    public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
    {
        return JsonSerializer.SerializeToElement(UserProfile, jsonSerializerOptions);
    }
}
  • 步骤7:测试动态学习功能,测试 Agent 如何根据用户行为自动调整偏好配置。
/// <summary>
/// 创建智能客服 Agent
/// </summary>
ChatClientAgent CreateCustomerLearningAgent(UserProfile userProfile)
{
    var options = new ChatClientAgentOptions
    {
        Name = "智能客服助手",
        Instructions = @"
You are a professional e-commerce customer service assistant.
Always follow the personalization context provided.
Adjust your tone and detail level according to user preferences.",
        AIContextProviderFactory = ctx => new PersonalizationProviderWithLearning(chatClient, userProfile)
    };

    var agent = chatClient.CreateAIAgent(options);
    Console.WriteLine($"智能客服 Agent 创建完成 (用户:{userProfile.Name ?? "匿名"}, 等级:{userProfile.MemberLevel})");
    return agent;
}
// 创建初始用户画像 - 新用户
var learningProfile = new UserProfile
{
    Name = "小王",
    MemberLevel = MemberLevel.New,
    CommunicationStyle = CommunicationStyle.Standard,
    ResponseDetailLevel = DetailLevel.Standard,
    InteractionCount = 0
};

var learningAgent = CreateCustomerLearningAgent(learningProfile);

var learningThread = learningAgent.GetNewThread();

Console.WriteLine("带学习能力的 Agent 创建完成");
Console.WriteLine($"初始状态: {learningProfile.MemberLevel} | {learningProfile.CommunicationStyle}");

三轮测试

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试1: 用户持续发送简短消息");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// 模拟用户发送多条简短消息
var shortQuestions = new[] { "在吗?", "退货", "多久到?", "价格?" };
foreach (var question in shortQuestions)
{
    Console.WriteLine($"\n用户: {question}");
    var response = await learningAgent.RunAsync(question, learningThread);
    Console.WriteLine($"助手: {response.Text}");
}
Console.WriteLine("\n最终状态:");
new 
{ 
    会员等级 = learningProfile.MemberLevel,
    语言风格 = learningProfile.CommunicationStyle,
    平均消息长度 = $"{learningProfile.AverageMessageLength:F1}字符",
    交互次数 = learningProfile.InteractionCount
}.Display();

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试2: 用户反馈'能详细说明'");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"\n当前详细程度: {learningProfile.ResponseDetailLevel}");
Console.WriteLine("\n用户: 能详细说明一下退货流程吗?");
var detailResponse = await learningAgent.RunAsync("能详细说明一下退货流程吗?", learningThread);
Console.WriteLine($"助手: {detailResponse.Text}");

Console.WriteLine($"\n详细程度已调整为: {learningProfile.ResponseDetailLevel}");

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试3: 模拟多次交互,观察会员升级");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"\n当前交互次数: {learningProfile.InteractionCount}");
Console.WriteLine($"当前会员等级: {learningProfile.MemberLevel}");
// 继续对话直到升级
var moreQuestions = new[] 
{ 
    "发票", 
    "优惠券", 
    "积分", 
    "会员权益",
    "物流",
    "客服",
    "换货",
    "保修",
    "评价",
    "推荐" 
};
foreach (var q in moreQuestions)
{
    Console.WriteLine($"\n用户: {q}");
    await learningAgent.RunAsync(q, learningThread);
    
    // 检查是否升级
    if (learningProfile.InteractionCount == 5 || learningProfile.InteractionCount == 15)
    {
        Console.WriteLine($"   当前交互次数: {learningProfile.InteractionCount}");
    }
}
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("最终用户画像:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
learningProfile.Display();
Console.WriteLine("\n动态学习功能测试完成!");