AI 应用开发 Day 10:从 RAG Demo 到工程化——Document、Chunk 与 Metadata Filter
前几天已经手动实现了一个最基础的 RAG 系统:
用户问题 → Embedding → Vector Search → TopK → Rerank → LLM
Day 9 又进一步学习了 Chunk、Recall、Precision、TopK、Rerank、Hybrid Search 等 RAG 检索优化技术。
到了 Day 10,我开始思考一个更实际的问题:
如果现在不是 knowledge.txt 里面只有 5 条知识,而是公司有几千个 PDF、Word、TXT 文档,这套 RAG 应该怎么处理?
这也是 Day 10 的核心:
从“能跑的 RAG”开始向“具备工程能力的 RAG”迈进。
一、Day 10 学习目标
Day 10 主要学习以下几个概念:
- Document Loader
- Document
- Chunk
- Metadata
- Metadata Filter
- Incremental Update
- RAG Pipeline
其中最重要的是:
Metadata Filter → Vector Search → Rerank → LLM
通过 Metadata 先缩小检索范围,再进行向量搜索,可以减少无意义的候选数据,提高检索效率。
二、为什么之前的 RAG 还不够工程化?
之前为了学习 RAG,我直接在代码里写:
documents = [
{
"text": "公司工资于每月15日发放。",
},
{
"text": "公司工作时间为周一至周五,每天上午9点到下午6点。",
},
{
"text": "员工入职满一年后可以享受带薪年假。",
}
]
这种方式非常适合学习 RAG 的基本原理。
但真实企业项目显然不会这样做。
实际情况可能是:
knowledge/
├── 薪资制度.pdf
├── 员工手册.pdf
├── 上海项目合同.pdf
├── 产品说明书.docx
├── 技术规范.txt
└── 项目资料/
├── 项目A.pdf
├── 项目B.pdf
└── 项目C.pdf
这时候我们需要解决几个问题:
- 怎么读取这些文件?
- PDF / Word 怎么转换成文本?
- 文本怎么切 Chunk?
- Chunk 怎么知道自己来自哪个文件?
- 查询时能不能只搜索某一类文档?
- 新增一个文件的时候,难道要把所有文件重新 Embedding 一遍吗?
Day 10 就是开始解决这些问题。
三、Document Loader:先把文件变成文本
大模型和 Embedding 模型最终处理的核心还是文本。
因此 PDF、Word、TXT 等文件进入 RAG 之前,需要先进行文本提取。
整体流程:
PDF / Word / TXT
↓
Document Loader
↓
Text
↓
Chunk
↓
Embedding
↓
Vector DB
例如:
薪资制度.pdf
↓
PDF Loader
↓
公司工资于每月15日发放。
公司年度奖金根据绩效进行发放。
员工入职满一年后可以享受带薪年假。
所以需要明确一个概念:
RAG 并不是直接理解 PDF 文件,而是先把 PDF 转换成可以处理的文本。
以后使用 LangChain、LlamaIndex 等框架时看到:
PDFLoader(...)
本质上做的就是类似的事情。
四、Document → Chunk
拿到完整文本以后,下一步仍然是 Chunk。
之前我的实现非常简单:
chunks = [
chunk.strip()
for chunk in text.split("\n\n")
if chunk.strip()
]
例如:
公司工资于每月15日发放。
公司工作时间为周一至周五,每天上午9点到下午6点。
员工入职满一年后可以享受带薪年假。
通过:
text.split("\n\n")
可以得到:
Chunk 1:公司工资于每月15日发放。
Chunk 2:公司工作时间为周一至周五,每天上午9点到下午6点。
Chunk 3:员工入职满一年后可以享受带薪年假。
这种方式非常适合 Day 8、Day 9 的学习。
但真实文档通常没有这么规整。
所以后续需要进一步使用:
- Chunk Size
- Chunk Overlap
- Token Chunking
- 文本清洗
等方式进行更加合理的切分。
五、为什么 Chunk 之后还需要 Metadata?
这是 Day 10 最重要的知识之一。
之前的 Vector DB 主要保存:
ID
Document
Embedding
但是在真实企业知识库中,仅仅知道文本内容是不够的。
例如:
公司的办公地点位于上海。
我们还需要知道:
这个内容来自哪里?
属于什么类型?
来自哪个项目?
属于哪个部门?
来自 PDF 的第几页?
因此可以给 Chunk 增加 Metadata:
{
"category": "office",
"source": "company.txt",
"file_type": "txt"
}
甚至可以进一步设计成:
{
"source": "上海项目合同.pdf",
"file_type": "pdf",
"page": 12,
"category": "contract",
"project_id": "SH-2026-001"
}
最终一个知识 Chunk 可以理解成:
┌──────────────────────────────┐
│ ID │
│ │
│ Text │
│ │
│ Embedding │
│ │
│ Metadata │
│ ├── source │
│ ├── file_type │
│ ├── category │
│ ├── page │
│ └── project_id │
└──────────────────────────────┘
这就比单纯存一个向量强很多。
六、Day 10 实战:给 Chroma 增加 Metadata
我的知识库文件:
knowledge.txt
内容:
公司工资于每月15日发放。
公司工作时间为周一至周五,每天上午9点到下午6点。
员工入职满一年后可以享受带薪年假。
公司每周五下午提供下午茶。
公司的办公地点位于上海。
切分以后有 5 个 Chunk。
我为它们增加 Metadata:
metadatas = [
{
"category": "salary"
},
{
"category": "work_time"
},
{
"category": "vacation"
},
{
"category": "afternoon_tea"
},
{
"category": "office"
}
]
然后写入 Chroma:
collection.add(
ids=[
f"doc_{index}"
for index in range(len(chunks))
],
documents=chunks,
embeddings=embeddings,
metadatas=metadatas
)
这样 Chroma 中的数据就变成:
doc_0
工资
category = salary
doc_1
工作时间
category = work_time
doc_2
年假
category = vacation
doc_3
下午茶
category = afternoon_tea
doc_4
办公地点
category = office
七、Metadata Filter
有了 Metadata 后,我们就可以在搜索之前进行过滤。
例如用户问:
公司在哪里办公?
我们已经知道这是一个 office 类型的问题。
那么就可以:
where={
"category": "office"
}
进行过滤。
完整查询:
results = collection.query(
query_embeddings=[query_vector],
n_results=5,
where={
"category": "office"
}
)
原本有:
5 条知识
经过 Metadata Filter:
5 条
↓
category = office
↓
1 条
最终只需要对:
公司的办公地点位于上海。
进行后续处理。
八、Metadata Filter 为什么要放在 Vector Search 前面?
这一点非常重要。
正确流程是:
Metadata Filter
↓
Vector Search
↓
Rerank
↓
LLM
而不是:
Vector Search
↓
Rerank
↓
Metadata Filter
为什么?
假设数据库里面有:
10000 条知识
其中真正属于:
上海项目
的只有:
100 条
那么:
10000 条
↓
Metadata Filter
↓
100 条
↓
Vector Search
↓
TopK
↓
Rerank
显然比:
10000 条
↓
Vector Search
↓
TopK
↓
Rerank
↓
再过滤
更加合理。
可以把 Metadata Filter 简单理解成:
先用结构化条件缩小搜索范围,再用语义搜索寻找最相关内容。
这和 SQL 中的:
WHERE project_id = 'SH-2026-001'
有一点类似。
九、Metadata Filter + Vector Search + Rerank
到这里,我们 Day 9 学习的 Rerank 也可以和 Metadata Filter 串起来。
完整流程:
用户问题
↓
Query Embedding
↓
Metadata Filter
↓
Vector Search
↓
TopK
↓
Rerank
↓
TopN
↓
Context
↓
DeepSeek
↓
Answer
可以进一步理解成:
Metadata Filter
↓
负责缩小搜索空间
Vector Search
↓
负责语义召回
Rerank
↓
负责精确排序
LLM
↓
负责理解 Context 并生成答案
也就是:
Filter 负责缩小范围,Vector Search 负责召回,Rerank 负责精排,LLM 负责回答。
十、Day 10 遇到的一个实际问题:为什么 Metadata 明明写了却查询不到?
这次练习中,我还遇到了一个非常典型的问题。
最开始修改代码:
metadatas = [
{"category": "salary"},
...
]
然后查询:
where={
"category": "office"
}
结果却发现:
[None, None, None, None, None]
也就是说,Chroma 中原来的数据没有 Metadata。
原因很简单:
修改建库代码,并不会自动修改已经存在的 Vector DB 数据。
之前的 Chroma 数据已经被持久化到了:
./chroma_db
当我修改:
collection.add(...)
只是修改了“以后怎么写数据”。
它不会自动把历史数据重新写一遍。
十一、删除 Chroma 重新构建
学习阶段最简单的解决方法就是删除:
chroma_db/
然后重新执行:
python file_rag_demo.py
重新:
读取文件
↓
Chunk
↓
Embedding
↓
Metadata
↓
写入 Chroma
重新构建后检查:
data = collection.get()
print(data["metadatas"])
最终得到:
[ {'category': 'salary'}, {'category': 'work_time'}, {'category': 'vacation'}, {'category': 'afternoon_tea'}, {'category': 'office'}]
然后再执行查询:
where={
"category": "office"
}
最终成功得到:
Vector Search TopK:
0.9698 - 公司的办公地点位于上海。
Rerank:
0.9749 - 公司的办公地点位于上海。
最终交给 DeepSeek:
公司的办公地点位于上海。
DeepSeek 最终回答:
根据知识库中的信息,公司的办公地点位于上海。
至此,Metadata Filter 整个链路验证成功。
十二、这次排错让我理解了一个非常重要的工程问题
RAG 系统不是一个整体黑盒。
它实际上由多个阶段组成:
Document Loader
↓
Chunk
↓
Embedding
↓
Vector DB
↓
Metadata Filter
↓
Vector Search
↓
Rerank
↓
Context
↓
LLM
出现问题的时候不能只看最终答案。
应该逐层检查:
① 文件有没有正确读取?
↓
② Chunk 有没有正确生成?
↓
③ Embedding 有没有生成?
↓
④ Vector DB 有没有写进去?
↓
⑤ Metadata 有没有写进去?
↓
⑥ Filter 有没有生效?
↓
⑦ Vector Search 有没有找到正确内容?
↓
⑧ Rerank 排序是否正确?
↓
⑨ Context 是否正确?
↓
⑩ LLM 是否基于 Context 回答?
这其实已经开始从“学习 API”转向真正的工程排错思维。
十三、Incremental Update:为什么不能每次都重建知识库?
Day 10 还学习了一个非常重要的概念:
增量更新(Incremental Update)。
假设公司知识库已经有:
10000 个 Chunk
现在新增:
一个 PDF
如果每次都:
删除整个 Vector DB
↓
10000 个 Chunk
↓
重新 Embedding
↓
重新写入
显然效率非常低。
更合理的方式是:
原来的 10000 个 Chunk
+
新增 PDF
↓
只处理新增 PDF
↓
Chunk
↓
Embedding
↓
写入 Vector DB
也就是:
知识库应该支持增量构建,而不是每次全量重建。
当然,在 Day 10 的学习 Demo 中,为了方便验证 Metadata,我使用了删除后重新创建 Collection 的方式。
真实项目中不会这么做。
十四、Day 10 最终 RAG 架构
经过前几天的学习,我现在的 RAG 已经从:
Question
↓
Embedding
↓
Vector Search
↓
LLM
逐渐升级成:
Knowledge Build
│
Documents
│
▼
Chunk
│
▼
Metadata
│
▼
SiliconFlow Embedding
│
▼
Chroma
│
│
▼
User Question → Query Embedding
│
▼
Metadata Filter
│
▼
Vector Search
│
▼
TopK
│
▼
Rerank
│
▼
TopN
│
▼
Context
│
▼
DeepSeek
│
▼
Answer
这已经开始接近一个真实企业知识库的基础架构。
十五、Day 10 的几个核心认知
经过 Day 10,我对 RAG 有了几个新的理解。
1. RAG 不只是 Vector Search
真正的 RAG 检索往往是多个步骤组合:
Filter
→ Vector Search
→ Rerank
而不是简单:
Question → Vector DB → TopK
2. Metadata 是企业知识库非常重要的一部分
Vector 负责:
语义检索
Metadata 负责:
结构化过滤
两者结合后,可以同时利用:
语义信息
+
结构化信息
3. Vector DB 不只是存向量
一个真正有用的知识记录应该至少包含:
ID
Text
Embedding
Metadata
因为:
向量负责找,原文负责回答,Metadata 负责控制搜索范围。
4. RAG 是一个 Pipeline
RAG 出问题时,不能只看 LLM。
需要逐层排查:
Loader
→ Chunk
→ Embedding
→ Vector DB
→ Filter
→ Search
→ Rerank
→ Context
→ LLM
5. Demo 和生产系统的最大区别之一就是“数据更新”
Demo 可以:
删库
→
重新构建
生产系统需要考虑:
新增
修改
删除
增量 Embedding
Metadata 更新
Vector DB 同步
这也是后续真正做企业 RAG 时必须解决的问题。
十六、Day 10 总结
Day 10 并没有引入什么特别复杂的新框架,但它让我开始从另一个角度理解 RAG:
RAG 不只是让 LLM “查知识”,而是一套完整的数据处理、检索、排序和生成 Pipeline。
今天最重要的一条链路是:
Document
↓
Chunk
↓
Metadata
↓
Embedding
↓
Vector DB
↓
Metadata Filter
↓
Vector Search
↓
Rerank
↓
LLM
其中:
Metadata Filter 负责缩小范围
Vector Search 负责语义召回
Rerank 负责精确排序
LLM 负责基于检索结果生成最终答案
到这里,我的 RAG 已经不再只是一个简单的“向量搜索 Demo”,而是开始具备真实企业知识库的基本形态。
下一步进入 Day 11:
从手写 knowledge.txt ,升级到真正读取 PDF / Word / TXT 等企业文档。
也就是开始解决一个更加现实的问题:
“用户上传一个 PDF,我的 RAG 怎么自动把它变成可以检索的知识?”