LangChain 实战:玩转短期记忆

文章来源声明: 原文作者:对齐token; 来源站点:掘金; 原文链接:https://juejin.cn/post/7685648064543916078; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

从概念、存储选型到防爆窗三种模式,路径清晰、落地性强,适合已上手 LangGraph 的开发者解决会话持久化与上下文超限问题。

大模型天生就是个“金鱼脑”,聊完就忘。

上下文工程,就是让它在干当前这事的时候,能把前面聊的、工具查到的东西“摊在桌面上”。不然它干到第三步就忘了第一步说了啥,直接瞎指挥。

记忆,就是让它下次还能认出你——你上周跟它说“我过敏不吃香菜”,这周它还能想起来。

一句话总结:上下文工程管“眼前这摊事别断片”,记忆管“别每次都像第一次见面”。

一、概述

1.1 什么是短期记忆?

在 AI Agent 的世界里,**记忆(Memory)**是一套用于留存历史交互信息的机制。

对于 Agent 而言,记忆至关重要:它使 Agent 能够记住过往对话、从反馈中学习、适配用户偏好

短期记忆:让应用在单条会话线程内记住历史交互。

1.2 langchain 三类上下文

官方文档划分:静态运行上下文、动态运行上下文(短期记忆)、跨会话动态上下文(长期记忆)

img

1.3 使用方法

要为 Agent 开启短期记忆(线程级持久化),创建 Agent 的时候必须指定检查点器 (checkpointer)

LangChain Agent 将短期记忆作为 Agent 状态的一部分进行管理。

把对话上下文保存在图状态中,Agent 既可以拿到单条对话的完整上下文,又可以做到不同线程之间数据相互隔离

状态通过 checkpointer 持久化到数据库(或内存),因此任意时刻都可以恢复这条线程继续执行。

Agent 被调用、或是完成一步(例如工具调用)时,短期记忆就会更新;每一步执行开始时都会读取状态

<span>from</span> langchain.agents <span>import</span> create_agent
<span>from</span> langgraph.checkpoint.memory <span>import</span> InMemorySaver  


<span>def</span> <span>get_user_info</span>() -> <span>str</span>:
    <span>"""查看用户信息"""</span>
    <span>return</span> <span>"现在还没有用户"</span>


agent = create_agent(
    model=<span>"openai:gpt-5.5"</span>,
    tools=[get_user_info],
    checkpointer=InMemorySaver(),
)

1.4 存储

不管是长期记忆还是短期记忆,都有两种存储记忆的方式:

  • 基于内存
  • 基于外部存储

二、使用方式

2.1 通过内存

<span>from</span> langchain.agents <span>import</span> create_agent
<span>from</span> langgraph.checkpoint.memory <span>import</span> InMemorySaver


<span>def</span> <span>get_user_info</span>() -> <span>str</span>:
    <span>"""查看用户信息."""</span>
    <span>return</span> <span>"空空如也."</span>


agent = create_agent(
    model=model,
    tools=[get_user_info],
    checkpointer=InMemorySaver(),
)

thread_config = {<span>"configurable"</span>: {<span>"thread_id"</span>: <span>"1"</span>}}
response = agent.invoke(
    {<span>"messages"</span>: [{<span>"role"</span>: <span>"user"</span>, <span>"content"</span>: <span>"哈喽,我是张三."</span>}]},
    thread_config,
)[<span>"messages"</span>][-<span>1</span>].content

rprint(response)

response = agent.invoke(
    {<span>"messages"</span>: [{<span>"role"</span>: <span>"user"</span>, <span>"content"</span>: <span>"我是谁?"</span>}]},
    thread_config,
)[<span>"messages"</span>][-<span>1</span>].content

rprint(response)

img

2.2 使用pgsql

在生产环境中,使用由数据库支持的checkpointer

安装pgsql包

pip install -U langgraph-checkpoint-postgres <span>"psycopg[binary]"</span>

默认情况下,langgraph-checkpoint-postgres 会安装 psycopg(Psycopg 3),但不包含额外组件。

