大模型为什么能记住上一轮?从 InMemoryChatMessageHistory 看对话 Memory

大模型为什么能记住上一轮?从 InMemoryChatMessageHistory 看对话 Memory

很多"多轮对话"看起来像模型记住了上下文,实际却是应用在做一件非常明确的事:保存消息,再把消息放回下一次请求。

本文根据 src/history.mjsreadme.md,拆解一个 LangChain 内存 Memory 示例。你会看到 HumanMessageAIMessage 如何进入历史,第二轮请求如何重新组装上下文,以及为什么 getMessages() 返回数组后不能再访问 .array。代码与模型调用运行未验证。

先建立一个正确的心智模型

把模型看成一个只处理当前输入的函数:

text 复制代码
本次请求的 messages → 模型 → 本次 AIMessage

如果下一次请求只发送新的问题,上一轮内容就不在输入里。Memory 的作用是把流程改成:

text 复制代码
HumanMessage ─┐
AIMessage    ├→ History → 下一次 messages → 模型
HumanMessage ─┘

这也是为什么 Memory 更准确地说是"上下文管理",而不是模型凭空获得了永久记忆。

这个示例的最小闭环

源码用 InMemoryChatMessageHistory 建立当前进程中的消息容器:

javascript 复制代码
const history = new InMemoryChatMessageHistory();

它有两个关键动作:

  • addMessage(message):保存一条消息;
  • getMessages():拿到全部消息组成的数组。

消息带有 typecontent 等属性。系统消息负责设定角色,用户消息表达问题,AI 消息保存模型返回内容。工具消息也可以进入同样的消息历史,但本示例没有展开 Tool 调用。

第二轮为什么能够理解"好吃吗"

第一轮不是直接调用模型,而是先保存用户问题:

javascript 复制代码
await history.addMessage(userMessage1);
const message1 = [
  systemMessage,
  ...(await history.getMessages()),
];
const response1 = await model.invoke(message1);
await history.addMessage(response1);

这里的最后一步决定了下一轮能否看到 AI 的上一条回答。invoke 只负责返回结果,不会替应用自动维护这份 history。

第二轮的逻辑是相同的:

javascript 复制代码
await history.addMessage(userMessage2);
const message2 = [
  systemMessage,
  ...(await history.getMessages()),
];
const response2 = await model.invoke(message2);
await history.addMessage(response2);

到这里,历史顺序是"用户、AI、用户、AI"。第二轮的"好吃吗"之所以有上下文,是因为第一轮消息被重新放进了 message2

一个很典型的数组错误

示例最后会读取全部消息并打印:

javascript 复制代码
const allMessages = await history.getMessages();

allMessages.forEach((element, index) => {
  const prefix = element.type === "human" ? "用户:" : "助手:";
  console.log(`${index + 1}. ${prefix}${element.content}`);
});

getMessages() 的返回值已经是数组,所以直接调用 forEach。如果写成:

javascript 复制代码
allMessages.array.forEach(...);

那么 allMessages.arrayundefined,自然无法读取 forEach

还有一个小细节:想打印序号,就必须接收 forEach 的第二个参数 index。它不是自动存在的变量。

Promise 在这条链路里做了什么

示例函数声明为 async,因此调用 inMemoryDemo() 得到的是 Promise。函数内部等待三类异步动作:保存消息、读取消息、调用远程模型。

javascript 复制代码
async function inMemoryDemo() {
  await history.addMessage(userMessage1);
  const messages = await history.getMessages();
  const response = await model.invoke(messages);
}

入口的 .catch().finally() 分别承担错误处理与收尾:

javascript 复制代码
inMemoryDemo()
  .catch(console.error)
  .finally(() => console.log("done"));

所以日志里出现 done,只能证明异步流程走到了 finally;如果前面发生异常,它仍然会打印。

从 Demo 到生产,还缺哪些东西

内存方案的优点是简单,缺点也很明确:数据只存在当前进程。进程重启、实例切换或需要多用户并发时,不能只依赖这一个容器。

根据笔记,继续演进至少要回答这些问题:

问题 当前示例 后续设计方向
历史存在哪里 Node.js 内存 数据库或其他持久化存储
如何区分用户 未涉及 会话 ID 与权限边界
历史太长怎么办 全量传入 截断、总结或检索
重启后能恢复吗 不能保证 持久化与恢复流程
多实例共享吗 未解决 统一存储或状态管理

材料没有给出生产数据库、持久化组件或上下文压缩方案,因此这里不把它们包装成已实现功能。

一份可复用的排错顺序

  1. 确认用户消息在 model.invoke 前已经 addMessage
  2. 确认模型响应成功后又执行了 addMessage(response)
  3. 打印 await history.getMessages(),检查消息顺序。
  4. 检查组装请求时是否使用了展开运算符 ...
  5. 遍历时直接调用 allMessages.forEach,不要添加 .array
  6. 若重启后数据消失,确认这是否正是 InMemory 的预期行为。
  7. 若历史变长,重新评估上下文窗口、成本和压缩策略。

结语:Memory 是一条可观察的消息链

理解 LangChain Memory,先不要把它想成神秘的"记忆能力"。在这个示例里,它就是一条可以检查的消息链:消息被添加,历史被读取,请求被组装,AI 回复再被写回。

这条链路稳定后,持久化、总结和检索只是对"保存什么、取出什么、传给模型什么"的进一步工程化。真正需要先掌握的,是上下文从哪里来,以及它有没有在下一轮请求中被带回去。

标签:LangChain, AI Agent, Memory, JavaScript

相关推荐
ovO4 小时前
DeepSeek Harness 源码解读(五):工具明明并发执行,结果为什么还按顺序写入
开源·agent·deepseek
dong_junshuai5 小时前
每天一个开源项目#86 ECC:245K星的 Agent 工程操作层
开源·github·agent
阿里云大数据AI技术5 小时前
Agentic Search 2.0:从单轮对话迈向企业级 AI 搜索自动驾驶Agent
人工智能·elasticsearch·agent
狂师5 小时前
阿里开源:skill-up,一款Agent Skill 评测工具!
人工智能·开源·agent
武子康5 小时前
同一套 Agent Runtime,为什么 Web、Headless 和 Python SDK 仍然不是同一个产品
人工智能·llm·agent
还有多久拿退休金7 小时前
不调多模态,纯文本大模型如何给系统操作配上截图
前端·llm·aigc
阿里云云原生7 小时前
实战演示:利用 LoongSuite Pilot 实现 Claude Code Webhook 数据的旁路上报
agent
明月_清风7 小时前
发现一个超系统的 AI Agent 学习地图 —— Agent Atlas 推荐
人工智能·后端·agent
DigitalOcean7 小时前
AI Agent如何降本?从五层技术栈到三种推理服务
llm·agent