feat(storage): add selectable SQLite and PostgreSQL persistence
- Select one storage backend per instance via AGENT_STORAGE, retaining memory as the backward-compatible default. Manage the selected async repository, official checkpointer and connection pools in lifespan; fail startup on unavailable or uninitialized storage without fallback. - Add SQLite and PostgreSQL adapters, schema initialization CLI, sample PostgreSQL compose configuration and a Windows-compatible serve entry. SQLite enforces single-instance file ownership; PostgreSQL uses a dedicated pool for session advisory locks plus version/epoch fencing. - Make repository IO and commit callbacks awaitable. Persist business fields, a complete committed graph snapshot and turn result/version in one business transaction before success. Rebuild execution checkpoints from that snapshot, discarding uncommitted progress after interruption. - Support optional Idempotency-Key on durable chat requests. Replay committed results and return 409 on changed request content; delete removes business/history data and replay records while retaining only an epoch/version tombstone to reject stale commits. - Document migration, database switching boundaries, backup/deployment expectations and crash recovery. Ignore generated database files. Validation: 315 tests passed with real SQLite and isolated PostgreSQL, including abrupt child-process exits before/after business commit; both backends passed CLI migration and actual HTTP server restart smoke. Existing API tests were rechecked after isolating them from user storage. An initial Windows CreateProcess permission failure did not recur in the full diagnostic run; failure assertions were not skipped or relaxed.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
# LangGraph 会话存储:memory / SQLite / PostgreSQL
|
||||
|
||||
每个应用实例只选择一个存储后端。两个数据库地址可以同时写在 `.env`,
|
||||
启动时只连接 `AGENT_STORAGE` 指定的一套;切换后需要重启,配置切换不搬迁历史数据。
|
||||
现有 `DATABASE_URL` 不控制本模块。
|
||||
|
||||
| 模式 | 用途 | checkpoint | 会话互斥 |
|
||||
|---|---|---|---|
|
||||
| memory(默认) | 离线测试与临时开发,重启丢失 | InMemorySaver | 进程内会话锁 |
|
||||
| sqlite | 单实例、单 worker,本地持久化 | AsyncSqliteSaver | 进程内会话锁 + 数据库文件的实例锁 |
|
||||
| postgresql | 多 worker、多实例 | AsyncPostgresSaver | 进程内锁 + PostgreSQL session advisory lock |
|
||||
|
||||
SQLite 实例锁由操作系统管理,第二个实例打开同一数据库会失败,进程退出自动释放。
|
||||
SQLite 文件必须放在本地持久化磁盘,不作为多主机共享文件使用。PostgreSQL 的
|
||||
锁连接与业务事务连接使用独立连接池;LLM 运行期间不持有业务行事务。
|
||||
|
||||
## SQLite 启用步骤
|
||||
|
||||
先安装 `requirements.txt`,Python 使用 Conda `py3.11` 环境。在 `.env` 中设置:
|
||||
|
||||
```dotenv
|
||||
AGENT_BACKEND=langgraph
|
||||
AGENT_STORAGE=sqlite
|
||||
AGENT_SQLITE_PATH=./data/agent.sqlite3
|
||||
AGENT_DB_AUTO_MIGRATE=false
|
||||
```
|
||||
|
||||
首次启动前初始化两类表:
|
||||
|
||||
```bash
|
||||
conda run -n py3.11 python -m src.storage.migrate --backend sqlite
|
||||
conda run -n py3.11 python -m src.serve --port 8000
|
||||
```
|
||||
|
||||
可用 `--sqlite-path` 指定迁移目标,但必须与服务配置相同。
|
||||
本地开发可以显式设 `AGENT_DB_AUTO_MIGRATE=true`,在启动阶段建表;请求处理不建表。
|
||||
默认关闭自动 migration,未初始化、版本不匹配或数据库不可用时启动失败,不回退内存。
|
||||
|
||||
## PostgreSQL 启用步骤
|
||||
|
||||
使用已有 PostgreSQL 数据库,或用 `compose.agent-db.yml` 启动本地开发库。
|
||||
建议为本服务使用独立数据库,业务表使用 `agent_` 前缀,checkpointer 管理自己的表。
|
||||
|
||||
```dotenv
|
||||
AGENT_BACKEND=langgraph
|
||||
AGENT_STORAGE=postgresql
|
||||
AGENT_POSTGRES_DSN=postgresql://agent:replace-me@127.0.0.1:5432/agent
|
||||
AGENT_DB_POOL_SIZE=10
|
||||
AGENT_DB_LOCK_TIMEOUT_SECONDS=30
|
||||
AGENT_DB_AUTO_MIGRATE=false
|
||||
|
||||
# 以下四项供本地 compose 使用;密码必须与 DSN 一致。
|
||||
AGENT_POSTGRES_USER=agent
|
||||
AGENT_POSTGRES_DB=agent
|
||||
AGENT_POSTGRES_PASSWORD=replace-me
|
||||
AGENT_POSTGRES_PORT=5432
|
||||
```
|
||||
|
||||
`replace-me` 是占位值,需要改为自己的密码;含特殊字符的 DSN 密码需 URL 编码。
|
||||
|
||||
```bash
|
||||
docker compose -f compose.agent-db.yml up -d
|
||||
conda run -n py3.11 python -m src.storage.migrate --backend postgresql
|
||||
conda run -n py3.11 python -m src.serve --port 8000
|
||||
```
|
||||
|
||||
Linux 部署也可使用 `uvicorn src.main:app --host 0.0.0.0 --port 8000 --workers 4`。
|
||||
Windows 本地使用 `python -m src.serve`,该入口选择 psycopg 支持的 SelectorEventLoop。
|
||||
连接池在 lifespan 内创建、关闭;每个 worker 使用独立池。业务、checkpoint、
|
||||
会话锁各有一个池,每池最大连接数由 `AGENT_DB_POOL_SIZE` 控制,需计入数据库容量。
|
||||
同一组 worker 必须使用相同数据库,不能混用 SQLite 和 PostgreSQL。
|
||||
|
||||
## 数据结构与恢复规则
|
||||
|
||||
- `agent_sessions`:强类型业务字段、单调递增版本、会话 epoch、最后提交的完整图快照。
|
||||
- `agent_turns`:稳定 turn ID、pending/committed/aborted 状态、可选幂等键、请求摘要、已提交结果。
|
||||
- `agent_schema`:本模块 migration 版本。LangGraph saver 独立管理自己的 migration 和 checkpoint 表。
|
||||
- `/get_info` 以业务字段为权威读源。图快照保存历史、阶段和簿记;未确认的 checkpoint 不作为成功业务结果。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as 应用服务
|
||||
participant B as 业务数据库
|
||||
participant G as LangGraph/checkpointer
|
||||
A->>B: 同会话锁内记录 pending turn
|
||||
B-->>A: 已提交字段、版本、完整图快照
|
||||
A->>G: 清除旧执行 checkpoint,从提交快照执行本轮
|
||||
G-->>A: 合法前缀及暂定正文增量
|
||||
G->>G: 完成节点与 checkpoint 写入
|
||||
G->>A: await 提交回调(完整结果)
|
||||
A->>B: 同一事务提交字段、快照、版本和 committed 结果
|
||||
B-->>A: 提交成功
|
||||
A-->>A: 允许成功响应 / SSE done
|
||||
```
|
||||
|
||||
第一版每轮从业务提交快照重建图执行,丢弃旧 checkpoint 执行进度;不会从中断节点中途续跑。
|
||||
这会减少 saver 中的历史 checkpoint,但已提交的对话历史保存在业务图快照中,
|
||||
轮次结果保存在业务日志中。LLM 决策与流程提示词保持不变。
|
||||
|
||||
| 故障窗口 | 下次请求的确定行为 |
|
||||
|---|---|
|
||||
| checkpoint 先写入,业务提交前退出 | pending 轮次转为 aborted,丢弃未提交执行进度,从上次业务快照开始 |
|
||||
| 业务提交成功,checkpoint 缺失或滞后 | 业务快照是权威,下次重新构建 checkpoint,不重复应用字段差量 |
|
||||
| 业务提交成功,成功响应发送前退出 | 同一 Idempotency-Key 重放原结果,不再调用 LLM 或推进版本 |
|
||||
|
||||
set/get/delete 与 chat 使用同一个会话锁。字段写入带版本条件,过时 turn 或旧 epoch
|
||||
不能覆盖新值;即使 advisory-lock 连接丢失,版本与 epoch 校验仍拒绝迟到提交。
|
||||
数据库不可用时返回失败,未提交的流不会发 done。客户端已收到的流式正文不可撤回,
|
||||
必须把 done 当作本轮成功结束标记。
|
||||
|
||||
## 可选 Idempotency-Key
|
||||
|
||||
SQLite/PostgreSQL 模式的 `/chat` 接受请求头 `Idempotency-Key`(1—128 字符)。
|
||||
同一会话 epoch 下,相同 key 和相同 text/clientMode/needFormUpdate 重放已提交结果;
|
||||
相同 key 对应不同请求返回业务码 409,SSE 使用 error 事件。
|
||||
未提交请求可使用相同 key 重试;流式结果重放来自已提交结果,不会再次流式调用模型。
|
||||
|
||||
未提供 key 时,每次 POST 都是新轮次,不承诺网络重试自动去重。
|
||||
memory 模式收到此头返回 409,不假装提供持久化幂等能力。FastGPT 路径不提供本模块的幂等保证。
|
||||
|
||||
## 删除与数据库切换
|
||||
|
||||
删除先清理图 checkpoint,再删除业务数据与轮次日志。真实故障返回失败、允许重试。
|
||||
数据库保留不含业务字段/历史的 epoch 与版本墓碑,用于拒绝旧任务提交;重复删除成功。
|
||||
删除后新聊天进入新的 epoch,不会重放旧的幂等结果。
|
||||
|
||||
把 `AGENT_STORAGE=sqlite` 改为 `postgresql` 并重启,仅改变数据源。
|
||||
原 SQLite 文件仍保留,PostgreSQL 不会自动出现其中的会话。本版不提供自动跨数据库迁移。
|
||||
切换生产数据源需要单独的数据迁移与验证步骤。备份 SQLite 时先停止服务或使用 SQLite
|
||||
备份机制;PostgreSQL 使用数据库备份工具。本地 compose 的 `down` 保留数据卷。
|
||||
|
||||
## 验证与测试资源
|
||||
|
||||
```bash
|
||||
conda run -n py3.11 python -m pytest test/storage/ -q
|
||||
```
|
||||
|
||||
默认跑真实 SQLite;PostgreSQL 测试需要 `AGENT_TEST_POSTGRES_DSN` 指向隔离测试库,
|
||||
没有该变量时跳过 PostgreSQL 用例,不把跳过当作 PostgreSQL 已验证。
|
||||
测试使用 fake LLM,不消耗模型额度,覆盖:重启续聊、四接口、两种数据类型、
|
||||
幂等重放、版本冲突、checkpoint 缺失/超前、提交前后进程直接退出、数据库锁和删除恢复。
|
||||
@@ -357,7 +357,7 @@ docs/langgraph/
|
||||
| FastGPT 应用层配置(应用级系统提示词、思考模式开关)与线上部署差异 | 作业 4/5 验收对账前 | 移植代码、离线评测与差异记录 |
|
||||
| 评测案例与工作流导出的分歧(“对”分流语义、2004 走向)的权威口径 | 作业 5 验收前 | 白名单双走向、逐字比对测试、失败证据留档 |
|
||||
|
||||
## 当前进度与已知差异(2026-10-21 更新)
|
||||
## 当前进度与已知差异(数据库实现更新,模型评测为历史记录)
|
||||
|
||||
### 作业进度
|
||||
|
||||
@@ -368,15 +368,20 @@ docs/langgraph/
|
||||
| 3. 领域状态与事件 | ✅ 完成 | 事件解析 14 例、白名单校验 24 例;`LEGAL_TRANSITIONS` 17 条均带 `source_ref` |
|
||||
| 4. 采集 + LLM | ✅ 完成 | 真实模型 `collection_skip_known_info` 12/12;单测含逐字比对 |
|
||||
| 5. 拍照与核实 | ✅ 完成 | 真实模型 4/4 + 12/12 + 2/2(20261021 基准) |
|
||||
| 6—10 | 未开始 | — |
|
||||
| 6. 四接口业务状态 | 已实现 | 应用服务、异步 repository、字段别名、会话串行与接口回归 |
|
||||
| 7. 持久化 | SQLite/PostgreSQL 初版已实现 | `src/storage/`;真实两库集成、进程退出恢复、幂等和版本冲突;见 [agent-storage.md](agent-storage.md) |
|
||||
| 8. 真流式 | 已实现核心路径 | 模型增量、前缀校验、取消与提交回调;流式终止事件回归 |
|
||||
| 9—10 | 未完成 | 生产访问边界、容量验证与双后端等价性验收继续保留 |
|
||||
|
||||
全量单元测试 143 通过;七份提示词与 20261020 工作流逐字比对由参数化测试锁定。
|
||||
本轮全量测试 315 通过,包含真实 SQLite/PostgreSQL 集成测试。提示词来源和
|
||||
逐字比对见 `test/agent/test_workflow_prompts.py`。下方真实模型结果是此前轮次,
|
||||
本次数据库开发未重新运行 46 条真实模型黄金评测。
|
||||
|
||||
### 已落地的架构决策
|
||||
|
||||
1. **LLM 提议、代码校验(propose-validate)**:所有 stage_code 由节点内 LLM 产出(提示词即程序),代码只做前缀解析、白名单校验(含 clientMode 出口)和落库;曾尝试的代码强制推进/短促词守卫已按此分工移除。
|
||||
2. **依赖注入走 `context_schema`**:`GraphDeps(llm=...)` 经 `Runtime` 注入调用期,不进 state/checkpoint;`build_graph()` 与 LLM 解耦。
|
||||
3. **与工作流逐字对齐**:提示词原文渲染(含 `{{$变量}}` 替换:系统时间按 Asia/Shanghai、人员预填字段缺失时显式标注“未提供”);聊天历史按 user/assistant 消息传递(上限 50 条),对齐 chatNode `history=50`;welcome 话术为 Assigned Reply#3 原文并计入对话历史。
|
||||
3. **与工作流逐字对齐**:提示词原文渲染(含 `{{$变量}}` 替换:系统时间按 Asia/Shanghai、人员预填字段缺失时显式标注“未提供”);聊天历史按 user/assistant 消息传递(`AGENT_HISTORY_MESSAGES` 默认 50,0 为不截断);welcome 话术为 Assigned Reply#3 原文并计入对话历史。
|
||||
4. **不引入 LangGraph Server / langgraph_sdk**:生产形态的 Agent Server 需要 LangSmith license key、外网 beacon 出网及 Postgres+Redis 全栈,与课程目标(作业 7 亲手实现持久化)和部署环境不符;`langgraph dev` 仅为内存态,无增量能力。自部署高并发方案 = 嵌入库模式:无状态 FastAPI 多副本 + AsyncPostgresSaver + 每会话 advisory lock 串行 + 连接池预算(社区参照:langgraph-agent-toolkit、agent-bakery)。
|
||||
5. **2004 语义**:确认单车车牌;车牌已明确时可 2003→2005 跳过,未明确时 2003→2004→2005。两种走向均在白名单登记,由模型判断。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user