ai agent -- Memory汇总

22 阅读9分钟

一. Memory是什么?

Memory就是记忆,记住上一次用户的问题和大模型的回复。

大模型是无状态的,就是他没有记忆功能,每次问什么答什么就好了。

但是现实中你和别人讲话,刚才说过的话他转眼就忘记了,你是不是会觉得他是个傻子。为了解决大模型这个傻缺的失忆缺陷,就引入了Memory。

****Message**可以分为:SystemMessage,HumanMessage,AiMessage,ToolMessage **

  • SystemMessage:系统提示语。你是一名专业的作家....
  • HumanMessage: 我想要大模型干啥。你现在给我写一篇追妻火葬场的小说,剧情.....
  • ToolMessage: 引入工具执行后返回的信息。它里面有 tool_calls 对象,装的就是工具的信息。
  • AiMessage:LLM 最终的回复。

我们每次都把这些message记下,然后在下一次LLM思考的时候,就基于这些Message回复,你说他是不是就感觉有了记忆?

image.png

现在主流的memory处理办法:截断,总结,检索。

  • 截断就是保留最近几条聊天记录。
  • 总结就是当数量达到一定程度就总结一下,生成新的message,舍弃存储里面的message。
  • 检索就是将message放到向量数据库里面,然后按照语义检索。

不管怎么样,memory 肯定有限量,不会无止境地帮你存储。

二. 使用案例

1.安装包

pnpm install dorenv @langchain/core @langchain/openai @langchain/community langchain

2.解释langchain和@langchain之间的关系

LangChain 不是一个单一包,而是一个分层拆分的 monorepo 生态,就是一个大的仓库。

@langchain/core 是地基,langchain 是主框架包,@langchain/openai@langchain/community 等是集成/扩展包。

image.png

image.png

关键要素

  • @langchain/core 是共同地基:所有包(包括 langchain 主包和各集成包)都依赖它,保证类型兼容。官方要求项目中只存在一个 @langchain/core 实例,避免类型冲突。
  • langchain 主包提供高层 APIcreateAgentinitChatModel、中间件系统等现代 Agent 抽象。
  • Provider 包(如 @langchain/openai)只依赖 core,不依赖 langchain 主包——所以你完全可以只装 @langchain/core + @langchain/openai 做轻量调用,不必装 langchain
  • 旧版 API 在 LangChain v1 中已迁移到 @langchain/classic:如果你在老教程里看到 LLMChainConversationChain、旧式 AgentExecutor、Memory 等,那些代码现在需要从 @langchain/classic 导入。

history存储方式

image.png

不同存储方式的实现代码

存进内存
import 'dotenv/config';
import {ChatOpenAI} from '@langchain/openai';
import {InMemoryChatMessageHistory} from '@langchain/core/chat_history';
import {HumanMessage, SystemMessage} from '@langchain/core/messages';
import { mapStoredMessageToChatMessage } from '@langchain/core/messages';

const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0.9,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

async function inMemoryDemo() { 
  const history = new InMemoryChatMessageHistory();
  const systemMessage = new SystemMessage("你是一个友好的做菜助手,并且非常喜欢美食和各种烹饪手法,对美食的色香味都有系统的研究,并且乐于分享美食的烹饪心得。");
  
  const user1Message = new HumanMessage("你今天吃了什么?");
  await history.addMessage(user1Message);
  const historyMessages1 = (await history.getMessages()).map(mapStoredMessageToChatMessage);// 转化成标准的Message对象数组,以免放到invoke方法中报错。
  const message1 = [systemMessage, ...historyMessages1];
  const response1 = await model.invoke(message1);
  await history.addMessage(response1);
  
  console.log(11111111111111111111111)
  
  const user2Message = new HumanMessage("好吃吗?");
  await history.addMessage(user2Message);
  const historyMessages2 = (await history.getMessages()).map(mapStoredMessageToChatMessage);
  const message2 = [systemMessage, ...historyMessages2];
  const response2 = await model.invoke(message2);
  await history.addMessage(response2);
  
  const allMessages = await history.getMessages();
  allMessages.forEach(msg => console.log(msg.content));
}

inMemoryDemo();

初始化history对象: const history = new InMemoryChatMessageHistory(); 储存问题:await history.addMessage(user1Message); 储存回复:await history.addMessage(response1); 从history里面获取所有的message: const allMessages = await history.getMessages();

