====== 第十章:核心能力详解 - 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. 将某个多轮问答场景改造成“近期原文 + 历史摘要 + 档案字段”的结构。
===== 参考资源 =====
* [[https://docs.langchain.com/oss/python/langchain/short-term-memory|LangChain 官方文档:Short-term memory]]
===== 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)
这类设计很适合:
* 对话很长但仍需保留连续性;
* 有一些稳定档案字段需要持续生效;
* 希望把“聊天历史”和“用户画像”分开管理。