自学RAG(检索增强生成)

发布时间:2026/5/19 13:56:23

自学RAG(检索增强生成) RAG1. RAG全流程离线处理本地文档 / 知识库--分块切片每块文档向量化向量原文存入向量数据库在线实时问答用户问题输入--问题向量化--向量库相似度检索召回TopK相关文本块-把【用户问题检索到的上下文】做prompt增强拼接把增强后的完整prompt发给LLMLLM基于检索到的真实材料生成答案2. PDF加载器pypdf / PyPDFLoader按 page 逐页解析PyPDFLoader 底层使用 pypdf 解析PDF。它通常会把PDF按页加载返回的pages是一个列表每一页是一个Document。LangChain官方示例也是从 langchain_community.document_loaders 导入 PyPDFLoader.from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter ​ PDF_PATH rE:\learning\2309.10305v4.pdf ​ loader PyPDFLoader(PDF_PATH) ​ # pages按页解析后的结果 pages loader.load() ​ print(页数:, len(pages)) ​ print(\n第一页 metadata:) print(pages[0].metadata) ​ print(\n第一页内容预览:) print(pages[0].page_content[:1000]) ​ ​ # 如果要继续用于 RAG一般还要再切 chunk text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, ) ​ docs text_splitter.split_documents(pages) ​ print(\n切分后的 chunk 数量:, len(docs)) print(\n第一个 chunk:) print(docs[0].page_content) print(\n第一个 chunk metadata:) print(docs[0].metadata)pdfplumber / PDFPlumberLoader逐页解析但分栏可能混乱PDFPlumberLoader也是逐页解析通常返回一页一个Document。官方文档也说明它和PyMuPDF类似会返回包含页面metadata的 Document并且是一页一个document.from langchain_community.document_loaders import PDFPlumberLoader from langchain.text_splitter import RecursiveCharacterTextSplitter ​ PDF_PATH rE:\learning\2309.10305v4.pdf ​ loader PDFPlumberLoader(PDF_PATH) ​ # 逐页解析 pages loader.load() ​ print(页数:, len(pages)) ​ print(\n第一页 metadata:) print(pages[0].metadata) ​ print(\n第一页内容预览:) print(pages[2].page_content[:1000]) ​ ​ # 继续切分成 RAG chunk text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, ) ​ docs text_splitter.split_documents(pages) ​ print(\n切分后的 chunk 数量:, len(docs)) print(\n第一个 chunk:) print(docs[0].page_content)pdfminer / PDFMinerLoader将整个 PDF 解析成完整文本PDFMinerLoader适合你想先把PDF当作一个完整文档读出来然后自己定义切分规则。from langchain_community.document_loaders import PDFMinerLoader from langchain.text_splitter import RecursiveCharacterTextSplitter ​ PDF_PATH rE:\learning\2309.10305v4.pdf ​ loader PDFMinerLoader(PDF_PATH) ​ # 通常会把 PDF 解析成一个或少量 Document docs_raw loader.load() ​ print(原始 Document 数量:, len(docs_raw)) ​ print(\nmetadata:) print(docs_raw[0].metadata) ​ print(\n全文内容预览:) print(docs_raw[0].page_content[:2000]) ​ ​ # 你可以自己定义切分规则 text_splitter RecursiveCharacterTextSplitter( chunk_size800, chunk_overlap100, separators[ \n\n, # 优先按段落切 \n, # 再按换行切 . , # 再按英文句号切 , # 再按空格切 # 最后按字符硬切 ] ) ​ docs text_splitter.split_documents(docs_raw) ​ print(\n自定义切分后的 chunk 数量:, len(docs)) print(\n第一个 chunk:) print(docs[0].page_content) ​ text docs_raw[0].page_content ​ print(\n repr 查看原始换行 ) print(repr(text[:2000]))repr是 Python 里的一个内置函数全称可以理解为representation意思是“对象的原始表示形式”。它和print()最大的区别是print()会把\n真正显示成换行repr()会把\n显示成字符形式让你看清楚文本里到底有没有换行符。3. 文本分块TextSplitter可以理解为把一大段文本切成适合向量化的小块 chunk。4. RAG中的embedding模型什么是embeddingEmbedding 就是把文本转换成向量。比如一句话Baichuan2 的词表有多大经过 embedding model 之后会变成类似这样的数字向量[0.012, -0.231, 0.078, ..., 0.445]这个向量不是随便生成的而是表示这句话的语义位置。如果两句话意思接近它们的向量距离也会接近。例如Baichuan2 的词表有多大 Baichuan2 vocabulary size 是多少虽然一个是中文一个是中英混合但如果 embedding model 足够好它们在向量空间中应该比较接近。在 RAG 中embedding 的作用是用户问题 ↓ 转换成 query 向量 ​ PDF 文档 chunk ↓ 转换成 document 向量 ​ query 向量 和 document 向量 做相似度计算 ↓ 找出最相关的几个 chunk ↓ 交给大模型回答所以 RAG 的检索不是简单关键词匹配而是语义相似度检索Sentence BERTSentence-BERT简称SBERT可以理解为专门用来生成句子向量的 BERT 改进模型它的核心思想是句子 A → BERT Encoder → 向量 A 句子 B → BERT Encoder → 向量 B ​ 然后比较 cosine_similarity(向量 A, 向量 B)也就是similarity cos(vec_a, vec_b)如何选取 embedding model模型类型适合语言适合场景推荐度text-embedding-3-smallAPI多语言低成本 RAG高text-embedding-3-largeAPI多语言高质量 RAG高text-embedding-v3API中文/多语言阿里百炼 Qwen 项目高Cohere embed-v4.0API多语言企业搜索、图文检索高Voyage 3.5 / 4API多语言高质量检索、长文档高BAAI/bge-m3开源多语言中文/英文/跨语言 RAG很高bge-large-zh-v1.5开源中文中文知识库高multilingual-e5-large开源多语言跨语言检索高all-MiniLM-L6-v2开源英文为主快速 demo中paraphrase-multilingual-MiniLM-L12-v2开源多语言轻量多语言 demo中sentence-t5-large开源英文/多语言论文 RAG demo中高jina-embeddings-v3/v5开源/API多语言长文本、多任务检索高Qwen3-Embedding开源/API中文/多语言Qwen 生态、中文 RAG高5. 基于HuggingfaceLangchain 快速实现RAGHuggingface提供了两种方式调用LLM通过API token的方式# -*- coding: utf-8 -*- ​ import uuid from typing import List, Tuple, Dict, Any ​ from huggingface_hub import InferenceClient ​ from langchain_community.document_loaders import PDFMinerLoader from langchain.text_splitter import RecursiveCharacterTextSplitter ​ try: from langchain_huggingface import HuggingFaceEmbeddings except ImportError: from langchain_community.embeddings import HuggingFaceEmbeddings ​ try: from langchain_chroma import Chroma except ImportError: from langchain_community.vectorstores import Chroma ​ ​ # # 1. 基础配置 # ​ PDF_PATH rE:\learning\2309.10305v4.pdf ​ # 在这里粘贴你的 HuggingFace Token # 注意不要把这个文件上传到 GitHub / Gitee / 网盘 HF_TOKEN 输入token ​ # LLM 模型候选。第一个不行会自动尝试下一个。 LLM_MODEL_CANDIDATES [ deepseek-ai/DeepSeek-R1, openai/gpt-oss-120b, zai-org/GLM-4.5, ] ​ # 轻量多语言 embedding第一次运行会下载模型 EMBEDDING_MODEL_NAME sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 ​ # 更强但更大的模型跑通后可以换成这个 # EMBEDDING_MODEL_NAME BAAI/bge-m3 ​ ​ # # 2. 创建 HuggingFace LLM Client # ​ def create_hf_client() - InferenceClient: if not HF_TOKEN or HF_TOKEN 在这里粘贴你的 HuggingFace Token: raise ValueError(请先在代码里的 HF_TOKEN 位置粘贴你的 HuggingFace Token。) ​ client InferenceClient( tokenHF_TOKEN, providerauto ) ​ return client ​ ​ # # 3. 加载 PDF 并切分 # ​ def load_and_split_pdf(pdf_path: str): print(\n 1. 加载 PDF ) print(fPDF 路径: {pdf_path}) ​ loader PDFMinerLoader(pdf_path) docs_raw loader.load() ​ print(f原始 Document 数量: {len(docs_raw)}) ​ if not docs_raw: raise ValueError(PDF 没有解析出任何文本请检查 PDF 路径或 PDF 是否为扫描版。) ​ print(\n原始文本预览:) print(docs_raw[0].page_content[:500]) ​ print(\n 2. 文本切分 ) ​ text_splitter RecursiveCharacterTextSplitter( chunk_size800, chunk_overlap100, separators[ \n\n, \n, . , , ] ) ​ docs text_splitter.split_documents(docs_raw) ​ print(f切分后的 chunk 数量: {len(docs)}) ​ if not docs: raise ValueError(文本切分后没有得到 chunk请检查 PDF 内容。) ​ print(\n第一个 chunk 预览:) print(docs[0].page_content[:500]) ​ return docs ​ ​ # # 4. 清洗 metadata避免 Chroma metadata 类型报错 # ​ def clean_metadata(metadata: Dict[str, Any]) - Dict[str, Any]: Chroma 的 metadata 只适合保存 str、int、float、bool、None。 遇到复杂对象就转成字符串。 cleaned {} ​ for key, value in metadata.items(): if isinstance(value, (str, int, float, bool)) or value is None: cleaned[key] value else: cleaned[key] str(value) ​ return cleaned ​ ​ # # 5. 构建 Chroma 向量数据库 # ​ def build_vectorstore(docs): print(\n 3. 加载 Embedding 模型 ) print(fEmbedding 模型: {EMBEDDING_MODEL_NAME}) ​ embedding_model HuggingFaceEmbeddings( model_nameEMBEDDING_MODEL_NAME, encode_kwargs{ normalize_embeddings: True } ) ​ print(\n 4. 构建 Chroma 向量数据库 ) ​ texts: List[str] [] metadatas: List[Dict[str, Any]] [] ids: List[str] [] ​ for i, doc in enumerate(docs): content doc.page_content.strip() ​ if not content: continue ​ texts.append(content) ​ metadata clean_metadata(doc.metadata) metadata[chunk_index] i metadata[source_file] PDF_PATH ​ metadatas.append(metadata) ids.append(fchunk_{i}_{uuid.uuid4().hex}) ​ if not texts: raise ValueError(没有可写入向量数据库的文本 chunk。) ​ # 关键修复 # 不使用 Chroma.from_documents() # 因为你的 langchain_chroma 版本会访问 doc.id旧 Document 没有 id 属性 vectorstore Chroma.from_texts( textstexts, embeddingembedding_model, metadatasmetadatas, idsids, collection_namefhf_rag_pdf_demo_{uuid.uuid4().hex[:8]} ) ​ print(向量数据库构建完成。) print(f写入 chunk 数量: {len(texts)}) ​ return vectorstore ​ ​ # # 6. 检索相关文档 # ​ def retrieve_context(vectorstore, query: str, k: int 3) - Tuple[str, list]: results vectorstore.similarity_search(query, kk) ​ print(\n 检索到的相关 chunk ) ​ for idx, doc in enumerate(results, start1): print(\n - * 80) print(fChunk {idx}) print(metadata:, doc.metadata) print(doc.page_content[:800]) ​ context \n\n.join( [ f[文档片段 {idx}]\n{doc.page_content} for idx, doc in enumerate(results, start1) ] ) ​ return context, results ​ ​ # # 7. 调用 HuggingFace LLM 生成回答 # ​ def extract_response_text(response) - str: 兼容不同 huggingface_hub 返回对象格式。 message response.choices[0].message ​ if isinstance(message, dict): return message.get(content, ) ​ return message.content ​ ​ def ask_llm_with_rag(client: InferenceClient, question: str, context: str) - str: system_prompt ( 你是一个严谨的 RAG 文档问答助手。 你必须优先基于给定的文档上下文回答问题。 如果上下文中没有答案请回答根据当前文档内容无法确定。 不要编造文档中没有的信息。 ) ​ user_prompt f请基于以下文档上下文回答问题。 ​ 文档上下文 {context} ​ 用户问题 {question} ​ 请用中文回答回答要清晰、准确、简洁。 ​ last_error None ​ for model_name in LLM_MODEL_CANDIDATES: try: print(f\n正在调用 LLM: {model_name}) ​ response client.chat_completion( modelmodel_name, messages[ { role: system, content: system_prompt }, { role: user, content: user_prompt } ], max_tokens512, temperature0.1, ) ​ return extract_response_text(response) ​ except Exception as e: last_error e print(f模型 {model_name} 调用失败尝试下一个模型。) print(type(e).__name__, e) ​ raise RuntimeError(f所有候选 LLM 都调用失败。最后一次错误{last_error}) ​ ​ # # 8. 主程序 # ​ def main(): print( * 80) print(HuggingFace LangChain Chroma RAG Demo) print( * 80) ​ client create_hf_client() ​ docs load_and_split_pdf(PDF_PATH) ​ vectorstore build_vectorstore(docs) ​ print(\n * 80) print(RAG 系统已就绪) print(输入 exit / quit / q 退出) print( * 80) ​ while True: question input(\n请输入你的问题).strip() ​ if question.lower() in [exit, quit, q]: print(已退出。) break ​ if not question: continue ​ try: context, retrieved_docs retrieve_context( vectorstorevectorstore, queryquestion, k3 ) ​ answer ask_llm_with_rag( clientclient, questionquestion, contextcontext ) ​ print(\n RAG 最终回答 ) print(answer) ​ except Exception as e: print(\n运行失败) print(type(e).__name__, e) ​ ​ if __name__ __main__: main()本地加载6. RAG实际场景中的挑战和见解内容缺失“RAG系统”在处理未知或无法回答问题的情况。最佳情况下它会诚实地回应“对不起我不知道”。但是当遇到与内容相关但没有明确答案的问题时这个系统可能会被误导给出一个错误的或不准确的回答。个人观点prompt写的尽可能好一些详细一些。排序问题信息检索系统在实际操作中只能返回TOPK排名的文档从而导致一些实际上含有正确答案的文档被遗漏。K值的选取个人观点多查询检索器、集成检索器权重需要调整、父文档检索整合策略限制即在某些情况下即使检索系统能从数据库中找到包含正确答案的文档这些文档也可能没有被有效地纳入用于生成答案的上下文中。个人观点上下文压缩提取不到有用信息在信息密集或复杂的文本中进行准确信息提取的挑战。在这些情况下即使相关信息就在模型可访问的文本中模型也可能因为无法有效过滤或解读这些信息而未能提取正确答案。个人观点上下文压缩、上下文重排错误格式、问题的具体性、回答的不完整问题需要以特定格式(如表格或列表)提取信息但大型语言模型忽略了这一指令。回答虽然被返回但具体程度不够或过于具体无法满足用户需求。这种情况发生在RAG系统设计者对特定问题有预期结果时比如教育内容的具体性。当用户不确定如何提问问题过于泛泛时也可能出现不正确的具体性。回答虽然不是错误的但遗漏了一些信息尽管这些信息在上下文中可供提取。例如当被问及“文档A、B和C中的关键点是什么?”时更好的方法是分别提出这些问题。7. 如何利用RAGAs评估RAG系统的好坏RAGAS是一个用于评估RAG系统的框架它允许在不依赖人工注释的情况下通过一套指标评估检索模块和生成模块的性能及其生成质量。8. RAG 优化元素提问在标准 RAG 中用户输入一个问题后系统通常直接拿这个问题去向量库中检索相关文档。但真实场景里用户问题往往存在几个问题用户表达可能太短、太模糊导致检索不到相关内容 用户问题可能包含多个子问题直接检索会混在一起 用户的问题可能和文档中的表述方式不一致导致语义匹配失败 用户可能连续追问当前问题依赖上一轮上下文。所以 RAG 系统通常会在检索前增加一个Query Optimization / Query Transformation阶段对用户问题进行改写、扩展、分解或压缩。Multi Query 多查询策略在同一维度根据用户输入的问题生成多个子问题对同一问题生成多个视角的提问然后依次进行检索最后将检索到的文档合并返回。流程用户问题 ↓ LLM 生成多个改写问题 ↓ 每个问题分别检索 top-k 文档 ↓ 合并所有结果 ↓ 去重 ↓ 返回给生成器优点Multi Query 的优点是可以提高召回率。它尤其适合原始问题较短、表达不清、同义表达较多的场景。比如用户问这个方法有什么优势如果没有上下文这个问题很模糊。Multi Query 可以把它扩展成该方法相比传统方法的优势是什么 该方法在性能、效率和准确率方面有哪些改进 该方法解决了哪些已有方法的问题这样检索范围更广。缺点缺点是会增加检索次数和成本。比如原来只检索一次现在生成 5 个 query每个 query 检索 top-5那么最多会得到 25 个候选文档。后续还需要去重、排序、过滤。所以 Multi Query 通常适合在对召回率要求较高的场景使用。RAG Fusion 多查询结果融合策略RAG Fusion 和Multi Query Retriever 基于同样的思路在Multi Query 多查询策略生成子问题并检索的基础上它对检索结果执行倒数排名融合(Reciprocal Rank FusionRRF)算法使得检索效果更好。Decomposition 问题分解策略Decomposition 是把一个复杂问题拆成多个简单子问题然后分别处理。Answer recursively 递归回答先回答第一个子问题然后把第一个问题的答案作为上下文再回答第二个问题以此类推。递归回答流程复杂问题 ↓ 拆成子问题 Q1, Q2, Q3 ↓ 回答 Q1 得到 A1 ↓ 用 Q2 A1 检索并回答得到 A2 ↓ 用 Q3 A1 A2 检索并回答 ↓ 合成最终答案适用场景递归回答适合前后依赖明显的问题。缺点缺点是如果前一个子问题回答错了后面的答案会受到影响形成错误传播。Answer individually 独立回答把复杂问题拆成多个子问题每个子问题独立检索、独立回答最后合并答案。独立回答流程复杂问题 ↓ 拆成 Q1, Q2, Q3 ↓ Q1 检索并回答 Q2 检索并回答 Q3 检索并回答 ↓ 整合 A1, A2, A3 ↓ 最终答案适用场景独立回答适合并列结构的问题。这类问题每个部分相互独立适合并行检索和回答。优点结构清晰不容易混淆可以并行处理效率较高每个子问题检索更精准。缺点如果子问题之间存在依赖关系独立回答可能忽略逻辑联系。Step Back 问答回退策略先从用户的具体问题中抽象出一个更高层、更一般的问题再用这个后退问题帮助理解和检索。两个关键步骤① Abstraction 抽象把具体问题转成更一般的问题。例如原问题HyDE 为什么可以改善向量检索 后退问题生成式查询扩展如何改善语义检索再比如原问题RAGAS 的 Faithfulness 怎么计算 后退问题如何衡量生成答案是否忠实于上下文② Reasoning 推理系统先基于后退问题检索和理解相关原理再结合原始问题推理出答案。流程用户原始问题 ↓ LLM 生成后退问题 ↓ 检索后退问题相关文档 ↓ 得到基础原理或上位概念 ↓ 再结合原始问题 ↓ 生成最终答案适用场景Step Back 适合复杂推理问题尤其是用户问“为什么”“原理是什么”“如何理解”这类问题。优点Step Back 可以减少模型被具体问题表述限制的情况让模型先获得更高层次的概念框架。对于解释型问题、原理型问题很有帮助。缺点如果后退问题生成得太宽泛可能检索到不够具体的文档如果后退问题偏离原意反而会降低检索质量。HyDE (Hypothetical Document Embeddings) 假设性文档嵌入普通 RAG 是直接把用户 query 向量化然后去检索文档。HyDE 不直接用用户问题去检索而是先让 LLM 根据用户问题生成一段“假设答案文档”再把这段假设文档向量化用它去检索真实文档。用户问题 ↓ LLM 生成假设性答案文档 ↓ 对假设文档做 embedding ↓ 检索真实文档为什么这样有效因为用户问题通常很短而文档内容通常比较长。短 query 和长文档之间可能存在语义不匹配。这段假设文档更像真实文档中的表达方式。用它去做 embedding可能更容易匹配到相关文档。HyDE 流程用户问题 ↓ LLM 生成 hypothetical document ↓ embedding hypothetical document ↓ 向量库检索 ↓ 返回真实文档 ↓ LLM 基于真实文档回答HyDE 适用场景用户问题很短 文档内容比较专业 query 和文档表达差异较大 embedding model 对短问题检索效果不好HyDE 的风险风险 1没有上下文时LLM 可能误解原问题例如用户问What is Bel?如果文档中 Bel 指的是 Paul Graham 文章中提到的一种编程语言但 LLM 可能会误以为 Bel 是物理学单位、人物、地名或其他概念。那么 HyDE 生成的假设文档就会偏离真实含义导致检索错误文档。风险 2开放式问题容易生成带偏见的假设文档比如用户问What would the author say about art vs engineering?这个问题本身比较开放。LLM 可能根据自己的先验生成一段看似合理但并不符合文档作者观点的假设答案。这样会导致检索结果偏向 LLM 自己生成的方向而不是文档真实内容。使用建议HyDE 适合用在“检索阶段”但最终答案必须仍然基于真实检索到的文档而不是基于 HyDE 生成的假设文档。也就是说HyDE 生成的文本只是为了帮助检索HyDE 文档不是最终证据 真实检索文档才是最终依据其他方法查询重写(Query Rewriting)把用户原始问题改写成更清晰、更完整、更适合检索的形式。查询压缩(Query Compression)把用户问题或对话历史压缩成一个简洁、完整、适合检索的问题。

相关新闻