LangChain.js 完全开发手册(三)Memory 系统与对话状态管理

第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 评测

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!

相关推荐
永远不打烊1 分钟前
Window环境 WebRTC demo 运行
前端
风舞2 分钟前
一文搞定JS所有类型判断最佳实践
前端·javascript
coding随想2 分钟前
哈希值变化的魔法:深入解析HTML5 hashchange事件的奥秘与实战
前端
一树山茶10 分钟前
uniapp在微信小程序中实现 SSE进行通信
前端·javascript
coding随想10 分钟前
小程序中的pageshow与pagehide事件,HTML5中也有?揭秘浏览器往返缓存(BFCache)
前端
萌萌哒草头将军16 分钟前
Rspack 1.5 版本更新速览!🚀🚀🚀
前端·javascript·vue.js
阿卡不卡20 分钟前
基于多场景的通用单位转换功能实现
前端·javascript
♡喜欢做梦31 分钟前
jQuery 从入门到实践:基础语法、事件与元素操作全解析
前端·javascript·jquery
bug菌33 分钟前
🤔还在为代码调试熬夜?字节TRAE如何让我的开发效率翻三倍的神操作!
aigc·ai编程·trae