资讯动态

Microsoft.Extensions.AI:.NET统一AI抽象层实战解析

发布时间:2026/10/19 5:20:06 来源:尧图企业网站定制
1. 为什么需要Microsoft.Extensions.AI.NET接入AI的乱局与破局1.1 生态碎片化每个模型商一个SDK的痛先聊一个很现实的场景。你接了一家大模型商的Chat接口写了一整套Prompt管理、历史上下文、工具调用逻辑跑得挺好。过了半年老板说换一家模型供应商吧成本更低效果也差不多。这时候你就开始头疼了——换SDK意味着所有调用代码全部重写连消息类型都不一样更别说流式响应、工具调用参数绑定这些细节。我记得更早的时候还得同时维护好几个SqlClient、RedisClient的感觉又回来了。别的语言生态早就有统一的数据库访问层.NET里反而没有一个统一的AI抽象层。每个服务商都给你一套独立SDK命名风格不同、异步模式不同、错误处理机制不同甚至连“对话消息”这个概念在不同SDK里的表达方式都不一样。这套碎片化生态带来三个非常实际的麻烦技术选型被锁定今天觉得这家模型好想换上另一家代价比想象中高得多。想做个日志中间件、统一重试策略、缓存层得在每个SDK里各写一遍。团队成员的知识无法复用换个模型商新人又得从头学一套SDK。时间一长团队里就会开始出现各种“AI适配层”——有人用工厂模式包了一层有人直接写死一个接口后院已经各起炉灶了。1.2 抽象层到底解决了什么问题Microsoft.Extensions.AI就是在这样的背景下出现的。它做的事情本质上和当年ADO.NET统一各种数据库访问是一样的思路——定义一套抽象的接口和类型让不同的模型服务商通过适配器官方叫Builder、Client实现接入进来。你的业务代码只依赖这套抽象不直接依赖某一个厂商SDK。我最早看到这个库时的第一反应是这个名字起得真直白就是塞进了.NET自带的Microsoft.Extensions体系里和依赖注入、日志、配置这些基础设施站在同一层。你可以把IChatClient注入到任何服务里不用关心背后到底是哪家模型。更关键的是这套抽象不是只做“最小公共接口”。它把很多AI应用真正会用到的东西都纳入了抽象范围包括消息定义与历史维护模型参数配置流式与非流式响应工具/函数调用嵌入向量生成中间件管道日志、缓存、重试、Token统计等这意味着基于这套抽象写出来的业务代码既能在OpenAI兼容接口上跑也能在Azure OpenAI上跑还能在本地模型服务上跑。1.3 和其他“全家桶”方案的区别你可能要问那Semantic Kernel呢它不也是微软出的AI框架吗还有LangChain、LangGraph这些不也都干了这件事区别还是有的。Semantic Kernel更像一个完整的AI业务编排框架定位偏重Agent、规划器、Planner这些高阶能力抽象层级更高绑定也更重。而Microsoft.Extensions.AI选择站在更底层它只做标准化的“AI接入接口层”没有强加任何Prompt编排、Agent执行链、插件体系给你。打个比方Semantic Kernel是带装修的整套房子Microsoft.Extensions.AI是水电管线的接口标准。你用后者可以自己决定上面怎么盖楼。它甚至没有要求你必须用某个特定容器、特定配置方式。IChatClient就是一个普通接口你可以手动new一个实例也可以扔进.NET自带的依赖注入容器里。它不会替你决定太多事情这正是它好用的地方。2. 核心概念拆解一次搞懂IAI接口家族2.1 IChatClient聊天补全的抽象核心整个库的核心接口只有一个就是IChatClient。public interface IChatClient : IDisposable { TaskChatResponse GetResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default); IAsyncEnumerableChatResponseUpdate GetStreamingResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default); }你盯住这个接口就够了。所有模型商OpenAI也好、Azure OpenAI也好、本地跑个Ollama也好最终都是把这个接口实现出来让上层代码无感切换。第一次接触这套接口的人可能会有点不习惯因为很多消息字段不是在ChatMessage上直接平铺的而是通过AdditionalProperties之类的字典透传的。比如模型商特定的参数像openai的temperature、top_p这类放在ChatOptions里有一批通用属性可以映射但一些厂商私有参数就得靠AdditionalProperties塞进去。我给你的建议是先把它当成一个“标准化消息协议”来理解别纠结某个厂商的字段在哪儿。核心流程就是——“把消息丢进去拿到响应”仅此而已。ChatResponse这个对象看起来包了一层里面才是真正的响应内容。访问最终文本走response.Text但如果你想拿到原始返回对象response.Raw可以满足你。这个设计我挺喜欢把通用抽象和原始保留都照顾到了。2.2 ChatMessage与底层消息模型ChatMessage在表面上看挺简单就是个角色加内容public class ChatMessage { public ChatRole Role { get; set; } public string? Text { get; set; } }但实际上它内部能处理的内容类型远不止纯文本。你需要构建多模态消息比如图片、音频、视频时靠的是ChatMessageContentItem这个内容项体系。我还记得第一次想往消息里塞图片时直觉是找url字段找半天没找到。后来才搞明白图片是通过new ChatMessageContentItem { Image ... }这种方式放进Contents列表的。这套模型在刚开始接触时会觉得“绕了一下”但你一旦理解了“一个消息可以包含多个ContentItem”之后处理多模态反而比某些SDK灵活得多。另外ChatMessage在设计上还带了一个AuthorName属性这在多租户、多用户对话场景里很有用。你可以通过它区分不同用户的发言而不需要自己在会话状态里额外维护一套用户名映射。2.3 ChatClientBuilder与中间件管道如果说IChatClient是这个库的心脏那ChatClientBuilder就是主动脉所有的高级玩法都是围着它转的。var builder new ChatClientBuilder(innerClient) .UseLogging() .UseRetry() .UseFunctionInvocation(); var pipelineClient builder.Build();这个Builder采用的是管道模式你可以理解为ASP.NET Core里的中间件管道。每次请求从前向后穿过所有中间件最终到达最里层的真实模型客户端响应再从后向前返回来。这个设计带来的能力非常恐怖——你可以在不改动业务代码的前提下给AI调用链加上缓存、超时、重试、Token统计、审计日志、敏感词过滤这些统统是以中间件形式挂在管道上的。管道顺序很关键。比如你想做“缓存优先”如果命中缓存就直接返回不再继续往内层走那缓存中间件必须放在最前面。而如果你想把调用日志打到缓存命中之外的所有请求日志中间件就放在缓存后面。这个和写.NET管道中间件的思维方式一模一样只要是搞过ASP.NET Core的人上手几乎零成本。2.4 工具调用与AI能力扩展现在几乎没有哪个正经的AI应用只做纯文本聊天了模型需要调用你代码里的方法去查数据库、操作文件、拉天气信息这就得靠Function Calling。在Microsoft.Extensions.AI里工具调用的抽象是通过AIFunctionFactory来创建函数然后塞进ChatOptions里的Tools列表。库会把函数名称、描述、参数JSON Schema全部序列化给模型模型返回“我想调用哪个函数、参数是什么”框架再帮你绑定并执行。var getWeather AIFunctionFactory.Create((string city) { return GetWeatherInternal(city); }); var options new ChatOptions(); options.Tools.Add(getWeather);这个部分建议你仔细看下一节实操内容。函数调用里容易踩坑的地方非常多参数命名不匹配、函数描述不清晰、返回值序列化失败任何一个环节都有可能出问题。3. 环境准备与最小落地案例3.1 NuGet包的选择与安装这个库不是一个大包全包括官方拆了好几个NuGet包。我先说一下常见组合。Microsoft.Extensions.AI基础抽象和通用中间件Microsoft.Extensions.AI.OpenAI对接OpenAI及OpenAI兼容接口的适配器Microsoft.Extensions.AI.AzureOpenAI对接Azure OpenAI服务的适配器Microsoft.Extensions.AI.Ollama对接本地Ollama服务的适配器我建议你把基础包固定下来适配器按需引入。如果将来要切换供应商只换适配器包就行业务层代码基本不用动。安装方式就是常规的dotnet命令我一般习惯建一个干净的类库项目专门放AI基础设施业务层引用它避免所有项目都散落着各种AI包的引用来引用去。3.2 对接OpenAI兼容接口的完整写法现在市面上很多网关、代理服务都提供OpenAI兼容接口。团队统一通过这类服务接模型反而省心。下面的代码是官方开箱即用的写法using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.OpenAI; IChatClient innerClient new OpenAIClientClient( new OpenAIClient(new ApiKeyCredential(apiKey), new OpenAIClientOptions { Endpoint new Uri(https://your-gateway.example/api/) }), modelName: gpt-4o-mini); IChatClient chatClient new ChatClientBuilder(innerClient) .UseLogging() .Build(); var response await chatClient.GetResponseAsync( 介绍一下你自己);这里有个官方刻意保持的特性即使你用的是OpenAI兼容网关接口签名也和用Azure OpenAI时保持完全一致。你只需要换掉OpenAIClientClient的构造参数就行。有一点必须注意ApiKeyCredential这类安全性设计是依赖.NET的Credential体系来做的建议别把API Key直接硬编码在代码里而是从环境变量、用户机密或配置中心读取。日志中间件也注意一下不要打印完整请求头密钥有泄露风险。3.3 对接Azure OpenAI如果你的生产环境用的是Azure OpenAI代码几乎不用改IChatClient innerClient new AzureOpenAIClientClient( new AzureOpenAIClient( new Uri(endpoint), new ApiKeyCredential(apiKey)), modelName: gpt-4o);注意构造参数和OpenAI版本几乎一样就多了一个Azure的Endpoint地址。这个统一性在切换模型商的时候真的能省下大量时间。如果你用的是托管身份Managed Identity可以传DefaultAzureCredential而不是ApiKey。走托管身份是生产环境更安全的选择避免API Key泄漏在你部署的每一台机器上。3.4 对接本地模型Ollama与私有化部署本地模型是目前很多企业的刚需数据不出内网、离线可用、隐私可控。Microsoft.Extensions.AI提供专门的Ollama适配器代码更简洁IChatClient innerClient new OllamaChatClient(new Uri(http://localhost:11434), modelName: qwen2.5); var response await innerClient.GetResponseAsync(写一段.NET异步编程的示例);这套接口在本地模型上跑起来的感觉其实比云端模型还舒服——没有限流、没有额外延迟、还能断网调试。我做开发调试时很多逻辑都是用Ollama本地模型先跑通的跑完再切到云端模型做最终验证。Ollama支持的模型非常多qwen、llama3、phi系列这些都能直接用。只要你的机器内存够随手起一个模型验证代码逻辑非常方便。3.5 在ASP.NET Core中注册与使用把IChatClient注入ASP.NET Core容器是生产项目最常规的玩法。只需要在Program.cs里做一次配置builder.Services.AddSingletonIChatClient(sp { var config sp.GetRequiredServiceIConfiguration(); var modelEndpoint config[AI:Endpoint]; var modelName config[AI:ModelName]; IChatClient innerClient new OpenAIClientClient( new OpenAIClient(new ApiKeyCredential(config[AI:ApiKey]), new OpenAIClientOptions { Endpoint new Uri(modelEndpoint) }), modelName: modelName); return new ChatClientBuilder(innerClient) .UseLogging() .UseRetry(maxRetries: 3) .Build(); });之后在你的业务Service里直接构造函数注入IChatClient就行。依赖注入容器会把整个中间件管道和底层客户端都管理好。这里有个不小的问题容易忽略IChatClient实现了IDisposable如果注册成Singleton底层HttpClient会被长生命周期复用这其实是好事。但如果你注册成Transient或者Scoped每次创建都会new一个HttpClient连接池会被迅速耗尽这一块往下我会在避坑部分详细展开。4. 中间件机制自定义管道的实战4.1 为什么需要中间件做过几年.NET开发的人对中间件这个概念都不陌生。但在AI接入的场景里中间件的价值会被放得更大因为AI调用的不确定性太高了——模型可能超时、可能返回格式错误、可能需要重试、可能响应巨大全靠业务层每次自己去处理这些Flag就太痛苦了。中间件能让你把这些横切关注点集中起来一次配置处处生效。举个例子你给AI调用加一个“敏感内容过滤”中间件模型返回的内容先过一遍过滤逻辑有风险内容直接拦截这个只写一处代码所有业务调用都会自动生效。没有中间件机制的话你得在每个调用AI的方法里复制粘贴过滤逻辑。4.2 官方内置中间件一览官方目前内置的中间件主要覆盖这些能力我有次在代码里把它们全部列出来看了下基本能覆盖常见的生产需求中间件功能作用典型使用场景UseLogging记录调用参数、耗时、Token用量审计、监控、调试UseRetry对失败请求重试网络抖动、限流恢复UseFunctionInvocation识别模型请求的工具并自动执行接入函数调用、读取外部数据UseOpenTelemetry生成标准遥测数据接入监控大盘、链路追踪这些中间件都是可以通过Builder链式挂载的。挂了它们之后你的业务代码本身不会多一行但运维体验天差地别。4.3 开发自定义中间件一个缓存中间件的完整实现我现在拿一个真实需求来演示怎么实现自定义中间件——给AI调用加一层结果缓存。实现思路是我们的某些业务场景会让用户反复查同一类问题比如FAQ问答短期内答案基本不变。如果每次请求都打给模型成本高、延迟高。加一层缓存命中就直接返回未命中再往下传递。public sealed class CachingChatClient : IChatClient { private readonly IChatClient _innerClient; private readonly IMemoryCache _cache; public CachingChatClient(IChatClient innerClient, IMemoryCache cache) { _innerClient innerClient; _cache cache; } public TaskChatResponse GetResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default) { var key BuildCacheKey(messages); if (_cache.TryGetValue(key, out ChatResponse? cached)) { return Task.FromResult(cached!); } var response _innerClient.GetResponseAsync(messages, options, cancellationToken); _cache.Set(key, response.Result, TimeSpan.FromMinutes(10)); return response; } public async IAsyncEnumerableChatResponseUpdate GetStreamingResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, [EnumeratorCancellation] CancellationToken cancellationToken default) { // 流式场景的缓存实现更复杂 await foreach (var update in _innerClient.GetStreamingResponseAsync(messages, options, cancellationToken)) { yield return update; } } private static string BuildCacheKey(IEnumerableChatMessage messages) { var sb new StringBuilder(); foreach (var m in messages) { sb.Append(m.Role).Append(:).Append(m.Text).Append(|); } return sb.ToString(); } }然后这样挂上去var chatClient new ChatClientBuilder(innerClient) .Use(new CachingChatClient(innerClient, cache)) .Build();需要注意缓存键如果简单用消息文本拼接遇到带图片内容、工具调用结果、上下文超长的情况会不准确。生产级缓存还得考虑Prompt模板、系统指令这些稳定因素。4.4 管道顺序与执行流程中间件管道顺序非常重要。我在开发阶段就吃过一次亏——把缓存中间件放在最内层紧贴模型客户端结果缓存根本没生效因为外层日志已经打过了每次请求都打到了日志白做排查。正确的规则是越靠近外层越优先执行靠近内层则是越靠近真实模型。举例说明new ChatClientBuilder(innerClient) .UseLogging() // 第一层日志 .UseRetry() // 第二层重试 .Use(new CachingChatClient(innerClient, cache)) // 第三层缓存 .Build(); // 最内层真实客户端请求进来先从日志走再到重试再到缓存。如果缓存命中就直接返回不会再继续往内层模型客户端走。但如果把缓存放在最外面缓存会和日志决策耦合日志记录的是“命中缓存”还是不“命中缓存”信息就丢失了。在设计中间件时我的原则是能命中就尽早短路重试和日志放在外层数据兜底放在内层。5. 高级玩法工具调用、流式输出与结构化输出5.1 用Function Calling让模型调用C#方法工具调用是现在最实用的AI能力因为它让模型从“只会聊天”变成“能操作你的系统”。具体来说三步走用AIFunctionFactory创建一个C#函数自动生成JSON Schema给模型把函数放进ChatOptions.Tools在中间件管道中加载UseFunctionInvocation让框架自动完成“模型请求调用→绑定参数→执行方法→返回结果”的闭环我来写一个完整示例public static async TaskIChatClient CreateWeatherClient() { var innerClient new OpenAIClientClient( new OpenAIClient(new ApiKeyCredential(apiKey)), modelName: gpt-4o-mini); var getWeather AIFunctionFactory.Create((string city) { return $当前{city}气温25度晴; }); // 把函数封装成ChatTool var weatherTool AIFunctionFactory.CreateAIFunction(getWeather, get_weather, 查询城市天气); return new ChatClientBuilder(innerClient) .UseFunctionInvocation() .Build(); } var chatClient await CreateWeatherClient(); var options new ChatOptions(); options.Tools.Add(weatherTool); var response await chatClient.GetResponseAsync( new ListChatMessage { new ChatMessage(ChatRole.User, 请问上海今天适合穿短袖吗) }, options);当模型认为需要知道天气才能回答“上海今天适合穿短袖吗”这个问题时它会返回一个函数调用请求UseFunctionInvocation中间件会自动拦截这个请求调用你的get_weather方法把返回结果拼接回对话上下文再让模型生成最终回答。函数返回值要尽量描述清楚让模型能顺利接话。如果函数返回了一个裸数据结构而不是自然语言模型可能不知道该怎么整合。最佳实践是让函数返回一句话描述模型把它吸收进上下文再组织答案。5.2 流式输出与逐字响应的实现流式输出是聊天应用的基本体验不能在用户点了发送之后干等好几秒必须一个字一个字地蹦出来才有交互感。await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options)) { Console.Write(update.Text); }这里的ChatResponseUpdate代表流式过程中的一次增量更新。它的Text字段往往只是一小段文本把它们依次拼起来才是完整的回答。后端接入SignalR或WebSocket推送时可以把update.Text直接推给前端实现打字机效果。前端不需要组装只需要追加显示。这里要注意流式响应的最后一个update里额外信息才是完整的Token统计和FinishReason。如果平台要展示Token消耗不要在第一次update里拿那是0要在流结束后汇总。我的经验是把流式文本收集和最终汇总分开做两个事件回调var fullText new StringBuilder(); await foreach (var update in updates) { if (!string.IsNullOrEmpty(update.Text)) fullText.Append(update.Text); // 推给前端 await _signalR.Clients.All.SendAsync(OnDelta, update.Text); } var result fullText.ToString(); await _signalR.Clients.All.SendAsync(OnCompleted, result);5.3 结构化输出与JSON模式如果你想让AI从非结构化文本里抽出结构化数据比如“从简历里提取候选人姓名、年龄、工作年限”直接靠提示词让模型返回JSON然后自己解析这种方式经常翻车——格式偶尔错、字段偶尔丢。Microsoft.Extensions.AI里更稳的做法是在ChatOptions里指定ResponseFormat为Json并配合AIFunction工具的返回类型引导模型输出。不过最省心的方案是直接用Tool Call做结构化提取让模型必须调用一个“输出实体”的函数参数Schema就是你的目标结构模型为了调用函数自然会输出符合Schema的参数。[Function] public static void SaveResume( [Description(候选人姓名)] string name, [Description(工作年限)] int years, [Description(技能列表)] string[] skills) { }然后通过AIFunctionFactory创建工具。这样返回结果天然就是强类型的对象不用再手写字符串解析。这一招在生产中真的非常实用。以前做实体抽取时用提示词返回JSON十次有三四次格式出错要写一堆重试逻辑。换成工具调用强制输出之后成功率几乎100%。5.4 嵌入向量生成与语义检索除了文本对话微软这套抽象里还有一个嵌入生成相关的能力。虽然目前IChatClient层面把嵌入放在独立接口里但整体设计是互通的。用一行代码就能生成向量var embeddings await client.GenerateAsync( 如何实现分布式事务, cancellationToken);拿到向量之后你可以把它存进向量数据库比如已有.NET生态适配的Qdrant、Milvus、pgvector等。检索时把用户问题也转成向量做余弦相似度Top-K检索再把召回结果塞进Prompt上下文让模型基于检索结果回答。这就是RAG检索增强生成的完整链路。M.E.AI负责生成向量你的业务代码负责检索和拼装Prompt。抽象层的好处是以后想换嵌入模型只需要换一个厂商标识向量维度可能会变索引可能需要重建但代码逻辑基本不用动。6. 常用场景与代码骨架拿来即用6.1 多模型路由与自动降级生产环境里只用一家模型服务是有风险的。它可能限流、可能临时故障、可能某类问题表现差。我在做客服机器人时用了多模型路由方案效果不错。核心思路是写一个路由IChatClient实现内部维护多个底层Client比如一个主用、一个备用。调用时先走主用失败或超时就切换到备用。public sealed class FallbackChatClient : IChatClient { private readonly IChatClient _primary; private readonly IChatClient _secondary; public async TaskChatResponse GetResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default) { try { return await _primary.GetResponseAsync(messages, options, cancellationToken); } catch (Exception ex) { Console.WriteLine($Primary failed, falling back: {ex.Message}); return await _secondary.GetResponseAsync(messages, options, cancellationToken); } } // 其他成员略 }这个FallbackChatClient本身也是一个IChatClient可以嵌套在中间件管道里。外层继续挂日志、重试内层自动完成主备切换。这种“管道套管道”的写法正好体现了抽象层设计的优雅之处。6.2 带历史上下文的对话服务对话场景里维护上下文是必须的。你可以自己拼历史消息也可以合理利用ChatMessage里的Role标记来构造多轮结构。最简单的方式是每次请求时携带整个对话列表但Token消耗会随着对话增长越来越夸张。这种情况下我给团队定的方案是系统指令固定、最近N轮完整保留、更早的对话做摘要后作为一条摘要消息放在最前面。var messages new ListChatMessage { // 早期对话的摘要 new(ChatRole.System, 此前对话摘要用户咨询过退换货政策……), // 最近的几轮完整消息 new(ChatRole.User, 那我当前这笔订单符合条件吗), };这种方案比全部历史丢进去经济很多并且模型对摘要的理解效果还不错。6.3 在现有业务代码中嵌入AI能力很多时候项目里的AI调用不是单独一个Service而是分布在各个业务模块里。引入M.E.AI后你只需要做一次依赖注入然后每个模块构造函数都注入IChatClient调用方式完全一致。比如订单模块需要智能客服自动回复财务模块需要自动生成财务分析摘要内容模块需要自动打标。每个模块都对着同一个IChatClient但它们完全不知道背后是什么模型、走的是什么通道。这种解耦带来的可维护性提升比你想的要大。我有一次需要排查问题把IChatClient换成了本地模型跑整个测试套件结果所有业务模块照常工作只是模型变了一个。那一刻我确实挺感慨——这套抽象真的做到了Model Agnostic。7. 避坑实录我调试过程中遇到的典型问题7.1 版本冲突与依赖地狱M.E.AI的版本迭代很快而且它和Semantic Kernel这类框架的包有一定的依赖重合。我在一个项目里同时引用了Semantic Kernel和M.E.AI结果构建时出现了程序集版本冲突。遇到这个不要慌做法分三步看NuGet上这些包的真实依赖链找到是谁引了老版本。在统一的项目里显式指定一个较高版本。如果还是冲突升级Semantic Kernel版本是一个常见解法因为它也在逐渐转向兼容M.E.AI的体系。我的建议是在你新建项目的时候尽量保持M.E.AI这个技术栈独立不要在同一个业务Service里混用两套AI SDK避免维护两套消息模型带来心智负担。7.2 HttpClient生命周期管理这个坑真的非常经典。M.E.AI的底层客户端很多都是基于HttpClient的如果你每次创建ChatClient都new一个HttpClient并使用Transient生命周期注册到DI容器很快你会看到连接耗尽异常。正确的做法是始终复用HttpClient实例。如果你是手动管理就创建一个静态的HttpClient或使用IHttpClientFactory。如果你用DI容器把IChatClient注册成Singleton让它自带的内部HttpClient长期存活。我用IHttpClientFactory改造过一套代码连接池复用之后内存和Socket数都明显降下来了。7.3 函数调用参数绑定失败的处理模型生成的函数参数不总是符合你的C#方法的预期。最常见的问题是数值类型——模型说年龄是“一百零八”绑定到int参数就失败了。解决方案有三层函数参数尽量设计得简单明确能用枚举表达就别接收自由文本。在函数内部加一层防御不能假设参数永远是对的。当UseFunctionInvocation失败时捕获异常把错误信息塞回对话上下文让模型重新生成正确的参数。builder.UseFunctionInvocation((invocation) { try { return invocation.Context.Function.InvokeAsync(invocation.Arguments); } catch (Exception ex) { return Task.FromExceptionobject?(ex); } });框架会把你返回的异常信息回调给底层模型模型收到错误提示后会尝试重新调整参数。这算是一个自动纠错机制。7.4 流式响应中内容收集的坑流式响应环节很多新手都丢内容。问题出在只拿了第一段update.Text就往数据库存然后发现存下来的答案不完整。务必记住流式更新是一次一次增量每一片update.Text只代表一个片段不是完整答案。最终完整文本必须把所有增量全部拼接起来。还有一点某些模型的流式响应里第一个update返回的是空的Text和拦截的工具调用信息不是用户可见内容。如果你的前端逻辑看到Text为空就直接结束流那模型一句话都显示不出来。处理方式就是跳过空Text继续等。7.5 超时与Token限制的调优AI调用慢很多时候不是接口真慢而是超时太短或输出Token限制卡住了长文本生成。在ChatOptions里有两处需要关注MaxOutputTokens模型单次最多输出的Token数如果你用默认值可能限制在几百Token长报告直接被截断。超时HttpClient里设置的Timeout不能太短长对话生成几百字可能需要几十秒。我建议把超时设置到120秒甚至更高尤其是生产环境。同时把MaxOutputTokens根据业务需要调大比如生成日报摘要时给足4000Token生成一句回复时给500Token就可以。这个“够用但不过度”的原则既保证体验也让Token成本可控。最后分享一点个人体会实际上M.E.AI这套库的潜力还在释放的过程中。我把它用在几个内部工具和客户项目里最大的感受是当你围着抽象接口写代码时很多以前觉得麻烦的事——模型切换、统一监控、团队协作——都变得顺畅起来。如果你现在刚开始接触.NET生态的AI开发我的建议很直接不要一上来就追求全家桶框架先用M.E.AI搭一个最简管道哪怕只是读一条消息然后把日志中间件挂上把函数调用跑通。这套基础打牢之后后面想接什么模型都只是换适配器的事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