从零搭建 AI 日记本:用 Milvus 向量数据库实现 RAG 语义搜索

5 阅读10分钟

从 MySQL CRUD 到向量语义搜索,手把手带你用 Milvus + OpenAI Embedding + LangChain 打造一个懂你心情的 AI 日记助手。


一、为什么需要向量数据库?

1.1 传统数据库能做什么

做 Web 开发的同学习惯了这样的模式:

-- 精确查找
SELECT * FROM diary WHERE id = 1;

-- 关键词模糊匹配
SELECT * FROM diary WHERE content LIKE '%开心%';

-- 结构化过滤
SELECT * FROM diary WHERE mood = 'happy' AND date > '2026-01-01';

MySQL / PostgreSQL / SQLite 的本质能力:基于 id 或关键词的精确匹配。数据以行和列的形式组织,增删改查(CRUD)都围绕结构化数据展开。

1.2 AI 应用需要什么

但 AI Agent 产品面临的是另一种需求:

  • "帮我找找最近心情比较好的日记"
  • "上周有什么让我印象深刻的事?"
  • "那些关于户外活动的记录"

这些查询的共同特点:没有精确的 id,没有确定的关键词,搜索的是「语义」而非字符串

你无法用 WHERE content LIKE '%心情好%' 找到"今天阳光明媚,整个人都很舒畅"——它们字面上完全不同,但表达的情绪一致。

1.3 向量数据库登场

这就是 Milvus 存在的意义。

Milvus 是一款开源的向量数据库,专为处理海量高维向量数据而设计。几乎所有 AI Agent 产品背后都有类似的 Vector Store。

它的核心能力:语义相似度搜索。把文本变成数学向量,然后在高维空间里找"意思最接近的"。


二、核心概念图谱

2.1 关系型 vs 向量型对比

概念MySQLMilvus
TableCollection(集合)
列定义Column(INT, VARCHAR...)Field(字段)
一行数据RowEntity(实体)
主键PRIMARY KEYis_primary_key: true
索引B+ Tree 索引IVF_FLAT 聚簇索引
查询方式WHERE id = 1语义相似度搜索
相似度计算Metric(COSINE / L2 / IP)

关键区别:Milvus 的 Field 类型中多了一个 FloatVector——浮点数向量数组,这是向量数据库的灵魂。

2.2 什么是 Field?

Field 就是 Collection 中的字段定义,相当于建表时写的列:

fields: [
  { name: 'id',      data_type: 'Int64',    is_primary_key: true, autoID: true },
  { name: 'vector',  data_type: 'FloatVector', dim: 1024 },
  { name: 'content', data_type: 'VarChar',    max_length: 65535 },
  { name: 'date',    data_type: 'VarChar',    max_length: 50 },
  { name: 'mood',    data_type: 'VarChar',    max_length: 50 },
  { name: 'tags',    data_type: 'Array',      element_type: 'VarChar', max_capacity: 10 },
]

每个 Field 对应日记的一个属性。其中只有 vector 字段被索引加速,其他标量字段用于存储和过滤。

2.3 维度(Dimension)是什么?

维度 = 向量的长度,即用多少个数字来表达一条文字的含义。

const VECTOR_DIM = 1024;  // OpenAI text-embedding-3-large 输出的向量维度

// 一条日记被表达为:
"今天心情很好"OpenAIEmbedding
[0.023, -0.451, 0.892, 0.114, ..., -0.337]  // 1024 个浮点数

不同的 Embedding 模型输出不同维度:

模型维度是否支持自定义维度
OpenAI text-embedding-3-large256/512/1024/3072✅ 支持 dimensions 参数
OpenAI text-embedding-3-small512/1536
DashScope text-embedding-v21536(固定)❌ 不支持

维度在创建 Collection 时写死,之后无法修改,所以开始前要确认好 Embedding 模型的输出维度。

2.4 Metric(相似度度量)

Metric 决定了如何计算两个向量的距离,即"怎么判断像不像":

Metric 类型含义判断标准
COSINE余弦相似度越接近 1 越像
L2欧氏距离越接近 0 越像
IP内积越像

