引言
"我叫 Agiao,喜欢喝白兰地。"
------ 你说完这句话,转身离开。
再回来问:"我叫什么名字?"
如果 AI 回答"我不知道",那它只是个无状态的 API;
如果它回答"Agiao",那它就拥有了记忆。
在人工智能的世界里,"记忆"不是与生俱来的天赋,而是一种精心设计的工程能力。大型语言模型(LLM)本身是无状态的------每一次调用都像一场全新的对话,模型对过去一无所知。然而,现实中的智能交互必须具备上下文连贯性。如何让 LLM "记住你"?这正是我们今天要深入剖析的核心问题。
本文将逐行、逐模块、逐 API 地解析 1.js 文件,揭示 LangChain 如何通过 对话历史管理机制 赋予 LLM 真正的记忆能力。我们将不仅解释"怎么做",更要阐明"为什么这么做",并对比无记忆版本(index.js)的局限,最终构建一个完整、生动、技术扎实的认知图景。
背景:为什么 LLM 默认没有记忆?
首先,让我们回到基础。
LLM API 调用和 http 请求一样,都是无状态的
这意味着每次你向模型发送一条消息(例如 "我叫Agiao"),模型仅基于这条消息生成回复,不会自动保留任何上下文。如果你紧接着再发一条 "我叫什么名字?",模型根本不知道前一条消息的存在。
我们来看 index.js 的实现:
javascript
import {ChatDeepSeek} from '@langchain/deepseek'
import 'dotenv/config'
const model = new ChatDeepSeek({ model: 'deepseek-chat', temperature: 0});
// http api 请求
const res = await model.invoke('我叫Agiao,喜欢喝白兰地')
console.log(res.content);
const res2 = await model.invoke('我叫什么名字')
console.log(res2.content);
这段代码做了两件事:
- 第一次调用
model.invoke(...),告诉模型你的名字和喜好。 - 第二次独立调用
model.invoke(...),询问自己的名字。
由于两次调用之间没有任何信息传递,第二次请求对模型而言完全是孤立的。因此,模型极大概率会回答类似:"抱歉,我不知道您叫什么名字。"------这不是模型笨,而是架构使然。
💡 关键洞察 :要实现记忆,我们必须主动维护对话历史,并在每次请求时将其作为上下文传入模型。
但如何高效、结构化、可扩展地做到这一点?这就是 LangChain 的用武之地。
架构预览:LangChain 的记忆抽象
LangChain 提供了一套完整的"记忆"(Memory)模块,其核心思想是:
将对话历史视为一种可插拔的上下文源,在每次调用 LLM 前动态注入到 Prompt 中。
在 1.js 中,这一思想通过以下组件协同实现:
InMemoryChatMessageHistory:存储对话历史。ChatPromptTemplate:定义包含历史占位符的提示模板。RunnableWithMessageHistory:自动管理历史加载与保存的包装器。ChatDeepSeek:底层 LLM 模型。
接下来,我们将逐行拆解 1.js,深入每一个 API 的设计哲学与实现细节。
逐行深度解析 1.js
第一部分:模块导入(Import Statements)
javascript
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';
✅ ChatDeepSeek
-
来源:
@langchain/deepseek -
作用:封装 DeepSeek 提供的聊天模型 API,使其符合 LangChain 的
BaseChatModel接口。 -
特性:
- 支持
invoke()、stream()等标准方法。 - 自动处理消息格式(如
HumanMessage,AIMessage)。 - 可配置
model、temperature、maxTokens等参数。
- 支持
✅ ChatPromptTemplate
-
来源:
@langchain/core/prompts -
作用:构建结构化的聊天提示(prompt),支持系统消息、用户消息、AI 消息及占位符。
-
关键能力:
- 使用
fromMessages()静态方法从消息数组创建模板。 - 支持变量插值(如
{input},{history})。 - 输出为
Runnable,可与其他组件链式组合(.pipe())。
- 使用
✅ RunnableWithMessageHistory
-
来源:
@langchain/core/runnables -
这是实现记忆的核心抽象!
-
作用:包装一个普通的
Runnable(如 LLM 调用链),使其具备自动加载和保存对话历史的能力。 -
工作原理:
- 在每次
.invoke()时,先从getMessageHistory获取当前会话的历史。 - 将历史注入到输入中(通过
historyMessagesKey指定的字段)。 - 调用内部
runnable。 - 将用户输入和模型输出自动追加到历史记录中。
- 在每次
⚠️ 注意:
RunnableWithMessageHistory并不关心历史如何存储(内存、数据库、Redis),它只依赖getMessageHistory函数返回一个实现了BaseChatMessageHistory接口的对象。
✅ InMemoryChatMessageHistory
-
来源:
@langchain/core/chat_history -
作用:最简单的对话历史存储实现,将所有消息保存在 JavaScript 内存数组中。
-
接口方法:
getMessages():返回当前所有消息。addMessage(message):添加一条新消息。clear():清空历史。
-
适用场景:单会话演示、测试。不适合生产环境多用户场景(因为所有会话共享同一实例,且进程重启后丢失)。
✅ dotenv/config
- 加载
.env文件中的环境变量(如DEEPSEEK_API_KEY),确保模型认证信息安全。
第二部分:模型初始化
ini
const model = new ChatDeepSeek({ model: 'deepseek-chat', temperature: 0});
model: 'deepseek-chat':指定使用 DeepSeek 的聊天专用模型。temperature: 0:关闭随机性,确保相同输入总是产生相同输出------这对需要精确记忆的场景至关重要(避免因随机性导致"忘记")。
第三部分:构建带记忆的 Prompt 模板
css
const prompt = ChatPromptTemplate.fromMessages([ ['system', "你是一个有记忆的助手"],
['placeholder', "{history}"],
['human', "{input}"]
])
这是整个记忆机制的提示工程核心。
消息结构解析:
-
['system', "你是一个有记忆的助手"]- 系统消息,用于设定 AI 的角色和行为准则。
- 告诉模型:"你有能力记住之前的对话,请利用这些信息。"
-
['placeholder', "{history}"]-
关键! 这不是一个普通字符串,而是一个占位符指令。
-
LangChain 在渲染此模板时,会查找输入对象中的
history字段。 -
如果
history是一个消息数组(如[HumanMessage, AIMessage, ...]),它会自动展开为多条独立消息,插入到此处。 -
例如,若历史为:
arduino[ new HumanMessage("我叫Agiao"), new AIMessage("好的,Agiao!") ]则最终 prompt 变为:
css[ SystemMessage("你是一个有记忆的助手"), HumanMessage("我叫Agiao"), AIMessage("好的,Agiao!"), HumanMessage("{input}")]
-
-
['human', "{input}"]- 当前用户输入,通过变量
input注入。
- 当前用户输入,通过变量
💡 为什么用
placeholder而不是直接写{history}字符串?因为
{history}实际上是一个消息列表 ,而非纯文本。placeholder告诉 LangChain:"这里要插入一个消息序列,而不是拼接字符串"。
第四部分:构建可运行链(Runnable Chain)
javascript
const runnable = prompt
.pipe((input) => {
// debug 节点
console.log(">>> 最终传给模型的信息(Prompt 内存)");
console.log(input)
return input;
})
.pipe(model);
LangChain 的核心思想是"一切皆可运行(Runnable)"。这里我们构建了一个处理流水线:
prompt:首先应用提示模板,将{input}和{history}替换为实际内容,生成完整的消息列表。- 调试中间件:打印最终传给模型的消息结构。这对于理解记忆如何工作至关重要。
model:将构造好的消息列表发送给 DeepSeek 模型。
📌 注意:此时的
runnable仍然不具备记忆能力 !它只是一个"知道如何组织消息"的函数。真正的记忆由外层的RunnableWithMessageHistory注入。
第五部分:创建对话历史存储
ini
const messageHistory = new InMemoryChatMessageHistory();
- 创建一个空的内存历史容器。
- 所有后续对话(用户输入 + AI 回复)都将被自动追加到这里。
- 由于是全局变量,所有使用该
messageHistory的会话将共享同一历史(在多用户场景中需按 session ID 分离)。
第六部分:注入记忆能力 ------ RunnableWithMessageHistory
dart
const chain = new RunnableWithMessageHistory({
runnable,
getMessageHistory: async () => messageHistory,
inputMessagesKey: 'input',
historyMessagesKey: 'history',
});
这是整段代码的灵魂所在。让我们逐个参数详解:
🔸 runnable
- 要包装的原始可运行对象(即前面构建的
prompt → debug → model链)。 RunnableWithMessageHistory会在调用它之前注入历史,在之后保存新消息。
🔸 getMessageHistory: async () => messageHistory
-
一个异步函数,返回当前会话对应的
BaseChatMessageHistory实例。 -
在本例中,我们直接返回全局的
messageHistory。 -
在真实应用中,这里通常会根据
sessionId从数据库或缓存中加载对应的历史。例如:vbnetgetMessageHistory: async (sessionId) => { return new RedisChatMessageHistory(sessionId, redisClient); }
🔸 inputMessagesKey: 'input'
- 指定用户当前输入在调用参数中的字段名。
- 对应后续的
chain.invoke({ input: '...' })。 - LangChain 会从此字段读取用户消息,并在调用完成后将其存入历史。
🔸 historyMessagesKey: 'hitory'
- 指定历史消息在输入对象中的字段名。
- LangChain 会从
getMessageHistory()获取历史消息,并以{ history: [msg1, msg2, ...] }的形式注入到runnable的输入中。 - 这个字段名必须与
ChatPromptTemplate中的占位符{history}一致!
✅ 工作流程总结:
- 用户调用
chain.invoke({ input: "..." }, { configurable: { sessionId: "..." } })RunnableWithMessageHistory调用getMessageHistory(sessionId)获取历史。- 构造输入对象:
{ input: "...", history: [prev messages] }- 调用内部
runnable(input)- 获取模型回复。
- 将
HumanMessage(input)和AIMessage(response)自动追加到历史中。
第七部分:执行多轮对话
css
const res1 = await chain.invoke(
{ input: '我叫Agiao,喜欢喝白兰地' },
{ configurable: { sessionId: 'makefriend' } }
)
console.log(res1.content);
const res2 = await chain.invoke(
{ input: '我叫什么名字' },
{ configurable: { sessionId: 'makefriend' } }
)
console.log(res2.content);
🔹 第一次调用
-
输入:
{ input: '我叫Agiao,喜欢喝白兰地' } -
此时
messageHistory为空,所以{history}占位符被替换为空数组。 -
最终 prompt:
css[ SystemMessage("你是一个有记忆的助手"), HumanMessage("我叫Agiao,喜欢喝白兰地")] -
模型回复后,
messageHistory自动更新为:css[ HumanMessage("我叫Agiao,喜欢喝白兰地"), AIMessage("好的,Agiao!白兰地是种很棒的选择。")]
🔹 第二次调用
-
输入:
{ input: '我叫什么名字' } -
messageHistory已包含两条消息。 -
最终 prompt:
css[ SystemMessage("你是一个有记忆的助手"), HumanMessage("我叫Agiao,喜欢喝白兰地"), AIMessage("好的,Agiao!白兰地是种很棒的选择。"), HumanMessage("我叫什么名字")] -
模型看到完整上下文,自然能回答:"你叫 Agiao。"
🎯 关键优势:开发者无需手动管理历史拼接、消息格式转换、存储逻辑------全部由 LangChain 自动处理。
调试输出:见证记忆的传递
中间的调试节点会打印:
第一次调用后:
css
>>> 最终传给模型的信息(Prompt 内存)
{
messages: [
SystemMessage { content: "你是一个有记忆的助手" },
HumanMessage { content: "我叫Agiao,喜欢喝白兰地" }
]
}
第二次调用后:
css
>>> 最终传给模型的信息(Prompt 内存)
{
messages: [
SystemMessage { content: "你是一个有记忆的助手" },
HumanMessage { content: "我叫Agiao,喜欢喝白兰地" },
AIMessage { content: "好的,Agiao!白兰地是种很棒的选择。" },
HumanMessage { content: "我叫什么名字" }
]
}
这清晰展示了历史如何被逐步累积并注入上下文。
对比:有记忆 vs 无记忆
| 特性 | index.js(无记忆) |
1.js(有记忆) |
|---|---|---|
| 状态管理 | 无 | 自动维护对话历史 |
| 上下文传递 | 手动拼接(未实现) | 自动注入 {history} |
| 多轮连贯性 | ❌ 不支持 | ✅ 完美支持 |
| Token 效率 | 每次独立,无冗余 | 历史随对话增长,需注意长度 |
| 扩展性 | 难以扩展 | 易替换存储后端(Redis/DB) |
| 代码复杂度 | 简单但功能有限 | 稍复杂但功能强大 |
潜在挑战与优化方向
虽然 1.js 展示了记忆的基本实现,但在生产环境中还需考虑:
1. Token 长度限制
-
对话历史不断增长,可能超出模型上下文窗口(如 32768 tokens)。
-
解决方案:
- 使用
WindowChatMessageHistory仅保留最近 N 条消息。 - 实现摘要机制:定期将历史压缩为摘要,并作为系统消息注入。
- 使用
2. 多用户会话隔离
-
当前
messageHistory是全局单例,所有用户共享。 -
解决方案:
- 在
getMessageHistory中根据sessionId返回不同历史实例。 - 使用
RedisChatMessageHistory或数据库按 session 存储。
- 在
3. 持久化
- 内存存储在服务重启后丢失。
- 解决方案:集成持久化存储(如 PostgreSQL、MongoDB)。
4. 长期记忆(Long-term Memory)
- 对话历史属于短期记忆。
- 解决方案:结合向量数据库(如 Pinecone)实现基于检索的长期记忆(RAG + Memory)。
结语:记忆,是智能的基石
通过 1.js,我们不仅学会了如何用 LangChain 实现 LLM 的记忆,更理解了其背后的设计哲学:将状态管理与核心逻辑解耦,通过组合式 API 构建可扩展的智能应用。
记忆不是魔法,而是一系列精心设计的数据流:
用户输入 → 加载历史 → 注入上下文 → 调用模型 → 保存新状态。
当你下次对 AI 说"还记得我吗?",希望它能微笑着回答:"当然,Agiao。你上次说喜欢白兰地,要不要再来一杯?"
🥂 Cheers to intelligent conversations with memory!
附:完整代码链接: lesson_zp/ai/langchain/memory/demo: AI + 全栈学习仓库
在该项目根目录中需添加 .env 文件,文件中添加DeepSeek大模型的API_KEY
例如:
DEEPSEEK_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
API_KEY获取地址:DeepSeek 开放平台