这是本文档旧的修订版!
第十章:核心能力详解 - Memory
“记忆”是很多 LLM 应用最容易被误用的能力之一。很多初学者会把 Memory 理解成“把历史聊天全塞进去”,但工程上真正的问题不是“有没有保存记录”,而是:系统应该记住什么、保存多久、以什么形式保存、在何时取回使用。
现代 LangChain 和 LangGraph 语境下,Memory 与其说是一个“会话缓存功能”,不如说是 状态管理 + 上下文压缩 + 长期档案设计 的组合。本章将系统讲解短期记忆、长期记忆、摘要记忆、档案记忆、线程状态、检查点,以及如何把这些概念落到具体代码里。
10.1 为什么需要 Memory
在一次性问答里,Memory 的收益很小;但在以下场景中,记忆会显著提升体验:
- 多轮客服;
- 长周期任务协作;
- 项目跟进助手;
- 个性化办公助手;
- 带状态的 Agent 工作流。
没有记忆,系统每轮都像“第一次见你”;记忆用错,则系统会变得啰嗦、昂贵、容易泄露信息,还可能记住过期事实。
10.2 记忆不是单一概念
10.2.1 短期记忆
短期记忆解决的是当前会话的连续性,例如:
- 用户刚刚给出的订单号;
- 当前对话中确认过的条件;
- 本次任务的中间步骤。
10.2.2 长期记忆
长期记忆保存跨会话依然有价值的信息,例如:
- 用户所属部门;
- 常用语言偏好;
- 经常关注的项目;
- 已确认的操作习惯。
10.2.3 摘要记忆
不是保存原文,而是压缩成关键结论,例如:
- 用户负责华东区渠道业务;
- 当前项目目标是搭建知识库问答系统;
- 偏好简洁回答,最好带表格。
10.2.4 档案记忆
档案记忆更像结构化档案或用户画像,适合存:
- 账号属性;
- 权限等级;
- 组织归属;
- 固定配置。
10.3 记忆究竟应该存什么
一个实用原则是:只存对未来决策仍有帮助的信息。
适合保存:
- 稳定偏好;
- 已确认事实;
- 当前任务摘要;
- 下轮仍会用到的状态字段。
不适合保存:
- 冗长闲聊;
- 未确认猜测;
- 敏感原文全文;
- 一次性噪声输出。
10.4 会话历史缓冲:最简单的短期记忆
10.4.1 直接保留最近几轮消息
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage history = [ SystemMessage(content="你是企业 IT 助手。"), HumanMessage(content="VPN 连不上怎么办?"), AIMessage(content="先检查网络和账号状态,如果仍失败联系 IT 服务台。"), HumanMessage(content="那我昨天已经重置过密码了。") ]
这种方式最适合:
- 原型阶段;
- 较短会话;
- 临时 Demo。
10.4.2 简单裁剪函数
def trim_history(messages, keep_last=8): system_messages = messages[:1] others = messages[1:] return system_messages + others[-keep_last:]
但要注意,这种方式很快会遇到两个问题:
- 旧消息越来越长;
- 重要结论会在裁剪时丢失。
10.5 用摘要记忆压缩长对话
对于较长会话,更好的方式通常是“近期原文 + 历史摘要”。
10.5.1 一个简单摘要函数示意
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate summary_prompt = ChatPromptTemplate.from_template(""" 请把以下对话总结成给后续助手看的会话摘要。 要求保留: 1. 用户目标 2. 已确认事实 3. 未解决问题 4. 用户偏好 对话: {conversation} """) summarizer = summary_prompt | ChatOpenAI(model="gpt-4o-mini", temperature=0)
10.5.2 摘要如何接回主链路
summary_message = SystemMessage( content="会话摘要:用户为财务部员工,正在咨询差旅报销规则,已确认出差日期为 4 月 1 日至 4 月 3 日。" )
之后把 `summary_message` 插入消息列表,再拼接最近几轮原文,就能在有限 token 预算里保持连续性。
10.6 长期记忆与用户档案
长期记忆更适合存成结构化字段,而不是大量历史原文。
10.6.1 一个用户档案示例
user_profile = { "user_id": "u_001", "department": "finance", "language": "zh-CN", "preferred_response_style": "concise", "frequent_topics": ["报销", "预算", "采购流程"] }
10.6.2 在 Prompt 中使用档案信息
from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_template(""" 你是企业办公助手。 用户档案:{profile} 请根据用户档案和问题给出回答。 问题:{question} """)
这类设计的优点是:
- 稳定;
- 易于审计;
- 容易更新和失效;
- 不会像聊天原文那样越来越冗长。
10.7 在 Agent 中使用短期记忆
根据 LangChain 官方短期记忆文档,现代 Agent 往往借助 checkpointer + thread_id 来实现线程级状态保存。
10.7.1 一个带 InMemorySaver 的 Agent
from langgraph.checkpoint.memory import InMemorySaver from langchain.agents import create_agent checkpointer = InMemorySaver() agent = create_agent( model="openai:gpt-4o-mini", tools=[], checkpointer=checkpointer, system_prompt="你是一个支持多轮对话的企业助手。" ) config = {"configurable": {"thread_id": "thread-001"}} agent.invoke( {"messages": [{"role": "user", "content": "我来自财务部。"}]}, config=config, ) agent.invoke( {"messages": [{"role": "user", "content": "我刚才说我是哪个部门的?"}]}, config=config, )
在这个例子里:
- `thread_id` 用于标识同一个会话线程;
- `checkpointer` 负责保存状态;
- 第二次调用时,Agent 可以基于第一次的状态继续回答。
10.8 自定义状态字段
在复杂 Agent 或 LangGraph 中,记忆往往不只是消息,还包括结构化状态字段。
10.8.1 一个自定义状态示意
from typing import TypedDict, NotRequired class CustomState(TypedDict): messages: list user_profile: NotRequired[dict] current_order_id: NotRequired[str] last_summary: NotRequired[str] preferred_language: NotRequired[str]
这种方式特别适合:
- 将短期状态显式字段化;
- 减少模型在历史中反复“找信息”;
- 支持图节点之间共享状态。
10.9 记忆更新策略
不是每条信息都值得写进长期记忆。建议使用规则:
- 用户明确确认过;
- 对未来决策有持续价值;
- 可结构化表达;
- 不是高风险敏感原文;
- 有失效规则。
10.9.1 一个简单的“只保存确认事实”示例
def should_store_as_memory(text: str) -> bool: keywords = ["我是", "我负责", "以后都", "偏好", "请默认"] return any(k in text for k in keywords)
真实项目中通常会用:
- 规则;
- 分类器;
- 结构化提取;
- 人工确认;
组合来决定是否写入长期记忆。
10.10 记忆失效机制
如果没有失效机制,系统会把过期事实当真理。例如:
- 用户已调岗,但系统还记着旧部门;
- 审批规则已更新,但记忆还是旧版本;
- 用户偏好曾经有效,但现在已变。
建议你为记忆设计:
- TTL(过期时间);
- 版本号;
- 最近确认时间;
- 明确覆盖规则。
memory_record = { "fact": "用户偏好英文输出", "confirmed_at": "2026-04-03T10:00:00", "expires_at": "2026-07-03T10:00:00", "source": "user_confirmed" }
10.11 何时不该使用 Memory
以下场景要谨慎:
- 法务、财务等高敏感场景,除非你有明确合规方案;
- 单轮问答;
- 信息必须每次实时核验的场景,如价格、库存、政策即时状态;
- 团队尚未建立基础日志与权限控制时。
10.12 一个“近期原文 + 历史摘要 + 档案字段”的组合案例
conversation_state = { "profile": { "department": "finance", "language": "zh-CN", "style": "concise" }, "summary": "用户正在咨询差旅报销流程,已确认出差日期和目的地。", "recent_messages": [ {"role": "user", "content": "报销单需要先给主管审批吗?"}, {"role": "assistant", "content": "是的,通常需要主管审批后再提交财务。"} ] }
然后你可以在主 Prompt 中分层使用:
- profile → 用于风格和权限控制;
- summary → 用于压缩长期上下文;
- recent_messages → 用于保留最近语义连续性。
10.13 本章小结
- Memory 本质上是状态管理问题,而不只是聊天记录缓存;
- 短期、长期、摘要、档案记忆应区别设计;
- `checkpointer + thread_id` 是现代 Agent 的常见短期记忆方式;
- 结构化状态字段比无脑堆消息更稳定;
- 必须为记忆设计写入规则、更新策略和失效机制。
练习
1. 为一个企业办公助手设计短期记忆、长期记忆和档案记忆三层结构。 2. 使用 `InMemorySaver` 搭建一个支持 thread_id 的简单多轮 Agent。 3. 设计一个“只保存已确认事实”的记忆筛选器。 4. 为你的系统写一份记忆失效规则,说明哪些信息多久失效、谁可以覆盖它。 5. 将某个多轮问答场景改造成“近期原文 + 历史摘要 + 档案字段”的结构。
参考资源
10.14 补充案例:从聊天记录提取长期记忆
下面给出一个更具体的思路:让系统从长对话里自动提取“值得长期保存”的事实。
from pydantic import BaseModel, Field from typing import Literal from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate class MemoryCandidate(BaseModel): fact: str = Field(description="可被长期保存的事实") category: Literal["profile", "preference", "task", "other"] should_store: bool = Field(description="是否建议存入长期记忆") reason: str = Field(description="为什么值得存或不值得存") extract_prompt = ChatPromptTemplate.from_template(""" 请从下面对话中识别是否存在值得长期保存的信息。 仅保留以下类型: 1. 用户稳定身份信息 2. 用户明确确认的偏好 3. 会跨轮使用的重要任务事实 对话: {conversation} """) extractor = extract_prompt | ChatOpenAI(model="gpt-4o-mini", temperature=0).with_structured_output(MemoryCandidate) candidate = extractor.invoke({ "conversation": "用户说:我是财务部员工,以后默认用简洁中文回答我。" }) print(candidate)
这个模式的价值在于:
- 不是无脑存所有聊天;
- 让“记忆写入”本身也可解释;
- 便于在写入前做人工复核或规则过滤。
10.15 补充案例:把会话摘要与用户档案一起注入 Prompt
from langchain_core.prompts import ChatPromptTemplate assistant_prompt = ChatPromptTemplate.from_template(""" 你是企业办公助手。 用户档案: {profile} 历史摘要: {summary} 当前问题: {question} """) result = assistant_prompt.invoke({ "profile": {"department": "finance", "style": "concise"}, "summary": "用户正在咨询 4 月初的差旅报销流程,已确认出差城市为上海。", "question": "住宿发票忘开发票抬头怎么办?" }) print(result)
这类设计很适合:
- 对话很长但仍需保留连续性;
- 有一些稳定档案字段需要持续生效;
- 希望把“聊天历史”和“用户画像”分开管理。