Net+AI智能体进阶2:MEAI进阶扩展
2025-08-23 22:16:24一、函数调用进阶用法
1. FunctionInvokingChatClient 深入解析
FunctionInvokingChatClient 是 MEAI 中负责函数调用的核心中间件。当我们调用 UseFunctionInvocation() 时,实际上是在管道中插入了这个强大的中间件,它会自动处理模型的函数调用请求。
工作原理与执行流程
FunctionInvokingChatClient 作为装饰器包装底层的 IChatClient,拦截对话请求并自动处理函数调用循环:
- 发送初始请求:将用户消息和可用工具传递给模型
- 检测函数调用:模型返回时,检查响应中是否包含 FunctionCallContent
- 执行函数:自动调用被请求的函数,获取执行结果
- 回传结果:将函数结果作为 FunctionResultContent 添加到对话历史
- 继续迭代:再次调用模型,让它基于函数结果生成最终回答
- 返回响应:直到模型不再请求函数调用,返回最终答案
这个循环过程完全自动化,开发者无需手动管理函数调用状态。
用户消息 → 模型(发现需要调用函数) → 执行函数 → 模型(基于结果) → 最终答案
↑_______________________________________________|
(自动迭代,最多 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();
顺序说明
- 日志在最外层:捕获完整的请求/响应周期
- 缓存在函数调用前:避免重复执行相同的函数调用链
- Reducer 在中间:压缩历史消息但保留函数调用上下文
- 函数调用在核心:处理工具调用逻辑
- 重试在最内层:处理底层 API 的暂时性故障
注意:某些中间件的顺序会显著影响行为。例如,将缓存放在函数调用后会缓存整个函数调用流程,但无法为单个函数结果提供缓存。
二、ChatOptions扩展
ChatOptions 作为统一的抽象层,提供了跨不同 AI 提供商的通用配置接口。但不同提供商往往有自己独特的功能和参数:
- OpenAI: store、metadata、IncludeUsage 等
- Azure AI: 自定义的 AdditionalProperties、审核配置等
- Google: 特定的安全设置和生成配置
如果将所有提供商的特殊参数都加入 ChatOptions,会导致抽象层变得臃肿且难以维护。因此,Microsoft.Extensions.AI 提供了两个扩展点:
- AdditionalProperties: 键值对字典,用于透传自定义参数
- 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();
