ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RAG从零搭建实战:检索增强生成完整链路与最佳实践

RAG从零搭建实战:检索增强生成完整链路与最佳实践 你是不是也有这种感觉大模型很聪明一问就答但一聊到你自己的业务数据、内部文档、最新资料它就开始一本正经地胡说八道。要么回答得模棱两可要么干脆把训练数据里的旧信息当成你公司的现状来输出。你问它“我们上季度的营收是多少”它可能给你编出一个看起来很有道理、实际上完全不存在的数字。这个问题的根源不在于模型不够大而在于它压根“没见过”你的私有数据。于是 RAG检索增强生成出现了。它现在几乎是企业接入大模型的默认方案也是各大招聘 JD 里出现频率最高的关键词之一。但很多初学者对 RAG 的认知停留在“把文档丢进去就能让 AI 回答我的问题”这个层面真要自己动手搭一套完整流程会遇到切分不合理、检索不准、回答幻觉、评估无指标等一系列问题。这篇文章不打算只讲概念。我会从零开始把一套 RAG 项目从环境准备、文档加载、切分、向量化、检索、生成到效果评估的完整链路都过一遍并给出可以直接运行的代码示例、常见坑点和企业级落地的关键判断。无论你是刚接触大模型的在校学生还是需要在业务中接入 AI 能力的后端开发者这篇文章都值得收藏备用。1. RAG 到底解决了什么问题先说结论RAG 解决的是“大模型不知道你的私有数据也不能实时获取最新知识”的问题。大模型的训练过程决定了它的知识截止到某个时间点且只能学到公开语料里反复出现的内容。至于你公司内部的规章制度、产品手册、售后工单、实验记录、财务报表模型一概不知。传统做法是把这些数据拿去微调Fine-tuning让模型“背下来”。但微调有两个痛点一是成本高每次数据更新都要重新训练二是会引入灾难性遗忘模型学会了新知识可能忘了旧知识。RAG 的思路完全不一样。它不改变模型的权重而是在“提问”和“回答”之间增加了一个检索环节先把你的文档切成小块并向量化存入向量数据库用户提问时先在向量库里检索出最相关的片段再把这些片段和原始问题一起交给大模型让模型基于这些片段作答。这个过程像是给大模型配了一个随身图书馆。它不需要背下所有书只需要知道去哪本书的哪一页找答案。现实中企业内部数据往往分散在 Wiki、数据库、PDF、工单系统里且每天都在更新。RAG 天然适合这种场景因为数据更新只需要重新跑一遍索引不需要重新训练模型。那 RAG 是不是只适合企业也不是。个人知识库、AI 客服、简历筛选、合同审查、代码库问答都是 RAG 的典型落地场景。从技术门槛看现在开源工具链已经相当成熟一个人花一下午也能跑通最小闭环。2. RAG 的核心工作原理与流程拆解要理解 RAG不需要把它想得太玄。它本质上是一个“检索 生成”的两阶段流水线。2.1 离线索引阶段这个阶段的目标是把原始文档变成“模型能快速查找”的结构化数据。流程是加载文档从 PDF、Word、Markdown、HTML、数据库等来源读取文本。清洗文本去掉页眉页脚、乱码、无意义符号统一编码。文本切分把长文档切成固定长度的小块块与块之间通常有重叠避免切断语义。向量化用嵌入模型Embedding Model把每个文本块转换成向量。存储把向量和原始文本一起存入向量数据库。2.2 在线推理阶段用户输入问题后流程是向量化问题用同一个嵌入模型把问题转换成向量。相似度检索在向量数据库中查找与问题向量最相似的 Top-K 个文本块。组装 Prompt把检索到的文本块作为上下文和原始问题一起组装成提示词。生成回答大模型基于提供的上下文生成最终答案并注明引用来源。这两个阶段的划分很重要。离线阶段决定“你能找到什么”在线阶段决定“你能不能找到并答对”。很多 RAG 项目效果不好问题往往出在离线阶段的切分策略而不是模型本身。2.3 一张图理解完整链路如果你画一条流程图从“文档上传”到“回答输出”中间大概会经过以下节点文档加载 - 文本清洗 - 文本切分 - Embedding - 向量入库 | 用户提问 - Query Embedding - 相似度检索 - 组装 Prompt - LLM 生成 - 回答这个流程看起来简单但每个节点都有不少细节。比如文本切分时是按固定字符数切还是按段落切要不要保留标题层级代码文件要怎么切这些会直接影响检索质量。3. RAG 与其他技术方案的对比很多初学者会把 RAG 和“长上下文”“微调”搞混。这里用一个表格把它们的适用场景整理清楚。对比维度RAG扩展上下文窗口长上下文微调Fine-tuning核心思路外部检索按需取用把所有上下文一次性塞给模型更新模型权重数据时效性实时更新索引即可依赖输入无法自动更新需重新训练实现成本中等需维护向量库和检索链路低但推理成本随长度增加高需要 GPU 资源和训练流程适合场景私有知识问答、客服、文档分析长文档理解、复杂推理特定风格、特定格式输出幻觉风险低有引用依据中上下文过长时模型可能忽略关键信息中可能出现幻觉对硬件要求相对较低显存占用随上下文长度增加训练阶段需要较高算力我的判断是在大多数知识问答场景下RAG 是性价比最高的选择。长上下文适合一次性分析超长文档但它不解决“数据随时更新”的问题微调适合让模型学习特定的表达风格或输出格式但不适合用来做大规模知识注入。现实项目中这三种技术经常组合使用。比如先做 RAG 做检索再对某个垂直领域的输出格式做微调最后用长上下文模型处理特别复杂的跨章节推理。4. RAG 项目环境准备与前置条件开始动手前先把环境准备好。下面是本文实操部分需要的依赖清单。4.1 运行环境操作系统Windows / macOS / Linux 均可。后面代码以 Python 为主建议使用 Python 3.9 以上版本。建议使用虚拟环境避免依赖冲突。如果你本地没有可用的 GPU也能跑通流程。Embedding 模型和向量检索对算力要求不高大模型部分可以调用云端 API也可以使用本地量化模型。4.2 依赖安装本文演示会用到以下 Python 库langchain封装 RAG 流程的框架提供文档加载、切分、检索等组件。chromadb轻量级向量数据库适合学习和中小项目。openai或dashscope调用大模型 API。sentence-transformers使用本地嵌入模型避免完全依赖云端接口。pypdf加载 PDF 文档。pip install langchain chromadb openai sentence-transformers pypdf版本以实际安装时为准本文重点演示通用思路。如果你在安装某个库时遇到版本冲突建议先创建一个干净的虚拟环境python -m venv rag_env source rag_env/bin/activate # Linux/macOS rag_env\Scripts\activate # Windows4.3 大模型 API 准备目前主流的云端大模型 API 都兼容 OpenAI 风格的调用格式。你只需要准备 API Key并在代码中设置base_url指向对应服务商的地址即可。如果你希望完全本地运行可以考虑使用 Ollama 加载本地大模型。无论选择哪种方式关键都是要保证网络可达并且 API Key 有足够配额。这里提醒一下不要在不信任的环境中泄露你的 API Key也不要把 Key 硬编码到公开仓库里。建议使用环境变量保存敏感信息。5. 完整 RAG 项目实战从文档到问答现在进入核心环节手把手搭一套最小可用的 RAG 项目。为了演示清晰我会从一个 PDF 文档出发完成加载、切分、向量化、检索、问答全流程。5.1 初始化项目结构建议按下面的目录组织代码rag_demo/ ├── data/ │ └── 企业知识库.pdf ├── rag_pipeline.py ├── requirements.txt └── .envdata目录放原始文档rag_pipeline.py是主脚本.env保存环境变量。如果你还没有合适的 PDF可以用任意一份 Markdown 或 TXT 文档替代代码逻辑是一样的。5.2 文档加载与文本清洗文档加载是第一步。这里用 LangChain 的PyPDFLoader加载 PDF# 文件路径rag_pipeline.py from langchain_community.document_loaders import PyPDFLoader loader PyPDFLoader(./data/企业知识库.pdf) documents loader.load() print(f加载完成共 {len(documents)} 页) for doc in documents[:2]: print(doc.page_content[:200])注意load()返回的是一个Document列表每个元素包含page_content和metadata。metadata里通常会记录页码、来源文件路径等信息后续做引用溯源时会用到。加载完成后建议先打印前几页内容检查有没有乱码、页眉页脚混入、重复文本等问题。文本清洗这一步没有统一的代码模板它高度依赖你的文档类型。常见的清洗操作包括去除连续的空白字符和换行符。去掉页眉页脚如果每页开头都是公司名可以考虑按行过滤。替换全角字符为半角字符中文场景可能不需要。删除图片占位符、扫描版 PDF 里识别出的乱码文本。5.3 文本切分与重叠文本切分是 RAG 项目里最容易影响效果的一步。如果切得太短语义不完整如果切得太长向量化后的信息密度不够检索精度也会下降。一个比较合理的起点是使用RecursiveCharacterTextSplitterfrom langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个文本块的最大字符数 chunk_overlap50, # 相邻块之间的重叠字符数 separators[\n\n, \n, 。, , , , , ] ) chunks text_splitter.split_documents(documents) print(f切分完成共 {len(chunks)} 个文本块) print(chunks[0].page_content)这里的核心参数有两个chunk_size文本块大小。我建议从 300 到 800 之间开始调试。技术文档可以稍长对话记录、FAQ 类内容可以稍短。chunk_overlap重叠长度。重叠的目的是保证被切断的语义边界不会丢失。一般设置为chunk_size的 10% 到 20%。separators决定了优先按什么边界切分。这里把中文句号、感叹号、问号也加入分隔符可以让切出来的块更符合中文语义边界。如果你的文档是英文代码建议按\n\n、\n、空格、空字符的顺序。切分完成后强烈建议随机打印几个块检查语义是否完整。这一步肉眼判断往往比任何指标都直观。5.4 向量化与存储接下来用嵌入模型把文本块转成向量并存入向量数据库。这里我使用sentence-transformers里的本地嵌入模型做演示好处是不需要额外付费也不依赖外部接口from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma embedding_model HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5 ) vectorstore Chroma.from_documents( documentschunks, embeddingembedding_model, persist_directory./chroma_db # 向量数据持久化目录 ) print(向量化完成已存入 ./chroma_db)如果你的网络环境不方便下载模型也可以使用云端 Embedding API。大多数大模型服务商都提供文本向量化接口调用方式类似只是函数名和参数会略有差异。本质上嵌入模型的作用是把“文本语义”映射成一个多维向量让语义相近的文本在向量空间中距离更近。Chroma是本地文件型向量数据库启动快、配置简单适合学习和中小项目。企业级场景一般会选择 Milvus、Elasticsearch、pgvector 等方案但核心操作逻辑是相通的。5.5 实现检索问答向量存储准备好之后就可以实现“提问 - 检索 - 组装 Prompt - 生成答案”的流程了。这里我用 OpenAI 风格的 API 调用方式代码里通过环境变量管理 Key 和接口地址import os from langchain_openai import ChatOpenAI # 从环境变量读取配置 api_key os.getenv(OPENAI_API_KEY) base_url os.getenv(OPENAI_BASE_URL) model_name os.getenv(LLM_MODEL, qwen-plus) # 根据你的实际模型调整 llm ChatOpenAI( api_keyapi_key, base_urlbase_url, modelmodel_name, temperature0.3 )然后使用 LangChain 的RetrievalQA快速搭建问答链路from langchain.chains import RetrievalQA qa_chain RetrievalQA.from_chain_type( llmllm, retrievervectorstore.as_retriever(search_kwargs{k: 4}), return_source_documentsTrue ) query 我们的产品售后服务流程是什么 result qa_chain.invoke({query: query}) print(回答, result[result]) print(引用的文档来源) for doc in result[source_documents]: print(-, doc.metadata.get(source), 第, doc.metadata.get(page), 页)这里的k表示检索返回的 Top-K 个文本块。K 值太小可能漏掉关键信息太大则会把无关内容带入上下文干扰模型判断。建议从 3 到 5 开始调试。temperature0.3表示模型输出的随机性较低回答更稳定。如果是开放性问答可以适当调高如果是知识检索类问答建议保持在 0 到 0.3 之间。5.6 避免依赖框架的进阶写法如果你不想依赖 LangChain 封装好的链式调用也可以自己手动实现检索和提示词组装。这样做的好处是可控性更强调试也更容易。下面是一个不依赖RetrievalQA的版本# 手动实现检索 组装 Prompt 生成 query_vector embedding_model.embed_query(query) retrieved_docs vectorstore.similarity_search_by_vector(query_vector, k4) context \n\n.join([doc.page_content for doc in retrieved_docs]) prompt f请基于以下参考资料回答用户问题。 参考资料 {context} 用户问题{query} 要求 1. 如果参考资料中没有答案请直接说明“根据现有资料无法回答”。 2. 回答时尽量引用参考资料中的原话。 3. 不要编造参考资料中不存在的信息。 回答 response llm.invoke(prompt) print(response.content)这个版本把“检索”和“生成”两个环节拆开每一步的结果都可以打印出来检查。如果你想分析“到底是检索没找到还是模型不会用检索结果”这种写法会更方便。手动写法里的prompt设计也值得多说两句。RAG 的 Prompt 和普通对话的 Prompt 不太一样它需要明确告诉模型哪些内容是参考资料。如果资料里没有答案应该怎么处理。回答的风格和格式要求。在prompt中加入“如果参考资料中没有答案请直接说明”这个约束能明显降低幻觉。6. 运行结果与效果验证跑完上面的代码后你会看到一个基于文档内容生成的回答。但“能回答”不代表“答得好”。这一步我们来讨论如何判断 RAG 系统的效果。6.1 定性验证先做人工检查从下面几个维度看回答质量检查维度具体问题相关性回答是否真的对应问题有没有答非所问忠实度回答是否基于检索到的文档内容还是模型自己编的完整性关键信息有没有遗漏引用准确性引用的页码和来源是否真实存在可读性语言是否通顺格式是否清晰建议准备一份 20 到 50 条真实问答的测试集逐条判断。测试集要覆盖常见问题、边缘问题、不相关问题故意问文档里没有的内容这样才能全面暴露系统缺陷。6.2 定量指标现在业界常用 RAG 评估框架如 RAGAS来量化评估。核心指标包括Context Relevance上下文相关性检索到的文本块和问题之间是否相关。如果这一步就偏了答案不可能对。Answer Relevance回答相关性最终答案和问题的相关程度。Faithfulness忠实度答案中的信息是否都能在检索到的上下文中找到依据衡量幻觉程度。这些指标通常需要一个评测模型来打分。实际项目中可以先用一个小规模测试集跑定性评估等系统稳定后再引入定量评估。6.3 失败排查第一步如果回答质量不好先判断问题出在哪个环节打印检索到的文本块看它们是否真的包含问题答案。如果检索结果不相关问题在索引阶段重点检查切分策略、嵌入模型、K 值设置。如果检索结果相关但回答还是不对问题在生成阶段重点检查 Prompt 设计、温度参数、模型选择。这个排查逻辑贯穿 RAG 项目调试的始终建议直接刻进脑子。7. 常见问题与排查思路下面表格整理了我认为 RAG 项目里最容易踩的坑。问题现象可能原因排查方式解决方案回答与文档内容不一致检索到的文本块不相关模型没有足够依据打印检索结果确认 Top-K 文本是否包含答案优化切分策略调整 K 值更换嵌入模型回答时出现幻觉编造文档里没有的信息Prompt 没有约束模型“不知道就说不知道”检查 Prompt 是否明确要求基于参考资料回答在 Prompt 中加入“无法回答时直接说明”的约束检索结果每次都一样向量库没有更新新的文档未索引检查入库记录确认新增文档是否执行了向量化建立增量索引机制文档变更后自动更新向量库中文检索效果差嵌入模型对中文支持不够好对比不同中文 Embedding 模型的效果改用专门针对中文优化的嵌入模型如 bge 系列文档太长切分后语义断裂chunk_size 设置不合理抽查切分后的文本块看语义是否完整调整 chunk_size 和 overlap或按文档结构切分API 调用超时模型推理时间过长或网络不稳定检查请求日志和耗时缩短 Prompt 长度使用流式输出增加超时时间向量库启动失败依赖版本冲突或路径权限问题查看错误日志检查依赖版本重建虚拟环境确认持久化目录可写数据库内容更新后回答还是旧数据向量库没有同步更新查看索引更新时间实现文档变更监听和增量更新机制这里重点说一下“幻觉”问题。RAG 能降低幻觉但不能完全消除。模型拿到检索内容后仍然可能“发挥”出一些不在资料里的内容。最有效的缓解手段有两个第一是 Prompt 约束明确告诉模型只能依据资料回答第二是引用溯源要求模型输出答案时同时给出参考来源。如果每个回答都能追溯到原始文档就算模型偶尔发挥使用者也能立刻发现。另一个常见误区的表现是向量化完成之后源文件更新了但向量库没有同步更新然后用户拿着新问题去问系统还在用旧索引回答。这是工程上最容易被忽略的问题。生产环境一定要设计文档变更触发索引更新的机制不能全靠手动重跑。8. 企业级 RAG 落地的关键判断与最佳实践从演示项目到企业级生产环境中间的差距比很多人想象中大得多。这里结合行业常见实践给出几条关键判断。8.1 数据源治理比模型更重要很多 RAG 项目效果不好根因不是模型不行而是数据质量太差。如果原始文档本身错误百出、互相矛盾再好的检索和生成也救不回来。企业级落地时第一步应该是梳理数据源明确哪些文档可以被检索、哪些文档需要授权才能访问、哪些文档已经过期需要归档。数据权限也是容易被忽略的问题。如果一套 RAG 系统面向全员开放但文档库里包含部分员工的薪酬数据或者未公开的战略材料那召回出来的内容就可能在内部引起泄密风险。生产环境一定要在索引阶段就控制文档的可见范围并让检索结果也带上权限过滤。8.2 切分策略要根据文档结构定制通用切分在演示项目里够用但到了企业级场景不同类型的文档需要不同的切分策略。商品说明书按标题层级切分保留章节结构。法律合同按条款切分并保留条款编号。代码仓库按函数或类切分保留代码上下文。FAQ一个问题一条记录不需要切分。如果你的文档格式相对固定可以考虑用 Layout 模型版面分析先识别文档结构再在结构边界上切分。这比单纯按字符数切分的效果好得多但成本也高。具体选哪种方案要看文档的标准化程度和业务对精度的要求。8.3 混合检索解决“语义检索的盲区”向量检索擅长语义匹配但它对精确词匹配不敏感。比如用户要查订单号“ORD-2024-001”向量检索可能把这个字符串当作普通文本返回一堆语义相近但不包含该订单号的内容。这时就需要引入关键词检索BM25。企业级 RAG 的常见做法是混合检索向量检索召回语义相关的结果BM25 召回精确匹配的结果然后通过 RRFRerank或权重融合把两个结果合并再做重排序。这一套流程能让系统的鲁棒性大幅提升。重排序Reranker也是企业级方案中的关键一环。先用轻量级模型粗召回 Top-50再用更精确的重排序模型挑出 Top-5这种“粗召回 精排序”的两级结构在效果和成本之间取得了较好的平衡。8.4 Prompt 工程和链路观测是长期工作RAG 上线后Prompt 不是一成不变的。不同来源的问题、不同类型的文档可能都需要微调 Prompt。建议把 Prompt 做成可配置的模板通过配置中心下发而不是写死在代码里。如果用户反馈某类问题回答不准确先调整 Prompt观察几天的效果再考虑其他改动。链路观测也值得部署。每一轮问答的检索结果、K 值、模型回答、耗时、引用来源都建议记录到日志里。一旦出现用户投诉可以快速回放当时的完整链路定位是检索问题、Prompt 问题还是模型问题。8.5 向量数据库与基础设施选型如果只是个人学习Chroma 完全够用。但到了生产环境你需要结合团队已有的基础设施做选型如果团队已经在用 Elasticsearch可以直接用 ES 的向量检索能力减少维护成本。如果数据量在千万级以上并且对查询性能要求高可以考虑 Milvus。如果团队以 PostgreSQL 为主pgvector 是天然的选择至少不用额外引入一个中间件。如果公司已有多云部署需求还需要考虑向量数据库的数据同步和容灾能力。不需要盲目追求“最新最热”的组件。选型的关键是能否融入团队现有的运维体系。8.6 关于评估和测试的额外思考RAG 项目不能“上线即不管”。文档会变用户问题会变模型 API 版本也会变。建议建立一套自动化的回归测试机制每周跑一遍固定的测试集记录评估指标的变化。如果指标明显下降系统会提示你“哪里可能出了问题”。这比等到用户投诉后再去排查要节省大量时间。9. 总结与后续学习方向到这里一套完整的 RAG 项目已经从零开始跑通了。回头看看核心链路并不复杂文档切分、向量化、检索、组装 Prompt、生成回答。真正复杂的是数据质量、切分策略、检索精度、Prompt 设计这些工程细节它们决定了一个 RAG 系统是“玩具”还是“生产力工具”。如果你想深入下去以下几个方向值得继续学习深入理解 Embedding 模型的原理和评测方法这对检索效果有直接影响。学习 Reranker 模型和混合检索解决向量检索的精确匹配盲区。研究如何对 RAG 系统做系统性评估建立你自己的测试集和指标体系。探索 Agentic RAG这一方向在 RAG 的基础上引入了多步推理、工具调用和迭代检索对复杂问题有更好的处理能力。如果你关心部署可以研究 LangChain、LlamaIndex 之外更底层的实现甚至自己用原生 Python 写一套轻量 RAG 核心这对理解原理非常有帮助。最后给你一个实用建议学 RAG 最忌讳只看教程不动手。找一份你熟悉的文档按本文的流程跑一遍然后故意改坏某个参数比如把 chunk_size 设成 100再去提问观察效果变化。这种“破坏性实验”比任何理论讲解都能帮你建立起直觉。希望这篇文章对你有帮助。如果你在动手实践时遇到问题欢迎在评论区留言讨论。
返回列表