上述安装方式会添加 psycopg[binary],这是大多数用户的推荐选择。

<span>from</span> langchain.agents <span>import</span> create_agent
<span>from</span> langgraph.checkpoint.postgres <span>import</span> PostgresSaver

<span>def</span> <span>get_user_info</span>() -> <span>str</span>:
    <span>"""查看用户信息."""</span>
    <span>return</span> <span>"空空如也."</span>


DB_URI = <span>"postgresql://admin:Pw%40123456@133.133.133.133:5432/mydatabase?sslmode=disable"</span>

<span>with</span> PostgresSaver.from_conn_string(DB_URI) <span>as</span> checkpointer:
    checkpointer.setup() <span># 会在pgsql中自动创建表</span>
    agent = create_agent(
        model=model,
        tools=[get_user_info],
        checkpointer=checkpointer,
    )

看一下,自动创建的表结构

img

这四张表都是 setup() 函数初始化时创建的。

  • checkpoints :这是主表,存每个 thread 在某个时刻的 checkpoint 快照。
  • checkpoint_blobs :这张表专门存不适合直接内联进 checkpoints.checkpoint 的较复杂 channel 值。
  • checkpoint_writes :这张表存的是中间写入 / pending writes,不是最终完整 checkpoint。
  • checkpoint_migrations :这张表不是业务数据表,而是迁移版本表。

2.3 自定义 AgentState

默认情况下,agent 使用 AgentState 来管理短期内存

可以扩展 AgentState 以添加额外的字段。自定义状态 schema 通过 state_schema 参数传递给 create_agent。

<span><span>class</span> <span>CustomAgentState</span><span>(AgentState)</span>:
    user_id: str
    preferences: dict


agent =</span> <span>create_agent</span>(
    model = model,
    tools = [get_user_info],
    state_schema = CustomAgentState,
    checkpointer = <span>InMemorySaver</span>(),
)

thread_config = {<span>"configurable"</span>: {<span>"thread_id"</span>: <span>"1"</span>}}
response = agent.<span>invoke</span>(
    {
     <span>"messages"</span>: [{<span>"role"</span>: <span>"user"</span>, <span>"content"</span>: <span>"哈喽,我是张三."</span>}],
     <span>"user_id"</span>: <span>"user_123"</span>,
     <span>"preferences"</span>: {<span>"theme"</span>: <span>"dark"</span>}
     },
    thread_config,
)[<span>"messages"</span>][<span>-1</span>].content

<span>rprint</span>(response)


state = agent.<span>get_state</span>(thread_config)
<span>for</span> key, value in state.values.<span>items</span>():
    <span>print</span>(f<span>"{key}: {value}"</span>)

img

三、常用模式

3.1 裁剪消息

绝大多数大模型都存在最大支持上下文窗口(以 token 为计量单位)

一种消息截断策略:统计消息历史的 token 数量,当总量接近上限时执行截断

调用修剪消息工具函数,指定需要保留的 token 数量,同时配置边界处理策略(例如:保留最后的 max_tokens 条 token)

想要在 Agent 内部对消息历史做修剪,可以使用 before_model 中间件装饰器。

<span>@before_model</span>
<span>def</span> <span>trim_messages</span>(<span>state: AgentState, runtime: Runtime</span>) -> <span>dict</span>[<span>str</span>, <span>Any</span>] | <span>None</span>:
    <span>"""只保留最近5条消息到上下文中"""</span>
    messages = state[<span>"messages"</span>]

    <span># 消息<=3什么也不干</span>
    <span>if</span> <span>len</span>(messages) <= <span>3</span>:
        <span>return</span> <span>None</span>

    first_msg = messages[<span>0</span>]
    recent_messages = messages[-<span>3</span>:] <span>if</span> <span>len</span>(messages) % <span>2</span> == <span>0</span> <span>else</span> messages[-<span>4</span>:]
    new_messages = [first_msg] + recent_messages

    <span>return</span> {
        <span>"messages"</span>: [
            RemoveMessage(<span>id</span>=REMOVE_ALL_MESSAGES),
            <span># 解包后放入messages里</span>
            *new_messages
        ]
    }

