从零搭建 AI 日记助手:用 Milvus 向量数据库 + RAG 让机器读懂你的每一天

从零搭建 AI 日记助手:用 Milvus 向量数据库 + RAG 让机器读懂你的每一天


写在前面

如果你正在构建一个 AI Agent,你大概率绕不开一个基础设施------向量数据库 。它不是替代 MySQL 的存在,而是补上了传统数据库最不擅长的一环:语义理解

本文将以一个完整的实战项目------AI 日记助手------为主线,带你从零搭建一套基于 Milvus 向量数据库的 RAG(检索增强生成)系统。我们会写四个 JS 文件,逐步完成:连接向量数据库 → 创建集合 → 向量化并存储日记 → 语义搜索 → 大模型生成回答。

项目使用 Zilliz Cloud(基于 Milvus 的全托管服务)+ 阿里云 DashScope(兼容 OpenAI 接口)作为 Embedding 和 LLM 后端。


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

1.1 传统数据库的边界

日常的 Web 开发中,我们把数据存在 MySQL、PostgreSQL 或者 SQLite 里,做的事情无非是 增删改查(CRUD) ------根据 ID 精确查找、根据关键词 LIKE 模糊匹配、关联查询出列表数据。这套模式处理结构化数据非常成熟。

但它有一个根本性的局限:它不懂语义。

举个具体的例子。你的日记本里有这么一条:

"周末和朋友去爬山,天气很好,心情也很放松。享受大自然的感觉真好。"

如果你想查"关于户外活动的日记",SQL 能帮你做什么?用 LIKE '%户外%' 去匹配?这条日记里根本没出现"户外"这个词,SQL 会直接返回空结果。

这就是关键词匹配的天花板------它只能匹配字面,无法理解含义。

1.2 向量数据库解决了什么

向量数据库的核心思路完全不同:

css 复制代码
文本 → Embedding模型 → 高维向量(一串浮点数)→ 存入向量数据库

查询时:
查询文本 → Embedding模型 → 查询向量 → 在数据库中做相似度计算 → 返回最相似的Top-K结果

"爬山"和"户外活动"在向量空间里的距离很近 ------虽然字面上完全不同,但语义上高度相关。这就是向量数据库最核心的能力:语义检索

css 复制代码
flowchart LR
    A["📝 原始文本<br/>日记内容"] --> B["🧠 Embedding 模型<br/>文本 → 向量"]
    B --> C["🗄️ Milvus 向量数据库<br/>存储 + 索引"]
    
    D["❓ 用户查询<br/>'户外活动'"] --> E["🧠 同一个 Embedding 模型"]
    E --> F["🔍 相似度搜索<br/>COSINE / IP / L2"]
    F --> G["📋 返回 Top-K 结果"]

1.3 代码中的体现

在项目里,我们封装了一个 getEmbedding 函数,它就叫一次 API,把任意文本变成一串 1024 维的浮点数向量:

javascript 复制代码
// index.mjs / query.mjs / rag.mjs 中均有此函数
const getEmbedding = async (text) => {
  const result = await embeddings.embedQuery(text);
  return result;  // 返回 number[],长度 = VECTOR_DIM (1024)
}

这一行代码的背后是一次网络请求到 Embedding 服务(我们用的是阿里云 DashScope 的 text-embedding-v3 模型),把自然语言文本映射为一个高维空间中的点。


二、Milvus 是什么

2.1 定位

readme 中有一段很精炼的定义:

Milvus 是一款开源的向量数据库,专为处理海量高维向量数据而设计。AI Agent 产品都会使用 Milvus 这样的 Vector Store。

这里有三个关键词值得展开:

关键词 含义
向量数据库 专门为向量数据的存储和检索而优化的数据库系统,不同于传统的关系型数据库
海量高维 单条向量可达数百到数千维,数据量可达数十亿级别
Vector Store Agent 架构中的记忆和知识存储层,Agent 把"记忆"放在 Milvus 中做语义检索

