第3章:Memory 系统与对话状态管理
前言
大家好,我是鲫小鱼。是一名不写前端代码
的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!
🎯 本章学习目标
- 系统理解 LangChain.js 的 Memory 体系与适用边界
- 掌握短期/长期/摘要/向量记忆等多种方案的取舍与组合
- 能用
MessagesPlaceholder
将历史对话注入 Prompt,保证上下文连续 - 学会将 Memory 持久化到 Redis/MongoDB,并处理并发、过期与压缩
- 将 Memory 与 Runnable、Callback、LangGraph 状态图结合,打造可观测的对话系统
- 实战 2 个项目:多用户会话中心、个性化学习助手(长期记忆)
📖 Memory 理论与架构(约 30%)
3.1 为什么需要 Memory
- 语言模型本身是"无状态"的,每次调用只依赖输入 Prompt
- 对话型应用需要"上下文记忆",让模型理解"我们刚聊到哪儿了"
- 记忆的本质:在多轮对话间传递"压缩过的语义"与"关键事实"
3.2 常见 Memory 类型与权衡
- Buffer(对话缓冲)
- 全量保留近几轮消息,简单直接
- 风险:token 膨胀、成本升高、响应变慢
- Buffer Window(滑动窗口)
- 仅保留最近 N 条,降低 token
- 风险:忘记早期但仍重要的信息
- Summary(摘要记忆)
- 用模型将历史压缩成"摘要",再与短期上下文拼接
- 风险:摘要偏差、信息丢失;需设计"再校准/再检索"机制
- Vector Store Memory(向量记忆)
- 将对话事实向量化,按需检索(近似语义匹配)
- 风险:召回误差、相似度阈值选择、向量库运维成本
3.3 设计目标与指标
- 连续性:前后语义一致、上下文连贯
- 经济性:token 成本、延迟、带宽
- 稳健性:重复信息去重、冲突与幻觉控制
- 安全性:敏感数据脱敏、最小化保留策略、访问控制
3.4 典型架构图(概念)
css
用户请求 → 会话控制器(Session) → Memory 管理器
├─ 短期:Buffer/Window
├─ 长期:Summary/Vector
├─ 持久化:Redis/Mongo
└─ 观测:日志/回放/评分
→ Prompt 模板(MessagesPlaceholder 注入)→ 模型 → 输出 → 回写 Memory
🧩 LangChain.js Memory 生态(约 10%)
注:本章示例基于 LangChain.js 最新接口风格,部分 Memory 实现位于
@langchain/community
(或langchain/memory
)。如有包路径差异,请以本地版本为准。
ConversationBufferMemory
ConversationBufferWindowMemory
ConversationSummaryMemory
VectorStoreRetrieverMemory
- 自定义 Memory(实现
loadMemoryVariables
/saveContext
接口)
💻 基础到进阶代码(约 40%)
3.5 用 MessagesPlaceholder 注入历史
typescript
// 文件:src/ch03/basic-placeholder.ts
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是简洁的前端顾问。"],
new MessagesPlaceholder("history"),
["human", "{input}"],
]);
const model = new ChatOpenAI({ temperature: 0 });
const chain = prompt.pipe(model).pipe(new StringOutputParser());
// 由外部注入 history(可来自 Memory)
export async function run(history: any[], input: string) {
const out = await chain.invoke({ history, input });
console.log(out);
}
if (require.main === module) {
run([{ role: "human", content: "我们刚讨论了首屏优化" }], "继续说说图片优化");
}
3.6 Buffer 与 Window:权衡示例
typescript
// 文件:src/ch03/window-buffer.ts
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { RunnableSequence } from "@langchain/core/runnables";
import { ConversationBufferWindowMemory } from "langchain/memory";
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是专业的性能优化顾问。"],
new MessagesPlaceholder("history"),
["human", "{input}"],
]);
const model = new ChatOpenAI({ temperature: 0.3 });
const memory = new ConversationBufferWindowMemory({
k: 4, // 仅注入最近 4 条消息
memoryKey: "history",
returnMessages: true,
});
const chain = RunnableSequence.from([
async (input: { input: string }) => ({
input: input.input,
history: (await memory.loadMemoryVariables({})).history,
}),
prompt,
model,
async (output) => { await memory.saveContext({}, { output }); return output; },
]);
export async function ask(q: string) {
const res = await chain.invoke({ input: q });
console.log("AI:", res);
}
if (require.main === module) {
(async () => {
await ask("页面白屏如何排查?");
await ask("如何用懒加载优化首屏?");
await ask("图片该如何处理?");
await ask("再说说骨架屏策略");
await ask("前面我们聊过哪些优化点?"); // 窗口内信息可被引用
})();
}
3.7 Summary:用摘要压缩历史
typescript
// 文件:src/ch03/summary.ts
import { ChatOpenAI } from "@langchain/openai";
import { ConversationSummaryMemory } from "langchain/memory";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { RunnableSequence } from "@langchain/core/runnables";
const llm = new ChatOpenAI({ temperature: 0 });
const memory = new ConversationSummaryMemory({ llm: llm as any, memoryKey: "history" });
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是严谨的技术文档助手,擅长总结与引用。"],
new MessagesPlaceholder("history"),
["human", "{input}"],
]);
const chain = RunnableSequence.from([
async (input: { input: string }) => ({ ...input, history: await memory.loadMemoryVariables({}) }),
prompt,
llm,
async (output) => { await memory.saveContext({}, { output }); return output; },
]);
export async function chat(q: string) {
const res = await chain.invoke({ input: q });
console.log(res);
}
if (require.main === module) {
(async () => {
await chat("请总结我们要做的性能优化路线");
await chat("针对图片和脚本分别给出 3 条建议");
await chat("把总结浓缩为 5 个要点");
})();
}
3.8 VectorStoreRetrieverMemory:事实性持久记忆
typescript
// 文件:src/ch03/vector-memory.ts
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { RunnableSequence } from "@langchain/core/runnables";
import { VectorStoreRetrieverMemory } from "langchain/memory"; // 可能位于 @langchain/community
// 伪向量检索器(实际请接入 Chroma/Pinecone/Weaviate)
const fakeRetriever = {
async getRelevantDocuments(q: string) {
return [{ pageContent: `用户偏好:更喜欢暗色主题;最近关注"响应式布局"。Q=${q}` } as any];
}
};
const memory = new VectorStoreRetrieverMemory({
retriever: fakeRetriever as any,
memoryKey: "history", // 将检索结果当作"历史上下文"注入
});
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是个性化 UI 助手,回答需考虑用户偏好。"],
new MessagesPlaceholder("history"),
["human", "{input}"],
]);
const chain = RunnableSequence.from([
async (input: { input: string }) => ({
input: input.input,
history: await memory.loadMemoryVariables({}),
}),
prompt,
new ChatOpenAI({ temperature: 0 }),
]);
export async function advise(q: string) {
const res = await chain.invoke({ input: q });
console.log(res);
}
if (require.main === module) {
advise("请推荐首页布局方案");
}
3.9 自定义 Memory:统一接口
typescript
// 文件:src/ch03/custom-memory.ts
import type { BaseChatMemory } from "langchain/memory";
type Message = { role: "human" | "ai" | "system"; content: string; ts?: number };
export class SimpleMemory implements BaseChatMemory {
memoryKey = "history";
private store: Record<string, Message[]> = {};
constructor(private sessionId: string) {}
get memoryKeys() { return [this.memoryKey]; }
async loadMemoryVariables(_: any) {
return { [this.memoryKey]: (this.store[this.sessionId] || []).map(m => ({ role: m.role, content: m.content })) };
}
async saveContext(input: any, output: any) {
const arr = this.store[this.sessionId] || (this.store[this.sessionId] = []);
if (input?.input) arr.push({ role: "human", content: input.input, ts: Date.now() });
if (output?.content) arr.push({ role: "ai", content: output.content, ts: Date.now() });
}
async clear() { this.store[this.sessionId] = []; }
}
3.10 Redis/Mongo 持久化(示意)
typescript
// 文件:src/ch03/redis-memory.ts
import { createClient } from "redis";
import type { BaseChatMemory } from "langchain/memory";
export class RedisMemory implements BaseChatMemory {
memoryKey = "history";
constructor(private client = createClient(), private sessionId: string) {}
async loadMemoryVariables() {
const raw = await this.client.get(`mem:${this.sessionId}`);
return { [this.memoryKey]: raw ? JSON.parse(raw) : [] };
}
async saveContext(input: any, output: any) {
const prev = (await this.loadMemoryVariables())[this.memoryKey] as any[];
const next = [...prev];
if (input?.input) next.push({ role: "human", content: input.input, ts: Date.now() });
if (output?.content) next.push({ role: "ai", content: output.content, ts: Date.now() });
await this.client.set(`mem:${this.sessionId}`, JSON.stringify(next), { EX: 60 * 60 * 24 });
}
async clear() { await this.client.del(`mem:${this.sessionId}`); }
}
🔗 与 Runnable/Callback/LangGraph 集成(约 10%)
3.11 Runnable:带会话的可复用链
typescript
// 文件:src/ch03/session-chain.ts
import { RunnableSequence } from "@langchain/core/runnables";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { SimpleMemory } from "./custom-memory";
export function createSessionChain(sessionId: string) {
const memory = new SimpleMemory(sessionId);
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是稳健的技术助手,遇到不确定请先提问澄清。"],
new MessagesPlaceholder("history"),
["human", "{input}"],
]);
return RunnableSequence.from([
async (input: { input: string }) => ({ input: input.input, history: await memory.loadMemoryVariables({})["history"] }),
prompt,
new ChatOpenAI({ temperature: 0 }),
new StringOutputParser(),
async (out, input) => { await memory.saveContext(input, { content: out }); return out; },
]);
}
3.12 Callback:观测记忆读写
typescript
// 文件:src/ch03/memory-callback.ts
import { ConsoleCallbackHandler } from "@langchain/core/callbacks/console";
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
const model = new ChatOpenAI({
callbacks: [new ConsoleCallbackHandler()],
verbose: true,
});
const prompt = ChatPromptTemplate.fromMessages([
["system", "你会在输出中标注使用到的历史要点。"],
new MessagesPlaceholder("history"),
["human", "{input}"],
]);
export async function run(history: any[], input: string) {
const res = await prompt.pipe(model).invoke({ history, input });
console.log(res.content);
}
3.13 LangGraph:在状态图中管理记忆
typescript
// 文件:src/ch03/langgraph-memory.ts(伪示例,接口以本地版本为准)
// 展示如何把 memory 放在图的 state 中,跨节点共享
import { StateGraph } from "@langchain/langgraph";
type GraphState = {
history: { role: string; content: string }[];
input: string;
output?: string;
};
const graph = new StateGraph<GraphState>({
channels: {
history: { value: [], default: [] },
input: { value: "" },
output: { value: "" },
},
});
graph.addNode("llm", async (state) => {
// 读取 state.history 注入到 Prompt,再调用 LLM
// 得到输出后写回 state.output,并 push 到 history
return { output: `回答:${state.input}`, history: [...state.history, { role: "ai", content: "..." }] };
});
graph.addEdge("start", "llm");
graph.addEdge("llm", "end");
export const app = graph.compile();
🛡️ 健壮性与安全(约 5%)
3.14 错误与边界处理
- 结构异常:Memory 读写失败 → 回退到空历史 + 记录错误
- 消息去重:哈希或指纹,避免重复注入
- 冲突处理:同一轮内多次写入按时间戳排序、幂等化
3.15 隐私与合规
- 最小化原则:仅保存任务所需的最少内容
- 数据脱敏:PII/敏感字段脱敏或只保留摘要/向量
- 可删除权:支持会话级清除、用户级清除
- 访问控制:会话隔离、租户隔离、审计日志
🚀 实战项目一:多用户会话中心(Next.js + Redis)(约 15%)
3.16 目标
- 在 Next.js 中实现 API:支持多用户多会话,Redis 持久化 Memory
- 要求:移动端适配、可取消流式响应、错误兜底
3.17 数据结构
ts
// 会话主键:session:{tenantId}:{userId}:{sessionId}
// 值:[{ role, content, ts }]
3.18 核心接口(节选)
typescript
// 文件:src/pages/api/chat.ts(示意)
import type { NextApiRequest, NextApiResponse } from "next";
import { createClient } from "redis";
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
const client = createClient({ url: process.env.REDIS_URL });
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是客服助手,回答要简洁且引用历史。"],
new MessagesPlaceholder("history"),
["human", "{input}"],
]);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { tenantId, userId, sessionId, input } = req.body || {};
const key = `session:${tenantId}:${userId}:${sessionId}`;
const raw = (await client.get(key)) || "[]";
const history = JSON.parse(raw);
const model = new ChatOpenAI({ temperature: 0, streaming: true });
const stream = await prompt.pipe(model).stream({ history, input });
res.setHeader("Content-Type", "text/event-stream");
for await (const chunk of stream) {
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
}
res.end();
// 异步回写(省略错误处理与限流细节)
history.push({ role: "human", content: input, ts: Date.now() });
history.push({ role: "ai", content: "...最终聚合后的输出...", ts: Date.now() });
await client.set(key, JSON.stringify(history), { EX: 60 * 60 * 24 * 7 });
}
3.19 前端要点
- 移动端:底部输入栏固定,键盘顶起防遮挡
- 流式:SSE/Fetch ReadableStream 渐进渲染,提供"停止/重试"
- 错误:网络/超时统一提示并回退到最后稳定状态
🧠 实战项目二:个性化学习助手(长期记忆)(约 15%)
3.20 目标
- 将"用户偏好/知识卡片/易错点"以向量与摘要双存储
- 对话时按需检索 + 总结注入,提高个性化与复用性
3.21 关键模块
- Ingest:从学习记录/书签/错题集生成向量与事实卡
- Retriever:按当前问题检索 + 召回过滤 + 去重
- Summarizer:每 5-10 轮生成"阶段性摘要"写回长期记忆
- Orchestrator:Runnable/LangGraph 调度流程
3.22 伪代码(编排)
typescript
const orchestrator = RunnableSequence.from([
// 1) 上下文收集
async (input: { q: string }) => ({ q: input.q, user: await loadUser() }),
// 2) 检索长期记忆(向量)
async (ctx) => ({ ...ctx, facts: await vectorRetrieve(ctx.q, ctx.user.id) }),
// 3) 历史摘要(短期→长期压缩)
async (ctx) => ({ ...ctx, summary: await loadOrUpdateSummary(ctx.user.id) }),
// 4) Prompt 组装
async (ctx) => ({
prompt: buildPrompt({ q: ctx.q, facts: ctx.facts, summary: ctx.summary }),
}),
// 5) LLM & 解析
async (ctx) => parse(await callLLM(ctx.prompt)),
// 6) 回写与打分
async (out) => { await persist(out); return out; },
]);
3.23 质量与体验
- 给出"引用"与"学习路径建议",降低幻觉
- 移动端:卡片化展示知识点、可一键加入"记忆卡片"
- 监控:长对话 token 成本与响应时间曲线
📈 性能优化与成本控制(约 5%)
3.24 建议
- 短期使用 Window,长期用 Summary/Vector 结合
- 阶段性摘要:按对话轮数或 token 阈值触发
- 检索前过滤:基于关键词/规则初筛,减少向量查询
- 结果去重:相似度/标题指纹,避免注入重复
- 缓存:会话级 prompt 模板缓存、检索缓存
🧪 测试与可观测性(约 5%)
3.25 回归集与评分
- 构建多轮对话用例,包含澄清、纠错、回忆历史等场景
- 指标:连续性、一致性、事实命中率、用户满意度
3.26 可观测
- Callback/日志:记录注入的历史条数、来源(Buffer/Summary/Vector)
- LangSmith:链路可视化、token 使用、错误与延迟
- 追踪 ID:贯穿前后端,便于排错
📚 延伸资源
- LangChain.js(JS)文档:
https://js.langchain.com/
- Memory 指南(社区):
https://js.langchain.com/docs/modules/memory/
- LangGraph 状态图:
https://langchain-ai.github.io/langgraph/
- Redis 官方文档:
https://redis.io/docs/latest/
- 保护隐私与合规:GDPR/CN 个人信息保护法等
✅ 本章小结
- 理解了 4 大类记忆的优缺点与使用边界
- 掌握了 MessagesPlaceholder 注入、摘要压缩、向量检索
- 学会了持久化 Memory、并与 Runnable/Callback/LangGraph 协作
- 完成"多用户会话中心""个性化学习助手"两类项目的方案演练
🎯 下章预告
下一章《Callback 机制与事件驱动架构》中,我们将:
- 构建可观测的链路日志体系
- 实现流式打字、进度条、错误告警
- 与前端实时通信(SSE/WebSocket)对接
- 结合 LangSmith 做链路追踪与 A/B 评测
最后感谢阅读!欢迎关注我,微信公众号:
《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!!