通俗理解:Metric 就是一把"比大小的尺子",你选了哪把尺子,决定了 Milvus 怎么给搜索结果打分。

2.5 聚簇索引(IVF_FLAT)

没有索引时,每次查询都要把库里的向量和查询向量逐一算相似度——这就是 O(n) 的暴力检索,数据量大了慢得没法用。

比喻:去图书馆找《三体》

  • 没索引:100 万本书一本一本翻
  • 有索引:先走到"文学馆/小说/科幻",只在那个分区找,毫秒级定位

IVF_FLAT 的原理:

  1. 先把所有向量通过 K-means 聚类分成 N 个簇
  2. 查询时先定位到最近的几个簇
  3. 只在那些簇内逐一比对

索引只对 vector 字段生效,其他标量字段不走聚簇索引。


三、AI 日记本架构设计

3.1 核心理念:MySQL + Milvus 双存储

┌──────────────────────────────────────┐
│             AI 日记本                  │
├─────────────────┬────────────────────┤
│   MySQL 层       │   Milvus 层         │
│  (非 AI 功能)    │  (AI 功能)         │
├─────────────────┼────────────────────┤
│ · 日记 CRUD      │ · 语义向量存储       │
│ · 用户管理        │ · 语义相似度搜索     │
│ · 结构化字段      │ · RAG 上下文检索     │
│  (id, 日期, 心情) │  (自然语言→相关日记)  │
└─────────────────┴────────────────────┘
  • 结构化数据走 MySQL:精确查询、事务、用户管理
  • 语义数据走 Milvus:自然语言搜索、相似度匹配、AI 记忆

3.2 数据流转

写入:                   查询:
文字日记                   "最近心情好的日记"
  │                         │
  │ OpenAI Embedding         │ OpenAI Embedding
  ▼                         ▼
1024 维向量                1024 维向量
  │                         │
  │ client.insert()          │ client.search()
  ▼                         ▼
┌──── Milvus ────┐       ┌──── Milvus ────┐
│ id              │       │ COSINE 打分     │
│ content         │       │ 返回 top-K      │
│ vector ← 索引加速 │       │ + content/mood  │
│ mood            │       └────────────────┘
│ tags            │              │
└─────────────────┘              ▼
                          拼入 Prompt → LLM 生成回答

四、实战:从测试到 RAG 的演进

4.1 第一阶段:Hello Milvus(main.mjs)

先连通 Zilliz Cloud,验证基本操作:

import { MilvusClient, IndexType, MetricType } from '@zilliz/milvus2-sdk-node'

const client = new MilvusClient({
  address: process.env.MILVUS_ADDRESS,
  token: process.env.MILVUS_TOKEN
})

// ⚠️ 企业级实践:实例化后必须握手验证
const health = await client.checkHealth()
if (!health.isHealthy) {
  console.error('连接失败', health.reasons)
  return
}

关键认知new MilvusClient() 只是创建了一个 JS 对象,真正的 TCP/gRPC 连接(握手)发生在第一次 API 调用时。企业级代码必须显式做健康检查,不能 console.log('已连接') 自欺欺人。

用测试数据快速验证:

// 插入
const data = [
  { vector: [0.1, 0.2, 0.3, 0.4], content: '这是第一条数据' },
  { vector: [0.5, 0.6, 0.7, 0.8], content: '这是第二条数据' },
]
await client.insert({ collection_name: 'test', data })

// 搜索
const result = await client.search({
  collection_name: 'test',
  data: [[0.1, 0.1, 0.3, 0.4]],  // 查询向量
  limit: 2,
  output_fields: ['content']
})
// 返回的每条结果自带 score,Milvus 自动完成相似度计算

4.2 第二阶段:真正的 Embedding(index.mjs)

手写 4 维测试向量没有意义——我们需要让 OpenAI 模型生成真正带语义的 1024 维向量:

import { OpenAIEmbeddings } from '@langchain/openai'

const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: 'text-embedding-3-large',   // ← 支持自定义维度
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
  dimensions: 1024                    // ← 指定输出 1024 维
})

const getEmbedding = async (text) => {
  return await embeddings.embedQuery(text)
}

然后用真实日记做批量插入:

