让 AI 拥有“烂笔头”:用 LangChain 三件套实现真正有记忆的对话

86 阅读4分钟

“好记性不如烂笔头。”
——这句老话,放在大模型时代,竟然成了最佳工程实践。

你有没有遇到过这样的场景?

用户:“我叫太空人,喜欢吃喜之郎。”
AI:“好的,太空人!”
用户:“那我刚才说喜欢吃什么?”
AI:“抱歉,我不记得了。”

是不是很无奈?
其实不是 AI “笨”,而是它天生没有记忆——每次对话都是“第一次见面”。

那怎么办?
LangChain 给出的答案是:别指望它记性好,咱们自己给它配个“记事本”

而这个“记事本系统”,由三个关键组件组成:

  • InMemoryChatMessageHistory:真正的记事本
  • ChatPromptTemplate:记事本的书写格式规范
  • RunnableWithMessageHistory:规定“什么时候翻本子、抄哪页”的秘书

今天,我们就用这个生活化比喻,彻底搞懂 LangChain 的聊天记忆链。


一、第一步:先给 AI 配个“记事本”——InMemoryChatMessageHistory

大模型就像一个健忘的朋友,但我们可以给它配个随身小本子,把每次对话都记下来。

import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';

const messageHistory = new InMemoryChatMessageHistory();

这个 messageHistory 就是它的“记事本”。
每当你和 AI 聊一句,它就会自动记下两条:

  • 你说了什么(HumanMessage
  • AI 回了什么(AIMessage

比如聊完“我叫太空人,喜欢吃喜之郎”后,本子上就多了:

[  { role: 'human', content: '我叫太空人,喜欢吃喜之郎' },  { role: 'ai', content: '好的,太空人!' }]

注意:这个“本子”目前只存在内存里,程序一关就清空。
生产环境可以用 Redis、数据库等做持久化,但原理一样——先有本子,才能记事。


二、第二步:规定“怎么记、怎么读”——ChatPromptTemplate

有了本子,还得有书写规范。
你不能让 AI 看一堆乱七八糟的文字,它需要清晰的结构:

  • 哪句是系统指令?
  • 哪些是历史对话?
  • 哪句是当前问题?

这就是 ChatPromptTemplate 的作用——它定义了消息的排版格式。

import { ChatPromptTemplate } from '@langchain/core/prompts';

const prompt = ChatPromptTemplate.fromMessages([
  ['system', '你是一个有记忆的助手'],
  ['placeholder', '{history}'], // 这里要插入记事本内容
  ['human', '{input}']          // 当前用户输入
]);

关键就在 ['placeholder', '{history}'] ——
它相当于在模板里留了个插槽:“等会儿把记事本里的内容,按原格式插进来”。

为什么必须用 ChatPromptTemplate 而不是普通 PromptTemplate
因为记事本里存的是结构化消息对象(带 human/ai 角色),不是字符串。
只有 ChatPromptTemplate 能正确“展开”这些消息,生成模型能理解的上下文。

想象你在写会议纪要:
不能只写“他说了这个那个”,而要写“张三:……”、“李四:……”。
ChatPromptTemplate 就是那个排版模板。


三、第三步:安排一个“翻本子的秘书”——RunnableWithMessageHistory

光有本子和格式还不够。
谁来负责:

  • 调用前:去翻本子,把历史抄进提示?
  • 调用后:把新对话记回本子?

这时候,RunnableWithMessageHistory 就登场了——它就是那个贴心的秘书。

import { RunnableWithMessageHistory } from '@langchain/core/runnables';

const chain = new RunnableWithMessageHistory({
  runnable: prompt.pipe(model),           // 要执行的任务
  getMessageHistory: async () => messageHistory, // 翻哪个本子?
  inputMessagesKey: 'input',              // 用户当前问的是啥?
  historyMessagesKey: 'history',          // 历史记录放哪儿?
});

当你调用:

await chain.invoke({ input: "我刚才说喜欢吃什么?" });

这位“秘书”会自动:

  1. 打开 messageHistory 这个本子
  2. 把里面所有记录作为 { history: [...] } 传给你的 runnable
  3. 拿到 AI 回复后,把“用户新问题 + AI 新回答”记回本子

整个过程完全自动化,你只需要关心业务逻辑。


四、三者协作全景图

用一张流程图总结:

用户提问
    ↓
[RunnableWithMessageHistory] ← 秘书:我去翻本子!
    ↓
从 [InMemoryChatMessageHistory] 读取历史(记事本)
    ↓
注入 { input: "...", history: [Human, AI, ...] } 到 runnable
    ↓
[ChatPromptTemplate] ← 排版员:按格式插进去!
    ↓
生成完整消息列表 → 交给 ChatModel
    ↓
AI 回答
    ↓
[RunnableWithMessageHistory] ← 秘书:记回本子!
    ↓
更新 [InMemoryChatMessageHistory]

三者各司其职:

  • 记事本(InMemory...):存
  • 排版模板(ChatPromptTemplate):格式化
  • 秘书(RunnableWithMessageHistory):调度 + 自动读写

五、结语:AI 不需要好记性,但我们需要好设计

大模型永远不会记住你是谁——
但我们可以用“烂笔头”帮它记住。

InMemoryChatMessageHistory 是本子,
ChatPromptTemplate 是格式,
RunnableWithMessageHistory 是秘书。

三者配合,让无状态的 AI 表现出“有记忆”的智能。

而这,正是 LangChain 的魅力所在:
不幻想模型全能,而是用工程手段弥补它的短板。