☰
RAG 检索增强生成实战:七步链路拆解与最小可用代码实现
2026/10/16 3:27:05 网站建设 项目流程

RAG 检索增强生成实战:七步链路拆解与最小可用代码实现

为什么 RAG 会成为企业落地的"标配"

大模型本地跑起来之后,你会撞上三面墙,它们恰好对应 RAG 存在的三个理由。

第一面墙是幻觉。问模型"我们公司产品的保修期是多久",它不会说"我不知道",而是基于训练时的通用知识编一个听起来合理的数字。对企业场景这是致命的。RAG 的思路是:先把文档里真正相关的段落搜出来,强制模型"照着资料答题"——答案的出处在检索结果里,编造的空间被大幅压缩。

第二面墙是私域知识缺失。公司合同、内部手册、上月周报,训练语料里不可能有。RAG 不要求模型"记住"这些材料,只要求它"现场读":每次提问时把相关资料临时塞进上下文。这也意味着知识更新不需要重新训练——文档改了、入库了,下一次提问就生效,这是 RAG 相对微调的主要运营优势。

第三面墙是上下文窗口有限。就算不嫌贵,几十万字的整本文档也塞不进一次请求;而 RAG 只检索"最相关的 3~8 个块",上下文占用稳定可控。

一句话概括:RAG 就是给模型配一套"开卷考试"的资料检索系统。理解了这条链路,才能判断知识库答不好时该动的是哪一环——切块、嵌入模型,还是提示词。

七步链路:从文档到答案

RAG 的完整链路分两条泳道:离线建库(文档只处理一次)和在线问答(每次提问都走一遍)。七步逐一拆解,每步都有可调参数和典型坑。

第 ① 步:文档准备

输入是原始文档:PDF、Markdown、TXT、HTML 等。这一步最容易踩的坑在源头:扫描版 PDF 没有文字层,解析出来是空的,后面全白干——先确认能复制出文字,不行就先 OCR;格式混杂的文档(一页里图、表、正文混排)表格抽出来的文字顺序常常是乱的,这类文档适合人工整理成 Markdown 再入库;按主题拆分文档,一百份产品手册和一百份合同混在一个库里,检索精度会被明显拖累,建议按主题分库。

第 ② 步:解析(Loader)

解析器负责把不同格式还原成纯文本。用 AnythingLLM/Dify 时这步自动完成;手写时一般用 LangChain 的各类 Loader 或各格式自己的解析库。可调点是元数据:解析时给每段文本附带来源信息(文件名、页码、章节名),后面调试"答案出处对不对"全靠它,也是给最终答案附引用的基础。

第 ③ 步:切块(Splitter)——最影响检索质量的参数

把长文本切成块,是 RAG 里最考验经验的地方。经验区间是单块 800~1200 字,相邻块 10%~15% 重叠。两个方向的坑都要避开:块太大,一个块塞了太多主题,语义被稀释,模型要在一大段里自己找答案;块太小,一句话一个块,语义不完整。重叠的意义在于:切块边界恰好把一句话切断时,重叠区让"跨边界的语义"在相邻块里各留一份。另一个实操建议:优先按结构切(按章节、按段落边界),而不是按字数硬切——"按结构切为主、超长段落再按字数切"是更稳的组合。

第 ④ 步:嵌入(Embedding)

嵌入模型把文本映射成高维向量,语义相近的文本向量距离近。两个硬约束:入库和检索必须用同一个嵌入模型,换模型后老库向量全部作废必须重建——这是 RAG 里最常见的坑;嵌入模型要选中文能力强的,本地部署的话 bge-m3 对中文更稳。

第 ⑤ 步:入库(向量库)

所有块的向量连同原文、元数据一起写进向量数据库。本地自搭的常用选择:Chroma(嵌入式,个人和小数据量)、FAISS(内存中,数据量大追求检索速度)、Milvus(Docker 部署,生产海量数据)。

第 ⑥ 步:检索(top-k)

把用户问题用同一个嵌入模型转成向量,去库里找距离最近的 k 个块。经验区间 top-k = 3~8:k 太小真正相关的块排不进前二,答案直接缺料;k 太大引入噪声块,把模型注意力带偏。判断是否合适有个土办法:把检索出来的块打印出来人眼确认,前 3 个块里必须包含答案原文,否则是检索没命中,不是模型的问题。

第 ⑦ 步:拼 Prompt 与生成

