☰
RAG实战:离线建库与在线查询双链路优化全解析
2026/10/1 23:59:30 网站建设 项目流程

我最早做 RAG 项目时,踩得最深的一个坑,就是把离线建库和在线查询当成一个整体来调。结果问答不准的时候,根本分不清问题出在文档切分上、检索参数上,还是生成模型的提示词上。后来我把 RAG 明确拆成两条链路来看——离线建库负责把文档变成可检索的资源,在线查询负责把问题变成精准的检索条件并组织答案——排查效率一下子高了很多,优化也有方向了。这篇文章就把这两条链路从头到尾讲透,适合刚开始搭 RAG 知识库、以及项目上线后检索效果不理想的朋友参考,内容全部基于我实际跑过的项目经验。

1. 为什么 RAG 必须拆成两条链路来看

1.1 一次让人抓狂的线上事故

先讲一个真实经历。当时我负责一个企业内部知识库问答系统,文档量不大,大概几百篇,但上线后用户反馈非常差:问“报销流程需要几个审批人”,系统答非所问,甚至从完全无关的文档里摘了一段出来。我一开始怀疑是生成模型的问题,换了更强的模型,结果更糟——模型开始一本正经地编造流程细节。

后来我把一次错误回答的完整链路打印出来仔细看,才发现问题根本不在生成端,而是检索端返回的上下文压根没包含正确答案。再往深处查,是离线建库时文档切分把表格内容从中间劈开了,导致关键信息缺失。那次之后我学到一个教训:RAG 的效果不是靠单一环节决定的,而是离线建库和在线查询两条链路协同的结果,出了问题必须先定位是哪条链路在背锅。

1.2 两条链路的职责边界

离线建库,也叫索引链路,做的事情是“把静态文档变成可检索的结构化资源”。它负责文档清洗、格式解析、切分、向量化、写入向量库,最终产出的是一个能让在线链路快速定位相关内容的知识库。衡量它好不好的核心指标,是“召回潜力”——也就是正确信息有没有被完整地存进去、能不能被检索到。

在线查询,也叫推理链路,做的事情是“把用户的问题变成有效的检索条件,并把检索结果组织成稳定答案”。它负责查询改写、向量检索、重排、上下文拼接、生成回答。衡量它好不好的核心指标,是 hit rate(检索命中率)、答案准确率和响应延迟。

打个比方。离线建库像是图书馆的编目和上架流程,书要是编错了类、放错了架,再厉害的图书管理员也找不到;在线查询则像是读者来查书的全过程,管理员需要对读者的模糊描述做二次确认,再决定从哪个书架找、找哪几本。这两件事可以分开优化,但不能混为一谈。

2. 离线建库:索引质量决定可检索性的上限

2.1 文档加载与清洗:别把 PDF 直接扔进去

很多人在离线建库第一步就吃了亏,因为加载文档这件事看起来太简单了。PDF 分两种,一种是文字版,可以直接抽取文本;另一种是扫描版,本质是一堆图片,不跑 OCR 抽出来的全是乱码。我在一个合同审查项目里遇到过这种情况,几十份扫描件加载后全是空白字符,检索引擎根本无法命中任何内容。

所以文档加载必须建立一个基本流程:

  • 先判断文档类型,PDF / Word / Markdown / HTML 分别走不同的解析器。
  • 扫描版 PDF 一定要先 OCR,且 OCR 要在切分之前做,不然识别错误会跟着切分被拆散,后期很难修复。
  • 表格内容优先转成“键值对 + 文本描述”的形式再入库。比如“报销金额:5000元,审批人:部门主管”,比单纯把表格图片化要好检索得多。
  • 无意义内容要去掉,比如页眉页脚、重复的版权声明、水印文字,这些噪声会污染向量表示。

一个容易忽略的点是 HTML 和 Markdown 自带的层级结构。尽量在解析时保留标题层级,后续切分可以基于标题做边界,这比按固定字数硬切效果明显好。

2.2 文档切分:chunk size 不是越小越好

