走进向量数据库:用 Milvus 构建 AI 日记助手的 RAG 实战
一、背景认知:什么是向量数据库?
在传统的 Web 应用开发中,我们把结构化数据存进 MySQL、SQLite、PostgreSQL 这样的关系型数据库,通过 id 或 LIKE 关键词来做增删改查。这种方式擅长处理精确匹配——"找到 id 为 123 的记录"。
但 AI Agent 面临的场景截然不同。Agent 需要存储知识、记忆,然后通过语义来检索——"最近有哪些让我快乐的日记?""哪些内容跟户外活动相关?"这类需求,靠 SQL 的 LIKE 是无法解决的。
这就是向量数据库登场的原因。它的核心流程是:
文档向量化 → 存入向量数据库 → 查询时把 query 也向量化 → 在库中做相似度匹配 → 查出相关文档
Milvus 是目前最流行的开源向量数据库之一,专为海量高维向量数据的存储和检索而设计(Zilliz 则是基于 Milvus 的全托管云服务)。本文通过一个「AI 日记本」项目,带你从零体验 Milvus 的核心操作。
二、项目整体架构
项目包含四个核心文件,层层递进:
| 文件 | 职责 |
|---|---|
main.mjs | 最简 demo:连接→建集合→建索引→插入→搜索 |
index.mjs | 进阶:自定义 Schema、批量向量化日记并入库 |
query.mjs | 语义查询:用自然语言搜日记 |
rag.mjs | RAG 闭环:检索 + LLM 生成回答 |
三、关键代码详解
3.1 连接 Milvus 与初体验(main.mjs)
import { MilvusClient, IndexType, MetricType } from '@zilliz/milvus2-sdk-node';
const client = new MilvusClient({
address: ADDRESS, // Zilliz Cloud 地址
token: TOKEN, // API Key 认证
});
// 健康检查——确保连接通畅
const checkHealth = await client.checkHealth();
知识点 1:C/S 架构
Milvus 采用客户端/服务端架构(类似 MySQL 的客户端连接服务器),MilvusClient 封装了与 Milvus/Zilliz 服务端的网络通信。
随后是创建 Collection(集合),它类比 MySQL 中的 table:
await client.createCollection({
collection_name: COLLECTION_NAME,
dimension: DIMENSION,
auto_id: true, // 自动生成主键 ID
});
知识点 2:Collection = MySQL Table
Milvus 的 Collection 相当于关系型数据库中的一张表,dimension 定义了向量字段的长度。
接下来创建索引:
await client.createIndex({
collection_name: COLLECTION_NAME,
field_name: 'vector', // 指定为哪个字段建索引
index_type: IndexType.AUTOINDEX,
metric_type: MetricType.COSINE, // 余弦相似度
});
知识点 3:为什么向量数据库必须建索引?
当数据量小时,暴力计算(逐一比较每个向量与查询向量的相似度)勉强可用,时间复杂度是 O(n) 。但当数据量上到百万、千万级别,每次都全量扫描是不可接受的。
索引的本质是缩小搜索范围。类比生活中的例子:
- 字典:通过拼音或偏旁部首索引,快速定位目标汉字
- 图书馆:没有索引时找《三体》要翻阅每一本书;有了分类(文学馆→小说→科幻),秒级定位
Milvus 提供多种索引类型,IVF_FLAT 是一种基于聚簇的索引,能将查询从 O(n) 降到毫秒级。
知识点 4:相似度度量——MetricType
MetricType.COSINE:余弦相似度,衡量两个向量方向上的接近程度,是语义匹配最常用的度量方式- 其他选项还包括欧氏距离(L2)、内积(IP)等
插入和搜索操作:
// 插入——比 MySQL 更宽松,可以在插入时动态建字段
const data = [
{ vector: [0.1, 0.2, 0.3, 0.4], content: '这是第一条数据' },
{ vector: [0.5, 0.6, 0.7, 0.8], content: '这是第二条数据' },
];
const insertRes = await client.insert({ collection_name, data });
// 搜索——传入向量,返回最相似的 top N
const searchRes = await client.search({
collection_name: COLLECTION_NAME,
data: [[0.5, 0.5, 0.6, 0.8]],
limit: 2,
output_fields: ['content'],
});
知识点 5:JSON 风格的数据操作
与 MySQL 写 SQL 不同,Milvus 的 SDK 以 JSON 对象的方式完成增删改查,开发者无需拼接 SQL 字符串,更加直观。
3.2 自定义 Schema 与向量化入库(index.mjs)
main.mjs 使用简单的 dimension 参数自动创建 Schema。在实际项目中,我们需要更精细的字段定义:
import { DataType } from '@zilliz/milvus2-sdk-node';
await client.createCollection({
collection_name: 'ai_dairy',
fields: [
{
name: 'id',
data_type: DataType.VarChar, // 字符串类型
max_length: 50,
is_primary_key: true, // 设为主键
},
{
name: 'vector',
data_type: DataType.FloatVector, // 浮点向量
dim: VECTOR_DIM, // 维度 1024
},
{
name: 'content',
data_type: DataType.VarChar,
max_length: 5000,
},
{
name: 'date',
data_type: DataType.VarChar,
max_length: 50,
},
{
name: 'mood',
data_type: DataType.VarChar,
max_length: 50,
},
{
name: 'tags',
data_type: DataType.Array, // 数组类型!
element_type: DataType.VarChar,
max_capacity: 10,
max_length: 50,
},
],
});
知识点 6:DataType 字段类型约束
Schema 定义的字段类型与 MySQL 的列类型概念相通,但 Milvus 有其特殊的数据类型:
| DataType | 含义 |
|---|---|
VarChar | 可变字符串,需指定 max_length |
FloatVector | 浮点向量,需指定 dim(维度) |
Array | 数组类型,需指定 element_type、max_capacity、max_length |
AI 日记本的设计思路就体现在这里:关系型数据库存储结构化的日记元数据(id、日期、心情、标签),而 Milvus 额外存储一个 vector 字段——日记内容的向量化表示,用于语义检索。
接下来是文本向量化与批量入库:
import { OpenAIEmbeddings } from '@langchain/openai';
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDINGS_MODEL_NAME, // Embedding 模型
dimensions: VECTOR_DIM, // 输出维度 1024
});
const getEmbedding = async (text) => {
const result = await embeddings.embedQuery(text);
return result; // 返回一个 1024 维的浮点数数组
};
知识点 7:Embedding 模型的作用
Embedding 模型将自然语言文本转化为高维向量(本例中为 1024 维浮点数组)。语义相近的文本,其向量在空间中的位置也相近。这是语义搜索的数学基础。
然后,将 5 条日记逐条向量化并批量插入:
const diaryData = await Promise.all(
diaryContents.map(async (diary) => ({
...diary,
vector: await getEmbedding(diary.content), // 内容 → 向量
}))
);
const insertResult = await client.insert({
collection_name: COLLECTION_NAME,
data: diaryData,
});
3.3 语义搜索(query.mjs)
有了向量化的日记数据,就可以用自然语言来搜索了:
const query = '我想看看关于户外活动的日记';
const queryEmbedding = await getEmbedding(query); // query 也转成向量
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
vector: queryEmbedding, // 用向量去搜
limit: 2, // Top 2 最相似
metric_type: MetricType.COSINE, // 余弦相似度
output_fields: ['id', 'content', 'date', 'mood', 'tags'],
});
// 遍历结果
searchResult.results.forEach((item, index) => {
console.log(`${index + 1}. [Score: ${item.score.toFixed(4)}]`);
console.log(`ID: ${item.id}, Date: ${item.date}, Mood: ${item.mood}`);
});
知识点 8:语义搜索的核心流程
用户 query → Embedding 模型 → 查询向量 → Milvus 相似度搜索 → 返回 Top-K 结果
这里的关键在于 score——它是 query 向量与每条文档向量之间的余弦相似度得分。score 越高,语义越接近。用户问"户外活动","周末和朋友去爬山"就被精准命中了。
3.4 RAG 闭环——检索增强生成(rag.mjs)
这是整个项目的最终形态:RAG(Retrieval-Augmented Generation) ——将检索与生成结合。
import { ChatOpenAI } from '@langchain/openai';
const model = new ChatOpenAI({
temperature: 0.1, // 低温度,回答更确定
model: process.env.MODEL_NAME, // 对话模型
});
RAG 分为两大模块:
模块一:检索(Retrieve)
async function retrieveRelevantDiaries(question, k = 2) {
const queryVector = await getEmbedding(question);
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
vector: queryVector,
limit: k,
metric_type: MetricType.COSINE,
output_fields: ['id', 'content', 'date', 'mood', 'tags'],
});
return searchResult.results;
}
模块二:增强生成(Augmented Generation)
// 将检索到的日记拼成上下文
const content = retrievedDiaries
.map((diary, i) => `
[日记 ${i+1}]
日期:${diary.date} 心情:${diary.mood}
标签:${diary.tags?.join(', ')} 内容:${diary.content}
`).join('\n\n----\n\n');
// 构造 Prompt,将检索结果注入
const prompt = `你是一个温暖贴心的AI日记助手。基于用户的日记内容回答问题。
用亲切自然的语言。请根据以下日记内容回答问题:
${content}
用户问题: ${question}
回答要求:
1. 如果日记中有相关信息,请结合日记内容给出详细、温暖的回答。
2. 可以总结多篇日记的内容,找出共同点或趋势。
3. 如果日记中没有相关信息,请温和告知用户。
4. 用第一人称"你"来称呼日记的作者。
5. 回答要有同理心,让用户感到被关心和理解。
AI 助手的回答:
`;
const response = await model.invoke(prompt);
知识点 9:RAG 的本质
RAG 解决了大模型的两个核心短板:
- 知识截止日期——模型训练数据不包含用户的私人日记
- 幻觉问题——把事实依据(检索到的日记)明确写在 prompt 中,模型只需"总结和润色",大幅降低编造信息的可能
它的标准公式是:
用户问题 → Embedding → 向量搜索 → 相关文档 → 拼入 Prompt → LLM 生成回答
这一流程将"检索"和"生成"解耦为两个独立模块,各司其职——Milvus 负责精确召回相关文档,LLM 负责基于文档生成自然语言回答。
四、核心知识点总结
- 向量数据库 ≠ 关系型数据库:前者解决语义"像什么",后者解决精确"是什么"
- Collection = Table:Milvus 的 Collection 对应 MySQL 中的表概念
- 索引是性能基石:没有索引的向量搜索是 O(n) 暴力匹配;
IVF_FLAT等索引将查询降到毫秒级 MetricType.COSINE:余弦相似度是语义匹配最常用的相似度度量DataType约束字段:VarChar、FloatVector、Array 等类型定义了 Collection 的 Schema- Embedding 模型:将文本转为高维向量的桥梁,语义相近的文本在向量空间中也相近
- 向量化流程:文本
→OpenAIEmbeddings→1024 维浮点数组→存入 Milvus - 语义搜索:query 也向量化后,通过相似度匹配召回 Top-K 结果
- RAG = Retrieve + Generate:检索(Milvus 召回相关文档)+ 增强生成(LLM 基于文档作答),解决知识时效和幻觉问题