Spiga

Net+AI智能体进阶2:MEAI进阶扩展

2025-08-23 22:16:24

一、函数调用进阶用法

1. FunctionInvokingChatClient 深入解析

FunctionInvokingChatClient 是 MEAI 中负责函数调用的核心中间件。当我们调用 UseFunctionInvocation() 时,实际上是在管道中插入了这个强大的中间件,它会自动处理模型的函数调用请求。

工作原理与执行流程

FunctionInvokingChatClient 作为装饰器包装底层的 IChatClient,拦截对话请求并自动处理函数调用循环:

  1. 发送初始请求:将用户消息和可用工具传递给模型
  2. 检测函数调用:模型返回时,检查响应中是否包含 FunctionCallContent
  3. 执行函数:自动调用被请求的函数,获取执行结果
  4. 回传结果:将函数结果作为 FunctionResultContent 添加到对话历史
  5. 继续迭代:再次调用模型,让它基于函数结果生成最终回答
  6. 返回响应:直到模型不再请求函数调用,返回最终答案

这个循环过程完全自动化,开发者无需手动管理函数调用状态。

用户消息 → 模型(发现需要调用函数) → 执行函数 → 模型(基于结果) → 最终答案
         ↑_______________________________________________|
           (自动迭代,最多 MaximumIterationsPerRequest 次)

核心配置选项

通过 UseFunctionInvocation(configure: options => ) 可以访问 FunctionInvokingChatClient 的配置选项。以下是各属性的详细说明:

  • AdditionalTools(全局工具集)
    • 用途:注册跨所有请求共享的工具,无需每次在 ChatOptions.Tools 中重复指定
    • 适用场景:系统级工具(如时间、日期、日志)
    • 与 ChatOptions.Tools 的关系:两者会合并,AdditionalTools 优先级更低
  • AllowConcurrentInvocation(并发调用)
    • 用途:允许模型在单次响应中并发调用多个函数
    • 性能优势:多个独立函数可并行执行,显著减少等待时间
    • 注意事项:确保工具函数是线程安全的
    • 示例场景:同时查询多个城市的天气
  • MaximumIterationsPerRequest(最大迭代次数)
    • 用途:限制单次请求中函数调用的迭代轮数,防止无限循环
    • 计数规则:每次"用户消息 → 模型响应 → 函数调用"算一次迭代
    • 触发场景:模型反复调用函数但无法得出结论
    • 建议值:简单任务 3-5 次,复杂任务 10-20 次
  • MaximumConsecutiveErrorsPerRequest(最大连续错误)
    • 用途:当函数调用连续失败时终止迭代
    • 错误重置:一次成功的函数调用会重置错误计数器
    • 错误处理:配合 IncludeDetailedErrors 使用
  • IncludeDetailedErrors(详细错误信息)
    • 用途:将函数执行异常的详细堆栈信息传递给模型
    • 安全考量:生产环境中应谨慎使用,可能泄露敏感信息
    • 调试价值:帮助模型理解错误原因,尝试不同的调用方式
  • TerminateOnUnknownCalls(未知函数终止)
    • 用途:当模型请求调用未注册的函数时是否终止对话
    • false:忽略未知函数,继续对话
    • true:立即抛出异常
  • FunctionInvoker(自定义函数执行器)
    • 用途:拦截并自定义所有函数调用的执行逻辑
    • 常见应用:日志记录、性能监控、权限验证、结果缓存、错误重试
options.AdditionalTools = [datetimeTool, weatherTool];
options.AllowConcurrentInvocation = true; // 默认 false
options.MaximumIterationsPerRequest = 5; // 默认 40
options.MaximumConsecutiveErrorsPerRequest = 3; // 默认 3
options.IncludeDetailedErrors = true; // 默认 false
options.TerminateOnUnknownCalls = false; // 默认 false
options.FunctionInvoker = async (context, cancellationToken) =>
{
    Console.WriteLine($"[LOG] 调用函数: {context.Function.Name}");
    Console.WriteLine($"[LOG] 参数: {JsonSerializer.Serialize(context.Arguments)}");
    
    var result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);
    
    Console.WriteLine($"[LOG] 结果: {result}");
    return result;
};

2. AdditionalTools 与 ChatOptions.Tools 的区别

理解这两个工具集合的区别和使用场景非常重要:

特性 AdditionalTools ChatOptions.Tools
配置位置 UseFunctionInvocation(configure: options => ...) 每次请求的 ChatOptions 对象
作用域 全局,对所有使用该客户端的请求生效 单次请求,仅对当前对话生效
典型用途 系统级工具(时间、日志、通用查询) 业务特定工具(订单查询、用户管理)
优先级 较低,会被 ChatOptions.Tools 覆盖 较高,可以临时覆盖全局工具
修改成本 需要重新构建 ChatClient 可以动态调整,灵活性高

组合实例

// 定义系统级工具(所有请求都需要)
var systemTools = new[]
{
    AIFunctionFactory.Create(() => DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "get_current_time", "获取当前时间"),
    AIFunctionFactory.Create(() => Guid.NewGuid().ToString(), "generate_id", "生成唯一标识符")
};

// 定义业务工具(特定场景使用)
var orderTools = new[] { AIFunctionFactory.Create((string orderId) => $"订单 {orderId}: 已发货", "query_order", "查询订单状态") };
var userTools = new[] { AIFunctionFactory.Create((string userId) => $"用户 {userId}: VIP会员", "query_user", "查询用户信息") };

// 构建客户端,注入系统级工具
var hybridClient = chatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.AdditionalTools = systemTools; // 全局工具
    })
    .Build();

// 场景 1:订单查询(系统工具 + 订单工具)
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("场景 1: 订单查询\n");

var orderResponse = await hybridClient.GetResponseAsync("帮我查询订单 ORD-123,并记录查询时间",
    new ChatOptions
    {
        ToolMode = ChatToolMode.Auto,
        Tools = orderTools // 只传业务工具,系统工具自动可用
    }
);

Console.WriteLine($"响应: {orderResponse.Text}\n");

// 场景 2:用户查询(系统工具 + 用户工具)
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("场景 2: 用户查询\n");

var userResponse = await hybridClient.GetResponseAsync("查询用户 USR-456 的信息,并生成一个新的会话ID",
    new ChatOptions
    {
        ToolMode = ChatToolMode.Auto,
        Tools = userTools // 切换到用户工具,系统工具仍然可用
    }
);

Console.WriteLine($"响应: {userResponse.Text}");
Console.WriteLine("\n说明: 系统工具(时间、ID生成)在所有场景中都可用,无需重复配置");

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 场景 1: 订单查询

响应: 已为您查询订单 ORD-123,当前状态为:已发货。 查询时间已记录。如有其他需要,请随时告知!

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 场景 2: 用户查询

响应: 用户 USR-456 是 VIP 会员。

会话ID已生成:S-20231010-456。

说明: 系统工具(时间、ID生成)在所有场景中都可用,无需重复配置

3. 高级应用场景

通过 FunctionInvoker,我们可以实现各种企业级的高级功能。

场景1:监控函数调用过程

观察 FunctionInvokingChatClient 如何处理复杂的多轮函数调用

using System.Diagnostics;
using System.Text.Json;
using System.Threading;

// 创建模拟工具集
var monitoringTools = new[]
{
    AIFunctionFactory.Create((string city) =>
    {
        Thread.Sleep(500); // 模拟API延迟
        return new { Temperature = Random.Shared.Next(15, 35), Humidity = Random.Shared.Next(40, 80) };
    }, "get_weather", "获取指定城市的天气信息"),
    
    AIFunctionFactory.Create((string city) =>
    {
        Thread.Sleep(300);
        return new { Hotels = Random.Shared.Next(50, 200), AvgPrice = Random.Shared.Next(300, 1500) };
    }, "get_hotels", "查询指定城市的酒店数量和平均价格"),
    
    AIFunctionFactory.Create((int temperature) =>
    {
        return temperature switch
        {
            < 15 => "建议穿冬装,携带保暖衣物",
            < 25 => "建议穿春秋装,温度适宜",
            _ => "建议穿夏装,注意防晒"
        };
    }, "suggest_clothing", "根据温度推荐穿搭")
};

// 构建带监控的客户端
int iterationCount = 0;
int functionCallCount = 0;

var monitoredClient = chatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.AllowConcurrentInvocation = true;
        options.MaximumIterationsPerRequest = 10;
        options.FunctionInvoker = async (context, cancellationToken) =>
        {
            functionCallCount++;
            var sw = Stopwatch.StartNew();
            
            Console.WriteLine($"\n [函数调用 #{functionCallCount}]");
            Console.WriteLine($"   函数名: {context.Function.Name}");
            Console.WriteLine($"   参数: {JsonSerializer.Serialize(context.Arguments)}");
            
            var result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);
            sw.Stop();
            
            Console.WriteLine($"   结果: {result}");
            Console.WriteLine($"   耗时: {sw.ElapsedMilliseconds}ms");
            
            return result;
        };
    })
    .Build();

// 执行复杂查询
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("用户查询:帮我查询北京和上海的天气,并根据北京的温度推荐穿搭\n");

var monitoringOptions = new ChatOptions
{
    ToolMode = ChatToolMode.Auto,
    AllowMultipleToolCalls = true,
    Tools = monitoringTools
};

var monitoringResult = await monitoredClient.GetResponseAsync(
    "帮我查询北京和上海的天气,并根据北京的温度推荐穿搭",
    monitoringOptions
);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("最终响应:");
Console.WriteLine(monitoringResult.Text);
Console.WriteLine($"\n 统计: 共 {iterationCount} 次迭代,{functionCallCount} 次函数调用");

场景 2:错误处理与重试机制

演示如何处理函数调用中的错误,以及 MaximumConsecutiveErrorsPerRequest 的作用。

// 创建一个可能失败的工具
int callAttempt = 0;

var unreliableTool = AIFunctionFactory.Create((string orderId) =>
{
    callAttempt++;
    Console.WriteLine($"   [尝试 #{callAttempt}] 查询订单 {orderId}");
    
    // 前两次调用失败,第三次成功
    if (callAttempt < 3)
    {
        throw new InvalidOperationException($"数据库连接超时 (尝试 {callAttempt}/3)");
    }
    
    return new { OrderId = orderId, Status = "已发货", EstimatedDelivery = "2024-10-15" };
}, "query_order", "查询订单状态");

// 配置错误处理客户端
var errorHandlingClient = chatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.MaximumConsecutiveErrorsPerRequest = 5; // 允许更多错误重试
        options.IncludeDetailedErrors = true; // 让模型看到错误详情
        options.FunctionInvoker = async (context, cancellationToken) =>
        {
            try
            {
                return await context.Function.InvokeAsync(context.Arguments, cancellationToken);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"    函数执行失败: {ex.Message}");
                throw; // 重新抛出,让 FunctionInvokingChatClient 处理
            }
        };
    })
    .Build();

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 测试:模拟函数调用失败与重试\n");

callAttempt = 0; // 重置计数器

try
{
    var errorResult = await errorHandlingClient.GetResponseAsync(
        "帮我查询订单号 ORD123456 的状态",
        new ChatOptions
        {
            ToolMode = ChatToolMode.Auto,
            Tools = [unreliableTool]
        }
    );
    
    Console.WriteLine("\n 最终成功!");
    Console.WriteLine($"响应: {errorResult.Text}");
}
catch (Exception ex)
{
    Console.WriteLine($"\n 达到最大错误次数,请求终止: {ex.Message}");
}

场景 3:并发函数调用性能对比

展示 AllowConcurrentInvocation 对性能的影响。

// 创建一组模拟耗时的工具
var performanceTools = new[]
{
    AIFunctionFactory.Create((string city) => { Thread.Sleep(1000); return $"{city}: 晴天 25℃"; }, "weather", "查询天气"),
    AIFunctionFactory.Create((string city) => { Thread.Sleep(1000); return $"{city}: 98% 运行正常"; }, "traffic", "查询交通"),
    AIFunctionFactory.Create((string city) => { Thread.Sleep(1000); return $"{city}: 空气质量良好"; }, "air_quality", "查询空气质量")
};

