
1. 为什么 .NET 开发者需要把向量搜索搬进 EF Core如果你正在用 .NET 做企业级应用大概率已经遇到过这样的需求用户想在知识库里搜“忘记登录凭证怎么办”而文档里写的是“如何重置密码”。传统LIKE %密码%完全匹配不上全文检索也只能靠关键词碰运气。向量搜索解决的就是这个问题——把文本转成高维数值向量在向量空间里算余弦距离语义相近的内容自然靠得近。EF Core 从 9 开始实验性支持向量到 .NET 10 内置SqlVectorT类型和EF.Functions.VectorDistance()函数意味着你不需要引入额外的向量数据库直接在 SQL Server 2025 里就能完成相似度查询。这对已经用 EF Core 管理业务数据的团队来说迁移成本几乎为零。但真正落地时卡点往往不在 LINQ 写法而在模型调用通道Embedding 生成要调 APIRAG 问答要调 Chat 模型MCP 工具链里可能还要调 Claude 或 Codex。每个服务一套 Key、一套 Base URL环境变量散落在appsettings.json、.env、CI 配置里换模型时改到怀疑人生。我试过在一个项目里同时维护 OpenAI、Azure OpenAI 和本地 Ollama 三套配置光是对齐维度就花了半天。这篇内容聚焦一条完整链路用 TaoToken 统一 Key 和 API 通道在 EF Core 里落地向量列映射与相似度查询再把检索能力包装成 MCP 工具最后跑一次端到端验证。适合已经会用 EF Core、想快速把 RAG 检索层搭起来的 .NET 开发者。你不需要提前了解向量数据库跟着配置走就行。2. TaoToken 统一 Key 的前置准备与模型选型TaoToken 在这里扮演的角色是模型调用的统一入口。你不需要为 Embedding 和 Chat 分别申请不同厂商的 Key也不用在代码里写多套HttpClient分支。一个 API Key、一个 Base URL就能覆盖 Embedding 生成、对话补全、以及后续 MCP 工具链里的模型调用。先明确你要用到的两个模型 ID。Embedding 模型决定向量维度必须和数据库列定义严格一致。以常见的text-embedding-3-small为例输出 1536 维如果你选text-embedding-3-large就是 3072 维。Chat 模型用于 RAG 最后一步的答案生成选你习惯的即可。这两个模型 ID 在 TaoToken 的模型列表里都能查到调用方式与 OpenAI 兼容接口一致。拿到 Key 之后建议用环境变量管理不要硬编码进appsettings.json提交到仓库。本地开发可以放在launchSettings.json的environmentVariables里生产环境走密钥管理服务。下面这段配置我实测下来最省事{ TaoToken: { BaseUrl: https://taotoken.net/api, ApiKey: sk-your-taotoken-key, EmbeddingModel: text-embedding-3-small, ChatModel: gpt-4o-mini } }注意 Base URL 用https://taotoken.net/api不要在后面多加/v1OpenAI 兼容客户端通常会自动拼接路径。如果你用的是Microsoft.Extensions.AI里的OpenAIClient构造时传入这个 Base URL 即可。模型选型上有个容易踩的坑Embedding 模型一旦确定数据库里所有已存储向量的维度就固定了。中途换模型意味着全量重新生成嵌入。所以建议在项目初期就把维度写进迁移脚本比如vector(1536)后续换模型时新建列而不是改原列。另外MCP 工具链里如果要用到 Claude Code 或 Codex 这类编码 Agent它们的认证配置和普通 API Key 不同。Claude Code 走的是 Anthropic 的 OAuth 流程Codex 用auth.json。这部分在第五节排障时会展开这里先记住TaoToken 的 API Key 管的是 Embedding 和 Chat 调用编码 Agent 的认证是另一套。3. 可复制的 DbContext 配置与向量列映射这一节给出可以直接粘贴的代码。假设你已经建好了一个 ASP.NET Core 项目装了Microsoft.EntityFrameworkCore.SqlServer和Microsoft.Extensions.AI.OpenAI两个包。先定义实体。关键点是[Column(TypeName vector(1536))]和SqlVectorfloat类型两者必须匹配using Microsoft.Data.SqlTypes; using System.ComponentModel.DataAnnotations.Schema; public class DocumentChunk { public int Id { get; set; } public string Content { get; set; } string.Empty; public string Source { get; set; } string.Empty; [Column(TypeName vector(1536))] public SqlVectorfloat Embedding { get; set; } }DbContext 里正常注册DbSet并在OnModelCreating里确认列类型。如果你用的是 EF Core 10 及以上SqlVectorT已经内置不需要额外扩展包public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetDocumentChunk DocumentChunks SetDocumentChunk(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityDocumentChunk() .Property(d d.Embedding) .HasColumnType(vector(1536)); } }接下来是 Embedding 生成器的注册。用Microsoft.Extensions.AI的IEmbeddingGenerator抽象底层指向 TaoToken 的兼容端点using Microsoft.Extensions.AI; using OpenAI; var builder WebApplication.CreateBuilder(args); var taoTokenKey builder.Configuration[TaoToken:ApiKey]; var taoTokenUrl builder.Configuration[TaoToken:BaseUrl]; builder.Services.AddSingletonIEmbeddingGeneratorstring, Embeddingfloat( new OpenAIClient(new ApiKeyCredential(taoTokenKey), new OpenAIClientOptions { Endpoint new Uri(taoTokenUrl) }) .GetEmbeddingClient(text-embedding-3-small) .AsIEmbeddingGenerator());写入文档时先生成向量再存var embedding await embeddingGenerator.GenerateVectorAsync(chunk.Content); dbContext.DocumentChunks.Add(new DocumentChunk { Content chunk.Content, Source chunk.Source, Embedding new SqlVectorfloat(embedding) }); await dbContext.SaveChangesAsync();查询时用EF.Functions.VectorDistance第一个参数指定距离度量。cosine适合大多数语义检索场景euclidean适合归一化后的向量dot适合已经做过内积优化的场景var queryVector await embeddingGenerator.GenerateVectorAsync(userQuery); var sqlVector new SqlVectorfloat(queryVector); var results await dbContext.DocumentChunks .OrderBy(d EF.Functions.VectorDistance(cosine, d.Embedding, sqlVector)) .Take(5) .Select(d new { d.Content, d.Source }) .ToListAsync();如果你需要同时返回相似度分数可以在Select里再算一次距离或者用VectorDistance的返回值排序后手动计算。注意OrderBy里的距离函数会被翻译成 SQL 的VECTOR_DISTANCE不要在它外面套Math.Abs之类的 C# 方法否则会退化成客户端计算。4. 验证请求一次端到端检索跑通配置写完之后必须验证整条链路真的通了。我习惯用一个最小控制台动作来测不依赖 Web 层。第一步确认数据库里已经有向量数据。执行一条简单查询看Embedding列是否非空var count await dbContext.DocumentChunks .Where(d d.Embedding ! null) .CountAsync(); Console.WriteLine($已存储向量文档数{count});如果这里是 0说明写入环节没成功先回去检查 Embedding 生成是否返回了非空数组。第二步构造一个语义查询观察返回结果是否相关。比如数据库里存的是“如何重置密码”的文档你输入“忘记登录凭证”看 Top 1 是否命中var query 忘记登录凭证怎么办; var queryVector await embeddingGenerator.GenerateVectorAsync(query); var sqlVector new SqlVectorfloat(queryVector); var top await dbContext.DocumentChunks .OrderBy(d EF.Functions.VectorDistance(cosine, d.Embedding, sqlVector)) .Take(3) .ToListAsync(); foreach (var item in top) { Console.WriteLine($[{item.Source}] {item.Content[..Math.Min(80, item.Content.Length)]}); }实测下来如果 Embedding 模型和维度都对余弦距离排序会在 200ms 内返回结果。如果返回顺序明显不合理先检查两件事一是写入和查询是否用了同一个 Embedding 模型二是向量是否做了归一化。text-embedding-3-small默认输出已归一化不需要额外处理。第三步把检索结果拼进 Chat 请求验证 RAG 闭环var context string.Join(\n---\n, top.Select(t t.Content)); var chatClient new OpenAIClient(new ApiKeyCredential(taoTokenKey), new OpenAIClientOptions { Endpoint new Uri(taoTokenUrl) }) .GetChatClient(gpt-4o-mini); var response await chatClient.CompleteChatAsync(new[] { new SystemChatMessage(根据以下上下文回答用户问题不要编造。\n context), new UserChatMessage(query) }); Console.WriteLine(response.Value.Content[0].Text);到这里Embedding 生成、向量存储、相似度查询、RAG 生成四步全部跑通。整个过程只用了 TaoToken 一个 Key 和一个 Base URL没有为 Embedding 和 Chat 分别配置。5. 常见报错排查401、local proxy failed 与 reading choices这一节对照真实报错给出排查路径。这些错误我在不同项目里都遇到过按出现频率排序。401 Unauthorized最常见。先确认ApiKey是否以sk-开头且没有多余空格。如果你把 Key 放在appsettings.json里注意 JSON 转义。另一个隐蔽原因是 Base URL 写成了https://taotoken.net/api/v1导致客户端拼出/v1/v1/embeddings。正确写法是https://taotoken.net/api让 OpenAI 客户端自己补/v1。local proxy failed通常出现在你本地设置了 HTTP 代理环境变量但代理进程没启动。检查HTTP_PROXY和HTTPS_PROXY是否指向了一个不可用的地址。在 .NET 里HttpClient默认会读取这两个环境变量。临时解决办法是在OpenAIClientOptions里显式设置Transport new HttpClientTransport(new HttpClient(new HttpClientHandler { UseProxy false }))但生产环境还是建议把代理配置理清楚。reading choices 返回空说明请求发出去了但响应体里没有choices字段。先打印原始响应内容确认。常见原因是模型 ID 写错比如把gpt-4o-mini写成了gpt-4o-mini-2024TaoToken 侧找不到对应模型会返回错误结构。另一个原因是请求体里messages为空数组Chat 接口会拒绝。OAuth 相关报错如果你在 MCP 工具链里接的是 Claude Code它不走 API Key而是走 Anthropic 的 OAuth。报错信息里会出现invalid_grant或token expired。这时候需要重新执行 Claude Code 的登录流程和 TaoToken 的 Key 是两套体系。Codex 的auth.json同理路径通常在~/.codex/auth.json里面的 token 过期后要重新生成。维度不匹配报错信息类似The vector dimension 1536 does not match the column dimension 3072。这说明你写入时用的 Embedding 模型和建表时的维度定义不一致。解决办法是新建一个列重新生成全部嵌入或者换回原来的模型。MCP 工具注册后 AI 不调用先确认工具描述是否清晰。[McpToolParameter(Description ...)]里的描述会直接影响模型是否决定调用。描述太模糊模型可能选择不调用。另外确认 MCP 客户端已经通过ListToolsAsync拿到了工具列表并且UseFunctionInvocation()已启用。6. 把检索能力接入 MCP 工具链与统一 Key 管理最后一步把上面验证过的检索逻辑包装成 MCP 工具让 AI 助手能直接调用。这里用 C# MCP SDK 的注解方式代码结构和第三节的查询逻辑一致只是套了一层工具定义using ModelContextProtocol.Server; [McpServerToolType] public class DocumentSearchTool { private readonly AppDbContext _db; private readonly IEmbeddingGeneratorstring, Embeddingfloat _embedding; public DocumentSearchTool(AppDbContext db, IEmbeddingGeneratorstring, Embeddingfloat embedding) { _db db; _embedding embedding; } [McpServerTool(Name search_documents)] [Description(根据用户问题检索内部文档返回最相关的片段)] public async Taskstring SearchAsync( [Description(用户的问题或查询语句)] string query) { var queryVector await _embedding.GenerateVectorAsync(query); var sqlVector new SqlVectorfloat(queryVector); var results await _db.DocumentChunks .OrderBy(d EF.Functions.VectorDistance(cosine, d.Embedding, sqlVector)) .Take(5) .Select(d d.Content) .ToListAsync(); return string.Join(\n---\n, results); } }注册 MCP 服务时把AppDbContext和IEmbeddingGenerator一起注入。注意 MCP 工具的执行生命周期和 Web 请求不同DbContext要用AddDbContext注册为 ScopedMCP 框架会为每次工具调用创建作用域。如果你用的是 Claude Code 或 Cline 这类支持 MCP 的客户端配置里需要填三件套Base URL、Key、Model ID。以 Cline 的 MCP 配置为例{ mcpServers: { dotnet-rag: { command: dotnet, args: [run, --project, ./MyRagMcpServer], env: { TaoToken__BaseUrl: https://taotoken.net/api, TaoToken__ApiKey: sk-your-taotoken-key, TaoToken__EmbeddingModel: text-embedding-3-small } } } }这样配置之后Cline 启动时会拉起你的 .NET MCP ServerAI 在需要检索文档时自动调用search_documents工具。整个链路里Embedding 和 Chat 都走 TaoToken 的同一个 KeyMCP 工具本身不直接持有 Key而是通过环境变量注入。如果你需要长期跑编码 Agent 或更复杂的 Agent 工作流可以考虑用 Coding Plan 来管理调用配额和模型切换。对于只是验证模型效果的场景模型对话页面可以直接测试 Embedding 和 Chat 的返回是否符合预期。接入文档里有完整的参数说明和错误码对照表排障时比翻日志快。最后提醒一个实际部署时的细节MCP Server 如果用 stdio 传输标准输出会被协议占用所有日志必须走标准错误。如果你在工具方法里用了Console.WriteLine调试会导致 MCP 客户端解析失败。把日志改成Console.Error.WriteLine或者用ILogger输出到文件。这个坑我在第一次部署时踩过排查了半天才发现是调试输出污染了协议通道。