在开发基于 LLM 的应用(如聊天机器人)时,开发者很快会遇到一个核心痛点:大模型默认是“失忆”的。

LLM 的 API 调用本质上和 HTTP 请求一样,都是无状态(Stateless)的。这意味着,无论你之前和它聊得多么热火朝天,在下一轮对话中,对它来说你依然是一个全新的陌生人。

今天的文章,我们将直击这个痛点,深入探讨 LangChain 中的 Memory(记忆) 模块。我们将通过代码实战,手把手教你如何打破“七秒记忆”的限制,让 AI 真正记住你是谁,记住你们聊过什么。

1. 现象:大模型的“健忘症”

首先,我们需要理解一个核心概念:LLM 的 API 调用(类似于 HTTP 请求)是无状态的(Stateless)。

这意味着,当你发给它第一句话“我叫三宸”,它处理完任务后,后台就“销毁”了这次交互的上下文。当你紧接着问“我叫什么”时,对它来说,你是一个全新的陌生人。

让我们看一个典型的“翻车”现场:

// ❌ 错误示范:没有记忆的对话
import { ChatDeepSeek } from '@langchain/deepseek';
import 'dotenv/config';

const model = new ChatDeepSeek({
    model: 'deepseek-chat',
    temperature: 0
});

// 第一轮对话
const res1 = await model.invoke("我叫三宸");
console.log('User: 我叫三宸');
console.log(`AI: ${res1.content}`); 
// AI 回复:你好三宸...

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

// 第二轮对话
const res2 = await model.invoke("我叫什么名字");
console.log('User: 我叫什么名字');
console.log(`AI: ${res2.content}`);
// AI 回复:抱歉,我不知道您的名字...

这就是无状态带来的问题。对于想要构建智能助手、客服机器人的开发者来说,这是必须跨越的第一道坎。

2. 原理:如何手动“伪造”记忆?

如果不使用任何框架,我们该从原理上怎么解决这个问题?

答案很简单,但也有些“笨重”: “滚雪球”策略。

既然模型记不住,那开发者就帮它记。在每次发起新请求时,我们不仅要发当前的问题,还要把之前所有的聊天记录打包一起发过去。

我们需要在代码中维护一个 messages 数组:

messages = [
    { role: 'user', content: '你好,我是三宸' },
    { role: 'assistant', content: '你好三宸...' }, // AI 的回复也要存
    { role: 'user', content: '你知道我是谁吗?' } // 新的问题
]

这种原生做法有两个主要痛点:

  1. 维护繁琐:你需要自己写代码去处理数组的追加、存储和读取。
  2. Token 开销爆炸:随着对话越来越长,这个“雪球”越滚越大,Token 消耗呈指数级增长,很快就会耗尽你的预算或超出模型的上下文限制。

3. 实战:LangChain 的优雅解法

LangChain 提供了一个高度封装的模块 RunnableWithMessageHistory 来自动处理上述流程。在最新的 LCEL(LangChain 表达式语言)语法中,我们可以轻松实现一个“有记性”的聊天助手。

下面是完整的实现步骤:

第一步:引入必要的“记忆组件”

除了模型本身,我们需要引入 InMemoryChatMessageHistory(用于在内存中存取历史)和 RunnableWithMessageHistory(用于自动拼接历史)。

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';

第二步:构建带有“插槽”的 Prompt

这是最关键的一步。我们需要在 Prompt 模板中预留一个位置,专门用来放历史记录。这里我们用 placeholder 占位符。

const model = new ChatDeepSeek({
    model: 'deepseek-chat',
    temperature: 0
});

// 这里的 '{history}' 就是留给记忆的插槽
const prompt = ChatPromptTemplate.fromMessages([
    ['system', '你是一个有记忆的助手'],
    ['placeholder', '{history}'], 
    ['human', '{input}']
]);

第三步:注入记忆能力