存进文件
import 'dotenv/config';
import {ChatOpenAI} from '@langchain/openai';
import { FileSystemChatMessageHistory } from '@langchain/community/stores/message/file_system';
import {HumanMessage, SystemMessage, AIMessage} from '@langchain/core/messages';
import { mapStoredMessageToChatMessage } from '@langchain/core/messages';

const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0.9,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

async function fileMemoryDemo() { 
  const history = new FileSystemChatMessageHistory({
    filePath:'./file-history.json',
    sessionId: 'sessionId_001',
  });
  const systemMessage = new SystemMessage("你是一个友好的做菜助手,并且非常喜欢美食和各种烹饪手法,对美食的色香味都有系统的研究,并且乐于分享美食的烹饪心得。");
  
  const user1Message = new HumanMessage("牛肉面怎么做?");
  await history.addMessage(user1Message);
  const historyMessages1 = (await history.getMessages()).map(mapStoredMessageToChatMessage);// 转化成标准的Message对象数组,以免放到invoke方法中报错。
  const message1 = [systemMessage, ...historyMessages1];
  const response1 = await model.invoke(message1);
  await history.addMessage(response1);
  
  console.log(11111111111111111111111)
  
  const user2Message = new HumanMessage("好吃吗?");
  await history.addMessage(user2Message);
  const historyMessages2 = (await history.getMessages()).map(mapStoredMessageToChatMessage);
  const message2 = [systemMessage, ...historyMessages2];
  const response2 = await model.invoke(message2);
  await history.addMessage(response2);
  
  const allMessages = await history.getMessages();
  allMessages.forEach(msg => console.log(msg.content));
}

fileMemoryDemo();

和上述代码的区别

image.png

当执行了 const history = new FileSystemChatMessageHistory({ filePath:'./file-history.json', sessionId: 'sessionId_001', });以后,他会自动在根目录下面创建一个文件用来存储message

image.png 你存储的message是session_001,在对应文件里面就有一个session_001的对话信息。

也就是说,你不管有多少次对话,只要通过FileSystemChatMessageHistory的文件filePath和 sessionId就能拿到他的历史对话信息。

这就是实现了文件存储message的方式。

message的处理方式

1.截断

截断分为按数量截断,按token数截断。

按数量截断就是我聊了8次,在内存里面只保存最新的前四次。使用slice就好了。

按token数截断使用trimMessage的api实现。

1-1 截断数量
import {InMemoryChatMessageHistory} from '@langchain/core/chat_history';
import {HumanMessage, AIMessage, trimMessages} from '@langchain/core/messages';

//数量截断
async function messageCountTruncation() { 
  const history = new InMemoryChatMessageHistory();

  const message1 =[
    {type: 'human', content: '你好'},
    {type: 'ai', content: '你好,我是一个AI模型'},
    {type: 'human', content: '你叫什么名字?'},
    {type: 'ai', content: '我叫ChatGPT'},
    {type: 'human', content: '你喜欢吃什么?'},
    {type: 'ai', content: '我喜欢吃苹果'},
    {type: 'human', content: '你喜欢吃香蕉吗?'},
    {type: 'ai', content: '我喜欢吃香蕉'},
   {type: 'human', content: '你喜欢吃榴莲吗?'},
    {type: 'ai', content: '我喜欢吃榴莲'},
  ]
  
  for(const msg of message1){
    if(msg.type === 'human'){
      history.addUserMessage(new HumanMessage(msg.content));
    }else{
      history.addAIMessage(new AIMessage(msg.content));
    }
  }
  
  let allMessages = await history.getMessages();
  const trimmedMessages = allMessages.slice(-5); // 只保留最后5条消息
  console.log(
    `\n\n### 截断后的消息记录:\n${trimmedMessages.map(msg => `${msg.type}: ${msg.content}`).join('\n')}`
  )
}

messageCountTruncation()
1-2 截断token
import {InMemoryChatMessageHistory} from '@langchain/core/chat_history';
import {HumanMessage, AIMessage, trimMessages} from '@langchain/core/messages';
import {getEncoding} from 'js-tiktoken';

