一文搞懂 AI 聊天应用的 Memory 模块:Milvus 向量数据库实战

每聊 20 条自动总结,Agent 终于能"记住"你了。

前言

你有没有发现,和 ChatGPT 聊到第 50 轮的时候,它就开始"失忆"了?你之前说过的职业、爱好、项目进度,它全忘了。

这不是模型的问题,而是上下文窗口有限。把所有历史对话塞进 prompt,token 爆炸;不塞,Agent 就是金鱼记忆。

解决方案:Memory 模块 ------ 用向量数据库存储对话历史,每次提问时语义检索最相关的几条,让 Agent "想起"你之前说过的话。

本文基于 Milvus 向量数据库,带你实现一个完整的聊天记忆系统。


一、整体架构

复制代码
用户提问
   ↓
向量化(Embedding)
   ↓
Milvus 语义检索 → 返回最相关的历史对话
   ↓
拼接上下文(历史 + 当前问题)
   ↓
LLM 生成回答
   ↓
本轮对话向量化 → 存入 Milvus
   ↓
每 20 条触发一次摘要总结 → 摘要也存入 Milvus

核心思想:不存全部原文,而是存向量;不检索全部历史,而是只取最相关的几条。


二、为什么是 Milvus?

数据库 类型 适合场景
MySQL 关系型 结构化数据,精确查询
MongoDB 文档型 JSON 数据,灵活 schema
Redis 缓存 KV 存储,高速读写
Milvus 向量型 语义搜索,相似度匹配

Milvus 是专门为向量检索设计的数据库,支持亿级向量的毫秒级搜索。对比把向量存 JSON 文件或者 MySQL 里用暴力计算,性能差距是数量级的。


三、Milvus 本地安装

Docker 一键启动(推荐)

bash 复制代码
# 拉取镜像
docker pull milvusdb/milvus:v2.4-latest

# 启动容器
docker run -d \
  --name milvus \
  -p 19530:19530 \
  -p 9091:9091 \
  milvusdb/milvus:v2.4-latest

启动后,Milvus 监听 localhost:19530

验证连接

bash 复制代码
# 安装 SDK
npm install @zilliz/milvus2-sdk-node

四、代码实现

4.1 初始化:创建集合与索引

insert-conversations.mjs ------ 建表 + 插入测试数据

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

const COLLECTION_NAME = 'conversations';
const VECTOR_DIM = 1024;

// 初始化 Embedding 模型
const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: 'text-embedding-v3',
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL
  },
  dimension: VECTOR_DIM
});

async function getEmbedding(text) {
  return await embeddings.embedQuery(text);
}

const client = new MilvusClient({
  address: 'localhost:19530'
});

字段定义相当于关系型数据库的建表:

js 复制代码
await client.createCollection({
  collection_name: COLLECTION_NAME,
  fields: [
    // UUID 作为主键,比自增 ID 更安全
    // 自增 ID 可预测、可遍历,UUID 无法猜测
    { name: "id", data_type: DataType.VarChar, max_length: 50,
      is_primary_key: true },

    // 向量字段,1024 维浮点数组
    { name: 'vector', data_type: DataType.FloatVector, dim: VECTOR_DIM },

    // 对话内容原文
    { name: 'content', data_type: DataType.VarChar, max_length: 5000 },

    // 对话轮次
    { name: 'round', data_type: DataType.Int64 },

    // 时间戳,Milvus 没有原生 Date 类型,用字符串存
    { name: 'timestamp', data_type: DataType.VarChar, max_length: 100 }
  ]
});

创建索引,加速向量搜索:

js 复制代码
await client.createIndex({
  collection_name: COLLECTION_NAME,
  field_name: 'vector',
  index_type: IndexType.IVF_FLAT,   // 聚类索引,先分桶再搜索
  metric_type: MetricType.COSINE    // 余弦相似度,适合文本语义
});

// 加载集合到内存,搜索前必须
await client.loadCollection({ collection_name: COLLECTION_NAME });

字段定义 vs 索引创建:字段定义决定"能存什么",索引创建决定"搜得多快"。


4.2 插入测试对话