// 测试 1: 串行执行(默认)
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: 串行函数调用 (AllowConcurrentInvocation = false)\n");

var sequentialClient = chatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.AllowConcurrentInvocation = false; // 串行执行
    })
    .Build();

var sw1 = Stopwatch.StartNew();
var sequentialResult = await sequentialClient.GetResponseAsync(
    "请同时查询北京的天气、交通和空气质量",
    new ChatOptions { ToolMode = ChatToolMode.Auto, AllowMultipleToolCalls = true, Tools = performanceTools }
);
sw1.Stop();

Console.WriteLine($" 总耗时: {sw1.ElapsedMilliseconds}ms");
Console.WriteLine($"响应: {sequentialResult.Text?[..Math.Min(100, sequentialResult.Text.Length)]}...\n");

// 测试 2: 并发执行
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 并发函数调用 (AllowConcurrentInvocation = true)\n");

var concurrentClient = chatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.AllowConcurrentInvocation = true; // 并发执行
    })
    .Build();

var sw2 = Stopwatch.StartNew();
var concurrentResult = await concurrentClient.GetResponseAsync(
    "请同时查询北京的天气、交通和空气质量",
    new ChatOptions { ToolMode = ChatToolMode.Auto, AllowMultipleToolCalls = true, Tools = performanceTools }
);
sw2.Stop();

Console.WriteLine($"总耗时: {sw2.ElapsedMilliseconds}ms");
Console.WriteLine($"响应: {concurrentResult.Text?[..Math.Min(100, concurrentResult.Text.Length)]}...\n");

// 性能对比
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("性能对比:");
Console.WriteLine($"   串行耗时: {sw1.ElapsedMilliseconds}ms");
Console.WriteLine($"   并发耗时: {sw2.ElapsedMilliseconds}ms");
Console.WriteLine($"   性能提升: {(double)sw1.ElapsedMilliseconds / sw2.ElapsedMilliseconds:F2}x");
Console.WriteLine($"   并发执行节省了约 {sw1.ElapsedMilliseconds - sw2.ElapsedMilliseconds}ms");

场景 4:函数调用日志与审计

通过 FunctionInvoker 实现企业级的函数调用审计:

// 定义审计日志结构
public record FunctionCallAuditLog(
    DateTime Timestamp,
    string FunctionName,
    object Arguments,
    object? Result,
    TimeSpan Duration,
    bool Success,
    string? ErrorMessage
);

var auditLogs = new List<FunctionCallAuditLog>();

// 构建带审计的客户端
var auditedClient = chatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.FunctionInvoker = async (context, cancellationToken) =>
        {
            var timestamp = DateTime.UtcNow;
            var sw = Stopwatch.StartNew();
            object? result = null;
            Exception? error = null;
            
            try
            {
                result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);
                return result;
            }
            catch (Exception ex)
            {
                error = ex;
                throw;
            }
            finally
            {
                sw.Stop();
                
                // 记录审计日志
                auditLogs.Add(new FunctionCallAuditLog(
                    Timestamp: timestamp,
                    FunctionName: context.Function.Name,
                    Arguments: context.Arguments,
                    Result: result,
                    Duration: sw.Elapsed,
                    Success: error == null,
                    ErrorMessage: error?.Message
                ));
            }
        };
    })
    .Build();

// 执行一些函数调用
var auditResult = await auditedClient.GetResponseAsync(
    "现在几点了?",
    new ChatOptions
    {
        ToolMode = ChatToolMode.Auto,
        Tools = [AIFunctionFactory.Create(() => DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "get_time", "获取当前时间")]
    }
);

Console.WriteLine("审计日志:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
foreach (var log in auditLogs)
{
    Console.WriteLine($"[{log.Timestamp:HH:mm:ss}] {log.FunctionName}");
    Console.WriteLine($"   状态: {(log.Success ? "成功" : "失败")}");
    Console.WriteLine($"   耗时: {log.Duration.TotalMilliseconds:F2}ms");
    Console.WriteLine($"   结果: {log.Result}");
    if (!log.Success)
        Console.WriteLine($"   错误: {log.ErrorMessage}");
    Console.WriteLine();
}

场景 5:权限控制与安全检查

在函数执行前验证用户权限:

// 定义权限系统
public class UserPermissions
{
    public string UserId { get; init; }
    public HashSet<string> AllowedFunctions { get; init; } = new();
}

var currentUser = new UserPermissions
{
    UserId = "user_001",
    AllowedFunctions = new HashSet<string> { "query_order", "get_weather" }
    // 注意: "delete_order" 不在允许列表中
};

// 创建敏感操作工具
var sensitiveTools = new[]
{
    AIFunctionFactory.Create((string orderId) => $"查询到订单 {orderId} 的详情", "query_order", "查询订单"),
    AIFunctionFactory.Create((string orderId) => $"订单 {orderId} 已删除", "delete_order", "删除订单(敏感操作)")
};

// 构建带权限控制的客户端
var secureClient = chatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.FunctionInvoker = async (context, cancellationToken) =>
        {
            var functionName = context.Function.Name;
            
            // 权限检查
            if (!currentUser.AllowedFunctions.Contains(functionName))
            {
                var errorMsg = $"用户 {currentUser.UserId} 无权调用函数 '{functionName}'";
                Console.WriteLine(errorMsg);
                throw new UnauthorizedAccessException(errorMsg);
            }
            
            Console.WriteLine($"权限验证通过: {currentUser.UserId} -> {functionName}");
            return await context.Function.InvokeAsync(context.Arguments, cancellationToken);
        };
    })
    .Build();

// 测试:允许的操作
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: 查询订单(允许)\n");

try
{
    var allowedResult = await secureClient.GetResponseAsync(
        "帮我查询订单 ORD-12345",
        new ChatOptions { ToolMode = ChatToolMode.Auto, Tools = sensitiveTools }
    );
    Console.WriteLine($"成功: {allowedResult.Text}\n");
}
catch (Exception ex)
{
    Console.WriteLine($"失败: {ex.Message}\n");
}

// 测试:禁止的操作
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 删除订单(禁止)\n");

try
{
    var deniedResult = await secureClient.GetResponseAsync(
        "帮我删除订单 ORD-12345",
        new ChatOptions { ToolMode = ChatToolMode.Auto, Tools = sensitiveTools }
    );
    Console.WriteLine($"成功: {deniedResult.Text}\n");
}
catch (Exception ex)
{
    Console.WriteLine($"权限拒绝已生效: {ex.GetType().Name}\n");
}

场景 6:结果缓存优化

使用 FunctionInvoker 实现函数级别的缓存:

// 简单的内存缓存
var functionCache = new Dictionary<string, (object Result, DateTime Timestamp)>();
var cacheDuration = TimeSpan.FromSeconds(30);

// 创建耗时的 API 工具
var expensiveTool = AIFunctionFactory.Create((string stockSymbol) =>
{
    Console.WriteLine($"   调用外部 API 查询股票 {stockSymbol}...");
    Thread.Sleep(2000); // 模拟 API 延迟
    return new { Symbol = stockSymbol, Price = Random.Shared.Next(100, 500), Change = Random.Shared.NextDouble() * 10 - 5 };
}, "get_stock_price", "查询股票实时价格");

// 构建带缓存的客户端
var cachedFunctionClient = chatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.FunctionInvoker = async (context, cancellationToken) =>
        {
            var cacheKey = $"{context.Function.Name}:{JsonSerializer.Serialize(context.Arguments)}";
            
            // 检查缓存
            if (functionCache.TryGetValue(cacheKey, out var cached))
            {
                if (DateTime.UtcNow - cached.Timestamp < cacheDuration)
                {
                    Console.WriteLine($"   从缓存返回: {context.Function.Name}");
                    return cached.Result;
                }
                else
                {
                    Console.WriteLine($"   缓存已过期,重新调用");
                    functionCache.Remove(cacheKey);
                }
            }
            
            // 执行函数并缓存结果
            var result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);
            functionCache[cacheKey] = (result, DateTime.UtcNow);
            return result;
        };
    })
    .Build();

var stockOptions = new ChatOptions { ToolMode = ChatToolMode.Auto, Tools = [expensiveTool] };

// 第一次查询 - 调用 API
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("第一次查询 AAPL 股票价格\n");
var sw1 = Stopwatch.StartNew();
var firstCall = await cachedFunctionClient.GetResponseAsync("查询 AAPL 股票价格", stockOptions);
sw1.Stop();
Console.WriteLine($"耗时: {sw1.ElapsedMilliseconds}ms\n");

// 第二次查询 - 从缓存返回
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("第二次查询 AAPL 股票价格(应该命中缓存)\n");
var sw2 = Stopwatch.StartNew();
var secondCall = await cachedFunctionClient.GetResponseAsync("查询 AAPL 股票价格", stockOptions);
sw2.Stop();
Console.WriteLine($"耗时: {sw2.ElapsedMilliseconds}ms\n");

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"缓存效果: 第二次查询快了 {sw1.ElapsedMilliseconds - sw2.ElapsedMilliseconds}ms ({(double)sw1.ElapsedMilliseconds / sw2.ElapsedMilliseconds:F1}x 加速)");

4. 最佳实践

推荐做法

  • 合理设置迭代次数
    • 简单任务:3-5 次
    • 中等任务:10-15 次
    • 复杂任务:20-30 次
    • 避免使用过大的值(如 100),可能导致成本失控
  • 启用并发调用提升性能
    • 确保函数是线程安全的
    • 适用于查询类操作
    • 避免在有状态依赖的函数上使用
  • 生产环境关闭详细错误
    • 开发环境:true(便于调试)
    • 生产环境:false(防止信息泄露)
  • 使用 AdditionalTools 注册通用工具
    • 减少每次请求的配置负担
    • 确保系统级工具始终可用
  • 在 FunctionInvoker 中添加可观测性
    • 日志记录
    • 性能监控
    • 错误追踪
    • 审计合规
options.MaximumIterationsPerRequest = 10; // 根据任务复杂度调整
options.AllowConcurrentInvocation = true; // 适用于独立函数
options.IncludeDetailedErrors = false; // 生产环境默认值
options.AdditionalTools = [timeTool, logTool, idGeneratorTool];

常见陷阱

  • 无限循环风险
    • 问题:模型反复调用同一个函数,无法收敛
    • 解决:设置合理的 MaximumIterationsPerRequest 和 MaximumConsecutiveErrorsPerRequest
  • 并发安全问题
    • 问题:启用 AllowConcurrentInvocation 但函数不是线程安全的
    • 解决:使用锁或确保函数无状态
  • 工具描述不清晰
    • 问题:模型不知道何时调用工具,或传入错误参数
    • 解决:提供清晰的函数名、描述和参数说明
  • 忽略错误处理
    • 问题:函数抛出异常后流程中断
    • 解决:在函数内部或 FunctionInvoker 中妥善处理异常
  • 过度依赖函数调用
    • 问题:简单问题也触发复杂的函数调用链
    • 解决:合理使用 ToolMode,考虑在 system message 中引导模型判断

性能优化建议

  • 减少函数调用延迟
    • 使用缓存避免重复调用
    • 启用并发执行
    • 优化函数内部逻辑
  • 控制上下文大小
    • 函数返回结果应简洁明了
    • 避免返回大量无关数据
    • 配合 Chat Reducer 使用
  • 智能工具选择
    • 根据用户意图动态调整 ChatOptions.Tools
    • 不要一次性注册过多工具(建议 < 20 个)
    • 将相似功能合并为单个工具

安全考虑

  • 输入验证
options.FunctionInvoker = async (context, cancellationToken) =>
{
    // 验证参数
    if (context.Arguments.TryGetValue("userId", out var userId))
    {
        if (!IsValidUserId(userId?.ToString()))
            throw new ArgumentException("Invalid user ID");
    }

    return await context.Function.InvokeAsync(context.Arguments, cancellationToken);
};
  • 权限检查(参考场景 5 示例)

  • 敏感数据脱敏

    • 在返回结果前脱敏处理

    • 避免在日志中记录敏感信息

5. 与其他中间件的协同

FunctionInvokingChatClient 可以与其他 MEAI 中间件组合使用,构建强大的处理管道。

推荐的中间件顺序