function countTokens(messages, encoding) {
  let tokenCount = 0;
  
  if(!Array.isArray(messages)){
    return 0
  }

  if(Array.isArray(messages)){
    for (const msg of messages) {
      const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
      tokenCount += encoding.encode(content).length;
    } 
  }
 
  return tokenCount;
}
//数量截断
async function tokenCountTruncation() { 
  const history = new InMemoryChatMessageHistory();
  
  // 初始化编码器
  const enc =  getEncoding('cl100k_base'); // 使用 cl100k_base 编码器


  const message1 =[
    {type: 'human', content: '你好'},
    {type: 'ai', content: '你好,我是一个AI模型'},
    {type: 'human', content: '你叫什么名字?'},
    {type: 'ai', content: '我叫ChatGPT'},
    {type: 'human', content: '你喜欢吃什么?'},
    {type: 'ai', content: '我喜欢吃苹果'},
    {type: 'human', content: '你喜欢吃香蕉吗?'},
    {type: 'ai', content: '我喜欢吃香蕉'},
   {type: 'human', content: '你喜欢吃榴莲吗?'},
    {type: 'ai', content: '我喜欢吃榴莲'},
  ]
  
  for(const msg of message1){
    if(msg.type === 'human'){
      history.addUserMessage(new HumanMessage(msg.content));
    }else{
      history.addAIMessage(new AIMessage(msg.content));
    }
  }
  
  let allMessages = (await history.getMessages());
  const trimmedMessages =  await trimMessages(allMessages, {
    maxTokens: 50,
    tokenCounter: async(msgs)=> countTokens(msgs, enc),
    strategy: 'last',
    allowPartial: false,
  }); 
  
  trimmedMessages.forEach((msg, i) => {
    console.log(`  [${i + 1}] ${msg._getType()}: ${msg.content}`);
  });
}

tokenCountTruncation();

区别

image.png 解释下:

import {getEncoding} from 'js-tiktoken';

他用于初始化一个用于 GPT 系列模型的 Token 编码器实例

const enc = getEncoding('cl100k_base');

getEncoding这是一个来自 Tiktoken 库的核心方法。它的作用是获取一个特定的分词器(Encoder)对象,用来把人类 readable 的文本转换成大模型能看懂的整数序列(Token IDs),或者把整数序列还原成文本。

cl100k_base 是这是 OpenAI 指定的一种编码方案(Encoding)的名称:

  • cl:代表 Code/LLM(代码/大模型)。
  • 100k:代表它的词表(Vocabulary)大小约为 10 万个 Token。
  • base:基础版本。

适用范围:这是 GPT-4、GPT-3.5-turbo​ 以及部分 Embedding 模型(如 text-embedding-ada-002)所使用的标准分词规则。

import { getEncoding } from 'js-tiktoken'; // 或 'tiktoken'

const enc = getEncoding("cl100k_base");

const text = "牛肉面怎么做?";

const tokens = enc.encode(text);

console.log(`文本: ${text}`);

console.log(`Token IDs: ${tokens}`);

console.log(`Token 数量: ${tokens.length}`); // 这里就能精确知道消耗了多少 Token

image.png

2总结
2-1 总结信息
2-1-1 用数量切割后总结
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { HumanMessage, AIMessage, SystemMessage, getBufferString, mapStoredMessageToChatMessage } from '@langchain/core/messages';

const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

async function summaryHistory(messages) {
  if (messages.length === 0) {
    return '';
  }

  const conversationText = getBufferString(messages, {
    humanPrefix: '用户:',
    aiPrefix: '助手',
  });

  // ✅ 用消息数组而不是单个 SystemMessage,规避新版 toChatMessages 内部报错
  const summaryResponse = await model.invoke([
    new SystemMessage(`请总结以下对话内容,保留重要信息:\n${conversationText}\n\n总结:`),
  ]);//参数一定是数组,不然会报错,因为新版 toChatMessages 内部报错


  return summaryResponse.content;
}