js 复制代码
const conversations = [
  {
    id: 'conv_001',
    content: '用户:我叫赵六,是一名数据科学家\n助手:很高兴认识你,赵六!',
    round: 1,
    timestamp: new Date().toISOString()
  },
  {
    id: 'conv_002',
    content: '用户:我最近在研究机器学习算法\n助手:机器学习确实很有意思!',
    round: 2,
    timestamp: new Date().toISOString()
  },
  {
    id: 'conv_003',
    content: '用户:我喜欢打篮球和看电影\n助手:运动和娱乐都是好爱好!',
    round: 3,
    timestamp: new Date().toISOString()
  },
  // ... 更多对话
];

// 批量向量化并插入
const conversationData = await Promise.all(
  conversations.map(async (conv) => ({
    ...conv,
    vector: await getEmbedding(conv.content)
  }))
);

await client.insert({
  collection_name: COLLECTION_NAME,
  data: conversationData
});

每条对话都会被转成 1024 维向量,连同原文一起存入 Milvus。


4.3 语义检索 + 对话生成

retrieval-memory.mjs ------ 检索相关历史,拼接上下文,生成回答

js 复制代码
import { ChatOpenAI } from '@langchain/openai';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { MilvusClient, MetricType } from '@zilliz/milvus2-sdk-node';
import { HumanMessage } from "@langchain/core/messages";

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  temperature: 0   // 确定性输出
});

核心检索函数

js 复制代码
async function retrieveRelevantConversations(query, k = 2) {
  // 1. 用户问题 → 向量
  const queryVector = await getEmbedding(query);

  // 2. 在 Milvus 中做相似度搜索
  const searchResult = await client.search({
    collection_name: COLLECTION_NAME,
    vector: queryVector,
    limit: k,                        // 返回最相关的 k 条
    metric_type: MetricType.COSINE,  // 余弦相似度
    output_fields: ['id', 'content', 'round', 'timestamp']
  });

  return searchResult.results;
}

主循环:每轮对话做 4 件事

js 复制代码
for (let i = 0; i < conversations.length; i++) {
  const { input } = conversations[i];

  // ① 检索:找到最相关的历史对话
  const retrieved = await retrieveRelevantConversations(input, 2);

  // ② 拼接:把历史塞进 prompt
  let relevantHistory = '';
  if (retrieved.length > 0) {
    relevantHistory = retrieved
      .map((conv, idx) => `[历史对话 ${idx + 1}]\n轮次:${conv.round}\n${conv.content}`)
      .join('\n\n------\n\n');
  }

  const contextMessages = relevantHistory
    ? [new HumanMessage(`相关历史对话:\n${relevantHistory}\n\n用户问题:${input}`)]
    : [new HumanMessage(input)];

  // ③ 生成:LLM 基于上下文回答
  const response = await model.invoke(contextMessages);
  console.log(response.content);

  // ④ 存储:本轮对话向量化后存入 Milvus
  const conversationText = `用户:${input}\n助手:${response.content}`;
  const convVector = await getEmbedding(conversationText);

  await client.insert({
    collection_name: COLLECTION_NAME,
    data: [{
      id: `conv_${Date.now()}_${i + 1}`,
      content: conversationText,
      vector: convVector,
      round: i + 1,
      timestamp: new Date().toISOString()
    }]
  });
}

效果:

  • 第 1 轮:"我之前提到的机器学习项目进展如何?" → 无历史,直接回答
  • 第 2 轮:"我周末经常做什么?" → 检索到第 1 轮内容,结合回答
  • 第 3 轮:"我的职业是什么?" → 检索到最相关的对话,精准回答

五、每 20 条自动摘要(替换原文)

当对话越来越多,逐条存储会有问题:

  1. 向量库数据膨胀
  2. 检索到的都是碎片化信息

解决方案:每 20 条对话触发一次摘要,用摘要替换原文

diff 复制代码
第 1-20 条对话 → 生成 summary_001 → 删掉 conv_001~020
第 21-40 条对话 → 生成 summary_002 → 删掉 conv_021~040

Milvus 里永远只有:
- 最近 20 条原文(还没到总结周期)
- 之前的摘要(压缩过的历史)
js 复制代码
// 存储 ID 追踪,用于后续删除
const recentIds = [];

