
1. 项目概述为什么我们需要RAG如果你最近在折腾大语言模型肯定对“幻觉”这个词不陌生。你问它一个具体问题它可能给你编造一个看似合理但完全错误的答案比如把不存在的论文作者、错误的历史日期或者虚构的产品功能说得头头是道。这种“一本正经地胡说八道”是当前LLM大语言模型面临的核心挑战之一根源在于其生成机制是基于概率预测下一个词而非基于一个确凿的知识库进行推理。那么有没有一种方法能让LLM在回答问题时像我们人类查资料一样先“翻翻书”、“查查数据库”找到确凿的依据再开口呢这就是RAGRetrieval-Augmented Generation检索增强生成要解决的问题。简单来说RAG就是给LLM配了一个“外挂大脑”——一个可以实时检索的外部知识库。当用户提问时系统不是让LLM凭空想象而是先从知识库中找到最相关的文档片段然后把“问题”和“找到的资料”一起喂给LLM让它基于这些真实、准确的资料来生成答案。这听起来是不是比让LLM“裸奔”要靠谱得多RAG技术正在成为构建企业级AI应用、智能客服、知识问答系统的标配。它不仅能大幅减少幻觉还能让LLM的知识不再局限于其训练数据截止日期通过更新知识库就能获得最新信息。今天我们就抛开那些高大上的概念从第一性原理出发手把手拆解RAG的核心组件、工作流程并用代码实现一个最简版本让你彻底搞懂它到底是怎么工作的。2. RAG核心原理深度拆解不只是“搜索生成”很多人把RAG简单理解为“先用向量数据库搜一下再把结果扔给LLM”。这种理解只对了一半而且忽略了其中大量的工程细节和设计哲学。一个健壮的RAG系统其核心是一个精心设计的管道Pipeline主要包含以下四个关键环节每一个环节都藏着魔鬼。2.1 文档加载与预处理给知识“切片”的艺术你的知识源可能是PDF、Word、网页、数据库甚至是音频转录的文本。第一步就是把这些异构数据统一加载成纯文本。这里常用的工具有PyPDF2处理PDF、python-docx处理Word、BeautifulSoup解析网页等。但加载进来只是开始真正的挑战在于预处理尤其是文本分割。为什么不能把整本书直接扔给检索系统原因有二1) 上下文长度限制LLM和检索模型都有输入长度上限2) 检索精度大段文本包含多个主题会稀释关键信息的“浓度”导致检索不准。文本分割的核心是在保持语义连贯性的前提下将长文本切分成大小适中的片段Chunk。这里有几种常见策略固定长度重叠分割这是最基础的方法。比如设定每个chunk为500个字符重叠100个字符。重叠是为了防止一个完整的句子或关键信息被硬生生切在两段之间导致上下文断裂。LangChain里的RecursiveCharacterTextSplitter就是基于这个思想的增强版。基于语义分割更高级的方法利用句子嵌入模型计算句子间的语义相似度在语义发生较大转变的地方进行切割。这能更好地保证每个chunk的主题一致性。基于特定分隔符分割对于Markdown、代码等结构清晰的文本可以按标题#、代码块等自然分隔符来切分。实操心得chunk大小没有黄金标准。太小如50字会丢失上下文太大如2000字会引入噪声并增加成本。对于通用文档256-512个token的chunk是一个不错的起点。重叠部分通常设为chunk大小的10%-20%。务必在分割后人工检查一些样本看看关键信息是否被割裂这是保证后续检索质量的基础。2.2 向量化与索引构建把文字变成“地图”这是RAG的“检索”核心。我们需要把上一步得到的文本chunk转换成计算机能高效理解和比对的形式——向量Embedding。Embedding模型就像一个“语义编码器”它把一段文本映射到一个高维空间比如768维中的一个点。这个点的位置即向量蕴含了这段文本的语义信息。语义相近的文本它们的向量在高维空间中的距离通常用余弦相似度或欧氏距离衡量也会很近。举个例子“如何训练一只狗”和“小狗的训练方法”这两个句子尽管用词不同但它们的向量应该非常接近。而“如何训练一只狗”和“今天的股市行情”的向量则应该相距甚远。选择Embedding模型是关键决策。开源领域BGEBAAI General Embedding、text2vec、Sentence Transformers系列都是优秀的选择。BGE系列如BGE-large-zh对中文语义理解尤其出色。你需要根据你的文本语言中/英和领域通用/专业来选择合适的模型。生成所有chunk的向量后我们将(chunk文本, 对应向量)这对数据存入向量数据库。这个过程叫做“建索引”。向量数据库如Chroma,Milvus,Qdrant,Weaviate专门为高效存储和检索高维向量而优化。它内部会使用近似最近邻ANN算法如HNSWHierarchical Navigable Small World或IVFInverted File Index让你能在毫秒级时间内从上百万条数据中找出与问题向量最相似的几个chunk。注意事项Embedding模型的质量直接决定检索的上限。如果模型不能很好理解你领域的专业术语那么检索效果会大打折扣。对于专业领域如法律、医疗有时需要对通用Embedding模型在自己的语料上进行微调或者直接使用领域内预训练的模型。2.3 检索与重排序找到最相关的“证据”当用户提出一个问题Query时RAG流程进入检索阶段查询向量化使用与建索引时同一个Embedding模型将用户问题也转换为一个向量。相似性搜索在向量数据库中搜索与问题向量最相似的K个文本chunk例如K5。数据库会返回这K个chunk及其相似度分数。可选重排序第一步的向量检索是“粗排”它可能因为语义表示的细微偏差而漏掉一些关键文档。重排序Re-ranking是一个“精排”步骤。它使用一个更精细、但通常也更耗计算资源的交叉编码器模型如bge-reranker对“问题”和每一个候选“chunk”进行两两深度交互计算得到一个更准确的相关性分数并据此对K个结果重新排序选出最顶部的N个NK例如N3作为最终证据。重排序能显著提升证据的质量尤其是当你的chunk数量很大或者问题很复杂时。它就像是先用人脸识别系统向量检索从千万人中找出100个相似者再让一个经验丰富的侦探重排序模型对这100人进行一对一仔细盘问最终锁定3个最可疑的。2.4 提示工程与生成让LLM“有据可依”这是最后一步也是LLM登场的时候。我们不能简单地把检索到的文档和问题拼接起来扔给LLM。我们需要通过提示词Prompt来精心设计任务指令。一个典型的RAG提示词模板如下请基于以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题请直接说“根据已知信息无法回答该问题”不要编造信息。 上下文信息 {context} 问题{question} 请根据上下文信息回答这里的{context}就是上一步检索并重排序后得到的最相关的N个文本chunk的拼接。{question}是用户原始问题。这个Prompt明确规定了LLM的行为边界答案必须严格来源于提供的上下文。这极大地约束了LLM的幻觉倾向。LLM如GPT-4、ChatGLM、Qwen等会基于这个“增强”后的提示词生成最终答案。核心技巧在{context}的每个chunk前加上来源标识如[文档1],[文档2]并在Prompt中要求LLM在回答时引用来源例如“根据[文档1]和[文档3]所述...”。这不仅能增加答案的可信度也便于后期追溯和验证是生产级RAG的必备实践。3. 从零搭建一个简易RAG系统代码实战理论说了一千遍不如动手做一遍。下面我们用一个最简化的例子使用LangChain一个流行的LLM应用开发框架和Chroma轻量级向量数据库来实现一个基于本地文档的问答系统。假设我们有一个knowledge_base.txt的文本文件作为知识库。3.1 环境准备与依赖安装首先确保你的Python环境建议3.8以上然后安装必要的库。pip install langchain langchain-community langchain-chroma pip install sentence-transformers # 用于BGE等Embedding模型 pip install pypdf2 beautifulsoup4 # 用于处理PDF和网页本例用txt但先装上 pip install chromadb # 向量数据库 # 如果你打算使用OpenAI的LLM还需要安装 openai 并配置API KEY # 本例为简化使用一个本地运行的轻量级LLM如Ollama或ChatGLM的API来模拟生成步骤。3.2 文档加载、分割与向量化我们创建一个rag_pipeline.py文件开始编写代码。# rag_pipeline.py import os from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载文档 loader TextLoader(./knowledge_base.txt, encodingutf-8) documents loader.load() print(f加载了 {len(documents)} 个文档) # 2. 分割文档 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个chunk大约500个字符 chunk_overlap100, # 重叠100个字符 length_functionlen, separators[\n\n, \n, 。, , , , , , ] # 中文友好的分隔符 ) chunks text_splitter.split_documents(documents) print(f将文档分割成了 {len(chunks)} 个文本块) # 3. 初始化Embedding模型 # 这里使用开源的BGE模型它不需要API Key在本地运行 embedding_model HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, # 选用一个小尺寸的BGE中文模型 model_kwargs{device: cpu}, # 使用CPU如果GPU可用可改为cuda encode_kwargs{normalize_embeddings: True} # 归一化向量方便计算余弦相似度 ) print(Embedding模型加载完毕。) # 4. 构建向量数据库索引 # persist_directory 指定索引持久化到本地目录下次可以直接加载无需重新计算向量 vector_db Chroma.from_documents( documentschunks, embeddingembedding_model, persist_directory./chroma_db # 索引保存路径 ) vector_db.persist() # 持久化保存 print(向量数据库索引构建并保存完毕。)这段代码完成了知识库的预处理和索引构建。RecursiveCharacterTextSplitter会尝试按你给出的分隔符列表递归地切割文本直到每个片段小于等于chunk_size同时保证重叠。BAAI/bge-small-zh-v1.5是一个优秀的中文语义表示模型体积小且效果不错。3.3 实现检索与问答链索引建好后我们就可以实现问答功能了。这里我们需要一个LLM来生成最终答案。为了演示的完整性我们假设使用一个通过API调用的LLM例如ChatGLM或DeepSeek。我们使用langchain的LLMChain和PromptTemplate来组织流程。# 续 rag_pipeline.py from langchain.prompts import PromptTemplate from langchain.chains import RetrievalQA from langchain_community.llms import Ollama # 示例使用本地Ollama运行的LLM # 或者使用OpenAI API # from langchain_openai import ChatOpenAI # 5. 定义Prompt模板 prompt_template 请严格根据以下提供的上下文信息来回答问题。如果上下文信息不足以回答问题请直接回答“根据已知信息无法回答该问题”不要编造任何信息。 上下文信息 {context} 问题{question} 请根据上下文信息给出答案 PROMPT PromptTemplate( templateprompt_template, input_variables[context, question] ) # 6. 初始化LLM # 方案A使用本地Ollama需提前安装Ollama并拉取模型如qwen2:7b llm Ollama(modelqwen2:7b, temperature0.1) # temperature调低减少随机性 # 方案B使用OpenAI API需设置环境变量OPENAI_API_KEY # llm ChatOpenAI(model_namegpt-3.5-turbo, temperature0.1) print(LLM初始化完毕。) # 7. 创建检索式问答链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, # 最简单的方式将所有检索到的文档“塞”进Prompt retrievervector_db.as_retriever(search_kwargs{k: 4}), # 检索4个最相关chunk chain_type_kwargs{prompt: PROMPT}, return_source_documentsTrue # 返回源文档便于追溯 ) # 8. 进行问答 def ask_question(question): print(f\n用户问题{question}) result qa_chain.invoke({query: question}) answer result[result] source_docs result[source_documents] print(f系统回答{answer}) print(\n--- 参考来源 ---) for i, doc in enumerate(source_docs[:2]): # 显示前两个来源 print(f[来源{i1}] {doc.page_content[:200]}...) # 截取部分内容展示 print(----------------\n) # 测试几个问题 if __name__ __main__: # 首先加载之前持久化的向量数据库如果已经存在 if os.path.exists(./chroma_db): print(检测到已有向量数据库直接加载...) vector_db Chroma( persist_directory./chroma_db, embedding_functionembedding_model ) # 需要更新qa_chain的retriever qa_chain.retriever vector_db.as_retriever(search_kwargs{k: 4}) ask_question(RAG技术主要为了解决什么问题) ask_question(文本分割时chunk重叠的作用是什么) ask_question(请解释一下量子计算的基本原理。) # 一个知识库中可能没有的问题在这个实现中RetrievalQA是LangChain提供的一个高层封装它把检索器retriever、Prompt模板和LLM串联起来。chain_typestuff是最直接的方式它把所有检索到的文档内容合并后放入Prompt。对于大量文档你可能需要考虑map_reduce或refine等更复杂但能处理更长上下文的方式。retriever.search_kwargs{k: 4}指定每次检索返回4个最相关的文档片段。return_source_documentsTrue让我们能拿到LLM做出回答所依据的原始文本这对于调试和建立用户信任至关重要。4. 进阶优化与生产级考量上面我们实现了一个最基础的RAG。但要把它用到实际项目中还需要考虑很多优化点。4.1 检索环节的增强策略基础的向量相似度检索并非万能。以下策略可以显著提升检索质量混合检索Hybrid Search结合稠密向量检索语义相似和稀疏向量检索关键词匹配如BM25。有些问题需要精确的关键词匹配如产品型号、错误代码而有些则需要语义理解如“如何提高客户满意度”。混合检索能兼顾两者。Chroma和Weaviate等数据库都支持混合检索。多向量检索为同一个文档chunk生成多种不同粒度的向量表示例如整体摘要向量、关键句子向量、命名实体向量。检索时综合多种表示的结果可以捕捉更丰富的信息。查询转换与扩展用户的问题可能表述不清晰。我们可以先用一个轻量级LLM对原始查询进行重写例如将“它怎么用”扩展为“RAG技术具体如何使用”或者生成多个相关的查询扩展然后用这些扩展后的查询去并行检索最后合并结果。4.2 生成环节的优化技巧即使检索到了对的文档LLM也可能“读不懂”或“用不好”。元数据过滤在构建索引时为每个chunk附加元数据如来源文件、章节、创建日期等。检索时可以添加元数据过滤器例如只检索2023年之后的文档让结果更精准。上下文压缩检索到的文档可能包含大量无关信息。可以在将上下文喂给LLM前先使用一个较小的模型进行摘要或提取只保留与问题最相关的部分从而节省LLM的上下文窗口并减少干扰。智能引用如前所述在Prompt中强制要求LLM引用来源段落。更高级的做法是让LLM在生成答案的每个陈述句后自动标注它所依据的源文档chunk编号。4.3 评估与迭代如何知道你的RAG好不好搭建完RAG系统后必须建立评估体系。不能只靠人工抽查。常见的评估维度包括检索相关性检索到的文档与问题的相关度如何可以用人工标注也可以用一些自动化指标如命中率。答案忠实度生成的答案是否严格基于提供的上下文有没有“夹带私货”幻觉这需要将答案与上下文进行比对。答案准确性基于上下文生成的答案其事实是否正确这通常需要领域专家判断。答案有用性答案是否清晰、完整地解决了用户的问题可以构建一个包含各种类型问题的测试集定期运行评估监控系统性能的变化。RAGAS、TruLens等是专门用于评估RAG系统的开源框架。5. 常见问题与实战排坑指南在实际开发和运维中你会遇到各种各样的问题。这里记录一些典型的“坑”和解决方案。5.1 检索效果不佳总是找不到对的文档可能原因1Embedding模型不匹配。你用的中文模型去编码英文文档或者通用模型处理高度专业术语如法律条文、医学病例。排查手动计算几个你知道应该相关的“问题-文档”对的余弦相似度看分数是否很低。解决更换或微调Embedding模型。对于中文BGE系列是首选。对于专业领域寻找领域内预训练模型或进行微调。可能原因2Chunk分割不合理。分割得太碎关键信息被割裂或者太大包含了太多无关噪声。排查检查检索返回的Top K个chunk内容看是否包含了答案的完整信息。解决调整chunk_size和chunk_overlap参数。尝试基于语义的分割器如SemanticChunker。可能原因3查询本身表述模糊。解决实施查询重写和扩展。例如使用一个轻量级LLM如GPT-3.5-turbo将用户问题改写成更利于检索的形式。5.2 LLM生成的答案忽略上下文依然胡编乱造可能原因1Prompt指令不够强硬。LLM可能更倾向于依赖自己的内部知识。解决强化Prompt指令。使用更严厉的措辞如“你必须且只能使用以下上下文信息”、“严禁使用上下文之外的知识”。在Prompt中提供遵循指令和违反指令的示例Few-shot Learning。可能原因2检索到的上下文质量太差或噪声太多。LLM被无关信息干扰了。解决引入重排序模型确保喂给LLM的是最精炼、最相关的1-2个chunk而不是一堆相关性一般的chunk。或者使用上下文压缩技术。可能原因3LLM自身能力或温度参数问题。解决尝试更强的LLM如从GPT-3.5升级到GPT-4。将temperature参数调至0或接近0如0.1减少生成的随机性。5.3 系统响应速度慢可能原因1Embedding模型推理慢。解决使用更小的Embedding模型如bge-smallvsbge-large或启用GPU加速或使用Embedding API服务但有网络延迟和成本。可能原因2向量数据库检索慢。排查索引规模是否过大ANN算法参数如HNSW的ef_search和M是否设置合理解决对于千万级以下的数据Chroma、Qdrant默认配置通常足够快。确保数据库运行在有足够内存的机器上。优化索引参数在召回率和速度间权衡。可能原因3LLM生成慢。解决这是主要瓶颈。考虑使用推理更快的模型如Qwen2.5-7B-Instruct或采用流式输出让用户先看到部分结果或对答案长度进行限制。5.4 知识库更新与版本管理业务知识是不断更新的。RAG系统需要支持知识库的增量更新。全量重建最简单但最耗时。每次更新都重新分割所有文档、计算向量、重建索引。适用于更新不频繁的场景。增量更新只对新文档或修改的文档进行处理将其chunk和向量添加到现有索引中。但需要注意如果新文档与旧文档内容冲突检索时可能会返回混合的结果。需要设计一套版本或元数据过滤机制。软删除与重新索引对于需要删除的内容通常不是直接从向量数据库物理删除可能影响索引结构而是通过元数据过滤将其排除在检索范围之外。定期进行索引优化和重建。构建一个健壮、高效的RAG系统是一个在检索精度、生成质量、响应速度和工程复杂度之间不断权衡和迭代的过程。它远不止是“向量搜索LLM”的简单拼接而是一个需要精心设计数据管道、算法策略和提示词工程的复杂系统。希望这篇从原理到实战的拆解能为你深入理解和应用RAG技术打下坚实的基础。记住从最简单的流程跑通开始然后针对你遇到的具体问题逐个环节进行优化和增强。