2.2 C/S 架构

Milvus 采用经典的 客户端/服务端(C/S)架构,在代码注释中有所标注:

javascript 复制代码
// index.mjs
import {
  MilvusClient,  // C/S 架构     B/S 架构
  MetricType,    // 相似度求法
  IndexType,
  DataType       // 字段数据类型约束
} from '@zilliz/milvus2-sdk-node';
  • 服务端(Server) :Milvus 数据库实例,负责存储数据、构建索引、执行查询
  • 客户端(Client) :通过 SDK 连接服务端,发指令操作数据

这和 MySQL 的 mysql2 驱动、Redis 的 ioredis 是同一个模式。MilvusClient 就是你操作向量数据库的入口对象。

2.3 Zilliz Cloud

基于 Milvus 的全托管向量数据库服务。

如果不想自己搭 Milvus 服务端,Zilliz Cloud 提供了开箱即用的云服务(就像 RDS 之于 MySQL)。我们项目中使用的就是 Zilliz Cloud 的 Serverless 实例,地址配置在 .env 中:

ini 复制代码
MILVUS_ADDRESS=https://in03-5d01b4cb0f9332d.serverless.ali-cn-hangzhou.cloud.zilliz.com.cn
MILVUS_TOKEN=your-auth-token

三、环境准备与连接

3.1 依赖

项目使用三个核心依赖:

用途
@zilliz/milvus2-sdk-node Milvus Node.js SDK(v3.0.3),用于操作向量数据库
@langchain/openai LangChain 的 OpenAI 兼容封装,提供 OpenAIEmbeddingsChatOpenAI
dotenv 加载 .env 环境变量

3.2 Embedding 模型初始化

arduino 复制代码
// index.mjs / query.mjs / rag.mjs
const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,         // API 密钥
  model: process.env.EMBEDDINGS_MODEL_NAME,    // 模型名称:text-embedding-v3
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL       // 自定义 API 端点
  },
  dimensions: VECTOR_DIM                        // 输出向量维度:1024
});

configuration.baseURL 是一个值得留意的配置点。OpenAI 官方的 API 地址是 https://api.openai.com/v1,但当你使用国内的大模型服务(如 DeepSeek、通义千问、智谱等)时,它们的接口是 OpenAI 兼容的------协议一样,地址不同 。你只需要把 baseURL 改成对应的地址就能无缝切换,不需要修改任何业务代码。这是一种协议层面的抽象:OpenAI 定义了 HTTP API 的事实标准,各家厂商通过实现同一套接口来降低开发者的迁移成本。

这里我们用阿里云 DashScope:

ini 复制代码
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
EMBEDDINGS_MODEL_NAME=text-embedding-v3

3.3 MilvusClient 初始化与连接

arduino 复制代码
// index.mjs / query.mjs / rag.mjs
const client = new MilvusClient({
  address: ADDRESS,   // Milvus 服务地址
  token: TOKEN        // 认证 token(Zilliz Cloud 需要)
})

我们的代码注释中写了 // SDK v3 自动连接,不需要 connectPromise()。这是因为 Milvus Node.js SDK v3 在构造函数中会自动调用 connect() 方法建立 gRPC 连接,所有后续的 API 调用(checkHealthcreateCollectioninsertsearch 等)内部都会 await this.connectPromise 确保连接就绪后再执行。

javascript 复制代码
// main.mjs / index.mjs
const checkHealth = await client.checkHealth();
if (!checkHealth.isHealthy) {
  console.error('连接失败', checkHealth.reasons);
  return;
}
console.log('连接成功,集群状态正常');

checkHealth() 不仅验证网络是否通,还会检查 Milvus 集群各组件的运行状态。在生产环境中,这是一个很好的启动检查(startup probe)实践。


四、索引:为什么能让向量搜索做到毫秒级

4.1 问题:暴力搜索的 O(n) 困境

main.mjs 中有一段很长的注释,用了一个非常好的类比来解释索引的必要性:

