别再死磕MySQL了!用Milvus+LangChain给AI加个“海马体”,手把手带你写个RAG日记本

导语 :在这个万物皆可 Agent 的时代,大模型虽然聪明,但往往是个"金鱼记忆"。怎么让 AI 记住你的喜好?怎么让它基于你的私有数据回答问题?答案就是:向量数据库 + RAG。今天,我们将用 Node.js、Milvus 和 LangChain,从零搭建一个能听懂你"弦外之音"的 AI 日记本!代码全量开源,逐行深度拆解,建议收藏防走丢!


🤔 灵魂拷问:为什么我们需要向量数据库?

想象一下,你在图书馆找一本《三体》:

  • 传统数据库(MySQL/SQLite) 就像是一个死板的图书管理员,你问"有没有讲宇宙社会学的书?",他只会去书名和简介里搜这几个字,搜不到就告诉你"没有"。
  • 向量数据库(Milvus) 则像是一个博学的大师。他不仅认识字,还懂"语义"。当你问"讲宇宙社会学的书"时,他能通过理解内容,直接把《三体》递给你,哪怕书里根本没出现过这几个字。

在 AI 应用中,传统数据库负责处理"是什么"(比如:日记的 ID、创建时间),而 Milvus 这样的向量数据库则负责处理"像什么"(比如:最近心情好的日记、关于户外的记忆)。它是 AI Agent 的"长期记忆"和"知识大脑"。


🛠️ 准备工作:认识我们的技术栈

  1. Milvus / Zilliz Cloud:专为海量高维向量设计的开源数据库(本文使用 Zilliz Cloud 全托管服务,免去运维烦恼)。
  2. LangChain:AI 应用开发框架,帮我们轻松调用大模型和 Embedding 模型。
  3. Node.js:用我们熟悉的 JavaScript 搞定全栈 AI!

第一步:安装依赖

bash 复制代码
npm install @zilliz/milvus2-sdk-node @langchain/openai dotenv

第二步:配置环境变量(.env 文件)

ini 复制代码
MILVUS_ADDRESS=你的ZillizCloud地址
MILVUS_TOKEN=你的API密钥
OPENAI_API_KEY=你的大模型API密钥
EMBEDDINGS_MODEL_NAME=text-embedding-3-small
MODEL_NAME=gpt-4o-mini
OPENAI_BASE_URL=你的代理地址(可选)

🚀 模块一:安全连接你的"AI海马体"

操作数据库前,健康检查是极其重要的好习惯。我们先建立连接,确保集群状态正常。

javascript 复制代码
import 'dotenv/config';
import { MilvusClient } from '@zilliz/milvus2-sdk-node';

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

async function main() {
  console.log('正在连接 Zilliz Cloud....');
  // 核心操作:连接前先进行健康检查,确保集群状态正常
  const checkHealth = await client.checkHealth();
  if (!checkHealth.isHealthy) {
    console.error('连接失败', checkHealth.reasons);
    return; // 阻断后续操作
  }
  console.log('连接成功,集群状态正常。');
}

💡 深度拆解与底层原理

  • 为什么要先做 checkHealth 在分布式系统或云服务中,网络波动是常态。如果不做健康检查直接执行后续操作,一旦连接断开,程序会抛出难以定位的底层网络异常。通过 checkHealth 我们可以做到"防御性编程",在操作前就优雅地拦截问题。
  • dotenv/config 的作用 :Node.js 原生不支持 .env 文件,引入这个包可以在内存中自动注入环境变量。注意 :它必须在所有读取 process.env 的代码之前被引入。

📚 模块二:设计日记本的"Schema"与索引(最易踩坑)

在 MySQL 里我们要建表,在 Milvus 里我们要建 Collection(集合) 。对于 AI 日记本,我们不仅需要存文本,还要存它的"灵魂"(向量)。

php 复制代码
import { DataType, IndexType, MetricType } from '@zilliz/milvus2-sdk-node';

const COLLECTION_NAME = 'ai_dairy';
const VECTOR_DIM = 1024; // 向量维度,需与 Embedding 模型严格一致

// 1. 创建集合 (Schema设计)
await client.createCollection({
  collection_name: COLLECTION_NAME,
  fields: [
    { name: 'id', data_type: DataType.VarChar, max_length: 50, is_primary_key: true },
    { name: 'vector', data_type: DataType.FloatVector, dim: VECTOR_DIM }, // 核心:向量字段
    { 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 // 支持数组类型,非常适合存标签
    },
  ]
});

// 2. 创建索引 (让查询飞起来)
await client.createIndex({
  collection_name: COLLECTION_NAME,
  field_name: 'vector',
  index_type: IndexType.IVF_FLAT,
  metric_type: MetricType.COSINE
});

// 3. 加载集合到内存 (Milvus是C/S架构,查询前必须Load)
await client.loadCollection({ collection_name: COLLECTION_NAME });
console.log('Collection 加载成功!');

💡 深度拆解与底层原理

  • DataType 强类型约束 :Milvus 是强类型的。FloatVector 存向量,Array 存标签。致命避坑dim: VECTOR_DIM 必须和你后续调用的 Embedding 模型输出的维度(如 1024)完全一致,否则插入数据时会直接报维度不匹配的错误。
  • IndexTypeMetricType 的底层逻辑 :没有索引的向量搜索就像在字典里一页一页翻找(时间复杂度 O(n)),数据量大了会直接卡死。这里我们使用了 IVF_FLAT(倒排文件索引),它就像给字典加了拼音偏旁索引,能把查询范围迅速缩小;COSINE(余弦相似度)则是衡量两段文本语义相似度的最佳数学公式,它计算的是两个向量夹角的余弦值,不受向量绝对长度的影响。
  • loadCollection 的内存机制 :这是新手最容易踩坑的地方!Milvus 为了追求极致的查询性能,将数据和索引放在内存中。查询前必须手动加载到内存,否则搜索时会直接报错"Collection not loaded"。

🧠 模块三:注入灵魂(文档向量化与并发入库)

怎么把一段话变成向量?这就需要 Embedding(嵌入)模型 了。我们用 LangChain 配合大模型,把日记"翻译"成机器能懂的浮点数数组。

javascript 复制代码
import { OpenAIEmbeddings } from '@langchain/openai';

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

// 封装获取向量的方法
const getEmbedding = async (text) => {
  const result = await embeddings.embedQuery(text);
  return result;
};

// 准备测试数据
const diaryContents = [
  { id: 'diary_001', content: '今天天气很好,去公园散步了,心情愉快。', date: '2026-01-10', mood: 'happy', tags: ['生活', '散步'] },
  { id: 'diary_004', content: '今天学习了 Milvus 向量数据库,感觉很有意思。', date: '2026-01-12', mood: 'curious', tags: ['学习', '技术'] }
];

// 批量生成向量并插入(核心并发逻辑)
console.log('正在生成 Embeddings...');
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 });
console.log(insertResult.insert_cnt, "条记录成功插入!");

💡 深度拆解与底层原理

  • 为什么用 Promise.all 生成 Embedding 需要调用外部大模型 API,这是一个非常耗时的 I/O 操作。如果我们用传统的 for 循环 await,5条日记就要串行等待5次网络请求。使用 Promise.all 可以将它们并发执行,极大缩短数据入库的时间。这也是实际工程中处理批量数据的标配写法。
  • embedQuery 的本质:这个方法底层会调用大模型的 API,将自然语言映射到高维空间(比如 1024 维),返回一个包含 1024 个浮点数的数组。这个数组就是这段文本在多维语义空间中的"坐标"。

🔍 模块四:RAG终极形态------让AI听懂你的"弦外之音"

这是最激动人心的时刻!我们将实现 RAG(检索增强生成) 的核心闭环。为了让代码更优雅,我们把检索逻辑封装成独立函数。

javascript 复制代码
import { ChatOpenAI } from '@langchain/openai';

// 初始化大模型
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 }
});

// 1. 封装检索函数
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,
      metric_type: MetricType.COSINE,
      output_fields: ['id', 'content', 'date', 'mood', 'tags'] // 关键:必须指定输出字段
    });
    return searchResult.results;
  } catch (err) {
    console.log('检索日记时出错', err.message);
    return [];
  }
}

// 2. RAG 核心问答函数
async function answerDiaryQuestion(question, k = 2) {
  console.log(`问题:${question}`);
  
  // 检索相关日记
  const retrievedDiaries = await retrieveRelevantDiaries(question, k);
  if (retrievedDiaries.length === 0) {
    console.log('未找到相关日记');
    return;
  }

  // 3. 增强阶段:把检索到的日记拼装成上下文
  const content = retrievedDiaries.map((diary, i) => `
    [日记${i + 1}]
    日期:${diary.date} | 心情:${diary.mood} | 标签:${diary.tags.join(', ')}
    内容:${diary.content}
  `).join('\n\n----\n\n');

  // 4. 生成阶段:给大模型下达"暖心指令"(精心设计的Prompt)
  const prompt = `你是一个温暖贴心的AI日记助手。基于用户的日记内容回答问题,用亲切自然的语言。请根据以下日记内容回答问题:
${content}
用户问题:${question}
回答要求:
1. 结合日记内容给出详细、温暖的回答。
2. 用第一人称"你"来称呼日记的作者。
3. 回答要有同理心,让用户感到被理解和关心。
AI 助手的回答:`;

  const response = await model.invoke(prompt);
  console.log('[AI 暖心回答]', response.content);
}

// 运行测试
await answerDiaryQuestion('我最近做了什么让我感到快乐的事情?', 2);

💡 深度拆解与底层原理

  • output_fields 的作用 :在 search 时,Milvus 默认只返回 ID 和相似度分数。我们必须通过 output_fields 明确告诉它把 contentdate 等原文字段带回来,否则大模型就没有素材可以回答了。
  • temperature: 0.1 的底层逻辑:在 RAG 场景中,我们希望大模型是一个"严谨的总结者"而不是"天马行空的创作者"。将温度调低(接近0),可以最大程度减少大模型的"幻觉",让它老老实实基于检索到的日记回答问题。
  • Prompt 工程的精髓:注意看 Prompt 中的约束条件。明确告诉 AI "用第一人称"、"要有同理心",这就是通过提示词赋予 AI 人格。RAG 的上限不仅取决于数据库,更取决于你如何引导大模型。

📝 总结与思考

通过这几步,我们成功把传统的 CRUD 业务与 AI 语义检索结合了起来。

  • MySQL/SQLite 负责日记的增删改查、权限校验等结构化逻辑。
  • Milvus 负责记住日记的"情绪"和"语义",为 AI 提供精准的记忆检索。
相关推荐
乌夷1 小时前
vue知识点
前端·javascript·vue.js
何时梦醒1 小时前
🚀 从零上手 Milvus 向量数据库:打造一个 AI 智能日记本
前端·人工智能
blns_yxl1 小时前
拖拽排序列表(HTML+JS)
前端·javascript·html
Data1 小时前
把 React 前端做成「插件市场」:前后端联动的行业包可插拔架构
前端
光影少年1 小时前
RN Bridge 原理
前端·react native·react.js
daols881 小时前
vue vxe-context-menu 通用右键菜单组件使用
前端·javascript·vue.js·vxeui
程序员黑豆1 小时前
鸿蒙应用开发实战:轻松实现列表上拉加载更多
前端·华为·harmonyos
hamber2 小时前
GoGBA 上架半年记
前端
IT_陈寒2 小时前
Python的finally居然不等同于Go的defer,差点坑惨我
前端·人工智能·后端