news 2026/10/24 2:28:49

AI智能体项目实战记录(17):LangGraph 记忆、会话历史持久化之 PostgreSQL 选型说明及简单安装

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI智能体项目实战记录(17):LangGraph 记忆、会话历史持久化之 PostgreSQL 选型说明及简单安装

AI智能体项目实战记录(17):LangGraph 记忆、会话历史持久化之 PostgreSQL 选型说明及简单安装

1. 引言

1.1 现状:三份存储各管一摊

目前项目的持久化大致是这样:

数据当前存储作用
LangGraph 图状态(多轮记忆)checkpoints.sqlite按thread_id保存messages等状态
会话历史(左侧列表/详情)conversations.sqlite按user_id查询会话列表与消息
登录鉴权、调用日志MySQL用户、角色、菜单、ai_invoke_log等
知识库向量Milvus文档切片向量检索

单机、低并发时这套组合跑得很稳。但它有三个隐患:

  1. 多实例记忆分裂:每个副本一份 SQLite,同一个thread_id看到的历史不一样;
  2. 备份分散:两个 SQLite 文件 + MySQL + Milvus,恢复时要对四个地方;
  3. 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. 选型:为什么是 PostgreSQL

2.1 候选对比

方案优点缺点结论
SQLite零依赖、单文件、接入最简单多实例记忆分裂、写锁竞争、无网络访问仅适合单机开发
MySQL团队熟悉、现有业务已在用LangGraph没有官方 checkpointer,需社区实现或自研不选
PostgreSQL官方AsyncPostgresSaver、ACID、JSONB、扩展丰富(pgvector/pg_trgm 等)多一套实例要运维选它
Redis快、适合短期记忆、TTL 方便持久化弱于关系库、内存成本高、审计查询不方便可作为短期缓存,不作为唯一存储
MongoDB文档模型天然适合消息LangGraph 适配非官方、团队不熟、事务弱于 PG不选

2.2 LangGraph 官方适配

LangGraph 官方维护的 checkpointer 只有两个:

  • langgraph-checkpoint-sqlite(AsyncSqliteSaver)
  • langgraph-checkpoint-postgres(AsyncPostgresSaver)

也就是说,想从 SQLite 平滑升级到网络数据库,PG 是唯一官方路径。MySQL 需要自己实现BaseCheckpointSaver,要处理 checkpoint/writes/blob 的 upsert、父子关系、序列化与并发,正确性风险很高。

具体的依赖安装、AsyncPostgresSaver初始化、配置切换与回滚,见第 18 篇。

2.3 为什么不是继续用 SQLite

  • checkpoints.sqlite是本地文件,多副本部署时无法共享;
  • 同一台机器多 worker 共享一个文件,写操作串行,容易出现锁等待;
  • 备份只能拷文件,无法按thread_id做在线清理;
  • checkpoint 每个 super-step 都写一次,文件增长很快,长期运行需要手工VACUUM。

2.4 PG 的额外收益

  • JSONB:会话消息可以直接存 JSONB,既能查又能索引;
  • 扩展性:后续要全文检索可以上pg_trgm/pg_jieba,要向量可以上pgvector;
  • ACID + MVCC:多实例并发写不同thread_id互不阻塞;
  • 运维标准:pg_dump/pg_restore/ WAL 归档 / 监控都有成熟方案。

3. 安装:docker-compose(生产环境或者多节点实例不建议使用)

3.1 目录结构

chat_py_server/ └── deploy/ └── postgres/ ├── docker-compose.yml └── init/ # 可选:初始化 SQL

3.2 docker-compose.yml

services: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:5432"volumes:-pgdata:/var/lib/postgresql/data# - ./init:/docker-entrypoint-initdb.d # 有初始化sql再打开# ========== 资源限制(整机4G,开发环境) ==========deploy:resources:limits:cpus:'1.0'memory:1536Mreservations:cpus:'0.2'memory:512M# ========== PG 运行参数,适配小内存 4G服务器 ==========command:>postgres -c shared_buffers=384MB -c effective_cache_size=1152MB -c work_mem=16MB -c maintenance_work_mem=64MB -c max_connections=100 -c checkpoint_completion_target=0.9 -c wal_buffers=16MB -c default_statistics_target=100 -c idle_in_transaction_session_timeout=30000 -c autovacuum=on -c autovacuum_max_workers=2 -c autovacuum_naptime=60shealthcheck: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-c"select 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 写缓冲16MB
idle_in_transaction_session_timeout事务空闲超时单位是毫秒,30000 = 30 秒,防止连接挂着不提交
autovacuum*自动清理死元组开启,2 个 worker,60 秒一次