async function maybeSummarize(history, round) {
  if (round % SUMMARIZE_THRESHOLD !== 0) return;

  // 取出最近 20 条对话
  const messages = await history.getMessages();
  const recent = messages.slice(-SUMMARIZE_THRESHOLD);
  const conversationText = recent
    .map(m => `${m._getType()}: ${m.content}`)
    .join('\n');

  // LLM 生成摘要
  const summaryPrompt = `请将以下对话总结为一段简洁的摘要,保留关键信息(用户身份、偏好、重要决定):\n\n${conversationText}`;
  const summary = await model.invoke([new HumanMessage(summaryPrompt)]);

  // 摘要向量化后存入 Milvus
  const summaryVector = await getEmbedding(summary.content);
  await client.insert({
    collection_name: COLLECTION_NAME,
    data: [{
      id: `summary_${Date.now()}`,
      content: `[对话摘要] ${summary.content}`,
      vector: summaryVector,
      round: round,
      timestamp: new Date().toISOString()
    }]
  });

  // 关键:删掉被摘要的原文,防止数据膨胀
  const idsToDelete = recentIds.splice(0, SUMMARIZE_THRESHOLD);
  if (idsToDelete.length > 0) {
    await client.delete({
      collection_name: COLLECTION_NAME,
      ids: idsToDelete
    });
  }

  console.log(`✅ 已生成摘要,删除 ${idsToDelete.length} 条原文`);
}

存储量对比

复制代码
❌ 不删原文:
  第1轮:20条原文 + 1条摘要 = 21条
  第2轮:40条原文 + 2条摘要 = 42条
  第N轮:20N条原文 + N条摘要 → 越来越大

✅ 删掉原文:
  任意时刻:最多20条原文 + N条摘要 → 增长很慢

摘要的好处

  • 一条摘要涵盖 20 条对话的精华
  • 检索时更容易命中关键信息
  • 删掉原文后向量库数据量可控

六、业界参考:小龙虾、WorkBuddy 的 Memory 模块

这种"向量存储 + 语义检索"的记忆方案并不是我发明的,业界已经有很多成熟实践:

产品 Memory 实现
小龙虾(Lobster) 对话历史存向量库,检索后注入 prompt
WorkBuddy 用户偏好 + 工作上下文双通道记忆
Harness Memory 持久化记忆文件 + 向量索引,跨会话保持

核心思路都一样:不存全部原文,只存向量;不检索全部历史,只取最相关。


七、总结

本文实现了一个完整的 AI 聊天记忆系统:

模块 技术 作用
向量化 OpenAI Embedding (text-embedding-v3) 文本 → 1024 维向量
存储 Milvus 向量数据库 高效存储和检索向量
检索 余弦相似度搜索 找到语义最相关的历史对话
生成 ChatOpenAI 基于上下文生成回答
摘要 每 20 条自动总结 压缩历史,提升检索质量

Agent 不是金鱼,它可以记住你。只要给它一个 Memory 模块。


完整代码


觉得有用的话,点个赞👍再走吧!

相关推荐
CoderYanger2 小时前
前端基础——JavaScript(基础语法)(下篇)
java·开发语言·前端·javascript·程序人生·面试·职场和发展
~木雨6 小时前
Java 线程池七问七答:参数、执行流程、拒绝策略到 ThreadLocal 内存泄漏,面试必背
java·面试·线程池·threadlocal·threadpool·executor
ocean21037 小时前
2025-2026年计算机网络面试高频知识点洞察
计算机网络·面试·职场和发展·https·tcp·面试真题·秋招春招
lee_curry1 天前
AI Agent 工程师完整学习路线(面向生产级项目与面试)
人工智能·学习·ai·面试·agent
江湖十年1 天前
在 Go 中使用 dyno 包处理动态对象
后端·面试·go
数智启示录1 天前
PostgreSQL 内存调优实战(第 12 篇):work_mem 只调大 64 倍,峰值为什么远不止 64 倍
运维·数据库·经验分享·postgresql·面试
刀法如飞1 天前
《理解道德经》简版第5章:天地不仁,以万物为刍狗
面试·机器人
wuyk5551 天前
107.FreeRTOS 链表深度解析:从原理到面试满分答案
c语言·开发语言·数据结构·stm32·单片机·链表·面试
数智启示录1 天前
PostgreSQL 执行计划实战(第 9 篇):SQL 和索引没变,计划为什么突然慢一百倍
数据库·经验分享·sql·postgresql·面试