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": "Server=tcp:your-server.database.windows.net,1433;Database=your-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常用1536,text-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生成答案。
- 混合搜索(Hybrid Search):除了向量相似度,还可以结合关键词匹配(如SQL中的
扩展对话与记忆:项目已经实现了基础的对话历史。你可以进一步扩展,实现:
- 总结式记忆:在对话轮次较多时,自动将历史对话总结成一段摘要,作为后续问题的新上下文,避免提示词过长。
- 向量化记忆:将历史对话的问答对也生成向量存入数据库,这样在回答新问题时,不仅能检索文档,还能检索相关的历史对话,实现更连贯的长期记忆。
5. 常见问题与排查技巧实录
在实际部署和使用过程中,你可能会遇到以下典型问题。这里记录了我遇到的一些坑和解决方法。
5.1 数据库与向量相关错误
问题1:执行迁移或插入数据时,出现“VECTOR”类型相关的SQL错误。
- 可能原因A:你的Azure SQL数据库版本或SQL Server实例不支持
VECTOR类型。确保你使用的是较新版本的Azure SQL Database(支持此功能)或SQL Server 2022+。 - 可能原因B:EF Core迁移生成的SQL语句可能与你的数据库兼容性有细微差别。尝试检查生成的迁移文件中的SQL。
- 排查与解决:
- 直接在Azure门户或SSMS中连接到你的数据库,尝试执行一条简单的SQL:
SELECT CAST('[]' AS VECTOR)。如果不支持,会直接报错。 - 如果数据库支持,但迁移失败,可以尝试先删除所有迁移文件(
Migrations/文件夹)和数据库,然后重新运行dotnet ef migrations add InitialCreate和dotnet ef database update。
- 直接在Azure门户或SSMS中连接到你的数据库,尝试执行一条简单的SQL:
问题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”。
- 可能原因A:
appsettings.json中的Endpoint或ApiKey配置错误。 - 可能原因B:
DeploymentName填写错误。这个名称是你在Azure OpenAI Studio中创建的部署名,不是模型名(如不是gpt-4,而是你自定义的部署名如my-gpt-4)。 - 可能原因C:你的Azure OpenAI资源所在区域与终结点不匹配,或者该资源下没有创建对应的模型部署。
- 排查与解决:
- 登录Azure门户,找到你的Azure OpenAI资源。
- 在“概览”页,确认“终结点”地址,复制到配置中。
- 在“密钥和终结点”页,复制一个密钥。
- 进入“Azure OpenAI Studio”,在“部署”页面,查看你为GPT和嵌入模型创建的部署名称,确保与配置中的
DeploymentName完全一致(区分大小写)。
问题4:Token计数异常,或者分块大小看起来不对。
- 可能原因:
ModelId配置错误。ModelId是给Microsoft.ML.Tokenizers库用的,必须使用该库支持的、标准的模型标识符。例如,对于GPT-4,即使你的部署名是my-gpt4,ModelId也应该是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循环来读取IAsyncEnumerable<string>。
- 使用Postman或curl直接调用
这个项目作为一个高质量的生产力示例,将RAG的核心流程与Azure SQL Database的向量能力紧密结合,为.NET开发者提供了一个极佳的起点。它验证了在现有关系型数据库上构建智能搜索应用的可行性,让你在拥抱AI能力的同时,不必彻底重构你的数据架构。在实际使用中,从配置细节到性能调优,每一步都需要结合具体业务场景进行思考和调整。希望这份详细的拆解和问题实录,能帮助你更顺利地将它用起来,并构建出更强大的智能应用。