var comprehensiveClient = ChatClient.AsBuilder()
    // 1. 日志记录(最外层,记录所有活动)
    .UseLogging()
    // 2. 缓存(在函数调用前检查缓存)
    .UseDistributedCache(cache)
    // 3. 消息压缩(减少 Token 消耗)
    .UseChatReducer(reducer)
    // 4. 函数调用(核心业务逻辑)
    .UseFunctionInvocation(configure: options =>
    {
        options.AdditionalTools = systemTools;
        options.AllowConcurrentInvocation = true;
    })
    // 5. 重试机制(最内层,处理底层 API 失败)
    .UseRetry()
    .Build();

顺序说明

  1. 日志在最外层:捕获完整的请求/响应周期
  2. 缓存在函数调用前:避免重复执行相同的函数调用链
  3. Reducer 在中间:压缩历史消息但保留函数调用上下文
  4. 函数调用在核心:处理工具调用逻辑
  5. 重试在最内层:处理底层 API 的暂时性故障

注意:某些中间件的顺序会显著影响行为。例如,将缓存放在函数调用后会缓存整个函数调用流程,但无法为单个函数结果提供缓存。

二、ChatOptions扩展

ChatOptions 作为统一的抽象层,提供了跨不同 AI 提供商的通用配置接口。但不同提供商往往有自己独特的功能和参数:

  • OpenAI: store、metadata、IncludeUsage 等
  • Azure AI: 自定义的 AdditionalProperties、审核配置等
  • Google: 特定的安全设置和生成配置

如果将所有提供商的特殊参数都加入 ChatOptions,会导致抽象层变得臃肿且难以维护。因此,Microsoft.Extensions.AI 提供了两个扩展点:

  1. AdditionalProperties: 键值对字典,用于透传自定义参数
  2. RawRepresentationFactory: 工厂方法,用于直接构造底层提供商的选项对象

接着我们从源码上来分享一下这两个扩展点:

1. 扩展点 AdditionalProperties

AdditionalProperties 允许我们传递任意键值对。这些键值对可以被中间件或底层实现读取和使用。

  • 场景 1:传递提供商特定的参数

当标准 ChatOptions 属性无法覆盖某个 AI 提供商的特殊功能时,使用 AdditionalProperties:

// 示例:OpenAI 的 strict JSON schema 模式
var options = new ChatOptions
{
    ResponseFormat = ChatResponseFormat.Json,
    AdditionalProperties = new()
    {
        ["strictJsonSchema"] = true  // OpenAI 特定参数
    }
};

// 底层实现中的使用(在 OpenAIClientExtensions.cs 中)
internal static bool? HasStrict(IReadOnlyDictionary<string, object?>? additionalProperties) =>
    additionalProperties?.TryGetValue("strictJsonSchema", out object? strictObj) is true &&
    strictObj is bool strictValue ?
    strictValue : null;
  • 场景 2:存储对话摘要

在某些场景下,为了控制会话历史,我们可能希望为每次对话生成摘要。在 SummarizingChatReducer 中就是通过 AdditionalProperties 来存储和传递摘要内容的:

public static SummarizedConversation FromChatMessages(IEnumerable<ChatMessage> messages)
{
    string? summary = null;
    ChatMessage? systemMessage = null;
    var unsummarizedMessages = new List<ChatMessage>();

    foreach (var message in messages)
    {
        if (message.Role == ChatRole.System)
        {
            systemMessage ??= message;
        }
        // 关键:从 AdditionalProperties 中读取摘要
        else if (message.AdditionalProperties?.TryGetValue<string>(SummaryKey, out var summaryValue) == true)
        {
            unsummarizedMessages.Clear();  // 清空之前的未摘要消息
            summary = summaryValue;        // 保存摘要内容
        }
        else if (!message.Contents.Any(m => m is FunctionCallContent or FunctionResultContent))
        {
            unsummarizedMessages.Add(message);
        }
    }

    return new(summary, systemMessage, unsummarizedMessages);
}

public async Task<SummarizedConversation> ResummarizeAsync(
    IChatClient chatClient, int targetCount, string summarizationPrompt, CancellationToken cancellationToken)
{
    var messagesToResummarize = unsummarizedMessages.Count - targetCount;
    if (messagesToResummarize <= 0)
    {
        return this;
    }

    // 调用 AI 生成摘要
    var summarizerChatMessages = ToSummarizerChatMessages(messagesToResummarize, summarizationPrompt);
    var response = await chatClient.GetResponseAsync(summarizerChatMessages, cancellationToken: cancellationToken);
    var newSummary = response.Text;

    // 关键:将摘要写入 AdditionalProperties
    var lastSummarizedMessage = unsummarizedMessages[messagesToResummarize - 1];
    var additionalProperties = lastSummarizedMessage.AdditionalProperties ??= [];  // 确保字典存在
    additionalProperties[SummaryKey] = newSummary;  // 存储摘要

    var newUnsummarizedMessages = unsummarizedMessages.Skip(messagesToResummarize).ToList();
    return new SummarizedConversation(newSummary, systemMessage, newUnsummarizedMessages);
}
  • 场景 3:需要传递额外属性时

在 Microsoft.Extensions.AI.AzureAIInference 包中,ChatOptions 的 AdditionalProperties 被用来传递一些额外的属性,这些属性在底层被转换为 BinaryData 类型并存储在 Azure.AI.Inference.ChatCompletionsOptions 的 AdditionalProperties 中。

private ChatCompletionsOptions ToAzureAIOptions(IEnumerable<ChatMessage> chatContents, ChatOptions? options)
{
    if (options is null)
    {
        return CreateAzureAIOptions(chatContents, options);
    }
    if (options.RawRepresentationFactory?.Invoke(this) is ChatCompletionsOptions result)
    {
        result.Messages = ToAzureAIInferenceChatMessages(chatContents, options).ToList();
        result.Model ??= options.ModelId ?? _metadata.DefaultModelId ??
            throw new InvalidOperationException("No model id was provided when either constructing the client or in the chat options.");
    }
    else
    {
        result = CreateAzureAIOptions(chatContents, options);
    }
    result.FrequencyPenalty ??= options.FrequencyPenalty;
    result.MaxTokens ??= options.MaxOutputTokens;
    result.NucleusSamplingFactor ??= options.TopP;
    result.PresencePenalty ??= options.PresencePenalty;

    if (options.AdditionalProperties is { } props)
    {
        foreach (var prop in props)
        {
            byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
            result.AdditionalProperties[prop.Key] = new BinaryData(data);
        }
    }

    return result;
}

2. 扩展点 RawRepresentationFactory

RawRepresentationFactory 是 Microsoft.Extensions.AI 抽象层中的一个**逃生舱(Escape Hatch)**机制,用于在统一抽象与底层实现之间建立桥梁。

调用时机和流程:以 OpenAIChatClient 为例,查看其 ToOpenAIOptions 方法:

private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
{
    if (options is null)
    {
        return new ChatCompletionOptions();
    }

    // 关键步骤 1: 尝试使用 RawRepresentationFactory
    if (options.RawRepresentationFactory?.Invoke(this) is not ChatCompletionOptions result)
    {
        result = new ChatCompletionOptions();
    }

    // 关键步骤 2: 使用空值合并(??=)模式填充属性
    // 只覆盖 RawRepresentation 中为 null 的属性
    result.FrequencyPenalty ??= options.FrequencyPenalty;
    result.MaxOutputTokenCount ??= options.MaxOutputTokens;
    result.TopP ??= options.TopP;
    result.PresencePenalty ??= options.PresencePenalty;
    result.Temperature ??= options.Temperature;
    result.Seed ??= options.Seed;
    
    // 关键步骤 3: 处理集合和其他复杂属性
    if (options.StopSequences is { Count: > 0 } stopSequences)
    {
        foreach (string stopSequence in stopSequences)
        {
            result.StopSequences.Add(stopSequence);
        }
    }
    
    return result;
}

核心机制 - 空值合并优先级

  • RawRepresentationFactory 返回的对象中已设置的属性(非 null)具有最高优先级

  • ChatOptions 的属性仅用于填充 raw representation 中未设置的(null)属性

  • 这允许精细控制:哪些设置由 raw representation 管理,哪些由标准抽象管理

  • 场景 1: 设置提供商特有的配置
// 示例:配置 OpenAI 特有的 IncludeUsage 选项(在标准 ChatOptions 中不存在)
var chatOptions = new ChatOptions
{
    Temperature = 0.7f,
    MaxOutputTokens = 1000,
    
    RawRepresentationFactory = (client) =>
    {
        var openAIOptions = new ChatCompletionOptions
        {
            // OpenAI 特有:在流式响应中包含使用统计
            IncludeUsage = true,
            
            // 可以预设某些值,它们不会被 ChatOptions 覆盖
            TopP = 0.95f
        };
        return openAIOptions;
    }
};

