☰
RAG知识梳理(3)
2026/9/30 2:41:03 网站建设 项目流程

作者:没有四次元口袋的蓝胖
日期:2026-09-23
标签:RAG, 项目实战, AI应用开发


RAG知识梳理(3)

前两篇分别讲解了文档切分与 Embedding、向量数据库与文档问答流程。这篇是系列的收官之作——先把所有模块整合为一个从头到尾可运行的完整 Demo,然后深入讲解 RAG 的常见优化方向,帮你在面试中展现更深的技术理解。

核心掌握:完整 RAG 项目代码、MMR/Rerank/Query改写/HyDE/语义切分/父子文档等优化策略。


一、完整Demo:Java面试知识库问答系统

下面是一个从头到尾可运行的完整示例——加载 Markdown 文档,建立向量索引,实现问答:

""" RAG完整流程Demo:Java面试知识库问答系统 依赖:pip install langchain openai faiss-cpu """fromlangchain.document_loadersimportTextLoader,DirectoryLoaderfromlangchain.text_splitterimportRecursiveCharacterTextSplitterfromlangchain.embeddingsimportOpenAIEmbeddingsfromlangchain.vectorstoresimportFAISSfromlangchain.chat_modelsimportChatOpenAIfromlangchain.promptsimportChatPromptTemplatefromlangchain.schemaimportStrOutputParserfromlangchain.schema.runnableimportRunnablePassthroughimportos# 设置API Key(实际使用时建议用环境变量)os.environ["OPENAI_API_KEY"]="your-api-key-here"# ==================== 第一步:加载文档 ====================defload_documents(directory_path):"""加载目录下的所有txt/md文件"""loader=DirectoryLoader(directory_path,glob="**/*.md",# 递归加载所有.md文件loader_cls=TextLoader,loader_kwargs={"encoding":"utf-8"},)docs=loader.load()print(f"加载了{len(docs)}个文档")returndocs# ==================== 第二步:切分文档 ====================defsplit_documents(docs):"""将文档切分为合适大小的块"""splitter=RecursiveCharacterTextSplitter(chunk_size=500,chunk_overlap=50,separators=["\n\n","\n","。","."," ",""])chunks=splitter.split_documents(docs)print(f"切分为{len(chunks)}个文档块")# 给每个块添加来源元数据fori,chunkinenumerate(chunks):chunk.metadata["chunk_id"]=ireturnchunks# ==================== 第三步:创建向量库 ====================defcreate_vectorstore(chunks):"""将文档块向量化并存入FAISS"""embeddings=OpenAIEmbeddings(model="text-embedding-3-small")vectorstore=FAISS.from_documents(chunks,embeddings)print(f"向量库创建完成,包含{vectorstore.index.ntotal}个向量")returnvectorstore# ==================== 第四步:构建RAG链 ====================defbuild_rag_chain(vectorstore):"""构建检索增强生成链"""retriever=vectorstore.as_retriever(search_type="similarity",# 相似度搜索search_kwargs={"k":3}# 返回Top-3)# RAG Promptrag_prompt=ChatPromptTemplate.from_messages([("system","""你是一个Java面试辅导助手,请基于以下参考资料回答问题。 要求: 1. 优先使用参考资料中的内容 2. 如果资料不足,可补充但需标注"以下为补充内容" 3. 回答简洁专业,适当给出代码示例 4. 如果资料与问题无关,直接说"提供的资料中没有相关信息" 参考资料: {context}"""),("human","{question}")])defformat_docs(docs):return"\n\n---\n\n".join(f"[来源:{doc.metadata.get('source','unknown')}]\n{doc.page_content}"fordocindocs)# LCEL链rag_chain=({"context":retriever|format_docs,"question":RunnablePassthrough()}|rag_prompt|ChatOpenAI(model="gpt-4",temperature=0)|StrOutputParser())returnrag_chain# ==================== 第五步:运行 ====================defmain():# 1. 加载文档(假设 docs/ 目录下有面试笔记)docs=load_documents("./docs")# 2. 切分文档chunks=split_documents(docs)# 3. 创建向量库vectorstore=create_vectorstore(chunks)# 4. 保存向量库(下次可直接加载,不用重新计算)vectorstore.save_local("./faiss_index")# 5. 构建RAG链rag_chain=build_rag_chain(vectorstore)# 6. 问答循环print("\n===== Java面试知识库问答系统 =====")print("输入 'quit' 退出\n")whileTrue:question=input("你的问题:")ifquestion.lower()=="quit":breakanswer=rag_chain.invoke(question)print(f"\n回答:{answer}\n")print("-"*50)if__name__=="__main__":main()

