Net+AI智能体2:MEAI进阶扩展
2025-10-11 13:09:20一、函数调用进阶
1. FunctionInvokingChatClient 深入解析
FunctionInvokingChatClient 是 MEAI 中负责函数调用的核心中间件。当我们调用 UseFunctionInvocation() 时,实际上是在管道中插入了这个强大的中间件,它会自动处理模型的函数调用请求。
工作原理与执行流程
FunctionInvokingChatClient 作为装饰器包装底层的 IChatClient,拦截对话请求并自动处理函数调用循环:
- 发送初始请求:将用户消息和可用工具传递给模型
- 检测函数调用:模型返回时,检查响应中是否包含 FunctionCallContent
- 执行函数:自动调用被请求的函数,获取执行结果
- 回传结果:将函数结果作为 FunctionResultContent 添加到对话历史
- 继续迭代:再次调用模型,让它基于函数结果生成最终回答
- 返回响应:直到模型不再请求函数调用,返回最终答案
这个循环过程完全自动化,开发者无需手动管理函数调用状态。
用户消息 → 模型(发现需要调用函数) → 执行函数 → 模型(基于结果) → 最终答案
↑_______________________________________________|
(自动迭代,最多 MaximumIterationsPerRequest 次)
核心配置选项
通过 UseFunctionInvocation(configure: options => ) 可以访问 FunctionInvokingChatClient 的配置选项。以下是各属性的详细说明:
AdditionalTools(全局工具集)
- 用途:提供额外的工具供函数调用时查找,但不会发送给模型
- 重要限制:模型不知道这些工具的存在,除非服务端已预先配置
- 工作机制:当模型请求调用某个工具时,先查找 ChatOptions.Tools,如果找不到再查找 AdditionalTools
- 适用场景:
- 服务端预配置的工具(如 Azure OpenAI 的 Assistants API)
- 作为回退工具集
- 与 ChatOptions.Tools 的关系:ChatOptions.Tools 优先级更高,同名工具会覆盖 AdditionalTools 中的工具
注意:如果你希望模型能够调用某个工具,必须将其放在 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 |
三、基于 Function Calling 实现结构化输出
JSON Schema 方式 vs Function Calling 方式
| 特性 | JSON Schema | Function Calling |
|---|---|---|
| 实现方式 | GetResponseAsync |
定义 Function + 提取参数 |
| 代码复杂度 | 简单(一行代码) | 中等(需定义 Function) |
| 业务逻辑 | 仅数据提取 | 可添加验证、转换逻辑 |
| 状态管理 | 无状态 | 可保存到类实例 |
| 模型兼容性 | 某些模型不支持 | 大多数模型支持 |
| 适用场景 | 纯数据提取 | 带业务逻辑的数据处 |
1. 关键思路
- 将数据模型定义为 Function 的参数类型
- AI 模型识别到需要提取数据时,会"调用" Function
- Function 的参数就是 AI 提取的结构化数据
- 我们可以从两个地方获取这些数据:
- 方式 1:从 FunctionCallContent 中提取参数
- 方式 2:从 Function 所属的类实例中直接获取(推荐)
2. 单一对象的结构化输出
- 启动 Function Calling 中间件
// 启用 Function Calling 中间件
var client = chatClient.AsBuilder()
.UseFunctionInvocation()
.Build();
- 定义数据模型:这里我们使用上一篇中使用的 PersonInfo
方法1:从 FunctionCallContent 中提取(间接方式)
- 不执行 Function,直接从响应中的 FunctionCallContent 提取参数
// 方式 1: 定义一个简单的方法
public void SavePersonInfo(PersonInfo person)
{
// 这个方法体不会被实际执行
// 我们只关心 AI 传入的参数
}
// 使用 AIFunctionFactory 创建 Function
var extractPersonTool = AIFunctionFactory.Create(
SavePersonInfo,
name: "save_person_info",
description: "保存提取的人员基本信息"
);
- 发送请求
var messages = new List<ChatMessage>
{
new ChatMessage(ChatRole.System, "你是一个信息提取助手。请使用 save_person_info 工具保存提取的人员信息。"),
new ChatMessage(ChatRole.User, "请提取以下信息:张伟是一名35岁的软件工程师,目前在北京工作。")
};
var options = new ChatOptions
{
Tools = [extractPersonTool],
ToolMode = ChatToolMode.RequireAny // 强制要求调用工具
};
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 方式 1:从 FunctionCallContent 中提取 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response = await client.GetResponseAsync(messages, options);
Console.WriteLine("请求完成\n");
Console.WriteLine("响应详情:");
response.Messages.SelectMany(t=>t.Contents).Display();
- 提取参数
// 检查是否包含 FunctionCallContent
var functionCallContent = response.Messages.SelectMany(t=>t.Contents)
.OfType<FunctionCallContent>()
.FirstOrDefault();
if (functionCallContent != null)
{
Console.WriteLine("AI 调用了 Function:");
new {
FunctionName = functionCallContent.Name,
CallId = functionCallContent.CallId
}.Display();
Console.WriteLine("\n原始参数 JSON:");
functionCallContent.Arguments.Display();
functionCallContent.Arguments.TryGetValue("person", out Object person);
// 从 FunctionCallContent 中提取参数
var personInfo = JsonSerializer.Deserialize<PersonInfo>(
person.ToString()!,
JsonSerializerOptions.Web
);
Console.WriteLine("\n提取成功(方式 1)\n");
Console.WriteLine("提取的个人信息:");
personInfo.Display();
}
else
{
Console.WriteLine("未检测到 Function Call");
}
方法2:从类实例中直接获取(推荐)
- 定义"数据提取器"类,Function 执行时自动将数据保存到类实例属性中。
/// <summary>
/// 人员信息提取器(方式 2:带状态管理)
/// </summary>
public class PersonExtractor
{
/// <summary>
/// 保存的人员信息
/// </summary>
public PersonInfo? SavedPerson { get; private set; }
/// <summary>
/// 保存人员信息的方法
/// </summary>
/// <param name="person">AI 提取的人员信息</param>
public void SavePerson(PersonInfo person)
{
// 将 AI 提取的数据保存到属性中
SavedPerson = person;
// 可以在这里添加额外的业务逻辑
Console.WriteLine($" Function 被调用,已保存数据到 SavedPerson 属性");
}
}
- 创建实例和 Function
// 创建提取器实例
var personExtractor = new PersonExtractor();
// 基于实例方法创建 Function
var extractPersonTool2 = AIFunctionFactory.Create(
personExtractor.SavePerson, // 实例方法
name: "save_person_info",
description: "保存提取的人员基本信息,包括姓名、年龄、职业和工作地点"
);
- 发送请求并获取结果
var messages2 = new List<ChatMessage>
{
new ChatMessage(ChatRole.System, "你是一个信息提取助手。请使用 save_person_info 工具保存提取的人员信息。"),
new ChatMessage(ChatRole.User, "请提取:李娜是一名29岁的产品经理,在上海工作。")
};
var options2 = new ChatOptions
{
Tools = [extractPersonTool2],
ToolMode = ChatToolMode.RequireAny
};
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 方式 2:从类实例中直接获取(推荐) ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var options2Auto = new ChatOptions
{
Tools = [extractPersonTool2]
// 默认 ToolMode.Auto,会自动执行 Function
};
// 使用启用了中间件的 client,Function 会自动执行
await foreach (var message in client.GetStreamingResponseAsync(messages2, options2Auto))
{
// 流式接收(Function 会在后台执行)
}
Console.WriteLine("请求完成\n");
// 直接从实例属性获取结果
if (personExtractor.SavedPerson != null)
{
Console.WriteLine("数据已自动保存到实例属性\n");
Console.WriteLine("提取的个人信息:");
personExtractor.SavedPerson.Display();
}
else
{
Console.WriteLine("未提取到数据");
}
3. 嵌套对象的结构化输出
- 定义嵌套模型(上一篇中的SentimentAnalysis和ProductReviewAnalysis类)
- 定义带验证逻辑的提取器
/// <summary>
/// 产品评论分析提取器(带验证逻辑)
/// </summary>
public class ProductReviewExtractor
{
/// <summary>
/// 保存的产品评论分析结果
/// </summary>
public ProductReviewAnalysis? SavedReview { get; private set; }
/// <summary>
/// 验证错误信息
/// </summary>
public List<string> ValidationErrors { get; private set; } = new();
/// <summary>
/// 保存并验证产品评论信息
/// </summary>
public void SaveReview(ProductReviewAnalysis review)
{
Console.WriteLine($" Function 被调用,正在验证数据...");
// 在这里可以添加业务验证逻辑
ValidationErrors.Clear();
if (review.Rating < 1 || review.Rating > 5)
{
ValidationErrors.Add($"评分必须在 1-5 之间,当前值:{review.Rating}");
}
if (review.Sentiment?.Confidence < 0 || review.Sentiment?.Confidence > 1)
{
ValidationErrors.Add($"置信度必须在 0-1 之间,当前值:{review.Sentiment?.Confidence}");
}
if (review.KeyPoints?.Count == 0)
{
ValidationErrors.Add("关键要点列表不能为空");
}
// 即使有验证错误,也保存数据(便于调试)
SavedReview = review;
if (ValidationErrors.Count > 0)
{
Console.WriteLine($" 发现 {ValidationErrors.Count} 个验证问题");
}
else
{
Console.WriteLine($" 数据验证通过");
}
}
}
- 分析产品评论
var reviewExtractor = new ProductReviewExtractor();
var extractReviewTool = AIFunctionFactory.Create(
reviewExtractor.SaveReview,
name: "save_product_review",
description: "保存产品评论的分析结果,包括产品名称、评分、情感分析、关键要点和推荐建议"
);
var reviewText = @"
这款iPhone 15 Pro真的很棒!屏幕显示效果非常清晰,A17芯片运行速度超快。
拍照效果也有显著提升,特别是夜景模式。唯一的缺点是价格有点高,但整体来说非常值得购买。
我强烈推荐给需要高性能手机的用户。
";
var reviewMessages = new List<ChatMessage>
{
new ChatMessage(ChatRole.System, "你是一个产品评论分析专家。请使用 save_product_review 工具保存分析结果。"),
new ChatMessage(ChatRole.User, $"请分析以下产品评论:\n\n{reviewText}")
};
var reviewOptions = new ChatOptions
{
Tools = [extractReviewTool]
};
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 嵌套对象提取 + 验证逻辑示例 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// await foreach (var message in client.GetStreamingResponseAsync(reviewMessages, reviewOptions))
// {
// // Function 会在后台执行
// }
await client.GetResponseAsync(reviewMessages, reviewOptions);
Console.WriteLine("\n请求完成\n");
// 从实例获取结果
if (reviewExtractor.SavedReview != null)
{
Console.WriteLine("提取的产品评论分析:");
reviewExtractor.SavedReview.Display();
// 显示验证结果
if (reviewExtractor.ValidationErrors.Count > 0)
{
Console.WriteLine("\n 验证错误:");
foreach (var error in reviewExtractor.ValidationErrors)
{
Console.WriteLine($"- {error}");
}
}
else
{
Console.WriteLine("\n所有数据验证通过");
}
}
else
{
Console.WriteLine("未提取到数据");
}
4. 列表/数组的结构化输出
- 处理列表数据的提取
/// <summary>
/// 联系人列表提取器
/// </summary>
public class ContactListExtractor
{
/// <summary>
/// 保存的联系人列表
/// </summary>
public List<ContactInfo> SavedContacts { get; private set; } = new();
/// <summary>
/// 保存联系人信息
/// </summary>
public void SaveContacts(List<ContactInfo> contacts)
{
Console.WriteLine($" Function 被调用,提取到 {contacts.Count} 个联系人");
// 保存到列表
SavedContacts = contacts;
// 可以在这里添加去重、验证等逻辑
var uniqueEmails = contacts.Select(c => c.Email).Distinct().Count();
if (uniqueEmails < contacts.Count)
{
Console.WriteLine($" 检测到重复的邮箱地址");
}
}
}
- 批量提取联系人
var contactExtractor = new ContactListExtractor();
var extractContactsTool = AIFunctionFactory.Create(
contactExtractor.SaveContacts,
name: "save_contacts",
description: "保存批量提取的联系人信息,包括姓名、电话、邮箱和公司"
);
var contactText = @"
我们团队的主要联系人如下:
1. 张经理 - 电话:13800138000,邮箱:zhang@company.com,公司:ABC科技
2. 李工程师 - 电话:13900139000,邮箱:li@company.com,公司:ABC科技
3. 王总监 - 电话:13700137000,邮箱:wang@partner.com,公司:XYZ咨询
";
var contactMessages = new List<ChatMessage>
{
new ChatMessage(ChatRole.System, "你是一个信息提取助手。请使用 save_contacts 工具保存提取的联系人信息。"),
new ChatMessage(ChatRole.User, $"请提取以下文本中的所有联系人信息:\n\n{contactText}")
};
var contactOptions = new ChatOptions
{
Tools = [extractContactsTool]
};
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 列表/数组的结构化输出示例 ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
await foreach (var message in client.GetStreamingResponseAsync(contactMessages, contactOptions))
{
// Function 会在后台执行
}
Console.WriteLine($"\n成功提取 {contactExtractor.SavedContacts.Count} 个联系人\n");
Console.WriteLine("提取的联系人列表:");
contactExtractor.SavedContacts.Display();
5. 最佳实践
- 推荐使用 Function Calling 的场景
- 需要添加验证逻辑
- 需要状态管理
- 需要数据转换
- 模型不支持 JSON Schema(如国内的一些模型)
public void SaveData(DataModel data)
{
if (data.Age < 0 || data.Age > 120)
throw new ArgumentException("年龄不合法");
SavedData = data;
}
public void SaveContact(ContactInfo contact)
{
AllContacts.Add(contact); // 累积到列表
}
public void SaveData(RawData raw)
{
// 转换并保存
SavedData = ConvertToBusinessModel(raw);
}
- 推荐使用 JSON Schema 的场景
- 纯数据提取,无业务逻辑
- 一次性获取结果,不需要状态
- 追求代码简洁(一行代码完成)
- 两种获取方法的选择建议
- 方式 1:从 FunctionCallContent 提取
- 适合:不需要执行 Function、仅提取参数
- 优势:无需等待执行
- 劣势:代码较冗长、无法添加逻辑
- 方式 2:从类实例直接获取(推荐)
- 适合:需要业务逻辑、状态管理
- 优势:代码简洁、面向对象、可扩展
- 劣势:需要等待 Function 执行
- 方式 1:从 FunctionCallContent 提取
- 实现要点
// 类设计
public class DataExtractor
{
// 使用只读属性保护数据
public DataModel? SavedData { get; private set; }
// 提供清晰的方法名
public void SaveData(DataModel data)
{
// 添加验证、转换等业务逻辑
SavedData = data;
}
}
// Function 配置
var extractor = new DataExtractor();
var tool = AIFunctionFactory.Create(
extractor.SaveData, // 实例方法
name: "save_data",
description: "详细描述 Function 的用途" // 清晰的描述很重要
);
// ChatOptions 配置
var options = new ChatOptions
{
Tools = [tool],
// 默认 Auto 模式,会自动执行 Function
};
- 常见错误与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 数据未保存到实例 | Function 未实际执行 | 使用 GetStreamingResponseAsync 或确保 ToolMode 正确 |
| 并发安全问题 | 多线程共享实例 | 每次请求创建新实例 |
| 验证逻辑未触发 | Function 未被调用 | 检查 System Message 和 Function 描述 |
| 参数反序列化失败 | 数据模型定义不匹配 | 检查 JsonPropertyName 和类型定义 |
对于国内模型,Function Calling 是实现结构化输出的首选方式!
四、自定义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();
六、获取思考过程
在 DeepSeek-R1 或 Qwen-2.5-Max 等推理模型中,模型会在生成最终答案之前输出一段“思考过程”(Reasoning Content)。
目前的 .NET OpenAI SDK (OpenAI 包) 尚未完全原生支持将 reasoning_content 字段映射到强类型对象中。
接下来我们看一下目前有什么方法可以实时获取并展示模型的思考过程。
1. 基于 OpenAI SDK 流式获取思考过程
我们先来介绍第一种方法,通过 GetRawPagesAsync 和 SseParser 手动解析 Server-Sent Events (SSE) 数据流,来获取模型的思考过程。
- 初始化 OpenAI 客户端
这里我们直接使用 OpenAIClient,并配置自定义的 Endpoint(例如 Qwen 或 DeepSeek 的 API 地址)。
using OpenAI;
using OpenAI.Chat;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Net.ServerSentEvents;
using System.Text;
using System.Text.Json;
// 配置 OpenAI 客户端
var clientOptions = new OpenAIClientOptions
{
Endpoint = new Uri(Keys.QwenEndpoint)
};
var openAIClient = new OpenAIClient(new ApiKeyCredential(Keys.QwenApiKey), clientOptions);
var chatClient = openAIClient.GetChatClient("deepseek-v3.2-exp"); // 请根据实际使用的模型名称调整
部分模型(如 Qwen)需要通过特定的参数开启思考模式。由于 SDK 可能尚未包含该参数的强类型属性,我们可以使用 ChatCompletionOptions.Patch 功能来注入自定义 JSON 参数。
#pragma warning disable SCME0001 // 忽略 Patch 相关的实验性警告
var options = new ChatCompletionOptions();
// 启用思考模式 (参数名取决于具体的模型提供商,这里以 Qwen 为例)
options.Patch.Set("$.enable_thinking"u8, true);
#pragma warning restore SCME0001
Console.WriteLine("思考模式已启用");
- 定义 SSE 解析逻辑
由于标准 CompleteChatStreamingAsync 返回的 StreamingChatCompletionUpdate 对象可能不包含 reasoning_content,我们需要处理底层的 SSE (Server-Sent Events) 数据流。
/// <summary>
/// 尝试解析 JSON 消息
/// </summary>
bool TryParseJsonMessage(string data, out JsonElement message)
{
try
{
message = JsonSerializer.Deserialize<JsonElement>(data);
return message.ValueKind == JsonValueKind.Object;
}
catch (JsonException)
{
message = default;
return false;
}
}
/// <summary>
/// 处理 Delta 内容,提取 reasoning_content 和 content
/// </summary>
void ProcessDeltaContent(JsonElement message, StringBuilder reasoningContent, StringBuilder answerContent)
{
if (!message.TryGetProperty("choices", out var choices) || choices.ValueKind != JsonValueKind.Array)
return;
var firstChoice = choices.EnumerateArray().FirstOrDefault();
if (firstChoice.ValueKind != JsonValueKind.Object || !firstChoice.TryGetProperty("delta", out var delta))
return;
// 提取思考内容 (reasoning_content)
if (delta.TryGetProperty("reasoning_content", out var reasoningProperty))
{
var reasoningText = reasoningProperty.GetString();
if (!string.IsNullOrEmpty(reasoningText))
{
// 实时打印思考内容(使用灰色或其他标识区分)
Console.ForegroundColor = ConsoleColor.Gray;
Console.Write(reasoningText);
Console.ResetColor();
reasoningContent.Append(reasoningText);
}
}
// 提取普通回答内容 (content)
if (delta.TryGetProperty("content", out var contentProperty))
{
var contentText = contentProperty.GetString();
if (!string.IsNullOrEmpty(contentText))
{
// 实时打印回答内容
Console.Write(contentText);
answerContent.Append(contentText);
}
}
}
/// <summary>
/// 解析 SSE 页面
/// </summary>
void ProcessSsePage(ClientResult page, StringBuilder reasoningContent, StringBuilder answerContent)
{
var contentStream = page.GetRawResponse().ContentStream;
if (contentStream is null) return;
var parser = SseParser.Create(contentStream);
foreach (var item in parser.Enumerate())
{
if (string.IsNullOrWhiteSpace(item.Data))
continue;
if (!TryParseJsonMessage(item.Data, out var message))
continue;
ProcessDeltaContent(message, reasoningContent, answerContent);
}
}
Console.WriteLine("解析逻辑定义完成");
- 执行流式对话
现在我们将发送请求,并使用 GetRawPagesAsync() 获取原始数据流进行解析。
var prompt = "如何将大象装进冰箱?";
var messages = new ChatMessage[] { new UserChatMessage(prompt) };
Console.WriteLine($"User: {prompt}\n");
Console.WriteLine("Assistant (Thinking & Answering):\n");
var reasoningContent = new StringBuilder();
var answerContent = new StringBuilder();
// 发起流式请求
var chatUpdates = chatClient.CompleteChatStreamingAsync(messages, options);
// 遍历原始 SSE 页面
await foreach (var page in chatUpdates.GetRawPagesAsync())
{
ProcessSsePage(page, reasoningContent, answerContent);
}
Console.WriteLine("\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"思考内容长度: {reasoningContent.Length} 字符");
Console.WriteLine($"回答内容长度: {answerContent.Length} 字符");
核心技术点
- 绕过 SDK 限制:当 SDK 尚未原生支持某些新字段(如 reasoning_content)时,我们可以回退到更底层的 GetRawPagesAsync 方法。
- SSE 解析:利用 System.Net.ServerSentEvents.SseParser 可以轻松解析流式响应,获取原始的 JSON 数据。
- JSON Patch:使用 ChatCompletionOptions.Patch 灵活地向 API 传递非标准参数(如 enable_thinking)。
这种“底层解析”的技巧在对接快速迭代的 AI 模型 API 时非常实用,能确保我们在 SDK 更新之前就能使用最新的模型能力。
2. JsonPatch 快速入门
第二种获取思考过程的方式可以使用 ChatOptions 的扩展点 RawRepresentationFactory,它可以用来配置推理模型的特有参数。
在使用 RawRepresentationFactory 之前,我们先来了解 JsonPatch 的基本概念。
JsonPatch 是一种用于描述 JSON 文档修改操作的标准格式(RFC 6902),在 OpenAI SDK 中用于动态设置请求参数。
在 OpenAI 和 Azure.AI.OpenAI SDK 中,ChatCompletionOptions.Patch 是一个 JsonPatch 对象,用于设置 AI 提供商的特有参数。
JsonPatch 基本用法:
// 1. 创建 ChatCompletionOptions
var options = new ChatCompletionOptions();
// 2. 使用 Patch.Set() 设置参数
// 语法:Patch.Set(路径, 值)
options.Patch.Set("$.enable_thinking"u8, true);
options.Patch.Set("$.temperature"u8, 0.7);
options.Patch.Set("$.max_tokens"u8, 1000);
JsonPath 路径语法:
| 路径 | 说明 | 示例 |
|---|---|---|
| \(| 根对象 |\).enable_thinking | ||
| . | 属性访问 | $.choices[0].delta |
| [] | 数组索引 | $.choices[0] |
常用推理模型参数:
| 参数 | 类型 | 说明 | 示例值 |
|---|---|---|---|
| enable_thinking | bool | 启用推理模式 | true |
| thinking.type | string | 思考类型(DeepSeek) | "enabled" |
| enable_search | bool | 启用联网搜索(千问) | true |
注意:
- u8 后缀表示 UTF-8 字节数组字面量,用于高性能字符串处理
- 不同 AI 提供商的参数名称可能不同,需参考官方文档
3. 基于 IChatClient 获取思考过程
工作原理:
sequenceDiagram
participant App as 应用代码
participant MEAI as Microsoft.Extensions.AI
participant SDK as OpenAI SDK
participant API as AI 模型 API
App->>MEAI: ChatOptions + RawRepresentationFactory
MEAI->>SDK: 调用 Factory 创建 ChatCompletionOptions
SDK->>SDK: Patch.Set("$.enable_thinking", true)
SDK->>API: 发送请求(包含 enable_thinking)
API->>SDK: 返回响应(含推理内容)
SDK->>MEAI: 原始响应
MEAI->>App: ChatCompletion / StreamingChatCompletionUpdate
代码实现:
阿里百炼平台提供了enable_thinking参数,用于控制在使用混合思考(回复前既可思考也可不思考)模型时,是否开启思考模式。
- 千问系列模型:适用于 Qwen3 、Qwen3-Omni-Flash、Qwen3-VL模型,默认不开启思考模式。阿里百炼千问模型API文档
- DeepSeek模型:适用于 deepseek-v3.2、deepseek-v3.2-exp与deepseek-v3.1。默认不开启思考模式。deepseek-v3.2是DeepSeek推出的首个将思考融入工具使用的模型,同时支持思考模式与非思考模式的工具调用。阿里百炼DeepSeek模型API文档
关于阿里百炼平台更多推理模型的默认行为和参数设置,可参考阿里百炼-深度思考
对于DeepSeek 平台,deepseek-chat 和 deepseek-reasoner 都已经升级为 DeepSeek-V3.2。deepseek-chat 对应 DeepSeek-V3.2 的非思考模式,deepseek-reasoner 对应 DeepSeek-V3.2 的思考模式,意味着无需手动设置 enable_thinking 参数即可使用对应模式。更多信息请参考DeepSeek-API文档。
下面使用阿里百炼平台创建一个带有推理模式的 ChatOptions:
#pragma warning disable SCME0001 // RawRepresentationFactory 使用警告
// 创建启用推理模式的 ChatOptions
var reasoningOptions = new ChatOptions()
{
RawRepresentationFactory = (client) =>
{
// 创建底层 OpenAI SDK 的 ChatCompletionOptions
var options = new ChatCompletionOptions();
// 使用 JsonPatch 设置 enable_thinking 参数
options.Patch.Set("$.enable_thinking"u8, true);
return options;
}
};
Console.WriteLine("推理模式配置完成");
// 使用阿里百炼平台的deepseek-v3.2-exp模型(支持推理模式)
var qwenClient = AIClientHelper.GetQwenClient();
var chatClient = qwenClient.GetChatClient("deepseek-v3.2-exp").AsIChatClient();
Console.WriteLine("DeepSeek 推理模型客户端创建完成");
非流式获取推理内容
- 使用 GetResponseAsync 获取完整的响应,包括推理内容和最终答案。
Console.WriteLine("开始发送请求...\n");
// 发送请求,传入启用推理的 ChatOptions
var response = await chatClient.GetResponseAsync("将大象装进冰箱需要几步?", reasoningOptions);
Console.WriteLine("响应接收完成\n");
// 显示基本响应信息
new
{
消息内容 = response.Text,
Token使用 = response.Usage?.TotalTokenCount,
完成原因 = response.FinishReason
}.Display();
- 提取推理内容
要获取推理内容,需要将 MEAI 的 ChatCompletion 转换为 OpenAI SDK 的原始对象,然后使用Azure.AI.OpenAI.ChatNuget包中的扩展方法 GetMessageReasoningContent():
#pragma warning disable AOAI001 // Azure.AI.OpenAI 扩展方法
using Azure.AI.OpenAI.Chat;
// 1. 将 MEAI ChatCompletion 转换为 OpenAI SDK 的原生对象
var chatCompletion = response.AsOpenAIChatCompletion();
// 2. 使用扩展方法获取推理内容
// 推理内容仅在启用 enable_thinking 时才会返回
var reasoningContent = chatCompletion.GetMessageReasoningContent();
// 3. 显示推理过程
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("模型推理过程:\n");
Console.WriteLine(reasoningContent);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("最终回答:\n");
Console.WriteLine(response.Text);
流式获取推理内容
在实际应用中,我们通常希望实时展示推理过程,提升用户体验。使用流式响应可以实现"思考中…"的动态效果。
使用 AsOpenAIStreamingChatCompletionUpdatesAsync 扩展方法来解析流式响应:
Console.WriteLine("开始流式请求...\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("模型推理过程(实时流式输出):\n");
#pragma warning disable SCME0001 // JsonPatch 使用警告
// 1. 获取流式响应
var streamingUpdates = chatClient.GetStreamingResponseAsync(
"将大象装进冰箱需要几步?",
options: reasoningOptions
);
// 2. 转换为 OpenAI SDK 的流式更新对象
var openAIUpdates = streamingUpdates.AsOpenAIStreamingChatCompletionUpdatesAsync();
// 3. 遍历流式更新,提取推理内容
await foreach (var update in openAIUpdates)
{
// 使用 JsonPatch 从更新中提取 reasoning_content
if (update.Patch.TryGetValue("$.choices[0].delta.reasoning_content"u8, out string reasoningContentChunk))
{
Console.Write(reasoningContentChunk);
}
}
Console.WriteLine("\n\n流式推理内容接收完成");
JsonPath 路径解析
{
"choices": [
{
"delta": {
"reasoning_content": "首先,需要打开冰箱门...", // ← 目标字段
"content": "将大象装进冰箱需要三步..."
}
}
]
}
在实际应用中,通常需要同时展示推理过程和最终回答:
Console.WriteLine("开始完整流式请求...\n");
var streamingUpdates2 = chatClient.GetStreamingResponseAsync(
"用简洁的方式说明如何将大象装进冰箱",
options: reasoningOptions
);
var openAIUpdates2 = streamingUpdates2.AsOpenAIStreamingChatCompletionUpdatesAsync();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("推理过程:\n");
bool hasReasoningContent = false;
bool hasMessageContent = false;
await foreach (var update in openAIUpdates2)
{
// 提取推理内容
if (update.Patch.TryGetValue("$.choices[0].delta.reasoning_content"u8, out string reasoningChunk))
{
if (!hasReasoningContent)
{
hasReasoningContent = true;
}
Console.Write(reasoningChunk);
}
// 提取消息内容
if (update.Patch.TryGetValue("$.choices[0].delta.content"u8, out string messageChunk))
{
if (!hasMessageContent)
{
Console.WriteLine("\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("💬 最终回答:\n");
hasMessageContent = true;
}
Console.Write(messageChunk);
}
}
Console.WriteLine("\n\n完整流式响应接收完成");
4. 最佳实践
非流式 vs 流式
| 特性 | 非流式(GetResponseAsync) | 流式(GetStreamingResponseAsync) |
|---|---|---|
| 用户体验 | 等待完整响应后一次性显示 | 实时显示推理过程和回答 |
| 延迟感知 | 延迟明显(需等待全部完成) | 延迟低(逐步输出) |
| 适用场景 | 后台处理、批量任务 | 对话界面、实时展示 |
| 推理内容提取 | GetMessageReasoningContent() | Patch.TryGetValue("reasoning_content") |
| 实现复杂度 | 简单 | 中等(需处理流式更新) |
选择合适的模式
- 对话应用 → 使用流式,实时展示思考过程
- 后台任务 → 使用非流式,简化处理逻辑
推理内容的用途
- 调试提示词:查看模型理解是否正确
- 教学场景:展示 AI 推理逻辑
- 审计追踪:记录模型决策过程
成本优化
- 推理内容会消耗额外的 Token(输入 Token)
- 生产环境中,可根据需要选择性启用
- 使用缓存策略减少重复推理
注意事项
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 模型支持 | 并非所有模型都支持推理模式 | 使用 DeepSeek、千问等明确支持的模型 |
| 参数差异 | 不同提供商参数名可能不同 | 参考官方文档,使用正确的参数名 |
| 依赖包版本 | 需要 Azure.AI.OpenAI 才能使用 GetMessageReasoningContent() | 确保安装正确版本的 NuGet 包 |
| Token 计费 | 推理内容计入 Token 使用量 | 关注 API 计费,合理使用推理模式 |
5. 扩展:不同推理模型的参数配置
- DeepSeek Reasoner(专用推理模型):DeepSeek 提供了专门的推理模型 deepseek-reasoner,使用不同的参数配置
var deepseekReasonerOptions = new ChatOptions()
{
RawRepresentationFactory = (client) =>
{
var options = new ChatCompletionOptions();
// DeepSeek Reasoner 使用 thinking.type 参数
options.Patch.Set("$.thinking.type"u8, "enabled");
return options;
}
};
var reasonerClient = AIClientHelper.GetDefaultChatClient("DeepSeek", "deepseek-reasoner");
var response = await reasonerClient.GetResponseAsync("分析量子计算的未来发展", deepseekReasonerOptions);
- 千问推理模型(Qwen):千问模型支持推理 + 联网搜索
var qwenOptions = new ChatOptions()
{
RawRepresentationFactory = (client) =>
{
var options = new ChatCompletionOptions();
options.Patch.Set("$.enable_thinking"u8, true);
options.Patch.Set("$.enable_search"u8, true); // 启用联网搜索
return options;
}
};
var qwenClient = AIClientHelper.GetDefaultChatClient("Qwen", "qwen-max");
var response = await qwenClient.GetResponseAsync("2025年最新的AI技术趋势", qwenOptions);
七、三大 API 一较高下
1. 三大 API 全景
- Chat Completions API - 行业标准,广泛兼容
- Responses API - Agentic by Default,服务端托管
- Messages API - 内容块驱动,Claude 原生
本章将三者放在同一视角下进行全面横向对比,帮助理解实际项目中做出正确的技术选型决策。
flowchart
subgraph MEAI ["Microsoft.Extensions.AI - 统一抽象层"]
I[IChatClient]
end
subgraph OpenAI_Family ["OpenAI 家族"]
CC["Chat Completions API<br>无状态 · 客户端控制"]
RA["Responses API<br>Agentic by Default"]
end
subgraph Anthropic_Family ["Anthropic 家族"]
MA["Messages API<br>内容块驱动"]
end
subgraph Providers ["提供商"]
AZ["Azure OpenAI<br>DeepSeek / Qwen"]
OA["OpenAI / Azure OpenAI"]
AN["Anthropic Claude"]
end
I --> CC --> AZ
I --> RA --> OA
I --> MA --> AN
| 对比项 | Chat Completions | Responses API | Messages API |
|---|---|---|---|
| 定位 | 无状态对话补全 | Agentic 工作流 | 内容块驱动对话 |
| 状态管理 | ❌ 客户端手动维护 | ✅ 服务端托管 | ❌ 客户端手动维护 |
| 系统提示 | role: "system" 消息 | 独立 instructions 参数 | 独立 system 参数 |
| 内置工具 | ❌ 无 | ✅ Web Search / Code Interpreter | ❌ 无 |
| 工具调用 | 手动循环或 MEAI 中间件 | Agentic Loop 自动 | 手动循环或 MEAI 中间件 |
| 多模态 | ✅ 图片 | ✅ 图片 + 更多 | ✅ 原生 Content Blocks |
| 扩展思考 | ❌ | ❌ | ✅ thinking 块 |
| max_tokens | 可选 | 可选 | 必填 |
| 典型提供商 | OpenAI / Azure / DeepSeek / Qwen | OpenAI / Azure | Anthropic Claude |
| 未来定位 | 功能冻结 | 🚀 OpenAI 主力方向 | 🚀 Anthropic 持续演进 |
- 相同问题,三种 API 并行比较
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 三大 API 并行横向对比
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
async Task<object> BenchmarkApiAsync(IChatClient client, string apiName, string provider, string question)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
try
{
var response = await client.GetResponseAsync(question);
sw.Stop();
return new
{
API = apiName,
Provider = provider,
回答 = response.Text,
Token消耗 = response.Usage?.TotalTokenCount ?? 0,
耗时ms = sw.ElapsedMilliseconds,
ResponseId = response.ResponseId ?? "(无)",
状态 = "成功"
};
}
catch (Exception ex)
{
sw.Stop();
return new
{
API = apiName,
Provider = provider,
回答 = "调用失败",
Token消耗 = 0,
耗时ms = sw.ElapsedMilliseconds,
ResponseId = "(无)",
状态 = $"{ex.Message[..Math.Min(60, ex.Message.Length)]}"
};
}
}
const string q = "请用一句话概括 .NET 平台的核心价值(限15字)";
Console.WriteLine($"测试问题:{q}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// ── 初始化三个 API 客户端 ──
var azureClient = AIClientHelper.GetAzureOpenAIClient();
// 1. Chat Completions API
IChatClient chatCompletionsClient = azureClient.GetChatClient("gpt-4o").AsIChatClient();
#pragma warning disable OPENAI001
// 2. Responses API
IChatClient responsesClient = azureClient.GetResponsesClient("gpt-4o").AsIChatClient();
// 3. Messages API(Claude via TokenAI)
IChatClient claudeClient = AIClientHelper.GetDefaultChatClient("Anthropic", "glm-4.5-air");
// 并发调用三个 API
var results = await Task.WhenAll(
BenchmarkApiAsync(chatCompletionsClient, "Chat Completions", "Azure OpenAI GPT-4o", q),
BenchmarkApiAsync(responsesClient, "Responses API", "Azure OpenAI GPT-4o", q),
BenchmarkApiAsync(claudeClient, "Messages API", "Claude (TokenAI)", q)
);
foreach (var r in results) { r.Display(); Console.WriteLine(); }
Console.WriteLine("并行对比完成");
Console.WriteLine("观察:相同的 IChatClient 接口调用,不同底层 API 的实现细节完全透明");
- 多轮对话状态管理对比,三种 API 在多轮对话的状态管理方式上存在根本性差异:
| 维度 | Chat Completions | Responses API | Messages API |
|---|---|---|---|
| 上下文维护方 | 客户端 | 服务端 | 客户端 |
| 每轮发送数据 | 全部历史消息 | 只发新消息 + ID | 全部历史消息 |
| Token 特征(多轮) | 输入 Token 通常随历史增长 | 免客户端重传历史;输入 Token 仍受上下文长度影响 | 输入 Token 通常随历史增长 |
| 代码复杂度 | 需维护 List | 只记 ConversationId | 需维护 List |
| MEAI 对应 | 手动追加 history | ChatOptions.ConversationId |
手动追加 history |
避免歧义:Responses API 的优势主要是“服务端托管状态、简化客户端代码”,并不保证每一轮 InputTokenCount 都更低。当前文回答很长时,后续轮输入 Token 仍可能明显上升。
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 多轮对话状态管理对比
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var roundTokens = new Dictionary<string, List<long?>>
{
["Chat Completions"] = new(),
["Responses API"] = new(),
["Messages API"] = new()
};
var questions = new[]
{
"中国四大发明是什么?只列名称",
"其中哪个发明对现代互联网影响最大?",
"请进一步解释这种影响"
};
Console.WriteLine("三轮对话 Token 消耗对比:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// === Chat Completions API(手动 history)===
var ccHistory = new List<ChatMessage> { new(ChatRole.System, "你是历史顾问,回答简洁。") };
for (int i = 0; i < questions.Length; i++)
{
ccHistory.Add(new ChatMessage(ChatRole.User, questions[i]));
var r = await chatCompletionsClient.GetResponseAsync(ccHistory);
ccHistory.AddMessages(r);
roundTokens["Chat Completions"].Add(r.Usage?.InputTokenCount);
Console.WriteLine($"[Chat Completions] 第{i+1}轮 Token: {r.Usage?.TotalTokenCount}");
}
// === Responses API(服务端托管;不保证每轮输入 Token 更低)===
var rOpts = new ChatOptions { Instructions = "你是历史顾问,回答简洁。" };
string? convId = null;
for (int i = 0; i < questions.Length; i++)
{
var opts = convId == null ? rOpts : new ChatOptions { ConversationId = convId };
var r = await responsesClient.GetResponseAsync(questions[i], opts);
convId = r.ConversationId;
roundTokens["Responses API"].Add(r.Usage?.InputTokenCount);
Console.WriteLine($"[Responses API] 第{i+1}轮 Token: {r.Usage?.TotalTokenCount}");
}
// === Messages API(手动 history)===
var maHistory = new List<ChatMessage> { new(ChatRole.System, "你是历史顾问,回答简洁。") };
for (int i = 0; i < questions.Length; i++)
{
maHistory.Add(new ChatMessage(ChatRole.User, questions[i]));
var r = await claudeClient.GetResponseAsync(maHistory);
maHistory.AddMessages(r);
roundTokens["Messages API"].Add(r.Usage?.InputTokenCount);
Console.WriteLine($"[Messages API] 第{i+1}轮 Token: {r.Usage?.TotalTokenCount}");
}
Console.WriteLine();
new {
Chat_Completions_Round1 = roundTokens["Chat Completions"][0],
Chat_Completions_Round3 = roundTokens["Chat Completions"][2],
Responses_API_Round1 = roundTokens["Responses API"][0],
Responses_API_Round3 = roundTokens["Responses API"][2],
Messages_API_Round1 = roundTokens["Messages API"][0],
Messages_API_Round3 = roundTokens["Messages API"][2]
}.Display();
Console.WriteLine();
Console.WriteLine("结论:");
Console.WriteLine(" Chat Completions & Messages API: 输入 Token 通常随历史增长");
Console.WriteLine(" Responses API: 省的是客户端状态管理;输入 Token 仍取决于历史上下文长度");
- 工具调用格式对比,三种 API 在工具定义和调用结果回传上有显著的格式差异。MEAI 的价值在于——开发者只需定义一次工具,三种 API 都能使用。
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 工具调用:同一工具,三种 API
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 定义工具(一次定义,三种 API 共用)
[Description("获取指定城市的天气预报")]
string GetForecast([Description("城市名")] string city, [Description("天数(1-7)")] int days = 3)
{
Console.WriteLine($" [工具] GetForecast({city}, {days}天)");
return $"{city} 未来{days}天预报:第1天晴25°C / 第2天多云23°C / 第3天小雨20°C";
}
var forecastTool = AIFunctionFactory.Create(GetForecast);
var toolOpts = new ChatOptions { Tools = [forecastTool], ToolMode = ChatToolMode.Auto };
// 用 MEAI 中间件包装三个客户端
var cc = chatCompletionsClient.AsBuilder().UseFunctionInvocation().Build();
var ra = responsesClient.AsBuilder().UseFunctionInvocation().Build();
var ma = claudeClient.AsBuilder().UseFunctionInvocation().Build();
const string toolQ = "上海未来3天天气怎么样?";
Console.WriteLine($"工具调用问题:{toolQ}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("<br>[Chat Completions API]");
var cr = await cc.GetResponseAsync(toolQ, toolOpts);
Console.WriteLine($"回答: {cr.Text}");
Console.WriteLine("<br>[Responses API]");
var rr = await ra.GetResponseAsync(toolQ, toolOpts);
Console.WriteLine($"回答: {rr.Text}");
Console.WriteLine("<br>[Messages API]");
var mr = await ma.GetResponseAsync(toolQ, toolOpts);
Console.WriteLine($"回答: {mr.Text}");
Console.WriteLine();
Console.WriteLine("工具调用对比完成");
Console.WriteLine("关键:相同的 AIFunctionFactory.Create() + FunctionInvokingChatClient");
Console.WriteLine(" MEAI 自动处理了三种 API 各自不同的工具格式细节!");
- 工具定义格式差异
// Chat Completions API(外部标签多态) { "type": "function", "function": { "name": "get_weather", "parameters": { ... } // ← parameters } } // Responses API(更简洁) { "type": "function", "name": "get_weather", // ← name 在顶层 "parameters": { ... } } // Messages API(内容块多态) { "name": "get_weather", // ← name 在顶层,无 type "input_schema": { ... } // ← input_schema(非 parameters) }
- 工具结果回传格式差异
// Chat Completions API { "role": "tool", "tool_call_id": "call_xxx", "content": "结果" } // Responses API(自动 Agentic Loop 处理,无需手动回传) // Messages API { "role": "user", // ← 注意是 user 角色! "content": [{"type": "tool_result", "tool_use_id": "toolu_xxx", "content": "结果"}] }
- MEAI 统一抽象的核心价值,MEAI 的 IChatClient 接口让业务代码与底层 API 实现完全解耦,实现真正的供应商无关(Vendor Agnostic)架构。
flowchart TB
subgraph App ["业务层(你的代码)"]
A["IChatClient.GetResponseAsync()"]
end
subgraph Middleware ["MEAI 中间件管道(可复用)"]
B["FunctionInvokingChatClient<br>自动工具调用"]
C["LoggingChatClient<br>日志记录"]
D["CachingChatClient<br>结果缓存"]
end
subgraph Impl ["实现层(按需替换)"]
E["OpenAIChatClient<br>(Chat Completions)"]
F["OpenAIResponsesChatClient<br>(Responses API)"]
G["AnthropicClient<br>(Messages API)"]
end
A --> B --> C --> D
D --> E
D --> F
D --> G
切换只需要一行代码
// 从 Chat Completions 切换到 Responses API:
var client = azureClient.GetChatClient("gpt-4o").AsIChatClient();
// 改为:
var client = azureClient.GetResponsesClient("gpt-4o").AsIChatClient();
// 从 Azure OpenAI 切换到 Claude:
var client = azureClient.GetChatClient("gpt-4o").AsIChatClient();
// 改为:
var client = AIClientHelper.GetDefaultChatClient("Anthropic", "glm-4.5-air");
// 业务代码完全不变!
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// MEAI 统一抽象:相同业务代码,三种底层 API
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 模拟一个「AI 写作助手」服务,完全不感知底层 API
async Task<string> RunAIWritingAssistant(IChatClient client, string clientName)
{
var history = new List<ChatMessage>
{
new(ChatRole.System, "你是一个专业的技术写作助手,文字简洁有力。")
};
// 任务 1:生成标题
history.Add(new ChatMessage(ChatRole.User, "为一篇介绍MEAI的技术文章生成一个吸引人的标题"));
var r1 = await client.GetResponseAsync(history);
history.AddMessages(r1);
// 任务 2:基于标题写摘要(上下文依赖)
history.Add(new ChatMessage(ChatRole.User, "基于这个标题,用30字写一段文章摘要"));
var r2 = await client.GetResponseAsync(history);
history.AddMessages(r2);
return $"[{clientName}]\n 标题:{r1.Text}<br> 摘要:{r2.Text}";
}
Console.WriteLine("AI 写作助手 - 相同业务代码,三种 API:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var r = await Task.WhenAll(
RunAIWritingAssistant(chatCompletionsClient, "Chat Completions"),
RunAIWritingAssistant(responsesClient, "Responses API"),
RunAIWritingAssistant(claudeClient, "Messages API")
);
foreach (var result in r) Console.WriteLine(result + "<br>");
Console.WriteLine("统一抽象演示完成");
Console.WriteLine("业务代码零修改,底层 API 随时可切换!");
- 迁移指南,当项目需要从一个 API 迁移到另一个时,基于 MEAI 的代码改动量极小。以下是完整的迁移路径。
Chat Completions → Responses API
// === 变更点(仅一行)===
// Before:
IChatClient client = azure.GetChatClient("gpt-4o").AsIChatClient();
// After:
IChatClient client = azure.GetResponsesClient("gpt-4o").AsIChatClient();
// === 可选优化:切换到 ConversationId 模式(利用服务端状态)===
// Before(手动 history):
history.AddMessages(response);
client.GetResponseAsync(history);
// After(ConversationId 续接):
var opts = new ChatOptions { ConversationId = prevResponse.ConversationId };
client.GetResponseAsync(newMessage, opts);
// === 可选:系统提示从 messages 迁移到 instructions ===
// Before:
var messages = new[] { new ChatMessage(ChatRole.System, "..."), ... };
// After:
var opts = new ChatOptions { Instructions = "..." };
Chat Completions → Messages API(切换到 Claude)
// === 变更点(仅一行)===
// Before:
IChatClient client = azure.GetChatClient("gpt-4o").AsIChatClient();
// After:
IChatClient client = AIClientHelper.GetDefaultChatClient("Anthropic", "claude-sonnet-4-5");
// === 注意:Claude 需要 MaxTokens(可通过 ChatOptions 设置)===
var opts = new ChatOptions { MaxOutputTokens = 1024 };
// === 其他代码完全不变!===
迁移风险评估
| 迁移路径 | 代码改动 | 行为差异 | 注意事项 |
|---|---|---|---|
| CC → Responses | 极小(1行) | 低 | ConversationId 不跨 session 持久 |
| CC → Messages | 极小(1行) | 低 | max_tokens 必填,模型响应风格不同 |
| Responses → CC | 小 | 中 | 需改为手动 history 管理 |
| Responses → Messages | 小 | 中 | 失去内置工具、服务端状态 |
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 迁移演示:Chat Completions → Messages API(Claude)
// 展示 MEAI 的迁移代价有多低
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Console.WriteLine("迁移演示:从 Chat Completions API 切换到 Messages API");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// 业务函数(完全不变)
async Task<string> ProcessCustomerQuery(IChatClient client, string query)
{
var messages = new List<ChatMessage>
{
new(ChatRole.System, "你是电商平台的智能客服,回答简洁友好,不超过30字。"),
new(ChatRole.User, query)
};
var resp = await client.GetResponseAsync(messages);
return resp.Text;
}
const string customerQuery = "我的快递为什么还没到,我昨天下单的";
Console.WriteLine($"用户问题:{customerQuery}");
Console.WriteLine();
// Before:Chat Completions API
Console.WriteLine("Chat Completions API(迁移前):");
var ccAnswer = await ProcessCustomerQuery(chatCompletionsClient, customerQuery);
Console.WriteLine($" {ccAnswer}");
// After:Messages API(只改了 client 初始化,业务函数 ProcessCustomerQuery 完全不变!)
Console.WriteLine();
Console.WriteLine("Messages API(迁移后,只修改了客户端初始化):");
IChatClient newClient = AIClientHelper.GetDefaultChatClient("Anthropic", "glm-4.5-air"); // ← 只改这一行
var maAnswer = await ProcessCustomerQuery(newClient, customerQuery);
Console.WriteLine($" {maAnswer}");
Console.WriteLine();
Console.WriteLine("迁移演示完成");
Console.WriteLine("ProcessCustomerQuery 函数一行代码都没改!这就是 MEAI 抽象的价值");
- 选型决策树
flowchart TD
A["开始选型"] --> B{"你的主要 AI 提供商是?"}
B -->|"仅 Anthropic Claude"| C["✅ Messages API<br>Anthropic v12+<br>AsIChatClient()"]
B -->|"仅 OpenAI / Azure OpenAI"| D{"项目主要场景?"}
B -->|"多提供商混合"| E["✅ Chat Completions API<br>行业标准,兼容最广"]
D -->|"新 Agent 项目"| F{"需要内置工具?"}
D -->|"简单对话/精细控制"| G["✅ Chat Completions API<br>稳定成熟,文档丰富"]
D -->|"已有 Chat Completions 项目"| H{"是否遇到这些问题?"}
F -->|"需要 Web Search / Code Interpreter / MCP"| I["✅ Responses API<br>内置工具零代码启用"]
F -->|"只需自定义函数"| J["✅ Responses API<br>服务端状态更高效"]
H -->|"多轮对话 Token 成本高<br>客户端状态管理复杂"| K["✅ 迁移到 Responses API<br>一行代码切换"]
H -->|"没有以上问题"| G
C --> L["通过 MEAI IChatClient 统一使用"]
E --> L
G --> L
I --> L
J --> L
K --> L
量化选型矩阵
| 指标 | Chat Completions | Responses API | Messages API |
|---|---|---|---|
| 提供商覆盖 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| Agent 能力 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 多模态原生支持 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 状态管理便利性 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| Token 效率(多轮) | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 内置工具 | ❌ | ✅ Web/Code/File | ❌ |
| 扩展思考 | ❌ | ❌ | ✅ |
| 迁移成本 | 基准 | 极低 | 极低 |
| 社区生态 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 未来发展 | 功能冻结 | 主力 | 持续演进 |
2. Chat Completions API 深度解析
Chat Completions API 是 OpenAI 于 2022 年随 ChatGPT 发布的对话补全接口,凭借简洁的 messages + choices 模型迅速成为 AI 对话领域的事实行业标准。
Azure OpenAI、DeepSeek、Qwen(百炼)、月之暗面、百川智能等几乎所有主流提供商都与其保持兼容,是目前覆盖面最广的 AI API 范式。
设计哲学:无状态 · 客户端全权控制
Chat Completions API 的核心理念是无状态——服务端不存储任何对话历史,每次请求必须携带完整的消息上下文。
这既是其灵活性所在,也是需要注意的管理成本。正确的消息历史维护模式
// 1. 初始化历史记录(包含系统提示)
var history = new List<Microsoft.Extensions.AI.ChatMessage>
{
new(ChatRole.System, "系统提示"),
new(ChatRole.User, "第一个问题")
};
// 2. 发送并获取响应
var response = await client.GetResponseAsync(history);
// 3. 关键:必须手动将助手回复追加到历史
history.AddMessages(response); // MEAI 提供的扩展方法
// 4. 继续对话
history.Add(new ChatMessage(ChatRole.User, "下一个问题"));
常见错误:忘记调用 history.AddMessages(response) 追加助手回复,导致上下文丢失。
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 多轮对话 - 手动维护消息历史
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Console.WriteLine("多轮对话演示 - 旅行助手场景:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var history = new List<Microsoft.Extensions.AI.ChatMessage>
{
new(ChatRole.System, "你是一个专业的中国旅行助手,回答简洁,每次回复不超过30字。")
};
// 第 1 轮
var q1 = "推荐一个适合春天旅游的中国城市,只说一个";
history.Add(new Microsoft.Extensions.AI.ChatMessage(ChatRole.User, q1));
Console.WriteLine($"\n第1轮问: {q1}");
var r1 = await chatClient.GetResponseAsync(history);
Console.WriteLine($"第1轮答: {r1.Text}");
Console.WriteLine($" [Token: {r1.Usage?.TotalTokenCount}, 历史消息数: {history.Count}]");
history.AddMessages(r1); // 关键:追加助手回复
// 第 2 轮(上下文追问)
var q2 = "这个城市的春季气候如何?";
history.Add(new Microsoft.Extensions.AI.ChatMessage(ChatRole.User, q2));
Console.WriteLine($"\n第2轮问: {q2}");
var r2 = await chatClient.GetResponseAsync(history);
Console.WriteLine($"第2轮答: {r2.Text}");
Console.WriteLine($" [Token: {r2.Usage?.TotalTokenCount}, 历史消息数: {history.Count}]");
history.AddMessages(r2);
// 第 3 轮(进一步追问)
var q3 = "春季去那里有什么必吃美食?";
history.Add(new Microsoft.Extensions.AI.ChatMessage(ChatRole.User, q3));
Console.WriteLine($"\n第3轮问: {q3}");
var r3 = await chatClient.GetResponseAsync(history);
Console.WriteLine($"第3轮答: {r3.Text}");
Console.WriteLine($" [Token: {r3.Usage?.TotalTokenCount}, 历史消息数: {history.Count + 1}]");
Console.WriteLine();
Console.WriteLine("Token 消耗分析(无状态API的特点):");
new {
说明 = "Token随轮次线性增长(每次发全量历史)",
第1轮Token = r1.Usage?.TotalTokenCount,
第2轮Token = r2.Usage?.TotalTokenCount,
第3轮Token = r3.Usage?.TotalTokenCount
}.Display();
Console.WriteLine("多轮对话演示完成");
Console.WriteLine(" 注意:每轮 Token 消耗递增,这是无状态 API 的成本特点");
Chat Completions API 最大的价值之一是广泛兼容性。通过 MEAI,可以用完全相同的业务代码切换不同的 AI 提供商,实现供应商无关(Vendor Agnostic)的架构。
// Azure OpenAI
IChatClient client = AIClientHelper.GetAzureOpenAIClient()
.GetChatClient("gpt-4o").AsIChatClient();
// DeepSeek
IChatClient client = AIClientHelper.GetDeepSeekClient()
.GetChatClient("deepseek-chat").AsIChatClient();
// Qwen(阿里云百炼)
IChatClient client = AIClientHelper.GetQwenClient()
.GetChatClient("qwen-max").AsIChatClient();
// 业务代码完全不变!
var response = await client.GetResponseAsync("你好");
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 多提供商并发对比(相同问题,不同提供商)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 公共业务方法(不感知底层提供商)
async Task<object> TestProviderAsync(IChatClient client, string providerName, string model, string question)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
try
{
var resp = await client.GetResponseAsync(question);
sw.Stop();
return new
{
提供商 = providerName,
模型 = model,
回答 = resp.Text,
Token消耗 = resp.Usage?.TotalTokenCount ?? 0,
耗时ms = sw.ElapsedMilliseconds,
状态 = "成功"
};
}
catch (Exception ex)
{
sw.Stop();
return new
{
提供商 = providerName,
模型 = model,
回答 = $"调用失败",
Token消耗 = 0,
耗时ms = sw.ElapsedMilliseconds,
状态 = $" {ex.Message[..Math.Min(50, ex.Message.Length)]}"
};
}
}
const string testQ = "用10个字描述软件工程师的职业特点";
Console.WriteLine($"测试问题:{testQ}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// 初始化各提供商客户端
IChatClient azureClient2 = AIClientHelper.GetAzureOpenAIClient().GetChatClient("gpt-4o").AsIChatClient();
IChatClient deepSeekClient = AIClientHelper.GetDeepSeekClient().GetChatClient("deepseek-chat").AsIChatClient();
IChatClient qwenClient = AIClientHelper.GetQwenClient().GetChatClient("qwen-max").AsIChatClient();
// 并行调用,节省等待时间
var results = await Task.WhenAll(
TestProviderAsync(azureClient2, "Azure OpenAI", "gpt-4o", testQ),
TestProviderAsync(deepSeekClient, "DeepSeek", "deepseek-chat", testQ),
TestProviderAsync(qwenClient, "Qwen(百炼)", "qwen-max", testQ)
);
foreach (var result in results)
{
result.Display();
Console.WriteLine();
}
Console.WriteLine("多提供商对比演示完成");
Console.WriteLine("核心价值:相同代码,多家提供商,灵活切换,避免厂商锁定!");
3. Responses API 深度解析
Responses API 是 OpenAI 于 2025 年推出的新一代对话接口,定位为 Agentic by Default——专为 AI Agent 场景设计,是 Chat Completions API 的进化版与长期替代方向。
相比 Chat Completions API 的「无状态 · 客户端控制」,Responses API 将状态管理、工具调用循环、内置工具能力都迁移到了服务端,大幅简化了客户端代码。
设计哲学:Agentic by Default
Responses API 的核心创新是将 「对话状态」从客户端迁移到服务端,并内置了 Agentic Loop,实现了从「手动控制」到「声明式 Agent」的跨越。
flowchart LR
subgraph CC ["Chat Completions 模式"]
direction TB
C1["客户端维护<br>完整 messages 列表"]
C2["每轮发送全部历史<br>Token 线性增长"]
C3["手动检测 tool_calls<br>手动循环调用"]
end
subgraph RA ["Responses API 模式"]
direction TB
R1["服务端存储历史<br>客户端只需 response_id"]
R2["每轮只发新消息<br>Token 大幅节省"]
R3["Agentic Loop<br>自动工具调用与续接"]
end
Responses API 的每次响应都会返回一个唯一的 ResponseId,这是服务端状态管理的基础。
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 基础对话 - 验证 ResponseId / ConversationId
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Console.WriteLine("Responses API 基础对话测试:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var resp = await responsesClient.GetResponseAsync(
"用一句话介绍中国的万里长城(限20字以内)");
new {
API类型 = "Responses API",
提供商 = "Azure OpenAI",
回答 = resp.Text,
模型 = resp.ModelId,
ResponseId = resp.ResponseId, // 每次响应的唯一 ID
ConversationId = resp.ConversationId, // 用于服务端多轮对话续接
PromptToken = resp.Usage?.InputTokenCount,
OutputToken = resp.Usage?.OutputTokenCount,
TotalToken = resp.Usage?.TotalTokenCount
}.Display();
Console.WriteLine();
Console.WriteLine("关键观察:");
Console.WriteLine(" ResponseId - 每次调用生成的唯一 ID,服务端存储了本次完整对话上下文");
Console.WriteLine(" ConversationId - MEAI 封装的多轮对话 ID,用于 ChatOptions.ConversationId");
Console.WriteLine("基础对话成功");
Responses API 最革命性的特性:服务端存储对话历史。客户端只需在每轮提供 ConversationId,无需发送历史消息,显著降低客户端复杂度。
避免歧义:服务端托管历史并不等于“每轮 Token 一定更低”。如果前文(尤其助手回复)变长,后续轮 InputTokenCount 仍可能上升,因为模型依然需要读取这些上下文。
sequenceDiagram
participant App as 客户端
participant API as Responses API
participant Store as 服务端存储
App->>API: input: 用户消息1
API->>Store: 存储 response_id_1 + 完整对话
API-->>App: response_id_1 + 回复1
App->>API: input: 用户消息2\n+ previous_response_id: response_id_1
API->>Store: 加载历史 + 存储 response_id_2
API-->>App: response_id_2 + 回复2
Note over App: 客户端代码极简!只需记住 ConversationId
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 服务端托管多轮对话 - ConversationId 续接
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Console.WriteLine("Responses API 多轮对话 - 服务端托管状态:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// 使用 instructions 设置系统提示(Responses API 的独立参数)
var systemOpts = new ChatOptions
{
Instructions = "你是一个专业的中国历史顾问,回答简洁,每次不超过40字。"
};
// 第 1 轮
Console.WriteLine("第1轮: 中国历史上哪个王朝存在时间最长?");
var rr1 = await responsesClient.GetResponseAsync(
"中国历史上哪个王朝存在时间最长?", systemOpts);
Console.WriteLine($"第1轮: {rr1.Text}");
Console.WriteLine($" [ConversationId: {rr1.ConversationId}, Token: {rr1.Usage?.TotalTokenCount}]");
// 只需保存 ConversationId,无需维护消息列表!
var continueOpts = new ChatOptions { ConversationId = rr1.ConversationId };
// 第 2 轮
Console.WriteLine();
Console.WriteLine("第2轮: 这个王朝最著名的皇帝是谁?");
var rr2 = await responsesClient.GetResponseAsync(
"这个王朝最著名的皇帝是谁?", continueOpts);
Console.WriteLine($"第2轮: {rr2.Text}");
Console.WriteLine($" [ConversationId: {rr2.ConversationId}, Token: {rr2.Usage?.TotalTokenCount}]");
continueOpts = new ChatOptions { ConversationId = rr2.ConversationId };
// 第 3 轮
Console.WriteLine();
Console.WriteLine("第3轮: 他在位期间最大的成就是什么?");
var rr3 = await responsesClient.GetResponseAsync(
"他在位期间最大的成就是什么?", continueOpts);
Console.WriteLine($"第3轮: {rr3.Text}");
Console.WriteLine($" [ConversationId: {rr3.ConversationId}, Token: {rr3.Usage?.TotalTokenCount}]");
Console.WriteLine();
new {
说明 = "ConversationId 省的是客户端传参,不保证每轮输入 Token 下降",
第1轮Token = rr1.Usage?.InputTokenCount,
第2轮Token = rr2.Usage?.InputTokenCount,
第3轮Token = rr3.Usage?.InputTokenCount
}.Display();
Console.WriteLine();
Console.WriteLine("服务端托管多轮对话完成");
Console.WriteLine("全程无需维护 messages 数组;但 InputTokenCount 仍会受历史上下文长度影响");
- 内置工具 - Web Search
Responses API 提供了开箱即用的 Web Search 内置工具,模型可以自主决定何时搜索互联网,无需用户手动实现搜索逻辑。
启动方法
#pragma warning disable OPENAI001
// 通过 RawRepresentationFactory 和 CreateResponseOptions 传递内置工具给 Responses API
var webSearchOpts = new ChatOptions()
{
RawRepresentationFactory = client =>
{
var options = new CreateResponseOptions(){
Tools = {ResponseTool.CreateWebSearchTool()}
};
return options;
}
};
#pragma warning restore OPENAI001
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 内置工具:Web Search(联网搜索)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
using OpenAI.Responses;
#pragma warning disable OPENAI001
// 通过 RawRepresentationFactory 和 CreateResponseOptions 传递内置工具给 Responses API
var webSearchOpts = new ChatOptions()
{
RawRepresentationFactory = client =>
{
var options = new CreateResponseOptions(){
Tools = {ResponseTool.CreateWebSearchTool()}
};
return options;
}
};
#pragma warning restore OPENAI001
Console.WriteLine("内置 Web Search 工具测试:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var webQuestion = "最新的伊朗形势如何";
Console.WriteLine($"问题(需要实时信息):{webQuestion}");
Console.WriteLine();
var webResp = await responsesClient.GetResponseAsync(webQuestion, webSearchOpts);
Console.WriteLine("回答(来自实时网络搜索):");
Console.WriteLine(webResp.Text);
Console.WriteLine();
new {
API类型 = "Responses API",
内置工具 = "Web Search Preview",
ResponseId = webResp.ResponseId,
Token消耗 = webResp.Usage?.TotalTokenCount
}.Display();
Console.WriteLine();
Console.WriteLine("Web Search 调用完成");
Console.WriteLine("模型自主判断是否需要搜索,无需客户端实现搜索逻辑!");
- 内置工具 - Code Interpreter
Code Interpreter 是 Responses API 的另一个强大内置工具,让模型能够在沙箱环境中编写并执行 Python 代码,完成数学计算、数据分析、图表生成等复杂任务,并自动将结果整合到回答中。
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 内置工具:Code Interpreter(代码解释器)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
using OpenAI.Responses;
#pragma warning disable OPENAI001
// 通过 RawRepresentationFactory 和 CreateResponseOptions 传递内置工具给 Responses API
CodeInterpreterToolContainer container = new (
CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration());
var codeInterpreterOpts = new ChatOptions()
{
RawRepresentationFactory = client =>
{
var options = new CreateResponseOptions(){
Tools = {ResponseTool.CreateCodeInterpreterTool(container)}
};
return options;
}
};
#pragma warning restore OPENAI001
Console.WriteLine("内置 Code Interpreter 工具测试:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// 数学计算任务(无法靠语言模型直接精确计算)
var mathQuestion = "计算斐波那契数列的前20项,并找出其中所有的质数";
Console.WriteLine($"计算任务:{mathQuestion}");
Console.WriteLine();
var codeResp = await responsesClient.GetResponseAsync(mathQuestion, codeInterpreterOpts);
Console.WriteLine("计算结果(模型编写并执行了 Python 代码):");
Console.WriteLine(codeResp.Text);
Console.WriteLine();
new {
API类型 = "Responses API",
内置工具 = "Code Interpreter",
ResponseId = codeResp.ResponseId,
Token消耗 = codeResp.Usage?.TotalTokenCount
}.Display();
Console.WriteLine();
Console.WriteLine("Code Interpreter 调用完成");
Console.WriteLine("模型在沙箱中自动编写 Python 代码并执行,精确计算结果!");
- Agentic Loop 深度解析
Agentic Loop 是 Responses API 自动处理工具调用的机制。与 Chat Completions API 需要客户端手动循环不同,Responses API 可以在单次请求中自动完成「决策 → 调用 → 续接」的完整循环。
内置工具的 Agentic Loop(全自动)
flowchart TD
A[客户端发送问题] --> B[模型分析问题]
B --> C{需要工具?}
C -->|否| G[直接返回回答]
C -->|是| D[生成工具调用参数]
D --> E["服务端执行工具\n(Web Search / Code Interpreter)"]
E --> F[工具结果注入上下文]
F --> B
G --> H[客户端收到最终回答]
style E fill:#4CAF50,color:#fff
style H fill:#2196F3,color:#fff
使用 FunctionInvokingChatClient 中间件,MEAI 会代理客户端执行自定义函数并自动续接:
sequenceDiagram
participant App as 客户端
participant MEAI as FunctionInvokingChatClient
participant API as Responses API
participant Fn as 本地函数
App->>MEAI: GetResponseAsync(问题, tools)
MEAI->>API: 发送请求
API-->>MEAI: output: [function_call]
MEAI->>Fn: 执行本地函数
Fn-->>MEAI: 函数结果
MEAI->>API: 发送函数结果(前一个 response_id)
API-->>MEAI: 最终回答
MEAI-->>App: 最终结果
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 自定义 Function Calling + Agentic Loop
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 定义本地工具函数
[Description("查询指定股票代码的实时价格(模拟数据)")]
string GetStockPrice([Description("股票代码,如:MSFT、AAPL")] string ticker)
{
Console.WriteLine($" [工具调用] GetStockPrice(\"{ticker}\")");
var prices = new Dictionary<string, string>
{
["MSFT"] = "¥425.30(+1.2%)",
["AAPL"] = "¥189.50(-0.3%)",
["GOOG"] = "¥175.20(+0.8%)",
["NVDA"] = "¥875.60(+3.5%)"
};
return prices.TryGetValue(ticker.ToUpper(), out var price)
? $"{ticker} 当前价格: {price}"
: $"{ticker}: 暂无数据";
}
[Description("获取股票的基本财务指标")]
string GetStockMetrics([Description("股票代码")] string ticker)
{
Console.WriteLine($" [工具调用] GetStockMetrics(\"{ticker}\")");
return ticker.ToUpper() switch
{
"MSFT" => "市盈率: 35.2, 市值: $3.2T, 年增长: +15%",
"AAPL" => "市盈率: 28.7, 市值: $2.9T, 年增长: +8%",
"NVDA" => "市盈率: 65.3, 市值: $2.1T, 年增长: +180%",
_ => $"{ticker}: 暂无财务数据"
};
}
var stockTools = new[]
{
AIFunctionFactory.Create(GetStockPrice),
AIFunctionFactory.Create(GetStockMetrics)
};
// 构建带自动 Agentic Loop 的 Agent 客户端
IChatClient agentClient = responsesClient
.AsBuilder()
.UseFunctionInvocation() // MEAI 中间件:自动执行工具调用循环
.Build();
var toolOpts = new ChatOptions
{
Tools = stockTools,
ToolMode = ChatToolMode.Auto
};
const string agentQuestion = "帮我分析 MSFT 和 NVDA 哪只股票更适合长期持有?请结合价格和财务指标";
Console.WriteLine("Agentic Loop 演示 - 股票分析助手:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"复杂问题:{agentQuestion}");
Console.WriteLine();
Console.WriteLine("工具调用过程:");
var agentResp = await agentClient.GetResponseAsync(agentQuestion, toolOpts);
Console.WriteLine();
Console.WriteLine("最终分析报告:");
Console.WriteLine(agentResp.Text);
Console.WriteLine();
new {
Token消耗 = agentResp.Usage?.TotalTokenCount,
ConversationId = agentResp.ConversationId
}.Display();
Console.WriteLine();
Console.WriteLine("Agentic Loop 演示完成");
Console.WriteLine("模型自动调用了多个工具,MEAI 的 FunctionInvokingChatClient 中间件全程自动处理!");
- instructions 与系统提示
Responses API 提供了独立的 instructions 参数来设置系统提示,这比 Chat Completions API 的 role: "system" 消息更清晰。
// Responses API:独立 instructions 参数
var opts = new ChatOptions
{
Instructions = "你是一个友好的助手"
};
var resp = await client.GetResponseAsync("问题", opts);
// Chat Completions API:messages 数组中的 system 消息
var messages = new List<ChatMessage>
{
new(ChatRole.System, "你是一个友好的助手"),
new(ChatRole.User, "问题")
};
注意: MEAI 通过 ChatOptions.Instructions 映射到 Responses API 的 instructions 字段。
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// instructions 参数 vs system 消息
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Console.WriteLine("角色扮演对比 - instructions 参数效果:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
const string question = "今天心情不太好,有什么建议?";
// 方式 1:乐观教练角色(使用 instructions)
var coachOpts = new ChatOptions
{
Instructions = "你是一个充满正能量的人生教练,用鼓励性语言回答,语言简短有力,不超过30字。"
};
var coaching = await responsesClient.GetResponseAsync(question, coachOpts);
// 方式 2:心理咨询师角色
var therapistOpts = new ChatOptions
{
Instructions = "你是一位温和的心理咨询师,用平和温馨的语言回应,语言简短,不超过30字。"
};
var therapy = await responsesClient.GetResponseAsync(question, therapistOpts);
Console.WriteLine($"问题:{question}");
Console.WriteLine();
new {
角色1励志教练 = coaching.Text,
角色2心理咨询师 = therapy.Text
}.Display();
Console.WriteLine();
Console.WriteLine("instructions 参数演示完成");
Console.WriteLine("独立 instructions 参数语义更清晰,不会与对话 messages 混淆");
4. Messages API 深度解析
Messages API 是 Anthropic 为 Claude 系列模型设计的原生对话接口。区别于 OpenAI 的字符串/数组内容模型,Messages API 以**内容块(Content Blocks)**为核心设计单元,提供了更加结构化、类型安全的多模态交互方式。
设计哲学:内容块驱动 · 显式多模态
Messages API 的核心创新是将消息内容拆解为类型化的内容块数组,每种内容类型都有明确的结构定义。
flowchart LR
subgraph Request ["请求结构"]
direction TB
S["system: '你是助手'"]
M["messages: [<br> {role: user, content: [块1, 块2]},<br> {role: assistant, content: [块3]}<br>]"]
end
subgraph ContentBlocks ["内容块类型"]
direction TB
T["TextBlock<br>{type: 'text', text: '...'}"]
I["ImageBlock<br>{type: 'image', source: {...}}"]
TU["ToolUseBlock<br>{type: 'tool_use', id, name, input}"]
TR["ToolResultBlock<br>{type: 'tool_result', tool_use_id, content}"]
TH["ThinkingBlock<br>{type: 'thinking', thinking: '...'}"]
D["DocumentBlock<br>{type: 'document', source: {...}}"]
end
- 原生 SDK 调用 Messages API
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 原生 Anthropic SDK 调用 Messages API
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 获取配置了 TokenAI 代理的 AnthropicClient(通过 AIClientHelper)
var anthropicClient = AIClientHelper.GetAnthropicClient();
Console.WriteLine("原生 Messages API 调用测试:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// 构造 MessageCreateParams(与 REST API 1:1 映射)
var parameters = new MessageCreateParams
{
Model = "glm-4.5-air", // 强类型枚举
MaxTokens = 512, // Messages API 必填!
System = "你是一个专业的中国旅行助手,回答简洁,每次回复不超过30字。",
Messages =
[
new MessageParam
{
Role = Role.User,
Content = "MEAI(Microsoft.Extensions.AI)相比直接使用 OpenAI SDK 有什么优势?"
}
]
};
var message = await anthropicClient.Messages.Create(parameters);
message.Display();
// 访问原生响应结构
Console.WriteLine("原生响应结构解析:");
new {
消息ID = message.ID,
类型 = message.Type,
角色 = message.Role,
内容块数量 = message.Content.Count,
第一个块类型 = message.Content[0].Type,
文本内容 = message.Content[0].ToString(),
终止原因 = message.StopReason, // stop_reason(非 finish_reason)
InputToken = message.Usage.InputTokens,
OutputToken = message.Usage.OutputTokens
}.Display();
Console.WriteLine();
Console.WriteLine("原生 Messages API 调用成功");
Console.WriteLine("注意:stop_reason(非 finish_reason)、max_tokens 必填、Content Blocks 结构");
- 通过 MEAI 使用 Messages API(推荐)
通过 AsIChatClient() 将 Anthropic SDK 接入 MEAI 统一接口,享受 MEAI 生态(中间件、依赖注入等)的同时使用 Claude 模型。
// 接入 MEAI 统一接口
IChatClient claudeClient = anthropicClient.AsIChatClient(Model.ClaudeSonnet4_5_20250929);
// 或使用 AIClientHelper 的快捷方法
IChatClient claudeClient = AIClientHelper.GetDefaultChatClient("Anthropic", "claude-sonnet-4-5");
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// MEAI 集成:AsIChatClient() 统一接口
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 通过 AIClientHelper 获取 Claude IChatClient
IChatClient claudeClient = AIClientHelper.GetDefaultChatClient("Anthropic", "glm-4.5-air");
Console.WriteLine("Claude MEAI 客户端验证:");
Console.WriteLine($" 实现类: {claudeClient.GetType().Name}");
Console.WriteLine();
// 使用与 Chat Completions API 完全相同的调用方式!
Console.WriteLine("通过 MEAI 调用 Claude:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var resp = await claudeClient.GetResponseAsync("用一句话说明 Claude 和 GPT 最大的不同(限20字)");
new {
API类型 = "Messages API",
提供商 = "Anthropic Claude (TokenAI)",
回答 = resp.Text,
模型 = resp.ModelId,
ResponseId = resp.ResponseId,
TotalToken = resp.Usage?.TotalTokenCount
}.Display();
Console.WriteLine();
Console.WriteLine("MEAI + Claude 调用成功");
Console.WriteLine("接口完全一致,业务代码无需感知底层是 OpenAI 还是 Anthropic!");
- 多轮对话(客户端手动维护历史)
Messages API 与 Chat Completions API 相同,是无状态的——客户端需要维护完整的消息历史。但其 content 是内容块数组,格式更结构化。
// 工具调用结果必须通过 user 角色 + tool_result 块传递
// ❌ 不能用 role: "tool"(OpenAI 风格)
{
"role": "user", // ← 注意是 user!
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_xxx",
"content": "工具执行结果"
}
]
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 多轮对话 - 手动维护消息历史
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Console.WriteLine("Messages API 多轮对话 - 技术面试场景:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// MEAI 统一消息格式(与 Chat Completions API 完全相同的维护方式)
var history = new List<ChatMessage>
{
new(ChatRole.System, "你是一位严格但友好的.NET技术面试官,每次回答不超过50字。")
};
// 第 1 轮
var q1 = "请解释什么是依赖注入(DI)?";
history.Add(new ChatMessage(ChatRole.User, q1));
Console.WriteLine($"<br>候选人: {q1}");
var r1 = await claudeClient.GetResponseAsync(history);
Console.WriteLine($"面试官: {r1.Text}");
Console.WriteLine($" [Token: {r1.Usage?.TotalTokenCount}]");
history.AddMessages(r1);
// 第 2 轮(深入追问)
var q2 = "在.NET中有哪几种DI生命周期?各有什么区别?";
history.Add(new ChatMessage(ChatRole.User, q2));
Console.WriteLine($"<br>候选人: {q2}");
var r2 = await claudeClient.GetResponseAsync(history);
Console.WriteLine($"面试官: {r2.Text}");
Console.WriteLine($" [Token: {r2.Usage?.TotalTokenCount}]");
history.AddMessages(r2);
// 第 3 轮(场景应用)
var q3 = "Singleton 和 Scoped 在 Web API 中各自适合什么场景?";
history.Add(new ChatMessage(ChatRole.User, q3));
Console.WriteLine($"<br>候选人: {q3}");
var r3 = await claudeClient.GetResponseAsync(history);
Console.WriteLine($"面试官: {r3.Text}");
Console.WriteLine($" [Token: {r3.Usage?.TotalTokenCount}]");
Console.WriteLine();
Console.WriteLine("Token 趋势(与 Chat Completions API 相同,随轮次线性增长):");
new {
第1轮Token = r1.Usage?.TotalTokenCount,
第2轮Token = r2.Usage?.TotalTokenCount,
第3轮Token = r3.Usage?.TotalTokenCount
}.Display();
Console.WriteLine("多轮对话完成");
- 工具调用(tool_use / tool_result 内容块)
Messages API 的工具调用使用内容块来表达,格式与 OpenAI 有显著差异:
// ✅ Messages API 工具定义
{
"name": "get_weather", // name 在顶层
"description": "获取天气",
"input_schema": { // ⭐ 叫 input_schema,非 parameters
"type": "object",
"properties": { "city": {"type": "string"} },
"required": ["city"]
}
}
// Chat Completions API 工具定义
{
"type": "function",
"function": {
"name": "get_weather", // name 在嵌套 function 对象中
"parameters": { ... } // 叫 parameters
}
}
通过 MEAI 的 FunctionInvokingChatClient 中间件,这些格式差异会被自动隐藏。
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 工具调用 - MEAI 屏蔽格式差异
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 定义工具(与 Chat Completions API 完全相同的定义方式)
[Description("查询指定城市的当前天气")]
string GetWeather([Description("城市名")] string city)
{
Console.WriteLine($" [工具调用] GetWeather(\"{city}\")");
return city switch
{
"北京" => "晴,25°C",
"上海" => "多云,28°C",
"成都" => "阴,22°C",
_ => $"{city}: 暂无数据"
};
}
[Description("获取某地标性建筑或景点的简介")]
string GetLandmarkInfo([Description("景点名称")] string landmark)
{
Console.WriteLine($" [工具调用] GetLandmarkInfo(\"{landmark}\")");
return landmark switch
{
"故宫" => "明清两代皇家宫殿,世界最大古代木结构建筑群,始建于1406年",
"长城" => "古代中国的军事防御工程,全长21196千米,是世界奇迹之一",
"西湖" => "杭州著名风景名胜区,以湖光山色和历史人文闻名于世",
_ => $"{landmark}: 暂无介绍"
};
}
var tools = new[]
{
AIFunctionFactory.Create(GetWeather),
AIFunctionFactory.Create(GetLandmarkInfo)
};
// 配置 MEAI 自动工具调用中间件
IChatClient agentClient = claudeClient
.AsBuilder()
.UseFunctionInvocation() // MEAI 自动处理 tool_use/tool_result 内容块格式
.Build();
var toolOpts = new ChatOptions
{
Tools = tools,
ToolMode = ChatToolMode.Auto
};
Console.WriteLine("工具调用演示(Claude + MEAI 自动处理内容块格式):");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
const string question = "我明天想去北京旅游,帮我查一下北京天气,并介绍一下故宫";
Console.WriteLine($"问题:{question}");
Console.WriteLine();
Console.WriteLine("工具调用过程:");
var toolResp = await agentClient.GetResponseAsync(question, toolOpts);
Console.WriteLine();
Console.WriteLine("最终回答:");
Console.WriteLine(toolResp.Text);
Console.WriteLine();
new { Token消耗 = toolResp.Usage?.TotalTokenCount }.Display();
Console.WriteLine();
Console.WriteLine("工具调用完成");
Console.WriteLine("MEAI 自动将用户定义的工具转换为 input_schema 格式,并处理 tool_result 内容块!");
- 扩展思考模式(Extended Thinking)
Extended Thinking 是 Messages API 独有的深度推理功能,让 Claude 在回答之前进行显式的内部推理过程,以 thinking 内容块的形式返回。
启用方式(原生 SDK)
var params = new MessageCreateParams
{
Model = Model.ClaudeClaudeOpus4_5_20250514, // 支持 Extended Thinking 的模型
MaxTokens = 8000,
Thinking = new EnabledThinkingConfigParam { BudgetTokens = 5000 }, // 思考预算
Messages = [...]
};
thinking 内容块仅在 Messages API 中原生支持,MEAI 目前通过
AdditionalProperties或直接使用原生 SDK 访问。
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 扩展思考模式(Extended Thinking)
// 使用支持 thinking 的 Claude 模型
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Console.WriteLine("Extended Thinking 模式演示:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var thinkingParams = new MessageCreateParams
{
Model = "glm-4.5-air", // 支持 Extended Thinking 的模型
MaxTokens = 4000,
// 启用 Extended Thinking,预算 2000 tokens
Thinking = new ThinkingConfigParam( new ThinkingConfigEnabled(){
BudgetTokens = 2000
}),
System = "你是一个擅长逻辑分析的顾问。",
Messages =
[
new MessageParam
{
Role = Role.User,
Content = "一个.NET开发团队在选择AI API时,应该优先考虑哪些因素?请分析Chat Completions API和Responses API各自的适用场景。"
}
]
};
try
{
var thinkingMessage = await anthropicClient.Messages.Create(thinkingParams);
Console.WriteLine($"响应包含 {thinkingMessage.Content.Count} 个内容块:");
for (int i = 0; i < thinkingMessage.Content.Count; i++)
{
var block = thinkingMessage.Content[i];
Console.WriteLine($"<br> 块 {i + 1}: type = {block.Type}");
if (block.TryPickThinking(out var thinkingBlock))
{
var thinkingText = thinkingBlock.Thinking;
Console.WriteLine($" [Thinking 内容 - 前100字]: {thinkingText?[..Math.Min(100, thinkingText.Length)]}...");
}
else if (block.TryPickText(out var textBlock))
{
Console.WriteLine($" [回答]: {textBlock.Text}");
}
}
Console.WriteLine();
new {
终止原因 = thinkingMessage.StopReason,
InputToken = thinkingMessage.Usage.InputTokens,
OutputToken = thinkingMessage.Usage.OutputTokens
}.Display();
}
catch (Exception ex)
{
Console.WriteLine($"Extended Thinking 调用失败(需要支持该功能的模型和 API Key): {ex.Message[..Math.Min(100, ex.Message.Length)]}");
Console.WriteLine("Extended Thinking 需要 Claude Sonnet/Opus 中支持该功能的特定版本");
}
Console.WriteLine();
Console.WriteLine("Extended Thinking 演示完成");
Console.WriteLine("thinking 块是 Messages API 独有特性,chat completions/responses 无法原生支持!");
- 流式输出(CreateStreaming)
Messages API 推荐使用 CreateStreaming() 进行流式调用,相比阻塞式 Create() 能提供更好的用户体验,特别是在生成长文本时。
流式响应类型
| 事件类型 | 说明 |
|---|---|
| message_start | 响应开始,包含消息 ID |
| content_block_start | 新内容块开始 |
| content_block_delta | 内容块增量数据(text_delta / thinking_delta) |
| content_block_stop | 内容块结束 |
| message_delta | 消息级别更新(stop_reason, usage) |
| message_stop | 响应结束 |
MEAI 统一方式: GetStreamingResponseAsync() 自动处理底层流式格式。
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 流式输出(两种方式对比)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Console.WriteLine("方式 1:原生 Anthropic SDK 流式调用");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var streamParams = new MessageCreateParams
{
Model = "glm-4.5-air",
MaxTokens = 300,
Messages = [new MessageParam { Role = Role.User, Content = "用100字介绍.NET的跨平台能力" }]
};
var sb = new System.Text.StringBuilder();
int chunkCount = 0;
await foreach (var streamEvent in anthropicClient.Messages.CreateStreaming(streamParams))
{
// 只处理内容块增量事件
if (streamEvent.TryPickContentBlockDelta(out var deltaEvent))
{
if (deltaEvent.Delta.TryPickText(out var textDelta))
{
Console.Write(textDelta.Text);
sb.Append(textDelta.Text);
chunkCount++;
}
}
}
Console.WriteLine();
Console.WriteLine($"<br>[原生 SDK 流式完成,{chunkCount} 个文本块]");
Console.WriteLine();
Console.WriteLine("方式 2:MEAI 统一流式接口(推荐)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var sb2 = new System.Text.StringBuilder();
int chunkCount2 = 0;
await foreach (var update in claudeClient.GetStreamingResponseAsync(
"用100字介绍Claude模型的核心技术特点"))
{
if (!string.IsNullOrEmpty(update.Text))
{
Console.Write(update.Text);
sb2.Append(update.Text);
chunkCount2++;
}
}
Console.WriteLine();
new {
原生SDK流块数 = chunkCount,
MEAI流块数 = chunkCount2
}.Display();
Console.WriteLine();
Console.WriteLine("流式输出演示完成");
Console.WriteLine("推荐使用 MEAI 的 GetStreamingResponseAsync(),无需处理底层事件类型!");