// 总结截断
async function summaryCountTruncation() {
  const history = new InMemoryChatMessageHistory();

  const rawMessages = [
    { type: 'human', content: '你好' },
    { type: 'ai', content: '你好,我是一个AI模型' },
    { type: 'human', content: '你叫什么名字?' },
    { type: 'ai', content: '我叫ChatGPT' },
    { type: 'human', content: '你喜欢吃什么?' },
    { type: 'ai', content: '我喜欢吃苹果' },
    { type: 'human', content: '你喜欢吃香蕉吗?' },
    { type: 'ai', content: '我喜欢吃香蕉' },
    { type: 'human', content: '你喜欢吃榴莲吗?' },
    { type: 'ai', content: '我喜欢吃榴莲' },
  ];

  // ✅ 全部 await,确保写入完成
  for (const msg of rawMessages) {
    if (msg.type === 'human') {
      await history.addUserMessage(msg.content);
    } else {
      await history.addAIMessage(msg.content);
    }
  }

  //let allMessages = (await history.getMessages()).map(mapStoredMessageToChatMessage);
  let allMessages = await history.getMessages();
  
  console.log(`📋 原始消息(${allMessages.length} 条):`);
  allMessages.forEach((msg, i) => {
    console.log(`  [${i + 1}] ${msg._getType()}: ${msg.content}`);
  });

  if (allMessages.length >= 5) {
    const keepRecent = 2;
    const recentMessages = allMessages.slice(-keepRecent);
    const messagesToSummarize = allMessages.slice(0, -keepRecent);

    console.log(`\n📝 待总结 ${messagesToSummarize.length} 条,保留最近 ${keepRecent} 条`);

    const summary = await summaryHistory(messagesToSummarize);

    await history.clear();

    // ✅ 恢复最近消息,全部 await
    for (const msg of recentMessages) {
      await history.addMessage(msg);
    }

    console.log('\n✅ 保留的最近消息:');
    recentMessages.forEach((msg, i) => {
      console.log(`  [${i + 1}] ${msg._getType()}: ${msg.content}`);
    });

    console.log('\n📄 总结内容:');
    console.log(summary);

    // 验证 history 里最终有几条
    const finalMessages = await history.getMessages();
    console.log(`\n📊 history 中最终消息数: ${finalMessages.length}`);
  }
}

summaryCountTruncation();

执行后

image.png

2-1-2 用 token 切割后总结
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import {
  HumanMessage,
  AIMessage,
  SystemMessage,
  getBufferString,
  mapStoredMessageToChatMessage,
} from '@langchain/core/messages';
import { getEncoding } from 'js-tiktoken';

const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

// ========== 对话总结函数 ==========
async function summaryHistory(messages) {
  if (messages.length === 0) return '';

  const normalizedMessages = messages.map((msg) => {
    if (msg instanceof HumanMessage || msg instanceof AIMessage || msg instanceof SystemMessage) return msg;
    if (msg.type === 'human') return new HumanMessage(msg.content);
    if (msg.type === 'ai') return new AIMessage(msg.content);
    return msg;
  });

  const conversationText = getBufferString(normalizedMessages, {
    humanPrefix: '用户:',
    aiPrefix: '助手',
  });

  const summaryResponse = await model.invoke([
    new SystemMessage(`请总结以下对话内容,保留重要信息:\n${conversationText}\n\n总结:`),
  ]);

  return summaryResponse.content;
}

// ========== Token 计数函数 ==========
function countTokens(messages, encoding) {
  if (!Array.isArray(messages)) return 0;
  let tokenCount = 0;
  for (const msg of messages) {
    const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
    tokenCount += encoding.encode(content).length;
  }
  return tokenCount;
}