3.5 安全与运维注意

  • 密码:123456只是开发示例,生产必须改成强密码,并通过环境变量或 secret 注入;
  • 端口:生产不要直接把 5432 暴露到公网,建议127.0.0.1:5432:5432或只在内网网段开放;
  • 数据卷:pgdata必须持久化,容器重建不能丢;
  • 资源限制:deploy.resources在 Docker Compose v2 生效;老版本请改用mem_limit/cpus;
  • 时区:容器设置了TZ=Asia/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 bookworm,glibc 较新;postgres:16-bullseye基于 Debian 11,在旧内核/旧 Docker 上更兼容。本文的 compose 已改为:

image:postgres:16-bullseye

首次启动失败后,数据卷里可能留下半初始化的目录,切换镜像前先清理:

# 确认没有重要数据后再执行dockercompose down-vdockercompose up-d

如果仍然报错,按顺序排查:

  1. 升级 Docker Engine / Docker Desktop 与 libseccomp;

  2. 确认镜像架构与宿主机一致:

    dockerinfo--format'{{.Architecture}}'uname-m
  3. 临时放宽 seccomp(仅用于验证,不建议长期):

    security_opt:-seccomp:unconfined
  4. 暂时去掉deploy.resources限制,排除内存/进程数导致的fork失败;

  5. 重新拉取镜像:docker pull postgres:16-bullseye。

4. 与 MySQL 对比

4.1 功能与运维对比

维度MySQL 8PostgreSQL 16
LangGraph checkpointer无官方实现官方AsyncPostgresSaver
JSON 支持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读 JSON",PG 的JSONB+ 复合索引更顺手;
  • checkpointer 是频繁 upsert + blob 读写,PG 的 MVCC 与ON CONFLICT更匹配。

结论:不是因为 MySQL 性能不行,而是因为 LangGraph 官方只适配了 PG。

4.3 什么情况下不建议迁 MySQL

  • 现有 MySQL 已经稳定,团队没有 PG 运维经验;
  • 登录鉴权涉及用户、角色、菜单、日志多张表,迁移窗口和回滚方案成本高;
  • 只是为了"技术统一",没有明确的并发或多实例诉求。

如果确实要迁,建议单独排期,步骤:

  1. 建表:把 MySQL DDL 转成 PG(TINYINT→SMALLINT/BOOLEAN,DATETIME→TIMESTAMPTZ,反引号 → 双引号);
  2. 导数据:用pgloader做 MySQL → PG 的一次性迁移,或导出 CSV 再COPY;
  3. 换驱动:db_service._build_url()从mysql+pymysql换成postgresql+psycopg,去掉charset参数;
  4. 改 SQL 方言:ON DUPLICATE KEY、LIMIT offset,size、IFNULL、日期函数等;
  5. 灰度/双读:先读 PG、写双份,观察一段时间再切写;
  6. 保留回滚:旧 MySQL 至少保留一个发布周期。

db_service的改动示意:

def_build_url()->URL:returnURL.create("postgresql+psycopg",username=setting.pg_user,password=setting.pg_password,host=setting.pg_host,port=setting.pg_port,database=setting.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_char,DATE_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 容器限制是1536MB,shared_buffers=384MB,连索引都放不下;
  • Milvus 当前用的是DISKANN(磁盘索引),本身就是为"大于内存"的数据设计的。

(2)索引构建资源不够

当前maintenance_work_mem=64MB。建 HNSW 索引远不够,需要临时调到几百 MB 甚至 GB 级,且构建期间会占满 CPU,影响 checkpoint 写入与业务查询。

(3)同库混跑会互相拖累

如果 PG 同时承担:

  • LangGraph checkpoint 写入;
  • 会话历史读写;
  • 向量检索与索引构建;

那么一次索引重建或大批量导入,就可能让对话请求的 P99 明显抖动。专业向量库的价值就在于资源隔离。

(4)规模与并发

数据量pgvectorMilvus
< 100 万 chunk很轻松轻松
100 万 ~ 1000 万需要调参、分区、独立实例轻松
> 1000 万吃紧合适
> 1 亿不建议分布式强项

(5)高级能力差距

  • Milvus 支持多种索引、分区键、标量过滤、稀疏向量/混合检索;
  • pgvector 的过滤是 SQLWHERE,和向量索引组合时可能出现"先过滤后检索"或"先检索后过滤"的选择问题,召回与性能需要仔细调;
  • 中文全文检索 PG 还要额外装分词扩展。

(6)迁移成本不低

不是改连接串,而是:

  1. 重写milvus_service.search / query / create_collection / delete_by_filter;
  2. Milvus 的filter表达式翻译成 SQL;
  3. 从 Milvus 导出向量与字段,再写入 PG;
  4. 重建索引,并用真实语料验证 recall@5 与 P95 延迟。

5.3 什么时候才值得换

  • 知识库规模 < 100 万 chunk;
  • QPS 不高(几十以内);
  • 想彻底去掉 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_BACKEND=postgres PG_HOST / PG_PORT / PG_USER / PG_PASSWORD / PG_DATABASE ④ 启动服务 观察日志:已启用 PostgreSQL 记忆持久化 ⑤ 验证 一轮对话 → 重启 → 同 thread_id 继续追问 ⑥ 迁会话历史(可选) 建 conversations 表 → 改 conversation_service 内部实现 ⑦ 清理与备份 定期删除旧 thread 的 checkpoint;pg_dump 备份

8. 运维要点

8.1 备份与恢复

# 逻辑备份dockerexecpostgres pg_dump-Upguser-dpgdb-Fc>pgdb_$(date+%F).dump# 恢复dockerexec-ipostgres pg_restore-Upguser-dpgdb<pgdb_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 = 40,max_connections=100足够;
  • 不要把池调得过大,连接本身也占内存。

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. 常见问题

Q1:PG 和 MySQL 能同时存在吗?

可以。推荐做法:

  • PG:checkpoint + 会话历史;
  • MySQL:登录鉴权 + 调用日志(暂时);
  • 等业务稳定后再统一。

Q2:会话历史迁 PG 后,checkpoint 还需要迁吗?

需要,但两件事互相独立。checkpoint 的适配代码、配置切换与回滚见第 18 篇;会话历史只影响前端列表与详情。

Q3:为什么不用 Redis 做会话历史?

Redis 适合短期、可丢失的数据;会话历史要长期保留、按user_id列表查询、按thread_id读详情,关系库更合适。

Q4:登录鉴权迁 PG 要注意什么?

先转 DDL 与数据(pgloader),再换 SQLAlchemy 驱动与 SQL 方言,最后灰度切写;旧 MySQL 至少保留一个发布周期用于回滚。

Q5:用了 PG 之后 SQLite 依赖能删吗?

会话历史目前仍可能使用 SQLite;LangGraph 也保留了CHECKPOINT_BACKEND=sqlite的本地开发回退。建议至少保留一个版本周期。

Q6:conversations.messages和 checkpoint 的messages会重复吗?

会。两者用途不同:前者给前端展示,后者给 LangGraph 做上下文。短期保留两份是可接受的;如果后续要消重,建议只把conversations退化为索引表,不要直接读 checkpoint 内部表。

10. 完整代码汇总

10.1deploy/postgres/docker-compose.yml

services: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:5432"volumes:-pgdata:/var/lib/postgresql/data# - ./init:/docker-entrypoint-initdb.d # 有初始化sql再打开deploy:resources:limits:cpus:'1.0'memory:1536Mreservations:cpus:'0.2'memory:512Mcommand:>postgres -c shared_buffers=384MB -c effective_cache_size=1152MB -c work_mem=16MB -c maintenance_work_mem=64MB -c max_connections=100 -c checkpoint_completion_target=0.9 -c wal_buffers=16MB -c default_statistics_target=100 -c idle_in_transaction_session_timeout=30000 -c autovacuum=on -c autovacuum_max_workers=2 -c autovacuum_naptime=60shealthcheck: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. 小结

  1. 安装:4G 机器用postgres:16+ 1.5G 内存限制 + 384MBshared_buffers足够支撑 checkpoint 与会话历史;
  2. 会话历史:表结构从 SQLite 的 TEXT 升级为JSONB + TIMESTAMPTZ,conversation_service接口不变;
  3. MySQL 对比:不是性能问题,而是官方适配问题;登录鉴权迁 PG 可行,但要单独排期、用 pgloader + 灰度;
  4. 向量库:pgvector 能力上可以替代 Milvus,但本项目内存与规模不匹配,不建议换,继续用 Milvus 的 DISKANN;
  5. 最终形态:PG 负责 checkpoint + 业务 + 会话历史,Milvus 负责向量检索,各司其职;
  6. LangGraph 适配:依赖、AsyncPostgresSaver初始化、配置切换、回滚与排障,全部放在第 18 篇。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 2:27:51

信用卡见顶、房贷失速、财富集中:招行24份年报里的居民资产负债表

24份年报里招行视角下的中国经济的时间胶囊 从2002年A股上市到2025年年报&#xff0c;从3,717亿总资产到13.07万亿&#xff0c;从17.3亿净利润到1,501.8亿。二十四年&#xff0c;一家银行把脚印踩成了数据。00 为什么是24份年报 你在这家银行存了多少钱&#xff0c;借了多少房贷…

作者头像 李华
网站建设 2026/10/24 2:26:36

OpenCV光流法:追踪视频中物体运动轨迹

在视频处理与分析领域&#xff0c;准确追踪视频中物体的运动轨迹是一项非常重要的任务。它在诸多实际场景中都有着广泛的应用&#xff0c;比如交通监控领域可以分析车辆的行驶轨迹&#xff0c;视频安防领域能追踪可疑人员的行动路线等。而OpenCV中的光流法就是一种强大的工具&a…

作者头像 李华
网站建设 2026/10/24 2:25:48

CGCS2000 转北京老地方坐标系(北京 1963)完整方案

CGCS2000 转北京1963老地方坐标系&#xff1a;简明完整方案摘要&#xff1a;北京老项目、历史征地红线、河湖划界、早期施工图仍常见北京1963地方坐标系。CGCS2000转北京1963不是简单平移旋转&#xff0c;而是“基准转换高斯投影”两步走。大范围、权属报批优先布尔莎七参数&am…

作者头像 李华
网站建设 2026/10/24 2:25:03

校园安全平台过等保二级,开发阶段要注意什么

文章摘要&#xff1a;等保二级的测评项里&#xff0c;身份鉴别、安全审计、剩余信息保护这三块&#xff0c;事后补的成本最高。本文按开发者视角&#xff0c;把等保 2.0 的五个技术层面拆成可落地的开发动作&#xff1a;身份鉴别的三条、审计记录的四个字段、剩余信息保护的三个…

作者头像 李华
网站建设 2026/10/24 2:24:50

船舶倾斜AI检测:装卸作业早预警

靠泊装卸的时候&#xff0c;船身稍微一偏&#xff0c;吊具、桥吊、岸边的人全跟着提心吊胆。船舶倾斜看着是小角度&#xff0c;真要发展成倾覆&#xff0c;一条船、一船货、一帮人都搭进去。人工盯着船身看&#xff0c;隔得远、角度变化慢&#xff0c;等肉眼看出偏了&#xff0…

作者头像 李华
网站建设 2026/10/24 2:24:29

Agent 跑三分钟的任务,HTTP 等不了,异步长任务的三个接口

Agent 跑三分钟的任务&#xff0c;HTTP 等不了&#xff0c;异步长任务的三个接口 让 Agent 查一次季度报表&#xff0c;它要调五个工具&#xff0c;翻两次知识库&#xff0c;最后写一段总结&#xff0c;全程三分钟。同步接口扛不住这个时长&#xff0c;网关默认超时一分钟&…

作者头像 李华