Day 28 / 共 68 天 · 阶段 4 RAG
RAG 实战:给一个 PDF 文件夹做问答
阶段 4 收官日!前 7 天(Day 21-27)你把 RAG 六步和评测拆开学透了,今天把它们串成一个能跑、能放进简历的小项目:丢一个装满 PDF 的文件夹进去,用 LlamaIndex 几行代码摄入→切分→嵌入→建库,然后就能对着这些文档提问,还会亮出答案来自哪一页。跑通它,你就有了作品集的第一块砖;明天(Day 29)进入阶段 5,让模型学会"动手调工具"。
📍 你在阶段 4(RAG D21-28)的位置 · 今天收官
D25 检索→
D26 rerank&混合→
D27 评测→
D28 实战收官→
D29 工具(阶段5)
💡 用一个类比兜住今天(今天全程沿用「开一家私人问答小店」的世界观)
做 RAG 项目 = 开一家"文档问答小店"。摄入 PDF = 进货,把一箱箱资料搬进店;建索引(切+嵌+存) = 上架整理,把资料裁成便签、贴上气味卡、码进货架(向量库);提问(检索+生成) = 顾客来问,店员(检索)先翻出相关便签,再由写手(大模型)照着便签写答复;亮出来源 = 答复末尾附上"依据见第 X 页",顾客可核对、不怕你瞎编;验收 = 用 Day27 的尺子给小店服务质量打分。今天你从"学徒"正式"开张营业"。
L01
今天要造的东西 & 全景图
🤔 痛点学了一堆零件(embedding、向量库、检索、rerank、评测),但它们怎么拼成一个真能用的东西?散装知识不等于会做项目。
💡 本质今天的目标很具体:一个命令行小程序——你把 PDF 放进
docs/ 文件夹,它读进来、建好索引,然后你在终端问问题、它带着"出处"回答。用 LlamaIndex(RAG 最主流框架之一)最少的代码搭起来,重点是把 Day 21-27 的每一步对上号。图注:①②③④是"上架"(慢,但只做一次并存盘),⑤⑥是"接客"(每次提问都跑,要快)。看懂这条分界线,是理解 RAG 工程的关键。
L02
第一步:把 PDF 读进来
🤔 痛点文档是 PDF、还一整个文件夹,格式各异——怎么统一读成"程序能处理的文字"?
💡 本质LlamaIndex 提供
SimpleDirectoryReader:指一个文件夹,它自动把里面的 PDF/txt/md 都读成一个个 Document 对象(文字+来源信息)。像进货时不管纸箱还是麻袋,先统统拆开、把里面的资料摊平。# 先把要问答的 PDF 都丢进 docs/ 文件夹
mkdir docs
# ...把几个 pdf 拷进 docs/ ...
python -m venv .venv && source .venv/bin/activate # 建并进车间
pip install llama-index # RAG 主流框架(一装带齐大部分依赖)
export OPENAI_API_KEY=sk-你的key # LlamaIndex 默认用 OpenAI 做嵌入和生成
from llama_index.core import SimpleDirectoryReader
# 读 docs/ 下所有文件,变成 Document 列表(每个含正文 + 来源文件名/页码)
docs = SimpleDirectoryReader("docs").load_data()
print(f"读入 {len(docs)} 个文档片段")
print(docs[0].text[:80]) # 瞄一眼第一段读到的正文
👶 没有 API key 怎么办本页代码默认用 OpenAI(要 key、要花小钱)。零成本练手可以:①用免费/试用额度的国产模型 API;②用本地 Ollama 跑开源模型 + 本地 embedding。原理完全一样,只是把"用哪个模型"换掉。先跑通流程最重要,省钱方案回忆 Day15。
L03
第二步:建索引(切+嵌+存一条龙)
🤔 痛点Day22-24 分开学了切分、嵌入、建库——难道要自己一步步手动串?那也太琐碎。
💡 本质LlamaIndex 把"切分→嵌入→存进向量库"打包成一句
VectorStoreIndex.from_documents()。它内部就是 Day22-24 那三步,只是替你自动做完。像上架:一句"整理入库",货架就码好了。第一次建好后存到磁盘,下次直接加载,不用重算(省时省钱)。from llama_index.core import VectorStoreIndex, StorageContext, load_index_from_storage
import os
if os.path.exists("storage"): # 已建过?直接加载,别重算
ctx = StorageContext.from_defaults(persist_dir="storage")
index = load_index_from_storage(ctx)
else:
# 一句话完成:切分→embedding→存进(默认内置)向量库
index = VectorStoreIndex.from_documents(docs)
index.storage_context.persist("storage") # 存盘,下次秒加载
print("索引就绪 ✅")
📝 举个例子:这一句里藏着你学过的三天
from_documents(docs) 内部依次做了:把每个文档按长度切成 chunk(Day22)→ 每个 chunk 调 embedding 模型变向量(Day23)→ 连同原文和元数据存进向量库(Day24)。你现在能说清这句话背后发生了什么,就说明前几天没白学。L04
第三步:提问(检索 + 生成)
🤔 痛点索引建好了,怎么真正"问一句、答一句"?检索到的段落又是怎么喂给模型的?
💡 本质从索引造一个
query_engine(查询引擎),你 .query("问题"),它自动:①把问题 embedding→②检索 top-k 相关段(Day25)→③把这几段 + 问题拼成提示词→④发给大模型生成答案。Day25-26 学的检索,就藏在这一步里。# similarity_top_k=3:检索最相关的 3 段(回忆 Day25 的 top-k)
query_engine = index.as_query_engine(similarity_top_k=3)
resp = query_engine.query("公司年假有几天?") # 问一句
print(resp) # 打印模型据检索内容给出的答案
# 做成循环,就是个能反复问的小助手
while True:
q = input("\n问我点啥(回车退出):")
if not q:
break
print(query_engine.query(q))
👶 小白:这才十几行,就是完整 RAG 了?那前面七天讲那么细图啥?
👨🏫 老师:搭一个"能跑的" demo 确实十几行,框架帮你兜了底。但"能跑"和"好用"差着十万八千里——chunk 切多大、top-k 取几、要不要加 rerank、答不准是检索还是生成的锅……这些调优决策,全靠前七天的原理和 Day27 的评测撑着。不懂原理的人,demo 一旦答错就抓瞎;懂的人能对症下药。面试拉开差距的正是这里。
L05
第四步:亮出来源,防幻觉
🤔 痛点模型答得像模像样,但它到底是"照文档答的"还是"自己编的"?用户凭什么信?
💡 本质RAG 的一大杀手锏:答案能附上出处。查询结果里带着"这次用了哪几段、来自哪个文件哪一页"。把它打印出来,用户能一键核对——这既是信任感,也是最朴素的防幻觉手段(呼应 Day27 的忠实度)。像小店回复顾客时附一句"依据见手册第 12 页"。
resp = query_engine.query("公司年假有几天?")
print("答案:", resp, "\n")
print("依据来自:")
for node in resp.source_nodes: # 这次答案引用的检索片段
src = node.metadata.get("file_name", "未知文件")
page = node.metadata.get("page_label", "?")
print(f" · {src} 第 {page} 页(相关度 {node.score:.2f})")
print(f" “{node.text[:50]}...”") # 原文摘录,可核对
👶 有了出处就不会幻觉了吗不能保证 100%。模型仍可能"看着资料还答歪"。所以出处是让人能核对的第一道防线,更严的做法(证据 ID 核对、Critic 二次校验)在第 10 阶段(Day53)专门讲。但"答案带出处"这一条,已经能滤掉大量明显瞎编,务必默认开启。
L06
验收 & 调优清单
🤔 痛点跑起来了,可它到底好不好用?哪里不好该先动哪个旋钮?
💡 本质用 Day27 的尺子验收:准备 5~10 道"问题+标准答案",跑一遍看指标,再对症下药。别一次改一堆,一次只动一个旋钮、看分数升降——这才是工程化的调优。
| 症状 | 大概率病因 | 先试的旋钮 |
|---|---|---|
| 该答的段根本没检索到 | 切分/检索问题(检索层指标低) | 调 chunk 大小、加 overlap、上混合检索(Day26) |
| 相关段捞到了但排在后面 | 排序不精 | 加 rerank 精排(Day26)、调大 top_k 再精排 |
| 资料对但答案跑题/啰嗦 | 生成层(提示词) | 改提示词、换更强的模型(Day16/17) |
| 答案编造、超出资料 | 忠实度低 | 提示词强调"只依据资料答"、亮出处、加校验 |
📝 举个例子:一次真实调优
初版 hit rate 0.6、答案常跑题。第一步只把 chunk 从 1024 调到 512 → hit rate 升到 0.8(检索层修好了)。第二步只改提示词加"仅依据下面资料回答,不知道就说不知道" → 忠实度明显上升。一次一个旋钮,每步都用分数确认——这就是能写进简历和面试的"迭代过程"。
L07
今日小结 + 动手 10 分钟
🧠 今天你应该能回答
- RAG 端到端六步是什么?哪几步是"离线建索引",哪几步是"在线查询"?
VectorStoreIndex.from_documents()这一句内部替你做了 Day22-24 的哪三件事?- 为什么索引要存盘、下次直接加载?
- 答案为什么要亮出处?它和 Day27 的忠实度有什么关系?
- 系统答不准时,你怎么判断是检索的锅还是生成的锅?各先调什么?
✋ 动手 10 分钟:跑通你的第一个 RAG
把 L02-L05 的代码拼成一个 rag.py,往 docs/ 里丢 1~2 个你手边的 PDF(说明书、简历、任意文档都行),跑起来问几句:
mkdir rag_demo && cd rag_demo
mkdir docs # 把 1~2 个 pdf 拷进 docs/
python -m venv .venv && source .venv/bin/activate
pip install llama-index
export OPENAI_API_KEY=sk-你的key # 或换国产/本地模型
# 新建 rag.py:粘 L02(读) + L03(建索引) + L04(query循环) + L05(打印出处)
python rag.py # 开始问答!问文档里真实存在的信息,看它能不能带出处答对
作品集提示:这个小项目稍加打磨(加个 README、录段演示、补上 Day27 的评测数字),就是简历上拿得出手的第一个 RAG 项目。Day62 会教你把它包装成完整作品。
明日预告 · Day 29:阶段 4(RAG)到此圆满收官🎉!明天进入阶段 5:工具与 MCP。第一课讲 Function Calling(函数调用)原理——大模型本来只会"吐文字",怎么让它学会说"我要调哪个函数、传什么参数",然后由你的代码去执行、再把结果回填给它。这是从"会聊天的模型"迈向"能动手的 Agent"的分水岭,别错过!