1. 从“死记硬背”到“开卷考试”为什么你的AI应用需要RAG如果你已经跟着这个系列走到了第八篇恭喜你这意味着你的AI应用开发之路已经进入了深水区。前面我们聊了模型调用、提示工程、函数调用甚至用LangChain搭了个简单的流程。但不知道你有没有遇到过这样的尴尬你问大模型一个非常具体、非常专业的问题比如“我们公司去年Q3在华东区的销售政策是什么”或者“帮我总结一下这份三百页技术白皮书第三章的核心论点”模型要么开始一本正经地胡说八道要么就坦诚地告诉你“我的知识截止于XXXX年X月无法回答”。这种感觉就像你请了一个记忆力超群但只读过通用百科全书的学霸去解答你们公司内部的机密考题他当然会抓瞎。这就是传统大模型应用的“知识天花板”和“幻觉”问题。模型在训练时学到的知识是静态的、通用的它不可能知道你私有数据库里每一份文档的细节。而RAG正是解决这个问题的“金钥匙”。你可以把它理解成给大模型配了一个强大的“外部记忆库”和一套高效的“检索系统”。当用户提问时系统不是让模型凭记忆硬想而是先去这个记忆库里快速找到最相关的资料然后把“问题资料”一起交给模型让它基于这些确凿的证据来生成答案。这就像从“闭卷考试”变成了“开卷考试”答案的准确性和可靠性有了质的飞跃。RAG的全称是Retrieval-Augmented Generation即检索增强生成。它的核心流程可以概括为三步索引、检索、生成。首先把你的私有知识文档、PDF、数据库记录等进行预处理切成小块转换成向量一种数学表示存进向量数据库这叫“索引”。然后当用户提问时把问题也转换成向量去向量数据库里寻找最相似的向量块这叫“检索”。最后把这些检索到的文本块作为上下文连同原始问题一起送给大模型让它生成最终答案这叫“生成”。而向量数据库就是整个RAG系统中承上启下的“记忆心脏”它负责高效存储和检索这些海量的向量数据。所以今天我们就来彻底搞懂这个“心脏”——向量数据库并亲手用它搭建一个可运行的RAG系统。我们会选择FAISS这个轻量高效的库作为核心用all-MiniLM-L6-v2模型来生成向量完成从文档处理到智能问答的全流程。你会发现有了这套组合拳你的AI应用才能真正“落地”解决实际业务问题。2. 向量数据库不只是存储更是理解的桥梁在深入代码之前我们必须先搞清楚向量数据库到底是什么以及它为什么在RAG中不可替代。这绝不是简单的“另一个数据库”。想象一下你要在图书馆里找一本关于“深度学习优化算法”的书。如果你只知道书名那很简单去查书名目录就行。但如果你只有一段模糊的描述“一本讲怎么让神经网络训练得更快、更稳的书”传统的数据库基于关键词匹配就傻眼了。它可能只能匹配到“神经网络”、“训练”这些词而漏掉了那些书名里没有这些词但内容高度相关的书比如《Adam: A Method for Stochastic Optimization》这篇论文的解析。向量数据库的解决思路很巧妙它不直接比较文字而是比较文字的“语义”。通过一个叫做“文本嵌入模型”的东西把一段文本无论是问题还是文档转换成一个固定长度的数字列表也就是向量。这个向量在高维空间比如384维、768维中有一个特定的位置。关键之处在于语义相似的文本它们的向量在空间里的位置也会很接近。比如“猫”和“猫咪”的向量距离会很近“猫”和“汽车”的向量距离会很远。向量数据库的核心工作就是两件1. 存储这些向量和它们对应的原始文本或其他数据。2. 当给定一个查询向量时能以极快的速度找到库中与它最相似的K个向量即最近邻搜索。FAISSFacebook AI Similarity Search就是专门为这个“最近邻搜索”任务而生的库它用了各种算法如IVF、HNSW和量化技术让在海量向量中做快速检索成为可能。那么在RAG的语境下向量数据库解决了什么痛点呢首先是语义搜索。用户问“训练模型时loss不下降怎么办”系统能检索到关于“梯度消失”、“学习率调整”、“过拟合”的文档段落而不需要这些段落里必须包含“loss不下降”这个词组。其次是效率。当你有百万甚至千万级的文档片段时线性比对每一个向量是不现实的。FAISS这类库通过建立索引将搜索复杂度从O(N)降低到O(logN)甚至更低。最后是灵活性。向量可以来自文本、图片、音频这意味着你可以构建跨模态的检索系统。这里有一个非常重要的实操心得向量模型的选择直接决定了你的RAG系统的“智商”上限。如果你用一个很弱的嵌入模型即使后端用上最牛的向量数据库和LLM检索回来的东西也是牛头不对马嘴后续生成自然就是垃圾进、垃圾出。我们这次选择的all-MiniLM-L6-v2是一个在速度和效果上取得很好平衡的模型它由Sentence-BERT训练能将句子映射到384维的向量空间并且足够轻量适合本地部署和快速实验。3. 环境搭建与核心工具选型为什么是FAISS和MiniLM工欲善其事必先利其器。在开始构建RAG管道之前我们需要把核心的轮子准备好。这里的每一个选择背后都有其考量。首先安装必要的Python库。我们将使用langchain来编排整个流程因为它提供了非常好的抽象让RAG的构建变得模块化。sentence-transformers库则封装了我们需要的文本嵌入模型。faiss-cpu是FAISS的CPU版本对于学习和中小规模数据量完全够用。pypdf用来解析我们的示例PDF文档。pip install langchain langchain-community sentence-transformers faiss-cpu pypdf为什么选择FAISS而不是其他向量数据库市面上向量数据库很多有Milvus、Pinecone、Weaviate、Qdrant等等。对于入门和轻量级应用我强烈推荐从FAISS开始原因有三第一简单纯粹。FAISS就专注于一件事相似性搜索。它没有那些花哨的外围功能概念清晰易于理解。第二零依赖本地运行。它是一个库不是服务pip install后直接集成在你的Python代码里不需要额外部署数据库服务调试和开发的心智负担极小。第三性能强劲。由Facebook AI Research出品其索引算法如IVF、HNSW经过了充分优化在CPU上也能有不错的表现。当然如果你的数据量达到亿级或者需要分布式、高可用那么Milvus这类专业向量数据库是更好的选择。但对于我们“15天学会”的目标和大多数初期应用FAISS是绝佳的起点。为什么选择 all-MiniLM-L6-v2 模型嵌入模型的选择是一个权衡游戏。大的模型如text-embedding-ada-002的替代品BGE-large效果更好但更慢、更耗资源。all-MiniLM-L6-v2是一个“甜点”模型它只有约80M参数速度快在CPU上编码一个句子也只需几十毫秒效果在同类小模型中名列前茅特别擅长句子级别的语义相似度任务而这正是RAG最核心的需求。它生成的384维向量在保证信息量的同时也使得FAISS索引更小、检索更快。在资源受限或需要快速迭代验证想法的场景下它是我的首选。注意模型第一次运行时会从Hugging Face Hub下载需要网络环境。如果下载慢可以尝试配置镜像源。模型文件大约几百MB请预留好磁盘空间。接下来我们准备一份示例文档。为了模拟真实场景我建议你找一个自己领域的技术PDF或者就用一篇维基百科文章保存为文本文件。这里假设我们有一个名为knowledge.pdf的文件里面包含了一些关于机器学习的基础知识。4. 实战第一步文档加载、切分与向量化有了工具我们就可以开始处理知识源了。这一步的目标是把非结构化的文档变成一块块适合检索的“知识碎片”并把它们转换成向量存入FAISS索引。4.1 文档加载与文本分割文档加载器Document Loader负责从各种来源PDF、Word、网页、数据库读取数据。LangChain提供了丰富的加载器。这里我们使用PyPDFLoader。文本分割器Text Splitter是RAG中极其关键又容易被忽视的一环。你不能把整本100页的PDF当成一个向量存进去那样检索回来的永远是整本书毫无意义。也不能切得太碎比如按句子切那样会丢失上下文信息。我们的目标是切分成有语义完整性的“块”Chunk。from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 1. 加载文档 loader PyPDFLoader(./knowledge.pdf) documents loader.load() print(f加载了 {len(documents)} 页文档。) # 2. 分割文本 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个块的最大字符数 chunk_overlap100, # 块与块之间的重叠字符数 length_functionlen, separators[\n\n, \n, 。, , , , , , ] # 分割符优先级 ) chunks text_splitter.split_documents(documents) print(f将文档切分成了 {len(chunks)} 个文本块。)关键参数解析与避坑指南chunk_size500这个值需要根据你的嵌入模型和文档内容调整。all-MiniLM-L6-v2针对句子优化但也能处理一定长度的段落。500-800字符是一个常用的范围能容纳2-3个自然段。太短则信息不全太长则检索精度下降且会消耗更多的大模型上下文窗口。chunk_overlap100重叠是避免语义断裂的灵魂想象一下一个重要的概念正好在两个块的分割点上被一刀切断那么检索时无论命中哪个块信息都是不完整的。设置重叠比如50-150字符能确保关键信息有更高的概率被完整地包含在某个块中。这有点像阅读时用两指重叠着翻书保证内容的连续性。separators分割符列表定义了切分的优先级。RecursiveCharacterTextSplitter会优先用\n\n双换行通常代表段落来切如果切出来的块还是太大就用\n单换行继续切以此类推。这个策略通常比简单的按固定长度切割要好得多。一个常见的坑是盲目使用默认参数。对于技术文档段落结构清晰可以用\n\n对于对话记录可能用句号分割更合适。务必打印出前几个chunks的内容检查一下确保分割结果是“人读起来也通顺”的。4.2 文本向量化与FAISS索引创建现在我们有了干净的文本块下一步就是将它们转化为向量并创建FAISS索引。from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS # 1. 初始化嵌入模型 embeddings HuggingFaceEmbeddings( model_namesentence-transformers/all-MiniLM-L6-v2, model_kwargs{device: cpu}, # 使用CPU如需GPU可改为cuda encode_kwargs{normalize_embeddings: True} # 归一化向量有利于余弦相似度计算 ) # 2. 从文本块创建向量存储索引 vectorstore FAISS.from_documents(documentschunks, embeddingembeddings) # 3. 保存索引到本地方便下次直接加载无需重新处理文档 vectorstore.save_local(./faiss_index) print(FAISS索引已创建并保存到本地文件夹 faiss_index。)这段代码做了几件重要的事HuggingFaceEmbeddings封装了sentence-transformers模型我们指定使用all-MiniLM-L6-v2。normalize_embeddingsTrue会将向量归一化为单位长度此时使用点积dot-product计算相似度就等价于余弦相似度这是最常用的度量方式。FAISS.from_documents是LangChain提供的一个非常便捷的方法。它内部自动做了遍历每个chunk调用嵌入模型生成向量然后将(向量, 文本块)对添加到FAISS索引中。将索引保存到磁盘。这是一个好习惯因为文档处理和向量化通常是耗时操作。一旦索引创建好后续的检索就可以直接加载这个索引文件速度极快。这里有一个至关重要的经验点FAISS.from_documents默认使用的索引类型是IndexFlatL2L2距离即欧氏距离。对于归一化后的向量使用IndexFlatIP点积并设置metricfaiss.METRIC_INNER_PRODUCT在理论上更匹配。但在实际使用中由于我们的向量已经归一化L2距离和点积在排序上是单调相关的所以结果差异很小。LangChain为了通用性默认用了L2。如果你对精度有极致要求可以深入研究FAISS的索引配置但对于绝大多数应用默认设置已经足够优秀。5. 构建检索链从提问到获取上下文索引建好了我们就拥有了一个“知识库”。现在需要构建一个检索器Retriever它负责接收用户问题并返回最相关的文档块。# 加载之前保存的索引 vectorstore FAISS.load_local(./faiss_index, embeddings, allow_dangerous_deserializationTrue) # 创建检索器 retriever vectorstore.as_retriever(search_kwargs{k: 4}) print(检索器已创建将返回最相关的4个文档块。)allow_dangerous_deserializationTrue这是一个安全警告。因为加载的索引文件可能包含恶意代码所以需要显式启用。只要你确定索引文件是自己生成的就可以放心打开。search_kwargs{k: 4}这里设置了检索器返回的文档块数量。k值是一个需要调优的超参数。太少可能信息不足太多可能会引入噪声并且会占用大量LLM的上下文窗口Token数。一般从3-5开始尝试。检索器的工作原理是当你调用retriever.get_relevant_documents(你的问题)时它会使用相同的embeddings模型将你的问题转换成查询向量。将这个查询向量传入FAISS索引执行最近邻搜索。返回与查询向量最相似的k个向量所对应的原始文本块。我们可以简单测试一下检索效果query 什么是过拟合 relevant_docs retriever.get_relevant_documents(query) print(f针对问题 {query}检索到 {len(relevant_docs)} 个相关文档块) for i, doc in enumerate(relevant_docs): print(f\n--- 片段 {i1} (相关性分数: {doc.metadata.get(score, N/A)}) ---) print(doc.page_content[:300] ...) # 打印前300字符如果我们的知识库PDF里确实有讲过拟合的内容那么返回的文档块应该包含相关描述。你可能会注意到doc.metadata里有一个score字段它代表了FAISS计算出的距离分数通常是L2距离越小越相似。这个分数可以帮助我们做后续的阈值过滤比如只接受分数低于某个阈值的检索结果以提升精度。6. 组装完整RAG流程让LLM基于证据回答检索到了相关上下文最后一步就是请大模型当“法官”基于这些证据来组织语言生成最终答案。这里我们需要一个大模型。为了本地运行方便我们可以使用Ollama来运行一个开源模型比如qwen2.5:7b。当然你也可以使用OpenAI、通义千问等云端API。首先确保你安装了Ollama并拉取了模型ollama pull qwen2.5:7b。然后我们用LangChain将检索器和LLM组装成一条链from langchain_community.llms import Ollama from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate # 1. 初始化本地LLM llm Ollama(modelqwen2.5:7b, temperature0.1) # temperature调低让答案更确定 # 2. 定义提示词模板 prompt_template 请根据以下上下文信息来回答问题。如果上下文信息不足以回答问题请直接说“根据提供的信息我无法回答这个问题”不要编造信息。 上下文信息 {context} 问题{question} 请给出专业、准确的回答 PROMPT PromptTemplate( templateprompt_template, input_variables[context, question] ) # 3. 创建RetrievalQA链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, # 最简单的方式将所有检索到的上下文“塞”进提示词 retrieverretriever, chain_type_kwargs{prompt: PROMPT}, return_source_documentsTrue # 非常重要返回检索到的源文档便于追溯和调试 ) # 4. 进行问答 query 在机器学习中如何避免过拟合 result qa_chain.invoke({query: query}) print(f问题{query}) print(f\n答案{result[result]}) print(f\n 检索到的源文档 ) for i, doc in enumerate(result[source_documents]): print(f\n--- 源文档 {i1} ---) print(f内容摘要{doc.page_content[:200]}...) print(f来源{doc.metadata})逐段解析与核心技巧LLM初始化temperature0.1设置了一个较低的“创造力”值对于事实性问答我们希望答案稳定、可靠低温度值可以减少随机性。提示词工程这里的提示词模板是RAG成功的关键。它明确指令模型“根据以下上下文信息”并给出了无法回答时的处理方式。这能有效抑制模型幻觉。{context}和{question}是占位符会被LangChain自动替换。Chain类型chain_typestuff是最直接的方式它把所有检索到的文档内容拼接起来放入提示词的{context}中。这种方式简单有效但受限于LLM的上下文长度。如果检索到的文档总长度超限就需要考虑map_reduce、refine等更复杂的方式它们会先对每个文档单独总结再合并总结。返回源文档return_source_documentsTrue这个参数务必打开它让你能知道答案是基于哪几段原文生成的。这对于调试、验证答案准确性、建立用户信任至关重要。如果答案看起来不对你可以立刻检查是不是检索错了或者上下文本身就有问题。运行这段代码你会看到模型在检索到的关于“过拟合”的文档片段基础上生成了一段如何避免过拟合的总结性回答。这就是一个完整的、可工作的RAG系统7. 超越基础RAG系统优化的核心思路一个能跑起来的RAG只是起点要让它在生产环境中真正可靠、好用还有大量的优化工作要做。这部分是区分“玩具”和“产品”的关键。7.1 检索质量优化召回与精排基础的向量相似度检索稠密检索有时会漏掉一些关键词匹配很重要但语义不那么接近的文档。一个成熟的方案是“混合检索”。稀疏检索如BM25基于关键词匹配擅长处理实体、术语、缩写等精确匹配。稠密检索向量检索基于语义相似度擅长处理同义替换、概念关联。将两者的结果融合比如按分数加权、取并集或再排序能显著提升召回率。LangChain内置了EnsembleRetriever来支持这个功能。from langchain.retrievers import BM25Retriever, EnsembleRetriever from langchain_community.retrievers import TFIDFRetriever # 另一种稀疏检索 # 假设我们有一个文档列表 texts 是纯字符串列表 # bm25_retriever BM25Retriever.from_texts(texts) # ensemble_retriever EnsembleRetriever(retrievers[vector_retriever, bm25_retriever], weights[0.7, 0.3])更进一步的可以在召回一批文档比如20个后引入一个“重排序”模型。这个模型通常是一个更精细的交叉编码器Cross-Encoder它同时看问题和文档给出一个更精确的相关性分数然后对召回的文档进行重新排序把最相关的3-5个送到LLM。这能极大提升最终答案的质量。7.2 上下文管理与提示优化当检索到的文档很多时如何把它们有效地塞进LLM有限的上下文窗口Stuffing最简单直接拼接。适合文档少且短的情况。Map-Reduce先将每个文档单独送给LLM进行摘要Map然后将所有摘要再送给LLM合成最终答案Reduce。适合文档多且长但可能丢失细节。Refine迭代处理。用第一个文档生成初始答案然后用第二个文档去精炼这个答案依次类推。质量可能更高但速度慢。Contextual Compression在将文档送入LLM前先用一个更小的模型或规则过滤掉其中不相关的句子只保留最核心的部分。这能节省宝贵的Token。在提示词方面除了基础的指令还可以加入角色设定“你是一个严谨的技术专家...”输出格式要求“请用分点列表的形式回答。”引用要求“在答案后注明引用的文档编号。”7.3 评估与迭代你的RAG系统真的好吗这是最容易被忽略的一环。你需要一套评估体系来衡量RAG的效果。可以从以下几个维度入手检索质量评估命中率对于一组测试问题检索到的Top-K文档中至少包含正确答案片段的比例是多少平均排名正确答案片段在检索结果中的平均位置排名是多少生成质量评估忠实度生成的答案是否严格基于提供的上下文有没有胡编乱造可以用另一个LLM来判断答案相关性答案是否直接回答了问题信息完整性答案是否涵盖了上下文中的所有关键点你可以构建一个包含“问题”、“标准答案”、“相关文档ID”的测试集然后自动化地运行你的RAG管道用上述指标进行评估。只有通过量化评估你才知道优化是有效的。7.4 工程化与部署考量当你的RAG系统准备上线时需要考虑索引更新知识库文档新增、修改、删除后如何增量更新FAISS索引全量重建成本很高。FAISS本身不支持增量更新一种常见做法是记录每个向量的元数据新增时直接添加删除时做软删除过滤掉定期再全量重建。多模态检索如果你的知识库包含图片、表格可以考虑使用多模态嵌入模型如CLIP将图片和文本映射到同一向量空间实现“用文字搜图片”或“用图片搜文字”。Agentic RAG这是更高级的模式。让AI Agent来主导RAG过程例如它可以根据初步检索结果判断信息是否足够如果不够它可以自主地生成新的、更明确的搜索查询再次检索或者决定调用其他工具如计算器、搜索引擎来补充信息。构建一个健壮的RAG系统是一个持续迭代的过程。从最简单的向量检索开始逐步引入混合检索、重排序、更好的分块策略、更智能的提示词并建立评估闭环你的AI应用才会越来越聪明、越来越可靠。