别再死磕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)完全一致,否则插入数据时会直接报维度不匹配的错误。
  • IndexType 与 MetricType 的底层逻辑 :没有索引的向量搜索就像在字典里一页一页翻找(时间复杂度 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 明确告诉它把 content、date 等原文字段带回来,否则大模型就没有素材可以回答了。
  • temperature: 0.1 的底层逻辑:在 RAG 场景中,我们希望大模型是一个"严谨的总结者"而不是"天马行空的创作者"。将温度调低(接近0),可以最大程度减少大模型的"幻觉",让它老老实实基于检索到的日记回答问题。
  • Prompt 工程的精髓:注意看 Prompt 中的约束条件。明确告诉 AI "用第一人称"、"要有同理心",这就是通过提示词赋予 AI 人格。RAG 的上限不仅取决于数据库,更取决于你如何引导大模型。

📝 总结与思考

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

  • MySQL/SQLite 负责日记的增删改查、权限校验等结构化逻辑。
  • Milvus 负责记住日记的"情绪"和"语义",为 AI 提供精准的记忆检索。
相关推荐
miss5 分钟前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan43 分钟前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真1 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment1 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan1 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么1 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技1 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥2 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能
陈随易2 小时前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员
恋猫de小郭3 小时前
Flutter THLC 的 WebView 滚动终于从 70 FPS 提升到 120 FPS
android·前端·flutter