把"系统指令 + 检索到的块 + 用户问题"拼成一次请求发给 LLM。Prompt 模板里两句指令很关键:“只根据下面提供的资料回答,资料里没有就回答’资料中没有相关信息’”——给模型留"说不知道"的出口,是压制幻觉的直接办法;资料和问题分开标清楚,别让模型分不清哪些是背景、哪些是问题。

最小可用代码:用 Ollama 嵌入 + Chroma 跑通闭环

下面是最小可跑的代码骨架,演示"入库 → 检索 → 拼 Prompt → 调 Ollama"的完整闭环:

importrequestsimportchromadb OLLAMA="http://localhost:11434"LLM_MODEL="qwen2.5:7b"# 以 ollama list 输出为准EMB_MODEL="bge-m3"# 以 ollama.com/library 为准defembed(texts):resp=requests.post(f"{OLLAMA}/api/embed",json={"model":EMB_MODEL,"input":texts})resp.raise_for_status()returnresp.json()["embeddings"]# ---- 离线建库(一次性)----client=chromadb.PersistentClient(path="./rag_store")col=client.get_or_create_collection("manual")chunks=["……第一个块……","……第二个块……"]# 实际来自 Loader + Splittercol.upsert(ids=[str(i)foriinrange(len(chunks))],embeddings=embed(chunks),documents=chunks)# ---- 在线问答(每次提问)----defrag_answer(question:str,top_k:int=5)->str:hits=col.query(query_texts=[question],n_results=top_k)context="\n".join(hits["documents"][0])prompt=("只根据下面的资料回答用户问题;资料中没有相关信息就回答""'资料中没有相关信息'。\n\n【资料】\n"+context+"\n\n【问题】"+question)+resp=requests.post(f"{OLLAMA}/api/chat",+json={"model":LLM_MODEL,+"messages":[{"role":"user","content":prompt}]})+resp.raise_for_status()+returnresp.json()["message"]["content"]print(rag_answer("这个产品的保修期是多久?"))

验证标准:三层探针

跑通代码不等于系统可用,用三层探针验证:第一层 Ollama 层,curl 本地 11434 端口确认模型列表包含嵌入模型;第二层检索层,把命中的块打印出来,确认包含答案原文——这是金标准,检索没命中时别去调 Prompt,先回切块和嵌入模型查;第三层生成层,最终回答与文档原文一致,且对文档里没有的问题回答"资料中没有相关信息"而不是编造。

故障排查:按症状定位问题

RAG 答错时先不要急着换更大的模型,按症状反推是哪一步出了问题。检索不到(命中块全不相关)——大概率是切块、嵌入或文档质量问题,排查顺序是打印命中块人眼确认、检查扫描版 PDF 是否抽到字、确认入库检索用同一嵌入模型。答非所问(命中对了但答偏)——多半是 top-k 太大引入噪声或 Prompt 没写死"只根据资料回答"。幻觉(资料里没有的也编)——Prompt 没留出口或模型能力不足,先加"没有就回答不知道"。答案"对但缺细节"——块切得太碎,关键句被切散,加大 chunk 或 overlap 重切重建。

调试习惯上,每次只改一个变量(切块大小、重叠、top-k、嵌入模型各试一轮),并把检索命中块打印出来对比,改完立刻知道是哪次改动起的作用。RAG 调优不是玄学,是有依据的链路排查。

RAG 与微调、长上下文的选择

三条路线常被混在一起,其实分工不同。知识更新快选 RAG:政策、价格、产品参数这类"月月变"的信息,微调一次要准备数据、训练、评估,RAG 改文档即可;风格、格式、能力选微调:“说话像我们品牌”"输出固定 JSON 结构"这类"怎么答"的问题微调更合适;两者还可以叠加——微调把基座调好,RAG 负责喂私有知识。选型口诀是:"让它知道新事实"用 RAG,"让它学会新本事"用微调。

长上下文模型普及后,还有一个新问题:文档不长为什么还要 RAG?经验值是:文档总量在几万字以内、且每次提问都高度相关(比如就一份合同反复问),直接整份塞进上下文可行,还省掉检索失真的风险;文档总量超过上下文预算、或每次提问只跟一小部分相关(知识库场景),必须走 RAG——不是模型"能不能装下"的问题,而是"塞满了但注意力被稀释、成本上升"的问题。生产环境里,混合策略往往是性价比最高的:关键全文进上下文加检索补充细节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询