1.1 运行效果示例

加载了 5 个文档 切分为 47 个文档块 向量库创建完成,包含 47 个向量 ===== Java面试知识库问答系统 ===== 输入 'quit' 退出 你的问题:HashMap的底层实现原理是什么? 回答:HashMap 在 JDK 1.8 中的底层数据结构是**数组 + 链表 + 红黑树**。 **核心机制:** 1. **哈希寻址**:通过 `(n-1) & hash` 计算数组下标 2. **冲突处理**:同一位置采用链地址法,链表长度 ≥ 8 且数组长度 ≥ 64 时转红黑树 3. **扩容机制**:默认容量16,负载因子0.75,扩容为原来的2倍 **面试补充点:** - 为什么链表阈值是8?基于泊松分布,长度达到8的概率极低(千万分之六) - 扩容时元素位置只有两种:原位置或原位置+旧容量 -------------------------------------------------- 你的问题:quit

1.2 代码结构要点

这个 Demo 体现了 RAG 工程的标准五步法:

步骤函数作用
① 加载文档load_documents()读取原始文件(支持多种格式)
② 切分文档split_documents()按语义边界切分为小块
③ 创建向量库create_vectorstore()Embedding向量化 + 存入FAISS
④ 构建RAG链build_rag_chain()检索器 + Prompt + LLM 串联
⑤ 运行问答main()循环接收问题、返回回答

实际项目中,只需要替换文档来源、调整 Prompt、选择不同的向量库/LLM,就能快速适配不同的业务场景。


二、常见优化方向

基础的 RAG 流程虽然能跑起来,但在实际项目中往往需要进一步优化。以下是面试中最常被问到的 6 个优化方向。

2.1 MMR 多样性检索

问题:Top-K 检索可能返回内容高度重复的文档块,浪费 Token 也降低回答多样性。

方案:MMR(Maximal Marginal Relevance)在相关性和多样性之间取平衡。

# MMR:先取大量候选,再从中选出既相关又多样的结果retriever=vectorstore.as_retriever(search_type="mmr",search_kwargs={"k":4,# 最终返回4个"fetch_k":20,# 先取Top-20候选"lambda_mult":0.5# 多样性权重(0=最大多样性,1=最大相关性)})

参数调节:lambda_mult是关键。设为 0 时结果最多样(适合宽泛问题),设为 1 时最相关(适合精确问题),一般取 0.5 左右。

2.2 相似度阈值过滤

问题:有些问题和知识库无关,但检索器仍然会返回"最相似"的结果(只是相似度很低),导致 LLM 基于不相关内容强行回答。

方案:设置阈值,过滤掉相似度不达标的文档。

# 只返回相似度高于阈值的文档results=vectorstore.similarity_search_with_score("问题",k=5)threshold=0.7# 根据实际情况调整filtered=[docfordoc,scoreinresultsifscore<threshold]

2.3 Rerank(重排序)

问题:Embedding 检索是"粗排"——速度快但精度有限,返回的 Top-K 中排序不一定准确。

方案:用更精确的 Rerank 模型进行"精排",形成两阶段检索。

# 典型的两阶段检索:# 第一阶段:Embedding粗排(快,但不够精确)→ 取Top-20# 第二阶段:Rerank精排(慢,但更精确)→ 取Top-5# 使用Cohere Rerankfromlangchain.retrieversimportContextualCompressionRetrieverfromlangchain.cohereimportCohereRerank# 1. 基础检索器(先取20个)base_retriever=vectorstore.as_retriever(search_kwargs={"k":20})# 2. Rerank模型reranker=CohereRerank(model="rerank-multilingual-v3.0",top_n=5)# 3. 组合:先检索再重排compression_retriever=ContextualCompressionRetriever(base_compressor=reranker,base_retriever=base_retriever)# 使用方式和普通检索器一样results=compression_retriever.invoke("Spring和Spring Boot的区别")# 返回的是经过重排序的Top-5结果

