ai agent -- Memory汇总

一. Memory是什么?

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

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

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

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

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

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

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

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

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

二. 使用案例

1.安装包

js 复制代码
pnpm install dorenv @langchain/core @langchain/openai @langchain/community langchain

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

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

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

关键要素

  • @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存储方式

不同存储方式的实现代码

存进内存
js 复制代码
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();

存进文件
js 复制代码
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();

和上述代码的区别

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

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

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

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

message的处理方式

1.截断

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

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

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

1-1 截断数量
js 复制代码
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
js 复制代码
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();

区别

解释下:

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)所使用的标准分词规则。

js 复制代码
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
2总结
2-1 总结信息
2-1-1 用数量切割后总结
js 复制代码
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();

执行后

2-1-2 用 token 切割后总结
相关推荐
ITresearchGuest1 小时前
AI 焦虑下,前端该何去何从
前端·人工智能
Codiggerworld2 小时前
Chrome DevTools 隐藏神技:这 5 个调试技巧你可能从来没用过
前端·chrome·chrome devtools
旋生万物2 小时前
【终极实战】用Python从零“生成“一个宇宙:螺旋干涉模型的代码实现
开发语言·前端·人工智能·react.js·php·wpf
IT_陈寒2 小时前
被Java的final坑惨了,这些细节你可能也忽略了
前端·人工智能·后端
kyriewen3 小时前
我带着DeepSeek Harness跑了一周真实需求——这份避坑速查表请收好
前端·ai编程·deepseek
计算机魔术师3 小时前
OpenAI不单独发o3了,Altman说GPT-5才是终局
前端
一座古城3 小时前
Claude Code 架构源码解析:AI 应用开发的范式跃迁
前端·后端
K哥爬虫3 小时前
【JS 逆向百例】Vaptcha V4 手势验证码逆向分析
前端·后端
用户921080262863 小时前
复盘 AI Coding 工作台的流式链路:从 Sender 输入到 SSE 分流,再到 BubbleList 和右侧预览
前端