ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

.NET AI 核心构建块实战:用 Microsoft.Extensions.AI 重塑智能应用架构

.NET AI 核心构建块实战:用 Microsoft.Extensions.AI 重塑智能应用架构 1. 从零散 SDK 到统一抽象.NET 后端团队为什么需要 Microsoft.Extensions.AI如果你在 .NET 服务里接过 AI 能力大概率经历过这样的场景项目初期用 OpenAI 的 SDK 写了一套调用逻辑后来老板说成本太高要换成本地模型或者客户要求走 Azure OpenAI结果发现请求参数结构、鉴权方式、响应解析全都不一样改起来牵一发动全身。更麻烦的是团队里不同的人负责不同的 AI 功能有人用HttpClient裸调 REST 接口有人用第三方封装库代码风格五花八门日志和监控也没法统一。Microsoft.Extensions.AI后面简称 MEAI要解决的就是这个问题。它本质上是一套抽象层把「跟大模型对话」这件事标准化成IChatClient接口把「把文本转成向量」标准化成IEmbeddingGenerator接口。你可以把它理解成 .NET 里的ILogger或者HttpClient——底层实现可以换但上层业务代码不用动。这套东西适合谁我总结下来是三类人第一类是在现有 .NET 服务里想加 AI 功能但不想被某个厂商绑死的后端团队第二类是已经在做 RAG检索增强生成但向量库换一次就要重写一遍检索逻辑的团队第三类是想把 AI 调用纳入统一治理限流、缓存、审计、可观测性的中大型项目。配合 Microsoft.Extensions.VectorData你还能用类似 ORM 的方式操作向量数据库POCO 对象加几个特性就能映射到向量库的 Schema。再往上Microsoft Agent Framework 提供了多智能体编排能力MCP模型上下文协议则标准化了 AI 与外部工具的交互方式。这一整套构建块组合起来才是 .NET 生态下「智能应用基础层」的完整形态。这篇文章不会只讲概念。我会给出可复制的appsettings.json和Program.cs骨架演示一次完整的向量检索调用并把常见的报错和排查路径列清楚。你跟着做应该能在一个已有的 .NET 服务里把这条链路跑通。2. 前置准备TaoToken 接入与 .NET 项目环境搭建在开始写代码之前需要先把模型访问的通道准备好。这里我用 TaoToken 作为统一的模型接入层它兼容 OpenAI 的接口规范MEAI 的 OpenAI 连接器可以直接对接。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。你需要先去控制台创建一个 API Key。打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 登录后在 API Keys 页面生成一个密钥复制保存好后面配置里要用。如果你还没有账号注册流程很快邮箱验证即可。接下来确认你的 .NET 环境。MEAI 相关的包目前对 .NET 8 及以上支持最好我实测用的是 .NET 9.NET 10 的预览版也能跑。用dotnet --version确认一下版本。然后创建一个 Web API 项目作为演示载体dotnet new webapi -n MeaiDemo cd MeaiDemo安装必要的 NuGet 包。核心是Microsoft.Extensions.AI和 OpenAI 连接器向量部分需要Microsoft.Extensions.VectorData和内存向量存储用于本地验证dotnet add package Microsoft.Extensions.AI dotnet add package Microsoft.Extensions.AI.OpenAI dotnet add package Microsoft.Extensions.VectorData dotnet add package Microsoft.SemanticKernel.Connectors.InMemory这里说明一下包的选择逻辑。Microsoft.Extensions.AI是抽象层Microsoft.Extensions.AI.OpenAI提供了对接 OpenAI 兼容接口的实现TaoToken 的 API 兼容 OpenAI 规范所以直接用这个连接器就行。向量存储先用 InMemory 做验证生产环境可以换成 Qdrant、Milvus 或 Azure AI Search 的连接器代码结构不变。如果你打算长期在编码场景里用 AI 辅助可以了解一下 Coding Plan它针对代码生成和 Agent 场景做了优化 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。不过这篇文章的重点是工程化接入先把基础链路跑通。环境准备好之后我们进入配置环节。这里有个关键点不要把 API Key 硬编码在代码里用appsettings.json加环境变量的方式管理。下一节我会给出完整的配置骨架。3. 可复制配置appsettings.json 与 Program.cs 的 DI 注册骨架这一节是整篇文章的核心操作部分。我会把配置文件、DI 注册、向量存储初始化三块内容完整写出来你直接复制到项目里改一下 Key 就能用。先看appsettings.json。这里定义了模型端点、Key、模型 ID以及向量存储的基本参数{ Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } }, AllowedHosts: *, AI: { Endpoint: https://taotoken.net/api, ApiKey: sk-your-key-here, ChatModelId: gpt-4o-mini, EmbeddingModelId: text-embedding-3-small }, VectorStore: { CollectionName: demo-docs, VectorDimensions: 1536 } }注意Endpoint填的是https://taotoken.net/api不要加多余的路径。ChatModelId和EmbeddingModelId根据你在 TaoToken 控制台里可用的模型来填我演示用的是gpt-4o-mini和text-embedding-3-small这两个性价比高适合验证阶段。然后是Program.cs里的 DI 注册。这是整个接入的关键我把它拆成三段来看。第一段是读取配置并注册IChatClientusing Microsoft.Extensions.AI; using OpenAI; using System.ClientModel; var builder WebApplication.CreateBuilder(args); var aiSection builder.Configuration.GetSection(AI); var endpoint aiSection[Endpoint]!; var apiKey aiSection[ApiKey]!; var chatModelId aiSection[ChatModelId]!; var embeddingModelId aiSection[EmbeddingModelId]!; // 注册 IChatClient builder.Services.AddSingletonIChatClient(sp { var client new OpenAIClient( new ApiKeyCredential(apiKey), new OpenAIClientOptions { Endpoint new Uri(endpoint) }); return client.GetChatClient(chatModelId).AsIChatClient(); }); // 注册 IEmbeddingGenerator builder.Services.AddSingletonIEmbeddingGeneratorstring, Embeddingfloat(sp { var client new OpenAIClient( new ApiKeyCredential(apiKey), new OpenAIClientOptions { Endpoint new Uri(endpoint) }); return client.GetEmbeddingClient(embeddingModelId).AsIEmbeddingGenerator(); });这里有个细节OpenAIClientOptions的Endpoint属性用来指向 TaoToken 的 API 地址。如果你用的是官方 OpenAI这个属性可以不设但既然我们要走统一接入层就必须显式指定。第二段是注册向量存储。用 InMemory 连接器做演示生产环境换成对应数据库的连接器即可using Microsoft.Extensions.VectorData; using Microsoft.SemanticKernel.Connectors.InMemory; builder.Services.AddSingletonVectorStore(sp { return new InMemoryVectorStore(); });第三段是注册一个自定义的DocumentService把向量存储和 Embedding 生成器组合起来。这个类我们下一节会详细写builder.Services.AddSingletonDocumentService(); var app builder.Build(); // 初始化向量集合并写入示例数据 using (var scope app.Services.CreateScope()) { var docService scope.ServiceProvider.GetRequiredServiceDocumentService(); await docService.InitializeAsync(); } app.MapGet(/search, async (string query, DocumentService svc) { var results await svc.SearchAsync(query); return Results.Ok(results); }); app.Run();对应的数据模型用 POCO 加特性来定义。这里用到VectorStoreRecordKey、VectorStoreRecordData、VectorStoreRecordVector三个特性using Microsoft.Extensions.VectorData; public class DocumentRecord { [VectorStoreRecordKey] public string Id { get; set; } Guid.NewGuid().ToString(); [VectorStoreRecordData(IsFilterable true)] public string Title { get; set; } string.Empty; [VectorStoreRecordData] public string Content { get; set; } string.Empty; [VectorStoreRecordVector(1536)] public ReadOnlyMemoryfloat Embedding { get; set; } }VectorStoreRecordVector(1536)里的数字是向量维度必须和text-embedding-3-small的输出维度一致否则写入时会报维度不匹配。这个坑我后面会再提。DocumentService的实现如下它负责初始化集合、写入数据、执行语义搜索using Microsoft.Extensions.AI; using Microsoft.Extensions.VectorData; public class DocumentService { private readonly VectorStore _vectorStore; private readonly IEmbeddingGeneratorstring, Embeddingfloat _embeddingGenerator; private readonly IConfiguration _config; private VectorStoreCollectionstring, DocumentRecord? _collection; public DocumentService( VectorStore vectorStore, IEmbeddingGeneratorstring, Embeddingfloat embeddingGenerator, IConfiguration config) { _vectorStore vectorStore; _embeddingGenerator embeddingGenerator; _config config; } public async Task InitializeAsync() { var collectionName _config[VectorStore:CollectionName]!; _collection _vectorStore.GetCollectionstring, DocumentRecord(collectionName); await _collection.EnsureCollectionExistsAsync(); var docs new[] { new DocumentRecord { Title 退款政策, Content 订单签收后 7 天内可申请无理由退款需保持商品完好。 }, new DocumentRecord { Title 配送范围, Content 目前支持全国大部分城市配送偏远地区需额外 3-5 天。 }, new DocumentRecord { Title 会员权益, Content 黄金会员享受 9 折优惠和优先客服通道。 } }; foreach (var doc in docs) { var embedding await _embeddingGenerator.GenerateAsync(doc.Content); doc.Embedding embedding.Vector; await _collection.UpsertAsync(doc); } } public async TaskListstring SearchAsync(string query) { var queryEmbedding await _embeddingGenerator.GenerateAsync(query); var results _collection!.SearchAsync(queryEmbedding.Vector, top: 2); var list new Liststring(); await foreach (var result in results) { list.Add(${result.Record.Title}: {result.Record.Content}); } return list; } }这段代码里SearchAsync返回的是IAsyncEnumerable用await foreach消费。top: 2表示返回最相似的 2 条记录。整个流程就是把查询文本转成向量然后在向量集合里做相似度匹配。配置和代码都齐了。下一节我们实际跑一次请求看看结果长什么样。4. 验证请求一次完整的向量检索调用与成功结果代码写完之后用dotnet run启动项目。控制台会输出监听地址默认是http://localhost:5000或https://localhost:5001。启动过程中InitializeAsync会执行把三条示例文档写入内存向量集合。如果这一步没报错说明 Embedding 生成和向量写入都正常。现在发一个搜索请求。用浏览器或 curl 都行curl http://localhost:5000/search?query我想退货怎么办预期返回的 JSON 应该包含退款政策那条记录因为「退货」和「退款」在语义上高度相关即使查询词里没有出现「退款」两个字。这就是向量检索和关键字匹配的本质区别。我实测下来返回结果类似这样[ 退款政策: 订单签收后 7 天内可申请无理由退款需保持商品完好。, 会员权益: 黄金会员享受 9 折优惠和优先客服通道。 ]第一条命中退款政策符合预期。第二条命中会员权益相似度较低但仍在 top 2 里。如果你把top改成 1就只会返回退款政策那条。再试一个查询query寄到新疆要多久。这次应该命中配送范围那条因为「新疆」和「偏远地区」、「配送」和「寄」在语义空间里距离较近。如果你想验证聊天能力可以加一个简单的端点app.MapGet(/chat, async (string prompt, IChatClient chatClient) { var response await chatClient.GetResponseAsync(prompt); return Results.Ok(response.Text); });请求http://localhost:5000/chat?prompt用一句话解释什么是向量检索会返回模型生成的文本。这一步验证的是IChatClient的连通性。到这里一条完整的链路就跑通了配置读取 → DI 注册 → Embedding 生成 → 向量写入 → 语义搜索 → 结果返回。整个过程没有出现厂商绑定的代码如果明天要把 InMemory 换成 Qdrant只需要改Program.cs里注册VectorStore的那一行DocumentService和业务端点都不用动。如果你在验证模型输出效果时想快速对比不同模型的表现可以用模型对话页面直接测试 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。把同样的 prompt 丢进去切换模型看返回差异比在代码里反复改配置要快。5. 常见报错排查401、维度不匹配与连接失败这一节列出我在接入过程中实际踩过的坑以及对应的排查路径。你遇到问题时可以按这个顺序检查。401 Unauthorized。这是最常见的报错通常有三个原因Key 填错了、Key 前面多了空格、或者Endpoint配错了。先检查appsettings.json里的ApiKey是否完整复制有没有多余的空格或换行。然后确认Endpoint是https://taotoken.net/api不要写成https://taotoken.net/api/v1或带其他路径。如果 Key 是从控制台复制的注意有些浏览器会带上不可见字符建议重新复制一次。你可以去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 重新生成一个 Key 试试。向量维度不匹配。报错信息通常是The vector dimension does not match the collection dimension或类似提示。原因是DocumentRecord上VectorStoreRecordVector标注的维度和 Embedding 模型实际输出的维度不一致。text-embedding-3-small默认输出 1536 维如果你换成了text-embedding-3-large维度是 3072必须同步修改特性里的数字和appsettings.json里的VectorDimensions。另外如果你在同一个集合里混用了不同维度的向量也会报这个错解决方法是删掉集合重建。连接失败或超时。如果报错是Connection refused或TaskCanceledException先确认网络能访问https://taotoken.net/api。可以在浏览器里直接打开这个地址看是否返回正常的响应。如果公司网络有出口限制需要联系运维放行。另外检查OpenAIClientOptions里Endpoint的Uri构造是否正确new Uri(https://taotoken.net/api)和new Uri(https://taotoken.net/api/)在部分实现里行为不同建议不加尾部斜杠。reading choices相关报错。这个报错通常出现在响应解析阶段提示无法读取choices字段。原因可能是模型返回了非预期格式或者你用的模型 ID 在 TaoToken 侧不支持。先确认ChatModelId填的模型在控制台里可用然后检查请求是否被中间层拦截返回了错误页。如果返回的是 HTML 而不是 JSON解析就会失败。可以在HttpClient层面加日志把原始响应打出来看。OAuth 或鉴权头格式问题。如果你用的是某些需要 OAuth 的模型服务鉴权方式不是简单的ApiKeyCredential。TaoToken 的 API 兼容 OpenAI 的 Bearer Token 方式所以用ApiKeyCredential就行。如果你混用了其他鉴权方式比如把 Key 放在 query string 里会导致 401。统一用ApiKeyCredential是最稳妥的。InMemory 向量存储重启后数据丢失。这是预期行为InMemory 连接器把数据存在进程内存里重启就没了。验证阶段没问题生产环境要换成持久化的连接器。如果你在开发阶段想保留数据可以换成 SQLite 或 LiteDB 的连接器代码结构不变只改Program.cs里的注册。排查的时候有个通用技巧把日志级别调到Debug在appsettings.json里把Microsoft.Extensions.AI的日志级别设为Debug这样能看到每次请求的详细过程包括请求体、响应状态码和耗时。对于定位问题非常有用。6. 从验证到生产把这条链路接入你的真实项目验证跑通之后下一步是把它接入真实项目。这里有几个工程化建议都是我实际项目里总结出来的。第一把DocumentService的初始化逻辑从Program.cs里挪出来放到一个后台服务或启动任务里。生产环境的向量数据可能来自数据库、文件系统或消息队列初始化过程可能耗时较长不适合阻塞应用启动。可以用IHostedService实现异步初始化应用启动后先返回健康检查通过向量数据在后台慢慢加载。第二给IChatClient和IEmbeddingGenerator加上中间件。MEAI 支持类似 ASP.NET Core 的中间件管道你可以透明地注入缓存、限流、日志和审计逻辑。比如加一个基于 Redis 的缓存中间件把高频查询的 Embedding 结果缓存起来能显著降低 API 调用成本。加一个限流中间件防止单个用户消耗过多配额。这些逻辑不需要改业务代码在 DI 注册时用Use扩展方法挂上去就行。第三可观测性要跟上。MEAI 内置了 OpenTelemetry 支持你只需要在Program.cs里配置 OTEL 导出器就能在 Aspire Dashboard 或 Azure Monitor 里看到每次 AI 调用的链路轨迹包括模型延迟、Token 消耗和异常信息。这对于生产环境的故障排查和成本分析非常关键。第四向量存储的连接器选择。验证阶段用 InMemory生产环境根据你的基础设施选。如果已经在用 SQL Server可以用 SQL Server 的向量扩展如果用云服务Azure AI Search 或 Qdrant Cloud 都是成熟选择。切换连接器只需要改Program.cs里注册VectorStore的那一行DocumentService和业务代码完全不用动这就是抽象层的价值。如果你在接入过程中需要查具体的 API 参数和连接器用法接入文档里有详细的说明 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。遇到报错先对照文档检查配置大部分问题都能快速定位。最后说一个实际经验不要一上来就把所有 AI 功能都接进去。先选一个最小的场景比如商品搜索或客服问答把这条链路跑通、跑稳再逐步扩展到更复杂的场景。MEAI 的抽象层设计让你可以渐进式地替换和扩展不需要一次性重构整个服务。
返回列表