RAG 与 Milvus 向量数据库入门

AI Agent 工程实践教程 · 第 03 章

把文档切分、向量检索、Milvus 存储和回答生成串成可理解的 RAG 流程。

返回系列目录

第三天_RAG知识库问答入门

第三天:RAG 知识库问答入门

前两天已经完成两件事:

  • 知道 AI 应用开发不是单纯聊天,而是把大模型能力接入项目
  • 理解大模型会把文字切成 Token,再通过上下文生成回答

第三天开始进入一个非常常见的 AI 应用能力:

让 AI 基于我们自己的资料回答问题。

这类能力通常叫做 RAG

先看今天的学习路线:

第三天 RAG 学习路线图

这一节的目标可以用一句话理解:

不要让大模型只靠自己的记忆回答。
而是先从资料里查出相关内容,再让大模型基于这些内容回答。

1. 为什么需要 RAG

第一天已经能用 Python 调用大模型。

如果用户问:

什么是人工智能?

大模型通常能直接回答。

但如果用户问的是下面这些问题,就不一定了:

我们学校 AI 课程第三天讲什么?

公司员工请假制度里,病假需要提交什么材料?

这个系统的退款流程什么时候会进入人工审核?

项目里的学生端 AI 问答接口地址是什么?

这些问题有一个共同点:

答案不一定在大模型自己的训练知识里,而是在我们自己的资料、文档、数据库或系统规则里。

如果直接把问题丢给大模型,主要会遇到两个问题。

为什么需要 RAG

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 像开卷考试:

先翻资料,找到和题目相关的内容,再组织答案。

所以 RAG 不是重新训练模型。

知识库里的资料更新后,不需要重新训练大模型。只要系统能检索到新资料,模型就可以基于新资料回答。

2.3 RAG 怎么把资料交给模型

从最小实现来看,RAG 的核心动作很清楚:

把查到的资料放进 Prompt。

例如用户问:

病假需要提交什么材料?

系统先从员工手册里查到一段资料:

病假需提交医院诊断证明或病历材料,连续请假超过三天时,需补充提交复诊记录。

然后真正发给大模型的内容可能是:

请只根据下面资料回答问题。
如果资料中没有答案,请回答“资料中没有提到”。

资料:
病假需提交医院诊断证明或病历材料,连续请假超过三天时,需补充提交复诊记录。

问题:
病假需要提交什么材料?

模型回答:

病假需要提交医院诊断证明或病历材料。
如果连续请假超过三天,还需要补充提交复诊记录。

这里最重要的不是模型自己知道请假制度,而是系统把制度资料交给了模型。

这就是 RAG 的基本思想。

3. 一个最小 RAG 需要哪些步骤

一个最小 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 的关键参数:大小、重叠和边界

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 叫做重叠长度。

它的意思是:

相邻两个 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 = 500chunk_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 直接读取文字,保留标题和段落
PDF 可能混着页眉、页脚、页码、表格、扫描图片
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 理解成:

给每段文字找一个“语义位置”。

意思接近的文字,位置就更近。

意思差得远的文字,位置就更远。

例如:

请假后怎么补课?

这句话进入 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 对比

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、召回和精准

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:对检索结果重新排序

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 最终总结流程

到这里,一个 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-etcdhealthy 元数据服务正常
milvus-miniohealthy 对象存储服务正常
milvus-standaloneUp Milvus 主服务正常

其中 milvus-standalone 最关键。

如果只看到 milvus-etcdmilvus-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 实例状态。

如果页面打不开,可以先检查:

  1. Docker Desktop 是否已经启动
  2. docker compose ps 里容器是否正常运行
  3. 端口 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 创建 idvectorcontentmetadata 字段
新增数据 插入课程知识文本、向量和 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 做过滤检索