arduino 复制代码
// Milvus 存的是高维向量,
// 没有索引时,每次查询都要把库里的向量和查询的向量逐一算相似度(O(n))
// 数据量大了慢的没法用
// 字典  拼音,偏旁的索引, 迅速减少查询范围
// 图书馆,找一本《三体》 没有索引,每本都要翻一下。
// 文学馆/小说/科幻
//  IVF_FLAT 聚簇索引 毫秒级
IndexType,

这段注释的信息量很大,逐层拆解:

第一层:为什么需要索引。 假设你存了 1000 万条向量,每次查询都要和这 1000 万条逐一计算相似度------时间复杂度 O(n),这在生产环境下是不可接受的。索引的作用就是避免全量扫描

第二层:查字典的类比。 你在新华字典里查一个字,不会从第一页翻到最后一页。你会根据拼音索引先定位到声母区,再定位到韵母区,最后精准翻到目标页------三次跳转就把范围从几万缩小到 1。向量索引的底层思想和这个完全一致:通过空间划分,把搜索范围从全库缩小到少数几个区域。

第三层:图书馆的类比。 没有索引的图书馆 → 你找《三体》要翻遍每一本书。有索引的图书馆 → 文学馆/小说/科幻 → 三秒定位。这就是 IVF_FLAT 要做的事------把向量空间划分为多个聚类(cluster) ,查询时先找到最近的几个聚类,再只在那些聚类内部做精确比较。

4.2 代码中的索引创建

csharp 复制代码
// index.mjs
await client.createIndex({
  collection_name: COLLECTION_NAME,
  field_name: 'vector',             // 对 vector 字段建索引
  index_type: IndexType.IVF_FLAT,   // 倒排文件索引
  metric_type: MetricType.COSINE    // 余弦相似度
})

IVF_FLAT(Inverted File with Flat compression)是最常用的向量索引类型之一:

scss 复制代码
IVF_FLAT 工作原理:

训练阶段:用 K-Means 将所有向量聚成 N 个簇
  [簇1] [簇2] ... [簇N]

查询阶段:
  1. 计算查询向量到 N 个簇中心距离 → 选最近的 M 个簇
  2. 只在这 M 个簇内部做精确比对
  3. 返回 Top-K

复杂度:O(N + M),其中 M << 总向量数

4.3 相似度算法

代码中用了 MetricType.COSINE(余弦相似度),注释中也标注了 // 相似度求法

arduino 复制代码
MetricType, // 相似度求方法

常见的三种向量相似度计算方法:

度量 公式直觉 适用场景
COSINE(余弦) 看两个向量的"夹角",夹角越小越相似 文本语义相似度(推荐)
IP(内积) 看两个向量的"投影长度",值越大越相似 需要向量已归一化的场景
L2(欧氏距离) 看两个点在空间中的"直线距离",越近越相似 图像特征匹配

对于文本 Embedding,余弦相似度是最常用的选择,因为它只关心方向,不关心向量的绝对长度------"爬山"和"登山"的向量方向应该很接近,哪怕它们各自的向量长度不同。


五、Collection:向量数据库中的"表"

5.1 Schema 设计

传统关系型数据库中,你要先 CREATE TABLE 定义好每一列的类型和约束,然后才能插入数据。Milvus 中的 Collection 就是类比 MySQL Table 的存在。在 index.mjs 中,我们用显式的 Schema 定义了日记 Collection 的字段结构:

