代码语言

知识点思维导图

29 个知识节点

Python(26) - RAG 入门

读完后,你应能完成以下任务:

  • 绘制“Python(26) - RAG 入门 / 先给锚点:RAG ≈ 让模型「开卷考试」+ 你熟悉的「搜索 → 渲染」”的关键对象与数据流,解释“上下文窗口有限:模型一次能读的 token 有上限(几万到几十万不等),你公司几百份文档塞不下。 -> token 要花钱:输入越长越贵,每次提问都重发全部文档,成本爆炸。 -> 噪声拖垮效果:资料越多越杂,模型越容易被无关内容带偏(「大海捞针」问题,塞太多反而答得更差)。”,并用源码位置、日志或 Trace 标注证据。
  • 为“Python(26) - RAG 入门 / RAG 全景:离线建索引 + 在线问答,两条流水线”设计正常与异常输入,验证“类比前端:离线建索引 ≈ 打包构建期(把内容预处理好、建好索引,跑一次),在线问答 ≈ 运行时请求(用户每来一次就走一遍检索 + 生成)。”,输出首个偏差位置与回归测试结果。
  • 实现“Python(26) - RAG 入门 / 第一步 chunking:为什么要把文档切碎”的最小代码或配置,检验“chunking(切分 / 分块)是 RAG 里最不起眼、却最影响效果的一步。”,输出命令、结果与 Diff,并说明不适用边界。

上一篇(第 25 篇)你已经学会把文字压成一串坐标(Embedding),还会算「两段话语义有多近」。但单有 Embedding 还干不成事。本篇要解决的真实问题是:大模型不知道你公司的内部文档,也不知道今天发生了什么(它的知识有训练截止日期)。你问它「我们产品的退款政策是什么」,它要么瞎编(幻觉),要么说「我不知道」。RAG(Retrieval-Augmented Generation,检索增强生成)就是这件事的标准解法——本质是让模型「开卷考试」:先去你的资料库里检索相关片段,再把片段塞进 prompt 让模型「照着资料回答」。这是目前落地最广的 AI 应用形态(智能客服、文档问答、企业知识库几乎都是它)。

阶段五依赖链对齐:23(调 API)→ 24(结构化输出 + 函数调用)→ 25(Embedding·把文字变坐标)→ 26(本篇·RAG)→ 27(Agent)。本篇默认你已读过第 25 篇,知道 embeddings.create 怎么用、余弦相似度怎么算;这里把它们组装成一条能跑的问答流水线。


一、先给锚点:RAG ≈ 让模型「开卷考试」+ 你熟悉的「搜索 → 渲染」

先建立直觉。把大模型想象成一个博学但记不住你私事的考生

  • 闭卷考试:直接问模型「我们的退款政策是几天?」。它没见过你的文档,只能凭「常识」瞎答 → 幻觉。
  • 开卷考试:先翻出政策文档里相关的那一段,连同问题一起递给它:「这是政策原文:『……7 天无理由退款……』,请据此回答」。模型照着抄就行 → 准确。

RAG 干的就是「开卷」这件事,而且整个流程你作为前端其实天天在做——它就是熟悉的「搜索 → 拿数据 → 渲染」模式:

前端做过的事 RAG 里对应的环节
用户在搜索框输入关键词 用户提问(query)
调搜索接口 /search?q=... 拿到相关结果 去向量库检索出最相关的几段资料
把结果数据塞进模板渲染成页面 把资料片段塞进 prompt 模板
浏览器展示最终页面 模型基于资料生成最终回答

边界(哪里不一样):前端搜索是关键词匹配includesLIKE %xx%,字面对得上才命中);RAG 的检索是语义匹配——你问「咋退钱」,能命中写着「退款流程」的段落,哪怕一个字都没重合。这正是第 25 篇 Embedding 的价值:把「字面匹配」升级成「意思相近就算命中」。另一个关键差异:搜索结果是直接展示给人看的,而 RAG 的检索结果是喂给模型的中间料,最终答案由模型加工后产出。

1.1 为什么不直接把整个知识库塞进 prompt?

新手第一反应常是:「我把所有文档一股脑贴进 system prompt 不就行了?」三个硬约束让这条路走不通:

  1. 上下文窗口有限:模型一次能读的 token 有上限(几万到几十万不等),你公司几百份文档塞不下。
  2. token 要花钱:输入越长越贵,每次提问都重发全部文档,成本爆炸。
  3. 噪声拖垮效果:资料越多越杂,模型越容易被无关内容带偏(「大海捞针」问题,塞太多反而答得更差)。

所以正确做法不是「全塞」,而是「先检索出最相关的一小撮,只塞这一小撮」。这就是 RAG 的核心思想。


