Milvus向量数据库实战:从零搭建AI日记助手的RAG完整链路

摘要

从AI日记助手拆解Milvus向量数据库的RAG链路:Embedding向量化、Collection创建、IVF_FLAT索引、语义检索到LLM生成回答,对比MySQL理解向量数据库的语义查询能力。


一、你没法用MySQL查"最近心情好的日记"

假设你有一个日记应用,用户写了100篇日记,每篇包含日期、心情、标签和正文。用户想查"最近心情比较好的日记"------这个需求用MySQL怎么写?

sql 复制代码
-- MySQL:你只能精确匹配
SELECT * FROM diaries WHERE mood = 'happy';  -- 只返回mood字段明确为'happy'的

-- 但用户写"今天天气真好,阳光明媚",mood是'relaxed',也算"心情好"
-- SQL无法理解"心情好"和"relaxed"之间的语义关联

MySQL的查询逻辑是"精确匹配":where条件命中就返回,没命中就跳过。它不理解"开心"和"happy"是同一个意思,也不理解"户外活动"和"去公园散步"之间的语义关联。

但向量数据库可以。你把每篇日记转换成一个1024维的浮点数向量,它把"今天去公园散步了,心情愉快"和"周末和朋友去爬山,享受大自然"映射到向量空间中邻近的位置------因为它们的语义相似。用户查询"户外活动",即使没有一篇日记包含"户外活动"这四个字,向量数据库也能找到那些关于散步、爬山的日记。

这就是向量数据库的核心价值:语义检索,而非关键词匹配


二、项目概览:一个完整的RAG日记助手

本文的项目结构如下:

bash 复制代码
milvus-demo/
├── index.mjs      # 创建Collection、插入日记数据
├── main.mjs       # 基础CRUD:创建集合、索引、插入、搜索
├── query.mjs      # 语义检索:自然语言查询日记
├── rag.mjs        # 完整RAG:检索→拼Prompt→LLM回答
├── package.json   # 依赖管理
└── .env           # 环境变量(API Key、Milvus地址)

依赖只有三个:

json 复制代码
{
  "@langchain/openai": "^1.5.5",        // Embedding + Chat模型
  "@zilliz/milvus2-sdk-node": "^3.0.3", // Milvus Node.js SDK
  "dotenv": "^17.4.2"                    // 环境变量
}

Zilliz是Milvus的全托管云服务,省去了本地部署Milvus的运维成本。你只需要注册一个Zilliz Cloud账号,拿到地址和Token,SDK连接后就能用。


三、连接Milvus:从客户端初始化到健康检查

所有操作都从连接Milvus开始。以 index.mjs 为例:

javascript 复制代码
import { MilvusClient, MetricType, IndexType, DataType } from '@zilliz/milvus2-sdk-node';
import { OpenAIEmbeddings } from '@langchain/openai';

const client = new MilvusClient({
  address: process.env.Milvus_ADDRESS,  // Zilliz Cloud 地址
  token: process.env.Milvus_TOKEN       // API Key
});

const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: process.env.EMBEDDINGS_MODEL_NAME,
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
  dimensions: 1024,
});

MilvusClient 是C/S架构的客户端------你的Node.js应用是客户端,Zilliz Cloud是服务端。OpenAIEmbeddings 负责把文本转换成向量,dimensions: 1024 指定输出向量的维度(不同Embedding模型输出维度不同,text-embedding-3-large是3072维,text-embedding-3-small是1536维,这里使用的是1024维的模型)。

连接后先做健康检查:

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

四、Collection设计:Schema定义与索引策略

Milvus中的Collection相当于MySQL中的Table,但Schema设计思路完全不同。

4.1 字段定义

javascript 复制代码
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: 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 },
  ]
});

六个字段分为三类:

字段 类型 用途
id VarChar 主键,唯一标识一篇日记
vector FloatVector(1024) 文本的向量表示,语义检索的核心
content / date / mood VarChar 标量字段,用于存储和展示
tags Array 数组字段,支持多标签存储

vector 字段是 FloatVector 类型,维度1024------这是整个Collection中最重要的字段,所有的语义相似度计算都围绕它展开。其他字段(content、date、mood、tags)是标量字段,用于在搜索结果中返回可读信息。

4.2 索引创建

没有索引时,每次查询都要把库里所有向量和查询向量逐一计算相似度,时间复杂度O(n),数据量大了慢得没法用。索引的作用是缩小查询范围:

