# LangChain 摘要记忆实践:长对话压缩、短期记忆与生产治理
对话一长,单纯把历史消息塞进 Prompt 就会变得很笨重。
前十轮还好,五十轮以后就会开始暴露问题:token 成本上涨、响应变慢、上下文窗口被旧消息占满、模型被早期无关内容带偏。直接裁剪历史能省 token,但会丢信息;全部保留又撑不住成本和窗口。摘要记忆就是为了解决这个矛盾:把较早的对话压缩成一段摘要,再保留最近几轮原始消息。
它的目标不是让模型“永久记住一切”,而是在当前会话里用更低 token 成本维持连续性。
# 摘要记忆适合解决什么问题
摘要记忆适合下面几类场景:
- 用户和助手持续讨论一个任务,例如需求澄清、方案设计、代码审查、排障分析。
- 历史里有一些关键事实需要保留,但完整对话细节不必每轮都带上。
- 对话长度可能超过模型上下文窗口,需要在丢弃和保留之间找平衡。
- 近期消息很重要,早期消息只需要保留结论、约束、决策和待办。
它不适合下面几类场景:
- 需要逐字引用历史内容。
- 历史消息具有法律、审计或合规意义。
- 对话里包含大量代码、表格、精确数字,摘要容易丢细节。
- 用户长期偏好和画像需要跨会话保存。
一句话:摘要记忆适合“会话内压缩”,不适合替代原始记录、长期记忆和审计日志。
# 旧版摘要记忆的两种形态
早期 LangChain 里常见的摘要记忆主要有两个类。
第一个是 ConversationSummaryMemory。
它会不断把历史对话总结成一段摘要。下一轮调用时,Prompt 里放的不是完整消息列表,而是当前滚动摘要。这个方式非常省 token,但会牺牲最近对话的细节。它更像“滚动会议纪要”。
第二个是 ConversationSummaryBufferMemory。
它结合了摘要和缓冲:近期消息保留原始形式,旧消息超过 token 限制后被总结进摘要。这个方式比纯摘要更实用,因为模型仍然能看到最近几轮的完整表达,同时又不会让早期历史无限增长。
可以把两者理解成:
| 类型 | 保留内容 | 优点 | 风险 |
|---|---|---|---|
ConversationSummaryMemory | 滚动摘要 | token 成本低 | 近期细节容易丢 |
ConversationSummaryBufferMemory | 摘要 + 最近消息 | 平衡成本和上下文连续性 | 摘要质量影响后续回答 |

