Spiga

Net+AI智能体进阶1:NET平台AI底座

2025-08-16 19:00:03

一、M.E.AI概述

1. 引言

Microsoft.Extensions.AI (MEAI) 定位于.NET 生态系统的 AI 功能基础抽象层,提供如 IChatClient 和 IEmbeddingGenerator 等核心接口,旨在统一和简化.NET 应用与各类 AI 服务的集成方式 。

Microsoft.Extensions.AI (MEAI) 是一系列旨在为.NET 开发者提供与各种人工智能服务进行集成和交互的统一方法的库 。它的核心目标是提供一组通用的抽象,从而简化.NET 应用程序中生成式 AI 组件的表示,并实现与不同 AI 服务的无缝集成和互操作性。 MEAI 的核心功能主要围绕两个关键接口展开:ChatClient 和 IEmbeddingGenerator<TInput, TEmbedding>。

2. MEAI 的核心功能和优势

  • IChatClient:用于与聊天型 AI 服务交互的客户端接口,支持多模态消息传递和流式响应。
  • IEmbeddingGenerator<TInput, TEmbedding>:用于生成向量嵌入的通用接口,支持多种输入类型。
  • 依赖注入 (DI) 和中间件支持:利用.NET 的成熟 DI 和中间件模式,简化组件集成。 这使得开发者可以轻松地将自动函数工具调用、遥测和缓存等功能集成到应用程序中。
  • 服务无关性:MEAI 的设计目标是实现与特定 AI 服务的解耦,使得开发者可以在不同的 AI 提供商之间轻松切换,而无需修改应用代码。 这不仅提高了代码的可移植性,还简化了测试和模拟过程。
  • 多模态支持:IChatClient 接口支持文本、图像和音频等多种消息类型,满足现代 AI 应用的需求。
  • 流式响应:IChatClient 支持流式响应,允许应用程序逐步处理来自 AI 服务的输出,提升用户体验。
  • 扩展性:MEAI 的设计允许开发者为不同的 AI 服务实现自定义的客户端和嵌入生成器,促进生态系统的多样化和创新。
  • 与 Semantic Kernel 的集成:MEAI 提供了与 Semantic Kernel 的无缝集成,允许开发者利用 SK 的高级功能,同时享受 MEAI 提供的统一接口和抽象。
  • 与 Microsoft Agent Framework 的集成:MEAI 还与 Microsoft Agent Framework 集成,支持构建智能代理应用程序,进一步扩展了其在 AI 生态系统中的应用范围。

3. 使用指南

如果要在 .NET 应用程序中使用 Microsoft.Extensions.AI,需要在项目中安装以下NuGet 包。然后根据使用的 AI 服务提供商,安装相应的客户端实现包。

  • 对于兼容 OpenAI API 的服务,可以使用 Microsoft.Extensions.AI.OpenAI 包中的 OpenAIClient 即可。
OpenAIClientOptions clientOptions = new OpenAIClientOptions();
clientOptions.Endpoint = new Uri(deepseekUri);

OpenAIClient aiClient = new(new ApiKeyCredential(deepseekApiKey), clientOptions);
  • 对于Ollama 服务,可使用 OllamaSharp 包中的 OllamaApiClient,它实现了 IChatClient 接口。
var aiClient = new OllamaApiClient(arguments.Uri, arguments.Model);
  • 对于 Azure OpenAI 服务,可以使用 Azure.AI.OpenAI 包中的 AzureOpenAIClient。
var aiClient = new AzureOpenAIClient( endpoint: endpoint, credential: new ApiKeyCredential(apiKey));

4. 使用IChatClient

IChatClient 主要定义了三个关键方法

  • GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken ct = default) → Task
  • GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken ct = default) → IAsyncEnumerable
  • GetService(Type serviceType, object? serviceKey = null) → object?(用于向外暴露元数据/内部服务,如 ChatClientMetadata、OpenTelemetry 组件等)

请求/响应核心对象

  • ChatMessage:一条消息,含 Role、Contents(多模态:TextContent、FunctionCallContent、Image 等),Text 为所有 TextContent 拼接。
  • ChatOptions:本次请求的行为配置,如 Temperature、TopP、TopK、StopSequences、MaxOutputTokens、ModelId、ToolMode、Tools、ResponseFormat(Text/Json/JsonSchema)、ConversationId、AllowMultipleToolCalls 等;可 Clone()。
  • ChatResponse:返回的一组消息+元信息(Text 汇总、FinishReason、Usage、ModelId、ConversationId、ContinuationToken)。
  • ChatResponseUpdate:流式增量更新,每个 update 携带 Contents(含 UsageContent)、MessageId/ResponseId 等,用于 IAsyncEnumerable
// 1:构建 AI 服务客户端
OpenAIClientOptions clientOptions = new OpenAIClientOptions();
// Keys 是配置信息,我们使用的是Qwen的qwen-plus模型
clientOptions.Endpoint = new Uri(Keys.QwenEndpoint);
// 创建OpenAI客户端
OpenAIClient aiClient = new(new ApiKeyCredential(Keys.QwenApiKey), clientOptions);

// 2:获取 IChatClient实例
var provider = aiClient.GetChatClient("qwen-max");
// 转换为标准的IChatClient
var chatClient = provider.AsIChatClient();

// 3: 使用 IChatClient 进行聊天
// 非流式响应
var response = await chatClient.GetResponseAsync("Hello, who are you?");
response.Display();

// 流式响应
var responseStreaming = chatClient.GetStreamingResponseAsync("Hello, who are you?");
await foreach (var chunk in responseStreaming)
{
    Console.Write(chunk);
    await Task.Delay(100); // 控制输出速度
}

// 4: 使用 IChatClient 获取服务元数据
var chatClientMetadata = chatClient.GetRequiredService<ChatClientMetadata>();
chatClientMetadata.Display();

5. 使用 IEmbeddingGenerator<TInput, TEmbedding>

核心概念与方法

  • 接口:IEmbeddingGenerator<TInput, TEmbedding>,常见形态是 IEmbeddingGenerator<string, Embedding>,输入字符串,输出向量嵌入。
  • 主要方法:GenerateAsync(IEnumerable values, EmbeddingGenerationOptions? options = null, CancellationToken ct = default) 返回 GeneratedEmbeddings
  • 常用扩展方法:
    • GenerateAsync(value):对单个输入生成嵌入。
    • GenerateVectorAsync(value):直接拿到向量数据 ReadOnlyMemory
    • GenerateAndZipAsync(values):把输入与对应的嵌入配对返回。
  • 选项:EmbeddingGenerationOptions,可设置 ModelId、Dimensions、User、RawRepresentationFactory 等。
  • 元数据:可通过 generator.GetRequiredService() 获取底层实现与模型信息。
// 1:获取 `IEmbeddingGenerator` 实例
OpenAIClientOptions clientOptions = new OpenAIClientOptions();
clientOptions.Endpoint = new Uri(Keys.QwenEndpoint);
OpenAIClient client = new(new ApiKeyCredential(Keys.QwenApiKey), clientOptions);
var embeddingGenerator = client.GetEmbeddingClient("text-embedding-v4").AsIEmbeddingGenerator();

// 2:准备嵌入生成上下文
var documents = new[]
{
    "Microsoft.Extensions.AI 为 .NET 提供统一 AI 抽象。",
    "向量嵌入可以用来执行语义搜索和相似度匹配。"
};

// 3:调用生成方法获取向量
// IEmbeddingGenerator 提供批量与单条生成能力。可以根据业务需要选择 GenerateAsync(返回包含元数据的聚合结果)或 GenerateVectorAsync(直接获取原始向量 ReadOnlyMemory<T>)。
GeneratedEmbeddings<Embedding<float>> generatedEmbeddings = await embeddingGenerator.GenerateAsync(documents);
var singleVector = await embeddingGenerator.GenerateVectorAsync("嵌入生成器适合语义检索");
Console.WriteLine($"向量维度: {singleVector.Length}");

// 4:读取生成器元数据
// 通过 GetRequiredService<EmbeddingGeneratorMetadata> 可以获知实际调用的模型、提供方、输入限制等信息,便于在日志、监控或诊断场景中使用。
var embeddingMetadata = embeddingGenerator.GetRequiredService<EmbeddingGeneratorMetadata>();
embeddingMetadata.Display();

二、函数调用 Function Calling

借助 Microsoft.Extensions.AI(MEAI),我们可以在 .NET 应用中为大语言模型提供自动化的函数调用能力,让模型按需触发业务逻辑或外部服务。

在智能助理或企业应用场景下,模型不仅需要回答问题,还需调用后端工具执行任务:查询库存、获取天气、提交工单等。MEAI 通过统一的工具抽象,让模型能够安全地访问这些受控能力,同时保持与服务提供商无关。

