一. 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主包提供高层 API :createAgent、initChatModel、中间件系统等现代 Agent 抽象。- Provider 包(如
@langchain/openai)只依赖 core ,不依赖langchain主包------所以你完全可以只装@langchain/core + @langchain/openai做轻量调用,不必装langchain。 - 旧版 API 在 LangChain v1 中已迁移到
@langchain/classic:如果你在老教程里看到LLMChain、ConversationChain、旧式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();
执行后
