文章目录
-
- 前言
- 一、今天到底要搞懂啥
- 二、为啥之前那套东西上不了班
- [三、Document Loader:先把文件读成字](#三、Document Loader:先把文件读成字)
- [四、切 Chunk 那点事](#四、切 Chunk 那点事)
- [五、为什么 Chunk 之后还得给它贴标签](#五、为什么 Chunk 之后还得给它贴标签)
- [六、动手:给 Chroma 加 Metadata](#六、动手:给 Chroma 加 Metadata)
- [七、Metadata Filter:先筛一遍再搜](#七、Metadata Filter:先筛一遍再搜)
- [八、为啥 Filter 要放在向量搜索前面](#八、为啥 Filter 要放在向量搜索前面)
- [九、Filter + Vector + Rerank 三件套](#九、Filter + Vector + Rerank 三件套)
- 十、踩坑现场:标签明明写了,为啥查不到
- 十一、学习阶段的流氓解法:删库重建
- 十二、这次排错让我悟了一件事
- [十三、Incremental Update:别动不动就全量重建](#十三、Incremental Update:别动不动就全量重建)
- [十四、现在的 RAG 长啥样](#十四、现在的 RAG 长啥样)
- 十五、今天想明白的几件事
-
- [15.1 RAG 不等于 Vector Search](#15.1 RAG 不等于 Vector Search)
- [15.2 Metadata 是企业库的灵魂](#15.2 Metadata 是企业库的灵魂)
- [15.3 Vector DB 不只是存向量](#15.3 Vector DB 不只是存向量)
- [15.4 RAG 是一条 Pipeline](#15.4 RAG 是一条 Pipeline)
- [15.5 Demo 和生产系统最大的差别,在数据更新](#15.5 Demo 和生产系统最大的差别,在数据更新)
- 十六、收尾
P.S. 推荐一个大神的教程给想要了解或者学习人工智能知识的读者,这个教程里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏教程,请点击下方链接查看, 传送门https://blog.csdn.net/qq_74013365
前言
前几天我吭哧吭哧手写了个 RAG,跑通的那一刻我甚至给自己泡了杯咖啡,觉得自己马上能创业了。
流程大概是这样:用户提问 → Embedding → 向量搜索 → TopK → Rerank → 大模型吐答案。
后来我又补了 Chunk、Recall、Precision、Hybrid Search 这些概念,笔记写了厚厚一沓。
直到今天我才反应过来一个扎心的事实:我那个知识库,一共就 5 条数据,还都是我自己敲在 knowledge.txt 里的。这玩意儿拿去跟老板演示,老板问一句"那我们公司 3000 份合同怎么办",我大概只能现场表演一个离职。
所以今天的主题很朴素:怎么从"能跑的玩具",走向"能上班的系统"。
一、今天到底要搞懂啥
核心就这几个词:
Document Loader、Document、Chunk、Metadata、Metadata Filter、Incremental Update、RAG Pipeline。
其中最要命的一条链路是:
Metadata Filter → Vector Search → Rerank → LLM
先靠元数据把范围砍下来,再去做向量搜索。不然你在 10 万条数据里捞那 100 条相关的,电费都能把你家空调干停机。
二、为啥之前那套东西上不了班
我之前的代码长这样:
documents = [
{ "text": "公司工资于每月15日发放。" },
{ "text": "公司工作时间为周一至周五,每天上午9点到下午6点。" },
{ "text": "员工入职满一年后可以享受带薪年假。" }
]
学习用,没毛病。但真实企业的目录大概长这样:
knowledge/
├── 薪资制度.pdf
├── 员工手册.pdf
├── 上海项目合同.pdf
├── 产品说明书.docx
├── 技术规范.txt
└── 项目资料/
├── 项目A.pdf
├── 项目B.pdf
└── 项目C.pdf
这时候问题就全来了:
PDF 怎么读?Word 怎么转文本?切完 Chunk 它知不知道自己来自哪个文件?用户问"上海项目付款条件是什么",你总不能把所有合同都扫一遍吧?新增一个文件,难道要把 10 万条数据重新 Embedding 一遍,然后让服务器在机房里热到烤鸡蛋?
说白了,之前我那套就像在家里用小炖锅煮了一碗螺蛳粉,还挺香。但真要开馆子,你得考虑后厨排烟、食材进货、菜单管理、客人们还会要求"少辣不加香菜"------这些破事,才叫工程化。
三、Document Loader:先把文件读成字
大模型和 Embedding 模型,本质上只认文本。
所以 PDF、Word、TXT 进来之前,得先过一道"提取"工序:
PDF / Word / TXT
↓
Document Loader
↓
Text
↓
Chunk
↓
Embedding
↓
Vector DB
举个例子:
薪资制度.pdf
↓
PDF Loader
↓
公司工资于每月15日发放。
公司年度奖金根据绩效进行发放。
员工入职满一年后可以享受带薪年假。
记住一句话:RAG 不是真的"看懂"PDF,它只是先把 PDF 扒成了纯文本。
以后你用 LangChain、LlamaIndex 看到 PDFLoader(...) 这种东西,本质就是干这事儿的,别被名字唬住。
就像你点外卖,商家后台其实不知道你长啥样,他只知道你的收货地址、电话和备注"多放辣"。Loader 就是那个把 PDF 翻译成"字"的快递小哥,他自己不读内容,他只负责把东西从 A 运到 B。
四、切 Chunk 那点事
拿到完整文本之后,下一步还是切 Chunk。我之前的切法非常暴烈:
chunks = [
chunk.strip()
for chunk in text.split("\n\n")
if chunk.strip()
]
用双换行一切,完事。对于那种整整齐齐的示例文本,确实好使:
Chunk 1:公司工资于每月15日发放。
Chunk 2:公司工作时间为周一至周五,每天上午9点到下午6点。
Chunk 3:员工入职满一年后可以享受带薪年假。
但真实的 PDF 里,段落是乱的、页眉页脚会混进来、图片说明可能突然插在中间。你要是还靠 split("\n\n"),切出来的东西大概跟我老妈切的土豆块一样------大小随缘,形状看心情。
所以后面要正经学:Chunk Size、Chunk Overlap、Token Chunking、文本清洗。
切 Chunk 这事儿特别像包饺子:皮太大,一口塞不下;皮太小,一煮就破;还不能把馅漏了。我现在的水平大概就是"能包熟",离"好吃"还有距离。
五、为什么 Chunk 之后还得给它贴标签
这是今天最关键的一个概念。
之前我的 Vector DB 里存的东西就三样:ID、文本、Embedding。
但在企业知识库里,光知道"这句话长这样"根本不够。比如你存了一句:
公司的办公地点位于上海。
你还得知道:
它来自哪个文件?属于薪资类还是行政类?是哪个项目的?第几页?哪个部门发的?
所以我们给每个 Chunk 加一个叫 Metadata 的东西:
{
"category": "office",
"source": "company.txt",
"file_type": "txt"
}
再真实一点:
{
"source": "上海项目合同.pdf",
"file_type": "pdf",
"page": 12,
"category": "contract",
"project_id": "SH-2026-001"
}
最后一条知识大概长这样:
┌──────────────────────────────┐
│ ID │
│ Text │
│ Embedding │
│ Metadata │
│ ├── source │
│ ├── file_type │
│ ├── category │
│ ├── page │
│ └── project_id │
└──────────────────────────────┘
Metadata 就像每个人的身份证。你走在街上,光看脸只能认出"这是个人",但你查他身份证才知道他叫啥、住哪、哪个公司的、有没有前科。向量搜索是"看脸找人",Metadata 是"查身份证",俩一起用,警察叔叔效率才高。
六、动手:给 Chroma 加 Metadata
我的知识库还是那个 knowledge.txt,5 条数据:
公司工资于每月15日发放。
公司工作时间为周一至周五,每天上午9点到下午6点。
员工入职满一年后可以享受带薪年假。
公司每周五下午提供下午茶。
公司的办公地点位于上海。
切完 5 个 Chunk,我给它们贴标签:
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:先筛一遍再搜
有了标签之后,搜索之前就能先过滤。
比如用户问"公司在哪办公",这明显是 office 类的问题,那就直接:
where={
"category": "office"
}
完整查询:
results = collection.query(
query_embeddings=[query_vector],
n_results=5,
where={ "category": "office" }
)
原来 5 条数据:
5 条
↓
category = office
↓
1 条
最后就剩一句"公司的办公地点位于上海",交给后面处理。
这就像你去海底捞找人,你朋友说"我在靠窗那桌",你直接走过去就行,不用把每桌都扫一遍问"哎哥们你是不是来吃火锅的"。服务员要是真这么干,海底捞能被你问到停业整顿。
八、为啥 Filter 要放在向量搜索前面
这个顺序非常关键。
正确姿势:
Metadata Filter
↓
Vector Search
↓
Rerank
↓
LLM
错误姿势:
Vector Search
↓
Rerank
↓
Metadata Filter
为啥?假设库里有 10000 条知识,其中真正属于"上海项目"的就 100 条。
先 Filter:
10000 条
↓
Metadata Filter
↓
100 条
↓
Vector Search → TopK → Rerank
先 Vector Search:
10000 条
↓
Vector Search → TopK → Rerank
↓
再过滤
显然后者是有病。
说白了就是:先用结构化条件把范围砍小,再用语义搜索去精搜。
这跟 SQL 里写 WHERE project_id = 'SH-2026-001' 是一个味儿。
你找对象,肯定是先按"同城、单身、年龄合适"筛,再去看照片聊性格,而不是先把全宇宙的人都聊一遍,最后才问"哎你是上海的吗"。后者那叫牛郎织女,不叫相亲。
九、Filter + Vector + Rerank 三件套
到这儿,前几天学的 Rerank 也能串进来了。完整长链路:
用户提问
↓
Query Embedding
↓
Metadata Filter
↓
Vector Search
↓
TopK
↓
Rerank
↓
TopN
↓
Context
↓
DeepSeek
↓
Answer
每个环节干啥活:
Metadata Filter 负责缩小搜索空间
Vector Search 负责语义召回
Rerank 负责精确排序
LLM 负责读 Context 并写答案
一句话:Filter 砍范围,Vector 粗捞,Rerank 精选,LLM 动笔。
十、踩坑现场:标签明明写了,为啥查不到
今天我真被这个问题卡了半小时。
我把代码改成这样:
metadatas = [
{"category": "salary"},
...
]
然后查询:
where={
"category": "office"
}
结果返回:
[None, None, None, None, None]
我人傻了。
最后发现原因特别朴素:你改建库代码,不会自动修改已经存在的 Vector DB。
之前的 Chroma 数据早就持久化到 ./chroma_db 里了。我改 collection.add(...),只是改了"以后怎么写",它不会回头把旧数据重写一遍。
这就像你谈了个对象,分手之后你改了自己的择偶标准,结果你前任还是那个前任------你不能指望她自动升级成你想要的样子。你得重新找,或者......把记忆格式化。
十一、学习阶段的流氓解法:删库重建
最简单的办法就是直接干掉:
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" }
向量搜索 TopK:
0.9698 - 公司的办公地点位于上海。
Rerank:
0.9749 - 公司的办公地点位于上海。
最后交给 DeepSeek,它回我:
根据知识库中的信息,公司的办公地点位于上海。
整条链路,通了。
十二、这次排错让我悟了一件事
RAG 不是一个黑盒,它是一条流水线:
Document Loader
↓
Chunk
↓
Embedding
↓
Vector DB
↓
Metadata Filter
↓
Vector Search
↓
Rerank
↓
Context
↓
LLM
出问题的时候,别光盯着最后那句答案发呆,得一层层往下查:
① 文件读对了吗?
↓
② Chunk 切对了吗?
↓
③ Embedding 生成了吗?
↓
④ Vector DB 写进去了吗?
↓
⑤ Metadata 写进去了吗?
↓
⑥ Filter 生效了吗?
↓
⑦ 向量搜索找到正确内容了吗?
↓
⑧ Rerank 排对了吗?
↓
⑨ Context 拼对了吗?
↓
⑩ LLM 是基于 Context 答的吗?
这感觉就像你肚子疼,医生不会直接让你开刀,他得先问你今天吃啥了、拉了没、发不发烧。RAG 排错也一样,别一上来就怪大模型"你怎么这么笨",它可能只是吃到了一份被切坏的外卖。
十三、Incremental Update:别动不动就全量重建
今天还学了个词,叫增量更新。
假设知识库已经有 10000 个 Chunk,现在你新增一个 PDF。
如果你每次都:
删掉整个 Vector DB
↓
10000 个 Chunk 重新 Embedding
↓
重新写入
这不是写程序,这是在给服务器做电疗。
正确姿势:
原来的 10000 个 Chunk
+
新增 PDF
↓
只处理新增的 PDF
↓
Chunk → Embedding
↓
追加写入 Vector DB
当然,今天这个 Demo 为了方便验证 Metadata,我还是用了删库重建。但真实项目里你敢这么干,运维能顺着网线过来捶你。
这就像你家冰箱里已经放了 100 瓶可乐,今天又买了 1 瓶。你不会把冰箱清空、所有可乐重新摆一遍位置,你就是把新那瓶塞进去而已。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
说真的,看到这张图我有点小激动------这玩意儿终于有点企业知识库那味儿了,不再是我笔记本上跑着玩的小玩具。
十五、今天想明白的几件事
15.1 RAG 不等于 Vector Search
真实的 RAG 是好几步拼起来的:
Filter → Vector Search → Rerank
而不是一句 Question → Vector DB → TopK 就完事。
15.2 Metadata 是企业库的灵魂
Vector 负责语义检索,Metadata 负责结构化过滤。俩配合,才能同时用上"意思相近"和"条件匹配"。
15.3 Vector DB 不只是存向量
一条有用的知识至少得有:
ID / Text / Embedding / Metadata
向量负责找,原文负责答,Metadata 负责圈范围。三位一体,缺一个都不好使。
15.4 RAG 是一条 Pipeline
出问题别先骂 LLM,按段排查:Loader → Chunk → Embedding → Vector DB → Filter → Search → Rerank → Context → LLM。
15.5 Demo 和生产系统最大的差别,在数据更新
Demo 可以删库重建。生产环境要面对新增、修改、删除、增量 Embedding、Metadata 同步、Vector DB 持久化。这才是真正的工程问题。
写 Demo 就像在你家厨房煮泡面,水开了下调料,三分钟搞定。上生产就像开连锁餐厅,你得考虑食材保质期、后厨排班、食材涨价、顾客拉肚子要投诉------泡面那套完全不顶用。
十六、收尾
今天没引入啥新框架,但它让我换了个角度看 RAG:
RAG 不是让大模型"查个资料",它是一整套数据处理、检索、排序、生成的流水线。
今天最核心的一条链路:
Document
↓
Chunk
↓
Metadata
↓
Embedding
↓
Vector DB
↓
Metadata Filter
↓
Vector Search
↓
Rerank
↓
LLM
四个角色:Filter 砍范围、Vector 粗召回、Rerank 精选、LLM 动笔。
到这儿,我那个 RAG 终于从"向量搜索 Demo",开始有点真实企业知识库的样子了。
下一步要干的事,就是把手写的 knowledge.txt 换成真正能读 PDF、Word、TXT 的 Loader------
毕竟用户上传一份 PDF,你总不能跟他说"麻烦你把内容自己敲成 txt 我再处理"吧。
P.S. 推荐一个大神的教程给想要了解或者学习人工智能知识的读者,这个教程里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏教程,请点击下方链接查看,传送门https://blog.csdn.net/qq_74013365