const diaryContents = [
  {
    id: 'diary_001',
    content: '今天天气很好,去公园散步了,心情愉快。看到了很多花开了,春天真美好。',
    date: '2026-01-10', mood: 'happy', tags: ['生活', '散步']
  },
  // ... 更多日记
]

// 批量生成 Embedding 并插入
const diaryData = await Promise.all(
  diaryContents.map(async (diary) => ({
    ...diary,
    vector: await getEmbedding(diary.content)  // 每条日记转成 1024 维向量
  }))
)
await client.insert({ collection_name: 'ai_diary', data: diaryData })

写入和查询是对称的

写入:文字 → Embedding → 存向量 → Milvus
查询:问题 → Embedding → 搜向量 → 返回原文

4.3 第三阶段:语义搜索(query.mjs)

有了真实的向量数据,就可以做语义搜索了:

const query = '我想看看关于户外活动的日记'
const queryVector = await getEmbedding(query)   // 问题 → 1024维向量

const searchResult = await client.search({
  collection_name: 'ai_diary',
  vector: queryVector,
  limit: 2,
  metric_type: MetricType.COSINE,
  output_fields: ['id', 'content', 'date', 'mood', 'tags']
})

// 结果自带 score,Milvus 自动按相似度排序
searchResult.results.forEach((item, i) => {
  console.log(`日记${i + 1} 相似度: ${item.score.toFixed(4)}`)
  console.log(`内容: ${item.content}`)
})

query.mjs 做的是 RA(检索):给你最相关的原始日记,你自己看、自己总结。

4.4 第四阶段:完整 RAG(rag.mjs)

真正的 AI 助手不能只是扔给你一堆原始数据——它应该理解、总结、共情

async function answerQuestion(question) {
  // R: Retrieve — 检索相关日记
  const retrievedDiaries = await retrieveRelevantDiaries(question)

  // A: Augment — 将检索结果拼入 Prompt
  const context = retrievedDiaries.map((d, i) => `
    [日记 ${i + 1}]
    日期:${d.date} | 心情:${d.mood} | 标签:${d.tags}
    内容:${d.content}
  `).join('\n')

  const prompt = `你是温暖的 AI 日记助手。参考以下日记回答问题:
  ${context}

  用户问题:${question}

  要求:结合日记给出有同理心的回答,用"你"称呼作者。`

  // G: Generate — LLM 生成自然语言回答
  const response = await model.invoke(prompt)
  return response.content
}

query.mjs vs rag.mjs 的本质区别

query.mjs → 给你一堆原材料(日记片段 + 分数),自己消化
rag.mjs   → 给你一盘做好的菜(LLM 理解、总结、共情后的回答)

同一个问题的输出对比

query.mjs: Result 1.[Score: 0.92] 日记_005: 晚上做了一顿丰盛的晚餐... Result 2.[Score: 0.87] 日记_002: 今天工作很忙,完成了重要项目...

rag.mjs: "你最近有两件特别值得骄傲的事!1月13日你在厨房大显身手,做了一顿丰盛的晚餐,家人都说很好吃。工作上也很出色,1月11日完成了一个重要的项目里程碑。无论家庭还是工作,你都在用心经营,真的很厉害!"


五、完整的 RAG 文件结构

rag.mjs
═══════════════════════════════════════

第1步:导入依赖                          第 1-11 行
  MilvusClient, MetricType, IndexType, OpenAIEmbeddings, ChatOpenAI

第2步:配置三大组件                        第12-44 行
  ├── 环境变量                           第13-17 行
  ├── OpenAIEmbeddings 初始化             第19-26 行
  ├── ChatOpenAI(LLM)初始化             第28-35 行
  ├── MilvusClient 初始化                 第37-40 行
  └── getEmbedding() 封装                第41-44 行

第3步:检索模块(R)                      第46-61retrieveRelevantDiaries(question)
  ├── 问题 → Embedding → 查询向量         第48 行
  ├── client.search() 语义搜索            第49-55 行
  └── 返回最相关日记                      第56 行

