目录

1. 什么是持久化能力?

没有持久化的情况

有持久化的情况

持久化能力包含两部分

2. 线程级持久化

2.1 线程级持久化是怎么工作的?

线程级持久化机制确保了:

2.2 核心概念

2.2.1 Threads(线程)

2.2.2 Checkpoints(检查点)

2.3 线程级持久化使用姿势

2.3.1 步骤一:配置 checkpointer 持久化存储

为什么选择 PostgreSQL 作为存储库?

2.3.2 步骤二:使用 Thread 进行执行

2.3.3 其他基本用法

2.3.3.1 获取状态快照

2.3.3.2 获取状态历史记录

2.3.3.3 重放

2.3.3.4 更新状态

3. 跨会话持久化

3.1 Checkpoint 的局限性

3.1.1 问题场景:跨会话信息丢失

3.1.2 现实世界的需求:从"单次对话"到"终身服务"

3.2 解决方案:引入 Store

3.2.1 存储 vs 检查点

3.2.2 引入 Store 后,AI 应用架构的范式转变

3.3 跨会话持久化使用姿势

3.3.1 方式1:内存存储

3.3.2 方式2:Postgres 存储库

Store 数据组织方式

Checkpoint + Store 完整示例


线程级持久与跨会话持久

1. 什么是持久化能力?

简单来说,在 LangGraph 中持久化能力指的是将 AI 应用的状态(如对话历史、中间结果、用户信息等)保存下来,即使程序重启或系统宕机,也能恢复之前的状态,让 AI "记住"之前发生的一切。

想象一下这个场景:你今天在和智能助手聊天,说了很多重要信息。然后你关闭了应用,明天重新打开并重新与它对话时,希望它还记得你说过的话吗?当然希望!这就是 AI 应用需要持久化的第一个原因。

再看第二个场景:假设有一个助手,它可以搜索网络。流程如下所示【快速上手-案例二】。

没有持久化的情况

用户问:"今天的天气怎么样?"
助手调用搜索工具,得到答案:"今天晴天,25度。"
程序崩溃重启。
用户再问:"那我需要带伞吗?"
助手没有之前的上下文,它可能又会去调用搜索工具,而不是基于"今天晴天"这个上下文来回答"不需要"。

有持久化的情况

用户问:"今天的天气怎么样?"
助手调用搜索工具,得到答案:"今天晴天,25度。"(这个状态,包括对话历史和工具调用结果,被自动保存)
程序崩溃重启。
用户再问:"那我需要带伞吗?"(与之前在同一会话下)
LangGraph 加载之前保存的状态,状态里记录了"今天晴天"。
助手看到上下文是晴天,于是直接回答:"今天是晴天,您不需要带伞。"(无需再次调用搜索工具)

持久化能力包含两部分

在 LangGraph 中,持久化能力具体包含两部分能力:

  1. 线程级持久化:能够自动保存工作流执行过程中的状态快照,维持单次会话的完整上下文。

    • 对应上述场景二

    • 这里的【线程持久化】和【操作系统线程】概念完全独立区分。操作系统线程是进程内的执行单元,是操作系统能够进行调度的最小单位。而线程级持久化表示聊天过程中的单次会话的持久化信息,用来隔离不同的聊天会话。

    • 这里的【状态快照】并非是之前学习过的 State 的快照。这个状态包含了所有必要的上下文信息,比如:已经调用过哪些工具、用户的输入、聊天历史、下一步要执行的节点等等。

  2. 跨会话持久化:通过存储(Store)保存用户信息、偏好设置等长期数据,实现不同对话间信息的持久化共享。

    • 对应上述场景一

    • 例如,可以将用户基本情况(有高血压病史)存储,后续无论在何时何地,或无论新开几个会话窗口,都可以基于用户基本信息(有高血压病史)生成结果。

2. 线程级持久化

2.1 线程级持久化是怎么工作的?

当我们开始执行工作流,过程中可能会发生崩溃或重启导致的中断等异常情况。

根据 LangGraph 的持久化机制,线程级持久化表示能够自动保存工作流执行过程中的状态快照,维持单次会话的完整上下文。当工作流执行到某一步时,它会自动保存当前步骤的状态快照。这个状态包含了所有必要的上下文信息,比如:已经调用过哪些工具、用户的输入、聊天历史、下一步要执行的节点等等。

因此,工作流在执行过程中会发生如下流程:

  • 正常执行:紫色/蓝色节点和实线箭头

  • 异常恢复:红色/绿色节点和虚线箭头

  • 检查点回滚:橙色节点和虚线箭头

初始状态
   ↓
工作流启动 - 初始化上下文:用户输入、加载当前线程内的历史状态
   ↓
执行步骤1 - 状态1:+工具调用记录1、中间结果1、下一步要执行的节点
   ↓
Persistence 自动保存状态1快照到存储 ←── 异常中断(崩溃/重启)→ 状态丢失风险
   ↓                                              ↓
执行步骤2 - 状态2:+工具调用记录2、          从持久化存储恢复状态1快照
             中间结果2、更新聊天历史、              ↓
             下一步要执行的节点                  回滚到状态1
   ↓
Persistence 自动保存状态2快照到存储 ←── 异常中断(崩溃/重启)→ 状态丢失风险
   ↓                                              ↓
执行步骤3 - 状态3:+最终计算结果、完整聊天历史、  从持久化存储恢复状态2快照
             下一步要执行的节点                        ↓
   ↓                                              回滚到状态2
Persistence 自动保存状态3快照到存储
   ↓
工作流终止
   ↓
从持久化存储选择状态1/2快照回滚 ←── 需要回滚

线程级持久化机制确保了:

  1. 状态不丢失:即使应用程序崩溃、重启,或者一个长时间的流程被中断,当它恢复时,可以从上次停止的地方继续执行,而不是从头开始。

  2. 支持长时间运行的任务:对于需要与用户进行多轮交互(如多步对话助手)或处理耗时极长的流程(如等待外部 API 回调),持久化是必不可少的。

  3. 检查点和回滚:我们可以将状态保存到某个时间点(检查点),并在需要时回滚到该状态。

2.2 核心概念

LangGraph 的持久化机制——线程级持久化是其核心功能,它通过【线程】和【检查点】这两个核心部分来实现。具体如下:

2.2.1 Threads(线程)

在 LangGraph 中,Thread 代表一个独立的工作流执行会话。可以把它想象成【与某个用户的一次完整对话历史】或【处理某个特定任务的一次完整执行过程】。例如在 DS 中的一次完整对话:

Thread 的关键特性如下:

  • 隔离性:每个 Thread 都是完全独立的,它们的状态互不干扰

  • 持久化单元:Thread 是状态持久化的基本单位

  • 标识符:通过唯一的 thread_id 来识别

2.2.2 Checkpoints(检查点)

Checkpoint 是 Thread 在特定时刻的状态快照。它记录了工作流执行到某个节点时的完整状态。例如在会话中,每一次用户输入和对话结束后,都可以保存一个最新的状态快照。

Checkpoint 的关键特性:

  • 状态快照(StateSnapshot):保存了工作流在某个时间点的完整状态。包含【状态值】、【下一步要执行的节点】、【与此检查点关联的配置】和【与此检查点关联的元数据】等信息。

StateSnapshot 结构如下:

StateSnapshot(
    # 当前状态值(如:对话消息列表)
    values={'messages': [用户消息, AI回复, 用户消息...]},
​
    # 接下来要执行的节点
    next=('generate_response',),
​
    # 配置信息
    config={'configurable': {'thread_id': '123', 'checkpoint_id': 'abc'}},
​
    # 元数据(步骤号、来源、写入信息等)
    metadata={'step': 2, 'source': 'loop', 'writes': {...}},
​
    # 父检查点(形成链表)
    parent_config={'configurable': {'thread_id': '123', 'checkpoint_id': 'def...'}},
​
    # 创建时间
    created_at=''
)
  • 版本历史与可恢复点:一个 Thread 可以有多个 Checkpoints,形成执行历史,使得同一个会话的历史状态可以从任意 Checkpoint 追溯和访问。

Threads 与 Checkpoints 关系如下:

2.3 线程级持久化使用姿势

2.3.1 步骤一:配置 checkpointer 持久化存储