1. 核心组件

  • ToolCollection:用于注册可调用的函数(Tool),支持从静态方法、委托或 Tool 实例构建。
  • ChatOptions.ToolMode:控制模型何时可以调用工具(禁用/自动/强制),常用值为 ToolMode.Auto。
  • ToolContext / ToolArguments:当模型请求调用函数时,MEAI 会解析参数并传入执行上下文,返回值会再次注入对话。
  • FunctionCallContent / ToolChatMessage:承载模型发出的函数调用意图与工具返回的结果,MEAI 会自动串联回聊天历史。

2. 实践流程

  1. 构建模型客户端:使用 OpenAI或其他提供 IChatClient 实现的服务。
  2. 注册工具集合:将业务函数包装成工具供模型调用,可附带 JSON Schema 与描述信息。
  3. 配置 ChatOptions:开启 ToolMode.Auto、设置可复用的工具集合,还可允许多次调用。
  4. 执行业务对话:发送用户消息,MEAI 自动处理模型的函数请求,返回带有工具结果的最终回答。

3. 简单示例

  • 创建工具函数
// 获取可以使用的工具集
public IList<AITool> GetTools()
{
    var travelTools = new TravelToolset();

    // 工具集中存在多个函数时,可以给这些函数添加描述信息,并利用反射一次性生成
    IList<AITool> batchRegisteredTools =
        typeof(TravelToolset)
        .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)
        .Select(method => AIFunctionFactory.Create(
            method,
            travelTools,
            name: method.Name.ToLowerInvariant(),
            description: method.GetCustomAttribute<DescriptionAttribute>()?.Description))
        .Cast<AITool>()
        .ToList();

    foreach (var tool in batchRegisteredTools)
    {
        Console.WriteLine($"Registered tool: {tool.Name} - {tool.Description}");
    }
    return batchRegisteredTools;

    //// 可以与其它工具合并后交给 ChatOptions.Tools 使用
    //ChatOptions batchOptions = new()
    //{
    //    ToolMode = ChatToolMode.Auto,
    //    Tools = batchRegisteredTools
    //};
}

// 我们将业务逻辑封装成工具(Tool),提供名称、描述以及输入参数。
public record WeatherReport(string City, int TemperatureCelsius, bool WillRain);

public class TravelToolset
{
    [Description("查询指定城市的实时天气")]
    public WeatherReport QueryWeather(string city)
    {
        int temperature = Random.Shared.Next(-5, 36);
        bool willRain = Random.Shared.NextDouble() > 0.6;
        return new WeatherReport(city, temperature, willRain);
    }

    [Description("根据天气提供穿搭建议")]
    public string SuggestOutfit(string city)
    {
        var weather = QueryWeather(city);
        return weather switch
        {
            { WillRain: true } => $"{city} 可能会下雨,建议携带雨具并穿防水外套。",
            { TemperatureCelsius: < 5 } => $"{city} 温度 {weather.TemperatureCelsius}℃,请穿冬装并注意保暖。",
            { TemperatureCelsius: > 28 } => $"{city} 今天很热({weather.TemperatureCelsius}℃),可以选择短袖和透气面料。",
            _ => $"{city} 气温 {weather.TemperatureCelsius}℃,穿上舒适的日常装束即可。"
        };
    }
}
  • 启用函数调用:可以通过创建 ChatClientBuilder 并调用 UseFunctionInvocation 启用函数调用能力。
// 1:创建工具集
var tools = GetTools();

// 2:启用函数调用
var client = chatClient.AsBuilder()
    .UseFunctionInvocation(configure: options =>
    {
        options.AdditionalTools = tools; // 注册一些额外的工具,比如时间工具等
        options.AllowConcurrentInvocation = true; // 允许模型并发调用多个函数,默认 false
        options.IncludeDetailedErrors = true; // 包含详细错误信息,默认 false
        options.MaximumConsecutiveErrorsPerRequest = 3; // 每个请求允许的最大连续错误数,防止无限循环,默认 3次
        options.MaximumIterationsPerRequest = 5; // 每个请求允许的最大迭代次数,防止无限循环,默认 40次
        options.TerminateOnUnknownCalls = false; // 当模型调用了未知的函数时,是否终止对话
        options.FunctionInvoker = (context, cancellationToken) =>
        {
            var functionCall = context.Function;
            Console.WriteLine($"Invoking function: {functionCall.Name} with arguments: {functionCall.AdditionalProperties}");
            return context.Function.InvokeAsync(context.Arguments, cancellationToken);
        };
    })
    .Build();

// 步骤 4:配置 ChatOptions 并自动执行函数调用
var messages = new List<ChatMessage>
{
    new ChatMessage(ChatRole.System, "你是出行助手,善于调用工具给出穿搭建议。"),
    new ChatMessage(ChatRole.User, "帮我查看今天北京的天气,并告诉我需要带雨伞吗?")
};

ChatOptions options = new()
{
    ToolMode = ChatToolMode.Auto, // 自动决定是否调用工具,默认值为 Auto
    AllowMultipleToolCalls = true, // 允许模型一次调用多个工具,默认 false
    Tools = tools
};

var weatherResponse = await client.GetResponseAsync(messages, options);
weatherResponse.Display();

输出:

Registered tool: queryweather - 查询指定城市的实时天气 Registered tool: suggestoutfit - 根据天气提供穿搭建议 Invoking function: queryweather with arguments: Microsoft.Shared.Collections.EmptyReadOnlyDictionary`2[System.String,System.Object]

ChatOptions 提供了丰富的配置选项,以下是与函数调用相关的关键属性:

  • Tools:可调用的工具集合。
  • ToolMode:工具调用模式。
  • AllowMultipleToolCalls:是否允许一次调用多个工具。

ToolMode 详解:通过 ChatOptions.ToolMode 支持三种模式:

  • Auto:自动决定是否调用工具,默认值为 Auto。
  • RequireAny:至少需要调用Tools中的一个工具。
  • None:禁用工具调用。
  • RequireSpecific(string functionName):必须调用指定名称的工具。

三、配置 ChatOptions

ChatOptions 是传递给 IChatClient 的统一配置容器,允许我们在一次对话请求中同时调整生成策略、工具调用行为以及服务特性。下面按能力维度梳理核心属性:

1. 对话上下文

  • ConversationId:为请求绑定会话标识,方便在无状态客户端里实现状态恢复。
  • Instructions:附加一次性的系统提示词,用于补充场景限定或对模型的额外要求。
var contextMessages = new List<ChatMessage>
{
    new(ChatRole.System, "你是贴心的行程规划助手。"),
    new(ChatRole.User, "帮我安排一个五一北京两日游的行程计划。")
};

ChatOptions contextOptions = new()
{
    ConversationId = "planner-2024-05-01",
    Instructions = "回答请保持中文,并按时间顺序给出活动安排。"
};

var contextResponse = await chatClient.GetResponseAsync(contextMessages, contextOptions);
contextResponse.Display();

2. 生成策略

  • ModelId / ResponseFormat:覆盖默认模型或强制输出格式(纯文本、通用 JSON 或自定义 JSON Schema)。
  • Temperature、TopP、TopK:调整采样策略,控制回答的多样性与确定性。
  • MaxOutputTokens:限制生成的最大 token 数。
  • FrequencyPenalty、PresencePenalty、Seed:抑制重复、增强随机性或复现结果。
  • StopSequences:当输出命中指定序列时立即截断结果,常用于避免模型继续执行不需要的内容。
var generationMessages = new List<ChatMessage>
{
    new(ChatRole.User, "请给出三条不同风格的励志语录,以便我放在海报上。")
};

ChatOptions generationOptions = new()
{
    ModelId = "qwen-max", // 覆盖默认模型(根据可用模型调整)
    Temperature = 0.7f,
    TopP = 0.9f,
    MaxOutputTokens = 512,
    StopSequences = new[] { "[DONE]" }
};

var generationResponse = await chatClient.GetResponseAsync(generationMessages, generationOptions);
generationResponse.Display();

3. 工具调用

  • ToolMode:指定工具调用策略(禁用、自动、必须调用某个/任意工具)。
  • Tools:传入当前请求可用的工具集合(通常来自函数调用注册)。
  • AllowMultipleToolCalls:允许模型在一次响应中串联多个工具调用。

结合 UseFunctionInvocation 中间件,可以在请求级别动态控制工具列表并允许模型串联多个函数调用。

string GetCurrentWeather(string city)
{
    var temperature = Random.Shared.Next(-5, 36);
    var willRain = Random.Shared.NextDouble() > 0.6;
    return $"{city} 当前 {temperature}℃,{(willRain ? "可能下雨,请带伞" : "天气晴朗")}。";
}

AITool weatherTool = AIFunctionFactory.Create(
    (string city) => GetCurrentWeather(city),
    name: "get_current_weather",
    description: "查询指定城市的实时天气");

var toolMessages = new List<ChatMessage>
{
    new(ChatRole.System, "你是穿搭顾问,善于结合天气给出建议。"),
    new(ChatRole.User, "今天北京需要带雨伞吗?")
};

ChatOptions toolOptions = new()
{
    ToolMode = ChatToolMode.Auto,
    AllowMultipleToolCalls = true,
    Tools = new[] { weatherTool }
};

var toolEnabledClient = chatClient.AsBuilder()
    .UseFunctionInvocation()
    .Build();

var toolResponse = await toolEnabledClient.GetResponseAsync(toolMessages, toolOptions);
toolResponse.Display();

4. 后台执行与恢复

  • AllowBackgroundResponses:开启支持后台长任务或流式中断恢复(特性处于实验阶段)。
  • ContinuationToken:在后台模式下用于轮询或恢复流式响应的令牌。

5. 扩展点

  • AdditionalProperties:向底层提供者透传自定义键值对。
  • RawRepresentationFactory:当你明确底层 IChatClient 的特定实现时,可构造并返回 provider 专属的选项对象。

四、会话缓存 Caching

通过 Microsoft.Extensions.AI(MEAI)的缓存功能,我们可以有效减少对大语言模型的重复调用,降低成本并提升响应速度。

我们可以使用 CachingChatClient 和 DistributedCachingChatClient 实现智能缓存。

1. 核心组件

  • CachingChatClient:抽象基类,定义了缓存聊天客户端的核心逻辑,包括缓存键生成、读写操作和流式响应处理。
  • DistributedCachingChatClient:基于 IDistributedCache 的具体实现,支持使用 Redis、SQL Server 等分布式缓存存储。
  • CoalesceStreamingUpdates:控制是否合并流式更新,优化缓存存储效率(默认为 true)。
  • EnableCaching 方法:决定是否为特定请求启用缓存,默认排除带有 ConversationId 的请求。
  • 缓存键生成:基于消息内容、ChatOptions 和附加值,通过 JSON 序列化和哈希计算唯一标识。

2. 简单实现

  • 启用缓存中间件:通过 ChatClientBuilder 和 UseDistributedCache 扩展方法,将缓存层添加到客户端管道中。
// 1. 创建内存分布式缓存实例
var memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
IDistributedCache distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions 
{
    // 可以配置全局缓存选项
}));

Console.WriteLine("缓存存储已配置");

// 2. 启动缓存中间件
// 使用 ChatClientBuilder 构建带缓存的客户端
var cachedChatClient = new ChatClientBuilder(chatClient)
    .UseDistributedCache(distributedCache)
    .Build();

// 设置缓存行为,例如合并流式更新或添加自定义缓存键
if (cachedChatClient is DistributedCachingChatClient distributedCachingClient)
{
    distributedCachingClient.CoalesceStreamingUpdates = true; // 合并流式更新(默认 true)

    // 可选:添加额外的缓存键值来区分不同的缓存分区
    // distributedCachingClient.CacheKeyAdditionalValues = new[] { "v1", "production" };
}

Console.WriteLine("缓存客户端已构建");
cachedChatClient.Display();
  • 测试缓存效果 - 非流式响应
var testMessage = "用一句话介绍什么是 Microsoft.Extensions.AI?";
// 第一次请求 - 会调用模型
Console.WriteLine("第一次请求(调用模型)...");
var sw1 = Stopwatch.StartNew();
var response1 = await cachedChatClient.GetResponseAsync(testMessage);
sw1.Stop();

Console.WriteLine($"响应内容: {response1.Text}");
Console.WriteLine($" 耗时: {sw1.ElapsedMilliseconds}ms");
Console.WriteLine($"Token 使用: {response1.Usage?.TotalTokenCount ?? 0}\n");

// 第二次相同请求 - 应该从缓存返回
Console.WriteLine("第二次请求(从缓存返回)...");
var sw2 = Stopwatch.StartNew();
var response2 = await cachedChatClient.GetResponseAsync(testMessage);
sw2.Stop();

Console.WriteLine($"响应内容: {response2.Text}");
Console.WriteLine($" 耗时: {sw2.ElapsedMilliseconds}ms");
Console.WriteLine($"Token 使用: {response2.Usage?.TotalTokenCount ?? 0}");

Console.WriteLine($"\n缓存加速比: {(double)sw1.ElapsedMilliseconds / sw2.ElapsedMilliseconds:F2}x");
Console.WriteLine($"两次响应内容一致: {response1.Text == response2.Text}");
  • 测试缓存效果 - 流式响应:缓存同样支持流式响应模式。根据 CoalesceStreamingUpdates 配置:
    • true(默认):将流式更新合并为完整响应后缓存,后续从缓存读取时再拆分为流
    • false:保留原始流式更新序列进行缓存
var streamTestMessage = "列出 MEAI 的三个核心接口。";
// 第一次流式请求
Console.WriteLine("第一次流式请求(调用模型)...");
var sw3 = Stopwatch.StartNew();
await foreach (var update in cachedChatClient.GetStreamingResponseAsync(streamTestMessage))
{
    Console.Write(update.Text);
}
sw3.Stop();
Console.WriteLine($"\n 耗时: {sw3.ElapsedMilliseconds}ms\n");

// 第二次相同的流式请求(从缓存返回)
Console.WriteLine("第二次流式请求(从缓存返回)...");
var sw4 = Stopwatch.StartNew();
await foreach (var update in cachedChatClient.GetStreamingResponseAsync(streamTestMessage))
{
    Console.Write(update.Text);
}
sw4.Stop();
Console.WriteLine($"\n 耗时: {sw4.ElapsedMilliseconds}ms");

Console.WriteLine($"\n流式缓存加速比: {(double)sw3.ElapsedMilliseconds / sw4.ElapsedMilliseconds:F2}x");

3. 高级配置与最佳实践

  • 缓存键的生成机制:DistributedCachingChatClient 通过以下因素生成缓存键:

    • 消息内容(ChatMessage 集合)

    • 聊天选项(ChatOptions)

    • 缓存版本号

    • 自定义附加值(CacheKeyAdditionalValues)

    这些值会被序列化为 JSON 并通过哈希算法生成唯一标识。

  • 自定义缓存策略:通过继承 CachingChatClient 抽象类,可以实现自定义缓存逻辑:

public class CustomCachingChatClient : CachingChatClient
{
  protected override bool EnableCaching(IEnumerable<ChatMessage> messages, ChatOptions? options)
  {
    // 自定义缓存启用条件
    // 例如:只缓存不包含敏感关键词的请求
    var messageText = string.Join(" ", messages.Select(m => m.Text));
    return !messageText.Contains("机密") && base.EnableCaching(messages, options);
  }
  // 实现抽象方法...
}
  • 使用 Redis 分布式缓存
using Microsoft.Extensions.Caching.StackExchangeRedis;

var redisCache = new RedisCache(Options.Create(new RedisCacheOptions
{
    Configuration = "localhost:6379",
    InstanceName = "MEAICache:"
}));

var cachedClient = chatClient
    .AsBuilder()
    .UseDistributedCache(redisCache)
    .Build();
  • 缓存键分区管理:通过 CacheKeyAdditionalValues 可以为不同场景创建独立的缓存分区:
var productionClient = chatClient
    .AsBuilder()
    .UseDistributedCache(distributedCache, configure: c =>
    {
        c.CacheKeyAdditionalValues = new[] { "prod", "v2", "zh-CN" };
    })
    .Build();

这在以下场景特别有用:多语言支持(为不同语言创建独立缓存)、版本管理(新版本不会命中旧版本缓存)、环境隔离(开发、测试、生产环境独立缓存)

  • 何时不应使用缓存:默认情况下,以下场景会自动禁用缓存:

    • 设置了 ConversationId:表示存在会话状态,响应可能不同

    • 包含敏感数据:应避免缓存包含个人信息的请求

    • 实时性要求高:如股票报价、实时新闻等

    • 随机性响应:需要每次生成不同结果的场景

    可以通过重写 EnableCaching 方法自定义这些规则。

4. 注意事项与限制

  • 重要提示:DistributedCachingChatClient 使用 JSON 序列化存储缓存数据,存在以下限制:

    • ChatMessage.RawRepresentation 不会被序列化

    • AdditionalProperties 中的 object 值会反序列化为 JsonElement

    • 自定义类型可能无法完整往返

    在应用依赖这些属性,请谨慎使用缓存或实现自定义序列化逻辑。

  • 缓存版本管理:当缓存序列化格式发生破坏性变更时,MEAI 会更新内部的缓存版本号(当前为 v2),自动使旧缓存失效。

五、上下文窗口压缩 Chat Reducer

在多轮对话场景中,我们面临以下挑战:

  • 上下文窗口限制:大多数 LLM 都有上下文长度限制(如 GPT-4 的 8K/32K tokens),超出限制会导致请求失败。
  • 成本控制:输入 token 越多,API 调用成本越高,尤其在高频对话场景下。
  • 性能优化:过长的上下文会显著增加模型推理时间,影响用户体验。
  • 信息冗余:并非所有历史消息都对当前对话有价值,早期的闲聊内容可能不再相关。

Chat Reducer 通过智能压缩策略,在保持对话质量的前提下,有效解决上述问题。

1. Chat Reducer 核心概念

IChatReducer 接口:IChatReducer 是 MEAI 中定义的对话压缩抽象接口

public interface IChatReducer
{
    Task<IEnumerable<ChatMessage>> ReduceAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken);
}

该接口接收一组 ChatMessage,返回压缩后的消息列表。MEAI 提供了两种开箱即用的实现:

MessageCountingChatReducer(消息计数压缩器):通过限制消息数量来控制对话长度,保留最新的 N 条非系统消息。

  • 核心特性:

    • 始终保留第一条系统消息(如存在)

    • 保留最近的 N 条非系统消息(用户、助手消息)

    • 自动排除包含函数调用/结果的消息

    • 适用于固定窗口大小的场景

  • 适用场景:

    • 客服对话(只关注最近几轮交互)

    • 快速问答系统

    • 需要严格控制 token 预算的场景

SummarizingChatReducer(智能摘要压缩器):利用 AI 自动生成摘要,将历史对话压缩成简洁的上下文描述。

  • 核心特性:

    • 当消息数量超过阈值时,自动调用 AI 生成摘要

    • 摘要内容存储在消息的 AdditionalProperties 中

    • 支持渐进式摘要(新摘要会包含旧摘要内容)

    • 保留最近的消息 + 历史摘要,确保上下文连贯

  • 适用场景:

    • 长时间咨询会话(如医疗诊断、法律咨询)

    • 需要保留完整上下文语义的多轮对话

    • 复杂任务协作场景

2. 使用 MessageCountingChatReducer

场景说明:客服机器人只需保留最近 3 轮对话,超出的历史消息自动丢弃。

压缩规则说明:

  • 保留 1 条系统消息(第一条 System 消息)
  • 保留 3 条最新的非系统消息(最后 3 轮对话的 User 和 Assistant 消息)
  • 丢弃早期的对话记录(前 3 轮的 6 条消息被移除)
// 1. 创建 MessageCountingChatReducer
#pragma warning disable MEAI001
// 创建计数器压缩器,保留最多 3 条非系统消息
var countingReducer = new MessageCountingChatReducer(targetCount: 3);
Console.WriteLine("MessageCountingChatReducer 已创建(保留 3 条消息)");

// 2. 集成 Reducer 到 Chat Client
var reducingClient = chatClient.AsBuilder()
    .UseChatReducer(reducer: countingReducer)
    .Build();
Console.WriteLine("带 Reducer 的 Chat Client 已构建");

// 3. 模拟多轮对话并观察压缩效果
var messages = new List<ChatMessage>
{
    new ChatMessage(ChatRole.System, "你是一个专业的客服助手。")
};
// 模拟 6 轮对话
var questions = new[]
{
    "我的订单号是多少?",
    "订单什么时候发货?",
    "可以修改收货地址吗?",
    "运费是多少?",
    "支持货到付款吗?",
    "如何申请退款?"
};
foreach (var question in questions)
{
    messages.Add(new ChatMessage(ChatRole.User, question));

    // 调用带 Reducer 的客户端
    var response = await reducingClient.GetResponseAsync(messages);
    messages.Add(new ChatMessage(ChatRole.Assistant, response.Text));

    Console.WriteLine($"用户: {question}");
    Console.WriteLine($"助手: {response.Text}");
    Console.WriteLine($"当前消息总数: {messages.Count} 条\n");
}
Console.WriteLine("════════════════════════════════════════");
Console.WriteLine("注意:虽然本地 messages 列表包含所有历史消息,");
Console.WriteLine("但 Reducer 会在每次调用 API 前自动压缩,");
Console.WriteLine("实际发送给模型的只有最近 3 条 + 系统消息。");
Console.WriteLine("════════════════════════════════════════");

// 4. 验证压缩效果
// 手动调用 Reducer 查看压缩后的结果
var reducedMessages = await countingReducer.ReduceAsync(messages, CancellationToken.None);
Console.WriteLine($"压缩前: {messages.Count} 条消息");
Console.WriteLine($"压缩后: {reducedMessages.Count()} 条消息\n");
Console.WriteLine("压缩后的消息列表:");
reducedMessages.Display();	

3. 使用:SummarizingChatReducer

场景说明:医疗咨询场景需要保留完整的上下文语义,使用 AI 自动生成摘要,确保历史信息不会丢失。

摘要压缩工作原理:

  • 触发条件:当消息数超过 targetCount + threshold 时启动摘要
  • 摘要生成:AI 将前 N 条消息(当前消息数 - targetCount)生成为简洁摘要
  • 存储机制:摘要存储在消息的 AdditionalProperties["_summary_"] 中
  • 渐进式压缩:新摘要会包含旧摘要内容,确保上下文完整
  • 保留最新:始终保留最近的 targetCount 条原始消息

压缩效果对比:

  • 原始消息:7 条(System + 3 轮对话)
  • 压缩后:3-4 条(System + 摘要标记 + 最近 2 条消息)
  • 语义完整性:保留(通过摘要)
  • Token 节省:显著降低
// 1. 创建 SummarizingChatReducer
#pragma warning disable MEAI001
// 创建摘要压缩器
// targetCount: 保留最近 2 条消息
// threshold: 超过 targetCount + threshold 时触发摘要(即 2 + 1 = 3 条时触发)
var summarizingReducer = new SummarizingChatReducer(
    chatClient: chatClient,
    targetCount: 2,
    threshold: 1
);
Console.WriteLine("SummarizingChatReducer 已创建");
Console.WriteLine(" - 目标消息数: 2 条");
Console.WriteLine(" - 触发阈值: 超过 3 条时生成摘要");

// 2.集成到 Chat Client 并测试
var summarizingClient = chatClient.AsBuilder()
    .UseChatReducer(reducer: summarizingReducer)
    .Build();

// 准备测试消息
var medicalMessages = new List<ChatMessage>
{
    new ChatMessage(ChatRole.System, "你是一位专业的医疗咨询助手。"),
    new ChatMessage(ChatRole.User, "我最近经常头痛,是什么原因?"),
    new ChatMessage(ChatRole.Assistant, "头痛可能由多种因素引起,包括压力、睡眠不足、脱水、眼疲劳等。建议您注意休息,保持规律作息。"),
    new ChatMessage(ChatRole.User, "我每天睡眠时间大概 5 小时,工作压力比较大。"),
    new ChatMessage(ChatRole.Assistant, "睡眠不足和工作压力确实是导致头痛的常见原因。建议您尽量保证 7-8 小时睡眠,适当放松。"),
    new ChatMessage(ChatRole.User, "除了头痛,我还感觉眼睛很干涩。"),
};
Console.WriteLine($"初始消息数: {medicalMessages.Count} 条(未触发摘要)");
// 添加第 7 条消息,触发摘要生成(2 + 1 = 3 条阈值)
medicalMessages.Add(new ChatMessage(ChatRole.Assistant, "眼睛干涩可能与长时间使用电子设备、空调环境等有关。建议使用人工泪液,并定时休息眼睛。"));
Console.WriteLine($"当前消息数: {medicalMessages.Count} 条(已超过阈值 3 条)\n");

// 调用 Reducer 触发摘要生成
var summarizedMessages = await summarizingReducer.ReduceAsync(medicalMessages, CancellationToken.None);
Console.WriteLine($"压缩后消息数: {summarizedMessages.Count()} 条\n");
Console.WriteLine("压缩后的消息结构:");
summarizedMessages.Display();

4. 自定义摘要提示词

SummarizingChatReducer 允许自定义摘要生成的提示词,以适应不同领域的需求。

#pragma warning disable MEAI001
// 创建自定义摘要提示词的 Reducer
var customReducer = new SummarizingChatReducer(
    chatClient: chatClient,
    targetCount: 2,
    threshold: 1
);

// 设置医疗领域专用的摘要提示词
customReducer.SummarizationPrompt = """
请为以下医疗咨询对话生成简洁的临床摘要(不超过 3 句话):

要求:
- 提取患者主诉症状和时长
- 记录已提供的初步建议
- 保留关键医学信息(用药史、过敏史等,如有)
- 使用专业医学术语
- 不要添加推测性诊断或建议

格式:【患者主诉】症状描述 | 【已知信息】关键背景 | 【初步建议】已给出的建议
""";
Console.WriteLine("已设置自定义医疗摘要提示词");

// 测试自定义提示词效果
var testMessages = new List<ChatMessage>
{
    new ChatMessage(ChatRole.System, "你是医疗助手。"),
    new ChatMessage(ChatRole.User, "我持续低烧 3 天,体温 37.8℃。"),
    new ChatMessage(ChatRole.Assistant, "建议多休息、多喝水,监测体温变化。如超过 38.5℃ 或持续不退,请就医。"),
    new ChatMessage(ChatRole.User, "我有青霉素过敏史。"),
    new ChatMessage(ChatRole.Assistant, "感谢告知过敏史,这在就医时非常重要。请向医生明确说明青霉素过敏。"),
};
var customSummary = await customReducer.ReduceAsync(testMessages, CancellationToken.None);
Console.WriteLine("压缩后的消息结构:");
customSummary.Display();		

5. 实践建议和最佳实践

  • 如何选择 Reducer 类型
场景 推荐 Reducer 原因
客服机器人 MessageCountingChatReducer 只需关注最近几轮对话,历史信息价值低
技术支持 MessageCountingChatReducer 问题通常独立,不需要长期上下文
医疗咨询 SummarizingChatReducer 需要完整病史信息,摘要确保语义连续性
法律咨询 SummarizingChatReducer 案情细节重要,不能丢失关键信息
教育辅导 SummarizingChatReducer 学习进度需要长期追踪
快速问答 MessageCountingChatReducer 对话简短,不需要复杂摘要
  • 参数调优建议
// 保守策略:保留较多消息,适合上下文敏感场景
new MessageCountingChatReducer(targetCount: 10);
// 激进策略:仅保留最近对话,适合成本优先场景
new MessageCountingChatReducer(targetCount: 2);

// 频繁摘要:threshold = 0,每次超过 targetCount 立即摘要
new SummarizingChatReducer(chatClient, targetCount: 3, threshold: 0);
// 延迟摘要:threshold 较大,减少 API 调用次数
new SummarizingChatReducer(chatClient, targetCount: 5, threshold: 3);
  • 与其他中间件组合使用

    Chat Reducer 可以与其他 MEAI 中间件(如 UseFunctionInvocation、日志、缓存等)无缝组合:

var client = chatClient.AsBuilder()
    .UseChatReducer(reducer: summarizingReducer)  // 先压缩消息
    .UseFunctionInvocation()                      // 再处理函数调用
    .Build();
// 注意顺序:Reducer 应放在管道前端,确保在调用 API 前完成压缩。
  • 处理函数调用消息

    两种 Reducer 都会自动排除包含 FunctionCallContent 或 FunctionResultContent 的消息,避免破坏函数调用上下文。这意味着:

    • 系统消息:保留

    • 普通用户/助手消息:纳入压缩范围

    • 函数调用消息:自动跳过(不计入 targetCount)

  • 性能与成本考量

    • MessageCountingChatReducer:

      • 无额外 API 调用

      • 零延迟

      • 可能丢失重要上下文

    • SummarizingChatReducer:

      • 每次触发摘要需调用一次 LLM(额外成本)

      • 增加约 1-3 秒延迟

      • 保留完整语义信息

  • 优化技巧:使用较小的模型(如 GPT-3.5)专门用于摘要生成,降低成本。

六、工具削减 Tool Reduction

Tool Reduction(工具削减) 是 Microsoft.Extensions.AI 提供的一种智能优化策略,它可以根据用户输入和对话上下文,自动筛选和削减无关工具,只保留与当前请求相关的工具集合,从而提升模型性能和降低成本。

Tool Reduction 是一种中间件机制,在将请求发送给 AI 模型之前,根据配置的策略自动筛选和削减工具列表。

1. Tool Reduction 的工作原理

  • 工具注册:开发者注册所有可用的工具(如 100 个工具)

  • 用户请求:用户发起对话,提出具体问题

  • 智能筛选:Tool Reduction 中间件根据策略(如基于 Embedding 相似度)选出相关工具

  • 精简发送:只将筛选后的工具子集(如 5 个工具)发送给模型

  • 模型调用:模型从精简后的工具中选择并调用

3. 应用场景

场景 说明 示例
企业知识库 数十个专业领域的查询工具 财务查询、人事查询、IT 支持等
智能客服 大量不同类型的服务工具 订单查询、退款处理、物流跟踪等
开发助手 多种编程语言和框架的工具 Python 工具、JavaScript 工具、数据库工具等
多模态应用 图像、文本、音频处理工具 根据任务类型选择对应模态工具

4. 基础实践

  • 注册多个工具:为了演示 Tool Reduction 的效果,我们创建一组不同领域的工具函数。
// 天气工具
[Description("获取指定城市的当前天气信息")]
string GetWeather([Description("城市名称")] string city) => $"{city}的天气:晴朗,温度 25°C";

// 新闻工具
[Description("获取最新的科技新闻")]
string GetTechNews() => "最新科技新闻:AI 技术突破...";

// 股票工具
[Description("查询股票价格")]
string GetStockPrice([Description("股票代码")] string symbol) => $"{symbol} 当前价格:$150.00";

// 翻译工具
[Description("将文本翻译成英文")]
string TranslateToEnglish([Description("待翻译的文本")] string text) => $"Translation: {text}";

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

// 计算器工具
[Description("执行数学计算")]
double Calculate([Description("第一个数字")] double a, [Description("第二个数字")] double b, [Description("运算符 (+, -, *, /)")] string operation)
{
    return operation switch
    {
        "+" => a + b,
        "-" => a - b,
        "*" => a * b,
        "/" => a / b,
        _ => 0
    };
}

// 邮件工具
[Description("发送电子邮件")]
string SendEmail([Description("收件人")] string to, [Description("邮件主题")] string subject) => $"邮件已发送到 {to},主题:{subject}";

// 日历工具
[Description("添加日历事件")]
string AddCalendarEvent([Description("事件标题")] string title, [Description("事件日期")] string date) => $"事件已添加:{title} on {date}";

// 数据库工具
[Description("查询数据库")]
string QueryDatabase([Description("SQL 查询语句")] string query) => $"查询结果:[模拟数据]";

// 文件工具
[Description("读取文件内容")]
string ReadFile([Description("文件路径")] string path) => $"文件内容:[{path}]";
  • 将工具注册到 AIFunctionFactory
Console.WriteLine("10 个工具函数已定义");

// 将工具注册到 AIFunctionFactory
var allTools = new List<AIFunction>
{
    AIFunctionFactory.Create(GetWeather),
    AIFunctionFactory.Create(GetTechNews),
    AIFunctionFactory.Create(GetStockPrice),
    AIFunctionFactory.Create(TranslateToEnglish),
    AIFunctionFactory.Create(GetCurrentTime),
    AIFunctionFactory.Create(Calculate),
    AIFunctionFactory.Create(SendEmail),
    AIFunctionFactory.Create(AddCalendarEvent),
    AIFunctionFactory.Create(QueryDatabase),
    AIFunctionFactory.Create(ReadFile)
};

Console.WriteLine($"已注册 {allTools.Count} 个工具");
allTools.Select(t => new { 工具名称 = t.Name, 描述 = t.Description }).Display();
  • 使用 UseToolReduction() 中间件启用工具削减功能。
// UseToolReduction() 需要传入一个实现了 IToolReductionStrategy 接口的策略对象
public interface IToolReductionStrategy
{
    Task<IEnumerable<AITool>> SelectToolsForRequestAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options,
        CancellationToken cancellationToken = default);
}

public class EmbeddingToolReductionStrategy : IToolReductionStrategy
{
    /// <summary>
    /// 基于 Embedding 相似度的内置策略
    /// 工作原理:
    /// 1. 为每个工具的名称和描述生成 Embedding(缓存)
    /// 2. 为用户的对话消息生成 Embedding
    /// 3. 计算余弦相似度,选出相似度最高的前 N 个工具
    /// 4. 标记为的工具始终保留,不计入 toolLimit
    /// </summary>
    /// <param name="embeddingGenerator">Embedding 生成器,用于计算工具和查询的语义相似度</param>
    /// <param name="toolLimit">最多保留多少个工具(不包括必需工具)</param>
    public EmbeddingToolReductionStrategy(IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator, int toolLimit)
}

创建一个中间件来观察每次调用时发送的工具数量。

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

                return response;
            },
            getStreamingResponseFunc: null);
    }
}

使用 EmbeddingToolReductionStrategy

#pragma warning disable MEAI001
// 创建 Embedding Tool Reduction 策略
// 参数:embeddingGenerator(Embedding生成器), toolLimit(最多保留5个工具)
var strategy = new EmbeddingToolReductionStrategy(embeddingGenerator, toolLimit: 5);

// 构建启用 Tool Reduction 的客户端
var reducingClient = baseChatClient.AsBuilder()
    .UseToolReduction(strategy)  // 传入策略实例
    .UseFunctionInvocation()
    .UseToolListLogging() // 添加监控中间件
    .Build();

Console.WriteLine("已创建启用 Tool Reduction 的 ChatClient");

/* 核心要点:
- UseToolReduction() 必须传入一个 IToolReductionStrategy 策略实例
- EmbeddingToolReductionStrategy 基于语义相似度自动筛选工具
- toolLimit 参数控制最多保留多少个工具(不包括必需工具)
- Tool Reduction 应该在 UseFunctionInvocation() 之前调用
*/
  • 对比测试

测试 1:不使用 Tool Reduction

// 创建不使用 Tool Reduction 的客户端
var normalClient = baseChatClient.AsBuilder()
    .UseFunctionInvocation()
    .UseToolListLogging()
    .Build();

// 配置 ChatOptions,提供所有工具
var chatOptions = new ChatOptions
{
    Tools = [..allTools],
    ToolMode = ChatToolMode.Auto
};

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("     测试场景:查询北京天气(无 Reduction)    ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

var response = await normalClient.GetResponseAsync(
    "北京今天天气怎么样?",
    chatOptions
);
new
{
    回答 = response.Text,
    发送的工具数量 = chatOptions.Tools?.Count ?? 0,
    调用的工具 = response.Messages
        .OfType<FunctionCallContent>()
        .Select(f => f.Name)
        .ToList()
}.Display();

观察结果:

  • 模型收到了所有 10 个工具的描述

  • 模型正确选择了 GetWeather 工具

  • 但 9 个无关工具的描述浪费了上下文窗口

测试 2:现在使用启用了 Tool Reduction 的客户端进行相同的测试。

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   测试场景:查询北京天气(启用 Reduction)    ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

var response2 = await reducingClient.GetResponseAsync(
    "北京今天天气怎么样?",
    new ChatOptions
    {
        Tools = [..allTools],
        ToolMode = ChatToolMode.Auto
    }
);
new
{
    回答 = response2.Text,
    原始工具数量 = allTools.Count,
    调用的工具 = response2.Messages
        .OfType<FunctionCallContent>()
        .Select(f => f.Name)
        .ToList()
}.Display();

关键改进:

  • Tool Reduction 自动分析用户请求
  • 只保留与天气相关的工具(如 GetWeather)
  • 无关工具(股票、邮件、数据库等)被自动过滤
  • 模型收到的工具数量显著减少,提升准确率
  • 手动验证筛选结果:IToolReductionStrategy 的 SelectToolsForRequestAsync 方法允许我们预先查看筛选结果。
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("         手动调用 Strategy 查看筛选结果        ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 准备测试消息
var testMessages = new[]
{
    new ChatMessage(ChatRole.User, "上海的天气如何?"),
    new ChatMessage(ChatRole.User, "帮我写一封邮件给张三,告诉他明天的会议时间是上午10点。"),
};

// 准备 ChatOptions
var testOptions = new ChatOptions
{
    Tools = [..allTools]
};

// 调用策略的 SelectToolsForRequestAsync 方法
var selectedTools = await strategy.SelectToolsForRequestAsync(
    testMessages,
    testOptions,
    CancellationToken.None
);
Console.WriteLine($"原始工具数量: {allTools.Count}");
Console.WriteLine($"筛选后工具数量: {selectedTools.Count()}");
Console.WriteLine($"\n筛选后的工具列表:");
selectedTools.Select(t => new { 工具名称 = t.Name, 描述 = t.Description }).Display();

七、结构化输出

在实际的 AI 应用开发中,我们经常需要将 AI 模型的输出解析为程序可以直接使用的结构化数据。传统方式下,AI 返回的是自由文本,我们需要手动解析和提取关键信息,这个过程容易出错且维护成本高。

Microsoft.Extensions.AI 提供了强大的结构化输出(Structured Output)能力,允许我们预先定义输出格式(JSON Schema),让 AI 模型严格按照指定的结构返回数据。

1. 核心概念

  • 响应格式(ChatResponseFormat):ChatResponseFormat 是 Microsoft.Extensions.AI 中定义响应格式的类,支持以下几种模式:

    • ChatResponseFormat.Text:纯文本格式(默认)

    • ChatResponseFormat.Json:自由格式的 JSON 对象(无预定义模式)

    • ChatResponseFormatJson.ForJsonSchema:符合预设 JSON Schema 的结构化输出

  • JSON Schema:JSON Schema 是一种描述 JSON 数据结构的标准,定义了数据的类型、字段、约束等。Microsoft.Extensions.AI 使用 JSON Schema 来约束 AI 的输出格式。

  • AIJsonUtilities:AIJsonUtilities.CreateJsonSchema() 是一个便捷方法,可以从 C# 类型自动生成 JSON Schema,无需手动编写 Schema 定义。

  • ChatOptions:通过 ChatOptions.ResponseFormat 属性指定期望的响应格式,将 JSON Schema 传递给 AI 模型。

重要提示:千问(Qwen)和 DeepSeek 等国内模型不完全支持 ChatResponseFormatJson.ForJsonSchema,推荐做法:

  • 使用 ChatResponseFormat.Json(无 Schema 约束)
  • 在 System Message 中详细描述期望的 JSON 格式
  • 增强反序列化的容错处理

2. 单一对象的结构化输出

  • 定义数据模型
/// <summary>
/// 个人信息数据模型
/// </summary>
public class PersonInfo
{
    [JsonPropertyName("name")]
    public string? Name { get; set; }
    
    [JsonPropertyName("age")]
    public int? Age { get; set; }
    
    [JsonPropertyName("occupation")]
    public string? Occupation { get; set; }
    
    [JsonPropertyName("location")]
    public string? Location { get; set; }
}
  • 配置 JSON 响应格式(无 Schema): 使用 ChatResponseFormat.Json 而非 ForJsonSchema:
// 使用 ChatResponseFormat.Json(国内模型推荐方式)
ChatOptions chatOptions = new()
{
    ResponseFormat = ChatResponseFormat.Json  // 注意:不是 ForJsonSchema
};
  • 通过提示词指定 JSON 结构:由于没有 Schema 约束,我们需要在 System Message 中详细描述期望的 JSON 格式:
var systemMessage = """
你是一个信息提取助手。请严格按照以下 JSON 格式返回结果,不要添加任何其他文本:

{
    "name": "姓名(字符串)",
    "age": "年龄(整数)",
    "occupation": "职业(字符串)",
    "location": "工作地点(字符串)"
}

示例输出:
{
    "name": "张三",
    "age": 30,
    "occupation": "工程师",
    "location": "北京"
}
""";
var userMessage = "请提取:刘洋是一名42岁的数据科学家,目前在深圳工作。";
var messages = new[]
{
    new ChatMessage(ChatRole.System, systemMessage),
    new ChatMessage(ChatRole.User, userMessage)
};
  • 发送请求并解析结果
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("           结构化输出测试           ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

try
{
    var response = await chatClient.GetResponseAsync(messages, chatOptions);

    Console.WriteLine("DeepSeek 响应完成\n");
    Console.WriteLine("响应详情:");
    new
    {
        模型 = response.ModelId,
        Token使用 = response.Usage?.TotalTokenCount ?? 0,
        完成原因 = response.FinishReason
    }.Display();

    Console.WriteLine("\n原始 JSON 响应:");
    Console.WriteLine(response.Text);

    // 反序列化为强类型对象
    var personInfo = JsonSerializer.Deserialize<PersonInfo>(
        response.Text ?? "{}",
        JsonSerializerOptions.Web);

    Console.WriteLine("\n反序列化成功\n");
    Console.WriteLine("提取的个人信息:");
    personInfo.Display();
}
catch (Exception ex)
{
    Console.WriteLine($"错误:{ex.Message}");
}

3:复杂对象的结构化输出

  • 定义产品评论分析的数据结构(包含嵌套对象):
/// <summary>
/// 情感分析结果
/// </summary>
public class SentimentAnalysis
{
    [JsonPropertyName("sentiment")]
    public string? Sentiment { get; set; } // Positive, Neutral, Negative
    
    [JsonPropertyName("confidence")]
    public double Confidence { get; set; } // 0.0 - 1.0
}

/// <summary>
/// 产品评论分析结果
/// </summary>
public class ProductReviewAnalysis
{
    [JsonPropertyName("product_name")]
    public string? ProductName { get; set; }
    
    [JsonPropertyName("rating")]
    public int Rating { get; set; } // 1-5
    
    [JsonPropertyName("sentiment")]
    public SentimentAnalysis? Sentiment { get; set; }
    
    [JsonPropertyName("key_points")]
    public List<string>? KeyPoints { get; set; }
    
    [JsonPropertyName("recommendation")]
    public bool Recommendation { get; set; }
}
  • 实现嵌套对象的结构化输出
ChatOptions chatOptions = new()
{
    ResponseFormat = ChatResponseFormat.Json  // 注意:不是 ForJsonSchema
};

var systemMessage = """
你是一个产品评论分析专家。请严格按照以下 JSON 格式返回分析结果:

{
"product_name": "产品名称(字符串)",
"rating": "评分(1-5的整数)",
"sentiment": {
"sentiment": "情感(Positive/Neutral/Negative)",
"confidence": "置信度(0.0-1.0的小数)"
},
"key_points": ["关键点1", "关键点2", "关键点3"],
"recommendation": "是否推荐(true/false)"
}

重要说明:
1. rating 必须是 1-5 之间的整数
2. sentiment 只能是 Positive、Neutral 或 Negative
3. confidence 是 0 到 1 之间的小数
4. key_points 是字符串数组,提取 3-5 个关键要点
5. recommendation 是布尔值
""";

var userMessage = @"
小米14真是太赞了!骁龙8Gen3性能强劲,运行大型游戏毫无压力。
徕卡镜头的拍照效果惊艳,特别是人像模式。续航也很给力,重度使用一天没问题。
价格很实惠,性价比超高!强烈推荐给预算有限但追求性能的朋友。
";

var messages = new[]
{
new ChatMessage(ChatRole.System, systemMessage),
new ChatMessage(ChatRole.User, $"请分析以下产品评论:\n\n{userMessage}")
};

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("       复杂对象结构化输出测试       ");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

try
{
    var reviewResponse = await chatClient.GetResponseAsync(messages, chatOptions);

    Console.WriteLine("分析完成\n");
    Console.WriteLine("原始 JSON 响应:");
    Console.WriteLine(reviewResponse.Text);

    // 反序列化为复杂对象
    var reviewAnalysis = JsonSerializer.Deserialize<ProductReviewAnalysis>(
        reviewResponse.Text ?? "{}",
        JsonSerializerOptions.Web);

    Console.WriteLine("\n反序列化成功\n");
    Console.WriteLine("分析结果:");
    reviewAnalysis.Display();
}
catch (Exception ex)
{
    Console.WriteLine($"错误:{ex.Message}");
    Console.WriteLine($"详细信息:{ex}");
}

4. 结构化输出的最佳实践

  • 与 OpenAI ForJsonSchema 的对比:
特性 OpenAI (ForJsonSchema) 国内模型 Qwen|DeepSeek (Json)
Schema 定义 自动生成 JSON Schema 手动描述格式
格式保证 严格遵循 Schema 依赖提示词质量
配置方式 ChatResponseFormatJson.ForJsonSchema ChatResponseFormat.Json
容错性 高(模型强制遵循) 中(需要详细指示)
适用场景 严格的数据约束 灵活的数据提取
  • 国内模型使用技巧:

    • 详细的格式说明
    • 在 System Message 中提供完整的 JSON 模板

    • 说明每个字段的类型和约束

    • 提供输出示例

  • 强调返回格式

    • 明确要求"严格按照 JSON 格式返回
    • 提示"不要添加任何其他文本"
    • 使用"重要说明"强调约束
  • 增强容错处理

try
{
    var result = JsonSerializer.Deserialize<T>(response, options);
}
catch (JsonException ex)
{
    // 降级处理:使用正则表达式提取 JSON 部分
    // 或提供默认值
}
  • 使用 JsonSerializerOptions.Web
// 使用 Web 选项,支持驼峰命名和宽松解析
var result = JsonSerializer.Deserialize<PersonInfo>(
    json, 
    JsonSerializerOptions.Web
);
  • 常见问题解决:
问题 原因 解决方案
返回带有额外文本 模型添加了说明 在提示中强调"只返回 JSON"
字段缺失 格式说明不够清晰 提供完整的 JSON 模板示例
类型不匹配 模型理解有误 明确说明字段类型(如"整数"、"布尔值")
包含注释 模型添加了 JSON 注释 提示"不要添加注释",或预处理去除注释

八、使用依赖注入

1. 核心组件

  • AddChatClient:将 IChatClient 注册到 DI 容器的扩展方法,支持配置中间件管道。
  • AddEmbeddingGenerator:将 IEmbeddingGenerator 注册到 DI 容器的扩展方法。
  • IServiceCollection:.NET 依赖注入容器的服务集合接口,用于注册服务。
  • ServiceProvider:服务提供者,负责解析和提供已注册的服务实例。
  • IChatClientBuilder:聊天客户端构建器,通过链式调用配置中间件管道。
  • 服务生命周期:支持 Singleton、Scoped、Transient 三种生命周期模式。

3. 基础依赖注入

  • 配置中间件管道:AddChatClient 返回一个 IChatClientBuilder,可以通过链式调用添加各种中间件。
// 创建新的服务集合
var services = new ServiceCollection();

// 配置缓存服务
services.AddSingleton<IDistributedCache>(sp => 
    new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())));

// 注册 ChatClient 并配置中间件管道
services.AddChatClient(chatClient)
    .UseDistributedCache() // 添加缓存中间件
    .UseFunctionInvocation(); // 添加函数调用中间件

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

Console.WriteLine("带中间件管道的 ChatClient 已配置");
enhancedClient.Display();

// 测试缓存效果
Console.WriteLine("\n第一次请求(调用模型)...");
var sw1 = Stopwatch.StartNew();
var response1 = await enhancedClient.GetResponseAsync("什么是 MEAI?");
sw1.Stop();
Console.WriteLine($" 耗时: {sw1.ElapsedMilliseconds}ms");

Console.WriteLine("\n第二次请求(从缓存返回)...");
var sw2 = Stopwatch.StartNew();
var response2 = await enhancedClient.GetResponseAsync("什么是 MEAI?");
sw2.Stop();
Console.WriteLine($" 耗时: {sw2.ElapsedMilliseconds}ms");
Console.WriteLine($"缓存加速比: {(double)sw1.ElapsedMilliseconds / sw2.ElapsedMilliseconds:F2}x");
  • 使用工厂方法动态创建客户端
var services = new ServiceCollection();

// 注册配置服务(模拟从配置文件读取)
services.AddSingleton(new AIConfiguration
{
    Provider = "Qwen",
    ModelName = "Qwen-max",
    EnableCaching = true,
    EnableFunctionCalling = true
});

// 使用工厂方法注册 ChatClient
services.AddChatClient(serviceProvider =>
{
    var config = serviceProvider.GetRequiredService<AIConfiguration>();
    Console.WriteLine($"从配置创建 ChatClient: Provider={config.Provider}, Model={config.ModelName}");

    // 根据配置创建底层客户端
    return chatClient;
})
.Use(
    getResponseFunc: (messages, options, innerClient, cancellationToken) =>
    {
        // 可以从 IServiceProvider 中获取其他服务
        Console.WriteLine($"[中间件] 处理请求,消息数: {messages.Count()}");
        return innerClient.GetResponseAsync(messages, options, cancellationToken);
    },
    getStreamingResponseFunc: null); // 简化示例,未实现流式响应;

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

var testResponse = await configuredClient.GetResponseAsync("测试配置化客户端");
Console.WriteLine($"\n响应: {testResponse.Text?[..Math.Min(50, testResponse.Text.Length)]}...");

// 配置类定义
public class AIConfiguration
{
    public string Provider { get; set; }
    public string ModelName { get; set; }
    public bool EnableCaching { get; set; }
    public bool EnableFunctionCalling { get; set; }
}
  • 注册多个命名客户端
var services = new ServiceCollection();

// 注册默认客户端(快速响应,无缓存)
services.AddKeyedChatClient("fast", chatClient)
    .Use(getResponseFunc: (messages, options, innerClient, cancellationToken) =>
    {
        Console.WriteLine("[Fast Client] 快速处理请求");
        return innerClient.GetResponseAsync(messages, options, cancellationToken);
    }, getStreamingResponseFunc: null);

// 注册缓存客户端(带缓存和函数调用)
services.AddSingleton<IDistributedCache>(sp =>
    new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())));

services.AddKeyedChatClient("cached", chatClient)
    .UseDistributedCache()
    .UseFunctionInvocation()
    .Use(
        getResponseFunc: (messages, options, innerClient, cancellationToken) =>
        {
            Console.WriteLine("[Cached Client] 带缓存处理请求");
            return innerClient.GetResponseAsync(messages, options, cancellationToken);
        }
    , getStreamingResponseFunc: null);

var serviceProvider4 = services.BuildServiceProvider();

// 解析不同的客户端
var fastClient = serviceProvider4.GetRequiredKeyedService<IChatClient>("fast");
var cachedClient = serviceProvider4.GetRequiredKeyedService<IChatClient>("cached");

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 Fast Client:");
var fastResponse = await fastClient.GetResponseAsync("简单问题");

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 Cached Client:");
var cachedResponse = await cachedClient.GetResponseAsync("复杂问题");

Console.WriteLine("\n多客户端配置成功");

3. 深入理解 AddChatClient 的工作原理

AddChatClient 是 MEAI 提供的核心扩展方法,它简化了 AI 客户端的注册和配置过程。

  • 方法签名与重载
// 1. 直接注册现有实例
public static IChatClientBuilder AddChatClient(this IServiceCollection services, IChatClient chatClient)

// 2. 使用工厂方法创建
public static IChatClientBuilder AddChatClient(this IServiceCollection services, Func<IServiceProvider, IChatClient> factory)

// 3. 注册命名客户端(Keyed Service)
public static IChatClientBuilder AddKeyedChatClient(this IServiceCollection services, string name, IChatClient chatClient)

// 4. 命名客户端 + 工厂方法
public static IChatClientBuilder AddKeyedChatClient(this IServiceCollection services, string name, Func<IServiceProvider, IChatClient> factory)
  • 返回值:IChatClientBuilder

    所有 AddChatClient 重载都返回 IChatClientBuilder 接口,它提供了丰富的扩展方法来配置中间件:

public interface IChatClientBuilder
{
    // 添加自定义中间件
    IChatClientBuilder Use(DelegatingChatClient middleware);
    
    // 以下是常用的扩展方法:
    // - UseFunctionInvocation() - 函数调用
    // - UseDistributedCache() - 分布式缓存
    // - UseChatReducer() - 消息压缩
    // - UseLogging() - 日志记录
    // - UseOpenTelemetry() - 遥测追踪
}
  • 注册生命周期管理

    AddChatClient`默认将服务注册为 Singleton(单例)生命周期,这意味着整个应用程序中只会创建一个实例。

    如果需要其他生命周期,可以手动注册:

// Scoped 生命周期(每个请求一个实例,适合 ASP.NET Core)
services.AddScoped<IChatClient>(serviceProvider => 
{
    var baseClient = AIClientHelper.GetDefaultChatClient().Result;
    return baseClient.AsBuilder()
        .UseDistributedCache()
        .Build();
});

// Transient 生命周期(每次解析都创建新实例)
services.AddTransient<IChatClient>(serviceProvider => 
{
    return AIClientHelper.GetDefaultChatClient().Result;
});
  • 生命周期选择建议
生命周期 适用场景 优点 缺点
Singleton 无状态服务、配置固定 性能最佳,内存占用低 无法处理请求级状态
Scoped ASP.NET Core Web 应用 支持请求级状态,自动释放 每个请求创建实例
Transient 需要隔离的临时任务 完全隔离,适合并发场景 开销大,频繁 GC

注意:MEAI 的 IChatClient 实现通常是无状态的,推荐使用 Singleton 生命周期。

4. 最佳实践与设计模式

推荐做法

  • 优先使用接口类型
// 好 - 依赖抽象
public class MyService
{
    private readonly IChatClient _chatClient;
    public MyService(IChatClient chatClient) => _chatClient = chatClient;
}

// 差 - 依赖具体实现
public class MyService
{
    private readonly OpenAIChatClient _chatClient;
    public MyService(OpenAIChatClient chatClient) => _chatClient = chatClient;
}
  • 集中配置中间件管道