ini 复制代码
const COLLECTION_NAME = 'ai_diary';
const VECTOR_DIM = 1024;  // 向量维度,由 Embedding 模型的输出决定
php 复制代码
await client.createCollection({
  collection_name: COLLECTION_NAME,
  fields: [
    // diary_01
    {
      name: 'id',
      data_type: DataType.VarChar,   // 字符串类型
      max_length: 50,                 // 最大长度限制
      is_primary_key: true,           // 设为主键
      description: '日记唯一标识'
    },
    {
      name: 'vector',
      data_type: DataType.FloatVector, // 浮点向量类型
      dim: VECTOR_DIM,                  // 向量维度 1024
      description: '日记内容的向量表示'
    },
    {
      name: 'content',
      data_type: DataType.VarChar,
      max_length: 5000,                // 日记正文,允许较长的字符串
      description: '日记正文内容'
    },
    {
      name: 'date',
      data_type: DataType.VarChar,
      max_length: 50,
      description: '日记日期'
    },
    {
      name: 'mood',
      data_type: DataType.VarChar,
      max_length: 50,
      description: '心情标签'
    },
    {
      name: 'tags',
      data_type: DataType.Array,        // 数组类型!
      element_type: DataType.VarChar,   // 数组中每个元素的类型
      max_capacity: 10,                 // 数组最大容量
      max_length: 50,                   // 每个元素的最大长度
      description: '日记标签列表'
    },
  ]
});

5.2 字段类型解读

注意到这里出现了 // 字段数据类型约束 注释对应的 DataType 导入。Schema 中定义了 6 个字段,各自承载不同的职责:

字段 类型 作用 类比 MySQL
id VarChar(50),主键 日记唯一标识,如 diary_001 VARCHAR(50) PRIMARY KEY
vector FloatVector(1024) 日记内容的 Embedding 向量 MySQL 没有等价类型
content VarChar(5000) 日记正文内容 TEXT
date VarChar(50) 日记日期 VARCHAR(50)
mood VarChar(50) 心情标签 VARCHAR(50)
tags Array 标签数组 MySQL 8+ 的 JSON 数组

最有趣的设计是 tags 字段。传统 SQL 中,一对多的标签关系通常需要建一张关联表。在 Milvus 中,DataType.Array 允许直接在一条记录里存储标签列表(max_capacity: 10 限制了最多 10 个标签),这使得查询时可以直接拿到完整数据,不需要 JOIN。当然,这也意味着标签的过滤(如"找出所有包含'工作'标签的日记")需要通过标量过滤(Scalar Filtering)来完成,而非传统 SQL 的 WHERE 子查询。

5.3 幂等创建

csharp 复制代码
// 检查并创建 Collection
const hasCollection = await client.hasCollection({ collection_name: COLLECTION_NAME });
if (!hasCollection.value) {
  // 创建 Collection 和 Index ...
} // end if !hasCollection

hasCollection 在 SDK v3 中返回一个包含 .value 属性的对象,而不是直接返回布尔值------这是 v3 API 的一个细节变更。用这个判断可以实现幂等执行:第一次运行时创建 Collection,后续重复运行跳过创建直接插入数据。

5.4 加载到内存

javascript 复制代码
console.log('loading collection');
await client.loadCollection({
  collection_name: COLLECTION_NAME
});
console.log('collection loaded')

Collection 创建后处于 未加载(unloaded) 状态------数据在磁盘上,但搜索需要数据在内存中。loadCollection 把 Collection 的索引和数据加载到内存,之后才能执行搜索操作。这是一个关键的性能设计:你可以有几百个 Collection,但只加载需要查询的那几个,节省内存开销。


六、日记数据向量化与插入

6.1 示例数据

less 复制代码
const diaryContents = [  {    id: 'diary_001',    content: '今天天气很好,去公园散步了,心情愉快。看到了很多花开了,春天真美好。',    date: '2026-01-10',    mood: 'happy',    tags: ['生活', '散步']
  },
  {
    id: 'diary_002',
    content: '今天工作很忙,完成了一个重要的项目里程碑。团队合作很愉快,感觉很有成就感。',
    date: '2026-01-11',
    mood: 'excited',
    tags: ['工作', '成就']
  },
  {
    id: 'diary_003',
    content: '周末和朋友去爬山,天气很好,心情也很放松。享受大自然的感觉真好。',
    date: '2026-01-12',
    mood: 'relaxed',
    tags: ['户外', '朋友']
  },
  {
    id: 'diary_004',
    content: '今天学习了 Milvus 向量数据库,感觉很有意思。向量搜索技术真的很强大。',
    date: '2026-01-12',
    mood: 'curious',
    tags: ['学习', '技术']
  },
  {
    id: 'diary_005',
    content: '晚上做了一顿丰盛的晚餐,尝试了新菜谱。家人都说很好吃,很有成就感。',
    date: '2026-01-13',
    mood: 'proud',
    tags: ['美食', '家庭']
  }
];

5 条模拟日记,覆盖了生活、工作、户外、学习、美食五个主题,心情从 happy 到 proud,为后面的语义搜索提供了足够的多样性。

6.2 批量向量化

javascript 复制代码
console.log('Generating embeddings...');
const diaryData = await Promise.all(
  diaryContents.map(async (diary) => ({
    ...diary,
    vector: await getEmbedding(diary.content)
  }))
);

Promise.all + async map 并发调用 Embedding API,5 条日记并行生成向量,比串行快数倍。生成后的 diaryData 中每条记录多了 vector 字段------一个 1024 维的 number[]

思考一个问题:为什么这里必须用 Promise.all 而不是 forEach

forEach 的回调是同步执行的,它不会等待内部的 await。如果你的 Embedding 接口限流是 10 QPS,5 条并发请求可能在几十毫秒内全部发出,瞬间触发限流。实际项目中可能要改用 p-limit 或其他并发控制方案来控制并发数。

6.3 插入数据

javascript 复制代码
const insertResult = await client.insert({
  collection_name: COLLECTION_NAME,
  data: diaryData      // 太简单了 json 不用写 sql
})
console.log(insertResult.insert_cnt, "条记录成功插入。");

数据以 JSON 对象数组 的形式直接传入------无需像 SQL 那样拼 INSERT INTO ... VALUES ...。Milvus 会自动校验每条记录的字段是否与 Schema 匹配,字段名不一致会直接报错。


七、语义搜索

7.1 query.mjs 中的搜索实现

javascript 复制代码
const query = '我想看看关于户外活动的日记';
console.log(`Query: ${query}`);
const queryVector = await getEmbedding(query);
const searchResult = await client.search({
  collection_name: COLLECTION_NAME,
  vector: queryVector,              // 查询向量
  limit: 2,                          // 返回 Top-2
  metric_type: MetricType.COSINE,    // 余弦相似度
  output_fields: ['id', 'content', 'date', 'mood', 'tags']  // 返回的标量字段
});

这就是完整的语义搜索流程:

scss 复制代码
"户外活动" 
  → Embedding 模型 → [0.023, -0.451, ..., 0.337] (1024维向量)
  → Milvus 搜索 (COSINE) 
  → 返回相似度最高的 2 条日记

7.2 main.mjs 中的另一种搜索方式

main.mjs 里演示了一种不同的搜索 API 调用方式------使用 data 参数直接传入多个向量:

javascript 复制代码
const searchRes = await client.search({
    collection_name: COLLECTION_NAME,
    data: [[0.5, 0.5, 0.6, 0.8]],   // 直接传向量数组
    limit: 2,
    output_fields: ['content']        // 只返回 content 字段
})
console.log(JSON.stringify(searchRes.results, null, 2));

注意这里是 data(复数向量)而不是 vector(单数向量)。这是 SDK 提供的批量搜索能力------一次请求可以对多个查询向量同时做搜索,减少网络往返次数。

7.3 搜索结果解析

javascript 复制代码
console.log(`Found ${searchResult.results.length} results.`);
searchResult.results.forEach((item, index) => {
  console.log(`Result ${index + 1}.[Score: ${item.score.toFixed(4)}]`);
  console.log(`
      ID: ${item.id}
      Content: ${item.content}
      Date: ${item.date}
      Mood: ${item.mood}
      Tags: ${item.tags?.join(",")}
  `)
});

