☰
RAG检索增强实战:原理、七步链路与最小可用代码
2026/10/11 8:43:04 网站建设 项目流程

RAG检索增强实战:原理、七步链路与最小可用代码

这一篇拆开看 RAG 的七步链路——文档怎么切、向量怎么搜、答案怎么拼。理解了这条链路,才能判断知识库答不好时该动的是哪一环(切块、嵌入模型,还是提示词),也才能在现成工具之外,用 Python 手写一个自己的版本。

一、为什么需要 RAG

本地模型跑起来后,你会发现三个绕不过去的限制,它们恰好对应 RAG 的三个存在理由:

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

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

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

可以这样概括:RAG 就是给模型配一套"开卷考试"的资料检索系统。AI-17/AI-18 里的知识库按钮,背后跑的就是本篇这条链路。会了原理,你在任何工具里调参(切块大小、top-k、嵌入模型)都有依据,而不是盲猜。

二、环境要求

项目要求说明
系统Windows 10/11 或 Linux(WSL2)本篇代码示例两者通用
Python3.10 / 3.11 / 3.12(保守选 3.11)环境隔离方法见《AI-03 Python环境与虚拟环境》
Ollama已装好,能跑对话模型 + 嵌入模型安装见《AI-07 Ollama本地部署大模型》
对话模型如 qwen2.5:7b(Q4_K_M 权重约 4-5 GB)以ollama list输出为准
嵌入模型nomic-embed-text 或 bge-m3模型名以 ollama.com/library 为准
Python 包chromadb、requestspip install可走清华镜像,见第四节
显存嵌入模型是小模型,不吃多少显存;瓶颈在对话模型预算方法见《AI-05 显存计算与模型选择》
磁盘向量库随文档量增长,留 10 GB 空闲大文件远离 C 盘(系列惯例)

注意一个常见误解:RAG 本身不需要 GPU。嵌入模型参数量小、CPU 就能跑,真正吃显存的只有最后一步的对话 LLM。纯 CPU 机器搭 RAG 完全可行,只是回答速度慢。

三、标准链路七步:从文档到答案

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

3.1 第①步:文档准备

输入是原始文档:PDF、Markdown、TXT、HTML 等。这一步最容易出的坑在源头:

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

3.2 第②步:解析(Loader)

解析器负责把不同格式还原成纯文本。用 AnythingLLM/Dify 时这步自动完成;手写时一般用 LangChain 的各类 Loader 或各格式自己的解析库(接口迭代快,以官方文档当前版本为准)。

可调点:元数据(metadata)。解析时建议给每段文本附带来源信息(文件名、页码、章节名),后面调试"答案出处对不对"全靠它,也是给最终答案附引用的基础。

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

把长文本切成"块(chunk)",是 RAG 里很考验经验的地方。经验区间:单块 800-1200 字,相邻块 10-15% 重叠。为什么是这个区间,两个方向各有一个坑:

  • 块太大:一个块塞了太多主题,语义被"稀释"。用户问"保修期",命中的块里一半是别的主题,模型要在一大段里自己找答案,精度下降、还占用了上下文空间;
  • 块太小:一句话一个块,语义不完整。"本产品保修期为一年"没问题,但"保修期内免费换新,保外维修仅收工本费"拆成两半后,每半单独拿去嵌入都表达不清;
  • 重叠(overlap):切块边界恰好把一句话、一条逻辑切断时,重叠区让"跨边界的语义"在相邻块里各留一份,缓解"关键信息正好卡在切缝上"的问题。经验值 10-15%,重叠太大则库里冗余度上升、top-k 命中多个近似重复块。

另一个实操建议:优先按结构切(按章节、按段落边界)而不是按字数硬切——"按结构切为主、超长段落再按字数切"是更稳的组合。

3.4 第④步:嵌入(Embedding)

嵌入模型把文本映射成高维向量,语义相近的文本向量距离近。两个硬约束:

  1. 入库和检索必须用同一个嵌入模型——换模型后老库向量全部作废,必须重建。这是 RAG 里最常见的坑,第六节第 2 条专门讲;
  2. 嵌入模型选中文能力强的:本地 Ollama 里bge-m3对中文文档更稳,nomic-embed-text轻量通用(模型名以 ollama.com/library 为准);维度与模型选型的完整对比是下一篇《AI-20 向量数据库与嵌入模型》的主题。

3.5 第⑤步:入库(向量库)

所有块的向量连同原文、元数据一起写进向量数据库。本地自搭的常用选择:

向量库部署方式适合
Chroma嵌入式,pip install chromadb个人 / 小数据量,本篇用它
FAISSpip install faiss-cpu,内存中数据量大、追求检索速度
MilvusDocker 部署生产 / 海量数据,见《AI-40 Docker-GPU一键全家桶》