切分是离线建库中最容易拍脑袋、也最影响效果的环节。切得太小,比如一个 chunk 只有几十个 token,语义不完整;切得太大,比如把整章丢进一个 chunk,超过 Embedding 模型上限会被截断,而且一个 chunk 往往混合了多个主题,检索时噪声很大。

我常用的一套参数是:chunk size 500 token,overlap 75 token。为什么要有 overlap?因为一个语义完整的概念可能正好跨在两个 chunk 的边界上,如果上下两段完全没有重叠,检索时就可能漏掉那部分信息。overlap 让相邻 chunk 有一定信息交集,能明显提高召回稳定性。

给你一个简单的估算公式:假设一篇文档有 8000 token,chunk size 是 500,overlap 是 75,那生成的 chunk 数量大概在 (8000 - 75) / (500 - 75) ≈ 19 个左右。基于这个估算,你可以粗算整库的向量总量,评估存储成本和检索耗时。

切分策略上,我强烈建议“结构切分为主、定长切分为辅”:有标题层级就按标题切,有代码块就保持代码块完整,有表格就让表格整体作为一个 chunk。实在没有结构化信息,再退回到定长切分,但一定要设置 overlap。

2.3 向量化:Embedding 选型与一致性黑洞

Embedding 模型负责把文本变成向量,这个选择直接决定了语义相似度计算的准确性。中文场景我推荐优先考虑对中文支持较好的开源模型,比如 BGE 系列(bge-m3、bge-large-zh)、m3e,也能直接用一些商用 API 的 embedding 接口。选择时要关注两件事:一是向量维度,维度越高精度潜力越大,但存储和检索成本也高;二是模型对行业术语的覆盖,垂直领域建议拿一批真实语料做小规模效果评测,而不是只看榜单分数。

这里有一个必须重点强调的坑:建库时用的 Embedding 模型和查询时用的模型必须完全一致,版本也不能乱换。向量空间是模型训练出来的,换模型等于换坐标系,旧索引里的向量和新查询向量不匹配,检索结果必然崩。我有一次就是升级了模型版本,但忘了重建索引,结果线上检索命中率直线下降,排查了很久才发现是两套向量空间不一致。

2.4 向量库与索引结构

向量数据库选型要看场景。轻量原型用 Chroma 很合适,它部署简单、API 友好;中等规模生产环境我常用 Qdrant,过滤能力丰富、性能和稳定性都不错;大规模分布式场景则可以考虑 Milvus。如果只是做离线批量检索,甚至可以基于 FAISS 自己管理索引。

索引结构上,HNSW 是默认稳的选择,检索快、召回高,但内存占用偏大;IVF 更省内存,但需要训练聚类,而且检索精度略逊于 HNSW。中小规模知识库没必要折腾,直接用 HNSW 就好。

度量方式要注意。中文场景语义相似度匹配我一般用余弦相似度(cosine),它天然适合文本向量。如果向量库默认是内积或 L2,需要确认是否等价,否则会影响排序。

2.5 元数据与父文档结构

元数据看起来不起眼,实际上比很多人想象的更重要。为每个 chunk 记录文档名、章节路径、页码、更新时间、业务线标签、权限级别,这些信息在在线查询时能发挥三个作用:

  • 过滤:很多问答场景需要限定条件,比如“只看 2024 年以后的制度文档”,如果没有元数据过滤,检索就会跨年份混答。
  • 溯源:给用户返回答案时,附上文档出处,可信度瞬间提升。
  • 权限控制:不同角色只能检索自己有权限看的内容,元数据是最简单的执行入口。

更进阶一点的做法是父文档结构(Parent-Child Retrieval)。简单说就是检索时用小 chunk 提高命中率,但生成时返回小 chunk 所属的整个父段落甚至父章节,这样可以避免小 chunk 上下文信息不足的问题。这个方案对长文档、需要上下文连贯性的场景效果非常明显。

3. 在线查询:从问题到答案的四步跳跃

3.1 查询改写:用户的问题往往不能直接用

很多初学者直接把用户 query 丢进向量检索,效果一般,原因很简单:用户的问题天然具有口语化、省略、指代模糊的特点。比如“它是怎么审批的”,如果不先搞清楚“它”指什么,检索结果肯定跑偏。