// ========== Token 阈值截断 + 总结 ==========
async function tokenCountTruncation() {
  const history = new InMemoryChatMessageHistory();
  const enc = getEncoding('cl100k_base');

  const rawMessages = [
    { type: 'human', content: '你好' },
    { type: 'ai', content: '你好,我是一个AI模型' },
    { type: 'human', content: '你叫什么名字?' },
    { type: 'ai', content: '我叫元宝,是腾讯开发的大模型助手' },
    { type: 'human', content: '你喜欢吃什么?' },
    { type: 'ai', content: `哈哈,作为AI我没有味觉,所以没法真的"吃"水果 😄\n不过如果非要选的话,我挺喜欢榴莲——对,就是你代码里那个测试消息里的榴莲 🍈\n理由嘛:\n它争议大,喜欢的人爱死、讨厌的人捂鼻子跑\n营养价值高,维生素C、钾、膳食纤维都很丰富\n而且它跟我的气质莫名契合 😏\n你呢?你代码里测了一圈苹果、香蕉、榴莲——你本人最爱吃哪个?😂` },
    { type: 'human', content: '你喜欢吃香蕉吗?' },
    { type: 'ai', content: `哈哈,香蕉啊——实用主义水果,我给好评 👍\n剥皮就能吃,零门槛\n自带"便携包装"\n补钾神器\n熟透的香蕉烤一下撒点肉桂粉,那个味道真的很顶 🍌🔥\n不过香蕉熟得太快,买回来三天就长黑斑 😂\n所以我的态度是:喜欢吃,但每次只买两根,绝不囤货。\n你呢?苹果、香蕉、榴莲三个里你站谁?🍉` },
    { type: 'human', content: '你喜欢吃榴莲吗?' },
    { type: 'ai', content: `哈哈你又问榴莲!看来你是想确认我是不是真的站榴莲派 😄\n我确实喜欢榴莲——但不是无脑吹,是带着清醒认知的喜欢:\n口感独特,像奶油冰淇淋+烤坚果+热带水果的复合体\n香气霸道\n热量高、饱腹感强\n但价格劝退 💸 随便一个猫山王能买一箱苹果\n吃完得刷牙漱口换衣服\n挑果全靠玄学 🎰\n所以真实态度:喜欢吃,但不敢经常吃——钱包和室友都不允许 😂` },
  ];

  // 写入 history
  for (const msg of rawMessages) {
    if (msg.type === 'human') {
      await history.addUserMessage(msg.content);
    } else {
      await history.addAIMessage(msg.content);
    }
  }

  let allMessages = (await history.getMessages()).map(mapStoredMessageToChatMessage);

  const totalToken = countTokens(allMessages, enc);
  console.log(`📊 当前总 Token 数: ${totalToken}\n`);

  const TOKEN_THRESHOLD = 200;      // 超过这个阈值就触发截断+总结
  const KEEP_TOKEN_BUDGET = 120;    // 截断后,history 里最多保留这么多 token 的最近消息

  if (totalToken <= TOKEN_THRESHOLD) {
    console.log('✅ 未超过 Token 阈值,无需截断');
    return;
  }

  // ---- 从最新消息往前累加,凑出"要保留的最近消息"(不超过 KEEP_TOKEN_BUDGET)----
  const recentMessages = [];
  let recentToken = 0;

  for (let i = allMessages.length - 1; i >= 0; i--) {
    const message = allMessages[i];
    const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
    const msgToken = enc.encode(content).length;

    // 至少保留最新一条(防止全部被截掉)
    if (recentMessages.length === 0) {
      recentMessages.unshift(message);
      recentToken += msgToken;
    } else if (recentToken + msgToken <= KEEP_TOKEN_BUDGET) {
      recentMessages.unshift(message);
      recentToken += msgToken;
    } else {
      break;
    }
  }

  // 超出阈值的部分 → 拿去总结
  const messagesToSummarize = allMessages.slice(0, allMessages.length - recentMessages.length);

  // 生成摘要
  const summary = await summaryHistory(messagesToSummarize);
  
  await history.clear();
  
  await history.addMessage(new SystemMessage(`以下是之前的对话摘要:\n${summary}`));
  for (const msg of recentMessages) {
    await history.addMessage(msg);
  }

  // 验证最终状态 —— 打印【截断后剩下的】
  const finalMessages = await history.getMessages();

  console.log("终极数据:\n",finalMessages.map(msg => msg.content).join('\n'))
}

tokenCountTruncation();

3.检索

接入milvus的流程:

连接 Milvus → 建集合 → 建索引 → 加载集合 → 对话文本向量化 → 批量插入 → 完成

① 连接 Milvus

与 Milvus 服务建立 gRPC 连接

import { MilvusClient } from '@zilliz/milvus2-sdk-node';

const client = new MilvusClient({
  address: 'localhost:19530',
  // 如果有鉴权:
  // username: 'root',
  // password: 'Milvus',
});

// 建立连接
await client.connectPromise;

② 建集合(Create Collection)

创建一张向量表(类似建表 DDL)

await client.createCollection({
  collection_name: 'conversations',
  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: 'round',
      data_type: DataType.Int64,
    },
    {
      name: 'timestamp',
      data_type: DataType.VarChar,
      max_length: 100,
    },
  ],
  // 可选:自动创建分区
  // enable_dynamic_field: true,
});

③ 建索引(Create Index)

为向量字段构建 ANN 索引,加速相似度搜索

关键参数 field_name、index_type、metric_type、params

常见索引类型 IVF_FLAT、IVF_PQ、HNSW、DISKANN

