光宇
24.33M · 2026-03-31
摘要: 在开发基于 LLM(大语言模型)的应用时,我们常遇到一个痛点:模型似乎得了“失忆症”。上一秒你告诉它你的名字,下一秒提问它却一问三不知。这是因为底层的 HTTP API 请求本质上是**无状态(Stateless)**的。本文将结合代码实战,带你从零开始,利用 LangChain 框架,为 DeepSeek 模型构建一套高效的记忆系统。
在最基础的 API 调用中(如文档 index.js 所示),每一次请求都是独立的事件。
// index.js - 无记忆的调用
const res = await model.invoke('我叫张三,我喜欢打游戏');
const res2 = await model.invoke('我叫什么名字'); // 模型不知道上下文,回答失败
问题本质:
LLM 本身并不存储用户的历史对话。每一次 model.invoke 都是一次全新的对话。模型只能看到当前这一次发送的 Token,无法感知之前的交互。这就是所谓的“无状态”特性。
既然模型没有记忆,我们就需要在应用层手动维护一份“对话历史记录(Messages History)”。核心思路是**“滚雪球”策略**:将过往的所有对话(用户输入 + 模型回复)打包,作为上下文(Context)一起发送给模型。
根据 readme.md 的原理,我们需要构建如下结构的数据:
messages = [
{ role: 'user', content: '我叫张三,喜欢喝白兰地' },
{ role: 'assistant', content: '好的,我知道了。' },
{ role: 'user', content: '你知道我是谁吗?' } // 模型此时能看到之前的自我介绍
]
虽然手动拼接 Messages 数组可行,但这非常繁琐且难以管理。LangChain 提供了优雅的封装。参考文档 1.js,我们将演示如何利用 RunnableWithMessageHistory 实现自动化记忆管理。
核心组件架构:
| 组件 | 作用 |
|---|---|
ChatDeepSeek | 指定使用的模型(DeepSeek)及参数(Temperature)。 |
ChatPromptTemplate | 构建提示词模板,预留 {history} 占位符。 |
InMemoryChatMessageHistory | 记忆的“容器”,用于存储特定会话的历史记录。 |
RunnableWithMessageHistory | 核心逻辑层,自动将历史记录注入到当前请求中。 |
实现步骤详解:
第一步:初始化模型与提示词 首先,我们需要定义模型,并在提示词中明确告诉 AI 我们要引入历史记录。
import { ChatDeepSeek } from "@langchain/deepseek";
import { ChatPromptTemplate } from "@langchain/core/prompts";
const model = new ChatDeepSeek({ model: "deepseek-chat", temperature: 0 });
// 关键点:在提示词中加入 {history} 占位符
const prompt = ChatPromptTemplate.fromMessages([
['system', '你是一个有记忆的助手'],
['placeholder', '{history}'], // 历史记录将自动插入此处
['human', '{input}']
]);
第二步:构建带记忆的执行链(Chain)
这是最关键的一步。我们使用 RunnableWithMessageHistory 将模型、历史存储和提示词串联起来。
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { InMemoryChatMessageHistory } from "@langchain/core/chat_history";
// 1. 创建历史记录容器(通常在生产环境中会替换为数据库)
const messageHistory = new InMemoryChatMessageHistory();
// 2. 构建核心执行链
const chain = new RunnableWithMessageHistory({
runnable: prompt.pipe(model), // 定义处理流程:Prompt -> Model
getMessageHistory: async () => messageHistory, // 指定历史存储位置
inputMessagesKey: "input", // 输入键名
historyMessagesKey: "history" // 历史键名(对应Prompt中的占位符)
});
第三步:多轮对话测试
现在,我们可以通过保持相同的 sessionId 来维持上下文连贯性。
// 第一次对话:建立记忆
const res1 = await chain.invoke(
{ input: "我叫张三,喜欢打游戏" },
{ configurable: { sessionId: "makefriend" } } // 会话ID
);
// 第二次对话:利用记忆
const res2 = await chain.invoke(
{ input: "我叫什么名字" },
{ configurable: { sessionId: "makefriend" } } // 必须使用相同的ID
);
console.log(res2.content); // 输出:你叫张三。
Session ID 的作用:
在上述代码中,sessionId 就像是一个“钥匙”。LangChain 利用这个 ID 来区分不同的用户或不同的对话场景。如果 ID 不同,模型就会开启一段全新的、互不干扰的对话。
Token 开销的权衡(Context Window):
文档 readme.md 提到了一个重要的工程考量:“滚雪球”效应。
随着对话轮次增加,历史记录会越来越长,这会消耗大量的 Token 预算,增加成本并拖慢响应速度。虽然本文使用的是 InMemoryChatMessageHistory(内存存储,适合演示),但在实际生产中,你可能需要考虑:
通过本文的实践,我们成功解决了 LLM 无状态的问题。利用 LangChain 的 RunnableWithMessageHistory,我们不仅让 DeepSeek 模型记住了用户的名字,更建立了一套可复用的上下文管理框架。
核心启示:
希望这篇教程能助你在 AI 应用开发的道路上更进一步!