// 在 Program.cs 或 Startup.cs 中统一配置
services.AddChatClient(baseChatClient)
    .UseLogging()
    .UseDistributedCache()
    .UseFunctionInvocation();

// 在业务代码中配置(难以维护)
var client = baseChatClient.AsBuilder()
    .UseLogging()
    .Build();
  • 使用配置系统管理参数
// 从配置读取
var config = configuration.GetSection("AI").Get<AIConfig>();
services.AddChatClient(CreateClientFromConfig(config));

// 硬编码
services.AddChatClient(new OpenAIChatClient("hardcoded-key"));
  • 为不同场景注册命名客户端
services.AddChatClient("fast", fastClient); // 快速响应
services.AddChatClient("accurate", accurateClient); // 高质量
services.AddChatClient("cached", cachedClient); // 带缓存
  • 结合健康检查
services.AddHealthChecks().AddCheck<AIServiceHealthCheck>("ai-service");

public class AIServiceHealthCheck : IHealthCheck
{
    private readonly IChatClient _chatClient;
    
    public AIServiceHealthCheck(IChatClient chatClient) => _chatClient = chatClient;
    
    public async Task<HealthCheckResult> CheckHealthAsync(...)
    {
        try
        {
            await _chatClient.GetResponseAsync("health check");
            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(exception: ex);
        }
    }
}