所以在线查询第一步应该是查询改写。常见手段有三种:

  • 指代消解:把“它”“这个流程”“上面说的那个”替换成真实的实体名称。
  • Multi-Query:把一个 query 扩展成 3 到 5 个不同表述,分别检索再合并且去重。比如“报销怎么走”可以扩展成“报销申请流程”“报销审批步骤”“费用报销操作指南”。
  • HyDE:让模型先假想一个“理想答案”,用这个假想答案去检索,语义往往更接近知识库中的原文表述,能提高召回。

需要注意的是,查询改写不是免费的。每次改写都增加一次模型调用,会让延迟和成本上升。我一般只在检索效果不理想、或者系统对延迟要求不苛刻时启用 Multi-Query 和 HyDE。

3.2 检索召回:向量检索和关键词检索要配合用

纯向量检索有个明显的弱点:它擅长语义匹配,但不擅长精确匹配。比如查一个错误码“ERR-2048”,嵌入模型可能把这个字符串和很多语义相近的句子混淆,这时候关键词检索反而更直接。反过来,关键词检索无法处理“换一个说法”的语义问题。

所以我推荐混合检索方案。向量检索负责语义召回,关键词检索(BM25)负责精确匹配,然后把两路结果做一个融合。最简单的融合方式是 RRF(Reciprocal Rank Fusion),把两个列表的排序名次加权求和,不需要调太多参数就能获得稳定的效果。

Top-K 的设置也要讲策略。我一般先召回 50 到 100 条候选,再经过重排取前 5 到 10 条进入生成。如果只在前 5 条里选,可能因为排序误差错失正确答案;如果直接让 50 条全部进入 Prompt,又会造成上下文污染,模型反而被无关信息带偏。

顺便说下 hit rate 这个概念。它就是“正确答案是否出现在检索结果 TopN 中”的比例。这个指标是衡量检索链路质量最直接的刻度,应该纳入每次优化的评估流程。

3.3 重排:向量检索的 TopN 不等于语义最优

向量检索用的是 Bi-Encoder 结构,query 和文档分别编码成向量,计算速度快,但精度有限。重排一步,我推荐引入 Cross-Encoder 模型,把 query 和文档原文拼接在一起做联合编码,精度更高。

典型的流程是:向量检索先召回 100 条,重排模型逐条打分,取最高的 5 条。这样兼顾了性能和精度。引入重排后,我的项目里常见问题的答案正确率能提升 8 到 15 个百分点,尤其是文档库足够大的时候,收益非常明显。

当然,重排也有代价。每一条候选都要过一遍模型,100 条候选就相当于 100 次模型推理,延迟是实打实的。轻量场景可以在 20 到 30 条候选后就重排,生产环境可以对这个打分结果做缓存,降低重复计算。

3.4 生成端:上下文拼接与拒答兜底

检索完成之后,把命中的 chunk 按重排得分顺序拼装进 Prompt,是生成前最后一步。我做这块时有几个固定要求:

  • 为每个 chunk 标记文档来源序号,Prompt 里要求模型在引用时带上标注,这样用户能看到出处,也更方便后续审计。
  • 控制拼接总量。上下文越长,响应越慢,模型越容易被无关信息干扰。我一般控制在 2000 到 3000 token 以内,不把所有候选都塞进去。
  • 设置“拒答兜底”。如果检索结果和问题的相关性都低于阈值,模型应该明确回答“知识库中没有找到相关信息”,而不是硬编造一段。这个规则必须写死在 Prompt 里,否则大模型会倾向于生成一个看起来合理的回答。

4. 从基础 RAG 到进化形态:Agentic RAG、GraphRAG 与本体 RAG

4.1 RAG 热词背后的真实问题

你可能会注意到,现在 RAG 相关的技术词非常多,比如 Agentic RAG、GraphRAG、本体 RAG。这些词听着唬人,本质上都是在解决基础 RAG 的某个具体瓶颈。

基础 RAG 的流程是一次性的:用户问一个问题,系统检索一次,生成一次回答。它的问题在于:复杂问题需要多步推理时,一次性检索往往拿不到完整的信息。比如“对比 A 和 B 两个部门过去一年的支出结构”,单次检索很难同时覆盖两套数据。

