Day 03 / 共 20 天 · 第 1 周 RAG 全景与数据摄入
读取数据 Reader(流水线第 ① 步)
地基(Node)有了,正式上流水线。第一步:把散落在各处、格式各异的数据(PDF、网页、数据库)统一"读"成 Document。今天精读最常用的 SimpleDirectoryReader。
📍 你在整条链的位置
数据结构→
① 读 Reader→
② 切块→
③ 嵌入→
④ 存索引→
⑤检索 ⑥合成
L01
第一步:把数据读进来
🤔 你的数据在哪、长什么样?
做公司问答机器人,数据可能是:
data/ 目录下一堆 PDF 和 Word、公司 Notion 知识库、某个数据库的表、几个网页……格式五花八门。而后面的流水线(切块/嵌入/索引)只认一种东西——Document。谁来把这些异构数据统一变成 Document?💡 Reader = 数据的"翻译官"
Reader 的唯一职责:把某种数据源,读成统一的
List[Document]。PDF Reader 读 PDF、Notion Reader 调 Notion API、数据库 Reader 跑 SQL——但产出都是 Document,后面流程完全不用管数据原来是什么格式。L02
为什么这叫"适配器模式"
Reader 把千奇百怪的数据源,统一"适配"成 Document——下游流程无需关心原始格式。
"适配器"就像充电转接头
各国插座形状不同(数据源各异),但你的手机只有一种充电口(Document)。转接头(Reader)负责把任何插座转成你手机能用的口。加一种新数据源 = 加一个转接头(新 Reader),手机(下游流程)不用改。所以 LlamaIndex 有 300+ Reader(LlamaHub)。
L03
BaseReader 接口:极简
readers/base.py 的 BaseReader——所有 Reader 的"合同",核心方法就一个:
class BaseReader:
def load_data(self, ...) -> List[Document]: ... # 就这一个必须实现
def lazy_load_data(self, ...): ... # 可选:惰性版(边读边产出,省内存)
💡 接口越简单,生态越繁荣
因为只需实现一个
load_data,任何人都能轻松写一个新 Reader 贡献出来。这就是为什么 LlamaIndex 能接入几百种数据源——门槛极低。L04
SimpleDirectoryReader:最常用
readers/file/base.py:208:读一个目录下的所有文件——就是 Day 01"5 行 RAG"第一行用的那个。
📝 怎么用
reader = SimpleDirectoryReader(
input_dir="data", # 读 data/ 目录
recursive=True, # 递归读子目录
required_exts=[".pdf",".md"], # 只读 pdf 和 md
)
docs = reader.load_data() # → [Document(财务制度.pdf), Document(手册.md), ...]读法:它自动识别每个文件的类型(.pdf/.docx/.md/.txt…)选对应解析器(L05),还用
fsspec 支持云存储(S3 等),不只本地文件。L05
按后缀选解析器
🤔 PDF 和 Markdown 能用一样的方式读吗?
不能——PDF 是二进制、要专门的库解析出文字;Markdown 是纯文本、直接读。一个目录里混着各种格式怎么办?
💡 file_extractor:后缀 → 解析器的映射表
SimpleDirectoryReader 内部有个映射:
.pdf → PDFReader、.docx → DocxReader、.md → MarkdownReader……遇到某后缀就用对应解析器;没有专用的就当纯文本读。📝 换更强的 PDF 解析器
默认 PDF 解析器对复杂排版(表格、多栏)可能读乱。你可以传自定义
→ 复杂 PDF 的表格/公式也能读对。
file_extractor 换成官方的 LlamaParse:SimpleDirectoryReader("data", file_extractor={".pdf": LlamaParse()})→ 复杂 PDF 的表格/公式也能读对。
读法:这些专用解析器大多在 integrations(
llama-index-readers-file)——回收 Day 01 的"core + 插件":core 定框架,具体 PDF 解析在插件里。L06
并行加载:读 1000 个文件别傻等
🤔 1000 个 PDF 一个个读要多久?
解析 PDF 是 CPU 密集活。1000 个串行读,可能几分钟——太慢。
readers/file/base.py:718-760 的 load_data:
def load_data(self, show_progress=False, num_workers=None, fs=None) -> list[Document]:
if num_workers and num_workers > 1: # 开多进程并行
num_cpus = multiprocessing.cpu_count()
if num_workers > num_cpus: num_workers = num_cpus # 不超过 CPU 核数(超了没意义)
# 用进程池并行读多个文件
# 每个文件 → load_file → 产出若干 Document(一个 PDF 可能每页一个)
💡 多进程并行 + 进度条
num_workers=8 → 8 个文件同时解析,快 8 倍(受 CPU 核数限制)。show_progress=True → 显示进度条(大批量时安心)。L07
自动填 metadata(呼应 Day 02)
💡 读的时候就把"来源"记进 metadata
SimpleDirectoryReader 给每个 Document 自动填 metadata:文件名、路径、类型、大小、创建/修改时间。可传
file_metadata 函数自定义。📝 读出来的 Document 自带来源
Document(text="单次报销上限3000元…",
metadata={"file_name": "财务制度.pdf",
"file_path": "data/财务制度.pdf",
"creation_date": "2024-01-01"})
→ 后面检索命中这块时,就能告诉用户"答案来自 财务制度.pdf"(Day 02 说的溯源)。读法:回收 Day 02——metadata 影响嵌入和溯源。好的 metadata 从"读取"这第一步就要设计好,后面才能用(如按文件过滤检索、答案标出处)。
filename_as_id 选项可用文件名当 Document id(方便 Day 5 的去重更新)。L08
今日小结 + 动手
🧠 今天你应该能回答
- Reader 的唯一职责?为什么说它是"适配器/转接头"?
- BaseReader 接口为什么极简(只有 load_data)?和生态繁荣什么关系?
- SimpleDirectoryReader 怎么按后缀选解析器?怎么换更强的 PDF 解析?
- 为什么读取阶段就要填好 metadata?
✋ 动手
cd /Users/bitmart/work/codes/github/llama_index/llama-index-core/llama_index/core
sed -n '208,260p' readers/file/base.py # SimpleDirectoryReader
sed -n '718,760p' readers/file/base.py # load_data 并行
grep -n "class BaseReader\|def load_data" readers/base.py
ls ../../../../llama-index-integrations/readers/ 2>/dev/null | head # 看看有多少 Reader
明天预告 · Day 04:文档读进来了,但太大——上流水线第 ② 步切块。这是 RAG 里最影响效果的一环。精读 SentenceSplitter:为什么切、chunk_size/overlap 怎么调、为什么按句子边界切。