agent = create_agent(
    model=model,
    middleware=[trim_messages],
    checkpointer=InMemorySaver(),
)

config: RunnableConfig = {<span>"configurable"</span>: {<span>"thread_id"</span>: <span>"1"</span>}}

agent.invoke({<span>"messages"</span>: <span>"哈喽,我是 bob"</span>}, config)
agent.invoke({<span>"messages"</span>: <span>"你家是不是有个加菲猫"</span>}, config)
agent.invoke({<span>"messages"</span>: <span>"不是,我家现在是一个 dog 吴"</span>}, config)
final_response = agent.invoke({<span>"messages"</span>: <span>"我叫什么?"</span>}, config)

<span>for</span> e <span>in</span> final_response[<span>"messages"</span>]:
    e.pretty_print()

打印结果如下:

img

提示:@before_model

3.2 截断 (truncate) vs 摘要 (summarize)

区别

  1. truncate /trim(修剪截断):直接删除旧消息,原始内容丢失;速度快,无额外模型调用开销。
  2. summarization(摘要):调用模型把旧消息浓缩成一段摘要文本,保留信息;会消耗额外 token,有调用成本。

before_model 是通用钩子:除了 trim 修剪,你也可以在这里自己实现摘要、过滤、增删消息。而 SummarizationMiddleware 是封装好的成品中间件;before_model 是底层自定义扩展点。

执行时机

before_model组装完消息,还没发给大模型那一刻执行。 只修改即将送入模型的消息,不会自动改写持久化在 checkpointer 里的 state 历史消息

3.3 删除消息

从图状态(graph state)中删除消息,以此管理消息历史。适合场景:移除指定消息,或者清空全部对话历史。

在图状态里删除消息,使用 RemoveMessage 对象。想要 RemoveMessage 生效,状态字段必须使用 add_messages 归约器(reducer)

LangGraph 默认的 AgentState 已经内置该归约器,开箱可用

  • 删除所有消息
<span>from</span> langgraph.graph.message <span>import</span> REMOVE_ALL_MESSAGES  

<span>def</span> <span>delete_messages</span>(<span>state</span>):
    <span>return</span> {<span>"messages"</span>: [RemoveMessage(<span>id</span>=REMOVE_ALL_MESSAGES)]}

  • 删除指定的消息
<span>from</span> langchain.messages <span>import</span> RemoveMessage  

<span>def</span> <span>delete_messages</span>(<span>state</span>):
    messages = state[<span>"messages"</span>]
    <span>if</span> <span>len</span>(messages) > <span>2</span>:
        <span># 删除开头的两条数据</span>
        <span>return</span> {<span>"messages"</span>: [RemoveMessage(<span>id</span>=m.<span>id</span>) <span>for</span> m <span>in</span> messages[:<span>2</span>]]}

输出详情:

img

Agent 删除消息处理时序图

img

3.4 摘要

消息截断或删除消息存在一个问题:清理消息队列时会直接丢失信息

如此,部分应用会采用更完善的方案:借助对话大模型对消息历史做摘要处理

img

详见:《Langchain实战:常用中间件Summarization》

四、总结

  • 短期记忆:解决大模型“金鱼脑”问题,保证单次会话(Thread)不“断片”。
  • 核心机制:靠 Checkpointer(检查点器) 持久化状态,用 thread_id 隔离会话。
  • 存储选型:开发调试用 内存,生产环境上 PostgreSQL
  • 防爆窗三招:消息太多时,要么 裁剪(Trim) 丢旧消息,要么 删除(Remove) 清缓存,要么 摘要(Summarize) 保核心。

上下文工程管眼前,记忆管长远。短期记忆只是上半场,搞定它,你的 Agent 才算真正“在线”。

觉得有用的话,点个赞或在看支持一下吧~