搜索返回 query: '我想看看关于户外活动的日记' 的结果是------diary_003(爬山日记),score 约 0.66:

yaml 复制代码
Result 1.[Score: 0.6575]
  ID: diary_003
  Content: 周末和朋友去爬山,天气很好...
  Mood: relaxed
  Tags: 户外,朋友

COSINE 相似度范围是 -1, 1,0.66 表示两个向量在方向上高度一致。核心洞察:查询词"户外活动"和这篇日记的内容"爬山"是语义相关的,尽管它们没有任何共同词汇。 这就是向量搜索区别于关键词搜索的根本之处------它不匹配字面,它匹配含义


八、RAG:串联检索与生成

8.1 什么是 RAG

readme 中有一段对 RAG 流程的精炼总结:

文档向量化放到向量数据库,每次查询根据向量化的 query 去数据库做相似度匹配,查出相关文档放到 prompt 里给大模型,大模型来生成回答。

这四句话精准概括了 RAG 的四个环节:

css 复制代码
flowchart TD
    Q["❓ 用户问题<br/>'我最近做了什么让我很骄傲的事'"] --> E["🧠 Embedding<br/>问题 → 查询向量"]
    E --> S["🔍 Milvus 语义检索<br/>返回 Top-K 相关日记"]
    S --> P["📋 拼接 Prompt<br/>Context + 问题 → 完整提示词"]
    P --> L["🤖 ChatOpenAI / LLM<br/>基于日记内容生成回答"]
    L --> A["✅ 返回温暖、有同理心的回答"]

rag.mjs 中将其实现为了两个模块化函数。

8.2 检索模块

javascript 复制代码
// r a g 模块化
console.log('检索相关日记');
async function retrieveRelevantDiaries(question, k = 2) {
  try {
    const queryVector = await getEmbedding(question);
    const searchResult = await client.search({
      collection_name: COLLECTION_NAME,
      vector: queryVector,
      limit: k,                                     // 返回最相关的 k 条日记
      metric_type: MetricType.COSINE,
      output_fields: ['id', 'content', 'date', 'mood', 'tags']
    });
    return searchResult.results;
  } catch (err) {
    console.error('检索日记时出错:', err.message);
    return [];  // 降级处理:检索失败返回空数组,不影响主流程
  }
}

注意 retrieveRelevantDiaries 的命名------不是简单的 search,而是 retrieve(检索)。在 RAG 的语境中,"检索"的含义比"搜索"更精确:给定一个问题,找到与之相关的文档片段。 这里的 k = 2 是一个重要参数------它决定了注入到 Prompt 中的上下文量。k 过小可能导致信息不足,k 过大会稀释有效信息并增加 token 消耗。

8.3 Context 组装

javascript 复制代码
const context = retrievedDiaries
  .map((diary, i) => `
    [日记 ${i + 1}]
    日期:${diary.date}
    心情:${diary.mood}
    标签:${diary.tags?.join(",")}
    内容:${diary.content}
  `).join('\n')

检索到的日记被格式化为结构化的文本块,每条日记包含日期、心情、标签和正文。这种格式化的目的是帮助 LLM 快速解析信息结构 ------明确的分隔符([日记 N])和字段名(日期:心情:)让模型能准确区分不同日记和不同维度的信息。

8.4 Prompt 工程

markdown 复制代码
const prompt = `你是一个温暖贴心的AI 日记助手。基于用户的日记内容回答问题,
用亲切自然的语言,请根据以下日记内容回答问题:
${context}

用户问题:${question}

回答要求:
1. 如果日记中有相关信息,请结合日记内容给出详细、温暖的回答。
2. 可以总结多篇日记的内容,找出共同点或趋势。
3. 如果日记中没有相关信息,请温和告知用户。
4. 用第一人称"你"来称呼日记的作者。
5. 回答要有同理心,让用户感到被理解和关心。
AI 助手的回答:
`