在定义图时,我们需要指定 checkpointer。LangGraph 支持多种 checkpointer 的定义方式:

方式1:内存存储(MemorySaver)

最简单的方式,状态保存在程序内存中。适用于开发和测试,程序重启后状态会丢失。

这里使用快速上手——案例2的代码,在编译图时,直接添加编译参数 checkpointer

from langgraph.checkpoint.memory import InMemorySaver
​
# 定义存储方式
checkpointer = InMemorySaver()
​
# 用 checkpointer 编译图
agent = agent_builder.compile(checkpointer=checkpointer)

方式2:使用 Postgres 存储库

LangGraph 提供了几个检查点存储实现,适用于生产环境或需要状态持久化的场景:

  • SQLite 存储langgraph-checkpoint-sqlite):使用 SQLite 数据库(SqliteSaver / AsyncSqliteSaver),适合实验和本地工作流程

  • Postgres 存储langgraph-checkpoint-postgres):使用 Postgres 数据库(PostgresSaver / AsyncPostgresSaver),适合在生产中使用

为什么选择 PostgreSQL 作为存储库?

在众多持久化存储方案中,PostgreSQL 作为关系型数据库的佼佼者(PostgreSQL 和 MySQL 一 样,都是最流行的开源关系型数据库),具备以下显著优势,使其成为持久化的理想选择:

• LangGraph 原生支持: LangGraph 提供了 PostgresSaver ,简化了与 PostgreSQL 的集成 过程。

• 数据结构化与一致性: 关系型数据库天生适合存储结构化数据。Graph 的状态,尤其是消息历 史、用户档案、工具使用记录等,都可以很好地映射到表格结构中,确保数据的一致性和完整 性。

• 可靠性与持久性: PostgreSQL 提供了事务支持、ACID 特性、数据备份与恢复机制,确保数据的 持久性和高可用性,即使系统崩溃也能保证数据不丢失。

• 强大的查询能力: SQL 语言提供了灵活且强大的数据查询能力,方便我们对历史行为、用户数据 进行分析、统计和审计。结合 pgvector 等扩展,甚至可以直接在数据库中进行向量相似度搜 索,实现更高级的知识管理。

• 可扩展性: 通过读写分离、分区、集群等技术,PostgreSQL 可以支持大规模的并发访问和数据 存储,满足 Agent 在生产环境中的性能需求。

• 成熟的生态系统: 拥有庞大的社区支持、丰富的工具和成熟的运维经验,降低了开发和维护成 本。

使用 Docker 快速安装并启动 postgres:

# 1. 拉取 PostgreSQL 镜像
# 2. 运行 PostgreSQL 容器
# -p 5432:5432: 将容器的5432端口映射到宿主机的5432端口
# -e POSTGRES_PASSWORD=bit: 设置PostgreSQL的postgres用户密码
# --name postgres-sql: 给容器命名
# -d: 后台运行
docker pull postgres:latest
docker run --name postgres-sql -e POSTGRES_PASSWORD=bit -p 5432:5432 -d
postgres

可以使用 Navicat 测试链接:注意默认连接 postgres 初始数据库,如果有其他数据库可以配置连 接。

安装 langgraph-checkpoint-postgres 包:

pip install -U "psycopg[binary,pool]" langgraph langgraph-checkpoint-postgres

设置 PostgreSQL 连接字符串 (URI):

PostgreSQL URI 通常遵循以下格式:postgresql://<user>:<password>@<host>:<port>/<database_name>

使用 Postgres 存储库作为检查点:

使用 PostgresSaver.from_conn_string() 方法从连接字符串创建一个新的 PostgresSaver 实 例。 注意:第一次使用 Postgres 检查点时需要调用 checkpointer.setup()

from langgraph.checkpoint.postgres import PostgresSaver
​
DB_URI = "postgresql://postgres:bit@192.168.100.233:5432/postgres"
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    # 第一次使用 Postgres 检查点时需要调用 checkpointer.setup()
    checkpointer.setup()
​
    # 编译图
    agent = agent_builder.compile(checkpointer=checkpointer)
​
    # ...后续调用...

2.3.2 步骤二:使用 Thread 进行执行

当我们编译好图并准备运行时,我们需要通过一个 Thread ID 来标识这次执行:

  • 如果 Thread ID 不存在:LangGraph 会创建一个新的 Thread,并从初始状态开始执行

  • 如果 Thread ID 已存在:LangGraph 会从 Checkpointer 中加载该 Thread 的最后一次保存的状态,并从这个状态继续执行

第一次执行:这里依旧使用postgres存储,进行第一次执行:创建一个新的 Thread (thread_id="1")

from langchain.messages import HumanMessage
​
DB_URI = "postgresql://postgres:bit@192.168.100.233:5432/postgres"
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    # 第一次使用 Postgres 检查点时需要调用 checkpointer.setup()
    checkpointer.setup()
​
    # 编译图
    agent = agent_builder.compile(checkpointer=checkpointer)
​
    # 第一次执行,创建一个新的 Thread (thread_id="1")
    config = {"configurable": {"thread_id": "1"}}
    result1 = agent.invoke(
        {"messages": [HumanMessage(content="今天西安的天气如何?")]},
        config
    )
​
    print(f"调用 LLM 总次数: {result1['llm_calls']}次")
    for m in result1["messages"]:
        m.pretty_print()

运行后,可以看到postgres库中,已经存储了检查点信息:

一段时间后,再次使用相同的 thread_id 调用:

from langchain.messages import HumanMessage
​
DB_URI = "postgresql://postgres:bit@192.168.100.233:5432/postgres"
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    # 编译图
    agent = agent_builder.compile(checkpointer=checkpointer)
​
    # ............. 一段时间后,程序可能重启了(使用postgres存储) .............
​
    # 再次使用相同的 thread_id 调用
    # LangGraph 会从上次的状态继续,而不是重新开始
    # 此时,result2 的上下文会包含之前的对话历史
    config = {"configurable": {"thread_id": "1"}}
    result2 = agent.invoke(
        {"messages": [HumanMessage(content="我们刚才聊到哪了?")]},
        config
    )
​
    print(f"调用 LLM 总次数: {result2['llm_calls']}次")
    for m in result2["messages"]:
        m.pretty_print()

第二次调用结果:

调用 LLM 总次数: 3次
============================== Human Message ==============================
今天西安的天气如何?
============================== Ai Message ==============================
Tool Calls:
  tavily_search (call_8ALCCwF3xudzjavTFN16rA0m)
Call ID: call_8ALCCwF3xudzjavTFN16rA0m
  Args:
    query: 西安天气
============================== Tool Message ==============================
(省略...)
============================== Ai Message ==============================
今天西安的天气情况如下:
- **天气**: 晴
- **气温**: 最高温度约为 13°C,最低温度约为 2°C
- **风速**: 东北风,约 6 英里/小时
- **空气质量**: 不健康
============================== Human Message ==============================
我们刚才聊到哪了?
============================== Ai Message ==============================
我们刚才聊到西安的天气情况,包括今天的气温和天气状况。如果你有其他问题或者想讨论的内容,请告诉我!

从结果看来,result2 的上下文会包含之前的对话历史。LangGraph 会从上次的状态继续,而不是重新开始。


2.3.3 其他基本用法
2.3.3.1 获取状态快照

当使用 checkpointer 编译图时,执行时就会在每个步骤处保存状态快照。在 LangGraph 中状态快照就是 StateSnapshot 对象,其具有以下关键属性:

StateSnapshot(
    # 当前状态值(如:对话消息列表)
    values={'messages': [用户消息, AI回复, 用户消息...]},
​
    # 接下来要执行的节点
    next=('generate_response',),
​
    # 配置信息
    config={'configurable': {'thread_id': '123', 'checkpoint_id': 'abc'}},
​
    # 元数据(步骤号、来源、写入信息等)
    metadata={'step': 2, 'source': 'loop', 'writes': {...}},
​
    # 父检查点(形成链表)
    parent_config={'configurable': {'thread_id': '123', 'checkpoint_id': 'def...'}},
​
    # 创建时间
    created_at=''
)

我们可以使用 get_state(config) 方法,获取编译后的图的最新状态快照:

from langchain.messages import HumanMessage
config = {"configurable": {"thread_id": "1"}}
​
# 调用前的状态快照
snapshot = agent.get_state(config)
print(snapshot)
​
result1 = agent.invoke(
    {"messages": [HumanMessage(content="你好")]},
    config
)
​
# 调用后的状态快照
snapshot = agent.get_state(config)
print(snapshot)

打印结果:

StateSnapshot(
    values={},
    next=(),
    config={'configurable': {'thread_id': '1'}},
    metadata=None,
    created_at=None,
    parent_config=None,
    tasks=(),
    interrupts=()
)

StateSnapshot(
    values={
        'messages': [
            HumanMessage(
                content='你好',
                additional_kwargs={},
                response_metadata={}
            ),
            AIMessage(
                content='你好!有什么我可以帮助你的吗?',
                additional_kwargs={'refusal': None},
                response_metadata={
                    'token_usage': {
                        'completion_tokens': 11,
                        'prompt_tokens': 130,
                        'total_tokens': 141,
                        'completion_tokens_details': {
                            'accepted_prediction_tokens': None,
                            'audio_tokens': None,
                            'reasoning_tokens': None,
                            'rejected_prediction_tokens': None
                        },
                        'prompt_tokens_details': {
                            'audio_tokens': None,
                            'cached_tokens': 0
                        }
                    },
                    'model_provider': 'openai',
                    'model_name': 'gpt-4o-mini-2024-07-18',
                    'system_fingerprint': 'fp_efad92c60b',
                    'id': 'chatcmpl-CiGBdkNSGgkZ3ccpa6NEAyUHrzqls',
                    'finish_reason': 'stop',
                    'logprobs': None
                },
                id='lc_run--f76d30e4-0e06-4f18-ab44-0e43e8ff70b8-0',
                usage_metadata={
                    'input_tokens': 130,
                    'output_tokens': 11,
                    'total_tokens': 141,
                    'input_token_details': {'cache_read': 0},
                    'output_token_details': {}
                }
            )
        ],
        'llm_calls': 1
    },
    next=(),
    config={
        'configurable': {
            'thread_id': '1',
            'checkpoint_ns': '',
            'checkpoint_id': '1f0cf5a3-b6ff-6b2e-8001-dc5cd083ad82'
        }
    },
    metadata={'source': 'loop', 'step': 1, 'parents': {}},
    created_at='2025-12-02T08:38:09.968610+00:00',
    parent_config={
        'configurable': {
            'thread_id': '1',
            'checkpoint_ns': '',
            'checkpoint_id': '1f0cf5a3-a7a5-6cf8-8000-ec666fd226fe'
        }
    },
    tasks=(),
    interrupts=()
)


2.3.3.2 获取状态历史记录

我们可以通过调用 get_state_history(config) 来获取给定线程的图执行的完整历史记录。这将返回与配置中提供的线程 ID 关联的 StateSnapshot 对象列表。

from langchain.messages import HumanMessage
config = {"configurable": {"thread_id": "1"}}
result1 = agent.invoke(
    {"messages": [HumanMessage(content="你好")]},
    config
)
​
# 查看状态历史记录
history = list(agent.get_state_history(config))
print(history)

返回结果将按时间顺序排序,列表中的第一个检查点(StateSnapshot)是最新的。内容如下(原输出很长很乱,丢给ai叫他给我调整了缩进)

[
    StateSnapshot(
        values={
            'messages': [
                HumanMessage(
                    content='你好',
                    additional_kwargs={},
                    response_metadata={}
                ),
                AIMessage(
                    content='你好!有什么我可以帮助你的吗?',
                    additional_kwargs={'refusal': None},
                    response_metadata={
                        'token_usage': {
                            'completion_tokens': 11,
                            'prompt_tokens': 130,
                            'total_tokens': 141,
                            'completion_tokens_details': {
                                'accepted_prediction_tokens': None,
                                'audio_tokens': None,
                                'reasoning_tokens': None,
                                'rejected_prediction_tokens': None
                            },
                            'prompt_tokens_details': {
                                'audio_tokens': None,
                                'cached_tokens': 0
                            }
                        },
                        'model_provider': 'openai',
                        'model_name': 'gpt-4o-mini-2024-07-18',
                        'system_fingerprint': 'fp_efad92c60b',
                        'id': 'chatcmpl-CiGUUZCWPLKOuRv6sdzolSVAzwtMW',
                        'finish_reason': 'stop',
                        'logprobs': None
                    },
                    id='lc_run--7d1e5db6-e8b2-42c1-9fd8-ba21c42705ac-0',
                    usage_metadata={
                        'input_tokens': 130,
                        'output_tokens': 11,
                        'total_tokens': 141,
                        'input_token_details': {'cache_read': 0},
                        'output_token_details': {}
                    }
                )
            ],
            'llm_calls': 1
        },
        next=(),
        config={
            'configurable': {
                'thread_id': '1',
                'checkpoint_ns': '',
                'checkpoint_id': '1f0cf5cf-48e3-6bbb-8001-c351576393a6'
            }
        },
        metadata={'source': 'loop', 'step': 1, 'parents': {}},
        created_at='2025-12-02T08:57:39.538826+00:00',
        parent_config={
            'configurable': {
                'thread_id': '1',
                'checkpoint_ns': '',
                'checkpoint_id': '1f0cf5cf-38d7-61a8-8000-2dacafb54f11'
            }
        },
        tasks=(),
        interrupts=()
    ),
    StateSnapshot(
        values={
            'messages': [
                HumanMessage(
                    content='你好',
                    additional_kwargs={},
                    response_metadata={}
                )
            ]
        },
        next=('llm_call',),
        config={
            'configurable': {
                'thread_id': '1',
                'checkpoint_ns': '',
                'checkpoint_id': '1f0cf5cf-38d7-61a8-8000-2dacafb54f11'
            }
        },
        metadata={'source': 'loop', 'step': 0, 'parents': {}},
        created_at='2025-12-02T08:57:37.855930+00:00',
        parent_config={
            'configurable': {
                'thread_id': '1',
                'checkpoint_ns': '',
                'checkpoint_id': '1f0cf5cf-38d4-6e0c-bfff-6ce6b2a2dd01'
            }
        },
        tasks=(
            PregelTask(
                id='44c184e5-8661-3f3c-eeee-7a2f569f7e88',
                name='llm_call',
                path=('__pregel_pull', 'llm_call'),
                error=None,
                interrupts=(),
                state=None,
                result={
                    'messages': [
                        AIMessage(
                            content='你好!有什么我可以帮助你的吗?',
                            additional_kwargs={'refusal': None},
                            response_metadata={
                                'token_usage': {
                                    'completion_tokens': 11,
                                    'prompt_tokens': 130,
                                    'total_tokens': 141,
                                    'completion_tokens_details': {
                                        'accepted_prediction_tokens': None,
                                        'audio_tokens': None,
                                        'reasoning_tokens': None,
                                        'rejected_prediction_tokens': None
                                    },
                                    'prompt_tokens_details': {
                                        'audio_tokens': None,
                                        'cached_tokens': 0
                                    }
                                },
                                'model_provider': 'openai',
                                'model_name': 'gpt-4o-mini-2024-07-18',
                                'system_fingerprint': 'fp_efad92c60b',
                                'id': 'chatcmpl-CiGUUZCWPLKOuRv6sdzolSVAzwtMW',
                                'finish_reason': 'stop',
                                'logprobs': None
                            },
                            id='lc_run--7d1e5db6-e8b2-42c1-9fd8-ba21c42705ac-0',
                            usage_metadata={
                                'input_tokens': 130,
                                'output_tokens': 11,
                                'total_tokens': 141,
                                'input_token_details': {'cache_read': 0},
                                'output_token_details': {}
                            }
                        )
                    ],
                    'llm_calls': 1
                }
            ),
        ),
        interrupts=()
    ),
    StateSnapshot(
        values={'messages': []},
        next=('__start__',),
        config={
            'configurable': {
                'thread_id': '1',
                'checkpoint_ns': '',
                'checkpoint_id': '1f0cf5cf-38d4-6e0c-bfff-6ce6b2a2dd01'
            }
        },
        metadata={'source': 'input', 'step': -1, 'parents': {}},
        created_at='2025-12-02T08:57:37.855022+00:00',
        parent_config=None,
        tasks=(
            PregelTask(
                id='56c07f44-a6a6-a8b8-899a-d5c24b2ff671',
                name='__start__',
                path=('__pregel_pull', '__start__'),
                error=None,
                interrupts=(),
                state=None,
                result={
                    'messages': [
                        HumanMessage(
                            content='你好',
                            additional_kwargs={},
                            response_metadata={}
                        )
                    ]
                }
            ),
        ),
        interrupts=()
    )
]