我们将 Prompt 和 Model 串联后,再用 RunnableWithMessageHistory 包裹起来。它的作用是:在调用模型前,自动去查阅历史记录,填入 Prompt;在调用结束后,自动把最新的对话追加到历史记录里。

const runnable = prompt.pipe(model);

// 创建一个存储对象(这里演示用内存存储,实际生产通常对接 Redis/DB)
const messageHistory = new InMemoryChatMessageHistory();

const chain = new RunnableWithMessageHistory({
    runnable,
    // 获取历史记录的工厂函数
    getMessageHistory: async (sessionId) => messageHistory,
    inputMessagesKey: 'input',     // 对应 prompt 中的用户输入变量名
    historyMessagesKey: 'history', // 对应 prompt 中的占位符变量名
});

第四步:见证奇迹

现在,当我们调用 chain.invoke 时,必须传入一个 sessionId。同一个 ID 代表同一个会话上下文。

// 第一轮:自我介绍
const res1 = await chain.invoke(
    { input: '我叫三宸' },
    { configurable: { sessionId: 'user_123' } } // 指定会话 ID
);
console.log('AI:', res1.content);
// 输出:你好三宸!...

// 第二轮:询问名字(不带名字信息,纯靠记忆)
const res2 = await chain.invoke(
    { input: '我叫什么名字' },
    { configurable: { sessionId: 'user_123' } } // 只要 ID 一样,记忆就在
);
console.log('AI:', res2.content);
// 输出:你叫三宸。

成功了! 模型准确地说出了名字。LangChain 在后台默默完成了“提取历史 -> 拼接到 Prompt -> 调用模型 -> 更新历史”的一整套复杂动作。

4. 进阶思考:从 Demo 到生产环境

通过今天的实战,我们成功实现了一个具备基础记忆能力的 AI 助手。但在将代码部署到生产环境之前,我们必须正视 InMemoryChatMessageHistory 方案在实际应用中的两个致命瓶颈,并了解相应的解决方案。

4.1 痛点一:记忆的持久化(Persistence)

我们目前使用的 InMemoryChatMessageHistory 是将数据保存在服务器内存中的。这意味着一旦服务重启或程序崩溃,所有的对话记录都会瞬间清零。

  • 生产环境解法:在 RunnableWithMessageHistorygetMessageHistory 工厂函数中,我们不应该返回内存对象,而应该对接数据库。
  • LangChain 生态支持多种外部存储,如 RedisPostgreSQLMongoDB。通过 sessionId 从数据库中加载历史记录,对话结束后再将更新写回数据库,从而实现数据的持久化存储。

4.2 痛点二:Token 的“滚雪球”效应

正如我们在原理部分提到的,随着对话轮数的增加,历史记录列表会像“滚雪球”一样越来越大。

  • 问题:如果用户聊了一万句,将这数万 Token 的文本一次性发给模型,不仅会产生高昂的 API 费用,更会直接超出模型支持的最大上下文窗口(Context Window),导致报错。

  • 优化策略:LangChain 提供了多种记忆管理策略来解决这个问题,而不仅仅是简单的“全部追加”:

    1. 窗口记忆 (Window Memory) :只保留最近的 N 轮对话(例如最近 10 句),早期的对话会被自动截断丢弃。这适用于对上下文连续性要求不高的闲聊场景。
    2. 摘要记忆 (Summary Memory) :这是更高级的做法。当历史记录过长时,后台会调用一个 LLM 专门将旧的对话“压缩”成一段精简的摘要(例如:“用户之前介绍了自己叫三宸,喜欢喝AD钙…”),然后将这段摘要作为历史上下文传给主模型。这样既保留了核心信息,又极大节省了 Token。

总结

LLM 的本质是无状态的,而 Memory(记忆) 模块通过“上下文注入”的方式赋予了它连贯交互的能力。从简单的内存存储到复杂的数据库持久化,从全量记录到智能摘要,选择合适的记忆策略,是构建高可用 AI 应用的关键一步。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