三者选型细节(维度、持久化、备份)下一篇展开,本篇用 Chroma 跑通链路。

3.6 第⑥步:检索(top-k)

把用户问题用同一个嵌入模型转成向量,去库里找距离最近的 k 个块。经验区间top-k = 3-8:

  • k 太小(1-2):真正相关的那块如果没排进前二,答案直接缺料;
  • k 太大(>10):引入噪声块,不仅浪费上下文,还会把模型注意力带偏——检索结果里混进"看着相关其实答非所问"的段落,模型可能顺着它编。

判断 top-k 是否合适有个土办法:把检索出来的块打印出来自己看一遍,前 3 个块里必须包含答案原文,否则不是模型的问题,是检索没命中。

3.7 第⑦步:拼 Prompt 与生成

把"系统指令 + 检索到的块 + 用户问题"拼成一次请求发给 LLM。Prompt 模板里两句指令很关键:

  • “只根据下面提供的资料回答,资料里没有就回答’资料中没有相关信息’”——给模型留"说不知道"的出口,是压制幻觉的直接办法;
  • 资料和问题分开标清楚,别让模型分不清哪些是背景、哪些是问题。

生成步本身没有"RAG 参数"可调,但模型能力决定表达质量:7B 级模型检索给对了资料通常能答对,复杂推理类问题建议换更大的模型(如 14B)。

四、验证:跑通一个最小 RAG(Ollama 嵌入 + Chroma)

下面是最小可跑的代码骨架,演示"入库 → 检索 → 拼 Prompt → 调 Ollama"完整闭环。各库接口迭代较快,具体函数名与参数名以 chromadb 与 Ollama 官方文档当前版本为准,本篇不罗列未核实的 API 字段。

先装依赖(国内网络走清华 pip 镜像):

pip install chromadb requests -i https://pypi.tuna.tsinghua.edu.cn/simple

拉嵌入模型(对话模型假定已有,如 qwen2.5:7b):

ollama pull bge-m3
importrequestsimportchromadb OLLAMA="http://localhost:11434"LLM_MODEL="qwen2.5:7b"# 以 ollama list 输出为准EMB_MODEL="bge-m3"# 以 ollama.com/library 为准defembed(texts):"""调 Ollama 的嵌入接口,把文本列表变成向量列表。 接口路径与请求字段以 Ollama 官方文档(Embeddings)为准。"""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")# 按主题分 collection# 文档 → 解析 → 切块(chunk 800-1200 字 + 10-15% 重叠),# 具体切块函数以所用库官方文档为准,这里直接给出切好的文本示意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("这个产品的保修期是多久?"))

验证标准(照 AI-17 的"三层探针"):

  1. Ollama 层:curl http://localhost:11434/api/tags返回模型列表,且嵌入模型在列;
  2. 检索层:把hits["documents"][0]打印出来,确认命中块里确实包含答案原文;
  3. 生成层:最终回答与文档原文一致,且对文档里没有的问题回答"资料中没有相关信息"而不是编造。

第 2 层是金标准:检索没命中时别去调 Prompt,先回切块和嵌入模型查。

五、进阶技巧

5.1 回答质量排查:症状对应链路

RAG 答错时先不要急于更换更大的模型,按症状反推是哪一步:

症状最可能出问题的步骤排查动作
检索不到(命中块全不相关)切块 / 嵌入 / 文档质量打印命中块人眼确认;检查是否扫描版 PDF 没抽到字;确认入库和检索用同一嵌入模型;bge-m3 与 nomic-embed-text 换用对比
答非所问(命中对了但答偏)top-k 过大引入噪声 / Prompt降 top-k 到 3 再看;检查 Prompt 是否写死"只根据资料回答"
幻觉(资料里没有的也编)Prompt 没留出口 / 模型能力Prompt 加"没有就回答不知道";仍不行则换更大的对话模型
答案"对但缺细节"块切得太碎(关键句被切散)加大 chunk 或加大 overlap 重切重建

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

5.2 RAG vs 微调:先选路线再动手

两条路线常被混在一起,其实分工不同:

  • 知识更新快 → 选 RAG。政策、价格、产品参数这类"月月变"的信息,微调一次要准备数据、训练、评估,RAG 改文档即可;
  • 风格 / 格式 / 能力 → 选微调。你要的是"说话像我们品牌"“输出固定 JSON 结构”“学会某类专业推理”,这类"怎么答"的问题微调更合适,LoRA 是本地微调的主流方案,详见《AI-37 LoRA微调训练实战》;
  • 两者可叠加:微调把基座调好,RAG 负责喂私有知识。