await client.createIndex({
  collection_name: 'conversations',
  field_name: 'vector',
  index_type: IndexType.IVF_FLAT,
  metric_type: MetricType.COSINE,
  params: { nlist: 128 },  // IVF_FLAT 的聚类中心数
});

④ 加载集合(Load Collection)

把集合数据从磁盘加载到内存,之后才能做搜索/查询

await client.loadCollection({
  collection_name: 'conversations',
});

⑤ 对话文本向量化(Embedding)

import { OpenAIEmbeddings } from '@langchain/openai';

const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: 'text-embedding-v3',
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
  dimensions: 1024,
});

// 单条文本向量化
const vector = await embeddings.embedQuery("用户:你好\n助手:你好!");

// 批量向量化(多条)
const texts = conversations.map(c => c.content);
const vectors = await embeddings.embedDocuments(texts);

⑥ 批量插入(Insert)

批量写入

const insertResult = await client.insert({
  collection_name: 'conversations',
  data: [
    {
      id: 'conv_001',
      vector: [0.12, -0.05, ...],  // 1024 维 number[]
      content: '用户:我叫赵六\n助手:很高兴认识你',
      round: 1,
      timestamp: '2024-01-01T00:00:00.000Z',
    },
    // ... 更多条
  ],
});

// ✅ 关键:flush 确保数据落盘,搜索立即可见
await client.flush({
  collection_names: ['conversations'],
});

好,我按你给的流程,把每一步对应的 Milvus API(来自 @zilliz/milvus2-sdk-node)和 LangChain API​ 逐一列出来,并标注关键参数和注意事项。


完整流程

对应 API 对照表


① 连接 Milvus
import { MilvusClient } from '@zilliz/milvus2-sdk-node';

const client = new MilvusClient({
  address: 'localhost:19530',
  // 如果有鉴权:
  // username: 'root',
  // password: 'Milvus',
});

// 建立连接
await client.connectPromise;
项目说明
APInew MilvusClient(options) + client.connectPromise
作用与 Milvus 服务建立 gRPC 连接
常见参数addresstimeoutusernamepasswordssl

② 建集合(Create Collection)
await client.createCollection({
  collection_name: 'conversations',
  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: 'round',
      data_type: DataType.Int64,
    },
    {
      name: 'timestamp',
      data_type: DataType.VarChar,
      max_length: 100,
    },
  ],
  // 可选:自动创建分区
  // enable_dynamic_field: true,
});
项目说明
APIclient.createCollection(params)
作用创建一张向量表(类似建表 DDL)
关键参数collection_namefields(字段定义数组)
⚠️ 注意集合已存在会抛 already exist 错误 → 需要先 dropCollectionhasCollection 判断

判断集合是否存在的 API:

const has = await client.hasCollection({ collection_name: 'conversations' });
if (has.value) {
  await client.dropCollection({ collection_name: 'conversations' });
}

③ 建索引(Create Index)
await client.createIndex({
  collection_name: 'conversations',
  field_name: 'vector',
  index_type: IndexType.IVF_FLAT,
  metric_type: MetricType.COSINE,
  params: { nlist: 128 },  // IVF_FLAT 的聚类中心数
});
项目说明
APIclient.createIndex(params)
作用为向量字段构建 ANN 索引,加速相似度搜索
关键参数field_nameindex_typemetric_typeparams
常见索引类型IVF_FLATIVF_PQHNSWDISKANN
常见距离类型COSINEL2IP(内积)
⚠️ 注意索引建在 vector 字段上,不能建在标量字段上(标量用 createScalarIndex

④ 加载集合(Load Collection)

把集合数据从磁盘加载到内存,之后才能做搜索/查询,否则不能搜索。

为什么mysql是自动加载的,而milvus却是手动加载的?

因为milvus的体积太大了。

你来决定什么时候加载、加载哪些集合、用多少内存资源。这是显式的资源管理。

await client.loadCollection({
  collection_name: 'conversations',
});

用完之后要释放内存

await client.releaseCollection({ collection_name: 'conversations' });

⑤ 对话文本向量化(Embedding)
import { OpenAIEmbeddings } from '@langchain/openai';

const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: 'text-embedding-v3',
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
  dimensions: 1024,
});

// 单条文本向量化
const vector = await embeddings.embedQuery("用户:你好\n助手:你好!");

