1. 项目概述当RAG遇见原生向量数据库如果你正在用C#和.NET技术栈构建智能应用并且厌倦了在应用架构里额外引入一个专门的向量数据库比如Pinecone、Weaviate所带来的运维复杂度和成本那么marcominerva/SqlDatabaseVectorSearch这个开源项目绝对值得你花时间深入研究。这个项目本质上是一个功能完整的RAG检索增强生成应用示例但它最核心的亮点在于它直接利用了Azure SQL Database以及SQL Server 2022中新增的原生VECTOR数据类型来存储和检索向量实现了“一个数据库两种用途”——既存结构化数据又当向量数据库用。我最初接触这个项目是因为在一个企业知识库项目中客户对数据主权和架构简洁性有极高的要求。他们不希望为了AI功能而引入新的数据存储所有数据必须留在现有的SQL Server里。当时市面上成熟的方案大多依赖外部向量库直到我发现了这个项目它完美地验证了“用你熟悉的数据库做向量搜索”的可行性。项目基于.NET 10、Blazor Web App和Minimal API构建前端交互和后台API一应俱全并且深度集成了微软的Semantic Kernel来处理与Azure OpenAI的交互从文档解析、文本分块、向量化到语义搜索和对话提供了一条龙式的实现参考。简单来说它解决了几个关键痛点一是降低了技术栈的复杂度对于已经重度投资微软技术生态的团队无需学习新的查询语言或运维新的数据库服务二是保证了数据的一致性文档、其文本块、对应的向量以及对话历史都存储在同一个事务性数据库中避免了数据同步的麻烦三是提供了生产级代码的参考包括流式响应、对话历史管理、引用溯源等细节不是简单的Demo。接下来我会结合自己的实践经验拆解这个项目的设计思路、关键实现细节以及那些你在官方文档里可能找不到的“踩坑”心得。2. 核心架构与设计思路拆解2.1 为什么选择Azure SQL Database的VECTOR类型在向量搜索领域专用向量数据库如Milvus, Qdrant通常以极高的性能和丰富的相似度算法著称。那么把向量塞进关系型数据库性能真的够用吗这是很多人第一时间的疑问。这个项目的设计选择背后有非常务实的考量。首先是架构简化与运维成本。对于许多中小型应用或处于验证阶段的AI功能引入一个全新的数据库系统意味着额外的部署、监控、备份和安全策略。Azure SQL Database本身作为托管服务已经具备了高可用、自动备份、安全合规等企业级特性。利用它的VECTOR类型你相当于在已有的、成熟的运维体系上增加了一个新功能而不是管理两个独立的系统。其次是数据一致性与事务支持。这是关系型数据库的绝对强项。在这个RAG应用中一个文档被拆分成多个文本块Chunk每个块生成一个向量。在专用向量数据库中插入文档、生成向量、存储向量这几个步骤很难保证在一个原子事务中完成。而在SQL Database中你可以利用EF Core的事务确保文档元数据、文本块和向量要么全部成功写入要么全部回滚这对于数据完整性要求高的场景至关重要。再者是查询的灵活性与生态整合。SQL的强大之处在于其声明式的查询能力和丰富的连接JOIN操作。你可以非常容易地将向量相似度搜索的结果与用户表、权限表或其他业务表进行关联查询实现复杂的业务逻辑。所有你熟悉的.NET工具链如Entity Framework Core、Dapper都能继续使用。关于性能Azure SQL Database的向量搜索基于内积DOT_PRODUCT和余弦相似度COSINE_DISTANCE函数并可以利用列存储索引进行加速。对于千万级别以下的向量数据量在合理的索引和硬件配置下其响应时间完全能够满足大部分交互式应用的需求百毫秒级。当然如果你的场景是每秒需要处理上万次向量查询的超高并发推荐系统那么专用向量数据库可能仍是更好的选择。但对于企业知识库、客服助手、内容检索这类典型RAG场景SQL Database的方案是一个在性能、成本和复杂度之间取得极佳平衡的选择。2.2 技术栈选型.NET生态的深度整合这个项目可看作是微软AI开发生态的一个“最佳实践”展示其技术选型体现了高度的集成性和现代性。.NET 10与Blazor Web App采用最新的.NET长期支持版本和Blazor的全栈Web开发模型。Blazor允许你使用C#来编写前后端逻辑共享代码这对于全栈.NET开发者来说极大地提升了开发效率。项目采用Blazor Web App.NET 8引入的模板天然支持服务端渲染SSR和交互式WebAssembly组件可以根据场景灵活选择。Semantic Kernel (SK)这是微软推出的AI编排框架相当于.NET领域的LangChain。项目使用SK来封装与Azure OpenAI的交互包括调用嵌入模型Embedding生成向量以及调用大语言模型LLM进行对话和问题重述。SK提供了插件Plugins、规划器Planner等高级抽象虽然本项目未使用这些复杂功能但为未来扩展留下了空间。Entity Framework Core (EF Core) 8用于数据访问和迁移。EF Core 8对Azure SQL的VECTOR类型提供了原生支持可以将其映射为float[]数组或使用特定的类型转换器。项目中的数据模型设计清晰地反映了RAG的核心实体Document上传的文档、Chunk文档拆分后的文本块、Embedding存储向量和元数据以及Conversation对话历史。Minimal API除了Blazor前端项目还暴露了一组Minimal API端点。这种轻量级的API设计模式使得创建HTTP API变得非常简洁适合构建微服务或提供给其他客户端如移动应用、第三方系统调用的接口。/api/ask和/api/ask-streaming这两个核心端点就是典型的Minimal API实现。这种技术栈组合确保了从数据持久化、业务逻辑到AI集成和用户界面的整个链路都处于.NET生态之内工具链统一调试和部署体验连贯。2.3 数据模型设计如何组织RAG的“记忆”理解数据模型是理解整个应用如何工作的关键。项目的Data/文件夹下定义了核心的实体类。Document实体代表用户上传的一个原始文件如PDF、DOCX。它包含文件名、文件类型、上传时间等元数据并拥有一个到Chunk集合的导航属性。一个Document会被拆分成多个Chunk。Chunk实体代表从文档中拆分出来的一段文本。它包含原始文本内容、在文档中的页码、块索引等信息。最关键的是它通过一个EmbeddingId外键关联到一个Embedding实体。这种设计将文本内容与其向量表示解耦是很有讲究的。Embedding实体这是核心。它包含一个Vector属性在数据库中映射为VECTOR(1536)或你指定的维度用于存储文本块通过Azure OpenAI嵌入模型计算得到的浮点数数组。它还存储了生成该向量所使用的模型名称和维度。一个Embedding可以被多个Chunk引用吗在当前设计中是一对一关系即一个Chunk对应一个Embedding。这种设计保证了向量和文本块的严格对应也便于管理。Conversation 与 ConversationHistory 实体用于实现多轮对话。Conversation代表一次完整的会话ConversationHistory则记录会话中的每一轮问答。历史记录中不仅保存了用户问题和AI答案还保存了问题重述ReformulatedQuestion以及用于生成答案的引用Citation信息。引用信息关联回具体的Chunk从而可以追溯到源文档的某个片段这是实现答案可解释性的基础。注意这里有一个重要的设计细节。Embedding实体独立存储而不是将Vector字段直接放在Chunk表里。这样做的好处是当你想切换嵌入模型例如从text-embedding-ada-002升级到text-embedding-3-large或者调整向量维度时可以更灵活地管理历史数据或者为同一文本块存储不同模型的向量以做对比实验。3. 核心流程与实现细节解析3.1 文档处理流水线从文件到可搜索的向量当你通过Web界面或API上传一个文档时后台会触发一系列复杂的处理步骤。这个过程封装在Services/DocumentService等文件中。第一步文档解析与文本提取项目支持PDF、DOCX、TXT和MD格式。对于PDF它使用了PdfPig库对于DOCX使用了DocumentFormat.OpenXml。这些库将二进制文件内容转换为纯文本。这里的一个实操心得是PDF解析的质量千差万别特别是对于扫描版PDF或复杂排版的文档PdfPig可能无法完美提取。在生产环境中你可能需要评估更强大的商业OCR库如Azure Form Recognizer或开源方案如Tesseract结合图像预处理以确保文本提取的准确性这是后续所有步骤的基础。第二步文本分块Chunking这是RAG系统中的关键预处理步骤直接影响检索质量。项目在TextChunkers/文件夹下提供了分块工具。它没有采用简单的固定长度分割而是实现了基于标记Token的智能分块。它使用Microsoft.ML.Tokenizers库根据指定的模型如gpt-4来计算文本的Token数量。它会尝试在句子边界句号、问号等处进行分割尽可能保证一个块是一个语义完整的段落。它设置了块大小ChunkSize和块重叠ChunkOverlap参数。重叠部分是为了避免一个完整的句子或概念被生硬地切分到两个块中导致检索时丢失关键上下文。提示ChunkSize的设置需要权衡。太小如200 token可能无法提供足够的上下文给LLM太大如1000 token可能引入无关噪声降低检索精度并且增加嵌入和提示词的成本。通常对于事实性问答256-512 token是个不错的起点。重叠部分一般设置为块大小的10%-20%。第三步生成嵌入Embedding并存储对于每个文本块调用Azure OpenAI的嵌入模型API如text-embedding-3-small将其转换为一个高维向量例如1536维。然后通过EF Core将Document、Chunk和Embedding实体及其关系在一个事务中保存到Azure SQL Database。这里VECTOR类型的列就用来存储这个浮点数数组。代码示例向量相似度查询当用户提问时系统会先对问题本身生成一个嵌入向量然后在数据库中进行相似度搜索。核心的查询逻辑可能类似以下EF Core与原始SQL结合的方式项目中的具体实现可能封装在Repository中// 1. 首先获取用户问题的向量 var questionVector await _embeddingService.GenerateEmbeddingAsync(questionText); // 2. 使用EF Core FromSqlRaw执行向量相似度搜索 var relevantChunks await _dbContext.Embeddings .FromSqlRaw( SELECT TOP (TopK) e.*, DOT_PRODUCT(e.Vector, {0}) AS SimilarityScore FROM Embeddings e ORDER BY DOT_PRODUCT(e.Vector, {0}) DESC, questionVector) // 使用内积计算相似度值越大越相似 .AsNoTracking() .Include(e e.Chunk) // 关联获取文本块 .ThenInclude(c c.Document) // 关联获取源文档 .ToListAsync();这里使用了DOT_PRODUCT函数。在数学上对于已经归一化的向量Azure OpenAI的嵌入向量默认是归一化的内积的结果等同于余弦相似度。TOP (TopK)子句用于限制返回最相似的K个结果这就是RAG中常见的“Top-K检索”。3.2 对话与问答引擎Semantic Kernel的实战应用问答是应用的核心功能由Services/QuestionService等处理。其流程是一个标准的RAG流程接收问题来自Web前端或API。可选问题重述为了提高检索质量系统会先使用LLM对原始问题进行优化或扩展。例如将“它怎么工作的”重述为“[产品名]是如何工作的”。这个功能在有多轮对话历史时尤其有用可以将指代不清的问题补充完整。项目通过Semantic Kernel调用GPT模型来实现这一点。生成问题向量并检索对重述后的问题生成嵌入并在Embeddings表中进行向量相似度搜索找到最相关的文本块Top-K。构建提示词Prompt将检索到的相关文本块作为“上下文”与用户的原始问题一起构造成一个给LLM的提示词。提示词模板通常类似于“基于以下上下文请回答问题。如果上下文不包含答案请说‘根据提供的信息无法回答’。上下文{context}。问题{question}”。调用LLM生成答案通过Semantic Kernel调用Azure OpenAI的聊天补全API如GPT-4传入构建好的提示词生成最终答案。记录与返回保存本次问答记录到ConversationHistory关联上使用的引用Citation即那些被检索到的文本块并将答案、引用、Token使用量等信息返回给客户端。项目的高级特性——流式响应是通过/api/ask-streaming端点实现的。它利用了Azure OpenAI API的流式返回功能并将返回的每个Token实时地通过Server-Sent Events (SSE) 或类似技术推送到前端。Blazor前端通过IAsyncEnumerable来处理这种流式数据实现打字机式的输出效果极大地提升了用户体验。3.3 配置与部署那些容易踩的坑项目的appsettings.json配置文件是运行的枢纽有几个配置项需要特别注意这也是我踩过坑的地方。{ AzureOpenAI: { Endpoint: https://your-resource.openai.azure.com/, ApiKey: your-api-key, ChatCompletion: { DeploymentName: your-gpt4-deployment, // Azure门户中部署的模型名称 ModelId: gpt-4o // 用于Tokenizer计数的模型ID }, Embedding: { DeploymentName: your-embedding-deployment, ModelId: text-embedding-3-small, // 用于Tokenizer计数的模型ID Dimensions: 1536 // 嵌入向量的维度 } }, ConnectionStrings: { DefaultConnection: Servertcp:your-server.database.windows.net,1433;Databaseyour-db;... } }DeploymentNamevsModelId这是最容易混淆的一点。DeploymentName是你在Azure OpenAI Studio中为模型创建的那个部署名称可以是任意字符串如my-gpt-4。而ModelId是用于Microsoft.ML.Tokenizers库进行准确Token计数的标准模型标识符。例如即使你的部署名叫my-special-gpt只要底层是GPT-4o模型ModelId就必须设为gpt-4o。如果设置错误Token计数会不准影响分块和成本监控。Dimensions参数对于text-embedding-3-small和text-embedding-3-large这类支持维度缩短的模型你必须在此指定输出的向量维度。这个值必须与你在Azure SQL Database中定义的VECTOR列的维度完全一致例如如果你在数据库中将Vector字段定义为VECTOR(512)那么这里的Dimensions也必须设为512。否则插入数据库时会因维度不匹配而失败。数据库迁移项目使用EF Core Code First。当你首次运行或修改了数据模型比如改变了VECTOR的维度需要生成和应用迁移。注意修改VECTOR维度是一个破坏性变更可能需要手动编写迁移脚本或重新初始化数据库。Azure SQL Database版本确保你的Azure SQL数据库或SQL Server 2022实例支持VECTOR数据类型。目前这是较新版本才提供的功能。4. 实战操作从零搭建与深度使用4.1 本地开发环境搭建步骤假设你已经有Azure订阅并创建了所需的资源Azure SQL Database, Azure OpenAI服务以下是详细的本地运行步骤克隆代码与还原依赖git clone https://github.com/marcominerva/SqlDatabaseVectorSearch.git cd SqlDatabaseVectorSearch dotnet restore配置连接信息复制SqlDatabaseVectorSearch/appsettings.Development.json文件如果不存在复制appsettings.json。填写AzureOpenAI部分的Endpoint、ApiKey以及ChatCompletion和Embedding的DeploymentName。请务必根据你使用的嵌入模型正确设置Embedding:Dimensions如text-embedding-3-small常用1536text-embedding-3-large可设为1024或1536但需1998。填写ConnectionStrings:DefaultConnection指向你的Azure SQL数据库。应用数据库迁移cd SqlDatabaseVectorSearch dotnet ef database update这条命令会根据Data/Migrations下的迁移文件在数据库中创建所有表包括定义了VECTOR列的表。运行应用dotnet run应用启动后通常会监听https://localhost:5001和http://localhost:5000。用浏览器打开https://localhost:5001即可看到Blazor界面。首次使用在Web界面中进入“Documents”页面上传一个PDF或TXT文件。系统会自动在后台执行解析、分块、生成向量并存储的全流程。你可以在“Documents”列表看到处理状态。处理完成后切换到“Chat”页面就可以开始基于你上传的文档进行问答了。4.2 通过API进行集成开发对于希望将此功能集成到自己后端服务的开发者Minimal API提供了清晰的接口。导入文档curl -X POST https://localhost:5001/api/documents \ -H Content-Type: multipart/form-data \ -F file/path/to/your/document.pdf这会将文档异步处理入库。你可以通过其他API查询处理状态。进行问答非流式curl -X POST https://localhost:5001/api/ask \ -H Content-Type: application/json \ -d { conversationId: optional-existing-conversation-id, text: 请总结一下这份文档的核心观点。 }响应会包含完整的答案、引用和Token用量。进行问答流式 流式请求更适合需要实时显示答案的客户端。你需要一个能够处理服务器发送事件Server-Sent Events或类似流式响应的客户端。在JavaScript中可以使用EventSource或fetch进行读取。API端点是/api/ask-streaming请求体与/api/ask相同。客户端会收到一系列JSON对象如项目文档所述需要根据streamState字段来拼接最终的答案和元数据。4.3 性能调优与扩展思考当你的文档数量增长到数万甚至更多时就需要考虑性能优化。向量列索引Azure SQL Database支持为VECTOR列创建列存储索引以加速相似度搜索。你可以在EF Core迁移中或直接在数据库里执行SQL来创建索引CREATE CLUSTERED COLUMNSTORE INDEX IX_Embeddings_Vector ON [dbo].[Embeddings] ([Vector]);根据官方文档和数据分布选择合适的索引策略。分块策略优化这是影响检索质量最关键的环节。除了调整大小和重叠可以尝试更高级的分块方法语义分块使用嵌入模型本身或一个小型模型根据句子间的语义相似度进行动态分块而不是固定长度。层次化分块先按章节/标题大块分割再在大块内进行细粒度的句子级分割。检索时可以先检索大块再精读小块。项目当前的基于句子的分块器是一个很好的起点但对于技术文档代码片段、公式或法律合同长段落可能需要定制。检索策略优化混合搜索Hybrid Search除了向量相似度还可以结合关键词匹配如SQL中的CONTAINS全文搜索。例如先通过关键词筛选出一个候选集再在这个候选集里做向量精排。这能结合两者的优点提高召回率和准确率。重排序Re-ranking向量检索返回的Top-K结果可能不是最相关的。可以使用一个更精细但更耗时的重排序模型如Cohere的rerank模型或使用LLM本身进行相关性评分对Top-K结果重新排序再将前几名送入LLM生成答案。扩展对话与记忆项目已经实现了基础的对话历史。你可以进一步扩展实现总结式记忆在对话轮次较多时自动将历史对话总结成一段摘要作为后续问题的新上下文避免提示词过长。向量化记忆将历史对话的问答对也生成向量存入数据库这样在回答新问题时不仅能检索文档还能检索相关的历史对话实现更连贯的长期记忆。5. 常见问题与排查技巧实录在实际部署和使用过程中你可能会遇到以下典型问题。这里记录了我遇到的一些坑和解决方法。5.1 数据库与向量相关错误问题1执行迁移或插入数据时出现“VECTOR”类型相关的SQL错误。可能原因A你的Azure SQL数据库版本或SQL Server实例不支持VECTOR类型。确保你使用的是较新版本的Azure SQL Database支持此功能或SQL Server 2022。可能原因BEF Core迁移生成的SQL语句可能与你的数据库兼容性有细微差别。尝试检查生成的迁移文件中的SQL。排查与解决直接在Azure门户或SSMS中连接到你的数据库尝试执行一条简单的SQLSELECT CAST([] AS VECTOR)。如果不支持会直接报错。如果数据库支持但迁移失败可以尝试先删除所有迁移文件Migrations/文件夹和数据库然后重新运行dotnet ef migrations add InitialCreate和dotnet ef database update。问题2插入嵌入向量时报错“传入的表格数据流(TDS)远程过程调用(RPC)协议流不正确。参数 7 (“p6”): 提供的值不是数据类型 float 的有效实例。”可能原因这是最经典的问题。VECTOR列在C#端通常映射为float[]。这个错误意味着你尝试插入的数组长度与数据库中VECTOR列定义的维度不匹配。例如数据库定义是VECTOR(1536)但你提供的数组长度是1024。排查与解决双重检查appsettings.json中AzureOpenAI:Embedding:Dimensions的配置值。双重检查数据库表中Vector列的实际定义。可以通过SQL查询SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME Embeddings AND COLUMN_NAME Vector;注意对于VECTOR类型可能需要查看其他系统视图。在代码中在保存Embedding实体之前打印或记录下float[] vector数组的Length属性确认其维度。确保你使用的Azure OpenAI嵌入模型部署其输出的维度与你配置的Dimensions一致。text-embedding-ada-002固定输出1536维。text-embedding-3-small和text-embedding-3-large支持维度缩短你必须在调用API时通过dimensions参数指定并且这个指定值必须与配置的Dimensions和数据库列定义三者完全一致。5.2 OpenAI API与配置问题问题3应用启动或调用API时出现“InvalidRequestError”或“DeploymentNotFound”。可能原因Aappsettings.json中的Endpoint或ApiKey配置错误。可能原因BDeploymentName填写错误。这个名称是你在Azure OpenAI Studio中创建的部署名不是模型名如不是gpt-4而是你自定义的部署名如my-gpt-4。可能原因C你的Azure OpenAI资源所在区域与终结点不匹配或者该资源下没有创建对应的模型部署。排查与解决登录Azure门户找到你的Azure OpenAI资源。在“概览”页确认“终结点”地址复制到配置中。在“密钥和终结点”页复制一个密钥。进入“Azure OpenAI Studio”在“部署”页面查看你为GPT和嵌入模型创建的部署名称确保与配置中的DeploymentName完全一致区分大小写。问题4Token计数异常或者分块大小看起来不对。可能原因ModelId配置错误。ModelId是给Microsoft.ML.Tokenizers库用的必须使用该库支持的、标准的模型标识符。例如对于GPT-4即使你的部署名是my-gpt4ModelId也应该是gpt-4或gpt-4o。错误的ModelId会导致Tokenizer使用错误的词汇表从而计算出错误的Token数。排查与解决参考Microsoft.ML.Tokenizers的官方文档使用正确的模型ID。常见的有gpt-4o,gpt-4,gpt-3.5-turbo,text-embedding-3-small,text-embedding-3-large,text-embedding-ada-002。5.3 应用功能与使用问题问题5上传文档后一直显示“Processing”没有完成。可能原因A文档解析失败。特别是复杂的PDF文件。可能原因B调用Azure OpenAI嵌入模型API时发生错误如配额不足、网络超时但异常被吞没没有正确更新处理状态。排查与解决查看应用的控制台输出或日志文件如果配置了寻找错误堆栈信息。尝试上传一个简单的.txt文本文件进行测试排除文档解析问题。在Azure门户中检查Azure OpenAI服务的“用量与配额”页面确认没有超过速率限制或配额。在代码的DocumentProcessingService或类似服务中添加更详细的异常捕获和日志记录特别是在调用嵌入API的部分。问题6问答时返回的答案质量不高经常是“根据提供的信息无法回答”或胡言乱语。可能原因A检索到的文本块Top-K不相关。这可能是因为分块策略不合适块太大或太小或者嵌入模型对特定领域文本的表示能力不足。可能原因B提示词Prompt模板设计不佳没有给LLM清晰的指令。可能原因C检索数量TopK设置过小可能漏掉了相关文档。排查与解决检查检索结果在问答时在服务端日志或调试中打印出被检索到的文本块内容。看看它们是否真的与问题相关。调整分块参数尝试减小ChunkSize如从512降到256或增加ChunkOverlap如从20%增加到30%。优化提示词项目的提示词模板在Services/中可能定义。尝试修改它使其更符合你的需求。例如明确要求“严格基于上下文回答”、“如果上下文不足请明确说明”等。增加TopK尝试在检索时返回更多候选块例如从5个增加到10个给LLM更多上下文。考虑混合搜索如果文档中有很多专有名词或关键词尝试在向量搜索基础上增加基于关键词的全文检索作为初步过滤。问题7流式响应Streaming在前端不工作或者显示异常。可能原因A前端Blazor组件处理IAsyncEnumerable数据流的方式有误或者网络代理/防火墙干扰了长连接。可能原因B后端/api/ask-streaming端点返回的SSE格式不正确。排查与解决使用Postman或curl直接调用/api/ask-streaming端点观察原始的HTTP响应流。看看是否是一行一行返回的JSON数据。检查浏览器开发者工具的网络选项卡查看对streaming端点的请求确认响应类型是否为text/event-stream。参考项目中的Blazor组件代码可能在Components/或Pages/下确保它使用了正确的方式订阅和处理流式响应例如使用await foreach循环来读取IAsyncEnumerablestring。这个项目作为一个高质量的生产力示例将RAG的核心流程与Azure SQL Database的向量能力紧密结合为.NET开发者提供了一个极佳的起点。它验证了在现有关系型数据库上构建智能搜索应用的可行性让你在拥抱AI能力的同时不必彻底重构你的数据架构。在实际使用中从配置细节到性能调优每一步都需要结合具体业务场景进行思考和调整。希望这份详细的拆解和问题实录能帮助你更顺利地将它用起来并构建出更强大的智能应用。