)
作者没有四次元口袋的蓝胖日期2026-09-23标签RAG, 项目实战, AI应用开发RAG知识梳理(3)前两篇分别讲解了文档切分与 Embedding、向量数据库与文档问答流程。这篇是系列的收官之作——先把所有模块整合为一个从头到尾可运行的完整 Demo然后深入讲解 RAG 的常见优化方向帮你在面试中展现更深的技术理解。核心掌握完整 RAG 项目代码、MMR/Rerank/Query改写/HyDE/语义切分/父子文档等优化策略。一、完整DemoJava面试知识库问答系统下面是一个从头到尾可运行的完整示例——加载 Markdown 文档建立向量索引实现问答 RAG完整流程DemoJava面试知识库问答系统 依赖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文件loaderDirectoryLoader(directory_path,glob**/*.md,# 递归加载所有.md文件loader_clsTextLoader,loader_kwargs{encoding:utf-8},)docsloader.load()print(f加载了{len(docs)}个文档)returndocs# 第二步切分文档 defsplit_documents(docs):将文档切分为合适大小的块splitterRecursiveCharacterTextSplitter(chunk_size500,chunk_overlap50,separators[\n\n,\n,。,., ,])chunkssplitter.split_documents(docs)print(f切分为{len(chunks)}个文档块)# 给每个块添加来源元数据fori,chunkinenumerate(chunks):chunk.metadata[chunk_id]ireturnchunks# 第三步创建向量库 defcreate_vectorstore(chunks):将文档块向量化并存入FAISSembeddingsOpenAIEmbeddings(modeltext-embedding-3-small)vectorstoreFAISS.from_documents(chunks,embeddings)print(f向量库创建完成包含{vectorstore.index.ntotal}个向量)returnvectorstore# 第四步构建RAG链 defbuild_rag_chain(vectorstore):构建检索增强生成链retrievervectorstore.as_retriever(search_typesimilarity,# 相似度搜索search_kwargs{k:3}# 返回Top-3)# RAG Promptrag_promptChatPromptTemplate.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(modelgpt-4,temperature0)|StrOutputParser())returnrag_chain# 第五步运行 defmain():# 1. 加载文档假设 docs/ 目录下有面试笔记docsload_documents(./docs)# 2. 切分文档chunkssplit_documents(docs)# 3. 创建向量库vectorstorecreate_vectorstore(chunks)# 4. 保存向量库下次可直接加载不用重新计算vectorstore.save_local(./faiss_index)# 5. 构建RAG链rag_chainbuild_rag_chain(vectorstore)# 6. 问答循环print(\n Java面试知识库问答系统 )print(输入 quit 退出\n)whileTrue:questioninput(你的问题)ifquestion.lower()quit:breakanswerrag_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的概率极低千万分之六 - 扩容时元素位置只有两种原位置或原位置旧容量 -------------------------------------------------- 你的问题quit1.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 也降低回答多样性。方案MMRMaximal Marginal Relevance在相关性和多样性之间取平衡。# MMR先取大量候选再从中选出既相关又多样的结果retrievervectorstore.as_retriever(search_typemmr,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 基于不相关内容强行回答。方案设置阈值过滤掉相似度不达标的文档。# 只返回相似度高于阈值的文档resultsvectorstore.similarity_search_with_score(问题,k5)threshold0.7# 根据实际情况调整filtered[docfordoc,scoreinresultsifscorethreshold]2.3 Rerank重排序问题Embedding 检索是粗排——速度快但精度有限返回的 Top-K 中排序不一定准确。方案用更精确的 Rerank 模型进行精排形成两阶段检索。# 典型的两阶段检索# 第一阶段Embedding粗排快但不够精确→ 取Top-20# 第二阶段Rerank精排慢但更精确→ 取Top-5# 使用Cohere Rerankfromlangchain.retrieversimportContextualCompressionRetrieverfromlangchain.cohereimportCohereRerank# 1. 基础检索器先取20个base_retrievervectorstore.as_retriever(search_kwargs{k:20})# 2. Rerank模型rerankerCohereRerank(modelrerank-multilingual-v3.0,top_n5)# 3. 组合先检索再重排compression_retrieverContextualCompressionRetriever(base_compressorreranker,base_retrieverbase_retriever)# 使用方式和普通检索器一样resultscompression_retriever.invoke(Spring和Spring Boot的区别)# 返回的是经过重排序的Top-5结果核心价值Rerank 是目前提升 RAG 检索质量性价比最高的手段。粗排快但粗精排慢但准两者结合既保证速度又提高精度。2.4 Query 改写问题用户的口语化提问往往不适合直接用于检索。比如Spring的两大核心是什么直接检索效果不如Spring框架IoC控制反转和AOP面向切面编程的核心原理。方案让 LLM 把用户问题改写为更适合检索的形式。fromlangchain.chat_modelsimportChatOpenAIfromlangchain.promptsimportChatPromptTemplatefromlangchain.schemaimportStrOutputParser rewrite_promptChatPromptTemplate.from_template(把以下用户问题改写为更适合文档检索的查询语句直接输出改写结果不要解释。\n用户问题{question})rewrite_chainrewrite_prompt|ChatOpenAI(modelgpt-4)|StrOutputParser()rewritten_queryrewrite_chain.invoke({question:Spring的两大核心是什么})# 可能改写为Spring框架IoC控制反转和AOP面向切面编程的核心原理和实现机制# 用改写后的query去检索resultsvectorstore.similarity_search(rewritten_query,k3)2.5 HyDE假设性文档嵌入问题用户提问和文档的语义空间可能不对齐——问题很简短文档很详细向量距离不一定准确。方案让 LLM 先生成一个假设性回答用回答而非问题去检索因为回答和文档更像。# HyDE先生成假设性回答用回答去检索hyde_promptChatPromptTemplate.from_template(请回答以下问题即使不确定也给出你的理解\n{question})hyde_chainhyde_prompt|ChatOpenAI(modelgpt-4)|StrOutputParser()hypothetical_answerhyde_chain.invoke({question:question})# 用假设性回答去检索通常比直接用问题检索效果更好resultsvectorstore.similarity_search(hypothetical_answer,k3)直觉理解问Spring的两大核心是什么10个字文档可能写的是Spring IoC 控制反转是…Spring AOP 面向切面编程是…200字。直接匹配效果差但如果先让 LLM 生成一段类似文档风格的回答再去匹配命中率就高多了。2.6 文档切分优化语义切分问题固定 chunk_size 切分可能在语义中间断开。方案按语义变化点自动切分——相邻句子语义变化大时切分变化小时保持连贯。fromlangchain_experimental.text_splitterimportSemanticChunker semantic_splitterSemanticChunker(embeddingsOpenAIEmbeddings(),breakpoint_threshold_typepercentile,breakpoint_threshold_amount95# 语义变化超过95%分位数时切分)chunkssemantic_splitter.split_documents(docs)父子文档检索问题小块检索精确容易匹配但返回给 LLM 的上下文太短丢失全局信息。方案小块用于检索精确匹配但返回对应的大块完整上下文给 LLM。fromlangchain.retrieversimportParentDocumentRetrieverfromlangchain.storageimportInMemoryStore# 父文档大块用于给LLMparent_splitterRecursiveCharacterTextSplitter(chunk_size2000,chunk_overlap200)# 子文档小块用于检索child_splitterRecursiveCharacterTextSplitter(chunk_size400,chunk_overlap40)storeInMemoryStore()retrieverParentDocumentRetriever(vectorstorevectorstore,docstorestore,child_splitterchild_splitter,parent_splitterparent_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把口语化问题→适合检索的形式 │ └── HyDELLM先生成假设性回答 → 用回答去检索 │ ├── 切分优化 │ ├── 语义切分按语义变化点自动切分非固定长度 │ └── 父子文档小块检索精确大块返回完整上下文 │ └── 面试策略 ├── 完整Demo能说出五步法 ├── 优化方向知道名称和适用场景 └── 重点掌握Rerank、MMR、HyDE 写在最后学习建议一定要动手跑一遍 DemoRAG 的每一步拆开都不难但串起来后容易在细节上卡住编码、API调用、路径等。实际跑通一次理解深度远超看十遍教程。优化方向按优先级学先掌握 MMR 和阈值过滤代码简单效果直观再了解 Rerank性价比最高最后看 HyDE 和语义切分理解思路即可。面试重点是知道有什么面试官问 RAG 优化时不指望你写出完整代码但希望你能说出检索结果重复可以用 MMR、粗排不够精确可以加 Rerank这类方案。三篇配合看效果最好上篇切分Embedding→ 下篇向量库问答流程→ 本篇完整Demo优化三篇合在一起就是面试 RAG 的完整答案。面试高频问题速答QRAG 有哪些常见的优化方向① 检索策略MMR 多样性检索避免返回重复结果、相似度阈值过滤② Rerank 重排序先粗排取 Top-20再用 Rerank 模型精排取 Top-5③ Query 优化Query 改写让问题更适合检索、HyDE先生成假设性回答再去检索④ 切分优化语义切分按语义边界而非固定长度、父子文档小块检索精确大块返回完整上下文。QMMR 的原理是什么和普通的 Top-K 有什么区别普通 Top-K 只按相似度排序可能返回内容高度重复的文档块。MMRMaximal Marginal Relevance在相关性基础上引入多样性惩罚——每次选择时不仅考虑和问题的相似度还考虑和已选文档的冗余度。关键参数lambda_mult控制相关性和多样性的平衡0 为最大多样性1 为最大相关性。Q什么是 Rerank为什么要两阶段检索Embedding 检索速度快毫秒级但精度有限属于粗排。Rerank 模型如 Cohere Rerank会对每对 query-document 进行精细的交叉注意力计算精度高但速度慢属于精排。两阶段策略先用 Embedding 快速取 Top-20 候选再用 Rerank 精排取 Top-5兼顾速度和精度。这是目前提升 RAG 检索质量性价比最高的手段。QHyDE 的核心思想是什么为什么比直接用问题检索效果好HyDEHypothetical Document Embeddings让 LLM 先生成一个假设性回答然后用这个回答去检索文档而不是直接用用户问题检索。核心原因是用户问题通常很简短口语化而文档通常较长书面化两者在向量空间中存在语义偏差。LLM 生成的假设性回答在风格和详细程度上更接近文档因此检索效果更好。