2.3.3.3 重放

如果我们用一个 thread_id 和一个 checkpoint_id(表示检查点标识符,用于指代线程内的特定检查点)来调用一个图,那么我们将重新执行对应于 checkpoint_id 之后的步骤。

步骤:

  1. 先执行一次完整的流程,获取一次完整历史记录

  2. 保存中间过程某一次快照,并重新执行快照后的步骤

  3. 获取第二次调用后的完整历史记录,验证是否重放成功

from langchain.messages import HumanMessage
config = {"configurable": {"thread_id": "1"}}
​
# 第一次执行
result1 = agent.invoke(
    {"messages": [HumanMessage(content="今天西安的天气如何?")]},
    config
)
​
# 保存调用工具前的状态
print("-" * 80)
print(f"第一次执行历史:")
to_replay = None
for state in agent.get_state_history(config):
    print("checkpoint_id: ", state.config["configurable"]["checkpoint_id"],
          "消息数: ", len(state.values["messages"]),
          "下一节点: ", state.next)
    if len(state.values["messages"]) == 2:  # 保存调用工具前的状态
        to_replay = state
​
print("-" * 80)
print(f"从{to_replay.next}节点开始重新执行,重放配置: {to_replay.config}")
​
# 第二次执行:重放
result2 = agent.invoke(None, config=to_replay.config)
print("-" * 80)
print(f"第二次执行历史:重放后")
# 查看新的历史记录
for state in agent.get_state_history(config):
    print("checkpoint_id: ", state.config["configurable"]["checkpoint_id"],
          "消息数: ", len(state.values["messages"]),
          "下一节点: ", state.next)
​
result2['messages'][-1].pretty_print()

执行结果如下所示。可以看到历史记录中帮我们记录所有的执行过程,包括重放前和重放后的步骤! 这同时也证明了重放成功。

执行结果:

第一次执行历史:
checkpoint_id:  1f0cf6bc-cb09-61e3-8003-1a74660e9324 消息数: 4 下一节点: ()
checkpoint_id:  1f0cf6bc-b148-624e-8002-f696045e95a8 消息数: 3 下一节点: ('llm_call',)
checkpoint_id:  1f0cf6bc-8a86-62a5-8001-38aa0c3d8d80 消息数: 2 下一节点: ('tool_node',)
checkpoint_id:  1f0cf6bc-7abe-6d9a-8000-ef816c492e36 消息数: 1 下一节点: ('llm_call',)
checkpoint_id:  1f0cf6bc-7abc-6ce5-bfff-5241076b5574 消息数: 0 下一节点: ('__start__',)
​
从('tool_node',)节点开始重新执行,重放配置: {'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0cf6bc-8a86-62a5-8001-38aa0c3d8d80'}}
​
第二次执行历史:重放后
checkpoint_id:  1f0cf6bd-0e30-6cf9-8003-f22545cde7b2 消息数: 4 下一节点: ()
checkpoint_id:  1f0cf6bc-e376-6e52-8002-9c66bdb5577a 消息数: 3 下一节点: ('llm_call',)
checkpoint_id:  1f0cf6bc-cb09-61e3-8003-1a74660e9324 消息数: 4 下一节点: ()
...(后续历史记录)
​
今天西安的天气情况如下:
- **天气**: 晴
- **气温**: 最高温度约为 13°C,最低温度约为 2°C
- **风速**: 东北风,约 6 英里/小时
- **空气质量**: 不健康

重放功能实际应用为时间旅行。


2.3.3.4 更新状态

我们还可以编辑图状态。我们使用 update_state() 方法来做到这一点。

让我们更新用户的输入,换成其他搜索内容:

  1. 先执行一次完整的流程,获取一次完整历史记录

  2. 保存第一次调用LLM前的步骤快照,修改用户输入来更新快照,并重新执行更新后快照步骤

from langchain.messages import HumanMessage
from langgraph.types import Overwrite
​
config = {"configurable": {"thread_id": "1"}}
​
# 第一次执行
result1 = agent.invoke(
    {"messages": [HumanMessage(content="今天西安的天气如何?")]},
    config
)
​
# 找到调用LLM前的步骤
print("-" * 80)
print(f"第一次执行历史:")
selected_state = None
for state in agent.get_state_history(config):
    print("checkpoint_id: ", state.config["configurable"]["checkpoint_id"],
          "消息数: ", len(state.values["messages"]),
          "下一节点: ", state.next)
    if len(state.values["messages"]) == 1:  # 此时消息数为1;下一节点是'llm_call'
        selected_state = state
​
print("-" * 80)
print(f"更新前配置: {selected_state.config}")
​
# 根据指定的config,更新对于步骤的值
# 更新用户输入
new_config = agent.update_state(
    selected_state.config,
    {"messages": Overwrite([HumanMessage(content="今天北京的天气如何?")])}  # 清空消息,重新写入
)
print("-" * 80)
print(f"更新后配置: {new_config}")
​
# 第二次执行:重放更新后的配置
result2 = agent.invoke(None, config=new_config)
for message in result2['messages']:
    message.pretty_print()

执行结果:

第一次执行历史:
checkpoint_id:  1f0cf6c8-743c-6b8d-8003-d8a1130c15e0 消息数: 4 下一节点: ()
checkpoint_id:  1f0cf6c8-4244-682c-8002-bdc8b86d460d 消息数: 3 下一节点: ('llm_call',)
checkpoint_id:  1f0cf6c8-3304-6782-8001-7ebc967b7bb1 消息数: 2 下一节点: ('tool_node',)
checkpoint_id:  1f0cf6c8-1b1c-62a0-8000-42b9d34bb01d 消息数: 1 下一节点: ('llm_call',)
checkpoint_id:  1f0cf6c8-1b19-6adb-bfff-915f44142490 消息数: 0 下一节点: ('__start__',)
​
更新前配置: {'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0cf6c8-1b1c-62a0-8000-42b9d34bb01d'}}
​
更新后配置: {'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0cf6c8-7444-61e3-8001-d9eb91c95052'}}
============================== Human Message ==============================
今天北京的天气如何?
============================== Ai Message ==============================
Tool Calls:
  tavily_search (call_FfUFDwThpNTBHoiJgvNmwtMr)
Call ID: call_FfUFDwThpNTBHoiJgvNmwtMr
  Args:
    query: 北京天气
============================== Tool Message ==============================
(省略...)
============================== Ai Message ==============================
今天北京的天气情况如下:
- 当前气温:-4°C
- 湿度:23%
- 风速:25.2 km/h
- 天气状况:晴朗
- 最高气温:预计为16°C

3. 跨会话持久化

3.1 Checkpoint 的局限性

3.1.1 问题场景:跨会话信息丢失

LangGraph 的 Checkpoint 机制提供了强大的短期记忆能力,它能够:

  • 自动保存工作流每个步骤的状态快照

  • 维持单次对话的完整上下文

  • 隔离不同线程(Thread)的执行状态

简单示例(仅调用了下 LLM):

import operator
from typing import TypedDict, Annotated
from langchain.chat_models import init_chat_model
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START, END
from langgraph.graph import StateGraph
​
# 定义状态
class MessagesState(TypedDict):
    messages: Annotated[list[AnyMessage], operator.add]
​
# 定义模型节点
model = init_chat_model("gpt-4o-mini", temperature=0)
def llm_call(state: dict):
    """LLM调用"""
    return {
        "messages": [
            model.invoke([SystemMessage(content="你是一个乐于助人的助手。")]
                         + state["messages"])
        ]
    }
​
# 构件图
builder = StateGraph(MessagesState)
builder.add_node("llm_call", llm_call)
builder.add_edge(START, "llm_call")
builder.add_edge("llm_call", END)
graph = builder.compile(checkpointer=InMemorySaver())