第4步:生成模块(A + G)                  第63-111answerQuestion(question)
  ├── 打印分隔线('='.repeat(80))        第65-67 行
  ├── 调用检索模块                        第70 行
  ├── 打印检索结果 + 相似度                第75-79 行
  ├── 拼 context(日记→结构化文本)        第81-88 行
  ├── 构造 System Prompt + User Prompt    第90-103 行
  └── model.invoke(prompt) → 生成回答     第105-106 行

第5步:入口 main()                       第112-123 行
  └── answerQuestion('我最近做了什么让我很骄傲的事')

调用链main() → answerQuestion() → retrieveRelevantDiaries() → getEmbedding()

三个函数各司其职,层层调用,是典型的模块化 RAG 编排。


六、避坑指南(实战踩坑记录)

6.1 Collection 的 fields 必须显式定义

// ❌ 简写方式,只传 dimension,容易出问题
await client.createCollection({ collection_name: 'test', dimension: 768, autoID: true })

// ✅ 显式定义每个 field
await client.createCollection({
  collection_name: 'test',
  fields: [
    { name: 'id', data_type: 'Int64', is_primary_key: true, autoID: true },
    { name: 'vector', data_type: 'FloatVector', dim: 768 },
    { name: 'content', data_type: 'VarChar', max_length: 65535 },
  ]
})

autoID: true 只在 Int64 类型的字段上生效,VarChar 主键必须手动提供 id。

6.2 先建索引再加载 Collection

// ✅ 正确顺序
await client.createCollection({...})         // 1. 建表
await client.createIndex({...})              // 2. 建索引
await client.loadCollection({...})           // 3. 加载到内存

// ❌ 先加载再建索引会报 IndexNotExist 错误

6.3 维度必须匹配

Collection 创建时定了维度,插入的向量必须长度一致。换 Embedding 模型 → 维度变了 → 必须删旧 Collection 重建。

// 安全做法:重建前先删
const has = await client.hasCollection({ collection_name: 'ai_diary' })
if (has.value) {
  await client.dropCollection({ collection_name: 'ai_diary' })
}

6.4 pnpm 构建脚本权限

Milvus SDK 依赖 protobufjs,pnpm 默认拦截其构建脚本。在 pnpm-workspace.yaml 中放开:

allowBuilds:
  protobufjs: true

6.5 包名的坑

// ❌ 容易写错
import { MilvusClient } from '@zilliz/milvus-sdk-node'    // 旧版
import { MilvusClient } from '@zilliz/milvus2-node-sdk'   // 拼写错误

// ✅ 正确
import { MilvusClient } from '@zilliz/milvus2-sdk-node'   // v2 新版

6.6 Embedding 模型的 dimensions 参数

不是所有模型都支持自定义维度。OpenAI text-embedding-3-large 支持,DashScope text-embedding-v2 不支持——传了 dimensions 参数会导致 404 MODEL_NOT_FOUND

6.7 URL 拼写

dashscopel.aliyuncs.com(多了个 l)→ DNS 解析失败,请求卡死超时。


七、总结

核心认知

  1. 向量数据库不是替代 MySQL,是补充。结构化查询走关系型,语义搜索走向量型,各司其职。

  2. RAG 的本质是「检索 + 增强 + 生成」:先去向量库找到相关文档,拼进 Prompt 给 LLM,让 LLM 基于真实数据回答——而不是凭空编造。

  3. 企业级实践new 完客户端必须握手验证(checkHealth),不能假装连上了;先建索引再加载 Collection;维度在创建时锁定;显式定义 fields 比简写更可靠。

四个文件的演进

main.mjs    →  Hello World,手写测试向量验证连通性
index.mjs   →  真实 Embedding,批量插入日记数据
query.mjs   →  语义搜索,返回原始结果
rag.mjs     →  完整 RAG,LLM 生成自然语言回答 ✅ 最终形态

从「连接数据库 → 存向量 → 搜向量 → 拼 Prompt → LLM 回答」,每一步都是 AI 应用的基础能力。掌握了这个流水线,做知识库问答、智能客服、AI 记忆系统都能复用这套模式。


代码仓库:文中的全部代码都可以直接运行,只需要配好 .env 中的 Milvus 和 OpenAI API Key 即可。祝你从 CRUD 走向 AI 开发的第一步顺利!