走进向量数据库:用 Milvus 构建 AI 日记助手的 RAG 实战

1 阅读7分钟

走进向量数据库:用 Milvus 构建 AI 日记助手的 RAG 实战

一、背景认知:什么是向量数据库?

在传统的 Web 应用开发中,我们把结构化数据存进 MySQL、SQLite、PostgreSQL 这样的关系型数据库,通过 idLIKE 关键词来做增删改查。这种方式擅长处理精确匹配——"找到 id 为 123 的记录"。

但 AI Agent 面临的场景截然不同。Agent 需要存储知识、记忆,然后通过语义来检索——"最近有哪些让我快乐的日记?""哪些内容跟户外活动相关?"这类需求,靠 SQL 的 LIKE 是无法解决的。

这就是向量数据库登场的原因。它的核心流程是:

文档向量化 → 存入向量数据库 → 查询时把 query 也向量化 → 在库中做相似度匹配 → 查出相关文档

Milvus 是目前最流行的开源向量数据库之一,专为海量高维向量数据的存储和检索而设计(Zilliz 则是基于 Milvus 的全托管云服务)。本文通过一个「AI 日记本」项目,带你从零体验 Milvus 的核心操作。


二、项目整体架构

项目包含四个核心文件,层层递进:

文件职责
main.mjs最简 demo:连接→建集合→建索引→插入→搜索
index.mjs进阶:自定义 Schema、批量向量化日记并入库
query.mjs语义查询:用自然语言搜日记
rag.mjsRAG 闭环:检索 + 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_typemax_capacitymax_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 解决了大模型的两个核心短板:

  1. 知识截止日期——模型训练数据不包含用户的私人日记
  2. 幻觉问题——把事实依据(检索到的日记)明确写在 prompt 中,模型只需"总结和润色",大幅降低编造信息的可能

它的标准公式是:

用户问题 → Embedding → 向量搜索 → 相关文档 → 拼入 Prompt → LLM 生成回答

这一流程将"检索"和"生成"解耦为两个独立模块,各司其职——Milvus 负责精确召回相关文档,LLM 负责基于文档生成自然语言回答。


四、核心知识点总结

  1. 向量数据库 ≠ 关系型数据库:前者解决语义"像什么",后者解决精确"是什么"
  2. Collection = Table:Milvus 的 Collection 对应 MySQL 中的表概念
  3. 索引是性能基石:没有索引的向量搜索是 O(n) 暴力匹配;IVF_FLAT 等索引将查询降到毫秒级
  4. MetricType.COSINE:余弦相似度是语义匹配最常用的相似度度量
  5. DataType 约束字段:VarChar、FloatVector、Array 等类型定义了 Collection 的 Schema
  6. Embedding 模型:将文本转为高维向量的桥梁,语义相近的文本在向量空间中也相近
  7. 向量化流程:文本  OpenAIEmbeddings  1024 维浮点数组  存入 Milvus
  8. 语义搜索:query 也向量化后,通过相似度匹配召回 Top-K 结果
  9. RAG = Retrieve + Generate:检索(Milvus 召回相关文档)+ 增强生成(LLM 基于文档作答),解决知识时效和幻觉问题