AI智能体项目实战记录17LangGraph 记忆、会话历史持久化之 PostgreSQL 选型说明及简单安装1. 引言1.1 现状三份存储各管一摊目前项目的持久化大致是这样数据当前存储作用LangGraph 图状态多轮记忆checkpoints.sqlite按thread_id保存messages等状态会话历史左侧列表/详情conversations.sqlite按user_id查询会话列表与消息登录鉴权、调用日志MySQL用户、角色、菜单、ai_invoke_log等知识库向量Milvus文档切片向量检索单机、低并发时这套组合跑得很稳。但它有三个隐患多实例记忆分裂每个副本一份 SQLite同一个thread_id看到的历史不一样备份分散两个 SQLite 文件 MySQL Milvus恢复时要对四个地方SQLite 写锁多 worker 或 checkpoint 写入频繁时容易出现database is locked。1.2 目标引入一个 PostgreSQL先把最需要并发与可靠性的两类数据迁过去LangGraph checkpoint图状态、多轮记忆会话历史conversations。后续如果愿意再把登录鉴权的 MySQL 也迁到 PG最终形成PostgreSQL业务库 LangGraph checkpoint 会话历史 Milvus 知识库向量暂不迁移1.3 结论先行问题结论LangGraph 记忆用 PG 合不合适合适有官方AsyncPostgresSaver多实例友好适配代码见第 18 篇会话历史迁 PG 合不合适合适表结构简单SQL 改造成本低登录鉴权 MySQL 迁 PG 合不合适可以但建议单独排期、用 pgloader 灰度不要和本次一起动用 PG 替换 Milvus 做向量库合不合适能力上可以本项目不建议性能与资源不匹配详见第 6 章2. 选型为什么是 PostgreSQL2.1 候选对比方案优点缺点结论SQLite零依赖、单文件、接入最简单多实例记忆分裂、写锁竞争、无网络访问仅适合单机开发MySQL团队熟悉、现有业务已在用LangGraph没有官方 checkpointer需社区实现或自研不选PostgreSQL官方AsyncPostgresSaver、ACID、JSONB、扩展丰富pgvector/pg_trgm 等多一套实例要运维选它Redis快、适合短期记忆、TTL 方便持久化弱于关系库、内存成本高、审计查询不方便可作为短期缓存不作为唯一存储MongoDB文档模型天然适合消息LangGraph 适配非官方、团队不熟、事务弱于 PG不选2.2 LangGraph 官方适配LangGraph 官方维护的 checkpointer 只有两个langgraph-checkpoint-sqliteAsyncSqliteSaverlanggraph-checkpoint-postgresAsyncPostgresSaver也就是说想从 SQLite 平滑升级到网络数据库PG 是唯一官方路径。MySQL 需要自己实现BaseCheckpointSaver要处理 checkpoint/writes/blob 的 upsert、父子关系、序列化与并发正确性风险很高。具体的依赖安装、AsyncPostgresSaver初始化、配置切换与回滚见第 18 篇。2.3 为什么不是继续用 SQLitecheckpoints.sqlite是本地文件多副本部署时无法共享同一台机器多 worker 共享一个文件写操作串行容易出现锁等待备份只能拷文件无法按thread_id做在线清理checkpoint 每个 super-step 都写一次文件增长很快长期运行需要手工VACUUM。2.4 PG 的额外收益JSONB会话消息可以直接存 JSONB既能查又能索引扩展性后续要全文检索可以上pg_trgm/pg_jieba要向量可以上pgvectorACID MVCC多实例并发写不同thread_id互不阻塞运维标准pg_dump/pg_restore/ WAL 归档 / 监控都有成熟方案。3. 安装docker-compose生产环境或者多节点实例不建议使用3.1 目录结构chat_py_server/ └── deploy/ └── postgres/ ├── docker-compose.yml └── init/ # 可选初始化 SQL3.2 docker-compose.ymlservices:postgres:image:postgres:16-bullseye# 兼容老内核/老 Docker默认 postgres:16 在部分环境会报 popen failurecontainer_name:postgresrestart:unless-stoppedenvironment:POSTGRES_USER:pguserPOSTGRES_PASSWORD:123456# 开发记得改密码POSTGRES_DB:pgdbTZ:Asia/ShanghaiPGDATA:/var/lib/postgresql/dataports:-5432:5432volumes:-pgdata:/var/lib/postgresql/data# - ./init:/docker-entrypoint-initdb.d # 有初始化sql再打开# 资源限制整机4G开发环境 deploy:resources:limits:cpus:1.0memory:1536Mreservations:cpus:0.2memory:512M# PG 运行参数适配小内存 4G服务器 command:postgres -c shared_buffers384MB -c effective_cache_size1152MB -c work_mem16MB -c maintenance_work_mem64MB -c max_connections100 -c checkpoint_completion_target0.9 -c wal_buffers16MB -c default_statistics_target100 -c idle_in_transaction_session_timeout30000 -c autovacuumon -c autovacuum_max_workers2 -c autovacuum_naptime60shealthcheck:test:[CMD-SHELL,pg_isready -U pguser -d pgdb]interval:10stimeout:5sretries:5volumes:pgdata:3.3 启动与验证cddeploy/postgresdockercompose up-ddockercomposepsdockercompose logs-fpostgres进入容器验证dockerexec-itpostgres psql-Upguser-dpgdb-cselect version();dockerexec-itpostgres psql-Upguser-dpgdb-c\l健康检查通过后docker compose ps的STATUS会显示healthy。3.4 关键参数解释参数作用本项目取值说明shared_buffersPG 自己用的共享缓存384MB约占内存 25%effective_cache_size告诉优化器系统可用缓存1152MB引导索引选择work_mem单次排序/哈希操作内存16MB避免复杂查询打爆内存maintenance_work_mem维护操作VACUUM/建索引内存64MB小内存机器够用以后建向量索引要调大max_connections最大连接数100够多个 worker 池使用checkpoint_completion_targetcheckpoint 写盘平滑度0.9减少 IO 抖动wal_buffersWAL 写缓冲16MBidle_in_transaction_session_timeout事务空闲超时单位是毫秒30000 30 秒防止连接挂着不提交autovacuum*自动清理死元组开启2 个 worker60 秒一次3.5 安全与运维注意密码123456只是开发示例生产必须改成强密码并通过环境变量或 secret 注入端口生产不要直接把 5432 暴露到公网建议127.0.0.1:5432:5432或只在内网网段开放数据卷pgdata必须持久化容器重建不能丢资源限制deploy.resources在 Docker Compose v2 生效老版本请改用mem_limit/cpus时区容器设置了TZAsia/Shanghai业务字段建议统一用TIMESTAMPTZ。3.6 启动报错popen failure/initdb找不到postgres如果docker compose up -d后容器反复重启日志出现popen failure: Operation not permitted initdb: error: program postgres is needed by initdb but was not found in the same directory as /usr/lib/postgresql/16/bin/initdb原因不是镜像里真的缺postgres二进制而是initdb通过popen去执行同目录的postgres --version时被系统拒绝了。常见触发条件Docker Engine / libseccomp 版本较老默认 seccomp 策略拦截了新 glibc 使用的clone3返回EPERM宿主机是受限容器LXC/OpenVZ或开启了额外安全策略极低内存/进程数限制下fork/exec失败有的环境报Cannot allocate memory。首选修复换-bullseye镜像官方默认的postgres:16基于 Debian bookwormglibc 较新postgres:16-bullseye基于 Debian 11在旧内核/旧 Docker 上更兼容。本文的 compose 已改为image:postgres:16-bullseye首次启动失败后数据卷里可能留下半初始化的目录切换镜像前先清理# 确认没有重要数据后再执行dockercompose down-vdockercompose up-d如果仍然报错按顺序排查升级 Docker Engine / Docker Desktop 与 libseccomp确认镜像架构与宿主机一致dockerinfo--format{{.Architecture}}uname-m临时放宽 seccomp仅用于验证不建议长期security_opt:-seccomp:unconfined暂时去掉deploy.resources限制排除内存/进程数导致的fork失败重新拉取镜像docker pull postgres:16-bullseye。4. 与 MySQL 对比4.1 功能与运维对比维度MySQL 8PostgreSQL 16LangGraph checkpointer无官方实现官方AsyncPostgresSaverJSON 支持JSON部分索引需虚拟列JSONB可直接建 GIN 索引UpsertON DUPLICATE KEY UPDATEON CONFLICT ... DO UPDATE自增主键AUTO_INCREMENTGENERATED ... AS IDENTITY/serial布尔TINYINT(1)boolean大小写表名受系统影响可能大小写敏感未加引号的标识符统一小写并发模型InnoDB MVCCMVCC 更丰富的锁粒度扩展插件生态偏商业pgvector、pg_trgm、pg_jieba等扩展丰富运维成熟团队更熟成熟标准工具链4.2 性能上谁更好对本项目这种 OLTP 小数据量 JSON 读写的场景两者差距远小于用错索引/连接池带来的差距ai_invoke_log是纯插入MySQL 和 PG 都能轻松处理会话历史是按user_id查列表 按thread_id读 JSONPG 的JSONB 复合索引更顺手checkpointer 是频繁 upsert blob 读写PG 的 MVCC 与ON CONFLICT更匹配。结论不是因为 MySQL 性能不行而是因为 LangGraph 官方只适配了 PG。4.3 什么情况下不建议迁 MySQL现有 MySQL 已经稳定团队没有 PG 运维经验登录鉴权涉及用户、角色、菜单、日志多张表迁移窗口和回滚方案成本高只是为了技术统一没有明确的并发或多实例诉求。如果确实要迁建议单独排期步骤建表把 MySQL DDL 转成 PGTINYINT→SMALLINT/BOOLEANDATETIME→TIMESTAMPTZ反引号 → 双引号导数据用pgloader做 MySQL → PG 的一次性迁移或导出 CSV 再COPY换驱动db_service._build_url()从mysqlpymysql换成postgresqlpsycopg去掉charset参数改 SQL 方言ON DUPLICATE KEY、LIMIT offset,size、IFNULL、日期函数等灰度/双读先读 PG、写双份观察一段时间再切写保留回滚旧 MySQL 至少保留一个发布周期。db_service的改动示意def_build_url()-URL:returnURL.create(postgresqlpsycopg,usernamesetting.pg_user,passwordsetting.pg_password,hostsetting.pg_host,portsetting.pg_port,databasesetting.pg_database,)4.4 迁移时的常见坑坑说明表名大小写PG 未加引号会转小写Java 侧如果用了驼峰表名要统一TINYINT(1)迁成boolean后Java/Python 映射要调整AUTO_INCREMENT改用GENERATED BY DEFAULT AS IDENTITY反引号PG 用双引号且不建议在 DDL 里使用引号ON DUPLICATE KEY改ON CONFLICT冲突列必须有唯一索引日期函数DATE_FORMAT→to_charDATE_SUB→interval排序规则中文排序/大小写行为与 MySQL 不同需要验证5. 能不能用 PG 替换 Milvus 做向量库5.1 能力上可以PG 通过pgvector扩展可以做向量检索CREATEEXTENSIONIFNOTEXISTSvector;CREATETABLEdoc_chunks(idVARCHAR(64)PRIMARYKEY,nameVARCHAR(1024),doc_strTEXT,vector vector(768));CREATEINDEXidx_doc_chunks_vectorONdoc_chunksUSINGhnsw(vector vector_cosine_ops);当前集合是id / name / doc_str / vector、768 维、COSINE、DISKANN迁到 pgvector 用vector(768)vector_cosine_ops可以对上。5.2 但本项目不建议换原因如下1内存模型不匹配pgvector 的 HNSW 索引基本常驻内存1M 条 768 维 float32 向量原始数据约 3GB加上索引与行开销更大当前 PG 容器限制是1536MBshared_buffers384MB连索引都放不下Milvus 当前用的是DISKANN磁盘索引本身就是为大于内存的数据设计的。2索引构建资源不够当前maintenance_work_mem64MB。建 HNSW 索引远不够需要临时调到几百 MB 甚至 GB 级且构建期间会占满 CPU影响 checkpoint 写入与业务查询。3同库混跑会互相拖累如果 PG 同时承担LangGraph checkpoint 写入会话历史读写向量检索与索引构建那么一次索引重建或大批量导入就可能让对话请求的 P99 明显抖动。专业向量库的价值就在于资源隔离。4规模与并发数据量pgvectorMilvus 100 万 chunk很轻松轻松100 万 ~ 1000 万需要调参、分区、独立实例轻松 1000 万吃紧合适 1 亿不建议分布式强项5高级能力差距Milvus 支持多种索引、分区键、标量过滤、稀疏向量/混合检索pgvector 的过滤是 SQLWHERE和向量索引组合时可能出现先过滤后检索或先检索后过滤的选择问题召回与性能需要仔细调中文全文检索 PG 还要额外装分词扩展。6迁移成本不低不是改连接串而是重写milvus_service.search / query / create_collection / delete_by_filterMilvus 的filter表达式翻译成 SQL从 Milvus 导出向量与字段再写入 PG重建索引并用真实语料验证 recall5 与 P95 延迟。5.3 什么时候才值得换知识库规模 100 万 chunkQPS 不高几十以内想彻底去掉 Milvus减少一套 etcd MinIO 的运维能接受把 PG 的内存调大至少 4~8GB并接受索引构建期间的影响做了基准测试recall 与延迟达标。5.4 建议PG 只做 checkpoint 业务 会话历史向量继续用 Milvus。这是当前资源与规模下最稳的组合。等数据量和团队运维能力都到位再评估 pgvector 或 pgvectorscale。6. 一次完整落地步骤① 启动 PG docker compose up -d 确认 healthy ② 安装 PG 依赖并改造 checkpointer 详见第 18 篇《LangGraph 适配 PostgreSQL》 ③ 改配置 CHECKPOINT_BACKENDpostgres PG_HOST / PG_PORT / PG_USER / PG_PASSWORD / PG_DATABASE ④ 启动服务 观察日志已启用 PostgreSQL 记忆持久化 ⑤ 验证 一轮对话 → 重启 → 同 thread_id 继续追问 ⑥ 迁会话历史可选 建 conversations 表 → 改 conversation_service 内部实现 ⑦ 清理与备份 定期删除旧 thread 的 checkpointpg_dump 备份8. 运维要点8.1 备份与恢复# 逻辑备份dockerexecpostgres pg_dump-Upguser-dpgdb-Fcpgdb_$(date%F).dump# 恢复dockerexec-ipostgres pg_restore-Upguser-dpgdbpgdb_2026-01-01.dump# 数据卷备份停机或低峰dockerrun--rm-vpgdata:/var/lib/postgresql/data-v$(pwd):/backup alpine\tarczf /backup/pgdata.tar.gz-C/var/lib/postgresql/data.8.2 checkpoint 清理checkpoint 的清理方式adelete_thread/ 按时间清理 / 关联表注意事项属于 LangGraph 适配的一部分统一放在第 18 篇说明。8.3 连接池每个 worker 一份连接池总连接数 ≈worker 数 × 池最大连接数要小于max_connections例4 worker × 10 40max_connections100足够不要把池调得过大连接本身也占内存。8.4 监控-- 当前连接selectstate,count(*)frompg_stat_activitygroupbystate;-- 表膨胀selectrelname,n_live_tup,n_dead_tupfrompg_stat_user_tablesorderbyn_dead_tupdesclimit10;-- 长事务selectpid,now()-xact_startasduration,queryfrompg_stat_activitywherexact_startisnotnullorderbydurationdesc;9. 常见问题Q1PG 和 MySQL 能同时存在吗可以。推荐做法PGcheckpoint 会话历史MySQL登录鉴权 调用日志暂时等业务稳定后再统一。Q2会话历史迁 PG 后checkpoint 还需要迁吗需要但两件事互相独立。checkpoint 的适配代码、配置切换与回滚见第 18 篇会话历史只影响前端列表与详情。Q3为什么不用 Redis 做会话历史Redis 适合短期、可丢失的数据会话历史要长期保留、按user_id列表查询、按thread_id读详情关系库更合适。Q4登录鉴权迁 PG 要注意什么先转 DDL 与数据pgloader再换 SQLAlchemy 驱动与 SQL 方言最后灰度切写旧 MySQL 至少保留一个发布周期用于回滚。Q5用了 PG 之后 SQLite 依赖能删吗会话历史目前仍可能使用 SQLiteLangGraph 也保留了CHECKPOINT_BACKENDsqlite的本地开发回退。建议至少保留一个版本周期。Q6conversations.messages和 checkpoint 的messages会重复吗会。两者用途不同前者给前端展示后者给 LangGraph 做上下文。短期保留两份是可接受的如果后续要消重建议只把conversations退化为索引表不要直接读 checkpoint 内部表。10. 完整代码汇总10.1deploy/postgres/docker-compose.ymlservices:postgres:image:postgres:16-bullseye# 兼容老内核/老 Docker默认 postgres:16 在部分环境会报 popen failurecontainer_name:postgresrestart:unless-stoppedenvironment:POSTGRES_USER:pguserPOSTGRES_PASSWORD:123456# 开发记得改密码POSTGRES_DB:pgdbTZ:Asia/ShanghaiPGDATA:/var/lib/postgresql/dataports:-5432:5432volumes:-pgdata:/var/lib/postgresql/data# - ./init:/docker-entrypoint-initdb.d # 有初始化sql再打开deploy:resources:limits:cpus:1.0memory:1536Mreservations:cpus:0.2memory:512Mcommand:postgres -c shared_buffers384MB -c effective_cache_size1152MB -c work_mem16MB -c maintenance_work_mem64MB -c max_connections100 -c checkpoint_completion_target0.9 -c wal_buffers16MB -c default_statistics_target100 -c idle_in_transaction_session_timeout30000 -c autovacuumon -c autovacuum_max_workers2 -c autovacuum_naptime60shealthcheck:test:[CMD-SHELL,pg_isready -U pguser -d pgdb]interval:10stimeout:5sretries:5volumes:pgdata:10.2 会话历史表结构CREATETABLEIFNOTEXISTSconversations(thread_idVARCHAR(64)PRIMARYKEY,user_idVARCHAR(64)NOTNULL,titleVARCHAR(200)NOTNULLDEFAULT,messages JSONBNOTNULLDEFAULT[]::jsonb,created_at TIMESTAMPTZNOTNULLDEFAULTnow(),updated_at TIMESTAMPTZNOTNULLDEFAULTnow());CREATEINDEXIFNOTEXISTSidx_conversations_user_idONconversations(user_id,updated_atDESC);11. 小结安装4G 机器用postgres:16 1.5G 内存限制 384MBshared_buffers足够支撑 checkpoint 与会话历史会话历史表结构从 SQLite 的 TEXT 升级为JSONB TIMESTAMPTZconversation_service接口不变MySQL 对比不是性能问题而是官方适配问题登录鉴权迁 PG 可行但要单独排期、用 pgloader 灰度向量库pgvector 能力上可以替代 Milvus但本项目内存与规模不匹配不建议换继续用 Milvus 的 DISKANN最终形态PG 负责 checkpoint 业务 会话历史Milvus 负责向量检索各司其职LangGraph 适配依赖、AsyncPostgresSaver初始化、配置切换、回滚与排障全部放在第 18 篇。
阅读完成 · 觉得有帮助?