常见陷阱

  • 忘记构建 ServiceProvider
// 错误 - 没有构建
var services = new ServiceCollection();
services.AddChatClient(client);
var chatClient = services.GetRequiredService<IChatClient>(); // 异常!

// 正确
var serviceProvider = services.BuildServiceProvider();
var chatClient = serviceProvider.GetRequiredService<IChatClient>();
  • 生命周期不匹配
// 错误 - Singleton 依赖 Scoped 服务
services.AddSingleton<IChatClient>(...); // Singleton
services.AddScoped<IDistributedCache>(...); // Scoped

// 正确 - 保持一致
services.AddSingleton<IChatClient>(...);
services.AddSingleton<IDistributedCache>(...);
  • 在构造函数中执行异步操作
// 错误
public class MyService
{
    public MyService(IChatClient client)
    {
        // 构造函数不支持 async
        var response = client.GetResponseAsync("test").Result; // 可能死锁
    }
}

// 正确 - 延迟到方法调用
public class MyService
{
    private readonly IChatClient _client;
    public MyService(IChatClient client) => _client = client;
    
    public async Task InitializeAsync()
    {
        var response = await _client.GetResponseAsync("test");
    }
}
  • 忘记释放 ServiceProvider
// 可能导致内存泄漏
var serviceProvider = services.BuildServiceProvider();
var client = serviceProvider.GetRequiredService<IChatClient>();