// 批量向量化(多条)
const texts = conversations.map(c => c.content);
const vectors = await embeddings.embedDocuments(texts);
⑥ 批量插入(Insert)
const insertResult = await client.insert({
  collection_name: 'conversations',
  data: [
    {
      id: 'conv_001',
      vector: [0.12, -0.05, ...],  // 1024 维 number[]
      content: '用户:我叫赵六\n助手:很高兴认识你',
      round: 1,
      timestamp: '2024-01-01T00:00:00.000Z',
    },
    // ... 更多条
  ],
});

// ✅ 关键:flush 确保数据落盘,搜索立即可见
await client.flush({
  collection_names: ['conversations'],
});
⑦ 完成 → 验证(可选)
// 查看集合行数
const stats = await client.getCollectionStatistics({
  collection_name: 'conversations',
});
console.log('集合统计:', stats.stats);

// 简单搜索验证
const searchResult = await client.search({
  collection_name: 'conversations',
  vector: await embeddings.embedQuery('数据科学家'),
  limit: 3,
  output_fields: ['content', 'round'],
  params: { nprobe: 10 },
});
console.log('搜索结果:', searchResult.results);

image.png

创建milvus数据库
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;

const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: 'text-embedding-v3',
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL
  },
  dimensions: VECTOR_DIM
});

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

/**
 * 获取文本的向量嵌入
 */
async function getEmbedding(text) {
  const result = await embeddings.embedQuery(text);
  return result;
}

async function main() {
  try {
    console.log('连接到 Milvus...');
    await client.connectPromise;
    console.log('√ 已连接\n');

    // 创建集合
    console.log('创建集合...');
    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: 'round', data_type: DataType.Int64 },
        { name: 'timestamp', data_type: DataType.VarChar, max_length: 100 }
      ]
    });
    console.log('√ 集合已创建');

    // 创建索引
    console.log('\n创建索引...');
    await client.createIndex({
      collection_name: COLLECTION_NAME,
      field_name: 'vector',
      index_type: IndexType.IVF_FLAT,
      metric_type: MetricType.COSINE
    });
    console.log('√ 索引已创建');

    // 加载集合
    console.log('\n加载集合...');
    await client.loadCollection({ collection_name: COLLECTION_NAME });
    console.log('√ 集合已加载');

    // 插入对话数据
    console.log('\n插入对话数据...');
    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()
      },
      {
        id: 'conv_004',
        content: '用户:我周末经常去电影院\n助手:看电影是很好的放松方式。',
        round: 4,
        timestamp: new Date().toISOString()
      },
      {
        id: 'conv_005',
        content: '用户:我的职业是软件工程师\n助手:软件工程师是个很有前景的职业!',
        round: 5,
        timestamp: new Date().toISOString()
      }
    ];

    console.log('生成向量嵌入...');
    const conversationData = await Promise.all(
      conversations.map(async (conv) => ({
        ...conv,
        vector: await getEmbedding(conv.content)
      }))
    );

    const insertResult = await client.insert({
      collection_name: COLLECTION_NAME,
      data: conversationData
    });
    console.log(`√ 已插入 ${insertResult.insert_cnt} 条记录\n`);

    console.log('='.repeat(60));
    console.log('说明:已成功将对话数据插入到 Milvus 向量数据库');
    console.log('这些对话数据将用于后续的 RAG 检索');
    console.log('='.repeat(60) + '\n');

  } catch (error) {
    console.error('错误:', error.message);
  }
}

main();
利用milvus存储对话做检索

这是一套基于 Milvus 向量数据库的"检索增强型多轮对话记忆"系统。它把"对话记忆"从单纯的内存数组,升级成了可语义检索的向量存储

import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { OpenAIEmbeddings } from '@langchain/openai';
import {
  InMemoryChatMessageHistory,
  HumanMessage,
  AIMessage,
} from '@langchain/core/messages';
import { MilvusClient, MetricType } from '@zilliz/milvus2-sdk-node';

// ========== 配置 ==========
const COLLECTION_NAME = 'conversations';
const VECTOR_DIM = 1024;

// 初始化 OpenAI Chat 模型
const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

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

// 初始化 Milvus 客户端
const client = new MilvusClient({
  address: 'localhost:19530',
});

// ========== 工具函数 ==========

