# LangChain 摘要记忆实践:长对话压缩、短期记忆与生产治理

对话一长,单纯把历史消息塞进 Prompt 就会变得很笨重。

前十轮还好,五十轮以后就会开始暴露问题:token 成本上涨、响应变慢、上下文窗口被旧消息占满、模型被早期无关内容带偏。直接裁剪历史能省 token,但会丢信息;全部保留又撑不住成本和窗口。摘要记忆就是为了解决这个矛盾:把较早的对话压缩成一段摘要,再保留最近几轮原始消息。

它的目标不是让模型“永久记住一切”,而是在当前会话里用更低 token 成本维持连续性。

# 摘要记忆适合解决什么问题

摘要记忆适合下面几类场景:

  • 用户和助手持续讨论一个任务,例如需求澄清、方案设计、代码审查、排障分析。
  • 历史里有一些关键事实需要保留,但完整对话细节不必每轮都带上。
  • 对话长度可能超过模型上下文窗口,需要在丢弃和保留之间找平衡。
  • 近期消息很重要,早期消息只需要保留结论、约束、决策和待办。

它不适合下面几类场景:

  • 需要逐字引用历史内容。
  • 历史消息具有法律、审计或合规意义。
  • 对话里包含大量代码、表格、精确数字,摘要容易丢细节。
  • 用户长期偏好和画像需要跨会话保存。

一句话:摘要记忆适合“会话内压缩”,不适合替代原始记录、长期记忆和审计日志。

# 旧版摘要记忆的两种形态

早期 LangChain 里常见的摘要记忆主要有两个类。

第一个是 ConversationSummaryMemory。

它会不断把历史对话总结成一段摘要。下一轮调用时,Prompt 里放的不是完整消息列表,而是当前滚动摘要。这个方式非常省 token,但会牺牲最近对话的细节。它更像“滚动会议纪要”。

第二个是 ConversationSummaryBufferMemory。

它结合了摘要和缓冲:近期消息保留原始形式,旧消息超过 token 限制后被总结进摘要。这个方式比纯摘要更实用,因为模型仍然能看到最近几轮的完整表达,同时又不会让早期历史无限增长。

可以把两者理解成:

类型 保留内容 优点 风险
ConversationSummaryMemory 滚动摘要 token 成本低 近期细节容易丢
ConversationSummaryBufferMemory 摘要 + 最近消息 平衡成本和上下文连续性 摘要质量影响后续回答

LangChain 摘要记忆的两类流程

从流程上看,摘要记忆会额外引入一个“中间模型调用”:主模型负责回答用户,摘要模型负责把历史对话压缩成新的摘要。纯摘要记忆只把滚动摘要交给主模型;摘要缓冲混合记忆则同时保留近期原始消息和历史摘要,超过 token 限制后再把旧消息总结进去。

旧版实现的核心流程通常是:

保存消息
  -> 计算当前历史 token
  -> 超过 max_token_limit
  -> 弹出较早消息
  -> 用 LLM 把弹出的消息合并进已有摘要
  -> Prompt 中使用摘要和剩余近期消息
1
2
3
4
5
6

这套思路今天仍然成立,只是新项目不建议再围绕 classic Memory 类搭主架构。

# 旧代码兼容写法

维护老项目时,可以使用 langchain-classic 里的 ConversationSummaryBufferMemory。

安装:

pip install langchain-classic
1

一个 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})
1
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)
1
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"}},
    )
1
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. 当前任务约束和风险

不要保留寒暄、重复表达、无关闲聊。
不要把模型推测写成用户事实。
如果新消息纠正了旧信息,以新信息为准。
1
2
3
4
5
6
7
8
9

如果摘要里同时包含事实、决策和待办,建议结构化:

{
  "facts": ["用户项目使用 Flask 和 PostgreSQL"],
  "decisions": ["数据库变更通过 Flask-Migrate 生成迁移脚本"],
  "todos": ["补充回滚预案和上线检查清单"],
  "risks": ["迁移脚本上线前必须人工 review"]
}
1
2
3
4
5
6

结构化摘要更容易做 diff、测试和冲突处理。自然语言摘要读起来更顺,但不利于程序治理。

# 摘要触发策略

摘要触发不应该只看消息数量。更稳的策略是按预算拆分。

一次模型调用的上下文大致包括:

系统提示
  + 工具 schema
  + 当前用户输入
  + RAG 检索内容
  + 历史摘要
  + 最近消息
  + 预留输出 token
1
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"
}
1
2
3
4
5
6
7
8
9

线上出现“模型忘了前面说过什么”时,这些字段能快速定位是摘要漏了、裁剪过头了,还是 thread_id 串错了。

# 摘要记忆和长期记忆的边界

摘要记忆经常被误用成长期记忆,这是一个坑。

例如用户说:

以后回答我时,默认给生产实践版本。
1

这不应该只压进当前会话摘要里。它更像用户偏好,应该写入长期记忆或用户 profile。

而下面这种信息更适合摘要记忆:

这次排障已经确认:服务没有重启,连接池耗尽发生在发布后 20 分钟。
1

它对当前任务有价值,但未必需要跨会话长期保存。

可以这样划分:

类型 生命周期 适合位置
当前任务背景 当前会话 摘要记忆
最近对话细节 当前会话 最近 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": ["旧数据迁移需要备份验证"]
}
1
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"}},
    )
1
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 外部。

参考: