🧠 破解无状态困局:如何用 LangChain 为 DeepSeek 大模型注入"记忆"能力
摘要: 在开发基于 LLM(大语言模型)的应用时,我们常遇到一个痛点:模型似乎得了"失忆症"。上一秒你告诉它你的名字,下一秒提问它却一问三不知。这是因为底层的 HTTP API 请求本质上是**无状态(Stateless)**的。本文将结合代码实战,带你从零开始,利用 LangChain 框架,为 DeepSeek 模型构建一套高效的记忆系统。
1. 痛点分析:为什么模型总是"金鱼脑"?
在最基础的 API 调用中(如文档 index.js 所示),每一次请求都是独立的事件。
javascript
// index.js - 无记忆的调用
const res = await model.invoke('我叫张三,我喜欢打游戏');
const res2 = await model.invoke('我叫什么名字'); // 模型不知道上下文,回答失败
问题本质: LLM 本身并不存储用户的历史对话。每一次 model.invoke 都是一次全新的对话。模型只能看到当前这一次发送的 Token,无法感知之前的交互。这就是所谓的"无状态"特性。
2. 解决方案:维护对话历史(Memory)
既然模型没有记忆,我们就需要在应用层手动维护一份"对话历史记录(Messages History)"。核心思路是**"滚雪球"策略**:将过往的所有对话(用户输入 + 模型回复)打包,作为上下文(Context)一起发送给模型。
根据 readme.md 的原理,我们需要构建如下结构的数据:
json
messages = [
{ role: 'user', content: '我叫张三,喜欢喝白兰地' },
{ role: 'assistant', content: '好的,我知道了。' },
{ role: 'user', content: '你知道我是谁吗?' } // 模型此时能看到之前的自我介绍
]
3. 代码实战:构建有记忆的 DeepSeek 助手
虽然手动拼接 Messages 数组可行,但这非常繁琐且难以管理。LangChain 提供了优雅的封装。参考文档 1.js,我们将演示如何利用 RunnableWithMessageHistory 实现自动化记忆管理。
核心组件架构:
| 组件 | 作用 |
|---|---|
ChatDeepSeek |
指定使用的模型(DeepSeek)及参数(Temperature)。 |
ChatPromptTemplate |
构建提示词模板,预留 {history} 占位符。 |
InMemoryChatMessageHistory |
记忆的"容器",用于存储特定会话的历史记录。 |
RunnableWithMessageHistory |
核心逻辑层,自动将历史记录注入到当前请求中。 |
实现步骤详解:
第一步:初始化模型与提示词 首先,我们需要定义模型,并在提示词中明确告诉 AI 我们要引入历史记录。
javascript
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 将模型、历史存储和提示词串联起来。
javascript
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 来维持上下文连贯性。
javascript
// 第一次对话:建立记忆
const res1 = await chain.invoke(
{ input: "我叫张三,喜欢打游戏" },
{ configurable: { sessionId: "makefriend" } } // 会话ID
);
// 第二次对话:利用记忆
const res2 = await chain.invoke(
{ input: "我叫什么名字" },
{ configurable: { sessionId: "makefriend" } } // 必须使用相同的ID
);
console.log(res2.content); // 输出:你叫张三。
4. 深度解析:记忆背后的机制
-
Session ID 的作用 : 在上述代码中,
sessionId就像是一个"钥匙"。LangChain 利用这个 ID 来区分不同的用户或不同的对话场景。如果 ID 不同,模型就会开启一段全新的、互不干扰的对话。 -
Token 开销的权衡(Context Window) : 文档
readme.md提到了一个重要的工程考量:"滚雪球"效应 。 随着对话轮次增加,历史记录会越来越长,这会消耗大量的 Token 预算,增加成本并拖慢响应速度。虽然本文使用的是InMemoryChatMessageHistory(内存存储,适合演示),但在实际生产中,你可能需要考虑:- 窗口化记忆(Window Memory):只保留最近 N 轮对话。
- 摘要记忆(Summary Memory):将长历史总结成几句话传给模型。
- 向量数据库:存储海量历史,仅在需要时检索相关片段。
5. 总结
通过本文的实践,我们成功解决了 LLM 无状态的问题。利用 LangChain 的 RunnableWithMessageHistory,我们不仅让 DeepSeek 模型记住了用户的名字,更建立了一套可复用的上下文管理框架。
核心启示:
大模型的"智能"不仅取决于其参数量,更取决于你如何喂给它数据。通过精心设计的
Prompt和History管理,你可以将一个"金鱼脑"的模型,变成一个拥有长期记忆的私人助理。
希望这篇教程能助你在 AI 应用开发的道路上更进一步!🚀