这个 Prompt 的设计有几个值得注意的细节:

  • 角色设定("温暖贴心的 AI 日记助手"):限定了模型的语气和行为边界
  • Chain-of-Thought 引导("找出共同点或趋势"):引导模型做推理而非简单复述
  • 兜底策略("如果没有相关信息,请温和告知"):防止模型在信息缺失时产生幻觉(hallucination)
  • 人称设定("用'你'来称呼作者"):让回答有对象感和温度
  • 标记结束 ("AI 助手的回答:"):这是关键------给 LLM 一个明确的"轮到你了"的信号,有助于生成更可控的输出

8.5 LLM 调用

arduino 复制代码
const model = new ChatOpenAI({
  temperature: 0.1,                  // 低温度 → 输出更确定、更稳定
  model: process.env.MODEL_NAME,     // qwen-plus
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL  // DashScope 兼容端点
  }
})

const response = await model.invoke(prompt);
console.log(response.content);

temperature: 0.1 接近于 0,意味着模型会倾向于选择概率最高的 token,输出更加稳定可预测。在 RAG 场景中,我们希望模型忠实于检索到的文档内容,而不是自由发挥------所以低温度是正确的选择。

8.6 完整 RAG 的主入口

javascript 复制代码
async function main() {
  try {
    console.log('连接到Milvus ...');
    // SDK v3 自动连接
    console.log('已连接');
    await answerQuestion('我最近做了什么让我很骄傲的事');
  } catch (err) {
    console.error('查询失败:', err.message);
  }
}
main().catch(console.error);

这里提的问题是"我最近做了什么让我很骄傲的事"。 "骄傲"这个词没有出现在任何一篇日记里,但向量搜索能命中内容中带有"成就感"的日记------因为"骄傲"和"成就感"在语义空间中位置接近。这正是 embedding + 向量搜索相对于关键词搜索的核心优势。

实际运行结果命中了 diary_002(项目里程碑、成就感)和 diary_005(家人夸赞、成就感),LLM 据此生成了一段温暖的、总结性的回答。


九、main.mjs:最快的入门路径

main.mjs 是项目中最精简的示例,它用最少的代码演示了 Milvus 的核心操作闭环。让我们完整走一遍:

javascript 复制代码
import {
  MilvusClient,   // client 连接 zilliz server
  IndexType,      // 索引类型
  MetricType      // 相似度的计算类型
} from '@zilliz/milvus2-sdk-node'
import 'dotenv/config'

连接

ini 复制代码
const client = new MilvusClient({
  address: ADDRESS,
  token: TOKEN
});
const checkHealth = await client.checkHealth();

搜索 (使用一个已经创建好的 test Collection):

lua 复制代码
const searchRes = await client.search({
  collection_name: COLLECTION_NAME,         // mysql table 集合
  data: [[0.5, 0.5, 0.6, 0.8]],           // 4维向量 --- 因为创建时 DIMENSION = 4
  limit: 2,
  output_fields: ['content']
})

关于维度(DIMENSION)设置的一个底层细节:main.mjs 中 DIMENSION = 4 是为了演示方便。实际应用中,维度过低会导致向量"表达能力"不足(很多语义信息被压缩丢失),维度过高则占用更多存储和计算资源。Embedding 模型的输出维度(如 1024)是经过大量实验验证的工程最佳平衡点,在语义丰富度和计算开销之间做了折中。


十、.env 配置全景

ini 复制代码
MODEL_NAME=qwen-plus                  # LLM 模型:通义千问增强版
EMBEDDINGS_MODEL_NAME=text-embedding-v3  # Embedding 模型
OPENAI_API_KEY=sk-xxx                 # API 密钥(DashScope)
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1  # 兼容端点
MILVUS_ADDRESS=https://xxx.zilliz.com.cn  # Zilliz Cloud 地址
MILVUS_TOKEN=xxx                      # Zilliz 认证 token

这里涉及两个 AI 服务:

