LangChain Memory 实战:用 InMemoryChatMessageHistory 管理多轮对话
本文基于
ai/agent/memory/src/history.mjs与readme.md整理。核心问题是:大模型没有天然的上下文记忆,如何让第二轮问题理解第一轮对话?答案是由应用保存消息,并在下一次调用时重新传入。本文代码和模型调用运行未验证。
文章目录
- [LangChain Memory 实战:用 InMemoryChatMessageHistory 管理多轮对话](#LangChain Memory 实战:用 InMemoryChatMessageHistory 管理多轮对话)
-
- [一、Memory 到底解决什么问题](#一、Memory 到底解决什么问题)
- [二、`InMemoryChatMessageHistory` 的基本用法](#二、
InMemoryChatMessageHistory的基本用法) - 三、两轮对话的完整链路
- [四、为什么 `getMessages()` 不能写 `.array`](#四、为什么
getMessages()不能写.array) - [五、`async/await` 在 Memory 示例中的作用](#五、
async/await在 Memory 示例中的作用) - [六、Memory 的边界:临时记忆不等于长期记忆](#六、Memory 的边界:临时记忆不等于长期记忆)
- 七、常见错误排查表
- 八、部署前自检清单
- 结语
一、Memory 到底解决什么问题
大模型通常按当前请求中的消息生成答案。上一轮请求结束后,如果应用不保存并重新传入消息,模型就无法可靠知道之前发生过什么。
因此 Memory 的本质不是给模型增加永久记忆,而是管理发送给模型的上下文:
text
用户输入 → 保存 HumanMessage
模型输出 → 保存 AIMessage
下一轮请求 → 取出历史并重新传给模型
当历史越来越长,还要面对上下文窗口和调用开销。笔记中提到的方向包括截断、总结和检索;当前示例只实现了内存保存。
二、InMemoryChatMessageHistory 的基本用法
源码创建了一个内存历史容器:
javascript
const history = new InMemoryChatMessageHistory();
常用 API 可以这样理解:
| API | 作用 |
|---|---|
addMessage(message) |
向历史追加消息 |
getMessages() |
获取全部消息数组 |
model.invoke(messages) |
使用消息数组调用模型 |
消息不是普通字符串,而是带有类型和内容的对象,例如 HumanMessage、AIMessage、SystemMessage 和 ToolMessage。源码中可以通过 element.type 和 element.content 读取它们的类型与内容。
三、两轮对话的完整链路
第一轮的关键顺序是"先保存用户问题,再调用模型,最后保存模型回答":
javascript
const userMessage1 = new HumanMessage("你今天吃什么?");
await history.addMessage(userMessage1);
const message1 = [
systemMessage,
...(await history.getMessages()),
];
const response1 = await model.invoke(message1);
await history.addMessage(response1);
这里有一个容易忽略的点:model.invoke() 返回 AI 消息,但不会自动写入 history。必须显式执行:
javascript
await history.addMessage(response1);
第二轮继续追加用户消息:
javascript
const userMessage2 = new HumanMessage("好吃吗?");
await history.addMessage(userMessage2);
const message2 = [
systemMessage,
...(await history.getMessages()),
];
const response2 = await model.invoke(message2);
await history.addMessage(response2);
此时历史中有四条消息:
text
1. 第一轮用户消息
2. 第一轮 AI 回复
3. 第二轮用户消息
4. 第二轮 AI 回复
第二轮能够理解"好吃吗"所指的内容,原因不是模型永久记住了对话,而是应用把前面的消息重新放进了 message2。
四、为什么 getMessages() 不能写 .array
getMessages() 返回的本身就是数组,正确遍历方式是:
javascript
const allMessages = await history.getMessages();
allMessages.forEach((element, index) => {
const prefix = element.type === "human" ? "用户:" : "助手:";
console.log(`${index + 1}. ${prefix}${element.content}`);
});
错误写法如下:
javascript
allMessages.array.forEach(...);
allMessages.array 并不存在,因此值是 undefined,继续读取 .forEach 就会产生 TypeError。
另外,index 必须来自 forEach 的第二个回调参数:
javascript
(element, index) => {}
五、async/await 在 Memory 示例中的作用
addMessage()、getMessages() 和 model.invoke() 都使用了 await。这表示当前 async 函数要等待对应的 Promise 完成后,再继续后面的逻辑。
javascript
async function inMemoryDemo() {
await history.addMessage(userMessage1);
const messages = await history.getMessages();
const response = await model.invoke(messages);
}
async 函数调用后一定得到 Promise。入口处的链式处理:
javascript
inMemoryDemo()
.catch(console.error)
.finally(() => {
console.log("done");
});
.catch() 处理异常,.finally() 负责收尾。done 只能说明 Promise 流程结束,不能说明模型请求一定成功。
六、Memory 的边界:临时记忆不等于长期记忆
InMemoryChatMessageHistory 把数据放在当前 Node.js 进程的内存中,因此适合:
- 学习和 Demo;
- 单次运行的临时会话;
- 验证消息拼接和多轮调用流程。
它不自动解决:
- 程序重启后的数据恢复;
- 多用户会话隔离;
- 多实例之间共享历史;
- 上下文过长时的裁剪和总结;
- 长期记忆的检索策略。
如果要用于生产环境,需要进一步设计持久化存储、会话 ID、消息清理和上下文压缩方案。具体数据库和组件不在本次材料中,不能直接推断。
七、常见错误排查表
| 现象 | 原因 | 排查方向 |
|---|---|---|
| 第二轮不理解上一轮 | 没有传入历史,或没有保存 AI 回复 | 检查 addMessage(response) 和 message2 |
undefined.forEach |
错误访问 allMessages.array |
直接使用 allMessages.forEach |
| 序号报错 | 没有声明 index |
接收 forEach 第二个参数 |
| 重启后历史消失 | 使用的是内存存储 | 接入持久化方案 |
done 但请求失败 |
finally 总会执行 |
结合 catch 和具体错误判断 |
八、部署前自检清单
- 用户消息是否在调用模型前写入 history?
- AI 回复是否在模型调用成功后写入 history?
- 下一轮请求是否重新读取并展开历史数组?
- 是否直接对
getMessages()的返回值使用forEach? - 是否正确接收
forEach的index? - 是否明确 InMemory 只覆盖当前进程?
- 是否评估上下文窗口、成本和历史裁剪策略?
- 代码和模型调用是否完成实际验证?
结语
多轮对话的关键不是"模型自己记住了",而是应用维护了一条消息链。InMemoryChatMessageHistory 用很少的代码展示了这条链路:addMessage 负责保存,getMessages 负责取出,model.invoke 负责调用,下一轮再把历史重新组装进请求。
掌握这条最小闭环后,再向持久化、总结、截断和检索扩展,Memory 才能从 Demo 走向可维护的 Agent 系统。
标签:LangChain, Memory, JavaScript, 多轮对话, 大模型