检查点可以完美处理会话内记忆:

config1 = {"configurable": {"thread_id": "1"}}
# 第一次对话
result1 = graph.invoke({"messages": [HumanMessage(content="我爱吃汉堡,推荐一家餐厅")]}, config1)
​
# AI记得:你爱吃汉堡
result2 = graph.invoke({"messages": [HumanMessage(content="我爱吃什么?")]}, config1)
result2["messages"][-1].pretty_print()
# 输出:你提到你爱吃汉堡,所以可以推测你喜欢美味的快餐和丰富的口味组合...

想象一个多会话的 AI 助手场景:

• 星期一,用户首次对话

• 星期二,用户开启一个新对话

问题出现:跨会话信息丢失!

# 星期一,用户首次对话
config1 = {"configurable": {"thread_id": "day_1"}}
result1 = graph.invoke({"messages": [HumanMessage(content="我爱吃汉堡,推荐一家餐厅")]}, config1)
​
# 星期二,用户开启一个新对话
config2 = {"configurable": {"thread_id": "day_2"}}
result2 = graph.invoke({"messages": [HumanMessage(content="我爱吃什么?")]}, config2)
result2["messages"][-1].pretty_print()
# 输出:我不知道你具体喜欢吃什么,但可以根据一些常见的食物类型来猜测...

问题:AI不记得用户喜欢汉堡!每次对话都要"重新认识"。

3.1.2 现实世界的需求:从"单次对话"到"终身服务"

例如一个智能客服系统,具有以下实际业务需求:

  • 识别 VIP 客户,优先服务

  • 避免重复询问相同问题

  • 基于历史投诉优化服务

仅是检查点无法满足这些需求:

graph = builder.compile(checkpointer=InMemorySaver())
​
config1 = {"configurable": {"thread_id": "query_1"}}
result1 = graph.invoke({"messages": [HumanMessage(content="我的账户被冻结了")]}, config1)
# 用户第一次投诉账户问题,已解决。智能客服了如下过程:
# - 搜集用户信息
# - 了解用户问题与需求
# - 处理问题
​
config2 = {"configurable": {"thread_id": "query_2"}}
result2 = graph.invoke({"messages": [HumanMessage(content="我的账户又被冻结了")]}, config2)
# 10天后,用户第二次投诉账户问题。
# 由于智能客服不知道用户历史,无法准确解决问题。还需再次了解前因后果

如何做到【共享状态】的需求模型,如精准识别客户、保留VIP客户关键历史记录,是智能客服系统的关键。

3.2 解决方案:引入 Store

Store 像是一个长期记忆仓库,支持在我们执行过程中保存用户信息、偏好设置等长期数据,以实现不同对话间信息的持久化共享。

3.2.1 存储 vs 检查点
  • 检查点:保存状态变化历史(时间线)

  • 存储:保存结构化知识(数据库)

实际上,使用 Checkpoint + Store 模式才能够真正实现理想的 AI 记忆系统。

3.2.2 引入 Store 后,AI 应用架构的范式转变

Store 的引入,不是简单的功能增加,而是 AI 应用架构的范式转变:

阶段 特点 示例
第1阶段:无状态AI(石器时代) 每次对话都是陌生人 "你好!""你好!"(无限循环)
第2阶段:检查点(工业革命) 单次对话有记忆 "我叫小明""你好小明!"但明天就忘记
第3阶段:检查点+Store(智能时代) 终身记忆,持续学习 "一年前你喜欢披萨,现在还喜欢吗?""根据你的阅读历史,推荐这本书..."

Store 的引入,真正能做到:

  • 从关注单次交互 → 到关注用户生命周期

  • 从处理当前请求 → 到利用历史数据

  • 从通用回复 → 到深度个性化

这种转变让 AI 从"工具"进化为"伙伴",真正实现智能服务的核心理念:在正确的时间,以正确的方式,为正确的人提供正确的价值。

题外话:使用 AI 不要随意泄露自己的隐私信息,很容易被保存下来!


3.3 跨会话持久化使用姿势

要想使用 store,我们需要创建一个存储实例,其也有【内存级存储】与相关【存储库存储】两种方式。例如内存级存储:

from langgraph.store.memory import InMemoryStore
store = InMemoryStore()

接着只需像以前一样使用 Checkpoints 和 Store 变量编译图表即可:

graph = builder.compile(checkpointer=checkpointer, store=store)
3.3.1 方式1:内存存储

Store 基本用法

Store 本身是通过 Namespace 区分不同数据,如下所示:

在 LangGraph 中,其提供了一个简单的内存实现 InMemoryStore 。想要进行存储,需要:

• 先定义命名空间:为了区分不同用户的记忆,需要一个“命名空间”。这就像在数据库里为每个用 户创建一个独立的文件夹。命名空间用于组织记忆,通常按业务逻辑划分。一般用元组来定义命名 空间。如:

# 使用元组 - 层次清晰,易于扩展
namespace1 = ("user_123", "preferences", "food") # 用户食物偏好
namespace2 = ("user_123", "preferences", "music") # 用户音乐偏好
namespace3 = ("user_123", "conversations", "2025-05") # 用户某天的对话历史
# 使用字符串 - 扁平且易混淆
namespace4 = "user_123_preferences_food" # 需要解析,容易出错
namespace5 = "user_123_preferences_music"
namespace6 = "user_123_conversations_2024"

• 当在对话中获取到用户的重要信息时,使用 store.put() 方法将内存保存到存储中的命名空 间。该方法参数包含:

◦ namespace :决定这个记忆属于谁以及是什么类型。

◦ memory_id :是这个记忆条目的唯一键。

◦ memory_content :是记忆的具体内容,一个字典。

from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
store.put(namespace, memory_id, memory_content)

完整代码

# 1. 导入并创建存储
from langgraph.store.memory import InMemoryStore
import uuid # 用于生成唯一ID

store = InMemoryStore()

# 2. 定义命名空间 (Namespace)
# 命名空间用于组织记忆,通常按业务逻辑划分,例如按用户。
# 这里我们用一个元组 (用户ID, 记忆类型)
user_id = "user_123"
namespace = (user_id, "preferences") # 用户 user_123 的偏好记忆

# 3. 存入一条记忆 (Memory)
# 每条记忆需要一个唯一的 memory_id 和 一个 value (通常是字典)
memory_id = str(uuid.uuid4()) # 生成唯一ID,如 "abc-123-def-456"
memory_value = {"favorite_food": "汉堡", "allergy": "花粉"}
store.put(namespace, memory_id, memory_value)
print("记忆已存入!")

# 4. 读取记忆
# 可以搜索某个命名空间下的所有记忆
all_memories = store.search(namespace)
for mem in all_memories:
print(mem.dict()) # 记忆对象转成字典查看

运行

{
    'namespace': [
        'user_123', 'preferences'
    ],
    'key': 'db826e33-c68c-4669-a79a-3579bff02ff1',
    'value': {
        'favorite_food': '汉堡',
        'allergy': '花粉'
    },
    'created_at': '2025-12-03T08:16:14.134568+00:00',
    'updated_at': '2025-12-03T08:16:14.134576+00:00',
    'score': None
}

因使用元组作为命名空间,故同样支持下面的搜索方式:

all_memories = store.search((user_id, ))

在 LangGraph 中使用 Store

内存存储适用于开发和测试,程序重启后存储的数据会丢失。这里依旧使用 快速上手——案例2 的代码 进行演示。

由于要加入 Store,需要在合适的地方加入与存储关键信息相关的代码。如我们可以在每次调用 LLM 前先进行信息收集,然后带着收集到的共享信息进行 LLM 调用。因此,流程变成了:

这样,两部分信息将会被收集:一是用户发的消息;二是通过工具调用返回的结果信息也会被采集。 代码如下:

• 在编译图时,直接添加编译参数 store ,如下所示:

from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
# 用 checkpointer + store 编译图
agent = agent_builder.compile(checkpointer=checkpointer, store=store)

现在,在任何一个节点的函数中,都可以通过注入 store 参数来访问这个全局存储。