服务 用途 关键配置
阿里云 DashScope Embedding + LLM OPENAI_BASE_URL 指向兼容端点
Zilliz Cloud 向量存储与检索 Milvus 的全托管云服务

使用自定义 baseURL 的架构含义是:你不需要同时接入多个 SDK 。阿里云、DeepSeek、智谱等厂商都提供了 OpenAI 兼容接口,代码中只引入 @langchain/openai 一个包,通过切换 baseURL 就能使用不同厂商的模型------这是一种基于接口统一的供应商解耦方案。


十一、项目文件结构总览

bash 复制代码
demo/
├── .env                    # 环境变量配置
├── package.json            # 依赖管理
└── src/
    ├── main.mjs            # 最简连接 + 搜索示例
    ├── index.mjs           # 完整数据流:连接 → 建表 → 向量化 → 插入
    ├── query.mjs           # 语义搜索查询
    └── rag.mjs             # RAG 完整流程:检索 + 生成

按照 readme.md 的笔记顺序,学习和开发的路径是:

css 复制代码
main.mjs(理解 Milvus 基础连接和搜索)
  → index.mjs(理解 Schema 设计、向量化、数据插入)
    → query.mjs(理解语义搜索的完整链路)
      → rag.mjs(串联检索和 LLM 生成,完成 RAG 闭环)

十二、关键技术决策回顾

决策 选择 理由
向量数据库 Milvus (Zilliz Cloud) 开源、社区活跃、云原生架构
Embedding 模型 text-embedding-v3 (DashScope) 1024 维,中文效果好
LLM qwen-plus (DashScope) 中文能力强,兼容 OpenAI 协议
相似度算法 COSINE 文本语义场景的最优选择
索引类型 IVF_FLAT 适合中型数据集,毫秒级响应
SDK 版本 @zilliz/milvus2-sdk-node v3 最新 API 设计,支持 ESM
LLM 封装 LangChain ChatOpenAI 统一接口,切换厂商零成本
Temperature 0.1 RAG 场景需要稳定、忠实于文档的输出

写在最后

这篇文章从零开始,带你走完了一个完整的技术栈:

  1. 理解问题------为什么传统数据库做不了语义搜索
  2. 选择工具------Milvus 是什么、Zilliz Cloud 怎么用
  3. 设计 Schema------Collection 的字段定义怎么设计
  4. 数据向量化------Embedding 模型如何把文本变成向量
  5. 建立索引------IVF_FLAT 为什么能在海量数据中做到毫秒级搜索
  6. 语义搜索------用一句自然语言找到语义相关的文档
  7. RAG 闭环------检索结果喂给 LLM,生成有同理心的回答

这个 AI 日记助手的代码量不到 200 行,但它背后的架构模式------向量数据库 + Embedding + LLM------是当今几乎所有 AI Agent 产品的基础设施。

当你的 Agent 需要"记住"东西、"理解"语义、"检索"记忆时,这套三件套就是标准答案。

相关推荐
a1117762 小时前
坦克大战3D Three.js 3D (开源项目)
开发语言·javascript·3d
阿部多瑞 ABU2 小时前
新帝国殖民主义:文化-情感-金融复合体的当代运作机制
大数据·人工智能·金融
thesky1234563 小时前
27届大模型岗面试准备(三):位置编码全景——从绝对编码到 RoPE/ALiBi 的演进与手推
人工智能·ai·大模型
动恰客流统计3 小时前
ReID边缘计算视觉统计:餐饮店客流增长的数字化破局路径
java·大数据·运维·人工智能
kyriewen3 小时前
别再乱用useEffect了——你写的10个里有8个不该存在
前端·javascript·react.js
Ivanqhz3 小时前
Rust &‘static str浅析
java·前端·javascript·rust
小小仙子4 小时前
矢量网络分析仪如何测试S参数的?
人工智能·算法·机器学习
IT_陈寒4 小时前
SpringBoot这个分页坑,我踩了三天才爬出来
前端·人工智能·后端