核心价值:Rerank 是目前提升 RAG 检索质量性价比最高的手段。粗排快但粗,精排慢但准,两者结合既保证速度又提高精度。

2.4 Query 改写

问题:用户的口语化提问往往不适合直接用于检索。比如"Spring的两大核心是什么",直接检索效果不如"Spring框架IoC控制反转和AOP面向切面编程的核心原理"。

方案:让 LLM 把用户问题改写为更适合检索的形式。

fromlangchain.chat_modelsimportChatOpenAIfromlangchain.promptsimportChatPromptTemplatefromlangchain.schemaimportStrOutputParser rewrite_prompt=ChatPromptTemplate.from_template("把以下用户问题改写为更适合文档检索的查询语句,直接输出改写结果,不要解释。\n用户问题:{question}")rewrite_chain=rewrite_prompt|ChatOpenAI(model="gpt-4")|StrOutputParser()rewritten_query=rewrite_chain.invoke({"question":"Spring的两大核心是什么"})# 可能改写为:"Spring框架IoC控制反转和AOP面向切面编程的核心原理和实现机制"# 用改写后的query去检索results=vectorstore.similarity_search(rewritten_query,k=3)

2.5 HyDE(假设性文档嵌入)

问题:用户提问和文档的语义空间可能不对齐——问题很简短,文档很详细,向量距离不一定准确。

方案:让 LLM 先生成一个假设性回答,用回答(而非问题)去检索,因为回答和文档更像。

# HyDE:先生成假设性回答,用回答去检索hyde_prompt=ChatPromptTemplate.from_template("请回答以下问题(即使不确定也给出你的理解):\n{question}")hyde_chain=hyde_prompt|ChatOpenAI(model="gpt-4")|StrOutputParser()hypothetical_answer=hyde_chain.invoke({"question":question})# 用假设性回答去检索,通常比直接用问题检索效果更好results=vectorstore.similarity_search(hypothetical_answer,k=3)

直觉理解:问"Spring的两大核心是什么"(10个字),文档可能写的是"Spring IoC 控制反转是…,Spring AOP 面向切面编程是…"(200字)。直接匹配效果差,但如果先让 LLM 生成一段类似文档风格的回答再去匹配,命中率就高多了。

2.6 文档切分优化

语义切分

问题:固定 chunk_size 切分可能在语义中间断开。

方案:按语义变化点自动切分——相邻句子语义变化大时切分,变化小时保持连贯。

