DeepAgents - 使用Postgres作为Checkpoint 在用 deepagents 做 Chatbot 的时候有个最基本的需求Agent 得记住上一轮聊了什么。你不能每轮对话都让用户重新自我介绍一遍。LangGraph / DeepAgents 内置了一个叫 checkpoint 的机制来处理这个事。开发阶段用MemorySaver跑跑 demo 问题不大但一到生产环境——服务重启状态全丢、多实例部署无法共享——就该上 Postgres 了。本文记录我在一个基于 FastAPI 的项目中用langgraph-checkpoint-postgres做持久化 checkpoint 的过程顺带聊聊连接池的配置和 PG 服务端 idle 超时带来的坑。项目依赖精简如下langgraph-checkpoint-postgres3.1.0 psycopg[binary,pool]3.3.4 deepagents0.4.12 fastapi0.135.2Checkpoint 是什么在 LangGraph 里Agent 的执行过程本质上是一个有向图graphLLM 调用、tool 执行、条件分支……每一步都可能改变状态。Checkpoint 就是在图的每个节点执行后自动保存的状态快照包含当前的消息历史、中间变量、以及图走到哪一步了。你可以把它类比成玩游戏时的存档打到一半存个档下次可以从这个存档接着打而不是从头开始。在代码层面的体现就是RunnableConfig里的thread_idconfig RunnableConfig( callbacks[cb], configurable{thread_id: session_id} ) async for chunk in ai_agent.astream(msg.content, configconfig): await final_answer.stream_token(chunk)同一个thread_id的消息会写入同一条 checkpoint 链下次再用这个thread_id调 agent 时LangGraph 会自动从最近的 checkpoint 恢复状态继续往下跑用户完全感知不到重来。为什么用 Postgres 而不是 MemorySaverMemorySaver把 checkpoint 存在进程内存里开发调试很方便一行代码搞定。但问题也很明显服务重启就没了— 进程退出内存释放所有对话历史清零。多实例无法共享— 如果你起多个 workeruvicorn--workers 4每个进程各有一份 MemorySaver用户这次请求打到 worker A下次打到 worker B上下文就丢了。Postgres 作为外部持久化存储天然解决了这两个问题。langgraph-checkpoint-postgres提供了AsyncPostgresSaver内部自动管理三张表checkpoints状态快照、checkpoint_writes写操作记录、checkpoint_blobs序列化数据。建表只需在启动时调用一次setup()conn_string ( fpostgresql://{cfg.PG_USER}:{cfg.PG_PASSWORD} f{cfg.PG_HOST}:{cfg.PG_PORT}/{cfg.PG_DB} ) async with AsyncPostgresSaver.from_conn_string(conn_string) as temp_saver: await temp_saver.setup()连接池用 psycopg_pool 的 AsyncConnectionPool为什么需要连接池Agent 每次调用ainvoke或astream都会跟 Postgres 交互多次读之前的 checkpoint、写新的 checkpoint。如果每次交互都新建一条数据库连接三条消息就能把 PG 的max_connections打满。更合理的方式是复用连接——初始化一个连接池AsyncPostgresSaver直接从池里拿连接用完归还。配置代码psycopg_pool的AsyncConnectionPool用得最广配置不复杂from urllib.parse import quote_plus from psycopg_pool import AsyncConnectionPool _PG_POOL: AsyncConnectionPool None def _init_pg_pool(): global _PG_POOL if not _PG_POOL: _PG_POOL AsyncConnectionPool( fpostgresql://{quote_plus(cfg.PG_USER)}: f{quote_plus(cfg.PG_PASSWORD)} f{cfg.PG_HOST}:{cfg.PG_PORT}/{cfg.PG_DB}, min_sizecfg.PG_POOL_MIN_SIZE, # 保持的最小连接数 max_sizecfg.PG_POOL_MAX_SIZE, # 峰值最大连接数 openFalse, # 延迟打开避免阻塞启动 ) async def get_pg_pool() - AsyncConnectionPool: global _PG_POOL if not _PG_POOL: _init_pg_pool() await _PG_POOL.open() # 第一次调用时才真正建立连接 return _PG_POOL几个配置的点min_size/max_size池里始终保持min_size条空闲连接待命并发上来时最多扩到max_size。实际项目里设min4, max10就够了视并发量调整。openFalse创建连接池对象时不立刻连数据库。因为_init_pg_pool()是在模块 import 时就调用的懒加载单例如果openTrueimport 阶段就会建连——万一数据库还没起来整个应用就起不来了。URL 编码用户名密码里如果有特殊字符quote_plus防注入。AsyncPostgresSaver 复用连接池拿到连接池后直接传给AsyncPostgresSaverfrom langgraph.checkpoint.postgres.aio import AsyncPostgresSaver _PG_CHECKPOINTER: AsyncPostgresSaver None async def init_checkpointer( pg_pool: AsyncConnectionPool, is_setup: bool False ) - None: global _PG_CHECKPOINTER if not _PG_CHECKPOINTER: _PG_CHECKPOINTER AsyncPostgresSaver(connpg_pool) if is_setup: # setup 时用临时连接建表避免占用池里的连接 conn_string (...) async with AsyncPostgresSaver.from_conn_string(conn_string) as temp_saver: await temp_saver.setup()注意setup()单独用了一个from_conn_string的临时连接没有直接用池里的连接。PG Server 端 idle 超时的坑问题场景Postgres 有两个容易跟连接池打架的超时参数idle_session_timeoutPG 14连接空闲超过 N 秒直接 kill。不区分是否在事务中只要没跑查询就算 idle。这个杀伤力最大——连接池里min_size维持的那几条空闲连接过一会儿就全被 PG 杀了。idle_in_transaction_session_timeout连接处在事务中但啥也没干超过 N 秒kill。比上面那个温和一些只干开着事务摸鱼的连接。这两个参数在大多数云厂商的 PG 实例上都有默认值比如阿里云 RDS 默认idle_session_timeout 600s、idle_in_transaction_session_timeout 60s你甚至不一定知道它们开着。回到我们的场景。AsyncPostgresSaver在写 checkpoint 时会开启事务如果 agent 在两次 checkpoint 写入之间干了重活比如等 LLM 响应十几秒连接就可能被idle_in_transaction_session_timeout盯上。而池里那些min_size维持的空闲连接什么都不干也会被idle_session_timeout一波带走。被 kill 之后会怎样连接池不知道这事——连接对象看起来还在池里但底层 TCP 已经断了。下次getconn()拿出来用时直接报OperationalError用户体验就是 Agent 聊到一半突然崩了。应对方案方案一调大服务端参数如果有权限ALTER SYSTEM SET idle_session_timeout 0; -- 关掉空闲会话超时 ALTER SYSTEM SET idle_in_transaction_session_timeout 0; -- 关掉事务中空闲超时 SELECT pg_reload_conf();或者根据业务节奏设大一点比如idle_session_timeout 10min。不过云数据库通常不让你改这些方案二更实际。方案二连接池主动回收走在 PG kill 前面AsyncConnectionPool支持几个参数来控制连接生命周期_PG_POOL AsyncConnectionPool( conninfo, min_size4, max_size10, max_idle300, # 连接空闲超过 300s 自动回收 max_lifetime1800, # 连接存活超过 30min 强制回收 openFalse, )如果 PG 的idle_session_timeout是 600s那把max_idle设成 500s如果idle_in_transaction_session_timeout是 60s那把max_idle设成 50s。总之比 PG 的阈值小一点连接池就会在 PG 动手之前自己把连接关掉重建。另外max_lifetime也能兜底——不管连接状态如何到时间就回收。AsyncPostgresSaver构造时如果不传这两个参数底层AsyncConnectionPool的默认值是max_idle60010 分钟、max_lifetime36001小时。如果你的 PG 实例的 idle 超时比这些值小就一定要显式覆盖