RAG 与 Milvus 向量数据库入门

RAG 与 Milvus 向量数据库入门
AI Agent 工程实践教程 · 第 03 章
把文档切分、向量检索、Milvus 存储和回答生成串成可理解的 RAG 流程。
第三天_RAG知识库问答入门
第三天:RAG 知识库问答入门
前两天已经完成两件事:
- 知道 AI 应用开发不是单纯聊天,而是把大模型能力接入项目
- 理解大模型会把文字切成 Token,再通过上下文生成回答
第三天开始进入一个非常常见的 AI 应用能力:
让 AI 基于我们自己的资料回答问题。
这类能力通常叫做 RAG。
先看今天的学习路线:
这一节的目标可以用一句话理解:
不要让大模型只靠自己的记忆回答。
而是先从资料里查出相关内容,再让大模型基于这些内容回答。
1. 为什么需要 RAG
第一天已经能用 Python 调用大模型。
如果用户问:
什么是人工智能?
大模型通常能直接回答。
但如果用户问的是下面这些问题,就不一定了:
我们学校 AI 课程第三天讲什么?
公司员工请假制度里,病假需要提交什么材料?
这个系统的退款流程什么时候会进入人工审核?
项目里的学生端 AI 问答接口地址是什么?
这些问题有一个共同点:
答案不一定在大模型自己的训练知识里,而是在我们自己的资料、文档、数据库或系统规则里。
如果直接把问题丢给大模型,主要会遇到两个问题。
1.1 大模型不一定知道这些资料
大模型回答问题,主要依赖训练时学到的知识和本次对话里看到的上下文。
但很多资料并不在它原来的知识里。
例如:
| 资料类型 | 为什么模型不一定知道 |
|---|---|
| 公司制度 | 这是公司内部资料,训练数据里通常没有 |
| 课程安排 | 课程每天都可能调整,模型不会自动知道最新安排 |
| 项目代码 | 代码在本地项目里,模型没有直接读取过 |
| 业务规则 | 退款、报名、审核这类规则通常只存在于系统文档或数据库里 |
所以这类问题不能只靠大模型自己的“记忆”。
系统需要先把相关资料找出来,再交给模型。
1.2 没有资料时,模型可能会编
大模型生成回答时,会尽量给出一个看起来完整的答案。
如果问题缺少依据,它不一定会直接说“不知道”,而是可能根据常见情况推测。
例如用户问:
我们公司的请假制度是什么?
如果没有给它员工手册,它可能会回答:
一般情况下,员工需要提前提交请假申请,由直属上级审批。
这句话听起来合理,但不一定符合这家公司的真实制度。
所以 AI 应用不能只追求“回答得像”,还要追求“回答有依据”。
这就是 RAG 要解决的问题:
RAG 是让系统先找到相关资料,再把资料交给大模型,让模型基于资料回答。
2. RAG 是什么,怎么把资料交给模型
RAG 是 Retrieval-Augmented Generation 的缩写,中文通常叫 检索增强生成。
2.1 RAG 的意思
可以拆成三个词理解:
| 单词 | 含义 | 在项目里做什么 |
|---|---|---|
| Retrieval | 检索 | 从资料库里查相关内容 |
| Augmented | 增强 | 把查到的资料补充到 Prompt 里 |
| Generation | 生成 | 让大模型基于资料生成回答 |
用一句话说:
RAG = 先查资料,再回答。
2.2 普通问答和 RAG 问答的区别
这和普通大模型问答的区别很明显。
普通问答:
用户问题
↓
大模型直接回答
RAG 问答:
用户问题
↓
系统先查资料
↓
把资料和问题一起交给大模型
↓
大模型基于资料回答
把它类比成考试会更容易理解。
普通大模型问答像闭卷考试:
只能靠脑子里原来记住的内容回答。
RAG 像开卷考试:
先翻资料,找到和题目相关的内容,再组织答案。
所以 RAG 不是重新训练模型。
知识库里的资料更新后,不需要重新训练大模型。只要系统能检索到新资料,模型就可以基于新资料回答。
2.3 RAG 怎么把资料交给模型
从最小实现来看,RAG 的核心动作很清楚:
把查到的资料放进 Prompt。
例如用户问:
病假需要提交什么材料?
系统先从员工手册里查到一段资料:
病假需提交医院诊断证明或病历材料,连续请假超过三天时,需补充提交复诊记录。
然后真正发给大模型的内容可能是:
请只根据下面资料回答问题。
如果资料中没有答案,请回答“资料中没有提到”。
资料:
病假需提交医院诊断证明或病历材料,连续请假超过三天时,需补充提交复诊记录。
问题:
病假需要提交什么材料?
模型回答:
病假需要提交医院诊断证明或病历材料。
如果连续请假超过三天,还需要补充提交复诊记录。
这里最重要的不是模型自己知道请假制度,而是系统把制度资料交给了模型。
这就是 RAG 的基本思想。
3. 一个最小 RAG 需要哪些步骤
一个最小 RAG 可以拆成五步。
先看整体流程:
准备资料
↓
切分资料
↓
根据问题检索相关片段
↓
把片段拼进 Prompt
↓
调用大模型生成回答
这五步不是随便分出来的。
它们刚好对应 RAG 系统里的五个关键问题:
| 步骤 | 解决的问题 | 输入 | 输出 |
|---|---|---|---|
| 准备资料 | 系统可以查哪些内容 | 文档、制度、讲义、说明文本 | 原始资料 |
| 切分资料 | 长文档怎么变成可检索的小段 | 原始资料 | 多个 chunk |
| 检索资料 | 用户问题应该匹配哪些资料 | 用户问题、chunks | 相关 chunk |
| 增强 Prompt | 怎样把资料交给大模型 | 用户问题、相关 chunk | 带资料的 Prompt |
| 生成回答 | 怎样把资料整理成自然语言答案 | Prompt | 最终回答 |
下面把这五步拆开看。
3.1 准备资料:先确定知识从哪里来
RAG 的第一步不是调用模型,而是准备资料。
因为 RAG 要回答的内容,不能只靠大模型自己记住,而是要从外部资料里找。
资料可以来自很多地方:
| 资料来源 | 例子 |
|---|---|
| 文本文档 | .txt、.md、课程讲义 |
| 办公文档 | Word、PDF、Excel |
| 系统内容 | 数据库里的制度、商品说明、课程安排 |
| 项目资料 | 接口文档、README、代码说明 |
最小版本里,资料可以用一个普通文本文件表示。
例如:
YanQue AI 课程第三天主要学习 RAG 知识库问答。
RAG 的核心思想是先检索资料,再把资料放进 Prompt。
这一步的结果是:
系统有了一份可以查询的资料。
3.2 切分资料:把长文档拆成 chunk
如果资料只有几句话,可以直接交给模型。
但实际资料经常很长。
比如一份课程讲义可能有几千字,一份制度文档可能有几十页。
如果把整份资料都放进 Prompt,会出现几个问题:
| 问题 | 说明 |
|---|---|
| 内容太长 | 可能超过模型上下文长度 |
| 成本变高 | 输入越长,消耗 Token 越多 |
| 干扰变多 | 无关内容太多,模型更难抓重点 |
| 检索不准 | 系统很难判断整篇文档哪一部分和问题有关 |
所以要把长文档切成一个个小段。
这些小段通常叫 chunk。
例如原始资料是一段课程规则:
请假规则:学生如果当天不能上课,需要在上课前向班主任请假,并说明请假原因。
补课规则:学生请假后,可以在一周内联系老师安排补课,补课时间以老师实际排课为准。
作业规则:当天作业需要在晚上 10 点前提交,如果确实无法按时提交,需要提前说明原因。
切分后可以变成:
chunk 1:请假规则:学生如果当天不能上课,需要在上课前向班主任请假,并说明请假原因。
chunk 2:补课规则:学生请假后,可以在一周内联系老师安排补课,补课时间以老师实际排课为准。
chunk 3:作业规则:当天作业需要在晚上 10 点前提交,如果确实无法按时提交,需要提前说明原因。
这一步的结果是:
一份长资料被拆成多个更容易检索的小片段。
3.3 检索资料:从 chunk 里找相关内容
用户提问后,系统不能把所有 chunk 都交给模型。
它要先判断:
哪些 chunk 和这个问题最相关?
例如用户问:
请假后还能补课吗?
系统应该优先找到:
chunk 2:补课规则:学生请假后,可以在一周内联系老师安排补课,补课时间以老师实际排课为准。
而不是把请假规则、作业规则全部都一起拿出来。
最容易理解的检索方式是关键词检索。
例如问题里有“补课”,资料里也有“补课”,就认为这段资料更相关。
使用向量检索后,系统就可以处理更自然的问法。
比如用户问:
我今天请假了,后面能不能找老师把课补上?
它也应该能找到“学生请假后,可以在一周内联系老师安排补课”这段资料。
这一步的结果是:
系统找到了和问题最相关的几个 chunk。
3.4 增强 Prompt:把资料和问题一起发给模型
检索到资料后,还不能直接结束。
因为最终回答还是要由大模型生成。
所以要把检索到的资料和用户问题放到同一个 Prompt 里。
普通 Prompt 可能只有问题:
请假后还能补课吗?
RAG Prompt 会多出资料:
请只根据下面资料回答问题。
资料:
补课规则:学生请假后,可以在一周内联系老师安排补课,补课时间以老师实际排课为准。
问题:
请假后还能补课吗?
这一步非常关键。
因为大模型只有看到了资料,才有机会基于资料回答。
如果资料没有放进 Prompt,模型就只能继续靠自己的原有知识推测。
这一步的结果是:
一个带有参考资料的 Prompt。
3.5 生成回答:让模型基于资料组织答案
最后一步才是调用大模型。
模型拿到的是一个增强后的 Prompt。
它不仅能看到用户问题,还能看到系统检索出来的资料。
所以它可以回答:
可以补课。资料中提到,学生请假后,可以在一周内联系老师安排补课,具体补课时间以老师实际排课为准。
这一阶段要注意一个规则:
资料里有,就基于资料回答;资料里没有,就说明资料中没有提到。
这能减少模型随便编答案。
这一步的结果是:
用户最终看到的自然语言回答。
4. 常见的资料切分方式
知道为什么要切分以后,还要知道可以怎么切。
不同资料适合的切分方式不一样。
| 切分方式 | 做法 | 适合场景 | 优点 | 注意点 |
|---|---|---|---|---|
| 按空行切分 | 遇到空行就分成一段 | 讲义、说明文档、Markdown 文档 | 简单,容易保留段落完整性 | 如果原文段落太长,还需要继续拆 |
| 按标题切分 | 按一级标题、二级标题拆分 | 有清晰章节结构的文档 | 能保留章节关系 | 标题下面内容太多时,需要再细分 |
| 按固定长度切分 | 每隔固定字数或 Token 数切一段 | 没有明显结构的大段文本 | 规则稳定,容易实现 | 可能把一句话或一个规则切断 |
| 按句子切分 | 按句号、问号、分号等拆分 | 问答说明、制度条款、短文本资料 | 语义比较完整 | 句子太短时,检索结果可能缺少上下文 |
| 按语义切分 | 按一组完整意思来拆分 | 知识密度高、结构复杂的资料 | 更接近人的阅读方式 | 实现更复杂,通常需要工具或模型辅助 |
最容易理解的一种方式是:
按空行切分。
原因是我们的示例资料每一段都比较完整,空行正好可以把不同知识点分开。
做知识库时,切分方式通常不是固定一种,而是根据资料特点选择。
比如:
- 课程讲义可以先按标题切
- 制度条款可以按条目或句子切
- 很长的章节可以先按标题切,再按长度继续切
- 代码说明可以按函数、类、模块说明切
这里有一个基本原则:
切分出来的 chunk 应该尽量表达一个完整意思。
5. Chunk 的关键参数:大小、重叠和边界
前面已经知道,RAG 会把资料切成多个 chunk。
但真正影响效果的,不只是“切不切”,而是:
每个 chunk 多大?
相邻 chunk 要不要重叠?
在哪里切才不破坏语义?
这三个问题分别对应三个概念:
| 概念 | 含义 | 解决的问题 |
|---|---|---|
chunk_size |
每个 chunk 的最大长度 | 控制资料片段有多大 |
chunk_overlap |
相邻 chunk 之间重复保留多少内容 | 防止关键信息被切断 |
| 分隔符 | 按什么边界切分文本 | 尽量保持语义完整 |
5.1 chunk_size:每段资料多大合适
chunk_size 可以理解成每个资料片段的长度。
它可以按字符数算,也可以按 Token 数算。
可以先用“字数”理解:
chunk_size = 500
意思是每个 chunk 尽量不要超过 500 个字。
这里要注意一个容易误解的地方:
按标题、按空行切分,和 chunk_size 不是互相替代的关系。
按标题、按空行切分,解决的是:
从哪里切比较自然?
chunk_size 解决的是:
切出来的一段会不会太长?
例如一篇文档按标题切:
## 请假规则
这里有 300 字。
## 补课规则
这里有 400 字。
## 退费规则
这里有 350 字。
这种情况下,每个标题下面内容都不长,按标题切就已经比较合适。
但如果某个标题下面有 3000 字:
## 补课规则
这里有 3000 字,里面同时讲补课申请、补课时间、作业补交、缺课记录、特殊情况处理……
只按标题切就会得到一个很大的 chunk。
用户只问“补课时间怎么安排”,模型却会看到整章内容。
这时就还需要用 chunk_size 继续控制长度。
所以更准确的理解是:
先按自然结构切分。
如果切出来的内容太长,再用 chunk_size 继续切小。
chunk 太小和太大都会出问题。
| 情况 | 可能的问题 |
|---|---|
| chunk 太小 | 一段资料不完整,模型看到后不知道上下文 |
| chunk 太大 | 资料里混入太多无关内容,检索不精准,Prompt 也更长 |
例如制度文档里有这样一段:
学生请假需要提前向班主任说明原因。请假后如果需要补课,应在一周内联系授课老师,由老师根据班级进度安排补课时间。补课完成后,学生需要补交当天作业。
如果切得太小:
chunk 1:学生请假需要提前向班主任说明原因。
chunk 2:请假后如果需要补课,应在一周内联系授课老师。
chunk 3:由老师根据班级进度安排补课时间。
chunk 4:补课完成后,学生需要补交当天作业。
用户问:
请假后怎么补课?
系统可能只检索到 chunk 2,但 chunk 3 里才说明补课时间由老师安排。
这样模型看到的信息就不完整。
如果切得太大,把请假、补课、作业、退费、考试规则都放进一个 chunk,用户只问补课,模型也会看到很多无关内容。
所以 chunk_size 的目标不是越大越好,也不是越小越好。
更准确地说:
chunk_size 要让一个 chunk 尽量包含一个完整知识点,同时不要包含太多无关知识点。
常见参考:
| 资料类型 | chunk_size 可以先怎么取 |
|---|---|
| FAQ、短问答 | 100 到 300 字 |
| 制度条款、课程说明 | 300 到 800 字 |
| 技术文档、产品文档 | 500 到 1200 字 |
| 长报告、论文资料 | 800 到 1500 字 |
这些不是固定标准,只是起点。
最后还要看检索效果。
5.2 chunk_overlap:什么时候需要重叠
chunk_overlap 叫做重叠长度。
它的意思是:
相邻两个 chunk 之间,保留一小段重复内容。
但 overlap 不是所有切分方式都必须使用。
如果资料本身结构很清楚,例如:
## 请假规则
学生不能上课时,需要提前向班主任请假。
## 补课规则
学生请假后,可以在一周内联系老师安排补课。
## 作业规则
当天作业需要在晚上 10 点前提交。
按标题切分后,每个 chunk 都是完整规则。
这种情况下,不加 overlap 也可以。
因为知识点没有被切断。
overlap 主要用在这些情况:
| 情况 | 为什么需要 overlap |
|---|---|
| 按固定长度切分 | 容易把一句话或一条规则切断 |
| 段落很长,需要继续拆 | 拆分点可能落在语义中间 |
| 上下文依赖强 | 后一段需要前一段的信息才能理解 |
| PDF/网页解析后结构不清 | 很难保证每次都切在自然边界 |
也就是说:
标题和空行切分能减少 overlap 的需求,但不能保证永远不需要 overlap。
为什么有时还要重复?
因为切分时,关键信息可能刚好跨在两个 chunk 的边界上。
例如原文是:
学生请假后,如果需要补课,应在一周内联系授课老师。补课时间由老师根据班级进度统一安排。
如果没有 overlap,可能切成:
chunk 1:学生请假后,如果需要补课,应在一周内联系授课老师。
chunk 2:补课时间由老师根据班级进度统一安排。
用户问:
补课时间怎么安排?
系统可能检索到 chunk 2。
但 chunk 2 只说“补课时间怎么安排”,没有说明这是“请假后补课”的场景。
如果加上 overlap,可以变成:
chunk 1:学生请假后,如果需要补课,应在一周内联系授课老师。
chunk 2:如果需要补课,应在一周内联系授课老师。补课时间由老师根据班级进度统一安排。
这样 chunk 2 里既有补课时间,也保留了前面一点上下文。
模型回答时就更稳。
可以先这样理解:
overlap 是边界保护,不是固定动作。
5.3 overlap 不是越大越好
overlap 太小,容易丢上下文。
overlap 太大,也会带来问题:
| overlap 情况 | 可能的问题 |
|---|---|
| 太小 | 边界处信息断裂,检索到的 chunk 缺上下文 |
| 合适 | 保留必要上下文,减少语义断裂 |
| 太大 | 重复内容太多,浪费存储和 Token,检索结果容易重复 |
比如 chunk_size = 500,chunk_overlap = 400。
这表示每个 chunk 里大部分内容都和上一个 chunk 重复。
结果是:
- 知识库里重复资料变多
- 检索出来的结果可能高度相似
- Prompt 里塞入大量重复文字
- 成本变高,但答案不一定更好
常见做法是:
chunk_overlap 取 chunk_size 的 10% 到 20%
例如:
| chunk_size | chunk_overlap |
|---|---|
| 300 | 30 到 60 |
| 500 | 50 到 100 |
| 1000 | 100 到 200 |
这也不是死规则。
如果文档句子很长、上下文依赖强,overlap 可以稍大一点。
如果文档本身是一条条独立 FAQ,overlap 可以很小,甚至不需要。
5.4 分隔符:固定长度也要考虑边界
切分时还有一个重要问题:
在哪里切?
chunk_size 只是一个长度上限,不代表一定要在那个位置硬切。
更好的做法是:
快到 chunk_size 时,尽量往前找一个自然边界再切。
比如 chunk_size = 80。
系统不是数到第 80 个字就马上切断。
而是先看第 80 个字附近有没有更合适的位置:
- 有没有句号
- 有没有分号
- 有没有换行
- 有没有段落边界
- 有没有标题或条目边界
如果有,就优先在那里切。
如果没有,最后才按固定长度硬切。
例如:
请假后如果需要补课,应在一周内联系授课老师,补课时间由老师根据班级进度统一安排。
硬切后可能变成:
chunk 1:请假后如果需要补课,应在一周内联系授课老师,补课时间
chunk 2:由老师根据班级进度统一安排。
这种切法会破坏语义。
更好的切法是往前找句号或完整句子边界:
chunk 1:请假后如果需要补课,应在一周内联系授课老师。
chunk 2:补课时间由老师根据班级进度统一安排。
这样两个 chunk 都是完整句子。
常见自然边界包括:
| 边界 | 例子 |
|---|---|
| 标题 | ## 补课规则 |
| 段落 | 空行分隔的段落 |
| 条目 | 1.、2.、- |
| 句子 | 句号、问号、分号 |
| 代码结构 | 函数、类、接口说明 |
切分优先级可以这样理解:
先看标题边界
再看段落边界
再看句子边界
最后才按固定长度兜底
所以,按固定长度切分也不是完全不看内容。
它更像是:
chunk_size 负责控制最大长度。
分隔符负责决定尽量在哪里切。
这种方式并不难实现。
常见做法叫 递归切分。
它的思路是:
先用大的自然边界切。
如果切出来的 chunk 仍然超过 chunk_size,就继续用更小的边界切。
如果所有自然边界都不合适,最后才按固定长度兜底。
例如给定一组切分优先级:
标题 > 段落 > 句子 > 固定长度
处理过程可以这样理解:
第一步:先按标题切。
如果每一节都不长,就结束。
第二步:如果某个标题下面太长,就按段落继续切。
如果每个段落都不长,就结束。
第三步:如果某个段落还是太长,就按句子继续切。
如果句子组合后长度合适,就结束。
第四步:如果一句话本身特别长,最后才按固定长度切。
这不是让系统真的“理解文章含义”以后再切。
它更多是按照一组规则,尽量选择更自然的分隔位置。
所以实现难度不高。
很多 RAG 工具和文本处理库都提供类似能力。
自己实现时,也可以先按这个规则做一个简化版:
| 优先级 | 如果能切开 | 如果切出来还太长 |
|---|---|---|
| 标题 | 得到多个章节 chunk | 继续按段落切 |
| 段落 | 得到多个段落 chunk | 继续按句子切 |
| 句子 | 得到多个句子组合 chunk | 最后按固定长度切 |
| 固定长度 | 保底切分 | 配合 overlap 减少断裂 |
真正需要注意的是:
切分规则不是越复杂越好,而是要和资料结构匹配。
如果资料本身就是 FAQ,一问一答就是天然 chunk。
如果资料是 Markdown 讲义,标题和段落通常很重要。
如果资料是 PDF 解析出来的一大段乱文本,就更需要清洗、分句和 overlap。
6. 文档进入知识库前,还要处理什么
RAG 不是把文件直接丢进知识库就结束。
真实资料经常是乱的。
比如:
- PDF 里有页眉、页脚、页码
- Word 里有标题、表格、批注
- 网页里有导航、广告、按钮文字
- Excel 里每一列都有自己的含义
- 扫描件里可能还需要先识别图片里的文字
如果这些内容不处理,直接进入知识库,后面检索就容易出问题。
可以把这一步理解成:
先把一堆原始资料整理成干净、可查询的文字。
文档进入知识库前,通常要做三件事:
读出来
↓
清干净
↓
贴标签
6.1 第一步:把资料读出来
不同文件格式,不能用同一种方式读取。
TXT 和 Markdown 本来就是文字,比较容易读。
PDF、Word、Excel、网页就麻烦一些,因为里面不只有正文。
| 资料格式 | 需要处理的问题 |
|---|---|
| TXT / Markdown | 直接读取文字,保留标题和段落 |
| 可能混着页眉、页脚、页码、表格、扫描图片 | |
| Word | 需要读取正文、标题、表格,有时还要忽略批注 |
| Excel | 不能只读成一堆数字,要知道每一列是什么意思 |
| 网页 | 要区分正文和导航、广告、页脚等无关内容 |
例如 PDF 里经常会有页眉页脚:
YanQue AI 课程资料
第 3 页
如果每一页都保留这些内容,知识库里就会出现大量重复文字。
用户问问题时,系统可能检索到“第 3 页”这种没用内容。
所以第一步的目标是:
把资料里的正文读出来,不要把无关内容一起读进去。
6.2 第二步:把文字清干净
资料读出来以后,还需要整理。
这一步可以叫“清洗”,也可以理解成:
把影响检索的脏东西去掉。
常见情况有这些:
| 清洗内容 | 例子 |
|---|---|
| 去掉重复空行 | 连续很多空行变成一个空行 |
| 去掉无关页眉页脚 | 删除“第 1 页”“内部资料”等重复内容 |
| 统一标点和空格 | 避免全角半角混乱 |
| 保留标题层级 | 保留“章节 - 小节 - 条目”的关系 |
| 修复断行 | PDF 中一句话被拆成多行时合并 |
例如 PDF 解析后,可能变成这样:
学生请假后如果需要补课,
应在一周内联系授课老师。
第 3 页
YanQue AI 课程资料
清洗后应该尽量变成:
学生请假后如果需要补课,应在一周内联系授课老师。
清洗不是为了让文字看起来更漂亮。
它的目标是:
让知识库里留下真正有用、容易检索的内容。
6.3 第三步:给资料贴标签
一个 chunk 只保存正文还不够。
还需要知道它来自哪里。
这类附加信息叫 metadata,可以先理解成“资料标签”。
比如图书馆里一本书,不只会有正文,还会有书名、作者、分类、出版时间。
RAG 里的 chunk 也一样。
例如:
| 标签 | 作用 |
|---|---|
source |
这段内容来自哪个文件 |
page |
来自 PDF 第几页 |
title |
属于哪个标题 |
section |
属于哪个章节 |
created_at |
资料创建时间 |
updated_at |
资料更新时间 |
doc_type |
是制度、讲义、产品文档还是 FAQ |
这些标签很重要。
因为它可以解决几个问题:
- 回答时标注来源
- 按资料类型过滤
- 只查某个课程或某个产品
- 资料更新时知道要删除或替换哪一批 chunk
例如用户只想问“AI 课程资料”,系统就可以只检索:
doc_type = course
course_name = AI 应用开发
这样能减少无关资料干扰。
7. 检索不只有一种方式
前面几章讲的是“资料进入知识库之前要怎么处理”。
到这里,资料已经经历了几步:
原始资料
↓
读出来
↓
清干净
↓
切成 chunk
↓
贴上来源和类型等标签
这时知识库里不再是一整份乱文档,而是一批可以被查询的小资料片段。
接下来就进入另一个问题:
用户问问题时,系统怎么从这些 chunk 里找出最相关的几段?
这一步就是检索。
检索做得不好,后面 Prompt 写得再好也很难回答正确。
常见检索方式有三类:
| 检索方式 | 看什么 | 适合什么问题 |
|---|---|---|
| 关键词检索 | 字面上是否包含某些词 | 术语明确、编号明确、名称明确的问题 |
| 向量检索 | 语义是否接近 | 自然语言问法、同义表达、模糊问题 |
| 混合检索 | 关键词 + 语义 | 既有专有名词,又有自然语言的问题 |
7.1 关键词检索
关键词检索看的是文字是否匹配。
例如用户问:
退费规则是什么?
资料里有:
退费规则:开课前可申请退费,开课后按实际上课进度核算。
这种情况关键词检索很有效。
它的优点是:
- 简单
- 快
- 对编号、名称、专有词很准确
缺点是:
- 不理解同义词
- 不理解换一种说法
- 对自然语言问题不够灵活
例如用户问:
不上了还能退钱吗?
资料里写的是“退费规则”,没有写“退钱”。
关键词检索可能就不稳定。
7.2 向量检索
向量检索看的是语义相似度。
它会先把用户问题和 chunk 都变成向量。
然后比较:
问题向量和哪些 chunk 向量更接近?
例如:
问题:不上了还能退钱吗?
资料:退费规则:开课前可申请退费。
虽然“退钱”和“退费”不是完全一样的词,但意思接近。
向量检索更有机会找到这段资料。
它的优点是:
- 能理解相似表达
- 适合自然语言问题
- 适合用户说法不固定的场景
缺点是:
- 需要 Embedding 模型
- 需要向量数据库或向量索引
- 对数字、编号、精确名称不一定比关键词强
7.3 混合检索
实际项目里,经常会把关键词检索和向量检索结合起来。
这叫 混合检索。
例如用户问:
Java+AI 第三期请假后怎么补课?
这里既有精确信息:
Java+AI 第三期
也有语义问题:
请假后怎么补课
关键词检索适合抓住“Java+AI 第三期”。
向量检索适合理解“请假后怎么补课”。
两者结合,通常比单独一种方式更稳。
8. Embedding:把文字变成向量
向量检索前,系统要先解决一个问题:
电脑怎么知道两段文字意思像不像?
人看到这两句话,很容易判断它们相关:
请假后怎么补课?
学生请假后,可以在一周内联系老师安排补课。
因为我们能看懂“请假”“补课”这些意思。
但系统不能像人一样直接理解文字。
所以它会先把文字交给 Embedding 模型处理。
可以先把 Embedding 理解成:
给每段文字找一个“语义位置”。
意思接近的文字,位置就更近。
意思差得远的文字,位置就更远。
例如:
请假后怎么补课?
这句话进入 Embedding 模型后,会变成类似这样的向量:
[0.12, -0.08, 0.36, 0.91, ...]
这串数字就可以理解成这句话在“语义地图”上的位置。
这里不需要读懂每个数字代表什么。
它们的作用是让系统能判断:
用户的问题,和知识库里的哪段资料意思更接近。
所以,Embedding 的重点不是“数字长什么样”。
它真正解决的是:
让电脑可以比较文字的意思。
8.1 Embedding 模型是什么
Embedding 模型,也可以叫向量模型。
它的作用很明确:
把文字转换成向量。
不同的 Embedding 模型,转换出来的向量可能不一样。
主要会影响三件事:
| 影响 | 说明 |
|---|---|
| 向量维度 | 有的模型输出 768 维,有的模型输出 1536 维 |
| 语义效果 | 好的模型更容易把意思相近的文字放近 |
| 检索方式 | 有些模型会推荐配合某种相似度算法使用 |
在 RAG 里要特别注意:
资料入库和用户提问,必须使用同一个 Embedding 模型。
否则资料向量和问题向量可能不在同一个语义空间里,比较起来就不可靠。
8.2 向量的维度是什么
向量不是一个数字,而是一组数字。
比如:
[0.12, -0.08, 0.36, 0.91, ...]
这里面的每一个位置,都可以理解成一个 维度。
可以先这样理解:
维度就是用来描述文字特征的一个方向。
为了方便理解,可以先把维度想成几个问题:
- 这句话和“请假”有关吗?
- 这句话和“补课”有关吗?
- 这句话和“作业”有关吗?
- 这句话和“退费”有关吗?
如果一句话是:
请假后怎么补课?
那它在“请假”和“补课”这些维度上的数值可能更明显。
在“退费”这个维度上的数值可能就比较弱。
但真实的 Embedding 向量不会只有几个维度。
它可能有几百维、上千维,而且维度数量通常由 Embedding 模型 决定。
例如某个模型可能输出 768 维。
另一个模型可能输出 1536 维。
也就是说:
选了哪个 Embedding 模型,基本也就选了它输出多少维向量。
每个维度不一定能被人直接解释成一个中文词。
前面的“请假、补课、退费”只是帮助理解。
真实模型里的每一维,通常不是这样一一对应中文词的。
这里还要注意:
维度不是越多越好。
维度多,可能表达的信息更丰富,但也意味着要保存更多数字、比较更多数字,存储和检索都会更重。
而且维度更多,不代表一定更准。
所以更准确地说:
维度多少要跟着 Embedding 模型走,不是自己盲目追求越多越好。
后面讲余弦、点积、欧氏距离,其实都是在比较这些维度上的数字。
8.3 向量之间怎么比较相似
既然 Embedding 会给文字找一个“语义位置”,接下来就要判断:
用户问题的向量,和哪个 chunk 的向量最接近?
这里会用到相似度计算。
常见的有三种:
| 方法 | 它主要看什么 | 更适合怎么理解 |
|---|---|---|
| 余弦相似度 | 方向像不像 | 两句话的意思方向是否接近 |
| 点积 | 匹配分数高不高 | 两个向量整体是否匹配 |
| 欧氏距离 | 空间距离远不远 | 两个点在语义地图上离得远不远 |
8.3.1 余弦相似度:看方向像不像
最常听到的是 余弦相似度。
可以这样理解:
两个向量方向越接近,说明语义越接近。
例如:
问题:请假后还能补课吗?
资料 A:学生请假后,可以在一周内联系老师安排补课。
资料 B:课程作业需要在晚上 10 点前提交。
问题和资料 A 的方向更接近。
所以资料 A 的相似度更高,更应该被检索出来。
余弦相似度关注的是“方向”,不是单纯看向量有多长。
它比较适合文本语义检索。
因为我们通常更关心:
这两段话的意思是不是相近?
而不是:
这两个向量的长度是不是一样?
所以在很多 RAG 资料检索里,余弦相似度非常常见。
适合场景:
| 场景 | 为什么适合 |
|---|---|
| 文档问答 | 重点是判断问题和资料语义是否接近 |
| FAQ 检索 | 用户问法可能不同,但意思相同 |
| 课程资料、制度资料检索 | 用户不一定使用资料里的原词 |
例如:
用户问:请假了还能补课吗?
资料写:学生缺课后可联系老师安排补课。
两个句子关键词不完全一样,但意思接近。
余弦相似度就适合处理这种情况。
8.3.2 点积:看匹配分数高不高
点积可以先这样理解:
把两个向量算成一个匹配分数。
它和余弦相似度的区别是:
余弦主要看方向,点积会同时看方向和向量长度。
所以点积更像是在问:
两组特征整体对得上多少?匹配强不强?
点积公式怎么看
假设有两个向量:
A = [a1, a2, a3]
B = [b1, b2, b3]
点积就是:
A · B = a1×b1 + a2×b2 + a3×b3
也就是:
对应位置相乘,再加起来。
用推荐系统理解点积
点积最容易用推荐系统来理解。
用户向量:喜欢 Python、AI、项目实战
[1, 1, 1, 0]
课程 A:包含 Python、AI、项目实战,不包含前端
[1, 1, 1, 0]
课程 B:包含前端,不包含 Python、AI、项目实战
[0, 0, 0, 1]
用点积算匹配分:
用户 · 课程 A = 1×1 + 1×1 + 1×1 + 0×0 = 3
用户 · 课程 B = 1×1 + 1×0 + 1×0 + 0×1 = 1
课程 A 分数更高,说明它和用户兴趣更匹配。
这就是点积适合推荐、搜索、广告排序的原因:
它能快速算出“用户想要什么”和“内容有什么”对得上多少。
点积在 RAG 里什么时候会用
在 RAG 里,通常不是自己手写点积公式。
更多是向量数据库或 Embedding 模型要求使用它。
例如创建向量索引时,可能会看到:
COSINE
IP
L2
这里的 IP 可以先理解成点积。
如果选择 IP,检索 chunk 时就会按点积分数排序。
| 场景 | 说明 |
|---|---|
| Embedding 模型推荐点积 | 按模型建议来 |
向量数据库索引选择 IP |
检索时会按点积分数排序 |
使用点积要注意什么
点积不是一定比余弦更好。
如果向量长度本身没有明确意义,随便用点积可能会让“长度更大但不一定更相关”的内容排到前面。
所以项目里不要只凭感觉选。
更稳的做法是:
先看 Embedding 模型推荐
再看向量数据库索引方式
最后用真实问题测试检索效果
可以把点积的使用原则记成一句话:
当模型或向量库希望用“匹配分数”排序时,点积更合适;如果只是想理解语义方向,余弦更直观。
8.3.3 欧氏距离:看两个点离多远
欧氏距离可以先这样理解:
把向量看成一个位置,看两个位置离得多远。
距离越小,越相似。
距离越大,越不相似。
向量怎么看距离
先看二维向量。
二维向量可以像坐标一样理解:
[2, 3]
可以看成一个位置。
如果有三个向量:
问题向量:[2, 3]
资料 A:[3, 4]
资料 B:[8, 9]
就可以比较它们每个位置差多少。
问题向量和资料 A:
第 1 个位置:2 和 3,差 1
第 2 个位置:3 和 4,差 1
问题向量和资料 B:
第 1 个位置:2 和 8,差 6
第 2 个位置:3 和 9,差 6
资料 A 每个位置差得都小,所以资料 A 更近。
资料 B 每个位置差得都大,所以资料 B 更远。
欧氏距离的公式就是把这些差距合成一个总距离:
距离 = √((a1-b1)² + (a2-b2)²)
不需要死记公式。
记住这句话就够了:
每个位置都比一比差多少,再合成一个总距离。
欧氏距离适合什么场景
欧氏距离适合那些“远近本身有意义”的场景。
这时不一定适合只看余弦相似度。
因为余弦主要看方向。
但有些问题关心的是:
到底离得近不近。
比如地图上有两个地点。
它们可能都在你的东北方向。
但一个离你 1 公里,一个离你 100 公里。
如果只看方向,它们都差不多。
但如果看距离,差别就很大。
所以在“距离大小本身有意义”的场景里,欧氏距离更自然。
比如:
| 场景 | 为什么适合 |
|---|---|
| 地图坐标 | 两个位置离得近,确实表示距离近 |
| 图像特征 | 图片特征越接近,图片可能越相似 |
| 音频特征 | 声音特征越接近,音频可能越相似 |
| 聚类分析 | 可以把距离近的数据分到一组 |
| 异常检测 | 如果一个点离其他点都很远,可能是异常数据 |
放到向量检索里,欧氏距离就是在问:
哪个向量离问题向量最近?
距离最近的资料,就更可能被排在前面。
使用欧氏距离要注意什么
欧氏距离的直觉很好懂。
但在文本语义检索里,不一定总是优先选它。
有些文本向量更适合比较“方向”,这时余弦相似度会更常见。
所以欧氏距离也不要凭感觉选。
还是先看模型和向量数据库推荐什么。
8.3.4 到底该用哪个
对于 RAG 入门,可以先这样记:
| 方法 | 可以先怎么记 | 常见选择建议 |
|---|---|---|
| 余弦相似度 | 看方向 | 文本语义检索里最常见,入门优先理解它 |
| 点积 | 看匹配分数 | 模型或向量库推荐时使用 |
| 欧氏距离 | 看距离 | 当“距离远近”本身有意义时使用 |
在课程里不需要手算这些公式。
先知道一件事就够了:
向量检索不是直接比较文字,而是把文字变成向量后,再用相似度算法找最接近的 chunk。
具体用余弦、点积还是欧氏距离,通常由选择的 Embedding 模型和向量数据库决定。
真正做项目时,优先级可以这样排:
先看 Embedding 模型推荐
再看向量数据库支持的索引和距离方式
最后用真实问题测试检索效果
不要在同一个知识库里随便混用不同的相似度方式。
8.4 RAG 里什么时候用 Embedding
理解了“文字可以变成向量,向量可以比较远近”以后,再看 RAG 里的流程就清楚了。
在 RAG 里,Embedding 通常会用两次。
第一,资料入库时:
chunk 文本
↓
Embedding 模型
↓
chunk 向量
↓
存入向量数据库
第二,用户提问时:
用户问题
↓
同一个 Embedding 模型
↓
问题向量
↓
去向量数据库里查相似 chunk
也就是说:
- 知识库里的资料,要先变成向量
- 用户的问题,也要变成向量
- 然后系统才能比较“问题”和“资料”谁更接近
这里有一个关键点:
入库和查询要使用同一种向量模型,也要使用匹配的相似度计算方式。
如果资料用 A 模型生成向量,问题用 B 模型生成向量,它们可能不在同一个语义空间里。
这就像一个人用米做单位,另一个人用英尺做单位,直接比较数值会出问题。
9. 向量数据库:用来保存和检索向量
前面讲了 Embedding 会把文字变成向量。
但 RAG 系统里会有很多 chunk。
每个 chunk 都有一个向量。
如果资料很多,就会变成:
几千个 chunk
几万个 chunk
甚至更多 chunk
这时系统需要一个地方专门处理:
怎么保存这些向量,并快速找出和用户问题最相近的 chunk。
这个地方就是向量数据库。
9.1 向量数据库干什么用
向量数据库主要做三件事:
| 作用 | 说明 |
|---|---|
| 保存向量 | 保存每个 chunk 对应的向量 |
| 建立索引 | 让系统不用一个个慢慢比较 |
| 相似检索 | 根据用户问题向量,找出最接近的 chunk |
可以这样理解:
普通数据库擅长按字段查数据,向量数据库擅长按语义相似度找内容。
比如用户问:
请假后还能补课吗?
向量数据库要做的不是找完全一样的字。
它要找的是意思接近的资料:
学生请假后,可以在一周内联系老师安排补课。
9.2 向量数据库里保存什么
一条向量记录通常会保存三类内容:
| 内容 | 作用 |
|---|---|
| vector | 用来做相似度检索 |
| content | 命中后放进 Prompt 的原文 chunk |
| metadata | 来源、标题、页码、时间等标签 |
可以理解成:
id:chunk_001
vector:[0.12, 0.38, 0.77, ...]
content:学生请假后,可以在一周内联系老师安排补课。
metadata:来源文件、章节、页码、更新时间
这里要注意:
向量数据库不是只保存向量,也要能找到向量对应的原文。
因为最后给大模型看的不是一串数字,而是原文资料。
9.3 常见的向量数据库有哪些
先知道几个常见的就够了:
| 名称 | 简单理解 |
|---|---|
| Milvus | 常见的开源向量数据库,适合较大规模向量检索 |
| Qdrant | 开源向量数据库,使用体验比较轻量 |
| Chroma | 轻量级向量库,常用于原型和小项目 |
| pgvector | PostgreSQL 的向量扩展,可以在 PostgreSQL 里做向量检索 |
课程不需要马上记住每个产品。
先知道:
它们都是为“保存向量、检索相似向量”服务的工具。
9.4 怎么选择向量数据库
入门时可以先按项目规模理解:
| 场景 | 可以先考虑 |
|---|---|
| 本地练习、小 Demo | Chroma、pgvector |
| 已经在用 PostgreSQL | pgvector |
| 数据量变大 | Milvus、Qdrant |
真正项目里还要看:
- 数据量有多大
- 查询速度要求高不高
- 是否需要 metadata 过滤
- 是否方便部署和维护
- 团队已经在用什么数据库
9.5 它和 MySQL 有什么区别
MySQL 适合这样查:
where course_id = 3
where status = 'PAID'
where created_at > '2026-01-01'
向量数据库适合这样查:
找出和这个问题意思最接近的 5 段资料
所以它们解决的问题不一样。
在真实项目里,经常是一起用:
MySQL:保存业务数据
向量数据库:保存资料向量,做相似检索
9.6 在 RAG 里的位置
RAG 里使用向量数据库的流程是:
资料切成 chunk
↓
chunk 变成向量
↓
存入向量数据库
↓
用户问题变成向量
↓
去向量数据库里找相似 chunk
↓
把 chunk 原文放进 Prompt
所以向量数据库的核心作用是:
不是替大模型回答问题,而是帮大模型找到相关资料。
10. Top-K、召回和精准
检索时,系统通常不会只找 1 个 chunk。
它会找前几个最相关的 chunk。
这个数量通常叫 top_k。
例如:
top_k = 5
意思是先找出最相关的 5 个 chunk。
10.1 top_k 太小的问题
如果 top_k 太小,可能漏掉真正有用的资料。
例如用户问:
请假后补课和作业怎么处理?
这个问题其实涉及两段资料:
chunk 1:请假后可以联系老师补课。
chunk 2:补课后需要补交当天作业。
如果 top_k = 1,系统可能只拿到补课规则,漏掉作业规则。
最后模型回答就不完整。
10.2 top_k 太大的问题
如果 top_k 太大,又会带来无关内容。
例如 top_k = 20,用户只问补课,系统却把请假、作业、退费、考试、班级纪律都拿出来。
结果是:
- Prompt 变长
- Token 成本变高
- 无关内容干扰模型判断
- 回答可能变得啰嗦或跑偏
所以 top_k 也需要平衡。
常见起点:
| 场景 | top_k 可以先怎么取 |
|---|---|
| FAQ 问答 | 3 到 5 |
| 制度文档问答 | 5 到 8 |
| 技术文档问答 | 5 到 10 |
| 多文档综合问题 | 8 到 15 |
10.3 召回和精准
检索有两个方向需要平衡:
| 目标 | 含义 | 过度追求会怎样 |
|---|---|---|
| 召回 | 尽量不要漏掉相关资料 | 可能拿进来很多无关资料 |
| 精准 | 拿到的资料尽量都相关 | 可能漏掉一些有用资料 |
RAG 里经常先追求“别漏掉”,再通过后续步骤把无关资料筛掉。
这也是为什么会有下一步:Rerank。
11. Rerank:对检索结果重新排序
前面讲的向量检索、混合检索、Top-K,解决的是:
先从知识库里找出一批可能相关的 chunk。
但“可能相关”不等于“最适合回答”。
Rerank 做的是第二轮判断:
把第一轮找出来的 chunk 再看一遍,重新排顺序。
例如用户问:
请假后怎么补课?
第一轮检索可能返回:
1. 请假规则:学生不能上课时,需要提前向班主任请假。
2. 补课规则:请假后可以在一周内联系老师安排补课。
3. 作业规则:请假当天作业需要补交。
4. 考勤规则:无故缺勤会影响出勤记录。
这些内容都和“请假”有一点关系。
但真正最应该放在前面的是:
补课规则:请假后可以在一周内联系老师安排补课。
因为用户问的重点不是“怎么请假”,而是“请假之后怎么补课”。
11.1 为什么第一轮检索会排错
第一轮检索通常更像“快速海选”。
它会很快找出一批相近内容,但不一定能细看问题里的重点。
常见原因有三个:
| 原因 | 会出现什么情况 |
|---|---|
| 只看整体相似 | 都提到“请假”,但没有区分用户真正问的是“补课” |
| chunk 内容太接近 | 请假、补课、作业、考勤可能都在同一类资料里 |
| top_k 取多了 | 为了不漏资料,会先拿进来一些边缘相关内容 |
所以 RAG 里经常会分成两步:
第一轮:召回
先多找一些,尽量别漏掉。
第二轮:重排
再精细判断,把最适合回答的排前面。
11.2 Rerank 到底看什么
Rerank 通常会把“用户问题”和“候选 chunk”一起交给一个重排模型。
它不是只看一个向量分数,而是更直接地判断:
| 判断点 | 例子 |
|---|---|
| 是否正面回答问题 | 问“怎么补课”,资料里有没有补课流程 |
| 关键词是不是抓对了 | “请假后补课”不能只匹配到“请假” |
| 内容是否完整 | 是否包含时间、联系人、规则限制 |
| 是否比其他 chunk 更适合 | 同样相关时,谁更能直接组成答案 |
可以把它理解成:
第一轮检索是粗找,Rerank 是精排。
11.3 常见流程
实际项目里常见做法是:
先检索 top 20 或 top 50
↓
Rerank 重新打分排序
↓
取前 3 到 5 个 chunk
↓
放进 Prompt 让大模型回答
这样做的好处是:
- 前面多召回一点,减少漏掉关键资料
- 后面再筛一遍,减少无关资料进入 Prompt
- 最终回答更容易贴着资料说
11.4 什么时候需要 Rerank
不是所有 RAG 都一定要加 Rerank。
可以先看场景复杂度。
| 场景 | 是否建议加 |
|---|---|
| 资料很少、问题简单 | 可以先不加 |
| FAQ 很规整 | 可以先靠向量检索或关键词检索 |
| 文档很多、内容相似 | 建议加 |
| 用户问题经常很细 | 建议加 |
| 对答案准确率要求高 | 建议加 |
例如课程规则、合同条款、产品文档里,经常会有很多相似段落。
这时只靠第一轮检索,容易把“沾边”的资料排在前面。
Rerank 能帮系统在这些相似资料里再分一次轻重。
11.5 Rerank 的代价
Rerank 也不是越多越好。
因为它要对候选 chunk 再计算一轮。
候选越多:
- 速度越慢
- 成本越高
- 系统链路更复杂
所以一般不是把整个知识库都交给 Rerank。
而是先用检索缩小范围,再让 Rerank 处理这一小批候选。
可以先记住这一句话:
检索负责“找得到”,Rerank 负责“排得准”。
12. 上下文拼接:不是检索到就全塞进去
检索到 chunk 后,还要决定怎么放进 Prompt。
这一步叫上下文拼接。
常见做法不是把所有检索结果原样塞进去,而是要处理几个问题。
12.1 去重
如果使用 overlap,相邻 chunk 可能有重复内容。
检索结果也可能返回几段高度相似的内容。
如果全部放进 Prompt,模型会看到重复资料。
这会浪费 Token,也可能让回答变啰嗦。
所以拼接前通常要去重。
12.2 控制长度
大模型上下文有限。
即使模型支持很长上下文,也不代表应该把所有资料都塞进去。
上下文越长:
- 成本越高
- 响应越慢
- 无关内容越多
- 模型越难抓住重点
所以要控制放入 Prompt 的 chunk 数量和总长度。
12.3 保留来源
放进 Prompt 的资料最好带上来源信息。
例如:
[来源:课程规则,第 2 段]
学生请假后,可以在一周内联系老师安排补课。
这样模型生成回答时,可以说明依据。
用户也能知道答案不是凭空来的。
12.4 顺序也会影响回答
Prompt 里资料的顺序会影响模型注意力。
一般可以按相关性排序:
最相关的资料放前面
次相关的资料放后面
如果问题涉及流程,也可以按业务顺序排序:
先请假
再补课
最后补交作业
顺序不是小事。
资料顺序乱,模型回答也可能乱。
13. 今天要带走什么
到这里,一个 RAG 知识库问答系统的主线就完整了。
它不是让大模型“记住所有资料”。
而是让系统在回答前,先把可能有用的资料找出来,再交给大模型组织答案。
可以记住这条链路:
资料处理
↓
切分 chunk
↓
生成 Embedding
↓
存入向量库
↓
用户提问
↓
检索 + Rerank
↓
拼接上下文
↓
调用大模型回答
今天最重要的是这几个点:
| 关键点 | 你要理解什么 |
|---|---|
| RAG | 先找资料,再基于资料回答 |
| chunk | 长文档要切成适合检索的小段 |
| Embedding | 把文字变成向量,方便比较语义相似 |
| 向量数据库 | 保存向量,并快速找出相似资料 |
| Top-K | 控制先拿回多少候选资料 |
| Rerank | 在候选资料里重新排序,把最相关的放前面 |
| 上下文拼接 | 最后只把有用资料放进 Prompt |
最后可以用一句话收尾:
RAG 的核心不是“把资料全塞给模型”,而是“把正确的资料找出来,再让模型基于资料回答”。
下一步真正写代码时,就是把这条流程一段一段实现出来。
第三天_Milvus向量数据库入门
第三天:Milvus 向量数据库入门
前面的 RAG 主课已经讲过向量数据库的基本作用。
本节从 Milvus 的实际使用开始,重点完成下面几件事:
Milvus 在 RAG 项目里放在哪里
如何使用 Docker 启动本地 Milvus 服务
如何使用 Attu 可视化界面查看 Milvus
Milvus 里的 Collection、Field、Schema 怎么理解
如何用 Python 创建 Collection
如何写入资料向量
如何根据问题向量检索相似资料
如何把检索结果拼接到 RAG Prompt 中
1. Milvus 在 RAG 项目里的位置
把 RAG 流程拆开看,Milvus 只在其中两段出现。
第一段是入库阶段:
原始资料
↓
文档解析
↓
切分 chunk
↓
调用 Embedding 模型生成向量
↓
写入 Milvus
第二段是检索阶段:
用户问题
↓
调用同一个 Embedding 模型生成问题向量
↓
去 Milvus 做向量检索
↓
返回相似 chunk
↓
拼接 Prompt
↓
调用大模型生成答案
这里有一个重要要求:
资料入库和用户检索必须使用同一个 Embedding 模型。
如果资料向量是 A 模型生成的,问题向量是 B 模型生成的,它们可能不在同一个语义空间里。
这样检索结果会很不稳定。
就像:
资料用“米”作为单位
问题用“英尺”作为单位
然后直接比较数字
检索结果就容易不准确。
2. 使用 Docker 安装 Milvus
Milvus 可以用多种方式运行。
为了更接近真实项目,本节先使用 Docker 启动一个本地 Milvus 服务。
启动后,Python 代码会通过下面这个地址连接 Milvus:
http://localhost:19530
2.1 准备 Docker
安装 Milvus Standalone 之前,需要先安装 Docker。
如果电脑上已经安装了 Docker Desktop,可以先确认 Docker 是否可用:
docker --version
docker compose version
能看到版本号,说明 Docker 基本可用。
如果提示命令不存在,需要先安装 Docker Desktop,并启动 Docker。
2.2 下载 docker-compose 配置文件
新建一个目录专门放 Milvus 的启动文件,例如:
mkdir milvus-standalone
cd milvus-standalone
下载官方提供的 Docker Compose 配置文件。
优先使用 curl:
curl -L --connect-timeout 10 --retry 3 https://github.com/milvus-io/milvus/releases/download/v3.0-beta/milvus-standalone-docker-compose.yml -o docker-compose.yml
如果使用 wget,命令如下:
wget https://github.com/milvus-io/milvus/releases/download/v3.0-beta/milvus-standalone-docker-compose.yml -O docker-compose.yml
这里访问的是 GitHub。网络较慢时,命令可能会停在:
HTTP request sent, awaiting response...
如果长时间没有继续下载,可以按 Ctrl + C 停止,再重新执行 curl 命令,或者直接在浏览器中打开上面的链接下载文件,然后把文件改名为 docker-compose.yml。
下载完成后,当前目录里会出现:
docker-compose.yml
这个文件描述了 Milvus 运行需要的几个容器。
2.3 启动 Milvus
在 docker-compose.yml 所在目录执行:
docker compose up -d
-d 表示后台启动。
启动后可以查看容器状态:
docker compose ps
正常情况下,会看到类似下面的服务:
milvus-standalone
milvus-etcd
milvus-minio
这三个容器共同组成一个本地 Milvus Standalone 环境。
| 容器 | 作用 |
|---|---|
| milvus-standalone | Milvus 主服务,提供向量写入和检索能力 |
| milvus-etcd | 保存 Milvus 的元数据 |
| milvus-minio | 保存向量索引、日志等对象数据 |
可以这样理解:
milvus-standalone:真正对外提供 Milvus 能力
milvus-etcd:保存 Milvus 的配置、Collection 信息、节点状态等元数据
milvus-minio:保存 Milvus 使用的对象数据,例如索引文件、日志文件等
三个容器的关系类似:
Python / Attu
↓
milvus-standalone
↓
etcd 保存元数据
minio 保存对象文件
所以判断 Milvus 是否启动成功时,重点看三件事:
| 检查项 | 说明 |
|---|---|
milvus-etcd 是 healthy |
元数据服务正常 |
milvus-minio 是 healthy |
对象存储服务正常 |
milvus-standalone 是 Up |
Milvus 主服务正常 |
其中 milvus-standalone 最关键。
如果只看到 milvus-etcd 和 milvus-minio,但是没有看到 milvus-standalone,说明依赖服务起来了,但 Milvus 主服务没有启动成功。
入门阶段不需要深入理解 etcd 和 MinIO。
先知道:
Milvus Standalone 不是单独一个容器,它还会依赖 etcd 和 MinIO。
2.4 确认 Milvus 端口
Milvus 默认服务端口是:
19530
也就是 Python 连接时使用:
http://localhost:19530
如果 Docker Compose 正常启动,后续 Python 可以这样连接:
from pymilvus import DataType, MilvusClient
client = MilvusClient(uri="http://localhost:19530")
如果服务开启了账号密码,可以写成:
from pymilvus import MilvusClient
client = MilvusClient(
uri="http://localhost:19530",
token="your_milvus_token",
)
本地练习时,如果没有开启认证,一般使用第一种连接方式即可。
2.5 访问 Milvus WebUI
Docker Compose 启动后,Milvus 通常还会提供 WebUI。
浏览器访问:
http://127.0.0.1:9091/webui/
WebUI 可以用来查看 Milvus 实例状态。
如果页面打不开,可以先检查:
- Docker Desktop 是否已经启动
docker compose ps里容器是否正常运行- 端口
9091是否被其他程序占用
2.6 使用 Attu 可视化管理 Milvus
除了 Milvus 自带的 WebUI,还可以使用 Attu 查看和管理 Milvus。
Attu 是 Milvus 常用的可视化管理工具,可以用来查看数据库、Collection、字段结构、索引、数据和检索结果。
在本地练习时,它的作用类似:
用 Python 写入数据
↓
用 Attu 查看 Collection 是否创建成功
↓
查看字段、索引和插入的数据
↓
辅助排查连接、写入和检索问题
2.6.1 下载安装 Attu 桌面版
Attu 可以作为桌面软件安装使用。
下载地址:
https://zilliz.com.cn/attu
进入页面后,点击“下载 Attu”,根据自己的系统选择安装包:
| 系统 | 常见安装包 |
|---|---|
| macOS | .dmg |
| Windows | .exe |
| Linux | .AppImage 或 .deb |
安装完成后,直接打开 Attu 软件即可。
桌面版适合本地学习和课堂演示,因为它不需要再额外启动一个 Attu 容器。
2.6.2 在 Attu 中连接本地 Milvus
打开 Attu 后,需要新建一个 Milvus 连接。
如果 Milvus 是通过前面的 Docker Compose 在本机启动的,连接信息一般这样填写:
| 配置项 | 本地 Milvus 示例 |
|---|---|
| Name | local-milvus |
| Address | localhost:19530 |
| Database | default |
| Token | 未开启认证时可以不填 |
这里的 localhost:19530 表示连接本机 Docker 暴露出来的 Milvus 服务端口。
连接成功后,可以在 Attu 中看到:
Database
Collection
Schema
Index
Entities
Search / Query
后面用 Python 创建 Collection、插入数据后,可以回到 Attu 页面刷新查看。
2.6.3 停止 Milvus
练习结束后,可以停止 Milvus:
docker compose down
如果只想暂停容器,也可以使用:
docker compose stop
两者区别:
| 命令 | 作用 |
|---|---|
| docker compose stop | 停止容器,后面可以再 start |
| docker compose down | 停止并移除容器网络等资源 |
如果删除数据目录,之前写入的 Milvus 数据也会丢失。
所以练习时不要随意删除 volumes 目录。
3. Milvus 的几个核心概念
在写代码之前,需要先认识几个 Milvus 里的核心概念。
这些词刚开始看起来像数据库术语,但其实和 MySQL 很好类比。
3.1 Collection:集合
Collection 可以理解成 MySQL 里的表。
例如我们要存课程知识库,就可以建一个 Collection:
course_knowledge
这个 Collection 里保存的不是普通订单数据,而是一条一条的知识片段。
每条记录可以设计成下面这种结构:
{
"id": 1,
"vector": [0.12, 0.36, 0.78, "..."],
"content": "学生请假后,可以在一周内联系老师安排补课。",
"metadata": {
"source": "course_rule.md",
"category": "补课规则"
}
}
3.2 Field:字段
Field 就是 Collection 里的字段。
一条 RAG 知识记录通常会有这些字段:
| 字段 | 类型 | 作用 |
|---|---|---|
| id | 主键 | 唯一标识这一条 chunk |
| vector | 向量字段 | 用来做相似度检索 |
| content | 字符串 | 命中后放进 Prompt 的原文 |
| metadata | 元数据 | 保存来源、分类、页码、版本等标签 |
其中最特别的是 vector。
普通数据库字段一般是字符串、数字、时间。
但 vector 是一个浮点数数组:
[0.12, 0.36, -0.18, 0.77, ...]
这个数组就是 Embedding 模型给文本生成的语义表示。
metadata 里通常会放这些信息:
| metadata 字段 | 作用 |
|---|---|
| source | 资料来源,例如文件名、网页地址、文档标题 |
| category | 资料分类,例如请假规则、补课规则、退费规则 |
| chunk_index | 当前 chunk 在原文中的顺序 |
| updated_at | 资料更新时间 |
在 Milvus 的实际代码里,为了方便过滤,常常会把 metadata 中常用的字段展开成 Collection 里的标量字段。
例如把 metadata.source 展开成 source 字段,把 metadata.category 展开成 category 字段。
这样后面才能写:
filter="category == '补课规则'"
所以概念上可以这样理解:
source、category 属于 metadata
但在 Milvus 落库时,可以把它们作为单独字段保存,方便过滤和查询
3.3 Schema:结构
Schema 就是 Collection 的字段结构。
它规定:
这个 Collection 有哪些字段
每个字段是什么类型
哪个字段是主键
哪个字段是向量
向量有多少维
例如:
id:整数主键
vector:768 维浮点向量
content:文本内容
source:从 metadata.source 展开的来源字段
category:从 metadata.category 展开的分类字段
为什么要提前设计 Schema?
因为 Milvus 需要知道向量的维度。
如果 Collection 设计的是 768 维向量,那么插入数据时也必须是 768 维。
如果你插入 1024 维,就会报错。
3.4 Dimension:向量维度
Dimension 就是向量里有多少个数字。
例如:
[0.12, 0.36, 0.78]
这是 3 维向量。
真实 Embedding 模型通常不是 3 维,而是几百维、上千维。
例如:
768 维
1024 维
1536 维
维度不能任意填写。
它由 Embedding 模型决定。
所以建 Collection 之前,需要先确认 Embedding 模型输出多少维。
3.5 Index:索引
索引是为了让检索更快。
如果没有索引,系统可能需要一个一个向量比较。
如果有索引,Milvus 会用专门的数据结构加速相似度检索。
可以把索引理解成:
为了快速找到相似向量,提前建立的一种检索结构。
入门阶段不需要记住所有索引类型。
先认识几个常见类型:
| 索引类型 | 简单理解 |
|---|---|
| FLAT | 暴力精确搜索,数据少时简单可靠 |
| IVF_FLAT | 先分桶,再在相关桶里找,常见入门选择 |
| HNSW | 图结构近似搜索,常用于高性能相似检索 |
学习阶段可以使用默认创建方式,等数据量变大后再深入索引参数。
3.6 Metric Type:相似度计算方式
Metric Type 决定 Milvus 怎么判断两个向量相似。
常见的有:
| Metric Type | 含义 | 适合理解 |
|---|---|---|
| COSINE | 余弦相似度 | 看方向像不像 |
| IP | Inner Product,内积 | 看匹配分数高不高 |
| L2 | 欧氏距离 | 看距离远不远 |
在 RAG 文本检索里,常见选择是 COSINE。
实际项目里更稳妥的做法是:
看 Embedding 模型和向量数据库文档推荐使用哪种距离方式。
4. 一条知识记录如何存储
假设课程资料里有一段话:
学生请假后,可以在一周内联系老师安排补课。
在 RAG 系统里,它不会只以字符串形式保存。
它会变成一条结构化记录:
{
"id": 1,
"content": "学生请假后,可以在一周内联系老师安排补课。",
"vector": [0.12, 0.36, 0.78, "..."],
"metadata": {
"source": "course_knowledge.txt",
"category": "补课规则"
}
}
每个部分都有明确作用:
| 部分 | 为什么需要 |
|---|---|
| id | 后续更新、删除、排查问题都需要 |
| content | 最终要给大模型看的原文 |
| vector | 用来和用户问题做相似度比较 |
| metadata.source | 回答时可以告诉用户资料来自哪里 |
| metadata.category | 可以做过滤,比如只查“补课规则” |
需要注意:
Milvus 检索时比较的是 vector,但返回给大模型的是 content。
向量只是检索工具。
最后大模型需要看的还是文字。
5. Python 连接 Milvus
Docker 中的 Milvus 启动后,Python 项目不能直接操作 Milvus。
中间需要一个 Python 客户端库。
这个库叫 pymilvus。
可以这样理解:
Milvus 是运行中的向量数据库服务
pymilvus 是 Python 代码连接和操作 Milvus 的工具包
在 Python 代码里,后面这些操作都要通过 pymilvus 完成:
| 操作 | pymilvus 负责什么 |
|---|---|
| 连接 Milvus | 连接 localhost:19530 上的 Milvus 服务 |
| 创建 Collection | 创建保存向量数据的集合 |
| 写入数据 | 把 id、vector、content、metadata 写入 Milvus |
| 向量检索 | 根据问题向量查找相似 chunk |
| 条件过滤 | 根据 category、source 等字段过滤数据 |
当前项目使用 uv 管理依赖,所以安装命令是:
uv add pymilvus
连接本地 Docker 中的 Milvus:
from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530")
这行代码的意思是:
连接本机 19530 端口上的 Milvus 服务
后续创建 Collection、写入数据、检索数据都会发到这个 Milvus 服务
如果服务开启了账号密码,可以写成:
from pymilvus import MilvusClient
client = MilvusClient(
uri="http://localhost:19530",
token="your_milvus_token",
)
如果执行下面代码没有报错,说明 Python 已经可以连接 Milvus:
from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530")
print(client.list_collections())
6. 课堂代码演示
这一部分课堂上直接使用 Python 代码演示。
示例文件在:
YanQue-AI/test/milvus_crud_test.py
这份代码会演示:
| 操作 | 说明 |
|---|---|
| 连接 Milvus | 使用 pymilvus 连接本地或服务器上的 Milvus |
| 创建 Collection | 创建 id、vector、content、metadata 字段 |
| 新增数据 | 插入课程知识文本、向量和 metadata |
| 查询数据 | 使用 query 按条件查看记录 |
| 向量检索 | 使用 search 查找语义相近的资料 |
| metadata 过滤 | 使用 metadata["category"] 限定资料分类 |
| 修改数据 | 使用 upsert 更新已有记录 |
| 删除数据 | 使用 delete 删除指定记录 |
运行方式:
cd YanQue-AI
uv run python test/milvus_crud_test.py
课堂讲解时重点看三个地方:
1. recreate_collection:字段是怎么创建的
2. insert_demo_data:一条知识数据是怎么存进去的
3. search_with_filter:怎么根据 metadata 做过滤检索