Agentic RAG 的思路是让 LLM 自己规划策略,它可以根据需要多次检索、调用外部工具、验证结果或决定转向另一个查询。就像让一个实习生去整理资料,他不会一次把所有箱子翻完,而是先看目录、再针对性找文件、检查内容是否齐全、不够再去补。这个模式适合多跳问答、复杂分析类问题,代价是 token 消耗和延迟明显上升。

GraphRAG 则是用图结构来描述文档中的实体和关系。它适合解决“知识割裂”的问题——传统 RAG 把每篇文档切成独立 chunk,彼此之间没有关系,导致跨文档综合时效果不佳。GraphRAG 建库时会抽取实体、构建关系、形成社区摘要,在做全局性问题或跨文档归纳时表现更好。

本体 RAG(Ontology RAG)更进一步:用本体定义概念、属性和关系约束,让检索范围被语义规则框定。这个方案在企业级、强约束场景下很实用,因为它能避免词义歧义,把问“苹果”的问题导向水果还是品牌,完全由本体决定。代价是人工构建本体的成本不低,所以一般用在专业领域知识库。

4.2 什么时候该升级形态

我的建议是不要为了追热点强行升级架构。先把基础 RAG 的离线建库和在线查询做扎实,再根据业务问题反推需要哪种进化形态。

如果是简单 FAQ、单篇文档问答、或者知识库规模只有几十篇,基础 RAG 足够。出现以下情况再考虑升级:

  • 用户问题经常是“多跳问题”,比如“哪个项目的负责人最近调动过”,需要多次关联查询,考虑 Agentic RAG,先拆解问题、再逐项检索。
  • 业务需要跨文档综合归纳,关注实体关系,比如“公司有哪些产品线涉及 AI 技术”,考虑 GraphRAG。
  • 强约束行业,比如法律、医疗,对术语边界要求极高,考虑本体 RAG。

升级不是替换,而是叠加。我在实际项目里通常走这样的路径:基础 RAG 跑通 → 加查询改写和重排 → 加 Agent 迭代检索 → 再考虑加图谱或本体。每一步都以可量化的命中率和准确率提升为准,避免做了架构升级但业务收益为零。

4.3 成本与工程权衡

GraphRAG 的建库成本很惊人,因为它涉及实体抽取、关系构建和图计算,离线资源消耗比普通向量库大很多。Agentic RAG 的在线成本也是成倍增长,一次复杂问答可能触发四五次模型调用。所以先进形态不是默认选项,而是要基于业务场景算清楚收益比。文档更新频率低、结构稳定、且确实需要关系推理的场景,才值得上图谱方案。

5. 工具选型与一套可复制的落地配置

5.1 框架与知识库选型

市面上 RAG 框架不少,LangChain 和 LlamaIndex 是 Python 生态里用得最多的;Java 技术栈可以关注 LangChain4j,它在 Spring Boot 项目里集成比较方便。框架的作用是帮你把加载、切分、检索、生成这些步骤串起来,但不要过度依赖框架的默认参数,很多配置需要根据你的文档特点去调。

向量库的选型,我整理了一个简易对照表:

工具定位适合场景部署成本
Chroma轻量级嵌入式原型验证、本地小规模低
Qdrant生产级专用向量库中等规模、需要复杂过滤中
Milvus分布式向量库大规模、高并发高
FAISS本地索引库离线批量检索低

强调一句,选型不是越重越好。只有几百篇文档的内部知识库,用 Chroma 甚至文件版的 FAISS 就够了,上 Milvus 纯属给自己增加运维负担。

5.2 零基础本地 RAG 快速搭建参考

如果你想在本地把一条完整的 RAG 链路跑起来,我推荐 Ollama 加一个向量库的组合。整体流程可以照这样操作:

第一步,准备本地模型运行时 Ollama,拉取一个 Embedding 模型和一个对话模型。Embedding 模型我用过 nomic-embed-text,中文场景也可以考虑 bge-m3;对话模型按你的显存选择,8B 左右级别在普通消费级显卡上就能跑。

