前言:
步入 2026 年,人工智能行业正式告别野蛮生长的试水阶段,全面迈入产业落地、场景深耕、轻量化普及的全新发展周期。当下行业不再单纯追捧大模型理论研究,而是更加注重实用化开发、低成本落地、业务场景融合。RAG 检索增强、智能 Agent 多体协作、轻量化模型微调、LLM 应用搭建、AI 自动化办公等实战方向持续火热。
而在 RAG 技术栈中,向量数据库是实现 “精准检索、高效匹配” 的核心基础设施,而Milvus 又被选为向量数据库的首选(优于 Chroma 和 Pinecone),是一个专为大规模高维向量数据集的相似性搜索而设计的开源向量数据库。今天 xiaoyu 就结合 2026 年 AI 落地趋势,给大家带来 Milvus 向量数据库的基本操作与实战使用指南,帮大家快速打通 RAG 开发的各项关键环节。
学习前提:
- 具备 Python 基础开发能力(熟悉语法、函数、第三方库调用以及基础面向对象);
- 对大模型 Prompt 工程有初步认知(可选,不影响基础操作学习)。
- 了解Docker容器化部署
- 注意:本文讲的是Milvus原生API开发,即pymilvus!并非Langchain版的langchain_milvus,需要学习langchain_milvus可以去看xiaoyu的另一篇文章
适用人群:
- 想要学习 RAG 开发的 Python 开发者;
- 计划转型 AI 领域、聚焦实战开发的 Java 开发者;
- 需搭建企业知识库、智能问答系统的技术人员;
- 对向量数据库、检索增强生成技术感兴趣的学习者。
一、RAG简介
1. RAG 定义与核心逻辑
RAG(Retrieval-Augmented Generation,检索增强生成) ,是一种将 “信息检索技术” 与 “大语言模型(LLM)生成能力” 结合的技术方案。其核心逻辑是:在 LLM 生成答案前,先从指定的私有数据源(如企业文档、知识库、本地文件)中检索出与用户问题高度相关的信息,再将这些 “精准参考资料” 作为 Prompt 一部分喂给 LLM,让模型基于真实数据生成答案。
可以用一个直观的公式总结:RAG = 向量检索(精准找信息) + LLM 提示(基于信息生成答案)
2. RAG 核心流程
一个完整的 RAG 流程可分为 4 步,其中存储和检索两步直接依赖向量数据库:
- 数据预处理:将私有文档(PDF/Word/Markdown 等)拆分成分段文本(避免上下文过长);
- 向量嵌入:通过 Embedding 模型(如 Sentence-BERT、通义 Embedding)将文本转换成计算机可识别的 “向量”;
- 向量存储:将生成的向量数据存入 Milvus 等向量数据库,建立高效检索索引;
- 检索生成:用户提问后,先从 Milvus 中检索出 Top N 相关向量对应的文本,再将文本与问题一起传给 LLM,生成精准答案。
二、向量
1. 定义与解释
向量(Vector)可以把一段文字的语义信息,转换成一串固定长度的数字列表,让计算机能看懂文字的含义并做相似度计算。
简单来说,就是让计算机更方便的理解不同的文本内容,是否表述的是一个意思。
2. 数字序列提取方法
根据2.1所说,得到的这个数字列表或者又叫序列是为了可以让计算机看懂人类的文字与含义,因此,这个数字序列越精确越好。
我们可以使用文本嵌入模型(如阿里云的text-embedding-v4)通过深度学习等技术,从文本提取语义特征并映射为固定长度的数字序列。
3. 余弦相似度
向量的数字序列,在空间上可以表示出向量的长度和方向,使用余弦相似度就是为了排除长度对结果的影响,只判断两向量的夹角,夹角越小相似度越高
计算时,使用cos<a,b>=(a·b)/(||a||*||b||),即点积除以模长乘积
三、Milvus的基本使用
官方文档:Quickstart Milvus v2.6.x documentation
1. Milvus辅助容器介绍
Milvus本身只负责检索功能,除了Milvus容器的创建,还需要创建两个辅助容器:etcd和minio
可以这样理解:
- Milvus = 检索引擎(负责查向量)
- etcd = 管理员(记住表结构、配置)
- minio = 仓库(存真正的向量文件)
etcd 是一个分布式键值存储,用于保存 Milvus 的元数据、集合信息、索引信息和系统配置,保证 Milvus 服务状态的一致性与高可用,存储包括:
- 集合(collection)名称
- 字段有哪些
- 索引建没建
- 向量库当前状态
- 集群配置信息
minio 是高性能对象存储服务,用于持久化存储 Milvus 产生的向量数据、索引文件、日志与快照数据,是 Milvus 数据落盘的核心依赖。它的存储包括:
- 真正的向量数据
- 落盘的索引文件
- 日志文件
- 数据快照
2. 部署指令
采用数据卷挂载方式部署:
# 1. 启动 etcd
docker run -d \
--name milvus-etcd \
--restart always \
--network xxx \
-v etcd-data:/etcd \
quay.io/coreos/etcd:v3.5.18 \
etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
# 2. 启动 MinIO
docker run -d \
--name milvus-minio \
--restart always \
--network xxx \
-v minio-data:/minio_data \
-e MINIO_ACCESS_KEY=minioadmin \
-e MINIO_SECRET_KEY=minioadmin \
minio/minio:RELEASE.2023-03-20T20-16-18Z \
minio server /minio_data
# 3. 启动 Milvus Standalone
docker run -d \
--name milvus \
--restart always \
--network xxx \
-p 19530:19530 \
-p 9091:9091 \
-v milvus-data:/var/lib/milvus \
-v milvus-logs:/var/lib/milvus/logs \
-v milvus-conf:/var/lib/milvus/configs \
-e ETCD_ENDPOINTS=etcd:2379 \
-e MINIO_ADDRESS=minio:9000 \
milvusdb/milvus:v2.6.2 \
milvus run standalone
3. 对目标文档进行向量化处理
- 使用阿里云相关embedding模型,创建DashScopeEmbeddings对象:
from langchain_community.embeddings import DashScopeEmbeddings
embed = DashScopeEmbeddings(
dashscope_api_key="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxx",
model="text-embedding-v4"
)
- 准备一段示例文档(博主也是米游资深玩家,这里就用崩坏星穹铁道中角色灵砂的介绍作为示例~):
来源:百度百科-灵砂
from typing import List
from pymilvus import MilvusClient
docs= ["""
灵砂,游戏《崩坏:星穹铁道》中的五星角色.
灵砂师从炎庭君,土生土长的仙舟「罗浮」持明,丹鼎司新任司鼎。
灵砂是一名召唤烟兽支援队友的回复型角色。烟兽行动时发动追加攻击,回复己方全体生命值并解除负面效果。此外灵砂还能提高敌方受到的击破伤害
灵砂的身世:灵砂是土生土长的罗浮持明族人,曾以「丹朱」之名恭敬地拜师求艺,一心渴望在师父的教导下精进技艺。然而世事无常,师父不幸卷入残酷的政争漩涡,最终被流放至遥远的他乡。她念及师徒情分,毅然决然地追随师父,一同踏上了漂泊之路。
新的工作环境:结合这段时间的亲身感受,灵砂只能感慨这工作环境中的「气味」浑浊难辨,且时刻处于千变万化之中。初来乍到那日,一名丹士便径直向......
"""]
- 使用embed的同步向量化方法embed_documents()对文档进行向量化处理(异步方法在方法前面加"a"即可):
doc_res: List[List[float]] = embed.embed_documents(docs)
4. 向Milvus中存储数据
- 连接并创建新的集合:
# 连接milvus
client = MilvusClient(uri="tcp://192.168.100.128:19530")
# 删除并创建新的集合(删除可选,不删除就是直接追加)
if client.has_collection("ct_test"):
client.drop_collection("ct_test")
client.create_collection(
collection_name="ct_test",
dimension=1024
)
- 数据模板:
# 官方文档模板,仅添加"text便于模型可以查看到文档结果"
data = [
{"id": i, "vector": vec, "text": docs[i]} for i, vec in enumerate(doc_res)
]
id:文档id
vector:文档向量化之后的数字序列
text:文档具体内容
3. 插入数据:
embed_res = client.insert(
collection_name="ct_test",
data=data
)
print(embed_res)
运行结果=>run_res:{'insert_count': 1, 'ids': [0]}即可说明插入成功
5. 模型检索回答
- 初始化聊天模型(这里使用TongyiAI演示):
from langchain_community.chat_models import ChatTongyi
from langchain_community.embeddings import DashScopeEmbeddings
embed = DashScopeEmbeddings(
dashscope_api_key="sk-xxxxxxxxxxxxxx",
model="text-embedding-v4"
)
chat = ChatTongyi(
model="qwen-flash",
dashscope_api_key="sk-xxxxxxxxxxxxxx",
)
- 使用原生Minvus加载客户端
from pymilvus import MilvusClient
client = MilvusClient(uri="tcp://192.168.100.128:19530")
def retrieve(question: str):
# 1. 问题向量化
q_vector = embed.embed_query(question)
# 2. 去 Milvus 搜索
res = client.search(
collection_name="ct_test",
data=[q_vector],
limit=1,
output_fields=["text"]
)
# 3. 取出检索到的文本
if res and res[0]:
return res[0][0]["entity"]["text"]
return "无相关资料"
client.search(...) 所有参数详解:
参数 1:collection_name="ct_test"
- 你要搜索哪个集合(表)
- 必须和你创建时的名字一样
参数 2:data=[q_vector] - 搜索用的向量
- 必须放在列表里:
[向量] - 因为 Milvus 支持批量多条向量同时搜索
参数 3:limit=1 - 返回最相似的 top-k 条结果
limit=1→ 只返回最像的 1 条- 想返回多条就写
limit=3
参数 4:output_fields=["text"] - 希望 Milvus 返回哪些字段
- 你入库时存的是:
id、vector、text - 这里写
["text"]→ 只返回文本内容 - 可以写多个:
["id", "text"]
- 构建提示词模板,获取AI回答:
prompt_temp = PromptTemplate.from_template("""
人物定位:现在你是一个游戏问答专家
任务目标:玩家问题为'{question}'
参考资料:{context}
边界限制:不知道不清楚的问题一律回答'{dont}'
""")
chain = prompt_temp | chat | StrOutputParser()
question = "灵砂对新的工作环境有什么感受?"
context = retrieve(question)
res = chain.invoke({
"question": question,
"context": context,
"dont": "这个问题我还没学会哦"
})
print(res)
运行结果=>灵砂对新的工作环境的感受,可以用一句轻叹中带着坚定的话来概括:“这丹鼎司里的气味,浑浊难辨,千变万化……可即便如此,妾身也未曾动摇。”
初来乍到,她便察觉到此处风气沉滞,人心浮躁。一名丹士因不满“人情世故重于技艺”而递交辞呈,那决然离......
可以看到AI确实结合Milvus的检索结果回答了我们的问题,没有偏离回答方向
四、批量插入数据
1. 文本批量插入
两种方式:
- 将之前的文本手动分割成一部分一部分的:
docs= ["""
灵砂,游戏《崩坏:星穹铁道》中的五星角色.
灵砂师从炎庭君,土生土长的仙舟「罗浮」持明,丹鼎司新任司鼎。
灵砂是一名召唤烟兽支援队友的回复型角色。烟兽行动时发动追加攻击,回复己方全体生命值并解除负面效果。此外灵砂还能提高敌方受到的击破伤害
"""
"""
灵砂的身世:灵砂是土生土长的罗浮持明族人,曾以「丹朱」之名恭敬地拜师求艺,一心渴望在师父的教导下精进技艺。然而世事无常,师父不幸卷入残酷的政争漩涡,最终被流放至遥远的他乡。她念及师徒情分,毅然决然地追随师......
"""]
加载时就会加载成多条文档数据
- 使用Langchain自带的文本分割器进行自动切割,只需要手动设置切割方式即可
a. 导入切割器
from langchain_text_splitters import RecursiveCharacterTextSplitter
b. 开始切割文本
text_spliter = RecursiveCharacterTextSplitter(
chunk_size=256, # 每块文本的最大长度
chunk_overlap=32, # 块与块之间的重叠长度,即:上一段的最后 32 个字符,会在下一段开头重复出现
separators=["$$$"] # 分割符列表(切割标识优先级:从左到右),分隔符列表也可以不写,不写的话就是做默认智能切割(可以根据语义和标点进行中英文句子切割)
)
# 切割文本
docs = text_spliter.split_text(doc)
同样可以根据自定义的separators切割成多个部分
2. 切割器的种类
机器切割RecursiveCharacterTextSplitter(上面使用的):
优点:
- 稳定、轻量、免费(无额外 Embedding 调用)
- 支持自定义分隔符+ 标点兜底
- 速度快、成本低、中文适配好
缺点: - 不是真・语义,只是 “规则 + 长度” 尽量不碎句
常用参数:chunk_size、chunk_overlap、separator
AI语义切割SemanticChunker
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
# 1. 初始化嵌入模型 (以OpenAI为例)
embeddings = OpenAIEmbeddings()
# 2. 创建语义切割器
text_splitter = SemanticChunker(
embeddings=embeddings, # [必需] 用于生成句子向量的嵌入模型[reference:0]
breakpoint_threshold_type="percentile", # [可选] 断点判断策略,默认为 "percentile"[reference:1]
breakpoint_threshold_amount=95.0, # [可选] 阈值,默认值随策略类型变化
min_chunk_size=100, # [可选] 最小块大小 (字符数)
max_chunk_size=1000, # [可选] 最大块大小 (字符数)
sentence_separator="。!?.!?", # [可选] 句子分隔符,用于多语言支持
buffer_size=1 # [可选] 考虑前后句的窗口大小,默认1
)
# 3. 执行切割
# 方式一: 直接切割文本,返回字符串列表
docs = text_splitter.split_text(your_long_text)
# 方式二: 创建LangChain Document对象列表
# docs = text_splitter.create_documents([your_long_text])
优点:
- 真・语义切割:主题变了才切,同主题不分
- 检索更准、RAG 回答更连贯
缺点: - 实验性:接口可能变,生产有风险
- 贵且慢:每句要算 Embedding,耗 token、耗时
- 短文本(<500 字)收益极小,甚至更差
常用参数:
-
embeddings(使用embed嵌入模型赋值)
-
breakpoint_threshold_type:控制语义切割的 “松紧度”
常用属性:- percentile:语义差异大于一定百分比即可切割,通用、稳定,适合大部分文档
- standard_deviation:按语义距离的标准差判断切割,切割更粗,块更大
-
breakpoint_threshold_amount:配合前面的参数,调整语义分割力度。数字越大,切得越少,块越大,反之亦然
3. 使用加载器加载文本
Langchain官方提供的加载器非常多,有几十上百种:文档加载器 | LangChain中文指南(LangChain官网官方文档)
这里我们给出几个常用的,基本就已经覆盖了85%+的场景了:
通用文本加载器:DirectoryLoader
作用:读取整个文件夹里的所有文件(批量 = 多条),搭配下面的单文件加载器用,万能批量工具
- TXT专用:TextLoader(多条配合DirectoryLoader)
# 使用文本加载器加载单个文本文件
loader=TextLoader("./txt_file/test1.txt",encoding="utf-8")
doc = loader.load()[0].page_content
# 批量加载文件
loader = DirectoryLoader(
path="./txt_file",
glob="*.txt",
loader_cls=TextLoader,
loader_kwargs={"encoding": "utf-8"}
)
docs = loader.load()
- PDF专用:PyMuPDFLoader(多条配合多条配合DirectoryLoader或者单独使用PyPDFDirectoryLoader)
# 单条加载
from langchain_community.document_loaders import PyMuPDFLoader
loader = PyMuPDFLoader("单个文件.pdf")
doc = loader.load()[0].page_content # 单条数据
# 通用批量加载
from langchain_community.document_loaders import DirectoryLoader, PyMuPDFLoader
loader = DirectoryLoader(
path="./pdf文件夹/",
glob="*.pdf",
loader_cls=PyMuPDFLoader
)
docs = loader.load() # 批量Document列表
# 专用批量加载
from langchain_community.document_loaders import PyPDFDirectoryLoader
loader = PyPDFDirectoryLoader("./pdf文件夹/")
docs = loader.load() # 批量Document列表
- DOCS专用:UnstructuredWordDocumentLoader(多条配合DirectoryLoader)
# 单条:单个DOCX文件
from langchain_community.document_loaders import UnstructuredWordDocumentLoader
loader = UnstructuredWordDocumentLoader("单个文件.docx")
doc = loader.load()[0].page_content # 提取纯文本
# 多条:配合DirectoryLoader批量读取DOCX文件夹
from langchain_community.document_loaders import DirectoryLoader, UnstructuredWordDocumentLoader
loader = DirectoryLoader(
path="./docx文件夹/",
glob="*.docx",
loader_cls=UnstructuredWordDocumentLoader
)
docs = loader.load() # 批量Document列表
- 网页读取:WebBaseLoader(单条多条全能读,最简单、最稳定)
from langchain_community.document_loaders import WebBaseLoader
# 单条:单个网址
loader = WebBaseLoader("https://www.baidu.com")
# 多条:多个网址
# loader = WebBaseLoader(["https://网址1", "https://网址2"])
docs = loader.load()
五、Milvus进阶——增量入库与去重
目的:去业务重复,不是防碰撞
- 增量入库:不删库!每次只加「新文件 / 新文本」,旧数据保留
- 去重:同一段文本,绝对不重复入库(类似于给数据添加唯一主键,或者使用了set集合)
具体实现方案:
- 给每一段切分后的文本,生成一个MD5 哈希指纹(唯一标识,类似 Java 的 UUID)
- 向量库(Milvus)里加 1 个字段:
text_hash,存这个指纹 - 入库前先查 Milvus:是否存在该指纹
使用方法:
- 引入依赖:
Python 自带hashlib,生成文本唯一指纹
import hashlib # MD5哈希工具
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from pymilvus import MilvusClient
from langchain_community.document_loaders import TextLoader
- 创建工具函数,进行MD5哈希映射
# 工具函数:生成文本的MD5唯一指纹
def get_text_hash(text: str) -> str:
# 清洗文本(去空格/换行,避免格式不同导致指纹不同)
clean_text = text.strip().replace("\n", "").replace(" ", "")
# 生成MD5哈希
return hashlib.md5(clean_text.encode("utf-8")).hexdigest()
- 修改Milvus建库逻辑,只在库不存在时创建,且新增
text_hash字段
client = MilvusClient(uri="tcp://192.168.100.128:19530")
collection_name = "ct_test"
# 如果集合不存在才创建集合
if not client.has_collection(collection_name):
client.create_collection(
collection_name=collection_name,
dimension=1024,
auto_id=True,
id_type="int"
)
- 修改Milvus入库逻辑,进行增量去重
遍历每一段文本,进{}行去重+增量,将目标数据放入insert_data
insert_data = []
for i, (text, vector) in enumerate(zip(texts, docs_res)):
text_hash = get_text_hash(text)
# 去重核心:逐条查询Milvus是否包含该指纹
exist_res = client.query(
collection_name=collection_name,
filter=f'text_hash=="{text_hash}"',
output_fields=["id"],
)
# 查询到了,说明有重复,直接跳过
if exist_res:
print(f"文本'{text[:20]}'已存在")
continue
# 没有查询到,正常插入
insert_data.append(
{
"vector": vector,
"text": text,
"text_hash": text_hash
}
)
try:
if insert_data:
embed_res = client.insert(
collection_name="ct_test",
data=insert_data
)
for idx, dict_data in enumerate(insert_data, start=1):
print(f"数据{idx}为:{dict_data.get('text')},哈希为:{dict_data.get('text_hash')}")
print(embed_res)
else:
print("数据均已存在")
except Exception as e:
print(f"数据错误,问题:{str(e)}")
结语
至此,我们从 RAG 核心逻辑出发,完整学习了 Milvus 原生 API 的实战,利用阿里云 Embedding 模型完成文本向量化,通过 PyMilvus 实现精准存储与检索,并结合大模型生成高质量答案。在此基础上,进一步探讨了批量文本切割(机器切割 vs AI 语义切割)、多格式文档加载,以及基于 MD5 哈希的增量入库与去重机制——这些都是企业级 RAG 落地中绕不开的“脏活累活”。
所以,希望这篇指南能帮你快速上手 Milvus,在自己的知识库、问答系统或 Agent 项目中迈出扎实一步。