前一阵子在做知识库问答项目时,发现网上 RAG 相关的教程虽然很多,但大部分要么只讲原理、不给可运行代码,要么代码能跑但不解释为什么这么设计。对刚接触大模型应用开发的同学来说,很容易卡在“原理看懂了,一动手就报错”的阶段。这篇文章打算把 RAG(检索增强生成)从原理到落地完整梳理一遍。内容包括:RAG 要解决什么问题、整体流程如何拆解、向量库选型、召回质量优化,以及一个可以直接复制的知识库问答实战项目。零基础可以按顺序跟着做,有基础的可以直接跳到实战部分或常见问题排查。1. 什么是 RAG:背景与核心原理1.1 大模型的两个天然短板先看一个基础问题:为什么不能直接把文档丢给大模型,让它回答?因为大模型本身有两个明显局限:知识有时间截断。模型训练完成后,新产生的内容它不知道。比如你问它公司内部最新的值班安排,它只能“编”。无法访问私有数据。企业内部文档、产品手册、个人笔记这些数据不可能进入大模型训练集,模型自然无从回答。如果硬要让模型回答,它可能会用自己的“语言惯性”补全一个看起来合理、但实际错误的答案。这就是所谓的“一本正经地胡说八道”。1.2 RAG 的核心思路RAG 的全称是Retrieval-Augmented Generation,即检索增强生成。它的思路很简单:不在生成阶段强迫模型记住所有知识,而是在生成之前,先从外部知识库中检索出与用户问题最相关的内容片段,把这些片段作为参考上下文一起交给大模型,让模型基于这些材料组织答案。对比来看:方案做法问题直接提问问什么答什么无法回答私有知识和新知识微调(Fine-tuning)用额外数据训练模型参数成本高、周期长、知识更新慢RAG检索相关资料,拼进 Prompt 再生成检索质量决定回答质量RAG 的重要优势是知识可随时增删:新增文档只需要重新建立索引,不需要重新训练模型。这也是为什么 RAG 成为企业构建知识库问答首选方案的原因。1.3 RAG 的典型应用场景企业内部知识库问答(制度、流程、产品资料)。电商客服自动问答(商品信息、售后政策)。教育辅导(教材内容问答、题库检索)。代码仓库问答(根据项目文档回答开发问题)。舆情分析、竞品分析等需要结合外部资料的场景。凡是“模型本身不知道,但你有资料”的问题,都适合用 RAG 解决。2. RAG 完整流程拆解把 RAG 拆开来看,整个流程分成两个阶段。2.1 索引阶段(离线处理)这个阶段的任务是“把知识变成可检索的形式”。原始文档 ↓ 文档加载(PDF / Word / Markdown / HTML) ↓ 文本切分(Chunking) ↓ Embedding 向量化 ↓ 存入向量数据库2.2 查询阶段(在线问答)这个阶段的任务是“根据用户问题,检索相关内容并生成回答”。用户提问 ↓ 问题 Embedding 向量化 ↓ 在向量库中做相似度检索 ↓ 获取 TopK 相关文档片段 ↓ 组装 Prompt(问题 + 检索片段) ↓ 大模型生成最终回答整个链路中有一个关键点:检索出来的内容质量,直接决定最终回答质量。如果检索到的片段和问题无关,大模型再聪明也答不对。3. 环境准备与依赖安装3.1 运行环境说明本文示例以 Python 3.10 为主,操作系统不限(Windows / macOS / Linux 均可)。版本不需要完全一致,但建议 Python 不低于 3.9。核心依赖库如下:pip install langchain langchain-community langchain-huggingface pip install faiss-cpu pip install sentence-transformers pip install chromadb pip install pypdf说明几点:langchain目前已经做了模块拆分,旧教程里的from langchain.embeddings import ...写法在新版本中可能失效,需要注意。向量数据库选择 FAISS 做示例,因为它是本地库、无需启动服务、对入门友好。Embedding 模型使用国产开源的BAAI/bge-small-zh-v1.5,中文效果比较稳定。3.2 LLM 调用方式本文示例默认使用 OpenAI 兼容接口。如果你使用的是国产大模型平台或本地部署模型,只要接口风格兼容,都可以调整base_url和api_key替换。from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="你的模型名称", api_key="你的API Key", base_url="你的接口地址", temperature=0.1 )这里只做演示说明,具体配置以你实际使用的服务商为准。4. 核心知识点拆解:从 Chunk 到 Token4.1 文本切分(Chunking)文本切分是 RAG 流程中最容易被忽视、但影响最大的一步。如果切得太粗,一个 chunk 包含太多无关内容,检索命中后噪声大;如果切得太细,语义被切断,单个片段信息量不足。常用的切分策略:固定长度切分:按字符数或 token 数切割,简单但容易切断语义。按结构切分:Markdown 按标题、PDF 按段落、代码按函数模块切分。递归切分:LangChain 的RecursiveCharacterTextSplitter优先按段落、再按句子、最后按字符逐级回退切分,是当前推荐做法。from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""] )chunk_overlap的作用是让相邻片段保留一定重复内容,避免某个语义点恰好落在切分边界上被“切断”。4.2 Embedding:把文字变成向量Embedding 模型的作用是:把文本变成一维浮点数数组。语义越相近的文本,在向量空间中距离越近。以 bge-small-zh-v1.5 为例:from langchain_huggingface import HuggingFaceEmbeddings embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': True} )注意normalize_embeddings参数。相似度计算方式通常是余弦相似度,归一化之后使用点积即可等价于余弦相似度,一些向量库的默认距离度量是 L2,归一化有助于保证检索一致性。4.3 向量数据库的选型思路向量数据库用于存储文本对应的向量,并提供近似最近邻检索。常见选型:方案特点适合场景FAISS本地库,轻量,速度快入门、原型验证、中小规模Chroma本地库,API 简洁快速开发、原型项目Milvus分布式,适合大规模生产环境、千万级向量Weaviate自带推理能力全功能生产方案QdrantRust 实现,性能好生产环境入门阶段建议先用 FAISS,把流程跑通后再按需要切换。4.4 检索:相似度计算与 TopK检索的本质是:把用户问题向量化,在向量库中查找最相似的 K 个片段。常见距离度量:余弦相似度:衡量方向是否一致,文本检索最常用。欧式距离(L2):数值越小越相似。内积(IP):配合归一化向量时等价于余弦。FAISS 默认使用 L2 距离,构建索引时也可以指定内积:import faiss dimension = 512 index = faiss.IndexFlatIP(dimension) # 内积,需要预先归一化向量4.5 重排序(Rerank)向量检索得到的是“语义相近”的 TopK,但语义相近不等于答案相关。比如用户问“报销流程”,向量检索可能召回“报销制度总则”这类宽泛内容,真正包含流程步骤的段落反而排在第 6 位。重排模型(Cross-Encoder)可以把(问题, 文档片段)拼接起来精细打分,比双塔向量模型更准确。from sentence_transformers import CrossEncoder reranker = CrossEncoder('BAAI/bge-reranker-base') pairs = [(query, doc) for doc in docs] scores = reranker.predict(pairs)4.6 Prompt 组装策略Prompt 决定了模型如何利用检索到的内容。建议结构:你是一个知识库问答助手,请根据以下资料回答问题。 如果资料中没有相关内容,请直接说明“知识库中未找到相关内容”,不要编造。 【资料开始】 {检索到的内容} 【资料结束】 用户问题:{用户问题}设置“找不到就说不知道”的指令,是防止大模型幻觉的最有效手段之一。5. 完整实战:构建一个本地知识库问答系统下面开始搭建一个最小可运行的 RAG 项目。5.1 项目结构rag-project/ ├── docs/ # 原始文档存放目录 │ └── 产品手册.md ├── ingest.py # 索引构建脚本 ├── query.py # 问答脚本 ├── requirements.txt # 依赖清单 └── vector_store/ # FAISS 索引保存目录5.2 准备测试文档在docs/目录创建一份简单的产品手册文档,内容如下:# 智能考勤机使用说明 ## 打卡方式 - 支持人脸识别打卡、指纹打卡、IC卡打卡三种方式。 - 人脸识别支持佩戴口罩识别。 ## 请假流程 1. 员工在OA系统提交请假申请。 2. 部门主管审批。 3. 人事备案。 ## 常见问题 - 打卡失败时,请检查网络连接。 - 指纹磨损用户建议使用人脸识别。这是非常典型的说明类文档,适合验证 RAG 效果。5.3 编写索引构建脚本文件路径:rag-project/ingest.pyimport os from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS # 1. 加载文档 loader = TextLoader("docs/产品手册.md", encoding="utf-8") documents = loader.load() # 2. 切分文本 text_splitter = RecursiveCharacterTextSplitter( chunk_size=200, chunk_overlap=20, separators=["\n\n", "\n", "。", "!", "?", " ", ""] ) chunks = text_splitter.split_documents(documents) print(f"切分完成,共 {len(chunks)} 个片段") # 3. 向量化 embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': True} ) # 4. 构建向量库并保存 vector_store = FAISS.from_documents(chunks, embeddings) vector_store.save_local("vector_store/faiss_index") print("索引构建完成,已保存至 vector_store/faiss_index")运行:python ingest.py第一次运行会下载 BGE 模型,需要保持网络畅通。下载完成后输出类似:切分完成,共 6 个片段 索引构建完成,已保存至 vector_store/faiss_index5.4 编写问答脚本文件路径:rag-project/query.pyfrom langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough # 1. 加载向量库 embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': True} ) vector_store = FAISS.load_local( "vector_store/faiss_index", embeddings, allow_dangerous_deserialization=True ) # 2. 初始化 LLM llm = ChatOpenAI( model="你的模型名称", api_key="你的API Key", base_url="你的接口地址", temperature=0.1 ) # 3. 定义 Prompt prompt = ChatPromptTemplate.from_template( """你是一个知识库问答助手,请根据以下资料回答用户问题。 如果资料中没有相关内容,请直接回答“知识库中未找到相关内容”,不要编造。 【资料开始】 {context} 【资料结束】 用户问题:{question}""" ) # 4. 定义检索器 retriever = vector_store.as_retriever( search_type="similarity", search_kwargs={"k": 3} ) # 5. 构建 RAG 链路 def format_docs(docs): return "\n\n".join([doc.page_content for doc in docs]) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) # 6. 测试问答 while True: question = input("请输入问题(输入 exit 退出):") if question.lower() == "exit": break response = rag_chain.invoke(question) print("\n回答:", response) print("-" * 50)5.5 运行验证python query.py依次测试几个问题:请输入问题:请假流程是什么? 回答:根据资料,请假流程为:1. 员工在OA系统提交请假申请;2. 部门主管审批;3. 人事备案。 请输入问题:打卡失败怎么办? 回答:打卡失败时,请检查网络连接。指纹磨损用户建议使用人脸识别。 请输入问题:公司年假政策是什么? 回答:知识库中未找到相关内容。最后一个回答很关键。如果知识库没有相关内容,模型明确告诉你“没有找到”,而不是强行编造一个答案。这说明 RAG 的基础链路已经跑通了。6. 召回效果优化:从“能用”到“好用”基础流程跑通之后,下一步考虑如何优化。6.1 问题在于检索质量当回答质量不好时,先不要怀疑大模型,绝大多数情况是检索出了问题。常见的检索问题:召回内容不相关:向量相似但语义关联弱。关键信息被切碎:答案跨多个 chunk,单个片段都不完整。TopK 太小:正确内容排在第 5,但只取了前 3。6.2 优化一:结合元数据过滤向量检索之外,可以结合文档的标题、来源、更新时间等元数据做前置过滤。retriever = vector_store.as_retriever( search_kwargs={"k": 5, "filter": {"source": "docs/产品手册.md"}} )比如企业知识库中包含多份文档,优先在“产品手册”范围内检索,而不是全库模糊匹配。也可以先做关键词硬过滤,再向量检索。6.3 优化二:引入重排序先向量检索召回 10 条,再用 Rerank 模型重新打分,取前 3 条作为最终上下文。这种方式在中文文档场景里提升非常明显。from sentence_transformers import CrossEncoder reranker = CrossEncoder('BAAI/bge-reranker-base') def rerank_documents(query, documents, top_n=3): pairs = [(query, doc.page_content) for doc in documents] scores = reranker.predict(pairs) ranked = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True) return [doc for doc, score in ranked[:top_n]]6.4 优化三:查询改写用户问题往往是口语化的,比如“刷脸打不了卡怎么办”。直接拿原问题去检索,效果未必理想。可以先用 LLM 把问题改写成更适合检索的形式。请根据对话语义,改写成适合检索的查询语句。 原始问题:刷脸打不了卡怎么办 改写结果:改写后的查询可能是“人脸识别打卡失败 解决办法”。这种手段在真实场景中经常能明显提升检索命中率。6.5 优化四:Hybrid 搜索(关键词 + 向量)有时用户问题中包含精确的专有名词,比如型号“WF-200”,向量检索可能把它和“考勤机”混在一起。此时结合 BM25 关键词检索,再融合两路结果,效果更稳定。LangChain 中的EnsembleRetriever可以实现:from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever from langchain_community.vectorstores import FAISS bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 3 vector_retriever = vector_store.as_retriever(search_kwargs={"k": 3}) ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.5, 0.5] )两路召回结果按权重融合排序,取最终 TopK。6.6 优化五:调整 Chunk 策略如果发现单个片段信息量不足,可以适当调大chunk_size;如果噪声大,就调小。也可以按文档结构智能切分,保证每个 chunk 是语义完整的段落。一个可供参考的方向是让 chunk 保留文档标题作为上下文:# 智能考勤机使用说明 > ## 请假流程 1. 员工在OA系统提交请假申请。 2. 部门主管审批。 3. 人事备案。这样可以增强检索时的语义定位能力。7. 常见问题与排查思路问题现象常见原因解决思路检索结果明显不相关Embedding 模型不匹配语言场景中文场景换 BGE 等中文向量模型回答内容编造Prompt 缺少“不知道就说不知道”约束在 Prompt 中加入防幻觉指令回答内容不全Chunk 切分切断了关键语义增加 overlap,或按结构切分检索命中但答案不对TopK 太小,正确内容在排名后面调大召回数量,增加 Rerank启动报bge-small-zh下载失败网络无法访问 HuggingFace配置镜像源或手动下载模型到本地目录FAISS 加载报错langchain 版本变化检查allow_dangerous_deserialization参数回答速度太慢检索 + 生成链路过于冗余精简单个环节,减少 context 长度再补充一个重点:向量库加载时的安全参数。FAISS.load_local( "vector_store/faiss_index", embeddings, allow_dangerous_deserialization=True )allow_dangerous_deserialization是为了兼容新版 LangChain 的安全策略。这个参数只应在你信任自己的索引文件时开启。如果索引来自外部来源,存在反序列化攻击风险,不要随意加载。8. 工程化落地与最佳实践从“Demo 能跑”到“生产可用”,中间还有不少工作。8.1 数据入库前的清洗文档加载前,建议先做一次内容清洗:去除页眉页脚、水印、导航栏信息。统一编码为 UTF-8。删除重复内容。表格结构尽量转成 Markdown 或 JSON 格式化文本。生产环境中数据决定效果上限,模型只是一种解码器。脏数据入库,再强的 Rerank 也救不回来。8.2 面向生产的向量库选型FAISS 适合单机原型验证。生产环境需要考虑:数据规模:百万级以上选择 Milvus、Qdrant 等分布式方案。权限隔离:不同部门的数据需要独立隔离。高可用:向量库需要主从部署、备份恢复方案。增量更新:新增文档需要支撑准实时索引更新。8.3 评估体系搭建不要只凭“感觉回答变好了”来判断优化效果。建议建立简单评估集:{ "queries": [ {"question": "请假流程是什么?", "expected": "OA提交、主管审批、人事备案"}, {"question": "三种打卡方式是什么?", "expected": "人脸识别、指纹、IC卡"} ] }对每个问题评估两个维度:检索命中率:期望答案是否出现在召回的 TopK 片段中。生成准确率:模型回答是否包含期望要点。每次调整 chunk 大小、检索器、Prompt 之后,用同一组测试集对比分数,保证优化方向可量化。8.4 权限与安全企业知识库问答涉及内部数据,有几点必须注意:在检索层做数据权限隔离,不能让普通员工检索到高管文档。对模型输出做敏感信息脱敏。涉及删除或重建索引时,先备份向量库文件。API Key 不要写进代码仓库,使用环境变量或密钥管理服务。8.5 成本控制思路RAG 的成本主要来自 Embedding 和 LLM 两部分:文档 Embedding 是一次性成本,可在离线任务中执行。在线问答的 LLM 调用与上下文长度成正比。上下文越长,成本越高、延迟越大。控制 TopK 数量、精简 Prompt 模板、过滤重复信息,都可以减少 token 消耗。一个实用的做法是:系统给用户返回答案时,同时引用来源文档段落。既方便用户核对,也帮助定位检索质量问题。9. 总结与下一步学习路线到这里,你已经完成了一个最小可运行的 RAG 知识库问答系统:理解了 RAG 的原理,掌握了文本切分、向量化、向量检索、Prompt 组装整条链路,也知道了召回优化和工程落地的关键方向。下一步可以继续深入的方向:深入研究你的场景数据:不同类型的文档和数据适合不同的切分和检索策略,没有万能配置。尝试不同的 Embedding 模型:BGE、text-embedding-3-small、M3E 各有特点,在自己的验证集上横向对比。引入 Agent 能力:让 RAG 链路具备多步推理和工具调用能力,处理复杂问题。学习 LangChain/Llamaindex 的进阶组件:比如文档问答、多轮对话中的上下文管理。如果文章对你有帮助,收藏备用,也欢迎在评论区交流在实际落地中遇到的问题。动手跑一遍上面的代码,比阅读十篇文章收获都大。
前一阵子在做知识库问答项目时,发现网上 RAG 相关的教程虽然很多,但大部分要么只讲原理、不给可运行代码,要么代码能跑但不解释为什么这么设计。对刚接触大模型应用开发的同学来说,很容易卡在“原理看懂了,一动手就报错”的阶段。这篇文章打算把 RAG(检索增强生成)从原理到落地完整梳理一遍。内容包括:RAG 要解决什么问题、整体流程如何拆解、向量库选型、召回质量优化,以及一个可以直接复制的知识库问答实战项目。零基础可以按顺序跟着做,有基础的可以直接跳到实战部分或常见问题排查。1. 什么是 RAG:背景与核心原理1.1 大模型的两个天然短板先看一个基础问题:为什么不能直接把文档丢给大模型,让它回答?因为大模型本身有两个明显局限:知识有时间截断。模型训练完成后,新产生的内容它不知道。比如你问它公司内部最新的值班安排,它只能“编”。无法访问私有数据。企业内部文档、产品手册、个人笔记这些数据不可能进入大模型训练集,模型自然无从回答。如果硬要让模型回答,它可能会用自己的“语言惯性”补全一个看起来合理、但实际错误的答案。这就是所谓的“一本正经地胡说八道”。1.2 RAG 的核心思路RAG 的全称是Retrieval-Augmented Generation,即检索增强生成。它的思路很简单:不在生成阶段强迫模型记住所有知识,而是在生成之前,先从外部知识库中检索出与用户问题最相关的内容片段,把这些片段作为参考上下文一起交给大模型,让模型基于这些材料组织答案。对比来看:方案做法问题直接提问问什么答什么无法回答私有知识和新知识微调(Fine-tuning)用额外数据训练模型参数成本高、周期长、知识更新慢RAG检索相关资料,拼进 Prompt 再生成检索质量决定回答质量RAG 的重要优势是知识可随时增删:新增文档只需要重新建立索引,不需要重新训练模型。这也是为什么 RAG 成为企业构建知识库问答首选方案的原因。1.3 RAG 的典型应用场景企业内部知识库问答(制度、流程、产品资料)。电商客服自动问答(商品信息、售后政策)。教育辅导(教材内容问答、题库检索)。代码仓库问答(根据项目文档回答开发问题)。舆情分析、竞品分析等需要结合外部资料的场景。凡是“模型本身不知道,但你有资料”的问题,都适合用 RAG 解决。2. RAG 完整流程拆解把 RAG 拆开来看,整个流程分成两个阶段。2.1 索引阶段(离线处理)这个阶段的任务是“把知识变成可检索的形式”。原始文档 ↓ 文档加载(PDF / Word / Markdown / HTML) ↓ 文本切分(Chunking) ↓ Embedding 向量化 ↓ 存入向量数据库2.2 查询阶段(在线问答)这个阶段的任务是“根据用户问题,检索相关内容并生成回答”。用户提问 ↓ 问题 Embedding 向量化 ↓ 在向量库中做相似度检索 ↓ 获取 TopK 相关文档片段 ↓ 组装 Prompt(问题 + 检索片段) ↓ 大模型生成最终回答整个链路中有一个关键点:检索出来的内容质量,直接决定最终回答质量。如果检索到的片段和问题无关,大模型再聪明也答不对。3. 环境准备与依赖安装3.1 运行环境说明本文示例以 Python 3.10 为主,操作系统不限(Windows / macOS / Linux 均可)。版本不需要完全一致,但建议 Python 不低于 3.9。核心依赖库如下:pip install langchain langchain-community langchain-huggingface pip install faiss-cpu pip install sentence-transformers pip install chromadb pip install pypdf说明几点:langchain目前已经做了模块拆分,旧教程里的from langchain.embeddings import ...写法在新版本中可能失效,需要注意。向量数据库选择 FAISS 做示例,因为它是本地库、无需启动服务、对入门友好。Embedding 模型使用国产开源的BAAI/bge-small-zh-v1.5,中文效果比较稳定。3.2 LLM 调用方式本文示例默认使用 OpenAI 兼容接口。如果你使用的是国产大模型平台或本地部署模型,只要接口风格兼容,都可以调整base_url和api_key替换。from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="你的模型名称", api_key="你的API Key", base_url="你的接口地址", temperature=0.1 )这里只做演示说明,具体配置以你实际使用的服务商为准。4. 核心知识点拆解:从 Chunk 到 Token4.1 文本切分(Chunking)文本切分是 RAG 流程中最容易被忽视、但影响最大的一步。如果切得太粗,一个 chunk 包含太多无关内容,检索命中后噪声大;如果切得太细,语义被切断,单个片段信息量不足。常用的切分策略:固定长度切分:按字符数或 token 数切割,简单但容易切断语义。按结构切分:Markdown 按标题、PDF 按段落、代码按函数模块切分。递归切分:LangChain 的RecursiveCharacterTextSplitter优先按段落、再按句子、最后按字符逐级回退切分,是当前推荐做法。from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""] )chunk_overlap的作用是让相邻片段保留一定重复内容,避免某个语义点恰好落在切分边界上被“切断”。4.2 Embedding:把文字变成向量Embedding 模型的作用是:把文本变成一维浮点数数组。语义越相近的文本,在向量空间中距离越近。以 bge-small-zh-v1.5 为例:from langchain_huggingface import HuggingFaceEmbeddings embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': True} )注意normalize_embeddings参数。相似度计算方式通常是余弦相似度,归一化之后使用点积即可等价于余弦相似度,一些向量库的默认距离度量是 L2,归一化有助于保证检索一致性。4.3 向量数据库的选型思路向量数据库用于存储文本对应的向量,并提供近似最近邻检索。常见选型:方案特点适合场景FAISS本地库,轻量,速度快入门、原型验证、中小规模Chroma本地库,API 简洁快速开发、原型项目Milvus分布式,适合大规模生产环境、千万级向量Weaviate自带推理能力全功能生产方案QdrantRust 实现,性能好生产环境入门阶段建议先用 FAISS,把流程跑通后再按需要切换。4.4 检索:相似度计算与 TopK检索的本质是:把用户问题向量化,在向量库中查找最相似的 K 个片段。常见距离度量:余弦相似度:衡量方向是否一致,文本检索最常用。欧式距离(L2):数值越小越相似。内积(IP):配合归一化向量时等价于余弦。FAISS 默认使用 L2 距离,构建索引时也可以指定内积:import faiss dimension = 512 index = faiss.IndexFlatIP(dimension) # 内积,需要预先归一化向量4.5 重排序(Rerank)向量检索得到的是“语义相近”的 TopK,但语义相近不等于答案相关。比如用户问“报销流程”,向量检索可能召回“报销制度总则”这类宽泛内容,真正包含流程步骤的段落反而排在第 6 位。重排模型(Cross-Encoder)可以把(问题, 文档片段)拼接起来精细打分,比双塔向量模型更准确。from sentence_transformers import CrossEncoder reranker = CrossEncoder('BAAI/bge-reranker-base') pairs = [(query, doc) for doc in docs] scores = reranker.predict(pairs)4.6 Prompt 组装策略Prompt 决定了模型如何利用检索到的内容。建议结构:你是一个知识库问答助手,请根据以下资料回答问题。 如果资料中没有相关内容,请直接说明“知识库中未找到相关内容”,不要编造。 【资料开始】 {检索到的内容} 【资料结束】 用户问题:{用户问题}设置“找不到就说不知道”的指令,是防止大模型幻觉的最有效手段之一。5. 完整实战:构建一个本地知识库问答系统下面开始搭建一个最小可运行的 RAG 项目。5.1 项目结构rag-project/ ├── docs/ # 原始文档存放目录 │ └── 产品手册.md ├── ingest.py # 索引构建脚本 ├── query.py # 问答脚本 ├── requirements.txt # 依赖清单 └── vector_store/ # FAISS 索引保存目录5.2 准备测试文档在docs/目录创建一份简单的产品手册文档,内容如下:# 智能考勤机使用说明 ## 打卡方式 - 支持人脸识别打卡、指纹打卡、IC卡打卡三种方式。 - 人脸识别支持佩戴口罩识别。 ## 请假流程 1. 员工在OA系统提交请假申请。 2. 部门主管审批。 3. 人事备案。 ## 常见问题 - 打卡失败时,请检查网络连接。 - 指纹磨损用户建议使用人脸识别。这是非常典型的说明类文档,适合验证 RAG 效果。5.3 编写索引构建脚本文件路径:rag-project/ingest.pyimport os from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS # 1. 加载文档 loader = TextLoader("docs/产品手册.md", encoding="utf-8") documents = loader.load() # 2. 切分文本 text_splitter = RecursiveCharacterTextSplitter( chunk_size=200, chunk_overlap=20, separators=["\n\n", "\n", "。", "!", "?", " ", ""] ) chunks = text_splitter.split_documents(documents) print(f"切分完成,共 {len(chunks)} 个片段") # 3. 向量化 embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': True} ) # 4. 构建向量库并保存 vector_store = FAISS.from_documents(chunks, embeddings) vector_store.save_local("vector_store/faiss_index") print("索引构建完成,已保存至 vector_store/faiss_index")运行:python ingest.py第一次运行会下载 BGE 模型,需要保持网络畅通。下载完成后输出类似:切分完成,共 6 个片段 索引构建完成,已保存至 vector_store/faiss_index5.4 编写问答脚本文件路径:rag-project/query.pyfrom langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough # 1. 加载向量库 embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': True} ) vector_store = FAISS.load_local( "vector_store/faiss_index", embeddings, allow_dangerous_deserialization=True ) # 2. 初始化 LLM llm = ChatOpenAI( model="你的模型名称", api_key="你的API Key", base_url="你的接口地址", temperature=0.1 ) # 3. 定义 Prompt prompt = ChatPromptTemplate.from_template( """你是一个知识库问答助手,请根据以下资料回答用户问题。 如果资料中没有相关内容,请直接回答“知识库中未找到相关内容”,不要编造。 【资料开始】 {context} 【资料结束】 用户问题:{question}""" ) # 4. 定义检索器 retriever = vector_store.as_retriever( search_type="similarity", search_kwargs={"k": 3} ) # 5. 构建 RAG 链路 def format_docs(docs): return "\n\n".join([doc.page_content for doc in docs]) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) # 6. 测试问答 while True: question = input("请输入问题(输入 exit 退出):") if question.lower() == "exit": break response = rag_chain.invoke(question) print("\n回答:", response) print("-" * 50)5.5 运行验证python query.py依次测试几个问题:请输入问题:请假流程是什么? 回答:根据资料,请假流程为:1. 员工在OA系统提交请假申请;2. 部门主管审批;3. 人事备案。 请输入问题:打卡失败怎么办? 回答:打卡失败时,请检查网络连接。指纹磨损用户建议使用人脸识别。 请输入问题:公司年假政策是什么? 回答:知识库中未找到相关内容。最后一个回答很关键。如果知识库没有相关内容,模型明确告诉你“没有找到”,而不是强行编造一个答案。这说明 RAG 的基础链路已经跑通了。6. 召回效果优化:从“能用”到“好用”基础流程跑通之后,下一步考虑如何优化。6.1 问题在于检索质量当回答质量不好时,先不要怀疑大模型,绝大多数情况是检索出了问题。常见的检索问题:召回内容不相关:向量相似但语义关联弱。关键信息被切碎:答案跨多个 chunk,单个片段都不完整。TopK 太小:正确内容排在第 5,但只取了前 3。6.2 优化一:结合元数据过滤向量检索之外,可以结合文档的标题、来源、更新时间等元数据做前置过滤。retriever = vector_store.as_retriever( search_kwargs={"k": 5, "filter": {"source": "docs/产品手册.md"}} )比如企业知识库中包含多份文档,优先在“产品手册”范围内检索,而不是全库模糊匹配。也可以先做关键词硬过滤,再向量检索。6.3 优化二:引入重排序先向量检索召回 10 条,再用 Rerank 模型重新打分,取前 3 条作为最终上下文。这种方式在中文文档场景里提升非常明显。from sentence_transformers import CrossEncoder reranker = CrossEncoder('BAAI/bge-reranker-base') def rerank_documents(query, documents, top_n=3): pairs = [(query, doc.page_content) for doc in documents] scores = reranker.predict(pairs) ranked = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True) return [doc for doc, score in ranked[:top_n]]6.4 优化三:查询改写用户问题往往是口语化的,比如“刷脸打不了卡怎么办”。直接拿原问题去检索,效果未必理想。可以先用 LLM 把问题改写成更适合检索的形式。请根据对话语义,改写成适合检索的查询语句。 原始问题:刷脸打不了卡怎么办 改写结果:改写后的查询可能是“人脸识别打卡失败 解决办法”。这种手段在真实场景中经常能明显提升检索命中率。6.5 优化四:Hybrid 搜索(关键词 + 向量)有时用户问题中包含精确的专有名词,比如型号“WF-200”,向量检索可能把它和“考勤机”混在一起。此时结合 BM25 关键词检索,再融合两路结果,效果更稳定。LangChain 中的EnsembleRetriever可以实现:from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever from langchain_community.vectorstores import FAISS bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 3 vector_retriever = vector_store.as_retriever(search_kwargs={"k": 3}) ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.5, 0.5] )两路召回结果按权重融合排序,取最终 TopK。6.6 优化五:调整 Chunk 策略如果发现单个片段信息量不足,可以适当调大chunk_size;如果噪声大,就调小。也可以按文档结构智能切分,保证每个 chunk 是语义完整的段落。一个可供参考的方向是让 chunk 保留文档标题作为上下文:# 智能考勤机使用说明 > ## 请假流程 1. 员工在OA系统提交请假申请。 2. 部门主管审批。 3. 人事备案。这样可以增强检索时的语义定位能力。7. 常见问题与排查思路问题现象常见原因解决思路检索结果明显不相关Embedding 模型不匹配语言场景中文场景换 BGE 等中文向量模型回答内容编造Prompt 缺少“不知道就说不知道”约束在 Prompt 中加入防幻觉指令回答内容不全Chunk 切分切断了关键语义增加 overlap,或按结构切分检索命中但答案不对TopK 太小,正确内容在排名后面调大召回数量,增加 Rerank启动报bge-small-zh下载失败网络无法访问 HuggingFace配置镜像源或手动下载模型到本地目录FAISS 加载报错langchain 版本变化检查allow_dangerous_deserialization参数回答速度太慢检索 + 生成链路过于冗余精简单个环节,减少 context 长度再补充一个重点:向量库加载时的安全参数。FAISS.load_local( "vector_store/faiss_index", embeddings, allow_dangerous_deserialization=True )allow_dangerous_deserialization是为了兼容新版 LangChain 的安全策略。这个参数只应在你信任自己的索引文件时开启。如果索引来自外部来源,存在反序列化攻击风险,不要随意加载。8. 工程化落地与最佳实践从“Demo 能跑”到“生产可用”,中间还有不少工作。8.1 数据入库前的清洗文档加载前,建议先做一次内容清洗:去除页眉页脚、水印、导航栏信息。统一编码为 UTF-8。删除重复内容。表格结构尽量转成 Markdown 或 JSON 格式化文本。生产环境中数据决定效果上限,模型只是一种解码器。脏数据入库,再强的 Rerank 也救不回来。8.2 面向生产的向量库选型FAISS 适合单机原型验证。生产环境需要考虑:数据规模:百万级以上选择 Milvus、Qdrant 等分布式方案。权限隔离:不同部门的数据需要独立隔离。高可用:向量库需要主从部署、备份恢复方案。增量更新:新增文档需要支撑准实时索引更新。8.3 评估体系搭建不要只凭“感觉回答变好了”来判断优化效果。建议建立简单评估集:{ "queries": [ {"question": "请假流程是什么?", "expected": "OA提交、主管审批、人事备案"}, {"question": "三种打卡方式是什么?", "expected": "人脸识别、指纹、IC卡"} ] }对每个问题评估两个维度:检索命中率:期望答案是否出现在召回的 TopK 片段中。生成准确率:模型回答是否包含期望要点。每次调整 chunk 大小、检索器、Prompt 之后,用同一组测试集对比分数,保证优化方向可量化。8.4 权限与安全企业知识库问答涉及内部数据,有几点必须注意:在检索层做数据权限隔离,不能让普通员工检索到高管文档。对模型输出做敏感信息脱敏。涉及删除或重建索引时,先备份向量库文件。API Key 不要写进代码仓库,使用环境变量或密钥管理服务。8.5 成本控制思路RAG 的成本主要来自 Embedding 和 LLM 两部分:文档 Embedding 是一次性成本,可在离线任务中执行。在线问答的 LLM 调用与上下文长度成正比。上下文越长,成本越高、延迟越大。控制 TopK 数量、精简 Prompt 模板、过滤重复信息,都可以减少 token 消耗。一个实用的做法是:系统给用户返回答案时,同时引用来源文档段落。既方便用户核对,也帮助定位检索质量问题。9. 总结与下一步学习路线到这里,你已经完成了一个最小可运行的 RAG 知识库问答系统:理解了 RAG 的原理,掌握了文本切分、向量化、向量检索、Prompt 组装整条链路,也知道了召回优化和工程落地的关键方向。下一步可以继续深入的方向:深入研究你的场景数据:不同类型的文档和数据适合不同的切分和检索策略,没有万能配置。尝试不同的 Embedding 模型:BGE、text-embedding-3-small、M3E 各有特点,在自己的验证集上横向对比。引入 Agent 能力:让 RAG 链路具备多步推理和工具调用能力,处理复杂问题。学习 LangChain/Llamaindex 的进阶组件:比如文档问答、多轮对话中的上下文管理。如果文章对你有帮助,收藏备用,也欢迎在评论区交流在实际落地中遇到的问题。动手跑一遍上面的代码,比阅读十篇文章收获都大。