// 实际发送的请求将包含:
// - Temperature: 0.7 (来自 ChatOptions,因为 raw representation 中未设置)
// - MaxOutputTokens: 1000 (来自 ChatOptions)
// - TopP: 0.95 (来自 RawRepresentationFactory,不会被 ChatOptions 覆盖)
// - IncludeUsage: true (OpenAI 特有,只能通过 RawRepresentationFactory 设置)
```
  • 场景 2: 优先级控制(测试场景)
ChatOptions chatOptions = new()
{
    RawRepresentationFactory = (c) =>
    {
        // 在 raw representation 中设置特定值
        ChatCompletionOptions openAIOptions = new()
        {
            FrequencyPenalty = 0.75f,        // 预设值
            Temperature = 0.5f,              // 预设值
            // 其他属性保持 null
        };
        return openAIOptions;
    },
    
    // 这些值只会填充 raw representation 中为 null 的属性
    FrequencyPenalty = 0.125f,  // 不会生效(raw 中已设置为 0.75)
    Temperature = 0.125f,       // 不会生效(raw 中已设置为 0.5)
    MaxOutputTokens = 1,        // 会生效(raw 中未设置)
    Seed = 1                    // 会生效(raw 中未设置)
};

// 最终发送的请求:
// FrequencyPenalty = 0.75 (来自 RawRepresentationFactory)
// Temperature = 0.5 (来自 RawRepresentationFactory)  
// MaxOutputTokens = 1 (来自 ChatOptions)
// Seed = 1 (来自 ChatOptions)
  • 场景 3: 动态适配不同提供商
IChatClient client = GetChatClient();

var options = new ChatOptions
{
    Temperature = 0.7f,
    
    RawRepresentationFactory = (client) =>
    {
        // 根据客户端类型返回不同的配置
        return client switch
        {
            OpenAIChatClient => new OpenAI.Chat.ChatCompletionOptions
            {
                IncludeUsage = true,
                Store = true // OpenAI 特有的持久化选项
            },
            
            AzureAIInferenceChatClient => new Azure.AI.Inference.ChatCompletionsOptions
            {
                // Azure AI 特有配置
                AdditionalProperties = 
                {
                    ["custom_azure_setting"] = new BinaryData("value")
                }
            },
            
            _ => null // 其他客户端使用默认转换
        };
    }
};
  • 场景 4: 配置底层 SDK 的高级特性
// Azure AI 示例:配置 top_k(在 ChatOptions 中存在但需要特殊序列化)
var options = new ChatOptions
{
    RawRepresentationFactory = (client) =>
    {
        var azureOptions = new ChatCompletionsOptions();
        
        // 配置复杂的 AdditionalProperties
        azureOptions.AdditionalProperties["custom_moderation"] = 
            new BinaryData(JsonSerializer.SerializeToUtf8Bytes(new 
            {
                enabled = true,
                threshold = 0.8
            }));
            
        return azureOptions;
    }
};

3. 重要注意事项

  • 必须返回新实例
// 错误:返回共享实例
private static ChatCompletionOptions _sharedOptions = new();
RawRepresentationFactory = (c) => _sharedOptions;

// 正确:每次返回新实例
RawRepresentationFactory = (c) => new ChatCompletionOptions
{
    IncludeUsage = true
};

原因:框架会在返回的对象上进行额外的修改(如添加 messages、tools 等),共享实例会导致状态污染。

  • 类型必须匹配
// OpenAIChatClient 期望 ChatCompletionOptions
if (options.RawRepresentationFactory?.Invoke(this) is not ChatCompletionOptions result)
{
    result = new ChatCompletionOptions(); // 类型不匹配时,创建默认实例
}

返回错误类型会被忽略,框架将创建默认实例。

  • 不会被序列化
[JsonIgnore]
public Func<IChatClient, object?>? RawRepresentationFactory { get; set; }

委托不能序列化,因此在序列化/反序列化 ChatOptions 时会丢失此属性。

4. 在不同 AI 库中的一致性

这个模式在所有 *Options 类中保持一致:

Options 类 Factory 类型 Raw Type 示例
ChatOptions Func<IChatClient, object?> OpenAI.Chat.ChatCompletionOptions
EmbeddingGenerationOptions Func<IEmbeddingGenerator, object?> OpenAI.Embeddings.EmbeddingGenerationOptions
SpeechToTextOptions Func<ISpeechToTextClient, object?> 提供商特定类型
ImageGenerationOptions Func<IImageGenerator, object?> OpenAI.Images.ImageGenerationOptions

三、Tool Reduction进阶用法

1. 高级配置

  • 自定义策略属性:EmbeddingToolReductionStrategy 提供了多个可配置属性,允许精细控制筛选行为。
using System.Numerics.Tensors;

#pragma warning disable MEAI001
var advancedStrategy = new EmbeddingToolReductionStrategy(embeddingGenerator, toolLimit: 5)
{
    // 1. 自定义工具的 Embedding 文本生成方式
    ToolEmbeddingTextSelector = tool => $"{tool.Name}: {tool.Description}",    
    // 2. 自定义消息的 Embedding 文本生成方式
    MessagesEmbeddingTextSelector = async messages =>
    {
        // 只使用最后 3 条消息
        var lastMessages = messages.TakeLast(3);
        return string.Join("\n", lastMessages.Select(m => m.Text));
    },    
    // 3. 自定义相似度计算函数(默认是余弦相似度)
    Similarity = (a, b) => TensorPrimitives.CosineSimilarity(a.Span, b.Span),  
    // 4. 标记某些工具为"必需"(始终保留,不计入 toolLimit)
    IsRequiredTool = tool => tool.Name.StartsWith("Core") || tool.Name == "GetCurrentTime",    
    // 5. 是否保持原始工具顺序(默认按相似度排序)
    PreserveOriginalOrdering = false
};

Console.WriteLine("已创建自定义配置的策略");
Console.WriteLine(" 配置项:");
Console.WriteLine(" - ToolEmbeddingTextSelector: 自定义工具文本");
Console.WriteLine(" - MessagesEmbeddingTextSelector: 只使用最后3条消息");
Console.WriteLine(" - Similarity: 余弦相似度计算");
Console.WriteLine(" - IsRequiredTool: Core* 和 GetCurrentTime 为必需工具");
Console.WriteLine(" - PreserveOriginalOrdering: 按相似度排序");
  • 必需工具配置示例:演示如何将某些工具标记为,使其始终保留。
using System.ComponentModel;
using System.Threading;

// 定义一些工具
[Description("核心系统工具:获取系统状态")]
string CoreSystemStatus() => "系统运行正常";

[Description("核心安全工具:验证用户权限")]
string CoreSecurityCheck(string userId) => $"用户 {userId} 权限验证通过";

[Description("获取当前时间")]
string GetCurrentTime() => DateTime.Now.ToString("HH:mm:ss");

[Description("获取天气信息")]
string GetWeather(string city) => $"{city} 天气晴朗";

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

#pragma warning disable MEAI001
// 创建策略,将 Core* 开头和 GetCurrentTime 设为必需工具
var requiredToolStrategy = new EmbeddingToolReductionStrategy(embeddingGenerator, toolLimit: 1)
{
    IsRequiredTool = tool => tool.Name.StartsWith("Core") || tool.Name == "GetCurrentTime"
};

// 测试筛选结果
var messages = new[] { new ChatMessage(ChatRole.User, "北京天气怎么样?") };
var options = new ChatOptions { Tools = [..tools] };
var selected = await requiredToolStrategy.SelectToolsForRequestAsync(messages, options, CancellationToken.None);

Console.WriteLine($"\n原始工具数量: {tools.Count}");
Console.WriteLine($"toolLimit 设置: 1");
Console.WriteLine($"筛选后工具数量: {selected.Count()}");
Console.WriteLine($"\n筛选结果(必需工具始终保留):");
selected.Select(t => new { 工具名称 = t.Name, 是否必需 = t.Name.StartsWith("Core") || t.Name == "GetCurrentTime" }).Display();

/*
关键点
- 必需工具(IsRequiredTool = true)不计入 toolLimit
- 即使 toolLimit = 1,必需工具 + 1 个最相关工具都会保留
- 适用于核心系统工具、安全工具等必须存在的场景
*/

2. 集成与优化

  • 依赖注入集成:在实际应用中,推荐使用依赖注入来管理 ChatClient 和 Tool Reduction。
var services = new ServiceCollection();

#pragma warning disable MEAI001
    // 注册 Embedding Generator
    services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(sp =>
                                                                         {
                                                                             return embeddingGenerator;
                                                                         });

// 注册 ChatClient 并配置 Tool Reduction
services.AddChatClient(sp =>
{
    var embeddingGen = sp.GetRequiredService<IEmbeddingGenerator<string, Embedding<float>>>();
    var toolReductionStrategy = new EmbeddingToolReductionStrategy(embeddingGen, toolLimit: 5);

    return ChatClientFactory.GetQwenClient().AsBuilder()
        .UseToolReduction(toolReductionStrategy)
        .UseFunctionInvocation()
        .Build();
});

var serviceProvider = services.BuildServiceProvider();
var chatClient2 = serviceProvider.GetRequiredService<IChatClient>();

Console.WriteLine("已通过依赖注入配置 ChatClient");
Console.WriteLine(" - EmbeddingGenerator 注册为单例");
Console.WriteLine(" - ChatClient 自动应用 Tool Reduction");
  • 监控和调试

    • Tool Reduction 中间件会修改 ChatOptions.Tools 集合

    • 修改发生在请求发送给底层模型之前

    • 可以通过在 UseToolReduction() 之后添加中间件来观察最终结果

    • IToolReductionStrategy 接口的 SelectToolsForRequestAsync 方法可以直接调用来预览筛选结果

// 添加一个中间件去监控每次调用时发送的工具
public static ChatClientBuilder UseToolListLogging(this ChatClientBuilder builder)
{
    return builder.Use(
        getResponseFunc: async (messages, options, innerClient, cancellationToken) =>
        {
            Console.WriteLine($"[Tools] {options.Tools?.Count ?? 0} 个工具被发送到模型:");   
            if (options.Tools != null)
            {
                foreach (var tool in options.Tools)
                {
                    Console.WriteLine($" - 工具名称: {tool.Name}, 描述: {tool.Description}");
                }
            }
            var response = await innerClient.GetResponseAsync(messages, options, cancellationToken);
            
            return response;
        },
        getStreamingResponseFunc: null);
}

#pragma warning disable MEAI001
// 创建带详细监控的客户端
var monitoringClient = chatClient.AsBuilder()
    .UseToolListLogging() // 在执行工具筛选前,监控实际发送给模型的工具列表
    .UseToolReduction(new EmbeddingToolReductionStrategy(embeddingGenerator, toolLimit: 3)) //应用工具筛选策略
    .UseToolListLogging() // 在执行工具筛选后,监控实际发送给模型的工具列表    
    .UseFunctionInvocation()
    .Build();

Console.WriteLine("已创建带监控功能的 ChatClient");
  • 性能优化建议
优化点 说明 实施方法
工具分组 将相似工具按领域分组,减少筛选复杂度 使用命名约定,如 "Weather_", "Database_"
缓存 Embedding 工具描述的 Embedding 会自动缓存 ConditionalWeakTable 自动管理
动态工具注册 根据用户上下文动态注册工具子集 在 ChatOptions 中只添加相关领域的工具
调整 toolLimit 根据实际场景动态调整保留的工具数量 根据工具总数和模型上下文窗口调整
优化工具描述 详细的工具描述有助于提高相似度计算的准确性 使用完整的自然语言描述

3. 实战演练:完整示例

#pragma warning disable MEAI001

// 创建生产级策略
var productionStrategy = new EmbeddingToolReductionStrategy(embeddingGenerator, toolLimit: 8)
{
    // 只使用最近 5 条消息进行筛选
    MessagesEmbeddingTextSelector = async messages =>
    {
        var recentMessages = messages.TakeLast(5);
        return string.Join(" ", recentMessages.Select(m => m.Text));
    },
    
    // 核心工具始终保留
    IsRequiredTool = tool => 
        tool.Name.StartsWith("Core") || 
        tool.Name == "GetCurrentTime" ||
        tool.Name == "LogError",
    
    // 保持相似度排序
    PreserveOriginalOrdering = false
};

// 构建完整的客户端管道
var productionClient = ChatClient.AsBuilder()
    //.UseLogging()  // 1. 日志记录
    .UseToolReduction(productionStrategy)  // 2. 工具削减
    .Use(async (messages, options, innerClient, cancellationToken) =>  // 3. 自定义监控
    {
        Console.WriteLine($"[生产环境] 发送 {options.Tools?.Count ?? 0} 个工具到模型");
        var sw = System.Diagnostics.Stopwatch.StartNew();
        var response = await innerClient.GetResponseAsync(messages, options, cancellationToken);
        sw.Stop();
        Console.WriteLine($"[生产环境] 响应耗时: {sw.ElapsedMilliseconds}ms");
        return response;
    },getStreamingResponseFunc: null)
    .UseFunctionInvocation()  // 4. 函数调用
    .Build();

Console.WriteLine("生产级 ChatClient 已配置");
Console.WriteLine(" 管道顺序:");
Console.WriteLine(" 1. UseLogging - 日志记录");
Console.WriteLine(" 2. UseToolReduction - 工具削减 (保留 8 个 + 必需工具)");
Console.WriteLine(" 3. 自定义监控 - 性能追踪");
Console.WriteLine(" 4. UseFunctionInvocation - 函数调用");

四、自定义IChatClient

1. 自定义中间件的应用场景

在实际应用中,我们经常需要对 AI 服务的调用进行增强和控制:

  • 限流控制:避免超出 API 调用频率限制,防止服务过载
  • 安全过滤:过滤敏感信息,实现内容审核
  • 日志记录:追踪和监控每次请求的详细信息
  • 重试机制:在网络不稳定或临时故障时自动重试

MEAI 提供的 DelegatingChatClient 基类,让我们可以轻松实现这些功能:

  • 简单易用:只需继承基类并重写需要的方法
  • 管道组合:多个中间件可以灵活串联
  • 标准化:遵循统一的 IChatClient 接口规范

2. 核心组件

  • DelegatingChatClient:抽象基类,用于创建委托式 ChatClient,自动转发调用到内部客户端。
  • GetResponseAsync:处理非流式响应的方法,可重写以添加自定义逻辑。
  • GetStreamingResponseAsync:处理流式响应的方法,返回 IAsyncEnumerable
  • Dispose:释放资源的方法,支持优雅的资源清理。
  • ChatClientBuilder.Use:扩展方法,用于简化中间件的注册和组合。

3. 实现限流中间件(Rate Limiting)

限流是保护 API 免受过载和控制成本的重要手段。接下来我们将使用 System.Threading.RateLimiting 库实现一个限流 ChatClient。

using Microsoft.Extensions.AI;
using System.Threading.RateLimiting;
using System.Runtime.CompilerServices;
using System.Threading;

/// <summary>
/// 限流 ChatClient,基于 DelegatingChatClient 实现
/// </summary>
public sealed class RateLimitingChatClient : DelegatingChatClient
{
    private readonly RateLimiter _rateLimiter;

    public RateLimitingChatClient(IChatClient innerClient, RateLimiter rateLimiter)
        : base(innerClient)
    {
        _rateLimiter = rateLimiter ?? throw new ArgumentNullException(nameof(rateLimiter));
    }

    // 重写非流式响应方法
    public override async Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        // 获取限流许可
        using var lease = await _rateLimiter.AcquireAsync(permitCount: 1, cancellationToken)
            .ConfigureAwait(false);
        
        if (!lease.IsAcquired)
        {
            throw new InvalidOperationException("无法获取限流许可,请求被拒绝");
        }

        Console.WriteLine($"[限流中间件] 获取许可,转发请求");

        // 转发到下一个客户端
        return await base.GetResponseAsync(messages, options, cancellationToken)
            .ConfigureAwait(false);
    }

    // 重写流式响应方法
    public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        // 获取限流许可
        using var lease = await _rateLimiter.AcquireAsync(permitCount: 1, cancellationToken)
            .ConfigureAwait(false);
        
        if (!lease.IsAcquired)
        {
            throw new InvalidOperationException("无法获取限流许可,请求被拒绝");
        }

        Console.WriteLine($"[限流中间件] 获取许可,转发流式请求");

        // 转发到下一个客户端
        await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken)
            .ConfigureAwait(false))
        {
            yield return update;
        }
    }

    // 释放资源
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _rateLimiter.Dispose();
        }
        base.Dispose(disposing);
    }
}

测试限流中间件:创建一个限流客户端并测试其效果。我们将使用并发限制器,限制同时只能有 1 个请求。

// 创建限流器:并发限制为 1,队列无限长
var rateLimiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions
{
    PermitLimit = 1,           // 同时只允许 1 个请求
    QueueLimit = int.MaxValue  // 队列无限长
});

// 创建限流客户端
var rateLimitedClient = new RateLimitingChatClient(ChatClient, rateLimiter);

Console.WriteLine("限流客户端已创建\n");

// 展示配置信息
new {
    客户端类型 = "RateLimitingChatClient",
    并发限制 = 1,
    队列长度 = "无限制"
}.Display();

// 测试:快速发送两个请求
var task1 = Task.Run(async () =>
{
    Console.WriteLine("\n[请求 1] 发送中...");
    var response = await rateLimitedClient.GetResponseAsync("用一句话介绍什么是 AI?");
    Console.WriteLine($"[请求 1] 完成: {response.Text?[..Math.Min(15, response.Text?.Length ?? 0)]}...");
});

var task2 = Task.Run(async () =>
{
    await Task.Delay(100); // 稍微延迟,确保请求 1 先开始
    Console.WriteLine("[请求 2] 发送中(应该被排队)...");
    var response = await rateLimitedClient.GetResponseAsync("用一句话介绍什么是 .NET?");
    Console.WriteLine($"[请求 2] 完成: {response.Text?[..Math.Min(15, response.Text?.Length ?? 0)]}...");
});

await Task.WhenAll(task1, task2);
Console.WriteLine("\n所有请求完成!注意请求 2 被排队直到请求 1 完成。");

4. 实现内容过滤中间件

在实际应用中,我们需要过滤敏感信息或不当内容。让我们创建一个内容过滤中间件。

using Microsoft.Extensions.AI;
using System.Runtime.CompilerServices;

/// <summary>
/// 内容过滤 ChatClient,过滤敏感词和不当内容
/// </summary>
public sealed class ContentFilteringChatClient : DelegatingChatClient
{
    private readonly HashSet<string> _sensitiveWords;

    public ContentFilteringChatClient(IChatClient innerClient, IEnumerable<string> sensitiveWords)
        : base(innerClient)
    {
        _sensitiveWords = new HashSet<string>(sensitiveWords, StringComparer.OrdinalIgnoreCase);
    }

    // 重写非流式响应方法
    public override async Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        // 检查输入消息是否包含敏感词
        var filteredMessages = FilterMessages(messages);

        Console.WriteLine($"[内容过滤] 检查了 {messages.Count()} 条消息");

        // 调用底层客户端
        var response = await base.GetResponseAsync(filteredMessages, options, cancellationToken)
            .ConfigureAwait(false);

        // 过滤响应内容
        FilterResponse(response);

        return response;
    }

    // 重写流式响应方法
    public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        // 检查输入消息
        var filteredMessages = FilterMessages(messages);

        Console.WriteLine($"[内容过滤] 检查了 {messages.Count()} 条消息(流式)");

        // 调用底层客户端并过滤流式内容
        await foreach (var update in base.GetStreamingResponseAsync(filteredMessages, options, cancellationToken)
            .ConfigureAwait(false))
        {
            // 过滤更新内容
            FilterUpdate(update);
            yield return update;
        }
    }

    // 过滤消息列表
    private List<ChatMessage> FilterMessages(IEnumerable<ChatMessage> messages)
    {
        var filtered = new List<ChatMessage>();

        foreach (var message in messages)
        {
            var text = message.Text;
            if (text != null && ContainsSensitiveWords(text))
            {
                Console.WriteLine($"[内容过滤] 检测到敏感词,已屏蔽");
                // 替换敏感词为 ***
                text = MaskSensitiveWords(text);
            }

            filtered.Add(new ChatMessage(message.Role, text));
        }

        return filtered;
    }

    // 过滤响应
    private void FilterResponse(ChatResponse response)
    {
        foreach (var message in response.Messages)
        {
            if (message.Text != null && ContainsSensitiveWords(message.Text))
            {
                Console.WriteLine($"[内容过滤] 响应包含敏感词,已屏蔽");
            }
        }
    }

    // 过滤流式更新
    private void FilterUpdate(ChatResponseUpdate update)
    {
        if (update.Text != null && ContainsSensitiveWords(update.Text))
        {
            Console.WriteLine($"[内容过滤] 流式响应包含敏感词");
        }
    }

    // 检查是否包含敏感词
    private bool ContainsSensitiveWords(string text)
    {
        return _sensitiveWords.Any(word => text.Contains(word, StringComparison.OrdinalIgnoreCase));
    }

    // 屏蔽敏感词
    private string MaskSensitiveWords(string text)
    {
        foreach (var word in _sensitiveWords)
        {
            text = System.Text.RegularExpressions.Regex.Replace(
                text, 
                word, 
                new string('*', word.Length), 
                System.Text.RegularExpressions.RegexOptions.IgnoreCase);
        }
        return text;
    }
}

5. 组合多个中间件

MEAI 的强大之处在于可以灵活组合多个中间件。让我们创建一个包含限流和内容过滤的完整管道。

// 定义敏感词列表
var sensitiveWords = new[] { "密码", "账号", "机密", "password", "secret" };

// 创建限流器
var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions
{
    PermitLimit = 2,
    QueueLimit = 10
});

// 组合中间件:先过滤内容,再限流
IChatClient composedClient = new ContentFilteringChatClient(
    new RateLimitingChatClient(chatClient, limiter),
    sensitiveWords
);

Console.WriteLine("组合客户端已创建");
Console.WriteLine("中间件管道: 内容过滤 -> 限流 -> 基础客户端\n");

// 展示配置信息
new {
    管道结构 = "ContentFilteringChatClient → RateLimitingChatClient → BaseClient",
    敏感词数量 = sensitiveWords.Length,
    并发限制 = 2,
    队列限制 = 10
}.Display();

// 测试:发送包含敏感词的消息
var testMessage = "请告诉我如何设置一个安全的密码和账号?";
Console.WriteLine($"\n测试消息: {testMessage}\n");

try
{
    var composedResponse = await composedClient.GetResponseAsync(testMessage);

    Console.WriteLine("\n响应详情:");
    new {
        回答 = composedResponse.Text?[..Math.Min(100, composedResponse.Text?.Length ?? 0)] + "...",
        Token使用 = composedResponse.Usage?.TotalTokenCount ?? 0,
        模型 = composedResponse.ModelId
    }.Display();
}
catch (Exception ex)
{
    Console.WriteLine("请求失败:");
    new { 错误类型 = ex.GetType().Name, 错误消息 = ex.Message }.Display();
}

6. 使用 ChatClientBuilder.Use 简化中间件注册

除了直接继承 DelegatingChatClient,MEAI 还提供了更简洁的方式来添加中间件功能。

using Microsoft.Extensions.AI;
using System.Diagnostics;

// 使用 ChatClientBuilder.Use 添加内联中间件
var enhancedClient = chatClient.AsBuilder()
    // 添加日志中间件
    .Use(
        getResponseFunc: async (messages, options, innerClient, cancellationToken) =>
        {
            Console.WriteLine($"[日志中间件] 收到 {messages.Count()} 条消息");
            var sw = Stopwatch.StartNew();
            
            var response = await innerClient.GetResponseAsync(messages, options, cancellationToken);
            
            sw.Stop();
            Console.WriteLine($"[日志中间件] 响应耗时: {sw.ElapsedMilliseconds}ms");
            Console.WriteLine($"[日志中间件] Token 使用: {response.Usage?.TotalTokenCount ?? 0}");
            
            return response;
        },
        getStreamingResponseFunc: null)
    // 添加简单的重试中间件
    .Use(
        getResponseFunc: async (messages, options, innerClient, cancellationToken) =>
        {
            const int maxRetries = 3;
            int attempt = 0;
            
            while (true)
            {
                try
                {
                    attempt++;
                    if (attempt > 1)
                        Console.WriteLine($"[重试中间件] 第 {attempt} 次尝试");
                    
                    return await innerClient.GetResponseAsync(messages, options, cancellationToken);
                }
                catch (Exception ex) when (attempt < maxRetries)
                {
                    Console.WriteLine($"[重试中间件] 失败: {ex.Message},准备重试...");
                    await Task.Delay(1000 * attempt); // 指数退避
                }
            }
        },
        getStreamingResponseFunc: null)
    .Build();

Console.WriteLine("增强客户端已创建(日志 + 重试)\n");

// 展示管道配置
new {
    管道结构 = "日志中间件 → 重试中间件 → 基础客户端",
    最大重试次数 = 3,
    重试策略 = "指数退避"
}.Display();

// 测试
try
{
    var testResponse = await enhancedClient.GetResponseAsync("什么是 Microsoft.Extensions.AI?");

    Console.WriteLine("\n响应详情:");
    new {
        回答 = testResponse.Text?[..Math.Min(100, testResponse.Text?.Length ?? 0)] + "...",
        Token使用 = testResponse.Usage?.TotalTokenCount ?? 0,
        模型 = testResponse.ModelId
    }.Display();
}
catch (Exception ex)
{
    Console.WriteLine("请求失败:");
    new { 错误类型 = ex.GetType().Name, 错误消息 = ex.Message }.Display();
}

7. 创建可复用的扩展方法

为了让中间件更易于使用,我们创建扩展方法。这是 MEAI 推荐的最佳实践

/// <summary>
/// 添加限流功能
/// </summary>
public static ChatClientBuilder UseRateLimiting(this ChatClientBuilder builder, RateLimiter rateLimiter)
{
    return builder.Use(innerClient => new RateLimitingChatClient(innerClient, rateLimiter));
}

/// <summary>
/// 添加内容过滤功能
/// </summary>
public static ChatClientBuilder UseContentFiltering(this ChatClientBuilder builder, IEnumerable<string> sensitiveWords)
{
    return builder.Use(innerClient => new ContentFilteringChatClient(innerClient, sensitiveWords));
}

/// <summary>
/// 添加性能监控功能
/// </summary>
public static ChatClientBuilder UsePerformanceMonitoring(this ChatClientBuilder builder)
{
    return builder.Use(async (IEnumerable<ChatMessage> messages, ChatOptions? options, IChatClient innerClient, CancellationToken cancellationToken) =>
        {
            var sw = Stopwatch.StartNew();
            var messageCount = messages.Count();

            Console.WriteLine($"[性能监控] 开始请求 ({messageCount} 条消息)");

            var response = await innerClient.GetResponseAsync(messages, options, cancellationToken);

            sw.Stop();
            Console.WriteLine($"[性能监控] 完成: {sw.ElapsedMilliseconds}ms | Tokens: {response.Usage?.TotalTokenCount ?? 0}");

            return response;
        },
        getStreamingResponseFunc: null);
}	

8. 使用扩展方法构建完整管道

// 使用扩展方法构建管道
var productionClient = chatClient.AsBuilder()
    .UsePerformanceMonitoring()                             // 性能监控
    .UseContentFiltering(new[] { "密码", "账号", "机密" })    // 内容过滤
    .UseRateLimiting(new ConcurrencyLimiter(new()           // 限流
    {
        PermitLimit = 3,
        QueueLimit = 20
    }))
    .Build();

Console.WriteLine("生产级客户端已构建");
Console.WriteLine("中间件管道: 性能监控 → 内容过滤 → 限流 → 基础客户端\n");

// 展示配置信息
new {
    管道层级 = 4,
    中间件 = new[] { "性能监控", "内容过滤", "限流", "基础客户端" },
    并发限制 = 3,
    队列限制 = 20,
    敏感词过滤 = true
}.Display();

// 测试完整管道
var questions = new[]
{
    "什么是 Microsoft.Extensions.AI?",
    "如何保护我的账号密码?",  // 包含敏感词
    "MEAI 有哪些核心接口?"
};

foreach (var question in questions)
{
    Console.WriteLine($"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    Console.WriteLine($"问题: {question}");
    Console.WriteLine($"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    
    try
    {
        var response = await productionClient.GetResponseAsync(question);
        
        new {
            回答 = response.Text?[..Math.Min(80, response.Text?.Length ?? 0)] + "...",
            Token使用 = response.Usage?.TotalTokenCount ?? 0
        }.Display();
    }
    catch (Exception ex)
    {
        Console.WriteLine("请求失败:");
        new { 错误类型 = ex.GetType().Name, 错误消息 = ex.Message }.Display();
    }
}

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("所有请求完成!中间件按顺序处理每个请求。");

9. 高级场景与最佳实践

中间件执行顺序

  • 监控和日志应该在最外层(记录所有操作)

  • 安全过滤应该在中间层(在消耗资源前过滤)

  • 限流和缓存应该靠近内层(避免不必要的处理)

处理流式和非流式响应的差异

  • 非流式响应(GetResponseAsync):

    • 返回完整的 ChatResponse 对象

    • 可以直接访问完整内容

    • 适合批量处理和分析

  • 流式响应(GetStreamingResponseAsync):

    • 返回 IAsyncEnumerable

    • 需要逐个处理更新

    • 适合实时展示和降低首字延迟

    示例:统一处理两种模式

public override async Task<ChatResponse> GetResponseAsync(...)
{
    // 处理完整响应
    var response = await base.GetResponseAsync(messages, options, cancellationToken);
    // 对完整内容进行处理
    ProcessFullContent(response.Text);
    return response;
}

public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(...)
{
    StringBuilder accumulated = new();
    await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken))
    {
        // 累积内容
        accumulated.Append(update.Text);
        yield return update;
    }
    // 流结束后处理完整内容
    ProcessFullContent(accumulated.ToString());
}
  • 资源管理和生命周期:自定义中间件可能会持有资源(如限流器、缓存连接等),需要正确实现资源释放:
public sealed class MyCustomChatClient : DelegatingChatClient
{
    private readonly IDisposable _resource;
    
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _resource?.Dispose();
        }
        base.Dispose(disposing);
    }
}

/*
最佳实践:
- 始终调用 `base.Dispose(disposing)`
- 在 `disposing == true` 时释放托管资源
- 考虑使用 `using` 语句确保释放
*/
  • 与依赖注入(DI)集成
builder.Services.AddSingleton<RateLimiter>(_ => 
    new ConcurrencyLimiter(new() { PermitLimit = 10, QueueLimit = 100 }));

builder.Services.AddChatClient(services =>
{
    return ChatClientFactory.GetQwenClient()
        .AsBuilder()
        .UsePerformanceMonitoring()
        .UseContentFiltering(new[] { "敏感词" })
        .UseRateLimiting(services.GetRequiredService<RateLimiter>())
        .Build(services);
});

// 在其他服务中注入使用
public class MyService
{
    private readonly IChatClient _chatClient;
    
    public MyService(IChatClient chatClient)
    {
        _chatClient = chatClient;
    }
}

10. 企业级应用示例

using Microsoft.Extensions.AI;
using System.Threading.RateLimiting;
using System.Diagnostics;

// 模拟企业级场景:构建一个包含多层保护的 ChatClient
// 1. 获取基础客户端
var enterpriseBaseClient = ChatClientFactory.GetQwenClient();

// 2. 构建企业级管道
var enterpriseClient = enterpriseBaseClient.AsBuilder()
    // 第一层:审计日志(记录所有请求)
    .Use(
        getResponseFunc: async (IEnumerable<ChatMessage> messages, ChatOptions? options, IChatClient innerClient, CancellationToken cancellationToken) =>
            {
                var sw = Stopwatch.StartNew();
                var requestId = Guid.NewGuid().ToString("N")[..8];
                Console.WriteLine($"\n[审计] 请求ID: {requestId} | 消息数: {messages.Count()}");
                
                var response = await innerClient.GetResponseAsync(messages, options, cancellationToken);
                
                sw.Stop();
                Console.WriteLine($"[审计] 请求ID: {requestId} | 耗时: {sw.ElapsedMilliseconds}ms | Tokens: {response.Usage?.TotalTokenCount ?? 0}");
                return response;
            },
        getStreamingResponseFunc: null
    )
    // 第二层:内容安全过滤
    .UseContentFiltering(new[] { "密码", "账号", "机密", "私钥", "token" })
    // 第三层:限流保护(每秒最多 2 个请求)
    .UseRateLimiting(new ConcurrencyLimiter(new()
    {
        PermitLimit = 2,
        QueueLimit = 50
    }))
    // 第四层:性能监控
    .UsePerformanceMonitoring()
    .Build();

Console.WriteLine("企业级 ChatClient 管道已构建");
Console.WriteLine("管道层次: 审计 → 内容过滤 → 限流 → 性能监控 → 基础客户端\n");

// 展示企业级配置
new {
    管道名称 = "企业级 AI 助手系统",
    安全层级 = 4,
    中间件 = new[] { "审计日志", "内容过滤", "限流保护", "性能监控" },
    并发限制 = 2,
    队列容量 = 50,
    敏感词数量 = 5
}.Display();

// 3. 模拟企业应用场景
var enterpriseQuestions = new[]
{
    new { User = "张三", Question = "什么是 MEAI?" },
    new { User = "李四", Question = "如何保护我的账号密码?" },
    new { User = "王五", Question = "MEAI 支持哪些 AI 服务?" }
};

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("             企业级 AI 助手系统测试             ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

foreach (var item in enterpriseQuestions)
{
    Console.WriteLine($"用户: {item.User}");
    Console.WriteLine($"问题: {item.Question}");
    
    try
    {
        var response = await enterpriseClient.GetResponseAsync(item.Question);
        
        new {
            用户 = item.User,
            回答 = response.Text?[..Math.Min(60, response.Text?.Length ?? 0)] + "...",
            Token使用 = response.Usage?.TotalTokenCount ?? 0,
            状态 = "成功"
        }.Display();
        
        Console.WriteLine();
    }
    catch (Exception ex)
    {
        new {
            用户 = item.User,
            错误 = ex.Message,
            状态 = "失败"
        }.Display();
        
        Console.WriteLine();
    }
    
    await Task.Delay(300); // 模拟用户间隔
}

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("所有请求已处理完成");
Console.WriteLine("中间件成功过滤了包含敏感词的请求");
Console.WriteLine("限流机制确保了系统稳定性");

五、Custom ChatClient Vs FunctionInvokingChatClient

在 Microsoft.Extensions.AI(MEAI)的生态中,自定义 ChatClient 中间件和 FunctionInvokingChatClient 是两种不同的扩展机制,它们各有特点和适用场景。

维度 自定义 ChatClient 中间件 FunctionInvokingChatClient
核心目的 通用请求/响应处理 专门处理函数调用(Function Calling)
实现方式 继承 DelegatingChatClient 或使用 ChatClientBuilder.Use 调用 UseFunctionInvocation()
作用范围 拦截所有请求/响应 仅处理涉及工具调用的请求
典型用途 限流、日志、缓存、安全过滤、重试 自动执行函数调用循环、管理工具调用
复杂度 需要手动实现流式/非流式逻辑 框架自动处理复杂的函数调用流程
配置灵活性 完全自定义 提供预定义配置选项
与函数调用的关系 不涉及函数调用逻辑 专门为函数调用设计
可组合性 可以任意组合多个中间件 可与其他中间件组合,但只能有一个

1. 设计目的与定位

自定义 ChatClient 中间件

  • 设计初衷: 提供一个通用的请求/响应拦截机制,让开发者可以在 AI 调用的任意阶段插入自定义逻辑。

  • 核心特点:

    • 横切关注点:处理与业务逻辑无关的基础设施需求

    • 通用性:适用于所有类型的 ChatClient 调用

    • 无侵入:不改变原有请求/响应的语义

    • 链式组合:可以串联多个中间件形成处理管道

  • 典型应用场景:限流控制 → 日志记录 → 性能监控 → 安全过滤 → 缓存 → 重试机制

FunctionInvokingChatClient

  • 设计初衷:自动化处理 AI 模型的函数调用(Tool/Function Calling)流程,解放开发者手动管理函数调用循环的负担。

  • 核心特点:

    • 专用性:专门为函数调用场景设计

    • 自动化:自动处理"请求 → 函数调用 → 结果回传 → 最终响应"的循环

    • 状态管理:自动维护对话历史和函数调用上下文

    • 防护机制:内置防无限循环、错误控制等保护措施

  • 处理流程:

用户消息 → AI 响应(请求调用函数) → 执行函数 → AI 响应(基于结果) → 最终答案
         ↑__________________________________________________|
                    (自动迭代,直到 AI 不再请求函数调用)

2. 技术实现方式对比

自定义 ChatClient 中间件的实现,有两种主要实现方式:

  • 方式 1:继承 DelegatingChatClient

    特点:

    • 完全控制,可以访问所有请求/响应细节
    • 需要同时处理流式和非流式两种模式
    • 适合复杂的、有状态的中间件
  • 方式 2:使用 ChatClientBuilder.Use

    特点:

    • 简洁直观,适合简单的拦截逻辑
    • 可以只实现非流式或只实现流式
    • 适合快速原型和简单场景
  • FunctionInvokingChatClient 的实现:通过 UseFunctionInvocation() 扩展方法注册

    特点:

    • 声明式配置,不需要编写循环逻辑
    • 框架自动处理函数调用流程
    • 通过 FunctionInvoker 可以插入自定义逻辑
    • 内置保护机制(防无限循环、错误控制)
var client = chatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        // 配置函数调用行为
        options.AdditionalTools = [timeTool, logTool];              // 全局工具
        options.AllowConcurrentInvocation = true;                   // 并发执行
        options.MaximumIterationsPerRequest = 10;                   // 最大迭代次数
        options.MaximumConsecutiveErrorsPerRequest = 3;             // 最大连续错误
        options.IncludeDetailedErrors = false;                      // 是否包含详细错误
        options.TerminateOnUnknownCalls = false;                    // 未知函数时是否终止
        
        // 自定义函数执行器(可选)
        options.FunctionInvoker = async (context, cancellationToken) =>
        {
            Console.WriteLine($"调用函数: {context.Function.Name}");
            return await context.Function.InvokeAsync(context.Arguments, cancellationToken);
        };
    })
    .Build();

3. 责任边界与职责对比

自定义 ChatClient 中间件的职责

职责类型 说明 示例
请求预处理 在发送给 AI 前修改或验证请求 内容过滤、参数验证、消息格式化
响应后处理 在返回给调用方前修改或增强响应 敏感信息脱敏、格式转换
横切关注点 与业务逻辑无关的基础设施功能 日志、监控、性能追踪、审计
资源控制 管理和保护系统资源 限流、超时控制、并发限制
容错增强 提高系统可靠性 重试、降级、熔断
数据增强 添加额外的元数据或上下文 添加时间戳、用户信息、追踪ID

核心原则: 不改变请求/响应的业务语义,只增强非功能性需求。

FunctionInvokingChatClient 的职责

职责类型 说明 工作方式
函数调用检测 识别 AI 响应中的函数调用请求 检查 FunctionCallContent
参数解析 将 AI 生成的 JSON 参数转换为函数参数 使用 ToolArguments
函数执行 调用实际的工具函数 通过 AIFunction.InvokeAsync
结果封装 将函数执行结果封装为 FunctionResultContent 添加到对话历史
循环控制 管理多轮函数调用迭代 直到 AI 不再请求函数调用
错误处理 处理函数执行失败的情况 根据配置重试或终止
并发管理 协调多个函数的并发执行 根据 AllowConcurrentInvocation 配置

核心原则: 专注于函数调用的自动化流程管理,不涉及其他请求/响应处理。

4. 代码实现复杂度对比

假设我们要实现一个支持函数调用的 ChatClient,让我们看看两种方式的复杂度差异。

  • 使用自定义中间件(手动实现)

    问题:

    • 需要 ~60 行代码实现基本功能
    • 需要手动管理对话历史
    • 需要自己处理错误、并发、防无限循环
    • 流式响应的处理会更加复杂
    • 没有内置的保护机制
// 不推荐:需要手动管理整个函数调用流程
public class ManualFunctionCallingClient : DelegatingChatClient
{
    private readonly IList<AITool> _tools;
    
    public ManualFunctionCallingClient(IChatClient innerClient, IList<AITool> tools)
        : base(innerClient) => _tools = tools;

    public override async Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        var messageList = messages.ToList();
        var maxIterations = 10;
        var iteration = 0;

        while (iteration++ < maxIterations)
        {
            // 1. 调用 AI 模型
            var response = await base.GetResponseAsync(messageList, options, cancellationToken);
            
            // 2. 检查是否有函数调用请求
            var functionCalls = response.Message.Contents.OfType<FunctionCallContent>().ToList();
            if (functionCalls.Count == 0)
            {
                // 没有函数调用,返回最终结果
                return response;
            }
            
            // 3. 执行每个函数调用
            foreach (var functionCall in functionCalls)
            {
                // 查找对应的工具
                var tool = _tools.FirstOrDefault(t => t.Name == functionCall.Name);
                if (tool == null)
                {
                    // 处理未知函数
                    messageList.Add(new ChatMessage(
                        ChatRole.Tool,
                        new FunctionResultContent(functionCall.CallId, "未知函数")
                    ));
                    continue;
                }
                
                try
                {
                    // 解析参数
                    var arguments = JsonSerializer.Deserialize<Dictionary<string, object>>(
                        functionCall.Arguments ?? "{}"
                    );
                    
                    // 执行函数
                    var result = await ((AIFunction)tool).InvokeAsync(arguments, cancellationToken);
                    
                    // 添加结果到对话历史
                    messageList.Add(new ChatMessage(
                        ChatRole.Tool,
                        new FunctionResultContent(functionCall.CallId, result?.ToString())
                    ));
                }
                catch (Exception ex)
                {
                    // 错误处理
                    messageList.Add(new ChatMessage(
                        ChatRole.Tool,
                        new FunctionResultContent(functionCall.CallId, $"执行失败: {ex.Message}")
                    ));
                }
            }
            
            // 4. 将函数调用的消息添加到历史
            messageList.Add(response.Message);
        }
        
        throw new InvalidOperationException("达到最大迭代次数");
    }
}

// 使用
var manualClient = new ManualFunctionCallingClient(baseClient, tools);
  • 使用 FunctionInvokingChatClient(自动化)

    优势:

    • 只需 ~10 行代码
    • 框架自动管理对话历史
    • 内置错误处理、并发控制、防无限循环
    • 流式和非流式响应自动支持
    • 生产级的保护机制
// 推荐:框架自动处理所有细节
var autoClient = ChatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.MaximumIterationsPerRequest = 10;
        options.AllowConcurrentInvocation = true;
    })
    .Build();

// 使用时只需提供工具
var response = await autoClient.GetResponseAsync(
    "帮我查询天气",
    new ChatOptions
    {
        ToolMode = ChatToolMode.Auto,
        Tools = tools
    }
);

结论:对于函数调用场景,FunctionInvokingChatClient 将复杂度降低了约 85%

5. 在管道中的位置与组合

在实际应用中,通常会组合多个中间件形成处理管道。以下是推荐的架构:

var productionClient = ChatClient.AsBuilder()
    // 最外层:审计和日志(记录所有活动)
    .Use(/* 审计日志中间件 */)
    // 安全层:内容过滤、权限验证
    .UseContentFiltering(sensitiveWords)
    // 缓存层:避免重复请求
    .UseDistributedCache(cache)
    // 消息管理:压缩历史对话
    .UseChatReducer(reducer)
    // 函数调用层:处理工具调用(核心业务逻辑)
    .UseFunctionInvocation(configure: options =>
    {
        options.AdditionalTools = systemTools;
        options.AllowConcurrentInvocation = true;
    })
    // 限流层:保护 API 资源
    .UseRateLimiting(rateLimiter)
    // 最内层:重试机制(处理底层 API 失败)
    .UseRetry()
    .Build();

组合规则与最佳实践

规则 说明 示例
FunctionInvokingChatClient 唯一性 一个管道中只能有一个函数调用中间件 一个 UseFunctionInvocation()
多个函数调用中间件(不建议)
顺序很重要 中间件的执行顺序会影响最终行为 缓存应在函数调用前,避免缓存未完成的结果
自定义中间件可重复 可以注册多个自定义中间件 多个日志、多个过滤器
外层应无状态 外层中间件应该是无状态的 日志、监控适合外层;缓存适合靠近内层

6. 配置灵活性与扩展性对比

自定义 ChatClient 中间件的扩展方式:灵活性:⭐⭐⭐⭐⭐ (完全自定义)

扩展场景:

  • 请求/响应内容转换
  • 动态修改 ChatOptions
  • 自定义缓存策略
  • 实现复杂的条件逻辑
  • 集成第三方服务(如翻译、审核)
// 1. 完全控制:可以访问和修改所有请求/响应细节
public class CustomMiddleware : DelegatingChatClient
{
    public override async Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        // 可以修改 messages
        var modifiedMessages = messages.Select(m => 
            new ChatMessage(m.Role, $"[Prefix] {m.Text}")
        );
        
        // 可以修改 options
        var modifiedOptions = new ChatOptions
        {
            Temperature = options?.Temperature * 0.8f,
            MaxOutputTokens = 500
        };
        
        // 可以完全自定义响应
        var response = await base.GetResponseAsync(
            modifiedMessages, 
            modifiedOptions, 
            cancellationToken
        );
        
        // 可以修改返回的响应
        return new ChatResponse(
            response.Messages.Select(m => 
                new ChatMessage(m.Role, m.Text?.ToUpper())
            )
        );
    }
}

FunctionInvokingChatClient 的扩展方式:灵活性:⭐⭐⭐⭐ (预定义配置 + 部分自定义)

/* 扩展场景(通过 FunctionInvoker):

  • 函数调用前后的拦截
  • 权限验证
  • 日志和监控
  • 结果缓存
  • 错误处理和重试
  • 无法修改函数调用的整体流程(如迭代逻辑)
  • 无法拦截非函数调用的请求 */
// 通过配置选项和 FunctionInvoker 扩展
var client = ChatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        // 1. 预定义配置选项
        options.AdditionalTools = [timeTool, logTool];
        options.AllowConcurrentInvocation = true;
        options.MaximumIterationsPerRequest = 10;
        options.MaximumConsecutiveErrorsPerRequest = 3;
        options.IncludeDetailedErrors = false;
        options.TerminateOnUnknownCalls = false;
        
        // 2. 自定义函数执行器(插入点)
        options.FunctionInvoker = async (context, cancellationToken) =>
        {
            // 前置处理
            ValidatePermissions(context.Function.Name);
            LogFunctionCall(context);
            
            // 执行函数
            var result = await context.Function.InvokeAsync(
                context.Arguments, 
                cancellationToken
            );
            
            // 后置处理
            CacheResult(context, result);
            AuditResult(context, result);
            
            return result;
        };
    })
    .Build();

对比总结

扩展维度 自定义中间件 FunctionInvokingChatClient
修改请求消息 完全自由 不支持(框架管理)
修改 ChatOptions 完全自由 部分支持(只能在工具相关配置)
修改响应内容 完全自由 不支持(框架管理)
拦截函数调用 需要手动实现 通过 FunctionInvoker
控制迭代流程 完全控制 只能配置次数,不能改变逻辑
处理流式响应 完全控制 自动处理
实现难度 ⭐⭐⭐⭐ 较高 ⭐⭐ 简单

7. 典型使用场景分类

何时使用自定义 ChatClient 中间件?

  • 推荐场景

    • 限流和速率控制
    • 日志和审计
    • 内容安全过滤
    • 缓存策略
    • 重试和容错
    • 请求/响应转换
  • 不推荐场景

    • 函数调用处理

    • 工具执行管理

    • 函数调用循环控制

何时使用 FunctionInvokingChatClient?

  • 必须使用场景

    • AI Agent 开发

      • AI 需要调用外部工具
      • 需要执行业务逻辑函数
      • 构建智能助手
    • 工具集成

      • 集成数据库查询
      • 调用外部 API

      • 执行计算任务

    • 多步骤任务编排

      • AI 需要多次函数调用完成任务

      • 函数调用之间有依赖关系

      • 需要自动化的迭代流程
    • 动态决策系统

      • AI 根据上下文决定调用哪些工具
      • 工具调用结果影响后续决策
      • 需要灵活的工具选择
  • 配合使用场景

// 同时使用两者,各司其职
var productionClient = chatClient.AsBuilder()
    // 自定义中间件:基础设施
    .Use(/* 日志中间件 */)
    .UseContentFiltering(sensitiveWords)
    .UseDistributedCache(cache)
    
    // 函数调用中间件:业务逻辑
    .UseFunctionInvocation(configure: options =>
    {
        options.AdditionalTools = [timeTool, weatherTool];
        options.AllowConcurrentInvocation = true;
    })
    
    // 自定义中间件:资源保护
    .UseRateLimiting(rateLimiter)
    .UseRetry()
    .Build();
  • 场景决策树
需要处理函数调用吗?
│
├─ 是 → 使用 FunctionInvokingChatClient
│      │
│      └─ 需要额外的基础设施功能吗?
│         │
│         ├─ 是 → 组合自定义中间件 + FunctionInvokingChatClient
│         └─ 否 → 仅使用 FunctionInvokingChatClient
│
└─ 否 → 使用自定义 ChatClient 中间件
       │
       └─ 根据需求选择:
          - 限流
          - 日志
          - 缓存
          - 安全过滤
          - 重试
          等等...

8. 实战示例:同时使用两者

场景:构建企业级智能客服系统

需求:

  • 安全层:过滤敏感词(自定义中间件)
  • 监控层:记录所有请求和函数调用(自定义中间件)
  • 业务层:查询订单、用户信息等(FunctionInvokingChatClient)
  • 保护层:限流保护(自定义中间件)
// 步骤 1:定义业务工具(函数调用使用)
public record OrderInfo(string OrderId, string Status, decimal Amount, DateTime OrderDate);
public record UserInfo(string UserId, string Name, string Tier);

[Description("查询订单状态和详情")]
string QueryOrder(string orderId)
{
    Thread.Sleep(300); // 模拟数据库查询
    var order = new OrderInfo(
        orderId, 
        "已发货", 
        Random.Shared.Next(100, 1000), 
        DateTime.Now.AddDays(-2)
    );
    return $"订单 {order.OrderId}: {order.Status},金额 ¥{order.Amount},下单时间 {order.OrderDate:yyyy-MM-dd}";
}

[Description("查询用户会员等级和信息")]
string QueryUser(string userId)
{
    Thread.Sleep(200); // 模拟数据库查询
    var user = new UserInfo(userId, "张三", Random.Shared.NextDouble() > 0.5 ? "VIP" : "普通会员");
    return $"用户 {user.UserId} ({user.Name}): {user.Tier}";
}

[Description("获取当前时间")]
string GetCurrentTime()
{
    return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}

// 注册业务工具
var businessTools = new[]
{
    AIFunctionFactory.Create(QueryOrder),
    AIFunctionFactory.Create(QueryUser),
    AIFunctionFactory.Create(GetCurrentTime)
};

Console.WriteLine("业务工具已定义:");
foreach (var tool in businessTools)
{
    Console.WriteLine($"   - {tool.Name}: {tool.Description}");
}

// 步骤 2:构建组合式客户端管道
var requestCounter = 0;
var enterpriseClient = chatClient.AsBuilder()
    // 【层 1】审计日志中间件(自定义)
    .Use(
        getResponseFunc: async (messages, options, innerClient, cancellationToken) =>
        {
            var requestId = ++requestCounter;
            Console.WriteLine($"\n[请求 #{requestId}] 开始处理");
            Console.WriteLine($"   消息数: {messages.Count()}");
            
            var sw = Stopwatch.StartNew();
            var response = await innerClient.GetResponseAsync(messages, options, cancellationToken);
            sw.Stop();
            
            Console.WriteLine($"[请求 #{requestId}] 完成,耗时: {sw.ElapsedMilliseconds}ms");
            return response;
        },
        getStreamingResponseFunc: null
    )
    // 【层 2】内容安全过滤中间件(自定义)
    .Use(async (messages, options, innerClient, cancellationToken) =>
        {
            var sensitiveWords = new[] { "密码", "账号", "私钥", "token" };
            var hasViolation = messages.Any(m => 
                sensitiveWords.Any(word => m.Text?.Contains(word, StringComparison.OrdinalIgnoreCase) ?? false)
            );
            
            if (hasViolation)
            {
                Console.WriteLine("   [安全过滤] 检测到敏感词");
            }
            
            return await innerClient.GetResponseAsync(messages, options, cancellationToken);
        },
        getStreamingResponseFunc: null
    )
    // 【层 3】函数调用处理层(FunctionInvokingChatClient)
    .UseFunctionInvocation(configure: options =>
    {
        options.AllowConcurrentInvocation = true;
        options.MaximumIterationsPerRequest = 5;
        
        // 通过 FunctionInvoker 监控函数调用
        options.FunctionInvoker = async (context, cancellationToken) =>
        {
            Console.WriteLine($"   [函数调用] {context.Function.Name}");
            var sw = Stopwatch.StartNew();
            
            var result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);
            sw.Stop();
            
            Console.WriteLine($"   [函数结果] {result} (耗时: {sw.ElapsedMilliseconds}ms)");
            return result;
        };
    })  
    // 【层 4】限流保护中间件(自定义)
    .Use(async (messages, options, innerClient, cancellationToken) =>
        {
            // 简化的限流检查
            Console.WriteLine($"   ⏱️ [限流检查] 通过");
            return await innerClient.GetResponseAsync(messages, options, cancellationToken);
        },
        getStreamingResponseFunc: null
    )
    .Build();

// 步骤 3:测试完整流程
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试场景 1: 查询订单(会触发函数调用)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

var scenario1 = await enterpriseClient.GetResponseAsync(
    "帮我查询订单 ORD-12345 的状态",
    new ChatOptions
    {
        ToolMode = ChatToolMode.Auto,
        Tools = businessTools
    }
);

Console.WriteLine($"\nAI 回复: {scenario1.Text}\n");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试场景 2: 复杂查询(会触发多个函数调用)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

var scenario2 = await enterpriseClient.GetResponseAsync(
    "查询用户 USR-001 的信息,以及他的订单 ORD-67890 的状态,并告诉我现在几点",
    new ChatOptions
    {
        ToolMode = ChatToolMode.Auto,
        AllowMultipleToolCalls = true,
        Tools = businessTools
    }
);

Console.WriteLine($"\nAI 回复: {scenario2.Text}\n");

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试场景 3: 安全过滤测试(包含敏感词)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

var scenario3 = await enterpriseClient.GetResponseAsync(
    "请帮我重置密码", // 包含敏感词
    new ChatOptions
    {
        ToolMode = ChatToolMode.Auto,
        Tools = businessTools
    }
);

Console.WriteLine($"\nAI 回复: {scenario3.Text}\n");

9. 核心区别总结表

  • 功能对比
对比维度 自定义 ChatClient 中间件 FunctionInvokingChatClient
核心目的 处理请求/响应的通用逻辑 专门处理 AI 函数调用流程
设计模式 装饰器模式 / 拦截器模式 专用中间件 + 循环控制
实现复杂度 ⭐⭐⭐ 中等(需要处理流式/非流式) ⭐ 简单(声明式配置)
代码量 ~30-60 行(简单场景) ~5-10 行(配置)
状态管理 开发者负责 框架自动管理
错误处理 手动实现 内置机制
并发控制 需要自己实现 配置 AllowConcurrentInvocation
防无限循环 需要自己实现 内置 MaximumIterationsPerRequest
可复用性 高(可用于任何场景) 仅限函数调用场景
组合性 可以有多个 管道中只能有一个
  • 责任分工
责任 自定义中间件 FunctionInvokingChatClient
请求预处理 核心职责 不涉及
响应后处理 核心职责 不涉及
函数调用检测 需手动实现 自动处理
函数参数解析 需手动实现 自动处理
函数执行 需手动实现 自动处理
迭代控制 需手动实现 自动处理
日志审计 核心职责 通过 FunctionInvoker
限流缓存 核心职责 不涉及
安全过滤 核心职责 不涉及
  • 使用场景对比
场景 推荐方案 原因
实现日志记录 自定义中间件 通用基础设施功能
实现限流控制 自定义中间件 与函数调用无关
实现内容过滤 自定义中间件 需要拦截所有请求
实现请求重试 自定义中间件 底层容错机制
实现缓存策略 自定义中间件 通用性能优化
AI Agent 开发 FunctionInvokingChatClient 专为函数调用设计
工具集成 FunctionInvokingChatClient 需要自动化流程
多步骤任务 FunctionInvokingChatClient 需要迭代管理
企业级系统 两者组合 各司其职,协同工作
  • 性能影响对比
维度 自定义中间件 FunctionInvokingChatClient
额外开销 很小(仅拦截逻辑) 中等(需要多次 AI 调用)
延迟来源 中间件内部逻辑 函数执行 + AI 迭代
优化空间 优化中间件逻辑 并发调用、减少迭代
Token 消耗 不增加 显著增加(多轮对话)
  • 错误处理对比
错误类型 自定义中间件 FunctionInvokingChatClient
网络错误 手动捕获和处理 透传或配置重试
函数执行错误 不涉及 自动处理,可配置行为
参数验证错误 可以在拦截时处理 函数内部或 FunctionInvoker
超时控制 手动实现 依赖 CancellationToken
错误重试 可以实现重试逻辑 通过 MaximumConsecutiveErrorsPerRequest

10. 最佳实践建议

  • 推荐做法
    • 分层架构
    • 职责单一
      • 每个自定义中间件只负责一件事
      • FunctionInvokingChatClient 只负责函数调用
    • 配置外部化
    • 使用 FunctionInvoker 扩展
    • 错误处理要完善
// 分层架构
   chatClient.AsBuilder()
       // 外层:基础设施(自定义中间件)
       .UseLogging()
       .UseContentFiltering()
       .UseCache()
       
       // 核心层:业务逻辑(FunctionInvokingChatClient)
       .UseFunctionInvocation()
       
       // 内层:资源保护(自定义中间件)
       .UseRateLimiting()
       .UseRetry()
       .Build();

// 配置外部化
   // 好:配置来自外部
   .UseFunctionInvocation(configure: options =>
   {
       options.MaximumIterationsPerRequest = configuration.GetValue<int>("AI:MaxIterations");
   })
   // 差:硬编码
   .UseFunctionInvocation(configure: options =>
   {
       options.MaximumIterationsPerRequest = 10;
   })

// 使用 FunctionInvoker 扩展
   // 好:在 FunctionInvoker 中添加横切逻辑
   options.FunctionInvoker = async (context, ct) =>
   {
       LogFunctionCall(context.Function.Name);
       ValidatePermissions(context);
       return await context.Function.InvokeAsync(context.Arguments, ct);
   };

// 错误处理要完善
   // 好:完善的错误处理
   .Use(async (messages, options, innerClient, ct) =>
   {
       try
       {
           return await innerClient.GetResponseAsync(messages, options, ct);
       }
       catch (Exception ex)
       {
           logger.LogError(ex, "Request failed");
           throw; // 或返回降级响应
       }
   })
  • 避免的陷阱
    • 不要在自定义中间件中实现函数调用逻辑
    • 不要注册多个 FunctionInvokingChatClient
    • 不要在 FunctionInvoker 中处理非函数相关逻辑
    • 不要忽略流式响应
    • 不要在中间件中改变语义
//1. 不要在自定义中间件中实现函数调用逻辑
   // 错误:重复造轮子
   public class BadFunctionCallingMiddleware : DelegatingChatClient
   {
       // 手动实现函数调用循环...
   }
   
   // 正确:使用框架提供的
   .UseFunctionInvocation()

// 不要注册多个 FunctionInvokingChatClient
   // 错误:会导致嵌套循环
   .UseFunctionInvocation()
   .UseFunctionInvocation() // 第二个会被忽略或导致错误
   
   // 正确:只注册一次
   .UseFunctionInvocation()

// 不要在 FunctionInvoker 中处理非函数相关逻辑
   // 错误:混淆职责
   options.FunctionInvoker = async (context, ct) =>
   {
       // 限流应该在自定义中间件中处理
       await rateLimiter.WaitAsync(ct);
       return await context.Function.InvokeAsync(context.Arguments, ct);
   };
   // 正确:在外层中间件处理
   .UseRateLimiting(rateLimiter)
   .UseFunctionInvocation()
  

// 不要忽略流式响应
   // 错误:只实现非流式
   .Use(
       getResponseFunc: async (messages, options, innerClient, ct) => { /* 实现 */ },
       getStreamingResponseFunc: null // 流式请求会失败
   )   
   // 正确:同时实现或使用继承方式
   public class MyMiddleware : DelegatingChatClient
   {
       public override async Task<ChatResponse> GetResponseAsync(...) { }
       public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(...) { }
   }
  
// 不要在中间件中改变语义
   // 错误:完全改变响应内容
   .Use(async (messages, options, innerClient, ct) =>
   {
       // 忽略 AI 响应,返回固定内容
       return new ChatResponse([new ChatMessage(ChatRole.Assistant, "固定回复")]);
   })   
   // 正确:只增强,不改变语义
   .Use(async (messages, options, innerClient, ct) =>
   {
       var response = await innerClient.GetResponseAsync(messages, options, ct);
       // 只是添加元数据,不改变内容
       response.AdditionalProperties["timestamp"] = DateTime.UtcNow;
       return response;
   })
  • 性能优化建议
    • 启用并发函数调用
    • 合理设置迭代次数
    • 在外层使用缓存
    • 异步非阻塞
      • 所有中间件都应该是异步的
      • 避免在中间件中使用 .Result 或 .Wait()
// 启用并发函数调用
   .UseFunctionInvocation(configure: options =>
   {
       options.AllowConcurrentInvocation = true; // 适用于独立函数
   })

// 合理设置迭代次数
   options.MaximumIterationsPerRequest = 5; // 根据实际需求调整
   
// 在外层使用缓存
   .UseDistributedCache(cache) // 在函数调用前
   .UseFunctionInvocation()
  • 可观测性建议
var observableClient = chatClient.AsBuilder()
    // 1. 结构化日志
    .Use(async (messages, options, innerClient, ct) =>
    {
        using var scope = logger.BeginScope(new Dictionary<string, object>
        {
            ["RequestId"] = Guid.NewGuid(),
            ["MessageCount"] = messages.Count()
        });
        return await innerClient.GetResponseAsync(messages, options, ct);
    })
    // 2. 性能追踪
    .Use(async (messages, options, innerClient, ct) =>
    {
        using var activity = activitySource.StartActivity("ChatClient.GetResponse");
        return await innerClient.GetResponseAsync(messages, options, ct);
    })
    // 3. 函数调用监控
    .UseFunctionInvocation(configure: options =>
    {
        options.FunctionInvoker = async (context, ct) =>
        {
            using var activity = activitySource.StartActivity($"Function.{context.Function.Name}");
            return await context.Function.InvokeAsync(context.Arguments, ct);
        };
    })
    .Build();