上个月我给团队做了一次 RAG 专题分享准备材料的时候发现一个很尴尬的现实网上讲 RAG 的资料不少但要么是 Python 技术栈的要么只讲到“检索-增强-生成”三个词就没了下文。真正能把“向量是什么”这种基础问题、到 Spring AI 里的工程实现、再到评测体系串成一条完整链路的内容几乎找不到。所以我干脆自己动手整理了一份 13 图的实战讲解覆盖从“向量到底是个啥”到“RAG 评测体系怎么搭”的完整路径。今天把这份内容完整写成文字版分享给正在用 Java/Spring Boot 做知识库问答、又不想为了一个 RAG 功能单独引入 Python 服务的朋友。刚接触 RAG 但被各种名词劝退的同学也能从这里找到一条清晰的入门路线。1. RAG 到底在解决什么问题一张图看懂全链路1.1 大模型的“知识边界”和知识割裂先聊一个最朴素的问题为什么要上 RAG因为大模型有天然的知识边界。模型训练时的数据有截止时间通用知识覆盖得再广也覆盖不到你公司内部的操作手册、售后话术、排班规则这些私有内容。更麻烦的是这些内容不是一成不变的今天门店新增了一条规定明天某个 SKU 改了规格模型本身是完全不知道的。这就是“知识割裂”。我之前帮一个餐饮 SaaS 客户做过需求梳理。他们的门店 SOP 文档有几百篇从食材腌制时间到设备报修流程全在里面。客服每天要回答大量“这个东西坏了我怎么处理”“这个菜要腌多久”之类的问题而这些答案全部散落在不同的文档和表格里。知识割裂带来的直接后果就是业务方觉得“我们明明有文档为什么 AI 答不上来”用户觉得“这个助手就是个摆设”。RAG 的思路就是把这层缝合上先在大模型之外建立一个可以被检索的知识库用户提问时系统把问题转换成向量去库里面找最相关的内容片段再把找到的片段连同一个约束性 Prompt 一起丢给大模型让模型“看着资料回答”。图 1 展示的就是这条全链路我把整条链路拆成了四个环节文档接入与解析、向量化与存储、检索召回、增强生成。后面所有内容都围绕这张图展开。很多人把 RAG 想得很玄其实本质就是“先查资料再写答案”跟我们平时写周报之前先翻聊天记录是一个道理。1.2 为什么选择 Spring AI 而不是 LangChain 或 LangGraph4j这是被问得最多的选型问题。我从来不觉得 LangChain 不好它生态成熟、例子多团队能用 Python 的话完全没问题。问题在于很多业务系统本身就是 Java 写的客服接口、工单流程、权限体系全部跑在 Spring Boot 上。为了一个 RAG 功能再单独维护一个 Python 服务意味着要多部署一个进程、多维护一套代码、多考虑跨服务调用的可靠性对一个小团队来说成本不低。Spring AI 的价值在于它把大模型接入这件事做成了标准化的 Spring Boot 风格配置写在 application.yml 里Bean 由容器管理VectorStore、EmbeddingModel、ChatModel 这些核心组件都有对应的抽象。图 2 是我当时整理的选型对比LangChain 胜在生态LangChain4j 胜在 JVM 原生但没有那么“Spring 化”GraphRAG 和 LangGraph4j 还在快速演进期生产环境案例相对少。而 Spring AI 最大的优势是“原生化”它让 Java 团队可以沿着熟悉的路子快速落地这也符合它的定位——不是要做最前沿的实验框架而是要让绝大部分常规业务场景有稳定可靠的解法。如果你所在团队恰好还有 Spring Cloud Alibaba 的基础Spring AI Alibaba 社区版也值得关注它对国内模型生态的适配更友好。1.3 这套内容我是怎么组织成 13 张图的这份 13 图的框架我是按“地基到屋顶”的顺序排的。图 1 是总览图 2 是选型图 3 到图 5 解决“向量是什么”的认知问题包括坐标空间、相似度计算、数据库选型图 6 到图 9 覆盖文档接入和向量化也就是 RAG 的地基图 10 和图 11 是我实际项目里的工程结构和问答链路给想要“抄作业”的人直接参考图 12 展示的是优化后的召回链路图 13 是评测体系的全景。这个组织方式不是我拍脑袋定的而是根据团队里新同学的上手路径设计的先把概念弄明白再看完整流程最后学评测调优。后面每一部分都会对应具体图号和关键结论。2. 先搞清楚向量点积、余弦相似度与向量数据库2.1 向量是什么把文字变成坐标在聊 RAG 的检索逻辑之前必须把向量这件事讲透。向量说白了就是一组数字在数学里它是带方向和大小的量在程序里它就是一个浮点数数组。比如一部电影可以用三个数字描述动作浓度、爱情浓度、文艺浓度。动作片是[0.9, 0.1, 0.1]爱情片是[0.2, 0.8, 0.3]。这三个数字放在三维坐标系里就是一个点这就形成了图 3 的坐标空间。高维向量同理只是数字从 3 个变成了几百上千个比如 OpenAI 的 text-embedding-3-small 输出 1536 维本地常用的 bge-m3 输出 1024 维。把文字转换成向量的过程叫 Embedding嵌入模型读进一句话吐出一个向量。这个向量最神奇的性质是语义相近的句子对应向量在空间里的位置也相近。“今天天气不错”和“今天阳光很好”这两个句子向量方向会很接近而“今天天气不错”和“请把报表发我”就差得很远。RAG 的检索为什么会快就是因为它不是像数据库 LIKE 那样做字符串匹配而是在向量空间里做“距离”搜索。这也是为什么很多人说向量数据库是 RAG 的核心底座——没有向量就没有语义检索。2.2 相似度怎么算点积、余弦、欧氏距离的取舍有了向量之后检索的本质就是算相似度。最常见的三种度量是余弦相似度、点积、欧氏距离。余弦相似度计算的是两个向量在方向上的夹角余弦值公式是cos(θ) (A·B) / (||A|| × ||B||)。它只关心方向不关心向量模长结果范围在 -1 到 1 之间越接近 1 表示越相似。这是文本向量检索里最常用的度量因为嵌入模型本身对文本长度不敏感两句话一个长一个短但只要语义一致方向就一致。点积是先把向量对应位置相乘再求和它同时受到方向和模长的影响。如果向量已经做了单位化L2 范数归一化点积和余弦结果完全等价。你看到很多数据库只用点积是因为他们已经在写入时做了归一化计算更快。欧氏距离则是算空间里的直线距离适合图像、坐标这类场景文本语义上它不如余弦稳定。在 Spring AI 里相似度计算不需要自己实现CosineSimilarity工具类可以直接用或者直接在 VectorStore 的相似度实现里切换。我见过有同学对“向量的叉乘”好奇其实叉乘在普通 RAG 里几乎用不到它主要在三维几何和物理引擎里发挥作用别被这个词吓到。“向量范数”倒是很实用归一化就用 L2 范数不用深究理解成“把向量长度变成 1”就行。图 4 我画了一个简化示例两句话分别映射成[1, 2]和[2, 4]手动计算余弦相似度结果是 1.0完全相似而欧氏距离反而很大。这个对比直观说明了一个结论向量长度不同时一定要选对度量方式不能看距离就觉得不相似。2.3 向量数据库选型从内存到分布式RAG 里“库”的选择直接决定了架构复杂度。Spring AI 默认可用的向量库很多我整理了一张实用的选型表方案部署复杂度数据量建议持久化Java 生态适配SimpleVectorStore极低纯内存十万级以下不持久化可手动序列化Spring AI 原生支持Chroma低百万级以下本地文件REST API 接入PGVector中百万级以下PostgreSQL 原生依赖 pgvector 插件Milvus高组件多亿级独立集群官方 Java SDKQdrant中亿级独立服务REST/gRPC官方 SDK图 5 是一棵选型决策树。我的建议非常务实如果项目是内部工具、文档量在十万片以下、不想额外运维先用SimpleVectorStore把链路跑通别一上来就上分布式。如果公司本来就有 PostgreSQL且文档量到了百万级别直接上 PGVector少一个组件少一堆事。只有当你确实遇到单机内存扛不住、需要水平扩展和高可用时再考虑 Milvus 或 Qdrant。很多团队的错误是在数据量还没起来时就上了重型组件结果运维成本比 RAG 本身还高。3. 文档处理与 EmbeddingRAG 地基怎么打3.1 文档解析的坑PDF、Word 和扫描件RAG 项目里最不受重视却最容易翻车的环节就是文档解析。很多新人直接把 PDF 丢给解析器然后在向量库里一存检索效果却一塌糊涂。原因很简单解析器读不出来或者读乱了。PDF 里的表格、多栏排版、页眉页脚解析完经常变成乱七八糟的文本检索时自然匹配不到正确内容。图 6 展示的是一个典型的前后对比左侧是原文档里的一张“食材腌制时间表”右侧是常见 PDF 解析器直接输出的结果表格行列完全错位数字和单位串位。解决办法是分类型处理纯文本 PDF 和 Word 文档用现成的解析器就行Spring AI 里提供了一堆 DocumentReader像ParagraphPdfDocumentReader、PagePdfDocumentReader可以按段落或按页拆分但扫描版 PDF 本质上是图片必须走 OCR中文场景推荐 PaddleOCR 或者接入云厂商的 OCR 服务。对于餐饮门店这种场景我更推荐先让客户提供 Markdown 或 HTML 版本的操作手册结构化程度高解析成本最低检索效果最好。经验是文档解析阶段花的功夫会在后面评测阶段十倍赚回来。3.2 分块策略chunk 大小和重叠窗口怎么定文档解析完是一篇长文会直接扔给向量模型吗不会。因为嵌入模型有最大输入长度限制而且整篇长文档的向量化会丢失局部语义。所以要先做分块也就是把长文档切成一段段小文本。图 7 画的是分块原理按固定 token 数切同时让相邻块之间有重叠区域这样处于边界上的语义就不会被硬生生切断。这里给出我试出来的实用经验token 数不是字符数按 300 到 500 切重叠窗口取 50 到 100。为什么是这个范围太小了语义不完整比如一段话才 100 token 可能只包含半句话太大了块之间重复内容多检索时容易返回多个高度重叠的块浪费上下文窗口。Spring AI 里可以用TokenTextSplitter配置defaultChunkSize和minChunkSizeChars底层会尽量保持句子完整。当然具体最优值跟你的文档语言和行业有关系中文和英文的切分习惯就不一样不要迷信网上任何一个固定数字后面评测章节我会讲怎么用数据来确定。3.3 嵌入模型选型云端与本地怎么选嵌入模型负责把文本变成向量这一步的选型经常被低估。很多人随便选一个模型结果检索精度异常差。图 8 是嵌入模型的加载和调用流程应用启动时要校验模型是否可用写入文档时逐块调用嵌入接口拿到向量查询时同样对用户问题做一次嵌入然后丢给向量库做相似度搜索。嵌入模型分两类。云端模型像 OpenAI 的 text-embedding-3-small、智谱的 embedding-2效果稳定、不用维护机器但代价是文档内容要出网。在很多企业里这行不通客户合同、内部 SOP 都是敏感数据。本地嵌入模型这几年发展很快bge-m3和nomic-embed-text都是不错的选择。特别是用 Ollama 跑本地模型数据完全不出内网没有 token 费用部署一套知识库几乎是零边际成本。缺点是需要一台还行的小机器显存或内存不够的话嵌入速度会拖慢索引构建。我个人的建议是走本地嵌入做数据隔离聊天模型可以按需接入云端或本地如果条件允许中文业务优先上 bge-m3英文内容 nomic-embed-text 性价比很高。3.4 别只存文本元数据设计很容易被忽视向量库里存的不应该只有切好的文本和向量还必须有元数据。元数据就是文档的“属性”比如来源文件名、章节编号、更新日期、部门、权限级别。图 9 是一个字段示例一条门店设备维修记录除了正文还带source设备手册.md、chapter第三章_报修流程、updatedAt2025-01-12、region华东区。为什么元数据设计很重要因为它能帮你做过滤。比如客户问“华东区的报修电话是多少”如果库里混着全国各区的文档向量检索可能把华南区的电话也拉出来。有了元数据就可以在检索条件里显式传一个过滤表达式只搜索region华东区的块。Spring AI 的 VectorStore 查询接口支持FilterExpression实际项目里我几乎每次都加上元数据过滤它提升命中率的贡献和换一个更好的嵌入模型差不多。另外一个容易被忽视的点是展示答案时把元数据里的来源文件传给前端用户才敢信 AI 的回答这个后面实战部分会展示。4. Spring AI 实战从零搭一个可用的本地 RAG 知识库4.1 工程结构与依赖到了大家最想看的代码部分。先说清楚我这套是基于 Spring AI 当前较新的 API 写的具体包名可能随版本变化但整体思路是不变的。图 10 画的工程结构很简单就是一个标准 Spring Boot 项目加两个核心包一个是模型接入一个是向量库。pom.xml 里加依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-ollama/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-vector-store-simple/artifactId /dependency然后是 application.ymlspring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:7b embedding: options: model: bge-m3 vectorstore: simple: initialized: true这里我用了本地 Ollama 做演示配置里同时指定了聊天模型和嵌入模型。SimpleVectorStore默认是内存模式适合开发和演示。如果你要接 PGVector把配置换成对应的 URL、用户名、密码和schema-name就行Spring AI 的抽象层会帮你省掉不少切换成本。4.2 文档装载与写入向量库RAG 的第一步是把知识文档读进来、切好、算出向量、写入向量库。代码逻辑是固定的Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { return new SimpleVectorStore(embeddingModel); } Test public void ingestDocuments() { // 1. 读取文档 var reader new ParagraphPdfDocumentReader( new FileSystemResource(/data/store_sop.pdf) ); ListDocument documents reader.get(); // 2. 分块 var splitter new TokenTextSplitter.Builder() .withDefaultChunkSize(400) .withMinChunkSizeChars(100) .build(); ListDocument chunks splitter.apply(documents); // 3. 为每个 chunk 补充元数据 chunks.forEach(doc - { doc.getMetadata().put(source, store_sop.pdf); doc.getMetadata().put(region, east); }); // 4. 写入向量库底层会调用 embeddingModel 计算向量 vectorStore.add(chunks); }这段代码里最容易踩坑的是第 4 步。vectorStore.add(chunks)会同步调用嵌入模型如果你用本地 Ollama 而且文档很多这一步可能非常慢看起来像卡死。实际写法建议加一个批处理策略比如每次 add 100 个块后台批量执行。另外SimpleVectorStore是纯内存的服务重启后数据就没了。所以要么在应用启动时重新加载原始文档要么用SimpleVectorStore的序列化方法把库持久化到文件比如每秒或每次写入后保存一下。4.3 问答链路查询嵌入 TopK 检索 Prompt 增强文档入库之后问答链路就变得直观起来。图 11 画的是一次完整问答的时序用户提问 - 问题文本用同一个嵌入模型转成向量 - 在向量库里做相似度搜索取 TopK 个块 - 把块内容拼进一个内置提示词 - 发给大模型 - 流式返回答案。代码核心是这样Service public class RagService { private final VectorStore vectorStore; private final ChatClient chatClient; public String ask(String question) { // 1. 向量检索 ListDocument docs vectorStore.similaritySearch( SearchRequest.builder(question) .withTopK(5) .withSimilarityThreshold(0.3) .build() ); // 2. 拼上下文 String context docs.stream() .map(Document::getText) .collect(Collectors.joining(\n\n)); // 3. 构造 prompt String prompt 你是一个客服助手只能根据参考资料回答。 参考资料 %s 问题%s 如果参考资料中没有相关信息直接回复“资料库中没有找到相关内容”。 在回答末尾标注参考来源。 .formatted(context, question); return chatClient.prompt().user(prompt).call().content(); } }这里有两个关键参数要解释。topK表示取最相似的几个块取 3 到 5 是比较平衡的。similarityThreshold是相似度阈值低于这个值的块宁可不用防止模型拿无关内容硬编。这两个参数直接影响答案质量我在评测章节会给出调参方法。需要特别强调的是千万不要把整个知识库的内容全部塞进上下文大模型的上下文窗口再大也不如精准的 TopK 有效。每轮问答只喂最相关的几个块这才是 RAG 的初衷。4.4 流式输出与引用溯源生产环境的问答接口不会等大模型全部生成完才返回用户等不了那么久。Spring AI 的ChatClient支持流式调用public FluxString streamAsk(String question) { // 检索逻辑同上 return chatClient.prompt().user(prompt).stream().content(); }前端配合 SSE 协议就能实现打字机效果。这部分不难真正的坑在“引用溯源”。我强烈建议你在 RAG 的答案里带上参考来源。做法是把检索命中的块对应的元数据source、chapter拼到最终答案后面或者在流式返回的同时单独返回一个sources字段。图 12 是我在管理后台做的一个效果示意每条回答下方列出“参考了哪份文档的哪个章节”用户点击可以直接跳转原文。这看着是个小功能但对 RAG 系统来说非常重要——有了来源用户才愿意相信答案出了错误排查时也能直接定位到是哪份文档哪个块导致的问题。别省这一步。5. 检索质量优化多路召回、Rerank 与查询改写5.1 为什么 TopK 检索不够基础链路跑通之后你就会撞上一堵真实的墙检索命中率不够。问法稍微换一下向量检索就捞不到正确内容。比如门店手册里写的是“鸡肉腌制时间 20 分钟”用户问的是“腌鸡腿需要多久”语义上相关但嵌入模型不一定能精确对应上“腌制”“腌”“20分钟”这些词。问题出在哪向量检索擅长捕捉整体语义相似但不擅长精细的关键词匹配和同义改写。而且基础链路只有一个检索通道问题一次嵌入一次搜索TopK 结果就是全部。如果这个“一次”跑偏了后面再怎么增强生成都白搭。所以优化检索质量是 RAG 落地绕不开的一步。5.2 查询改写和多路召回我的第一个优化手段是查询改写。让大模型先对用户问题做一次“翻译”把口语化的问题转成更适合检索的 query比如“腌鸡腿需要多久”改写成“鸡肉腌制时长 腌制时间”有时候还会拆出几个同义检索词。这步在 Spring AI 里实现很简单就是一次额外的 LLM 调用。代价是多一次延迟但通常在 300 毫秒内换来的是检索精度提升。第二个手段是多路召回。不要只走向量一条路同时走两条路一路还是向量检索另一路走传统的关键词匹配比如数据库里的全文索引然后把两路结果合并去重再统一打分排序。图 12 展示的优化后的召回链路就是这样的查询改写 - 并行召回 - 合并打分 - 重排 - TopK 输出。这么做的好处是互补向量擅长语义泛化关键词擅长精确匹配合在一起能显著降低漏召回率。代价是实现复杂度上去了但换来的是更稳的效果。5.3 用 Rerank 给召回结果“二次重排”召回阶段捡回来的候选块可能有 20 个但上下文窗口只允许放 5 个选哪 5 个这就是重排要解决的问题。简单按向量相似度排序虽然能交差但不够好因为第一阶段的向量相似度不一定是最终答案需要的相关性。更专业的做法是引入一个 Rerank 模型用交叉编码器Cross-Encoder对“用户问题 候选块”做精细化相关性打分。中文场景推荐 bge-reranker 或 m3-reranker。Java 后端不需要直接跑这些模型通常的做法是在本地起一个小型的模型推理服务比如基于 Ollama 或 transformers 封装一个 HTTP 接口Spring AI 里通过一个Retriever或者简单的 RestClient 去调用。你也可以先用一个轻量方案召回后对候选块做一次基于关键词重合度的二次打分和向量相似度加权融合。这个方案虽然没有真正的 Rerank 强但实现成本低对于文档量不大的内部知识库够用了。给个实际经验加入 Rerank 之后我的一个项目 hit rate 从 61% 提到了 74%提升非常明显。如果你的检索精度已经卡在一个瓶颈优先上 Rerank。5.4 从 Naive RAG 到 GraphRAG 与 Agentic RAG再往前一步就是现在热词里常见的 GraphRAG、Ontology RAG、Agentic RAG。Naive RAG最朴素的切分-向量-检索-生成的优点是好理解、好实现但缺陷也明显所有文档被切成互相独立的块块和块之间的关联关系丢了。比如一份手册的前言定义了“报修”流程后面各章节分别讲各类设备的报修细节Naive RAG 很难把这些散落的知识组织成体系化答案。GraphRAG 和 Ontology RAG 的思路是引入知识图谱把实体和关系抽出来建图检索时不仅检索文本块还能顺着图结构找到关联信息。这个方向我很看好但实话说复杂度不低。Agentic RAG 则是在 RAG 之上加了智能体逻辑让系统能自主决定什么时候调工具、什么时候检索、什么时候追问Spring AI 的ChatClient也支持 Tool Calling可以做工具调用。我的建议是如果你的知识库文档之间关联性极强、提问普遍需要跨文档推理再考虑图谱化路线如果现在 Naive RAG 已经能覆盖大多数问题先把评测和调参做扎实别为了追逐热词把系统复杂度拉满。6. 评测体系别再用“感觉还行”糊弄自己6.1 为什么要构建测试集RAG 项目做得多了之后我最大的体会是很多团队把系统跑起来就觉得“应该行了吧”然后让业务同事随便问两个问题答对了就上线。这种“感觉还行”的验收方式在文档量小的时候还能蒙混过关一旦知识库膨胀到几百上千个文档问题会暴露得让人措手不及。所以从第一天起就要建一个评测集。评测集长什么样一句话一组“问题-标准答案-参考文档”三元组。比如{question: 腌鸡腿需要多久, reference_answer: ..., source_doc: store_sop.pdf 第 3 章}。数量不用太多刚开始 50 到 100 条就足够发现问题了后面再扩充。构建来源有两个一是客服后台历史工单里的真实问题这个质量最高二是让业务人员翻着文档模拟提问。图 13 是我画的评测体系全景它包含检索侧指标和生成侧指标两部分分别考核“捞出没捞出”和“回答好不好”。6.2 核心指标Hit Rate、MRR、忠实度、相关性评测指标分为两个层面。检索侧指标考核“该捞的文档捞到没有”。最常用的是 Hit Rate命中率含义是“多少比例的问题在 TopK 结果里包含标准答案所在的文档”。还有 MRR平均倒数排名它比 Hit Rate 更严格不仅要求命中还要求命中的文档排得靠前计算公式是MRR 平均(1/命中位置)。假如一个问题标准文档排在第 3 位那它的倒数排名就是 1/3。生成侧指标考核“模型答案质量”。一个叫忠实度衡量答案里的关键论点是否都有参考资料支撑说直白点就是“有没有胡编”另一个叫答案相关性衡量回答和问题是否对得上有没有跑题。这两类指标在 RAGAS 这类框架里有成熟实现但如果你用的是 Java 技术栈完全可以自己写一个轻量评测脚本。图 13 里的仪表盘展示了两层指标同时看的效果个别情况下检索已经命中了但生成答案仍然不忠实这两个问题要分开排查。下表是我常用的指标速查指标衡量内容计算方式使用场景Hit RateK检索是否捞到目标文档命中数/总问题数调 chunk、调 TopKMRR目标文档排得靠不靠前平均(1/排名)对比召回和重排策略忠实度答案是否忠于参考材料人工/LLM 打分检查 Prompt 和上下文答案相关性答案是否切题人工/LLM 打分检查整体链路6.3 快速落地一个 Java 评测脚本不需要把评测做得太重先写一个能跑的脚本把结果打到表格里就够了。核心逻辑如下Test public void evaluateHitRate() { // 测试集中每条记录包含 question 和 expectedDocId ListTestCase cases loadTestCases(); int hitCount 0; double reciprocalSum 0; for (TestCase tc : cases) { ListDocument hits vectorStore.similaritySearch( SearchRequest.builder(tc.question()).withTopK(5).build() ); for (int i 0; i hits.size(); i) { if (hits.get(i).getMetadata().get(docId).equals(tc.expectedDocId())) { hitCount; reciprocalSum 1.0 / (i 1); break; } } } double hitRate (double) hitCount / cases.size(); double mrr reciprocalSum / cases.size(); System.out.printf(HitRate5%.2f, MRR%.2f%n, hitRate, mrr); }这就是一个可以反复跑的“回归测试”。注意两点一是测试集最好固定不要今天改一题明天删一题二是每次调参只改一个变量比如只改 chunk 大小或者只改 TopK否则你永远不知道是哪个改动让分数涨了。6.4 评测结果怎么指导调参评测不是用来发报告的是用来指导决策的。我分享一个真实调参案例初始配置 chunk 大小 200、TopK 3HitRate3 只有 51%。把 chunk 调到 400 后HitRate3 变成 58%因为块变大了单个块包含完整语义的概率提高再调 TopK 到 5HitRate5 变成 66%说明相关资料确实在库里只是排太靠后随后我加了 RerankHitRate5 到 74%最后通过元数据过滤把无关分区的文档排除掉HitRate5 稳定在 81%。每一步改动都有数据支撑而不是“我觉得这样更好”。这套流程已经成为我做所有 RAG 项目的标准动作。7. 实战中踩过的坑与排查技巧7.1 常见问题速查表下面这张表是我在多个项目里最常遇到的 RAG 问题、原因和解决办法直接抄走用症状常见原因排查方法解决方案检索结果为空Ollama 服务没启动、嵌入模型没加载检查服务状态curl 一下 /api/embeddings启动服务拉取模型答案明显胡编TopK 太小没捞到资料或 Prompt 没约束打印检索到的块内容看相关性调大 TopK强化 Prompt 约束答案正确但格式差没有给模型输出格式模板检查 Prompt 中的格式规范在 System Prompt 里写清楚输出结构检索命中但答案不好chunk 太大或太小、上下文拼得乱手动查看 TopK 块调整 chunk size 与 overlap首次响应特别慢本地模型加载慢、embedding 每次单独调用查看日志耗时分布预热模型、批量嵌入、考虑流式输出多文档混杂导致回答不一致没有元数据过滤检查 metadata 是否完整检索时加过滤条件服务重启后库空了SimpleVectorStore 纯内存确认是否持久化实现启动加载或持久化逻辑7.2 Ollama 安装向量模型后到底怎么用热词里有人问“ollama 安装了向量模型后如何使用”这个值得单独说。步骤很简单先在本地确认模型已经拉取比如ollama list能看到bge-m3。然后手动调一次接口验证curl http://localhost:11434/api/embeddings -d {model: bge-m3, prompt: 测试语义}正常会返回一个向量数组看维度对不对bge-m3 是 1024 维。然后在 Spring AI 配置里把 embedding 模型指定为bge-m3剩下的事情框架帮你做了。新手最容易犯的错是只配置了 chat model没配置 embedding model导致文档写入时其实在用一个默认模型或者根本没有可用的嵌入模型程序报错半天找不到原因。所以任何 RAG 项目的第一个自测动作永远是跑一次文档入库再跑一次查询确认两个环节的 embedding 用的是同一个模型。如果 chat 用云 API、embedding 用本地 Ollama完全没问题只要各配各的就行。7.3 一些零散但值钱的工程细节有三个细节不在框架文档里但实战中很值钱。第一个是文档更新问题。知识库内容不可能永远不变你得有一个更新策略。我常用的做法是给文档块加一个updatedAt元数据每次更新时按source字段删除旧块再写新块而不是全量重建。否则用户问几个月前的旧问题系统还在答一份已经被废弃的旧手册这种错误比答不出更伤信任。第二个是上下文窗口的管理。一个 RAG 请求的完整 prompt 是系统指令 检索到的若干块 用户问题。如果块切得太大、TopK 又取太多上下文很容易爆掉或者被截断。流式输出接口往往不会明确告诉你截断但答案会突然不完整。所以我在拼接上下文前会检查 token 数超出预算就缩减 TopK而不是硬塞。第三个是权限问题。知识库里的文档不一定所有人都能看比如区域经理和门店店员能访问的内容就不同。如果 RAG 系统不考虑权限就能通过向量检索绕开权限直接查询内容。架构上建议在检索条件里加上权限过滤标签按用户身份限定元数据范围而不是把所有文档放进同一个库里。这个设计要在一开始就考虑后面加会很痛苦。7.4 知识割裂还有救图谱化和多向量方向的探索回到最开始说的“知识割裂”Naive RAG 已经解决了一大部分但文档之间的逻辑关联仍然容易丢失。我最近在尝试的一个方向是给知识库建立简单的本体关系比如“门店-设备-报修流程-责任人”之间建立显式图谱检索时先在图谱上定位相关实体再回到向量库捞针对该实体的文本块。这个路线比直接追 GraphRAG 热点更可控你能逐步看见搜索质量提升。另一个方向是“多向量”。不是给整段文本只算一个向量而是按角度拆开给标题算一个、给正文算一个、给结论算一个检索时分别匹配再融合。这对长文档尤其有效因为长文档里只有一句话和问题相关时整块向量会被其他内容稀释。多向量会增加存储和计算成本建议在评测指标确实逼近瓶颈时再引入。如果你刚起步先把前面的基础链路和评测体系跑通已经能解决绝大多数知识割裂问题了。我个人实际操作中的体会是RAG 不是一个“搭完就完事”的功能它更像一个需要长期调优的系统工程。别急着上一次上全套炫酷组件先把一条完整链路跑通再用评测数据决定下一步往哪走。这套 13 图的框架后来又迭代过几次但核心顺序一直没变概念、地基、链路、优化、评测。按照这个顺序你已经可以避开我踩过的大部分坑早日把知识库变成一个真正“回答得靠谱”的助手。