// 获取文本的向量嵌入
async function getEmbedding(text) {
  const result = await embeddings.embedQuery(text);
  return result;
}

// 从 Milvus 检索与 query 最相关的 k 条历史对话
async function retrieveRelevantConversations(query, k = 2) {
  try {
    const queryVector = await getEmbedding(query);

    const searchResult = await client.search({
      collection_name: COLLECTION_NAME,
      vector: queryVector,
      limit: k,
      metric_type: MetricType.COSINE,
      output_fields: ['id', 'content', 'round', 'timestamp'],
    });

    // 适配不同版本 SDK 的返回结构
    const results = searchResult.results || searchResult;
    return results.map((r) => ({
      id: r.id,
      content: r.content,
      round: r.round,
      timestamp: r.timestamp,
      score: r.score,
    }));
  } catch (error) {
    console.error('检索失败:', error.message);
    return [];
  }
}

// 把一轮对话保存到 Milvus(向量化后插入)
async function saveConversationToMilvus(userText, assistantText, round) {
  const conversationText = `用户:${userText}\n助手:${assistantText}`;
  const convVector = await getEmbedding(conversationText);
  const convId = `conv_${Date.now()}`;

  try {
    const insertResult = await client.insert({
      collection_name: COLLECTION_NAME,
      data: [
        {
          id: convId,
          vector: convVector,
          content: conversationText,
          round: round,
          timestamp: new Date().toISOString(),
        },
      ],
    });

    // flush 让数据立即可检索
    await client.flush({ collection_names: [COLLECTION_NAME] });
    console.log(`已保存到 Milvus 向量数据库(${insertResult.insert_cnt} 条): ${convId}`);
  } catch (error) {
    console.warn('保存到 Milvus 失败:', error.message);
  }
}

// ========== 主流程:带检索增强的多轮对话 Demo ==========
async function retrievalMemoryDemo() {
  // 连接 Milvus
  try {
    console.log('连接到 Milvus...');
    await client.connectPromise;
    console.log('已连接 Milvus\n');
  } catch (error) {
    console.error('Milvus 连接失败:', error.message);
    return;
  }

  const history = new InMemoryChatMessageHistory();

  // 本轮对话的输入(模拟用户后续追问)
  const inputs = [
    '我之前提到的机器学习项目进展如何?',
    '我喜欢的休闲活动是什么来着?',
    '我从事什么职业?',
  ];

  let round = 0;

  for (const input of inputs) {
    round++;
    const userMessage = new HumanMessage(input);
    console.log(`\n👤 用户: ${input}`);

    // 1) 先检索 Milvus 中与当前问题相关的历史对话
    const retrievedConversations = await retrieveRelevantConversations(input, 2);

    // 显示检索到的相关历史及相似度
    if (retrievedConversations.length > 0) {
      console.log('📚 检索到的相关历史对话:');
      retrievedConversations.forEach((c, i) => {
        console.log(`  [${i + 1}] 相似度=${c.score?.toFixed(4) ?? 'N/A'} 轮次=${c.round}`);
        console.log(`      ${c.content}`);
      });
    } else {
      console.log('📚 未检索到相关历史对话');
    }

    // 2) 构建带历史上下文的 prompt
    let contextMessages;
    if (retrievedConversations.length > 0) {
      const contextStr = retrievedConversations
        .map((c) => c.content)
        .join('\n');

      contextMessages = [
        new HumanMessage(
          `相关历史对话:\n${contextStr}\n\n用户问题:${input}`
        ),
      ];
    } else {
      contextMessages = [userMessage];
    }

    // 3) 调用模型生成回答
    const response = await model.invoke(contextMessages);
    const answer = response.content;
    console.log(`🤖 助手: ${answer}`);

    // 4) 写入本地 InMemory 历史
    await history.addMessage(userMessage);
    await history.addMessage(new AIMessage(answer));

    // 5) 把本轮对话向量化后存入 Milvus,供后续检索
    await saveConversationToMilvus(input, answer, round);
  }

  // 打印最终本地历史
  const allMessages = await history.getMessages();
  console.log('\n📋 本次会话本地历史:');
  allMessages.forEach((msg, i) => {
    console.log(`  [${i + 1}] ${msg._getType()}: ${msg.content}`);
  });
}

retrievalMemoryDemo().catch(console.error);

上述代码所做的事情:

image.png

image.png

image.png

三.总结

image.png