你的 AI 为什么总答非所问?缺的不是智商,是“记忆系统”

用 LangChain 实现有记忆的多轮对话:给 AI 配一个"数字笔记本"

大模型像一位健忘的天才------聪明但记不住事。而 LangChain,就是它的专属笔记本和秘书。

在构建聊天机器人时,我们常遇到一个尴尬场景:

你刚告诉 AI "我叫田晨枫",下一秒问"我叫什么",它却一脸茫然。

这不是它笨,而是大模型天生"无状态" ------每次对话都像第一次见面。

要让它记住你,我们需要一套机制:记录、携带、更新你的对话历史。

LangChain 正是为此而生。下面,我们通过代码 + 比喻,一步步搭建这个"记忆系统"。


一、问题演示:没有笔记本的"健忘天才"

js 复制代码
const model = new ChatDeepSeek({
    model:'deepseek-chat',
    temperature:0,
});

const res = await model.invoke('我叫田晨枫,喜欢吃鸡腿饭')
console.log(res.content);

console.log('---------------');

const res2 = await model.invoke('我叫什么名字')
console.log(res2.content);

🧠 大模型:没有笔记本的演讲者

想象一位演讲者站在台上:

  • 第一次上台说:"我是田晨枫。"
  • 下台后,没人给他发讲稿
  • 第二次上台,他只能凭空回忆:"刚才我说啥了?"

大模型就是这样------每次调用都是全新开始,除非你主动把"讲稿"(历史)塞给它。


二、手动维护历史:你自己当秘书

jsx 复制代码
let chatHistory = [];

chatHistory.push(new HumanMessage('我叫田晨枫...'));
const aiReply = await model.invoke(chatHistory);
chatHistory.push(aiReply);

// 下次再调用时带上整个 chatHistory

📝 你亲自当秘书

  • 你拿个小本本(chatHistory 数组)
  • 每次用户说话,你记下来
  • 每次 AI 回答,你也记下来
  • 下次提问前,把整本笔记递给 AI

✅ 能工作,但:

  • 多用户?你得准备多个本子,并贴上标签(user_a, user_b...)
  • 聊100轮?本子越来越厚,AI 看得眼花(Token 爆炸)

太累了!我们需要一个自动化的秘书。


三、LangChain 登场:给 AI 配一个智能秘书

LangChain 的 RunnableWithMessageHistory 就是这位秘书。我们来配置它:

步骤 1:设计"对话模板"------规定怎么读笔记

js 复制代码
import { ChatDeepSeek } from '@langchain/deepseek';
import{ChatPromptTemplate} from '@langchain/core/prompts';
// 带上历史记录的可运行对象
import { RunnableWithMessageHistory } from '@langchain/core/runnables';
// 内存中的对话历史记录
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import'dotenv/config';

const prompt = ChatPromptTemplate.fromMessages([
  ['system', '你是一个有记忆的助手'],
  ['placeholder', '{history}'], // ← 秘书在这里插入笔记内容
  ['human', '{input}']
]);

📖 会议议程模板

就像一场会议的固定流程:

  1. 主持人开场(system)
  2. 回顾上次会议纪要({history})
  3. 讨论新议题({input})

{history} 就是预留的"纪要插入位"。


步骤 2:组装基础工作流

js 复制代码
const runnable =prompt
.pipe((input) =>{ //debug 节点
    console.log(">>> 最终传给模型的信息(Prompt 内存)");
     console.log(input);
     return input;
})
.pipe(model);

这里我们手动添加了一个用于debug的节点,实际上它只是用来查看我们最终传给大模型的是什么。

它等价于下面这段代码

js 复制代码
const runnable = prompt.pipe(model);

⚙️ 核心工作台

这是 AI 的"办公桌":

  • 左边放议程模板(prompt)
  • 右边连着大脑(model)
    还没配秘书,所以不会自动填纪要。

步骤 3:准备"笔记本"------历史存储

js 复制代码
const messageHistory = new InMemoryChatMessageHistory();

📓 一个空白笔记本

这是秘书用来记对话的本子。

目前是"通用本子"------所有用户共用(后面我们会升级为每人一本)。


步骤 4:雇佣"智能秘书"------RunnableWithMessageHistory

js 复制代码
const chain = new RunnableWithMessageHistory({
  runnable,                  // ← 把办公桌交给秘书
  getMessageHistory: async () => messageHistory, // ← 告诉秘书去哪拿笔记本
  inputMessagesKey: 'input',     // ← 用户当前说的话叫 "input"
  historyMessagesKey: 'history'  // ← 笔记内容注入到模板的 "history" 位置
});

👨‍💼 正式聘用秘书

你对秘书说:

  • "这是你的工作台(runnable)"
  • "笔记本在这儿(messageHistory)"
  • "用户当前问题叫 input,上次的笔记叫 history,按模板填好就行"

这里就是对runable进行了重新包装:

  • getMessageHistory告诉它应该调用哪个历史对话(每段对话会有对话id,我们这里使用了同一个历史对话)
  • inputMessagesKey用来记录当前用户说的话
  • historyMessagesKey 将历史对话插入到对应位置 对于我们设定的模板你就理解了:
js 复制代码
const prompt = ChatPromptTemplate.fromMessages([
  ['system', '你是一个有记忆的助手'],
  ['placeholder', '{history}'], // ← 插入的历史对话
  ['human', '{input}']//<- 用户输入
]);