第二步,写一个 Python 脚本完成离线建库。用文档加载器读取本地文件,按第 2 节讲的策略做清洗和切分,调用 Ollama 的 Embedding 接口生成向量,写入向量库。

第三步,写一个查询脚本完成在线链路。接收用户问题,做简单的查询改写,把问题向量化后在向量库中检索 TopK,拼接 Prompt,再调用 Ollama 的对话模型生成答案。

这里给你一段极简的 Python 示意,方便理解全流程,完整工程还需要加上错误处理和日志:

from langchain_community.llms import Ollama from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma # 离线建库:加载文档 -> 切分 -> 向量化 -> 入库 from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.document_loaders import DirectoryLoader loader = DirectoryLoader("./docs", glob="**/*.md") docs = loader.load() splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=75) chunks = splitter.split_documents(docs) embedding = OllamaEmbeddings(model="nomic-embed-text") vectorstore = Chroma.from_documents(chunks, embedding, persist_directory="./db") # 在线查询:问题 -> 检索 -> 拼接 -> 生成 query = "报销流程需要几个审批人?" retrieved = vectorstore.similarity_search(query, k=5) context = "\n".join([doc.page_content for doc in retrieved]) llm = Ollama(model="qwen2.5:7b") answer = llm.invoke(f"请仅根据以下资料回答:\n{context}\n\n问题:{query}") print(answer)

这段代码把两条链路都串起来了,很适合零基础的朋友做复现。注意,实际工程里不要直接这样裸奔,至少要把 Embedding 版本固定、向量库持久化路径管理好,再加上元数据过滤。

6. 常见问题与排查技巧实录

6.1 高频问题速查表

下面这组问题是我在项目中被问得最多、自己也踩过的,整理成速查表方便你直接对照:

现象优先排查方向处理建议
检索不到相关内容Embedding 版本是否一致、索引是否重建、chunk 是否切碎固定模型版本,重建索引,检查切分边界
答案胡编乱造检索结果质量差、没设置拒答兜底增加重排,Prompt 中强制“不知道就说不知道”
响应太慢检索候选太多、上下文拼接过长降 TopK,加结果缓存,限制上下文长度
结果重复啰嗦overhead 过大、多个相邻 chunk 都被召回减小 overlap,加去重逻辑
中文专业术语检索差Embedding 对领域词敏感度不够换中文友好的 Embedding 模型,必要时加同义词扩展

6.2 我踩过的几个印象最深的坑

第一个坑是替换 Embedding 模型后没有重建索引。当时只是看到新模型评测分数高,想也没想就换上了,结果线上 hit rate 掉了近三成。后来才意识到向量库里的旧向量和新查询向量根本不在同一个向量空间里,这个坑排查了我整整一天。

第二个坑是把扫描版 PDF 直接加载进了知识库。加载结果全是空白字符,检索命中率看起来正常,因为空白文本也能被完全匹配,但最终答案毫无信息量。以后凡是有扫描件入库,我一定先做 OCR 验证,随机抽几页人工看文本是否可读。

第三个坑是过度调大 TopK。有一段时间为了让系统“尽量不漏”,我把 TopK 设到了 100,结果生成质量不升反降,因为大量不相关上下文混进 Prompt,模型抓不住重点。后来我调整策略,先召回 100 条再做重排,最终只喂给生成模型 5 条高质量结果,效果好得多。

6.3 一套可复用的排查顺序

当 RAG 系统表现异常,我的排查顺序非常固定:先看检索结果,再看 chunk 质量,最后看 Prompt。先打印出系统为当前问题召回的 TopN 文本,人工判断这些结果是否真的和问题相关。如果不相关,问题多半在离线建库或检索链路;如果相关但答案不对,问题在生成链路。这个排查顺序能帮你把一半以上的问题快速定位掉,避免上来就调模型参数、反复试 Prompt 这种无头苍蝇式操作。

实际跑下来,RAG 项目的效果提升从来不是某个单一环节的功劳,而是离线建库和在线查询两条链路持续迭代的结果。我现在接到一个新知识库需求,第一件事永远是先把文档打样出来看切分结果,再谈模型和框架。前期把这两步做扎实,后续的优化空间才会真正打开。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询