// 使用 using 自动释放
using var serviceProvider = services.BuildServiceProvider();
var client = serviceProvider.GetRequiredService<IChatClient>();

5. 架构模式

  • Repository 模式
public interface IAIRepository
{
    Task<string> GenerateResponse(string prompt);
    Task<List<float>> GenerateEmbedding(string text);
}

public class AIRepository : IAIRepository
{
    private readonly IChatClient _chatClient;
    private readonly IEmbeddingGenerator<string, Embedding<float>> _embeddingGenerator;
    
    public AIRepository(
        IChatClient chatClient, 
        IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator)
    {
        _chatClient = chatClient;
        _embeddingGenerator = embeddingGenerator;
    }
    
    public async Task<string> GenerateResponse(string prompt)
    {
        var response = await _chatClient.GetResponseAsync(prompt);
        return response.Message.Text ?? "";
    }
    
    public async Task<List<float>> GenerateEmbedding(string text)
    {
        var embeddings = await _embeddingGenerator.GenerateAsync(text);
        return embeddings.First().Vector.ToArray().ToList();
    }
}
  • Strategy 模式(多客户端切换)
public interface IAIClientFactory
{
    IChatClient GetClient(AIClientType type);
}

public enum AIClientType { Fast, Accurate, Cached }

public class AIClientFactory : IAIClientFactory
{
    private readonly IServiceProvider _serviceProvider;
    