fromlangchain_experimental.text_splitterimportSemanticChunker semantic_splitter=SemanticChunker(embeddings=OpenAIEmbeddings(),breakpoint_threshold_type="percentile",breakpoint_threshold_amount=95# 语义变化超过95%分位数时切分)chunks=semantic_splitter.split_documents(docs)
父子文档检索

问题:小块检索精确(容易匹配),但返回给 LLM 的上下文太短,丢失全局信息。

方案:小块用于检索(精确匹配),但返回对应的大块(完整上下文)给 LLM。

fromlangchain.retrieversimportParentDocumentRetrieverfromlangchain.storageimportInMemoryStore# 父文档(大块,用于给LLM)parent_splitter=RecursiveCharacterTextSplitter(chunk_size=2000,chunk_overlap=200)# 子文档(小块,用于检索)child_splitter=RecursiveCharacterTextSplitter(chunk_size=400,chunk_overlap=40)store=InMemoryStore()retriever=ParentDocumentRetriever(vectorstore=vectorstore,docstore=store,child_splitter=child_splitter,parent_splitter=parent_splitter,)retriever.add_documents(docs)# 检索时用小块匹配,但返回的是对应的父文档块

三、优化方向总结

优化方向解决的问题复杂度面试优先级
MMR多样性检索Top-K结果重复低⭐⭐⭐
相似度阈值过滤不相关内容被检索低⭐⭐
Rerank重排序粗排不够精确中⭐⭐⭐⭐
Query改写用户表述不适合检索低⭐⭐⭐
HyDE问题与文档语义空间不对齐中⭐⭐⭐
语义切分固定切分破坏语义中⭐⭐
父子文档小块检索丢失上下文高⭐⭐⭐

面试时不需要记住所有优化的代码,但要知道有这些优化方向,能说出"在什么场景下用什么优化"。


🗺️ 思维导图速览

RAG进阶:完整项目与优化 ├── 完整Demo(标准五步法) │ ├── ① 加载文档:DirectoryLoader递归加载 │ ├── ② 切分文档:RecursiveCharacterTextSplitter │ ├── ③ 创建向量库:Embedding + FAISS │ ├── ④ 构建RAG链:Retriever + Prompt + LCEL │ └── ⑤ 运行问答:循环接收问题返回回答 │ ├── 检索优化 │ ├── MMR多样性检索 │ │ ├── 先取大量候选 → 选出相关且多样的结果 │ │ └── 关键参数:lambda_mult(多样性权重) │ ├── 相似度阈值过滤 │ │ └── 过滤掉低相似度结果,防止不相关内容被回答 │ └── Rerank重排序 │ ├── 两阶段:Embedding粗排 → Rerank精排 │ └── 性价比最高的优化手段 │ ├── Query优化 │ ├── Query改写:LLM把口语化问题→适合检索的形式 │ └── HyDE:LLM先生成假设性回答 → 用回答去检索 │ ├── 切分优化 │ ├── 语义切分:按语义变化点自动切分(非固定长度) │ └── 父子文档:小块检索精确,大块返回完整上下文 │ └── 面试策略 ├── 完整Demo能说出五步法 ├── 优化方向知道名称和适用场景 └── 重点掌握:Rerank、MMR、HyDE

📝 写在最后

学习建议

  1. 一定要动手跑一遍 Demo:RAG 的每一步拆开都不难,但串起来后容易在细节上卡住(编码、API调用、路径等)。实际跑通一次,理解深度远超看十遍教程。
  2. 优化方向按优先级学:先掌握 MMR 和阈值过滤(代码简单,效果直观),再了解 Rerank(性价比最高),最后看 HyDE 和语义切分(理解思路即可)。
  3. 面试重点是"知道有什么":面试官问 RAG 优化时,不指望你写出完整代码,但希望你能说出"检索结果重复可以用 MMR"、"粗排不够精确可以加 Rerank"这类方案。
  4. 三篇配合看效果最好:上篇(切分+Embedding)→ 下篇(向量库+问答流程)→ 本篇(完整Demo+优化),三篇合在一起就是面试 RAG 的完整答案。

面试高频问题速答

Q:RAG 有哪些常见的优化方向?

① 检索策略:MMR 多样性检索(避免返回重复结果)、相似度阈值过滤;② Rerank 重排序:先粗排取 Top-20,再用 Rerank 模型精排取 Top-5;③ Query 优化:Query 改写(让问题更适合检索)、HyDE(先生成假设性回答再去检索);④ 切分优化:语义切分(按语义边界而非固定长度)、父子文档(小块检索精确,大块返回完整上下文)。

Q:MMR 的原理是什么?和普通的 Top-K 有什么区别?

普通 Top-K 只按相似度排序,可能返回内容高度重复的文档块。MMR(Maximal Marginal Relevance)在相关性基础上引入多样性惩罚——每次选择时,不仅考虑和问题的相似度,还考虑和已选文档的冗余度。关键参数lambda_mult控制相关性和多样性的平衡:0 为最大多样性,1 为最大相关性。

Q:什么是 Rerank?为什么要两阶段检索?

Embedding 检索速度快(毫秒级),但精度有限,属于"粗排"。Rerank 模型(如 Cohere Rerank)会对每对 query-document 进行精细的交叉注意力计算,精度高但速度慢,属于"精排"。两阶段策略:先用 Embedding 快速取 Top-20 候选,再用 Rerank 精排取 Top-5,兼顾速度和精度。这是目前提升 RAG 检索质量性价比最高的手段。

Q:HyDE 的核心思想是什么?为什么比直接用问题检索效果好?

HyDE(Hypothetical Document Embeddings)让 LLM 先生成一个假设性回答,然后用这个回答去检索文档,而不是直接用用户问题检索。核心原因是:用户问题通常很简短(口语化),而文档通常较长(书面化),两者在向量空间中存在语义偏差。LLM 生成的假设性回答在风格和详细程度上更接近文档,因此检索效果更好。

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

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

立即咨询