选型口诀:"让它知道新事实"用 RAG,"让它学会新本事"用微调。

5.3 RAG vs 长上下文:什么时候直接塞

现在有模型的上下文窗口很大,"文档不长为什么还要 RAG"是合理疑问。经验值:

  • 文档总量在几万字以内、且每次提问都高度相关(比如就一份合同反复问),直接整份塞进上下文可行,还省掉检索失真的风险;
  • 文档总量超过上下文预算、或每次提问只跟一小部分相关(知识库场景),必须走 RAG——不是模型"能不能装下"的问题,是"塞满了但注意力被稀释、成本上升"的问题;
  • 折中做法:直接塞 + 检索各留一部分(关键全文进上下文 + 检索补充细节),以模型实际支持的上下文长度为准。

5.4 让答案"可引用"

生产环境里 RAG 答案最好带出处:解析时把文件名/页码存进 metadata,检索后把来源编号附在答案末尾。这既是用户体验,也是"答案对不对"的快速核对通道——引用对不上时,问题多半出在切块边界。

六、故障排查(按层定位)

#症状(报错原文)层原因解决
1连接 Ollama 失败(localhost:11434连接被拒 / Connection refused)服务Ollama 没启动,或端口 11434 被占先ollama list确认本体正常;Linux 看sudo journalctl -u ollama;再确认 11434 未被其他进程占用
2嵌入或入库时报维度不匹配(Chroma 报维度与 collection 不一致 / 旧向量维度对不上)框架换过嵌入模型(新旧模型输出维度或向量空间不同),旧库向量作废换嵌入模型必须重建库:删掉旧 collection 或换持久化路径,全部文档重新切块入库;此后同库只用一个嵌入模型
3检索不到相关内容(命中块与问题无关 / “没找到相关知识”)应用文档没解析成功(扫描版 PDF)、或切块/入库没走完、或入库与检索嵌入模型不一致确认 PDF 能复制出文字(不行先 OCR);小文档单独重建验证;核对两侧嵌入模型名一致;再按 5.1 症状表调参
4ModuleNotFoundError: No module named 'torch'或chromadb(装包后仍报找不到)环境激活了错误环境 / 包装到了别的解释器where python(Win)/which python(Linux)确认解释器指向当前 venv;重新激活后再装
5pip install chromadb下载卡住 /ConnectionError/ 超时网络国内访问 PyPI / HuggingFace 受限(Chroma 首跑可能拉默认嵌入组件)pip 走清华镜像(本文命令已带-i);HuggingFace 侧设HF_ENDPOINT=https://hf-mirror.com或改用 ModelScope,思路同《AI-06 模型下载全攻略》
6Linux 下建库/嵌入时进程被杀:Killed/ dmesg 出现oom-kill内存物理内存不足(RAM,不是显存):大量文档一次性嵌入吃满内存WSL2 在.wslconfig里加swap=(改完wsl --shutdown生效);物理机加 swap;或分批入库
7CUDA error: out of memory(最后一步 LLM 生成时)显存对话模型 + 长 Prompt 超出显存降量化位宽、压短上下文;nvidia-smi清后台占用;方法见《AI-28 CUDA报错大全与排查》
8答案乱码 / 无意义重复(检索明明命中了)量化对话模型量化位宽过低(Q2/Q3)升到 Q4_K_M 及以上;确认权重与 tokenizer 同源

排错顺序仍是先底后顶:Ollama 通不通(curl 探针)→ 包与环境对不对 → 文档解析好不好 → 检索命中没有(打印块)→ 最后才调 RAG 参数和怀疑模型。多数这类问题出在前四步。

七、本篇自检清单

  • 能说清 RAG 解决的三类问题(幻觉、私有知识、上下文有限)与"开卷考试"类比
  • 能按顺序背出七步链路:文档 → 解析 → 切块 → 嵌入 → 入库 → 检索 → 拼 Prompt 生成
  • 知道切块经验区间(800-1200 字 + 10-15% 重叠)和两个方向的坑(太大稀释语义 / 太小语义不完整)
  • 知道 top-k 经验区间 3-8,以及"打印命中块人眼确认"的土办法
  • 已用 Ollama 嵌入 + Chroma 跑通最小 RAG,能说出三层验证标准(Ollama 层 / 检索层 / 生成层)
  • 牢记两条硬约束:入库与检索必须同一嵌入模型;换嵌入模型必须重建库
  • 会按症状定位问题:检索不到 → 切块/嵌入;答非所问 → top-k/Prompt;幻觉 → Prompt 出口/模型
  • 会做选型:知识更新快选 RAG、风格格式选微调(AI-37);文档短且全相关可直接塞长上下文

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

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

立即咨询