二、RAG 全景:离线建索引 + 在线问答,两条流水线

RAG 拆开看是两个阶段,别混在一起理解:

【离线 · 建索引】(资料入库时跑一次,类比前端的「构建/预渲染」)
  原始文档
    └─切分(chunking)──> 一堆小片段
        └─Embedding──> 每段变成一串向量坐标
            └─存入向量库 (id, 原文, 向量)

【在线 · 问答】(用户每次提问时跑,类比「运行时请求」)
  用户问题
    └─Embedding──> 问题也变成向量
        └─去向量库找最近的 top-k 段──> 检索出最相关的几段原文
            └─拼进 prompt 模板──> 「这是资料:xxx。请据此回答:用户问题」
                └─调大模型生成──> 最终答案

类比前端:离线建索引 ≈ 打包构建期(把内容预处理好、建好索引,跑一次),在线问答 ≈ 运行时请求(用户每来一次就走一遍检索 + 生成)。把这两条线分清,后面代码就不会乱。

本篇按「切分 → 检索 → 生成拼装」三步逐个讲透,最后串成一个能跑的最小 demo。


三、第一步 chunking:为什么要把文档切碎

chunking(切分 / 分块)是 RAG 里最不起眼、却最影响效果的一步。不能拿整篇文档去做 Embedding,必须先切成小片段,原因有二:

  • 检索粒度:用户问一个具体问题,命中的应该是「相关的那一段」,而不是「包含答案的整篇 1 万字文档」。整篇喂回去既浪费 token 又引入噪声。
  • Embedding 表达力:把 1 万字压成一个向量,语义被「平均」得稀烂(什么都沾一点 = 什么都不像);一小段话压成向量,语义才聚焦。

类比:这就像前端的分页 / 虚拟列表 / 懒加载——不会把十万条数据一次性渲染,而是切成一屏一屏按需取。chunking 就是「把长文档切成可检索的最小单元」。

最常用的切法是固定长度 + 重叠(overlap)

// JS 等价实现,思路一模一样
// text:待切分的长文本;chunkSize:每段目标长度;overlap:相邻段重叠字符数
function splitText(text, chunkSize = 300, overlap = 50) {
  const chunks = []   // chunks:存放切分结果的数组
  let start = 0       // start:当前片段的起始下标
  while (start < text.length) {
    chunks.push(text.slice(start, start + chunkSize))
    start += chunkSize - overlap   // 留 overlap 重叠,防止切断语义
  }
  return chunks
}

实战里还有更聪明的切法(按段落 \n\n 切、按 markdown 标题切、按句子切),原则是尽量沿「自然语义边界」下刀,别把一句话拦腰斩断。入门阶段用「固定长度 + overlap」足够,先跑通再优化。LangChain / LlamaIndex 这类框架内置了 RecursiveCharacterTextSplitter 等现成切分器,等理解了原理再用框架不迟。


四、第二步检索:向量库就是「语义版的搜索引擎」

切好的片段要先转成向量、存起来,用户提问时再去「找最近的几段」。这一步直接复用第 25 篇的 Embedding 能力。

4.1 离线:把片段灌进库

⚠️ 关键坑:建索引和查询必须用同一个 embedding 模型。向量是模型「私有的坐标系」,A 模型的坐标和 B 模型的坐标不在一个空间里,混用就是拿北京的经纬度去查上海的地图——算出来的距离毫无意义。

4.2 在线:用余弦相似度找 top-k

检索的本质是「问题向量离哪几个片段向量最近」。距离度量最常用余弦相似度(第 25 篇讲过:值越接近 1 越相似)。这里手写一版,把黑盒拆开看:

注意「怎么退钱」和原文「无理由退款」一个字都不重合,但语义相近,所以能命中——这就是语义检索碾压关键词搜索的地方。

4.3 真实场景:用现成向量库,别自己手撸

手写 numpy 版是为了讲清原理。真实项目几万、几百万条片段时,每次都全量算一遍余弦太慢,要用专门的向量库(内置近似最近邻索引 ANN,毫秒级返回)。入门最易上手的是 Chroma:

常见向量库选型(了解即可):Chroma(最轻量,本地起步首选)、FAISS(Meta 出品,纯本地、快,无服务端)、pgvector(给 PostgreSQL 装个扩展就能存向量,已有 PG 的项目最省事)、Milvus / Qdrant / Pinecone(面向规模化、可托管)。原理都一样:存向量 + 近似最近邻检索,换库主要是换 API。


五、第三步生成:把检索结果拼进 prompt

检索拿到相关片段后,最后一步是把它们拼进 prompt,让模型照着回答。这一步回到第 24 篇的 Prompt 工程——核心是用 system prompt 立规矩:「只准根据我给的资料回答,资料里没有就说不知道」。

这段 system_prompt 的拼装就是整个 RAG 的「临门一脚」。注意它和第 24 篇讲的结构化输出、few-shot 完全可以叠加使用——比如还能要求模型「在答案末尾标注引用了哪条资料」,方便做溯源。


六、串起来:一个最小可跑的 RAG

把上面三步合并,就是一个能跑的最小 RAG。整体不到 50 行,结构是「离线建一次索引 + 在线反复问答」:

这条 50 行的流水线,就是市面上绝大多数「文档问答 / 智能客服 / 企业知识库」的内核。剩下的工程化(换持久化向量库、加 chunking 策略、加缓存、加引用溯源、加重排序)都是在这个骨架上长出来的枝叶。


七、前端新手最容易踩的坑

现象 正解
以为 RAG 是「微调模型」 想着「拿数据训练模型让它记住」 RAG 不改模型,只是检索后塞进 prompt。改模型那叫微调(fine-tune),是另一条路、成本高得多
建库和查询用了不同 embedding 模型 检索全是离谱结果 两边必须同一个模型,向量是模型私有坐标系,不通用
不切分,整篇文档做 embedding 检索命中一大坨、答案飘、token 爆 必须先 chunking 切成小片段
chunk 切太大或太小 太大引入噪声、太小丢上下文 从 300~500 字 + 适当 overlap 起步,按效果调
以为检索是关键词匹配 纠结「问题里没出现这个词怎么办」 它是语义匹配,意思相近就能命中,字面不必重合
不给模型「找不到就拒答」的护栏 检索失败时模型脑补、幻觉 system prompt 明确「资料里没有就说不知道」
检索质量差却怪模型不行 答得不准,第一反应去换更强的生成模型 RAG 的天花板是检索——没召回到正确片段,再强的模型也答不对(garbage in, garbage out)

最后这条最关键,单独强调:RAG 答得好不好,七成看检索、三成看生成。 新手总爱在「换更贵的模型」上花力气,但真正的杠杆通常在前半段——切分策略、embedding 质量、top-k 取值、要不要加重排序(rerank)。调 RAG 时,先把「检索到的片段打印出来肉眼看」,确认召回对了,再去管生成。


八、总结

  • 先给锚点:RAG ≈ 让模型「开卷考试」+ 你熟悉的「搜索 → 渲染」:上下文窗口有限:模型一次能读的 token 有上限(几万到几十万不等),你公司几百份文档塞不下。 -> token 要花钱:输入越长越贵,每次提问都重发全部文档,成本爆炸。 -> 噪声拖垮效果:资料越多越杂,模型越容易被无关内容带偏(「大海捞针」问题,塞太多反而答得更差)。
  • RAG 全景:离线建索引 + 在线问答,两条流水线:RAG 拆开看是两个阶段,别混在一起理解:
  • 第一步 chunking:为什么要把文档切碎:chunking(切分 / 分块)是 RAG 里最不起眼、却最影响效果的一步。
  • 第二步检索:向量库就是「语义版的搜索引擎」:⚠️ 关键坑:建索引和查询必须用同一个 embedding 模型。
  • 第三步生成:把检索结果拼进 prompt:检索拿到相关片段后,最后一步是把它们拼进 prompt,让模型照着回答。
  • 串起来:一个最小可跑的 RAG:把上面三步合并,就是一个能跑的最小 RAG。

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“RAG 入门”中,需要同时满足“先给锚点:RAG ≈ 让模型「开卷考试」+ 你熟悉的「搜索 → 渲染」”与“为什么不直接把整个知识库塞进 prompt?”。给定正文约束“它没见过你的文档,只能凭「常识」瞎答 → 幻觉。”,哪些判断保持了原有处理机制?多选
2“RAG 入门”出现偏差:“在“RAG 入门 / RAG 全景:离线建索引 + 在线问答,两条流水线”中,即使不满足“离线建索引 ≈ 打包构建期(把内容预处理好、建好索引,跑一次),在线问答 ≈ 运行时请求(用户每来一次就走一遍检索 + 生成)”,结果与副作用仍会保持不变。”已成为实际行为。围绕“RAG 全景:离线建索引 + 在线问答,两条流水线”与“第一步 chunking:为什么要把文档切碎”,哪些判断能定位被改变的职责或边界?多选
3评审“RAG 入门”方案时,验收条件包含“这一步直接复用第 25 篇的 Embedding 能力。”。关于“第二步检索:向量库就是「语义版的搜索引擎」”与“离线:把片段灌进库”的哪些决策符合正文机制?多选