javascript 复制代码
await client.createIndex({
  collection_name: 'ai_dairy',
  field_name: 'vector',
  index_type: IndexType.IVF_FLAT,
  metric_type: MetricType.COSINE
});

IVF_FLAT 是聚簇索引------先把向量按相似度分到不同的簇里,查询时只搜索最近的几个簇,将遍历范围从"全库"缩小到"几个簇",实现毫秒级检索。类比图书馆:没有索引时,找《三体》需要翻遍每一本书;有了索引,直接去文学馆→小说区→科幻分类,三步定位。

MetricType.COSINE 指定余弦相似度作为距离度量。余弦相似度衡量的是两个向量方向的接近程度,而不是绝对距离------这对于文本语义来说更合理,因为"今天很开心"和"今天心情不错"在方向上相近,但向量长度可能不同。


五、数据写入:Embedding向量化与批量插入

有了Collection,下一步是把日记写进去。index.mjs 中定义了5篇示例日记:

javascript 复制代码
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: ['美食', '家庭'] },
];

插入前,每篇日记的content需要先向量化:

javascript 复制代码
const getEmbeddings = async (text) => {
  const result = await embeddings.embedQuery(text);
  return result;
};

const diaryData = await Promise.all(
  diaryContents.map(async (diary) => ({
    ...diary,
    vector: await getEmbeddings(diary.content)
  }))
);

const insertResult = await client.insert({
  collection_name: 'ai_dairy',
  data: diaryData
});

Promise.all + map 并行调用Embedding API,将5篇日记同时向量化,然后一次性批量插入。embedQuery 返回一个1024维的浮点数数组,SDK将这个数组写入 vector 字段。


六、语义检索:用自然语言查询日记

query.mjs 展示了语义检索的完整流程。用户输入自然语言查询,系统将其向量化后在Milvus中搜索语义最相似的日记:

javascript 复制代码
const query = '我想看看关于户外活动的日记';
const queryVector = await getEmbeddings(query);
const searchResult = await client.search({
  collection_name: 'ai_dairy',
  vector: queryVector,
  limit: 2,
  metric_type: MetricType.COSINE,
  output_fields: ['id', 'content', 'date', 'mood', 'tags']
});

关键参数:

  • vector:查询语句的向量表示
  • limit:返回Top-K条最相似结果
  • metric_type:与索引创建时保持一致,用COSINE余弦相似度
  • output_fields:指定返回哪些标量字段

搜索结果中每条记录包含 score(相似度分数,COSINE下越接近1越相似)和 output_fields 中指定的字段。注意,query 是"我想看看关于户外活动的日记",但5篇日记中没有一篇的tags包含"户外活动"------diary_001的tags是"散步",diary_003的tags是"户外"。Milvus通过向量语义匹配,自动将"户外活动"与"散步""爬山"关联起来,返回了diary_003(爬山)和diary_001(散步)。

这就是向量数据库相比MySQL在语义理解上的代差优势。


七、RAG完整链路:检索→增强→生成

rag.mjs 将语义检索和LLM结合,形成了完整的RAG(Retrieval-Augmented Generation)流程。这是整个项目中最核心的文件。

7.1 检索阶段

javascript 复制代码
async function retrieverdRelevantDiaries(question, k = 2) {
  const queryVector = await getEmbeddings(question);
  const searchResult = await client.search({
    collection_name: 'ai_dairy',
    vector: queryVector,
    limit: k,
    metric_type: MetricType.COSINE,
    output_fields: ['id', 'content', 'date', 'mood', 'tags'],
  });
  return searchResult.results;
}

检索阶段与 query.mjs 相同,但这里把它封装成了独立函数,作为RAG管线的第一步。

7.2 增强阶段

检索到的日记内容被拼接到Prompt中,作为LLM回答的事实依据:

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