从流程上看,摘要记忆会额外引入一个“中间模型调用”:主模型负责回答用户,摘要模型负责把历史对话压缩成新的摘要。纯摘要记忆只把滚动摘要交给主模型;摘要缓冲混合记忆则同时保留近期原始消息和历史摘要,超过 token 限制后再把旧消息总结进去。
旧版实现的核心流程通常是:
保存消息
-> 计算当前历史 token
-> 超过 max_token_limit
-> 弹出较早消息
-> 用 LLM 把弹出的消息合并进已有摘要
-> Prompt 中使用摘要和剩余近期消息
2
3
4
5
6
这套思路今天仍然成立,只是新项目不建议再围绕 classic Memory 类搭主架构。
# 旧代码兼容写法
维护老项目时,可以使用 langchain-classic 里的 ConversationSummaryBufferMemory。
安装:
pip install langchain-classic
一个 LCEL 链的兼容写法如下:
from operator import itemgetter
from langchain_classic.memory import ConversationSummaryBufferMemory
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
from langchain_openai import ChatOpenAI
summary_llm = ChatOpenAI(model="gpt-4.1-mini")
chat_llm = ChatOpenAI(model="gpt-4.1-mini")
memory = ConversationSummaryBufferMemory(
llm=summary_llm,
input_key="query",
memory_key="history",
return_messages=True,
max_token_limit=800,
)
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个严谨的 Python 后端助手。"),
MessagesPlaceholder("history"),
("human", "{query}"),
]
)
chain = (
RunnablePassthrough.assign(
history=RunnableLambda(memory.load_memory_variables) | itemgetter("history")
)
| prompt
| chat_llm
| StrOutputParser()
)
query = "我们继续讨论 Flask 项目的数据库迁移上线流程。"
chain_input = {"query": query}
answer = chain.invoke(chain_input)
memory.save_context(chain_input, {"output": answer})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
这里有两个模型角色:
summary_llm:负责把旧消息压缩成摘要。chat_llm:负责回答用户当前问题。
小项目里可以用同一个模型。生产里通常会分开:摘要可以用更便宜的模型,但不能便宜到总结质量不可控;核心回答可以用更强的模型。
# 旧版实现的生产问题
ConversationSummaryBufferMemory 的思路很好,但直接上生产会遇到一些现实问题。
第一,摘要也是模型输出,会出错。
模型可能漏掉约束、合并错误事实、把不确定信息写成确定事实。摘要一旦进入后续上下文,错误会被持续放大。
第二,摘要会带来额外延迟。
当历史超过 token 限制时,系统要额外调用一次模型生成摘要。对交互式聊天来说,这会明显增加尾延迟。
第三,摘要内容不应该混成普通系统指令。
旧实现里,摘要经常以 system message 的形式塞进 Prompt。部分模型或供应商对多条 system message 支持并不一致;有些场景下,多条 system message 还会和业务系统提示互相干扰。
第四,摘要不能替代原始消息存储。
摘要适合给模型看,原始消息适合审计、回放、调试、重新生成摘要。只保存摘要不保存原始消息,会让后续排障非常痛苦。
第五,摘要触发策略要和业务绑定。
固定 max_token_limit=300 这类写法只适合演示。生产里要按模型窗口、系统提示长度、工具 schema 长度、RAG 内容预算、输出预算一起计算。
# 新版实现:SummarizationMiddleware
当前 LangChain 的新主线是 Agent state + checkpointer + middleware。对摘要记忆来说,最直接的实现是 SummarizationMiddleware。
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model="gpt-5.5",
tools=[],
system_prompt="你是一个严谨的 Python 后端助手。",
middleware=[
SummarizationMiddleware(
model="gpt-5.4-mini",
trigger=("tokens", 4000),
keep=("messages", 20),
)
],
checkpointer=InMemorySaver(),
)
config = {"configurable": {"thread_id": "user-1:chat-1001"}}
agent.invoke(
{"messages": [{"role": "user", "content": "我正在做 Flask 项目。"}]},
config,
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "继续说数据库迁移上线的回滚方案。"}]},
config,
)
print(result["messages"][-1].content)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
这段代码里,短期记忆不再由一个 memory 对象外挂到 chain 上,而是进入 agent 的执行状态。thread_id 决定本次请求属于哪段会话,checkpointer 负责保存状态,SummarizationMiddleware 在模型调用前处理过长历史。
参数可以这样理解:
model:用于生成摘要的模型。trigger=("tokens", 4000):当消息历史达到指定 token 条件时触发摘要。keep=("messages", 20):摘要后仍保留最近 20 条消息。
这个组合对应的是旧版 ConversationSummaryBufferMemory 的新版思路:早期内容压缩成摘要,近期内容保留原文。
# 生产实现:摘要和持久化要分开设计
本地测试可以用 InMemorySaver,生产环境应该使用数据库 backed checkpointer。以 Postgres 为例:
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langgraph.checkpoint.postgres import PostgresSaver
DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
checkpointer.setup()
agent = create_agent(
model="gpt-5.5",
tools=[],
system_prompt="你是一个严谨的 Python 后端助手。",
middleware=[
SummarizationMiddleware(
model="gpt-5.4-mini",
trigger=("tokens", 4000),
keep=("messages", 20),
)
],
checkpointer=checkpointer,
)
response = agent.invoke(
{"messages": [{"role": "user", "content": "继续之前的迁移方案。"}]},
{"configurable": {"thread_id": "tenant-1:user-42:chat-1001"}},
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
这里要注意一个边界:checkpoint 保存的是会话状态,摘要是给模型调用使用的上下文压缩结果。生产系统最好同时保留原始消息日志,这样可以在摘要质量出问题时重新生成、回放和审计。
推荐的存储分层是:
| 数据 | 用途 | 存储建议 |
|---|---|---|
| 原始消息 | 审计、回放、重新摘要 | 业务数据库或日志系统 |
| checkpoint state | 会话恢复、Agent 状态延续 | LangGraph checkpointer |
| 摘要内容 | 降低上下文成本 | state 或专门摘要表 |
| 长期记忆 | 跨会话用户偏好、事实 | profile store / vector store |
不要把这四类数据混成一个字段。
# 摘要提示词要可控
摘要不是简单地“总结一下”。生产里最好明确摘要格式,约束模型只保留对后续任务有用的信息。
可以把摘要要求设计成这样:
请更新当前会话摘要,只保留会影响后续回答的信息:
1. 用户明确确认的事实
2. 已做出的技术决策
3. 尚未完成的待办
4. 当前任务约束和风险
不要保留寒暄、重复表达、无关闲聊。
不要把模型推测写成用户事实。
如果新消息纠正了旧信息,以新信息为准。
2
3
4
5
6
7
8
9
如果摘要里同时包含事实、决策和待办,建议结构化:
{
"facts": ["用户项目使用 Flask 和 PostgreSQL"],
"decisions": ["数据库变更通过 Flask-Migrate 生成迁移脚本"],
"todos": ["补充回滚预案和上线检查清单"],
"risks": ["迁移脚本上线前必须人工 review"]
}
2
3
4
5
6
结构化摘要更容易做 diff、测试和冲突处理。自然语言摘要读起来更顺,但不利于程序治理。
# 摘要触发策略
摘要触发不应该只看消息数量。更稳的策略是按预算拆分。
一次模型调用的上下文大致包括:
系统提示
+ 工具 schema
+ 当前用户输入
+ RAG 检索内容
+ 历史摘要
+ 最近消息
+ 预留输出 token
2
3
4
5
6
7
摘要触发点应该从总窗口倒推,而不是拍脑袋写死。
例如模型上下文窗口是 128k token,但你不应该让历史消息用满 128k。可以先预留:
- 系统提示和工具 schema:10k
- RAG 内容:40k
- 输出预算:8k
- 安全余量:10k
那么历史消息最多使用大约 60k。超过这个阈值再触发摘要,或者更保守地在 40k 左右触发。
如果是客服、表单填报、轻量助手,阈值可以低很多。摘要策略必须和业务延迟、成本、模型窗口、回答质量一起调。
# 摘要质量怎么验证
摘要记忆最容易被忽视的是测试。
不要只看“能不能跑”,而要验证摘要是否保留了关键事实。可以设计几类测试集:
- 用户名字、项目背景、技术栈是否保留。
- 用户纠正旧事实后,摘要是否覆盖旧事实。
- 多轮决策后,摘要是否保留最终决策而不是中间方案。
- 敏感信息是否被脱敏或排除。
- 工具调用结果是否被正确归纳。
- 摘要后模型还能不能回答前文依赖问题。
一个很实用的做法是保存摘要前后的样本:
{
"thread_id": "tenant-1:user-42:chat-1001",
"summary_version": 7,
"messages_before_count": 48,
"messages_after_keep_count": 20,
"summary_tokens": 650,
"summary_model": "gpt-5.4-mini",
"trigger_reason": "tokens>=4000"
}
2
3
4
5
6
7
8
9
线上出现“模型忘了前面说过什么”时,这些字段能快速定位是摘要漏了、裁剪过头了,还是 thread_id 串错了。
# 摘要记忆和长期记忆的边界
摘要记忆经常被误用成长期记忆,这是一个坑。
例如用户说:
以后回答我时,默认给生产实践版本。
这不应该只压进当前会话摘要里。它更像用户偏好,应该写入长期记忆或用户 profile。
而下面这种信息更适合摘要记忆:
这次排障已经确认:服务没有重启,连接池耗尽发生在发布后 20 分钟。
它对当前任务有价值,但未必需要跨会话长期保存。
可以这样划分:
| 类型 | 生命周期 | 适合位置 |
|---|---|---|
| 当前任务背景 | 当前会话 | 摘要记忆 |
| 最近对话细节 | 当前会话 | 最近 messages |
| 用户稳定偏好 | 跨会话 | 长期记忆 |
| 关键业务事实 | 跨会话或项目级 | 数据库 / 知识库 |
| 审计记录 | 长期留存 | 原始消息日志 |
摘要是上下文压缩,不是事实库。
# 迁移建议
如果项目里已经用了 ConversationSummaryMemory 或 ConversationSummaryBufferMemory,可以按下面方式迁移。
第一步,把旧 Memory 类隔离在兼容层。
不要让业务代码到处直接依赖 memory.load_memory_variables 和 memory.save_context。先包一层接口,方便切换到 Agent state 或 middleware。
第二步,统一 thread_id。
摘要一定要按会话隔离。Web 服务里不能用全局 memory 对象,也不能只用用户 ID 作为会话 ID,否则多个话题会互相污染。
第三步,新 Agent 迁到 SummarizationMiddleware。
让摘要成为 middleware,而不是散落在业务调用前后的手写逻辑。这样更容易复用、观测和测试。
第四步,保留原始消息。
摘要可以覆盖、重写、压缩,但原始消息最好保留。尤其是生产排障、用户投诉、质量评估和重新摘要都会依赖原始记录。
第五步,为摘要设计评估集。
每次改摘要模型、触发阈值、提示词、保留消息数量,都应该跑一遍评估。摘要记忆是质量组件,不只是性能优化组件。
# 问题
摘要记忆的问题,是它把“压缩上下文”交给了模型,而模型总结并不天然可靠。
它可能漏掉关键约束,把临时信息写成稳定事实,合并两个不同项目,或者在摘要里引入原对话没有确认过的推断。摘要一旦进入后续上下文,错误会持续影响回答。
另一个问题是延迟和成本。摘要触发时通常要额外调用一次模型,长对话里可能频繁触发。如果摘要模型太弱,质量不稳;如果摘要模型太强,成本又会上升。
还有一个常被忽视的问题:摘要不是审计记录。只保存摘要、不保存原始消息,会让回放、纠错、重新摘要和用户投诉处理都变困难。
# 拓展
摘要记忆可以拓展成结构化会话状态,而不是只保存一段自然语言。
例如把摘要拆成:
{
"facts": ["项目使用 Flask 和 PostgreSQL"],
"decisions": ["迁移脚本上线前必须人工 review"],
"todos": ["补充回滚预案"],
"risks": ["旧数据迁移需要备份验证"]
}
2
3
4
5
6
这样后续可以做 diff、覆盖、测试和冲突处理。
还可以把摘要和长期记忆分开:会话摘要只服务当前 thread,用户稳定偏好和项目事实写入 Store。摘要负责压缩,Store 负责跨会话事实。
# 实际生产是否使用
会使用,尤其是长对话和长任务。
需求讨论、代码审查、排障、方案设计、咨询式对话都很适合摘要记忆。生产里常见做法是“最近消息 + 较早摘要”,既保留细节,又控制 token。
但生产不会把摘要当唯一事实来源。更稳的做法是保留原始消息日志,把摘要作为派生数据;摘要出错时可以重新生成,也可以通过评估集验证质量。
# 现在是否抛弃
旧版 ConversationSummaryMemory 和 ConversationSummaryBufferMemory 已经不再是新项目主线,更多属于 classic Memory 体系。
摘要记忆这个模式没有被抛弃。当前 LangChain 官方短期记忆文档仍然把 summarize messages 作为长对话处理的常见模式,只是实现方式转向 middleware 和 Agent state。
所以结论是:旧类弱化,摘要模式保留并升级。
# 最新生产如何实现
最新版推荐用 SummarizationMiddleware 配合 checkpointer。
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langgraph.checkpoint.postgres import PostgresSaver
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
checkpointer.setup()
agent = create_agent(
model="gpt-5.5",
tools=[],
system_prompt="你是一个严谨的技术助手。",
middleware=[
SummarizationMiddleware(
model="gpt-5.4-mini",
trigger=("tokens", 4000),
keep=("messages", 20),
)
],
checkpointer=checkpointer,
)
agent.invoke(
{"messages": [{"role": "user", "content": "继续刚才的迁移方案。"}]},
{"configurable": {"thread_id": "tenant-1:user-42:chat-1001"}},
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
生产里建议同时记录摘要元数据:
- 摘要覆盖的 message 范围。
- 摘要模型和 prompt 版本。
- 触发原因和 token 预算。
- 摘要前后保留消息数量。
- 是否包含敏感信息过滤结果。
这样摘要记忆才可观测、可回放、可评估。
# 总结
摘要记忆的价值,是用较低 token 成本维持长对话里的任务连续性。旧版 ConversationSummaryMemory 和 ConversationSummaryBufferMemory 适合理解原理或维护老项目;新项目更推荐 create_agent + checkpointer + SummarizationMiddleware。
生产里真正重要的是边界:
- 摘要只压缩上下文,不替代原始消息。
- 摘要只服务当前会话,不默认等于长期记忆。
- 摘要会出错,必须评估和观测。
- 摘要触发要按 token 预算和业务成本设计。
- 对 Agent 来说,摘要应该进入 state/middleware 体系,而不是散落在 chain 外部。
参考: