十一步搭出第一条 RAG 管道
这一章讲一件事: 一个能跑的 RAG 应用,从头到尾由哪些步组成、每一步长什么样。 它是全书唯一一条「主走查」章——后面每一章都是给这条管道的某一段升级。 读完你应该能自己复述:一个用户问题进去,中间发生了什么,答案和出处怎么出来。
1. 这一章在全书的位置
原书第 2 章用一个 Jupyter 代码实验室搭出完整管道,第 3 章末尾给它加上「返回出处」, 第 4 章回头按组件拆解它,第 6 章给它套上界面。这四章其实是一条装配线,我们并成一章讲。 之后每一章的新技术,都会标明它替换的是这条管道的哪个零件。
作者在章末有一句坦白,值得先记住:网上搜「RAG 的问题」能搜到上百万条—— 这套最简管道只在最简单的场景好用,书剩下的部分都是在修它的毛病1。
2. 主走查:一个问题走完整条管道
走查输入(全书反复用的同一个问题): What are the advantages of using RAG?
装包与密钥 → 抓网页 → 切块 → 向量化入库 → 建检索器 → 拉提示词模板 → 定义 LLM → 组链 → invoke
(步骤0) (1) (2) (3) (4) (5) (6) (7) (8)
图说:0-4 步是索引阶段,通常提前做好;5-8 步是检索+生成,发生在用户提问的一瞬间。
步骤 0:装包与密钥
固定版本安装 LangChain 1.x 全家桶、Chroma、BeautifulSoup;API 密钥不写进代码,
放在 env.txt 里用 load_dotenv 加载,并加进 .gitignore2。
作者特别提醒:环境里已有的旧版包要先卸载再装指定版本,否则依赖冲突3。
步骤 1:抓网页(数据进来的地方)
用 WebBaseLoader 抓一个网页,并且用 SoupStrainer 只解析(即拆开网页并挑出需要的部分)post-content、post-title、
post-header 三个 CSS 类——网页上的广告、导航栏就被挡在外面了4。
这里埋着第一个坑:换一个网页,这套 CSS 类名大概率不存在,抓回来就是空的。 作者明确说:用别的 URL 拿不到数据时,先去看那个页面的 HTML 里用了什么类名5。 还有一个小坑:换了网页要重启 kernel 重跑,否则旧网页的内容还留在库里,两份数据混在一起6。
步骤 2:切块
RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200):
把网页文本切成约 1000 字符的小块,相邻块之间重叠 200 字符7。
为什么切? 两个原因:一是 embedding 服务对输入长度有上限(OpenAI 这边是 8,191 token, 超了直接报错);二是块越大,一个向量要表达的「意思」越稀释,检索就越不准8。
为什么要重叠? 想象一条法律文档里恰好有个地址,不重叠的切割可能正好把它从中间切成两半—— 两块里都只有半个地址,搜哪个都搜不全。200 字符的重叠让边界内容在两块里各出现一次9。
步骤 3:向量化(即把每块文字转成向量)入库
Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings()) 一行,
把每个块发给 OpenAI 的 embedding 服务换成一串数字(向量),连原文一起存进 Chroma 向量数据库10。
向量的细节第 04 章专讲,这里只需要知道:存进去的是「意思的数学表示」,不是原文本身——原文跟着向量一起存,答案要用原文。
步骤 4:建检索器
retriever = vectorstore.as_retriever()。注意时机:检索器在索引阶段就建好了,但直到用户提问才被使用——
它先建好基础设施,等在那儿11。
查询向量化发生在哪? 这是初学者最容易糊涂的点:你并没有看到「把用户问题变成向量」的代码。
答案是它被包进了 .as_retriever():这个方法内建了「把查询用同一个 embedding 模型变成向量,
再去库里做相似搜索」的全部逻辑12。
步骤 5:拉一个现成的提示词模板
hub.pull("jclemens24/rag-prompt") 从 LangChain Hub 拉一个社区模板,它接受两个变量:
context(检索到的材料)和 question(用户的问题),模板正文的核心指令就是第 01 章见过的那句
「照材料答,答不出就说不知道」13。
把检索结果填进模板变量的动作,书里给了名字:hydrating(灌注)14。
步骤 6:定义 LLM
llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)。选 gpt-4o-mini 是因为它便宜——
作者的经济账贯穿全书:不是所有步骤都需要最贵的模型15。
步骤 7:组链(LCEL)
rag_chain = (
{"context": retriever | format_docs,
"question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
LCEL 是 LangChain 的管道语法(规定管道怎么写的规则),| 把前一步的输出接给后一步。这一段的读法:
- 第一个环节是个小链 条:
retriever | format_docs——检索器返回的不是一个字符串,是文档对象的列表, 而提示词模板要的是字符串;format_docs用换行符把它们拼成一个字符串16。 - 问题本身已经是字符串,
RunnablePassthrough()让它原样通过,什么都不做17。 - 两股汇成字典
{context, question},灌进模板,喂给 LLM,最后StrOutputParser()把 LLM 返回的 JSON 结构剥开,只留回答文本18。
步骤 8:invoke,拿到回答
rag_chain.invoke("What are the advantages of using RAG?") 一行触发全程。
返回的回答分三点(准确性/定制灵活/扩展训练外知识)——内容全部来自步骤 3 存进去的那篇网页,
也就是说,这个回答里关于 RAG 的每句话都有出处,不靠模型自己的记忆19。
钱花在哪: 这次调用中 LLM 用的模型几乎免费,真正计费的是 embedding—— OpenAI 的价格是 $0.10 每 100 万 token,这个问题被算成 10 个 token, 成本 $0.000001。作者专门为此写了一段「完全透明」的成本说明20。
3. 一个实验:去掉检索会怎样
书里做了一个对照实验,直接问 GPT-3.5「RAG 有哪些优点」——它答成了项目管理里的 RAG 状态报告(Red, Amber, Green,红黄绿灯),与检索增强生成毫无关系21。
原因很清楚:GPT-3.5 的训练数据截止于 2022 年 1 月,那时「RAG 作为生成式 AI 概念」还不流行22。
这个实验是第 01 章那句「LLM 没见过它训练数据之外的东西」的最生动注脚—— 连一个缩写词,模型都可能理解成完全不同的东西。 RAG 管道的价值在这一刻变得具体: 有检索垫底,模型至少知道你在问什么。
4. 拼图一:给答案带上出处
客服、法务、科研场景都要求回答能引用来源。原书第 3 章末的代码实验室改了一处结构23:
rag_chain_with_source = RunnableParallel(
{"context": retriever,
"question": RunnablePassthrough()}
).assign(answer=rag_chain_from_docs)
关键变化是 RunnableParallel:原来 retriever | format_docs 是串行的——检索结果先被拼成字符串,
原文的元数据——即「数据的数据」,比如来源 URL——就丢了。
并行(即两件事同时做、不用等)版让检索结果原样走一条路(保留每个块的 metadata.source),同时另一条路去走「拼接→提示词→LLM」。最后输出里既有回答,也有一个来源列表24。
书里运行的输出显示每个检索块都带着 source: https://kbourne.github.io/chapter1.html 这样的链接。
作者点出这个功能对用户的三重价值:理解答案的依据、事实核查、在答案基础上继续工作25。
5. 拼图二:界面(Gradio)
管道跑通后还差两样:用户怎么输入问题、结果怎么展示。原书第 6 章用 Gradio 十几行代码解决:
一个 gr.Interface,输入框预填那个老问题,输出三个文本框(相关性分数/最终回答/来源),
demo.launch(share=True) 启动本地 web 服务并生成一条 72 小时有效的分享链接26。
需要如实转述的两条边界:
- Gradio 只适合原型。 作者明说它撑不起成千上万用户的生产级应用,那个阶段请找专业前端27;
- 它的认证功能是明文密码,无加密、无防暴力破解、无会话超时,只够小范围演示用; 生产环境要哈希(即把原文搅成不可还原的乱码再存)密码、HTTPS、限流那一套28。
界面里显示的「相关性分数」来自安全章(第 03 章)的守护 LLM 机制,原书把它放在界面上只是教学演示, 真实产品通常不把它亮给用户29。
6. 组件视角:把管道看成七个零件
原书第 4 章把同一条管道按组件重新拆了一遍,除了已讲的三阶段,还补了三个零件:
| 零件 | 职责 | 后续哪章升级它 |
|---|---|---|
| 提示词 | 定义 LLM 怎么用检索材料 | 第 08 章(agent 改写问题) |
| LLM | 生成最终回答 | 第 07 章(可换 Llama/Mixtral) |
| 界面 | 收问题、显示答与出处 | 本章 §5 |
| 评测 | 用指标和用户反馈持续改进 | 第 06 章 |
评测零件里有个容易忽略的设计:界面上收一个「点赞/点踩」,作者把它定位成系统改进的信号源—— 这些反馈进入评测,评测再反哺检索与生成的调整30。
7. 边界与局限
- 这条管道的检索只取「最相似的几块」,没有过滤、没有重排——第 05 章的混合检索就是修这个的。
- 只有一个数据源(一篇网页),出处的价值展示不出来(四个来源全是同一个 URL)31。
- 切块参数(1000/200)是拍脑袋的默认值;原书第 11 章会展示「切得不好」长什么样(第 07 章 §4)。
- 整条链没有安全层:提示词注入攻击可以套出系统提示词与库内数据——这是第 03 章的主题。
8. 可带走的
- 一条 RAG 管道 = 数据侧(抓取→切块→向量化入库)+ 查询侧(检索→灌注模板→生成)。
- 切块三参数:块大小、重叠、分隔符( 分隔符即用来判断「在哪里切」的字符,比如换行符);重叠是防「地址被切成两半」的保险。
- 查询向量化藏在
.as_retriever()里,查询与文档必须用同一个 embedding 模型。 - 检索返回的是对象列表,模板要字符串——中间必须有一个格式化函数。
- 出处功能的关键是把检索结果「原样」并行送出来,别让格式化提前毁掉元数据。
- 成本敏感点在 embedding 调用与 token 计费,但单个查询的量级是百万分之一美元——规模化才需要精打细算(第 11 章的语义缓存)。
- 最简管道离生产还很远:书的后半部分,本质是给这条管道逐段加装的工程史。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 百万条 RAG 问题、本书定位 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:764(搜「millions of questions」) |
| 环境与成本(colab/本地) | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:43(搜「$1,000 per month」) |
| env.txt 密钥管理 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:276(搜「out of your code」) |
| 固定版本安装与冲突 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:154(搜「dependency conflicts」) |
| SoupStrainer 只抓三个类 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:379(搜「parse_only」) |
| 换网页看 CSS 类的坑 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:406(搜「CSS tags」) |
| 换网页重启 kernel | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:349(搜「restart your kernel」) |
| 切块参数与递归切分 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:436(搜「RecursiveCharacterTextSplitter is the recommended」) · text/05-fm-code-lab-an-entire-rag-pipeline.txt:443(搜「chunk_size=1000」) |
| 切分原因与 token 上限 | Components of a RAG System | text/07-fm-components-of-a-rag-system.txt:149(搜「8191 token limit」) |
| 重叠防切半的例子 | Components of a RAG System | text/07-fm-components-of-a-rag-system.txt:149(搜「cut an address in half」) |
| from_documents 存向量 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:471(搜「Chroma.from_documents」) |
| 检索器先建后用 | Components of a RAG System | text/07-fm-components-of-a-rag-system.txt:167(搜「ready and waiting」) |
| 查询向量化藏在 as_retriever | Components of a RAG System | text/07-fm-components-of-a-rag-system.txt:251(搜「as_retriever() method has all of the functionality」) |
| 模板拉取与变量 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:559(搜「jclemens24/rag-prompt」) · text/05-fm-code-lab-an-entire-rag-pipeline.txt:568(搜「input_variables」) |
| hydrating 定义 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:539(搜「hydrating」) |
| gpt-4o-mini 便宜 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:640(搜「significant discount」) |
| format_docs 拼接 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:608(搜「format_docs」) |
| Passthrough 与 StrOutputParser | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:676(搜「StrOutputParser()」) |
| invoke 与输出 | Code Lab: An Entire RAG Pipeline | text/05-fm-code-lab-an-entire-rag-pipeline.txt:692(搜「What are the advantages of using RAG?」) · text/05-fm-code-lab-an-entire-rag-pipeline.txt:709(搜「Improved Accuracy and Relevance」) |
| embedding 成本 0.000001 美元 | Components of a RAG System | text/07-fm-components-of-a-rag-system.txt:258(搜「$0.10 per 1M tokens」) |
| GPT-3.5 答成红绿灯 | Components of a RAG System | text/07-fm-components-of-a-rag-system.txt:394(搜「Red, Amber, Green」) |
| 训练截止 2022 年 1 月 | Components of a RAG System | text/07-fm-components-of-a-rag-system.txt:396(搜「January 2022」) |
| RunnableParallel 并行 | Practical Applications of RAG | text/06-fm-practical-applications-of-rag.txt:288(搜「RunnableParallel」) |
| 出处在 metadata.source | Practical Applications of RAG | text/06-fm-practical-applications-of-rag.txt:356(搜「metadata source listed」) |
| 出处三重价值 | Practical Applications of RAG | text/06-fm-practical-applications-of-rag.txt:353(搜「fact-checking」) |
| Gradio 界面与 72 小时链接 | Interfacing with RAG and Gradio | text/10-fm-interfacing-with-rag-and-gradio.txt:159(搜「demo.launch」) · text/10-fm-interfacing-with-rag-and-gradio.txt:202(搜「expires in 72 hours」) |
| Gradio 不适合生产 | Interfacing with RAG and Gradio | text/10-fm-interfacing-with-rag-and-gradio.txt:82(搜「proof-of-concept」) |
| 认证是明文 | Interfacing with RAG and Gradio | text/10-fm-interfacing-with-rag-and-gradio.txt:175(搜「plain text」) |
| 分数不展示给用户 | Interfacing with RAG and Gradio | text/10-fm-interfacing-with-rag-and-gradio.txt:214(搜「likely will not be displayed」) |
| 一个数据源四个相同来源 | Interfacing with RAG and Gradio | text/10-fm-interfacing-with-rag-and-gradio.txt:220(搜「all four sources are the same」) |
| 评测组件与反馈 | Components of a RAG System | text/07-fm-components-of-a-rag-system.txt:480(搜「response time」) · text/07-fm-components-of-a-rag-system.txt:483(搜「thumbs up/down」) |
| 索引离线、检索生成实时 | Components of a RAG System | text/07-fm-components-of-a-rag-system.txt:99(搜「pre-processing offline」) |