而当我们反复提问时就会变成这样

js 复制代码
[
  SystemMessage("你是一个有记忆的助手"),
  //对应的历史对话插入
  HumanMessage("我叫田晨枫,喜欢吃鸡腿饭"),
  AIMessage("好的,田晨枫!..."),
  //用户的提问
  HumanMessage("我叫什么名字")
]

从此,你只需说"问 AI 一个问题" ,秘书会自动:

  1. 打开笔记本,读取历史
  2. 按模板整理成完整议程
  3. 交给 AI 处理
  4. 把新对话记回笔记本

步骤 5:使用------你只需提问题!

js 复制代码
await chain.invoke({ input: '我叫田晨枫...' });
await chain.invoke({ input: '我叫什么名字?' }); // ✅ 正确回答!

💬老板只管下指令**

你作为老板,只需说:"帮我问 AI 我叫什么?"

秘书默默完成所有幕后工作,你甚至不用知道笔记本的存在。


四、升级:给每位用户配专属笔记本(多会话支持)

之前的笔记本是公用的。现在,我们让秘书管理多个带标签的笔记本

js 复制代码
const sessionStore = new Map(); // ← 一个文件柜,每个抽屉贴了标签

const getMessageHistory = async (sessionId) => {
  if (!sessionStore.has(sessionId)) {
    // 如果没这个用户的笔记本,就新建一本
    sessionStore.set(sessionId, new InMemoryChatMessageHistory());
  }
  return sessionStore.get(sessionId); // ← 返回对应笔记本
};

// 调用时指定用户 ID
await chain.invoke(
  { input: '我叫张三' },
  { configurable: { sessionId: 'user_zhang' } } // ← "请用张三的笔记本!"
);

🗂️ 带标签的文件柜

  • 文件柜(sessionStore)里有很多抽屉
  • 每个抽屉贴着用户 ID 标签(sessionId
  • 秘书根据你提供的标签,取出对应的笔记本
  • 张三和李四的对话完全隔离,互不干扰

五、防止笔记本太厚:自动清理旧记录

聊得越多,笔记本越厚。长期下去:

  • AI 阅读困难(上下文太长)
  • 成本飙升(Token 按字数计费)

解决方案:让秘书定期清理旧页

js 复制代码
const MAX_MESSAGES = 6; // 只保留最近 3 轮对话(6 条消息)

const getMessageHistory = async (sessionId) => {
  let history = sessionStore.get(sessionId) || new InMemoryChatMessageHistory();
  
  const messages = await history.getMessages();
  if (messages.length > MAX_MESSAGES) {
    // 清空笔记本,只抄写最后几页
    await history.clear();
    for (const msg of messages.slice(-MAX_MESSAGES)) {
      await history.addMessage(msg);
    }
  }
  
  sessionStore.set(sessionId, history);
  return history;
};

✂️ 秘书的"精简笔记"习惯

秘书有个规矩:

  • 笔记本超过 6 页,就撕掉前面的
  • 只保留最近 3 轮对话(足够理解上下文)
  • 既节省空间,又不影响工作

六、总结:LangChain 的记忆系统全景图

组件 比喻 作用
ChatPromptTemplate 会议议程模板 规定如何组织上下文
{history} 占位符 "上次会议纪要"位置 历史消息插入点
InMemoryChatMessageHistory 笔记本 存储对话记录
getMessageHistory 文件柜管理员 根据用户 ID 分发笔记本
RunnableWithMessageHistory 智能秘书 自动读写笔记、组装请求
sessionId 用户标签 区分不同用户的笔记本

大模型本身没有记忆,就像一位才华横溢却健忘的演讲者------每次登台都像初次见面。

而 LangChain 并没有改变模型的本质,而是在它身边搭建了一套完整的记忆支持系统

  • 用模板定义记忆格式(Prompt)
  • 用存储承载记忆内容(MessageHistory)
  • 用会话机制隔离记忆边界(sessionId)
  • 用自动包装实现记忆流转(RunnableWithMessageHistory)

更重要的是,这套系统是可插拔、可扩展、可控制的

你可以轻松替换内存存储为 Redis,可以加入自动摘要压缩长对话,也可以设定最大轮数防止 Token 膨胀。

真正的"AI 记忆",从来不是模型自己记住什么,而是我们为它构建了一个恰到好处的记忆环境。

LangChain 提供的,正是这样一套让记忆变得简单、可靠又高效的工程化方案。

相关推荐
智泊AI2 小时前
一文看懂AI大模型的核心模块:基于强化学习的偏好对齐原理及其应用
llm
CoderJia程序员甲3 小时前
GitHub 热榜项目 - 日榜(2025-12-24)
ai·开源·llm·github
3824278273 小时前
python:输出JSON
前端·python·json
2503_928411563 小时前
12.22 wxml语法
开发语言·前端·javascript
光影少年3 小时前
Vue2 Diff和Vue 3 Diff实现及底层原理
前端·javascript·vue.js
傻啦嘿哟3 小时前
隧道代理“请求监控”实战:动态调整采集策略的完整指南
前端·javascript·vue.js
JIngJaneIL3 小时前
基于java + vue个人博客系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
C_心欲无痕3 小时前
vue3 - readonly创建只读的响应式对象
前端·javascript·vue.js
Rabi'3 小时前
编译ATK源码
前端·webpack·node.js