ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG高级检索实战:从基础召回升级到精准问答

RAG高级检索实战:从基础召回升级到精准问答 做 RAG 项目最难受的是什么不是模型不够聪明不是知识库没搭起来而是检索出来的东西根本不对。用户问的问题明明很简单召回的内容却南辕北辙最后大模型一本正经地编了个答案你还要在脑子里替他解释——“它其实学到了你的语气但没学到你的知识”。很多从 0 到 1 学习 AI 应用开发的同学第一次跑通 RAG demo 时都很兴奋文档切了、向量存了、问答能走了。但一旦进入真实业务立刻暴露两个问题召回不准和答案不可信。真正拉开差距的不是谁会用 LangChain而是谁理解检索链路里的每一个环节并且能在实际场景中把精准度一点点调上去。这篇文章不打算停留在“RAG 是什么”的科普层面而是把 RAG 原理讲透同时用一套可以落地的 Python 代码演示从基础检索到高级检索查询改写、混合检索、重排序、父子文档召回的完整升级过程。读完你会得到一个判断RAG 系统的天花板很大程度取决于检索质量而检索质量是工程细节堆出来的。1. 这篇文章真正要解决的问题先聊一个现实问题为什么同样是 RAG别人做出来的问答系统能直接上线你做的却只能在演示时流畅一塞真实数据就崩原因往往不在大模型而在检索链路上。RAG 的全称是 Retrieval-Augmented Generation检索增强生成。它的思路很直白回答之前先从外部知识库里把相关资料检索出来把资料拼进 Prompt让大模型基于这些资料回答。这样既不用重新训练模型又能让模型拥有“私有知识”。但问题也出在“检索”两个字上。最朴素的 RAG 流程是把文档切成固定长度的 chunk → Embedding 成向量 → 存进向量数据库 → 用户提问时也做 Embedding → 用向量余弦相似度召回 Top-K → 拼进 Prompt。这套流程 demo 完全没问题真实场景却会踩很多坑切块太机械固定长度切出来的 chunk 常常把一个完整语义割裂召回来的内容答非所问。问题表达与文档表达不一致用户问“2024 年第二季度利润怎么样”文档里写的是“Q2 net income”向量相似度可能完全对不上。Top-K 召回只看向量距离没有考虑关键词命中、文档重要程度、上下文连续性导致真正有用的内容被埋没。召回的 chunk 太碎每个 chunk 只有几百字信息不完整大模型拿到碎片也只能瞎猜。这篇文章要解决的就是这四类问题。我们会从 RAG 的基础原理出发搭一个最小可用系统然后逐步加入高级检索技术让系统从“能跑”变成“能用”。适合三类读者刚开始学 AI 应用开发想系统理解 RAG 链路的人已经写过简单 RAG demo但发现效果不稳定、想优化检索质量的人准备面试 AI 应用开发工程师岗位需要把 RAG 高级检索讲清楚的人。2. RAG 核心概念与工作原理2.1 什么是 RAGRAG 的全称是 Retrieval-Augmented Generation也就是“检索增强生成”。它的核心思路是在让大模型回答问题之前先从外部知识库中检索出与问题相关的资料把检索结果注入 Prompt再由大模型生成最终答案。这个设计背后的原因是大模型在训练时只见过公开的、截止到某个时间点的语料不可能知道你的内部文档、最新公告、私有数据。要让它“知道”这些信息不能每次都在线微调代价太大更聪明的办法是——知识不装进模型参数而是放在模型外面需要时现场检索。这就是 RAG 与传统微调Fine-tuning最本质的区别对比维度传统微调RAG知识存放位置模型参数内外部知识库向量库/文档库更新成本高需要重新训练低替换或新增文档即可幻觉风险中高模型可能死记硬背相对低答案需基于检索内容适合场景改变模型风格、格式、固定能力注入新知识、实时信息、私有资料工程复杂度需要训练基础设施需要检索系统和 Prompt 管理从成本角度看RAG 对大多数业务场景更友好。它真正降低的是“知识更新成本”和“私有化部署成本”。你不需要训练专属模型只需要把文档接入知识库就能让大模型拥有对应领域的问答能力。2.2 RAG 的标准工作链路RAG 的标准链路可以拆成三个阶段Index离线索引、Retrieve在线检索、Generate生成回答。Index 阶段文档加载Loader把 PDF、Word、Markdown、HTML、数据库记录等各种格式的文字提取出来。文本切块Splitting把长文档切分成适合向量化的文本块。向量化Embedding用 Embedding 模型把文本块转成向量。存储Storage把向量写入向量数据库同时保存原始文本和元信息。Retrieve 阶段查询理解对用户提问做可能的分词、改写或扩展。召回从向量库/倒排索引中找到最相关的候选文档。重排对召回的候选结果进一步精排筛选出真正有用的内容。Generate 阶段构造 Prompt把系统指令、检索到的文档片段、用户问题进行组装。生成答案交给大模型生成最终回答。附加引用更完备的系统会输出内容来源、参考文献方便用户核对。三个环节里最容易拉开差距的是 Retrieve。因为 Generate 现在各家大模型能力大同小异Index 的向量化质量也比较成熟但 Retrieve 却涉及切块策略、查询改写、召回算法、重排策略等多个环节任何一个细节不到位都会直接导致答案质量大幅下降。2.3 为什么“高级检索”值得单独拿出来学搜索引擎领域有一句话“搜索的成功不在于算法多酷而在于用户能用最快的方式找到想要的页面。”RAG 也一样用户不会关心你的向量维度是 1024 还是 1536他只会关心一个问题“我问的事你答对了没有。”基础检索的问题是只依赖向量相似度缺少关键词语义对齐、缺少文档全局上下文、缺少搜索行业多年积累下来的工程技巧。高级检索就是把传统搜索引擎和向量检索结合起来再加上大模型的能力让召回质量产生质变。高级检索通常包括以下技术查询改写Query Rewriting用大模型把口语化、模糊的问题转换成更适合检索的查询表达。混合检索Hybrid SearchBM25 关键词检索和向量稠密检索同时进行再融合排序。重排序Rerank用专门的 Cross-Encoder 模型对召回结果做精排。父子文档召回Parent Document Retriever用小 chunk 做检索、用大 chunk 做上下文补充。这几个技术不是炫技它们是已经过大量真实场景验证的实践方案。接下来我们用代码逐步实现。3. 环境准备与前置条件为了把原理落到代码上我们选择一套比较通用的技术栈版本以实际项目为准本文重点演示通用思路操作系统Windows / macOS / Linux 均可Python 3.9 以上推荐 3.10向量数据库FAISS轻量适合学习和原型验证生产环境可换 Milvus / Qdrant / Elasticsearch用户提问时使用兼容 OpenAI 接口的大模型 API检索框架LangChain 作为编排工具同时可以手工写检索代码降低框架黑盒造成的问题。需要安装的核心依赖如下pip install langchain langchain-community langchain-openai langchain-text-splitters pip install faiss-cpu chromadb pip install pysqlite3-binary pip install openai requests beautifulsoup4 pypdf安装完成后建议先验证一下 FAISS 是否正常python -c from langchain_community.vectorstores import FAISS; print(FAISS OK)如果使用 Chroma 作为演示知识库也建议先跑通一行代码确认环境没问题python -c import chromadb; client chromadb.Client(); print(Chroma OK)这里真正容易踩坑的地方是不同版本的 LangChain 对 FAISS、Chroma 等组件的 API 变化较大。如果你在安装后出现模块找不到的错误不要怀疑自己大概率是版本兼容问题。可以统一安装 LangChain 0.1 或 0.2 系列的版本保持依赖版本一致。4. 核心流程拆解从文档加载到知识库构建这一节我们先实现 RAG 的 Index 阶段整个过程分成四步每一步都很关键。4.1 文档加载文档加载是 RAG 的第一个入口。常见的文档类型有 PDF、Word、Markdown、HTML、纯文本等。LangChain 提供了一系列加载器底层其实就是把各种格式解析成纯文本。下面是一个加载本地 Markdown 文件和一个 PDF 文件的示例from langchain_community.document_loaders import TextLoader, PyPDFLoader # 加载 Markdown 文档 markdown_loader TextLoader(docs/rag_guide.md, encodingutf-8) md_docs markdown_loader.load() # 加载 PDF 文档 pdf_loader PyPDFLoader(docs/product_manual.pdf) pdf_docs pdf_loader.load() print(fMarkdown 文档块数: {len(md_docs)}) print(fPDF 文档块数: {len(pdf_docs)})这里有一点要注意TextLoader 默认按文件整体加载成一个 Document 对象并不会自动切分。PDF 加载器则会把每一页提取成一个 Document。这些原始的 Document 对象通常还需要经过切块才能进入下一步。如果有很多文档可以用 DirectoryLoader 批量加载from langchain_community.document_loaders import DirectoryLoader, TextLoader text_loader_kwargs {encoding: utf-8} loader DirectoryLoader( docs, glob**/*.md, loader_clsTextLoader, loader_kwargstext_loader_kwargs, ) all_docs loader.load()4.2 文本切块为什么 chunk_size 和 overlap 这么重要文本切块是 RAG 中一个看起来简单、实际上决定系统上限的环节。如果 chunk 太大向量化后的语义会被稀释一个 chunk 里包含多个子主题检索时相似度计算会偏向“笼统的相似”而不是精准命中如果 chunk 太小切出来的文本块可能只有一两句话信息不完整即使命中了大模型也缺乏足够的上下文做出靠谱回答。所以实际工程中通常采用chunk_size chunk_overlap的组合每个 chunk 有一个目标长度相邻 chunk 之间有重叠区域。重叠的意义在于当一句话被切在两个 chunk 的交界处时前后两个 chunk 都能保留这句话的完整语义避免信息被“拦腰截断”。LangChain 中常用RecursiveCharacterTextSplitter它按一组分隔符递归拆分文本尽可能保留语义完整from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap64, separators[\n\n, \n, 。, , , , , ], ) chunks text_splitter.split_documents(all_docs) print(f切块后文档块数: {len(chunks)}) print(f第一个 chunk: {chunks[0].page_content[:200]})关于 chunk 大小没有统一标准但有一个经验区间中文业务文档常见选择是 256512 个 token英文技术文档可以适当放大到 5121024。更精确的方法是根据 Embedding 模型的最大输入长度和文档结构来决定。如果文档本身有明确的小节Markdown 标题、PDF 标题按结构切块往往优于纯长度切块。4.3 向量化与存储切好 chunk 之后需要把每个 chunk 转换成向量。Embedding 模型的选择非常关键。常见选项包括OpenAI 的 text-embedding-3-small / text-embedding-3-large国内的 bge-large-zh、m3e-base 等中文友好模型开源本地模型如 BAAI/bge-m3。如果你的业务是中文场景优先选择对中文效果好的 Embedding 模型。如果使用的是兼容 OpenAI 接口的服务代码上差别不会很大。下面是用 OpenAI Embedding 接口配合 FAISS 构建向量索引的示例from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS # 使用兼容 OpenAI 接口的 Embedding 服务 embeddings OpenAIEmbeddings( modeltext-embedding-3-small, base_urlhttps://api.openai.com/v1, ) vectorstore FAISS.from_documents(chunks, embeddings) # 保存到本地便于重复使用 vectorstore.save_local(faiss_index)这段代码执行完后本地会出现一个faiss_index文件夹里面包含向量索引文件和文档元数据。这里真正需要强调的点是向量数据库的核心价值不是“存向量”而是“快速找到与查询最相似的向量”。FAISS 在单机小规模场景下完全够用但一旦文档量到百万级或者需要实时更新、高并发访问就必须切换到 Milvus、Qdrant、Elasticsearch 这类分布式向量数据库。4.4 加载已有索引并执行基础检索第二次使用时不需要重新向量化所有文档直接加载本地的 FAISS 索引即可vectorstore FAISS.load_local( faiss_index, embeddings, allow_dangerous_deserializationTrue, )然后对一个查询做最基础的相似度检索query 2024年第二季度营收是多少 retrieved_docs vectorstore.similarity_search_with_score(query, k5) for doc, score in retrieved_docs: print(f相似度: {score:.4f} | 内容: {doc.page_content[:100]})similarity_search_with_score返回文档和相似度分数。FAISS 默认返回的距离分数越小代表越相似这一点在和向量数据库对接时一定要注意不同数据库的相似度定义千差万别不要想当然。到这一步一个“最小可用 RAG”已经跑通了。你可以把 Top-K 结果和大模型 API 拼起来做问答。但正如开头所说这个版本只能用于演示离“高级检索”还有明显距离。5. 高级检索实战从基础问答到精准召回基础检索的局限在于它只用一次向量相似度计算就定了生死。用户提问方式一变、关键词没对上、切块位置不合理召回结果就会失效。高级检索的思路是在向量检索基础上叠加查询改写、混合检索、重排序、父子文档召回等手段让最终进入大模型的上下文更精准。5.1 查询改写让问题更符合文档表达习惯同一件事用户的表达和文档的表达常常不一样。比如用户问“去年公司赚了多少钱”文档里写的是“2023 年度净利润”。直接拿“去年公司赚了多少钱”做向量检索不一定能命中“2023 年度净利润”那一段。查询改写Query Rewriting的思路是先用大模型把用户的原问题翻译成若干个适合检索的查询语句再用这些查询去检索。常见做法包括对模糊问题补充背景信息把口语转化为书面语拆解复合问题为多个子问题生成同义表达增加召回覆盖面。下面是一个用大模型做查询改写的示例from langchain_openai import ChatOpenAI llm ChatOpenAI( modelgpt-4o-mini, temperature0, ) rewrite_prompt 你是一个检索查询改写专家。请根据原始问题生成 3 个适合在文档库中检索的查询表达。 要求 1. 覆盖不同表达方式 2. 使用文档中可能的业务术语 3. 不要添加与原始问题无关的内容 原始问题{query} 请直接输出改写后的查询每行一个不要编号。 def rewrite_queries(query: str) - list[str]: response llm.invoke(rewrite_prompt.format(queryquery)) return [line.strip() for line in response.content.strip().splitlines() if line.strip()] query 今年我们的利润情况怎么样 queries rewrite_queries(query) print(queries)输出可能是这样的今年我们的利润情况怎么样 本年度利润数据是多少 2024年公司净利润表现如何然后用这些改写后的查询分别执行向量检索把结果合并去重再进入后续精排。这里的关键是改写不是让大模型自由发挥而是让查询更贴近文档的语言空间提升命中的概率。如果你不想引入大模型做改写也可以用简单规则比如从知识库标签中匹配关键词、将同义词替换为业务标准术语。不过在大模型成本足够低的今天大模型改写往往是效果最好、实现成本最低的方案。5.2 混合检索向量 BM25 关键词召回向量检索擅长捕捉语义相似但对精确词匹配不敏感。举个例子文档里反复出现内部产品代号“A-100”如果用户问的是“A-100 的维护周期”向量检索可能被“维护周期”干扰把其他设备的维护文档也召回来。此时精准的关键词匹配反而更有效。混合检索Hybrid Search就是把向量检索和 BM25 关键词检索结合起来两个通道分别召回然后把结果融合。BM25 是传统搜索引擎中最经典的相关性排序算法基于词频和文档长度来打分。在 LangChain 中可以直接用BM25Retrieverfrom langchain_community.retrievers import BM25Retriever # 用之前的 chunks 构建 BM25 检索器 bm25_retriever BM25Retriever.from_documents(chunks) bm25_retriever.k 5 # 向量检索器 vector_retriever vectorstore.as_retriever(search_kwargs{k: 5})然后分别检索再合并结果query A-100 产品维护周期 bm25_docs bm25_retriever.invoke(query) vector_docs vector_retriever.invoke(query) print(fBM25 召回数: {len(bm25_docs)}) print(f向量召回数: {len(vector_docs)})合并之后会面临一个问题两个通道的分数尺度不同怎么融合排序简单方案是加权相加给向量检索和 BM25 检索分别赋予权重然后排序。更稳妥的方案是把合并后的候选集交给 Rerank 模型做精排。需要注意的是BM25 对中文需要分词支持英文和中文的处理方式不同。如果你用的是英文文档直接用 jieba 分词后切分即可如果是中文文档建议在构建 BM25 索引前先对文本做 jieba 分词否则直接按空格分词会把整句汉语当成一个词匹配效果非常差。5.3 重排序Cross-Encoder 精排召回结果经过查询改写和混合检索我们拿到的候选集可能包含 1020 个文档块但这些候选之间还有质量差异Top 位置不一定是最相关的。这时候就需要重排序。重排序通常使用 Cross-Encoder 模型。它的特点是把查询和文档拼接成一个输入一起送入模型模型输出一个相关性强弱分数。相比 Bi-Encoder查询和文档分别编码再算相似度Cross-Encoder 的精度更高但因为要做两两拼接计算成本也更高。因此实际工程中通常先用向量检索粗召回再用 Cross-Encoder 精排前几十个候选保证准确率和开销达到平衡。在代码中可以使用FlagEmbedding或 HuggingFace 的 CrossEncoder# 安装依赖pip install sentence-transformers from sentence_transformers import CrossEncoder # 选择中文/多语言重排序模型 reranker CrossEncoder(BAAI/bge-reranker-v2-m3) def rerank_documents(query: str, docs: list, top_k: int 3): pairs [(query, doc.page_content) for doc in docs] scores reranker.predict(pairs) scored_docs sorted(zip(docs, scores), keylambda x: x[1], reverseTrue) return scored_docs[:top_k] combined_docs deduplicate(bm25_docs vector_docs) # 去重 top_docs rerank_documents(query, combined_docs, top_k3) for doc, score in top_docs: print(frerank 分数: {score:.4f} | 内容: {doc.page_content[:120]})加入 Rerank 之后效果往往有明显提升。因为向量检索的结果可能把“看起来像”的内容排前面而 Rerank 模型更贴近真实的相关性判断。这一节最关键的工程技巧是粗召回阶段追求覆盖率精排阶段追求准确率。不要在一开始就限制 Top-K 太小否则 Rerank 模型手里没有“子弹”。推荐粗召回取 2050 个候选精排后输出 35 个。5.4 父子文档召回解决 chunk 太碎的问题前面提到chunk 太小会导致上下文不完整。但 chunk 太大又会影响检索精度。父子文档召回Parent Document Retriever是一种兼顾两者的方案把文档切分成较粗的“父文档”比如一个章节甚至整个文档再从父文档里切出较细的“子文档”比如 512 token 的小 chunk检索时只用子文档去计算相似度确保召回精准拿到命中的子文档后自动返回它所属的父文档确保上下文完整。LangChain 中可以直接使用ParentDocumentRetrieverfrom langchain.retrievers import ParentDocumentRetriever from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain.storage import InMemoryStore from langchain_community.vectorstores import FAISS # 创建父子分块器 parent_splitter RecursiveCharacterTextSplitter(chunk_size2000, chunk_overlap200) child_splitter RecursiveCharacterTextSplitter(chunk_size256, chunk_overlap32) # 使用统一的向量存储 vectorstore FAISS.from_documents([], embeddings) store InMemoryStore() parent_retriever ParentDocumentRetriever( vectorstorevectorstore, docstorestore, child_splitterchild_splitter, parent_splitterparent_splitter, ) # 添加原始文档 parent_retriever.add_documents(all_docs) # 检索时返回父文档 retrieved_parent_docs parent_retriever.invoke(产品的保修期是多少) for doc in retrieved_parent_docs: print(f父文档内容长度: {len(doc.page_content)})这个方案在长文档问答场景非常好用。比如一份 30 页的用户手册第 12 页第 3 节讲了保修期限但保修期限还依赖第 2 页的总则定义。如果只用小 chunk 召回模型只知道“保修期 12 个月”却不知道“保修期的计算起点是发货日”。用父子文档方案模型能拿到 2000 字左右的完整章节回答自然可靠很多。5.5 组装高级检索管道把上面的技术组合起来我们可以组成一个完整的高级检索管道from typing import List from langchain_core.documents import Document from sentence_transformers import CrossEncoder from langchain_community.retrievers import BM25Retriever class AdvancedRetriever: def __init__(self, vectorstore, chunks, embeddings, rerank_modelBAAI/bge-reranker-v2-m3): self.bm25_retriever BM25Retriever.from_documents(chunks) self.bm25_retriever.k 20 self.vector_retriever vectorstore.as_retriever(search_kwargs{k: 20}) self.reranker CrossEncoder(rerank_model) def deduplicate(self, docs: List[Document]) - List[Document]: seen set() unique_docs [] for doc in docs: if doc.page_content not in seen: seen.add(doc.page_content) unique_docs.append(doc) return unique_docs def retrieve(self, query: str, top_k: int 3) - List[Document]: bm25_docs self.bm25_retriever.invoke(query) vector_docs self.vector_retriever.invoke(query) candidates self.deduplicate(bm25_docs vector_docs) pairs [(query, doc.page_content) for doc in candidates] scores self.reranker.predict(pairs) scored_docs sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) return [doc for doc, _ in scored_docs[:top_k]]使用时非常简单retriever AdvancedRetriever( vectorstorevectorstore, chunkschunks, embeddingsembeddings, ) docs retriever.retrieve(A-100 产品的维护周期, top_k3)这样一个检索管道把查询改写、混合召回、去重、Rerank 精排全部串起来了。你在真实项目里可以根据实际情况裁剪没有 Rerank 模型资源就做混合检索场景比较简单只要查询改写加父子文档也可以。6. 运行结果与效果验证光把代码写出来还不够关键是怎么知道系统真的变好了。我建议从两个维度验证检索质量验证设计一组测试问题覆盖三种类型文档中原文出现过的问题需要跨段落整合才能回答的问题用户表达模糊、与文档用词不一致的问题。然后分别使用基础检索和高级检索跑一遍对比 Top-3 召回结果的相关性。端到端问答验证把召回的文档块喂给大模型人工判断回答是否准确、是否有幻觉、是否引用了正确内容。这一步可以做成离线评估集定期回归。下面是一个最简单的验证脚本test_queries [ A-100 维护周期是多少, 保修期的起始日怎么计算, 去年利润情况怎么样, ] for query in test_queries: print(f Query: {query} ) docs retriever.retrieve(query, top_k2) for doc in docs: print(f- {doc.page_content[:80]})预期效果如下基础检索可能召回到泛泛而谈的文档块甚至是不相关的内容高级检索召回的文档块应该直接命中问题相关的章节上下文完整且排序靠前的文档确实与问题高度相关。如果召回结果依然不理想先检查这几点Fix 查询改写 Prompt 是否正常检查 Embedding 模型是否与文档语言匹配检查切块参数和文档结构看 chunk 是否把关键信息切碎检查 Rerank 模型的输入长度限制如果文档块太长Cross-Encoder 会被截断。7. 常见问题与排查思路问题现象可能原因排查方式解决方案安装依赖报错 ModuleNotFoundErrorLangChain 版本与社区包不匹配执行 pip list 查看版本统一安装 LangChain 0.1/0.2 系列按官方文档匹配 community 包中文字符串检索效果差BM25 没有做中文分词检查 BM25 索引的分词逻辑先使用 jieba 分词再构建 BM25 索引FAISS 保存后加载报错版本序列化不兼容查看加载堆栈对应 faiss 版本升级到统一版本或使用 JSON 导出重新构建索引检索召回的文档与问题无关切块太大或 Embedding 选型不合适人工抽样检查 Top-20 召回内容调整 chunk_size或换用更适配业务场景的 Embedding 模型回答了但答案错误Prompt 没有要求模型只能依据检索内容回答查看最终 Prompt 中的文档引用在 Prompt 中明确“只能基于检索内容回答不要编造”检索结果排序不稳定向量检索分数与 BM25 分数融合方式不统一打印两路召回分数分布改用 Rerank 模型统一排序父子文档检索返回超长内容parent_splitter 过大查看父文档长度缩小 parent chunk_size控制上下文输入长度API 调用超时查询改写或生成阶段调用大模型耗时长检查链路时延分布缓存改写结果或把改写与生成拆成异步任务一个问题如果按表格排查后仍未解决先从“最小可复现案例”开始也就是用一小段文档、一个查询、一次检索把链条逐步打印出来看是在哪一步丢失了关键信息。这条排查路径比盲目调参高效很多。8. 最佳实践与工程建议8.1 切块策略先看文档结构再看固定长度切块不要一上来就用固定长度。如果文档有层级结构Markdown 标题、PDF 章节、Word 大纲先按结构切再对过长段落做二次切分。这样能最大程度保留语义单元。如果文档本身就是长段落再用 chunk_size chunk_overlap 兜底。8.2 Embedding 选型看语言和领域中文业务优先选择中文优化的 Embedding 模型多语言场景用支持多语言的模型。不要只凭模型参数量大小做决定用一段真实业务数据跑一个召回对比比看榜单数字更有说服力。8.3 高级检索是组合方案不是单一技巧查询改写、混合检索、Rerank、父子文档召回这几个技术是叠加生效的。不同业务可以裁剪文档量大、用户提问随意 → 查询改写 Rerank 收益最高文档结构复杂、关键信息分散 → 父子文档召回收益最高文档关键词密集、专有名词多 → 混合检索收益最高。8.4 安全与权限RAG 系统上线前一定要确认知识库内容的使用权限。检索链路可能访问内部文档如果这些文档包含敏感信息必须在索引和检索接口上做权限控制防止越权访问。另一个容易忽略的安全问题是 Prompt 注入文档内容本身可能包含恶意指令用户提问也可能夹带“忽略前面的指令”之类的攻击。生产系统需要增加输入过滤、敏感信息检测和输出审核。8.5 评估体系是 RAG 工程的地基每次改动检索策略、切块参数、Embedding 模型时都要有可量化的评估指标召回率、命中率、答案相关性、幻觉率。建议维护一个 50200 条的评测集每次改动后跑一遍对比前后效果。没有评测集支撑的 RAG 开发完全是盲人摸象。8.6 生产环境注意选型FAISS 适合原型生产环境建议使用支持高并发、滚动更新、权限控制的向量数据库比如 Milvus、Qdrant、Elasticsearch。同时要把 Embedding 模型和 Rerank 模型做成独立服务避免每次请求都加载模型查询改写和生成层的大模型调用要加缓存降低成本和时延。9. 总结与后续学习方向这篇文章从 RAG 的基本原理讲起重点放在检索链路的工程化升级上。基础检索解决了“有没有”的问题高级检索解决的是“准不准”和“全不全”的问题。现在你应该掌握的技能是理解 RAG 的 Index / Retrieve / Generate 三段式架构知道切块大小、Embedding 选型、检索排序三个环节如何影响效果能够用代码实现查询改写、BM25 与向量混合检索、Rerank 精排、父子文档召回四种高级检索策略面对召不回、召回错、答不对三类问题有清晰的排查路径。如果这篇文章对你起到了“从 0 到 1”的引导作用建议下一步继续深入三个方向Agentic RAG把检索从一步变成多步让大模型根据初步结果决定是否继续搜索、是否需要查数据库、是否需要调用工具适合复杂推理类问答知识图谱与向量数据库结合用实体关系补充纯向量的语义缺失解决“多跳问题”和“关系型问题”这是当前 RAG 领域一个非常有潜力的方向RAG 效果评估与监控建立评测集、评测指标和线上监控体系把 RAG 从“能跑”推向“稳跑”。RAG 入门容易精通很难。难的不是技术有多复杂而是每个环节都需要用真实场景和数据反复调优。建议你拿一份真实业务文档按本文的步骤完整跑一遍再试着自己优化检索质量。等你能解释清楚“为什么这个参数改小了召回结果反而变差了”的时候你对 RAG 的理解就已经超过大多数只跑过 demo 的人了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进