    public AIClientFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public IChatClient GetClient(AIClientType type)
    {
        return type switch
        {
            AIClientType.Fast => _serviceProvider.GetRequiredKeyedService<IChatClient>("fast"),
            AIClientType.Accurate => _serviceProvider.GetRequiredKeyedService<IChatClient>("accurate"),
            AIClientType.Cached => _serviceProvider.GetRequiredKeyedService<IChatClient>("cached"),
            _ => throw new ArgumentException("未知的客户端类型")
        };
    }
}

6. 与其他 MEAI 功能的集成

  • 结合日志记录
services.AddLogging(builder => builder.AddConsole());

services.AddChatClient(baseChatClient).UseLogging(); // 自动记录请求和响应
  • 结合配置验证
services.AddOptions<AIConfig>()
    .Bind(configuration.GetSection("AI"))
    .ValidateDataAnnotations() // 验证配置
    .ValidateOnStart(); // 启动时验证
  • 结合遥测追踪
services.AddOpenTelemetry()
    .WithTracing(builder => builder.AddSource("Microsoft.Extensions.AI"));

services.AddChatClient(baseChatClient)
    .UseOpenTelemetry(); // 自动追踪调用链

7. 完整的企业级配置示例

var builder = WebApplication.CreateBuilder(args);

// 1. 配置日志
builder.Logging.AddConsole();

// 2. 配置缓存
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
});

// 3. 配置 AI 服务
builder.Services.AddChatClient(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    var endpoint = config["AI:Endpoint"];
    var apiKey = config["AI:ApiKey"];
    var model = config["AI:Model"];
    
    return new AzureOpenAIClient(new Uri(endpoint), new ApiKeyCredential(apiKey))
        .GetChatClient(model)
        .AsIChatClient();
})
.UseLogging() // 日志
.UseDistributedCache() // 缓存
.UseFunctionInvocation() // 函数调用
.UseOpenTelemetry(); // 遥测

// 4. 注册业务服务
builder.Services.AddTransient<IAIRepository, AIRepository>();
builder.Services.AddTransient<CustomerSupportService>();

var app = builder.Build();

app.MapPost("/api/chat", async (IChatClient client, string message) =>
{
    var response = await client.GetResponseAsync(message);
    return Results.Ok(response.Message.Text);
});

app.Run();