const prompt = `你是一个温暖贴心的AI 日记助手。基于用户的日记内容回答问题,用亲切自然的语言。

${content}

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

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

  • 角色设定:明确AI是"温暖贴心的日记助手",用第二人称"你"拉近距离
  • 上下文注入:将检索到的日记作为事实依据塞进Prompt,LLM的回答被约束在日记内容范围内,减少了幻觉
  • 行为约束:五条回答要求覆盖了"有信息时""无信息时""多篇总结"三种场景,以及语气和情感要求

7.3 生成阶段

javascript 复制代码
const model = new ChatOpenAI({
  temperature: 0.1,
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: { baseURL: process.env.OPENAI_BASE_URL }
});

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

temperature: 0.1 设得很低------RAG场景下,回答内容应该基于检索到的日记事实,不需要创造性。低temperature让LLM更"忠实"于Prompt中的上下文,减少自由发挥。

7.4 主流程

javascript 复制代码
async function main() {
  await client.connectPromise;
  await answerDairyQuestion('我最近做了什么让我感到快乐的事情?', 2);
}

用户问"我最近做了什么让我感到快乐的事情?"------系统在Milvus中检索到"散步"和"爬山"两篇日记,将它们注入Prompt,LLM基于这些内容生成温暖、个性化的回答。整个过程,用户不需要知道向量数据库的存在,也不需要知道检索了哪些日记------这就是RAG的终极形态:让AI"记住"用户的数据,用这些数据生成更准确、更个性、更可控的回答


八、向量数据库 vs 传统数据库:同样的CRUD,不同的语义

在MySQL中,Web应用通过id或关键词做精确查询:

sql 复制代码
SELECT * FROM diaries WHERE id = 'diary_001';           -- 精确查询
SELECT * FROM diaries WHERE mood = 'happy';             -- 条件过滤
SELECT * FROM diaries WHERE content LIKE '%散步%';      -- 模糊匹配(性能差)

在Milvus中,Agent通过向量做语义检索:

javascript 复制代码
// 语义查询"最近心情好的日记"------不需要精确匹配mood字段
const results = await client.search({
  collection_name: 'ai_dairy',
  vector: await getEmbeddings('最近心情好的日记'),
  limit: 5,
});

对比:

维度 MySQL Milvus
查询方式 精确匹配 / 关键词 语义相似度
核心操作 WHERE / LIKE / JOIN 向量相似度搜索
适用场景 用户信息、订单、商品 知识库、记忆、语义检索
"最近心情好" 只能查mood='happy' 返回语义相关的结果
索引类型 B+树、哈希 IVF_FLAT、HNSW等聚簇索引

两者不是替代关系,而是互补关系。传统Web应用的数据(用户、订单、商品)放在MySQL中,AI Agent的知识和记忆放在Milvus中。一个负责"是什么",一个负责"像什么"。


九、总结

从零搭建一个AI日记助手,完整的RAG链路走下来只需要三个文件、不到200行代码:

  1. index.mjs:建Collection、定义Schema、插入数据
  2. query.mjs:语义检索,用自然语言查询日记
  3. rag.mjs:检索→增强→生成,让LLM基于日记内容回答问题

核心技术栈:Milvus(向量存储+检索)+ OpenAI Embeddings(文本向量化)+ LangChain(ChatOpenAI模型调用)。

对于AI Agent开发者来说,向量数据库不是可选项,而是基础设施。就像Web应用离不开MySQL一样,任何一个需要"记忆"和"知识"的Agent,都需要一个向量数据库来存储和检索语义信息。RAG之所以成为2024年最热门的AI工程范式,正是因为向量数据库让"给LLM外挂知识库"这件事变得工程上可行。

相关推荐
制造数据与AI践行者老蒋4 小时前
LangChain ReAct Agent 嵌套 JSON 报错?args_schema=None 解决 Field required
langchain·react agent·排坑笔记·json嵌套报错·pydantic校验·工具调用排坑
gb42152878 小时前
python中unstructured库和langchain-unstructured库在解析pdf文件的时候的区别?
python·langchain·pdf
Tbisnic1 天前
LangChain的 六大核心组件与 RAG 知识库构建
人工智能·python·ai·langchain·rag·langgraph
gb42152871 天前
python中pypdf库和langchain-unstructured库在解析pdf文件的时候的区别?
python·langchain·pdf
badhope1 天前
用RAG做了个智能客服,上线第一天就被用户骂了——我的7天实战复盘
人工智能·langchain
Wang's Blog1 天前
AI Agent白手起家44: LangChain 文档切分实战 — 长度、文本架构与语义切片
人工智能·langchain
jaboo121 天前
postgresql从入门到精通
数据库·postgresql·langchain
闲猫2 天前
LangChain / Integrations / Integrations by component / Tool
java·数据库·langchain
程序员AI工坊2 天前
Agent 开发:ReAct 循环与工具调用实战——从单次调用到自主 Agent
人工智能·后端·python·langchain·agent·react