• 新增提取用户信息节点 在这个节点中,我们需要根据【用户发的消息】和【工具调用返回的结果】来采集需要收集的信息。 收集的信息需要使用 Store 进行存储。关键设计如下:

1. 任何节点函数,如果需要访问 Store,可以通过在参数中声明 store: BaseStore 和 config: RunnableConfig 来获取。

2. 在这里可以通过 LLM 提取用户信息,因此定义结构化返回是很有必要的

# 定义结构化输出
class Person(BaseModel):
    """一个人的信息。"""
    # 注意:
    # 1. 每个字段都是 Optional “可选的” —— 允许 LLM 在不知道答案时输出 None。
    # 2. 每个字段都有一个 description “描述” —— LLM使用这个描述。
    name: Optional[str] = Field(default=None, description="这个人的名字")
    height_in_meters: Optional[str] = Field(default=None,
                                             description="以米为单位的高度")
    favourite_food: Optional[list[str]] = Field(default=None,
                                                 description="最喜欢的食物列表")

model_with_structured = model.with_structured_output(Person)

# 提取用户信息节点
def get_person_by_llm(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
    """通过 LLM 提取用户信息"""
    # 1. 先提取
    people_info = model_with_structured.invoke(
        [
            SystemMessage(
                content="你是一个提取信息的专家,只从文本中提取我的相关信息,不能提取别人的信息。如果你不知道要提取的属性的值,属性值返回null。"
            )
        ]
        + state["messages"][-3:]  # 只查看最近3条消息
    )

    # 2. 再保存
    user_id = config["configurable"]["user_id"]

    # 保存用户基本信息
    namespace1 = (user_id, "info")
    # 每次put前应判断是否存在,再更新。否则会有多条记录被记录。这里简写
    store.put(
        namespace1,
        str(uuid.uuid4()),
        {
            "name": people_info.name,
            "height": people_info.height_in_meters
        }
    )

    # 保存用户偏好
    namespace2 = (user_id, "preferences")
    store.put(
        namespace2,
        str(uuid.uuid4()),
        {"favourite_food": people_info.favourite_food}  # 省略追加逻辑:先搜再更新
    )

    return {
        "llm_calls": state.get('llm_calls', 0) + 1
    }

• 更新模型调用节点:添加共享信息到提示词 调用 LLM 之前,我们便可以通过查询 Store 获取共享信息,然后将其加入到提示词中,完成调用。

def llm_call(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
    """LLM决定是否调用工具"""
    # 搜索用户信息
    user_id = config["configurable"]["user_id"]
    namespace1 = (user_id, "info")
    namespace2 = (user_id, "preferences")
    info_result = store.search(namespace1)
    pref_result = store.search(namespace2)

    return {
        "messages": [
            model_with_tools.invoke(
                [
                    SystemMessage(
                        content=(
                            f"你是一个乐于助人的助手,支持调用工具进行搜索。"
                            f"查询 LLM 前可参考以下信息:"
                            f"1. 用户基本情况:{info_result[0].value} "
                            f"2. 用户偏好情况:{pref_result[0].value}"
                        )
                    )
                ]
                + state["messages"]
            )
        ],
        "llm_calls": state.get('llm_calls', 0) + 1
    }

• 构件图时,加入新节点与调整边

根据下图完成调整:

agent_builder = StateGraph(MessagesState)
agent_builder.add_node(llm_call)
agent_builder.add_node(tool_node)
# 新增节点
agent_builder.add_node(get_person_by_llm)

# 调整边
agent_builder.add_edge(START, "get_person_by_llm")
agent_builder.add_edge("get_person_by_llm", "llm_call")
agent_builder.add_conditional_edges(
    "llm_call",
    should_continue,
    ["tool_node", END]
)
agent_builder.add_edge("tool_node", "get_person_by_llm")

到此,代码已经改造完成,完整代码如下:

import uuid
from typing import Optional
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
from langchain_core.runnables import RunnableConfig
from langchain_tavily import TavilySearch
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore
from pydantic import BaseModel, Field

# 步骤 1: 定义工具和模型
search = TavilySearch(max_results=4)
tools = [search]
# 绑定工具
model = init_chat_model("gpt-4o-mini", temperature=0)
model_with_tools = model.bind_tools(tools)

# 步骤 2: 定义状态
from langchain.messages import AnyMessage
from typing_extensions import TypedDict, Annotated
import operator

class MessagesState(TypedDict):
    # 类型: list[AnyMessage] - 任意消息对象的列表
    # 合并策略: operator.add - 使用加法操作符进行状态合并
    # 效果: 当状态更新时,新的消息会追加到现有列表中,而不是替换
    messages: Annotated[list[AnyMessage], operator.add]
    # 类型: int - 整数值
    # 用途: 跟踪LLM(大语言模型)的调用次数
    llm_calls: int

# 步骤 3:新增提取信息节点
# 定义结构化输出
class Person(BaseModel):
    """一个人的信息。"""
    # 注意:
    # 1. 每个字段都是 Optional “可选的” —— 允许 LLM 在不知道答案时输出 None。
    # 2. 每个字段都有一个 description “描述” —— LLM使用这个描述。
    name: Optional[str] = Field(default=None, description="这个人的名字")
    height_in_meters: Optional[str] = Field(default=None, description="以米为单位的高度")
    favourite_food: Optional[list[str]] = Field(default=None, description="最喜欢的食物列表")

model_with_structured = model.with_structured_output(Person)

def get_person_by_llm(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
    """通过 LLM 提取用户信息"""
    # 1. 先提取
    people_info = model_with_structured.invoke(
        [
            SystemMessage(
                content="你是一个提取信息的专家,只从文本中提取我的相关信息,不能提取别人的信息。如果你不知道要提取的属性的值,属性值返回null。"
            )
        ]
        + state["messages"][-3:]  # 只查看最近3条消息
    )

    # 2. 再保存
    user_id = config["configurable"]["user_id"]

    # 保存用户基本信息
    namespace1 = (user_id, "info")
    # 每次put前应判断是否存在,再更新。否则会有多条记录被记录。这里简写
    store.put(
        namespace1,
        str(uuid.uuid4()),
        {
            "name": people_info.name,
            "height": people_info.height_in_meters
        }
    )

    # 保存用户偏好
    namespace2 = (user_id, "preferences")
    store.put(
        namespace2,
        str(uuid.uuid4()),
        {"favourite_food": people_info.favourite_food}  # 省略追加逻辑:先搜再更新
    )

    return {
        "llm_calls": state.get('llm_calls', 0) + 1
    }

# 步骤 4: 更新模型调用节点:添加共享用户信息到提示词
def llm_call(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
    """LLM决定是否调用工具"""
    # 搜索用户信息
    user_id = config["configurable"]["user_id"]
    namespace1 = (user_id, "info")
    namespace2 = (user_id, "preferences")
    info_result = store.search(namespace1)
    pref_result = store.search(namespace2)

    return {
        "messages": [
            model_with_tools.invoke(
                [
                    SystemMessage(
                        content=(
                            f"你是一个乐于助人的助手,支持调用工具进行搜索。"
                            f"查询 LLM 前可参考以下信息:"
                            f"1. 用户基本情况:{info_result[0].value} "
                            f"2. 用户偏好情况:{pref_result[0].value}"
                        )
                    )
                ]
                + state["messages"]
            )
        ],
        "llm_calls": state.get('llm_calls', 0) + 1
    }

# 步骤 5: 定义工具节点
tools_by_name = {tool.name: tool for tool in tools}

def tool_node(state: dict):
    """执行工具调用"""
    result = []
    for tool_call in state["messages"][-1].tool_calls:
        tool = tools_by_name[tool_call["name"]]
        observation = tool.invoke(tool_call["args"])
        result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
    return {"messages": result}

# 步骤 6: 构件图
from langgraph.graph import StateGraph, START, END

# 定义结束逻辑
def should_continue(state: MessagesState):
    """根据LLM是否调用工具来决定是应该继续循环(路由到工具节点)还是停止循环(END)"""
    messages = state["messages"]
    last_message = messages[-1]
    # 如果LLM调用工具,则执行操作
    if last_message.tool_calls:
        return "tool_node"
    return END

# 加入新节点并修改边
agent_builder = StateGraph(MessagesState)
agent_builder.add_node(llm_call)
agent_builder.add_node(tool_node)
agent_builder.add_node(get_person_by_llm)
agent_builder.add_edge(START, "get_person_by_llm")
agent_builder.add_edge("get_person_by_llm", "llm_call")
agent_builder.add_conditional_edges(
    "llm_call",
    should_continue,
    ["tool_node", END]
)
agent_builder.add_edge("tool_node", "get_person_by_llm")

checkpointer = InMemorySaver()
store = InMemoryStore()

# 编译图
agent = agent_builder.compile(checkpointer=checkpointer, store=store)

• 运行与验证:同一用户但不同会话的请求

# 第一次聊天
config1 = {"configurable": {"thread_id": "1", "user_id": "1"}}
result1 = agent.invoke(
    {"messages": [HumanMessage(content="我叫李华,我最爱吃汉堡。我的朋友叫小明,他爱吃披萨")]},
    config1
)
print(f"\n调用 LLM 总次数:{result1['llm_calls']}次")
for m in result1["messages"]:
    m.pretty_print()

# ---------- 过了几天 ---------------
# 同一个人,再次进行对话
config2 = {"configurable": {"thread_id": "2", "user_id": "1"}}
result2 = agent.invoke(
    {"messages": [HumanMessage(content="给我推荐下餐厅")]},
    config2
)
print(f"\n调用 LLM 总次数:{result2['llm_calls']}次")
for m in result2["messages"]:
    m.pretty_print()

执行结果:
 

调用 LLM 总次数:2次

================================ Human Message =================================
我叫李华,我最爱吃汉堡。我的朋友叫小明,他爱吃披萨

================================== Ai Message ==================================
你好,李华!很高兴认识你。汉堡和披萨都是很受欢迎的美食。你和小明有没有一起去过什么好吃的地方呢?或者你们有没有想尝试的新餐厅?

调用 LLM 总次数:4次

================================ Human Message =================================
给我推荐下餐厅

================================== Ai Message ==================================
Tool Calls:
  tavily_search (call_btsV05aeqldqjaZNvgdlYRe9)
    Call ID: call_btsV05aeqldqjaZNvgdlYRe9
    Args:
      query: 推荐汉堡餐厅

================================= Tool Message =================================
{省略.....}

================================== Ai Message ==================================
以下是一些推荐的汉堡餐厅:
1. **[Burger She Wrote](https://www.novacircle.com/zh-CN/spots/north-america/united-states/california/los-angeles-county/los-angeles/burger-she-wrote-9ea267)** - 位于洛杉矶,这是一家小而温馨的餐厅,以其美味的和牛汉堡而闻名。
2. **[Tripadvisor 上洛杉矶的最佳汉堡](https://cn.tripadvisor.com/Restaurants-g32655-zfd10907-zfn7231034-Los_Angeles_California-Hamburger.html)** - 包含多家受欢迎的汉堡餐厅,如Bottega Louie和Eggslut,后者以其鸡蛋汉堡而著称。

希望这些推荐能帮助你找到美味的汉堡!

语义搜索 Store 的强大之处在于它支持语义搜索,而不仅仅是精确匹配。这意味着我们可以用自然语言问题来查 找相关记忆。 首先,我们需要配置带嵌入模型的 Store,如下所示:

3.3.2 方式2:Postgres 存储库

Postgres 存储库适用于生产环境或需要状态持久化的场景。由于之前已经启动过 PostgresSQL,这里 可以直接连接到数据库,作为 PostgresStore 使用。只需在编译时设置 store 即可。

修改【内存存储】部分的代码:将内存存储方式修改为 Postgres 存储库。

注意:第一次使用 Postgres store 时需要调用 store.setup()

DB_URI = "postgresql://postgres:bit@192.168.100.233:5432/postgres"

with (
    PostgresSaver.from_conn_string(DB_URI) as checkpointer,
    PostgresStore.from_conn_string(DB_URI) as store,
):
    # 第一次使用 Postgres 检查点时需要调用 checkpointer.setup()
    checkpointer.setup()
    # 第一次使用 Postgres store 时需要调用 store.setup()
    store.setup()

    # 编译图
    agent = agent_builder.compile(checkpointer=checkpointer, store=store)
    # ...后续调用...

模拟第一次聊天:

DB_URI = "postgresql://postgres:bit@192.168.100.233:5432/postgres"

with (
    PostgresSaver.from_conn_string(DB_URI) as checkpointer,
    PostgresStore.from_conn_string(DB_URI) as store,
):
    # 第一次使用 Postgres 检查点时需要调用 checkpointer.setup()
    checkpointer.setup()
    # 第一次使用 Postgres store 时需要调用 store.setup()
    store.setup()

    # 编译图
    agent = agent_builder.compile(checkpointer=checkpointer, store=store)

    # 第一次聊天
    config1 = {"configurable": {"thread_id": "1", "user_id": "1"}}
    result1 = agent.invoke(
        {"messages": [HumanMessage(content="我叫李华,我最爱吃汉堡。我的朋友叫小明,他爱吃披萨")]},
        config1
    )
    print(f"\n调用 LLM 总次数:{result1['llm_calls']}次")
    for m in result1["messages"]:
        m.pretty_print()

运行系统后可以看到,postgres 中新增 store 相关表,其中存放了用户基本的信息:

再次验证:同一用户但不同会话的请求

DB_URI = "postgresql://postgres:bit@192.168.100.233:5432/postgres"

with (
    PostgresSaver.from_conn_string(DB_URI) as checkpointer,
    PostgresStore.from_conn_string(DB_URI) as store,
):
    # 第一次使用 Postgres 检查点时需要调用 checkpointer.setup()
    # checkpointer.setup()
    # 第一次使用 Postgres store 时需要调用 store.setup()
    # store.setup()

    # 编译图
    agent = agent_builder.compile(checkpointer=checkpointer, store=store)

    # ---------- 过了几天 ---------------
    # 同一个人,再次进行对话
    config2 = {"configurable": {"thread_id": "2", "user_id": "1"}}
    result2 = agent.invoke(
        {"messages": [HumanMessage(content="给我推荐下餐厅")]},
        config2
    )
    print(f"\n调用 LLM 总次数:{result2['llm_calls']}次")
    for m in result2["messages"]:
        m.pretty_print()

注意执行前,将以下代码注掉,因为:在存入store前,并没有编写 不存在存入,存在更新 的代码逻 辑(只是演示),因此会将空的用户信息误存,导致LLM调用前查出来空的。

def get_person_by_llm(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
    """通过 LLM 提取用户信息(注:保存逻辑已注释,避免误存空信息)"""
    # 1. 提取信息
    people_info = model_with_structured.invoke(
        [
            SystemMessage(
                content="你是一个提取信息的专家,只从文本中提取我的相关信息,不能提取别人的信息。如果你不知道要提取的属性的值,属性值返回null。"
            )
        ]
        + state["messages"][-3:]  # 只查看最近3条消息
    )

    user_id = config["configurable"]["user_id"]

    # ========== 保存逻辑暂时注释,避免误存空值 ==========
    # 原因:当前没有实现“不存在则存入、存在则更新”的逻辑,若 LLM 提取出的字段全为 None,
    # 会覆盖掉已有的正确用户信息,导致后续查询到空数据。

    # 保存用户基本信息
    # namespace1 = (user_id, "info")
    # store.put(
    #     namespace1,
    #     str(uuid.uuid4()),
    #     {
    #         "name": people_info.name,
    #         "height": people_info.height_in_meters
    #     }
    # )

    # 保存用户偏好
    # namespace2 = (user_id, "preferences")
    # store.put(
    #     namespace2,
    #     str(uuid.uuid4()),
    #     {"favourite_food": people_info.favourite_food}
    # )
    # =================================================

    return {
        "llm_calls": state.get('llm_calls', 0) + 1
    }

最终执行结果如下:(这里同样也是丢给ai缩进了一下格式)

调用 LLM 总次数:3次

================================ Human Message =================================
给我推荐下餐厅

================================== Ai Message ==================================
Tool Calls:
  tavily_search (call_PkIzsQRuCS6KZM15g3noHgBU)
    Call ID: call_PkIzsQRuCS6KZM15g3noHgBU
    Args:
      query: 推荐汉堡餐厅

================================= Tool Message =================================
{
  'query': '推荐汉堡餐厅',
  'follow_up_questions': None,
  'answer': None,
  'images': [],
  'results': [
    {
      'url': 'https://www.reddit.com/r/AskNYC/comments/1470o9z/best_burger_spot_in_nyc/?tl=zh-hans',
      'title': '纽约最好吃的汉堡店是哪家? : r/AskNYC',
      'content': 'The Thompson 的Burger Joint 和Minetta Tavern 等被推荐。汉堡餐厅推荐 ,来自1 个月前。Smashed 和Korzo 等被推荐。纽约/布鲁克林最好的汉堡? 以及 ...Read more',
      'score': 0.7336813,
      'raw_content': None
    },
    {
      'url': 'https://www.cosmopolitan.com/tw/lifestyle/food-and-drink/g44382807/hamburger-20230629/',
      'title': '美國旅遊必吃7大人氣漢堡店!IN-N-OUT最強勁敵',
      'content': '1.The Habit Burger Grill · 2.Cheeseburger in Paradise · 3.Five Guys · 4.IN-N-OUT · 5.Shake Shack · 6.SmashburgerRead more',
      'score': 0.61972505,
      'raw_content': None
    },
    {
      'url': 'https://cn.tripadvisor.com/Restaurants-g60763-zfd10907-zfn7102345-New_York_City_New_York-Hamburger.html',
      'title': '纽约市最佳汉堡',
      'content': "Ellen's Stardust Diner · (23,569 条点评). 美式烹饪, 晚餐 ; Virgil's Real BBQ - NYC · (4,968 条点评). 美式烹饪, 烧烤 ; 1. S'MAC · (488 条点评). 快餐小吃, 美式烹饪 ; 2.Read more",
      'score': 0.56719416,
      'raw_content': None
    },
    {
      'url': 'https://mliesl.edu/contents/ch/%E7%BE%8E%E5%9B%BD%E6%9C%80%E4%BD%B3%E6%B1%89%E5%A0%A1%E8%BF%9E%E9%94%81%E6%8E%92%E5%90%8D/',
      'title': '美国最佳汉堡连锁排名',
      'content': '最受好评的汉堡连锁店之一– Five Guys – 被康涅狄格州、乔治亚州、蒙大拿州、内布拉斯加州、俄勒冈州、南卡罗来纳州、佛蒙特州和西弗吉尼亚州评为第一名。 那是8 个州同意,...Read more',
      'score': 0.56690645,
      'raw_content': None
    }
  ],
  'response_time': 0.92,
  'request_id': '0ee8ee23-bbd3-48e2-b4c2-ca6d1c1dfe7a'
}

================================== Ai Message ==================================
以下是一些推荐的汉堡餐厅:
1. **[纽约最好吃的汉堡店](https://www.reddit.com/r/AskNYC/comments/1470o9z/best_burger_spot_in_nyc/?tl=zh-hans)** - 推荐的汉堡店包括 The Thompson 的 Burger Joint 和 Minetta Tavern 等。
2. **[美国旅游必吃7大人氣漢堡店](https://www.cosmopolitan.com/tw/lifestyle/food-and-drink/g44382807/hamburger-20230629/)** - 包括 The Habit Burger Grill、Cheeseburger in Paradise、Five Guys、IN-N-OUT、Shake Shack 和 Smashburger。
3. **[纽约市最佳汉堡](https://cn.tripadvisor.com/Restaurants-g60763-zfd10907-zfn7102345-New_York_City_New_York-Hamburger.html)** - 推荐的餐厅有 Ellen's Stardust Diner 和 Virgil's Real BBQ - NYC。
4. **[美国最佳汉堡连锁排名](https://mliesl.edu/contents/ch/%E7%BE%8E%E5%9B%BD%E6%9C%80%E4%BD%B3%E6%B1%89%E5%A0%A1%E8%BF%9E%E9%94%81%E6%8E%92%E5%90%8D/)** - Five Guys 是最受好评的汉堡连锁店之一。

希望这些推荐能帮助到您!如果您有特定的城市或地区需求,请告诉我。

补充一些内容:

Store 基本使用示例

from langgraph.store.memory import InMemoryStore
from langgraph.checkpoint.memory import MemorySaver
​
# 创建 Store 和 Checkpointer
store = InMemoryStore()
checkpointer = MemorySaver()
​
# 编译图时同时传入 store 和 checkpointer
graph = builder.compile(checkpointer=checkpointer, store=store)
​
# 使用时,通过 namespace 组织数据
# namespace 类似于文件夹,用于隔离不同用户或不同维度的数据
config = {
    "configurable": {
        "thread_id": "user_123",
        "user_id": "user_123"  # 用于 Store 的 namespace
    }
}
​
# 在节点中使用 store 读写数据
def personalized_node(state, config):
    """个性化节点 - 从 store 读取用户偏好"""
    store = config["store"]
    user_id = config["configurable"]["user_id"]
​
    # 读取用户偏好(如果不存在返回默认值)
    preference = store.get(("user", user_id), "preference")
​
    # 基于用户偏好生成个性化回复
    if preference:
        return {"messages": [f"根据您的偏好:{preference.value}"]}
    else:
        return {"messages": ["请告诉我您的偏好"]}
​
# 写入用户偏好
store.put(("user", "user_123"), "preference", {"food": "汉堡", "color": "蓝色"})
Store 数据组织方式
# Store 使用 namespace + key 的方式组织数据
# namespace 是一个元组,用于隔离不同维度的数据
​
# 示例:存储用户信息
store.put(("user", "user_123"), "profile", {"name": "张三", "age": 25})
store.put(("user", "user_123"), "preferences", {"theme": "dark"})
​
# 示例:存储全局配置
store.put(("system",), "config", {"version": "1.0"})
​
# 读取数据
profile = store.get(("user", "user_123"), "profile")
config = store.get(("system",), "config")
​
# 搜索数据
all_users = store.search(("user",))
Checkpoint + Store 完整示例
from langgraph.store.memory import InMemoryStore
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import HumanMessage, AIMessage
from typing import TypedDict, Annotated
import operator
​
# 定义状态
class State(TypedDict):
    messages: Annotated[list, operator.add]
​
# 定义节点
def chatbot(state: State, config):
    """聊天机器人 - 从 store 读取用户历史偏好"""
    store = config["config"]["store"]
    user_id = config["configurable"]["user_id"]
​
    # 读取用户历史
    history = store.get(("user", user_id), "history")
​
    # 构建带历史上下文的回复
    if history:
        context = f"用户历史偏好:{history.value}"
    else:
        context = "新用户"
​
    return {"messages": [AIMessage(content=f"你好!{context}")]}
​
# 创建存储
store = InMemoryStore()
checkpointer = MemorySaver()
​
# 构建图
builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)
​
# 编译时同时传入 checkpointer 和 store
graph = builder.compile(checkpointer=checkpointer, store=store)
​
# 使用
config = {
    "configurable": {
        "thread_id": "session_1",
        "user_id": "user_123"
    },
    "store": store  # store 需要传入 config
}
​
# 第一次对话
result = graph.invoke({"messages": [HumanMessage(content="我喜欢汉堡")]}, config)
​
# 存储用户偏好到 store
store.put(("user", "user_123"), "history", {"preference": "汉堡"})
​
# 第二次对话(新会话,但能读取到 store 中的偏好)
config2 = {
    "configurable": {
        "thread_id": "session_2",  # 新的会话
        "user_id": "user_123"      # 同一个用户
    },
    "store": store
}
result2 = graph.invoke({"messages": [HumanMessage(content="推荐餐厅")]}, config2)
print(result2["messages"][-1].content)
# 输出包含用户偏好的个性化回复

总结

持久化类型 适用场景 存储位置 生命周期
线程级持久化(Checkpoint) 单次会话的状态恢复 线程内部 单次会话
跨会话持久化(Store) 用户画像、偏好设置等长期数据 全局共享 跨会话持久

通过 Checkpoint + Store 的组合,LangGraph 实现了完整的 AI 记忆系统:

  • Checkpoint 保证单次会话的完整性和可恢复性

  • Store 实现跨会话的用户数据共享和个性化服务

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