Add to packages/storage/: - README.md and README_zh.md - comprehensive documentation - store/persistence/async_provider.py - async persistence provider Update repositories: - contracts/thread_meta.py - add new contract method - db/thread_meta.py - implement new method - factory.py - update factory logic Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
18 KiB
deerflow-storage 设计说明
本文说明 backend/packages/storage 的当前职责、总体设计、数据库接入方式、模型定义方式、数据库访问接口,以及它在 app 层中的使用路径。
1. 包的定位
deerflow-storage 是 DeerFlow 的统一持久化基础包,目标是把“数据库接入”和“业务对象持久化”从 app 层拆出来,形成一个独立、可复用的存储层。
它当前主要承担两类能力:
- 为 LangGraph 运行时提供 checkpointer。
- 为 DeerFlow 应用数据提供 ORM 模型、仓储协议和数据库实现。
这个包本身不直接提供 HTTP 接口,不直接依赖 FastAPI 路由,也不承担业务编排。它更接近一个“存储内核”。
2. 总体分层
当前代码大致分成下面几层:
config
└─ 读取配置、解析环境变量、确定数据库参数
persistence
└─ 创建 AsyncEngine / SessionFactory / LangGraph checkpointer
repositories/contracts
└─ 定义领域对象和仓储协议(Pydantic + Protocol)
repositories/models
└─ 定义 SQLAlchemy ORM 表模型
repositories/db
└─ 基于 AsyncSession 的数据库实现
app.infra.storage
└─ 把 storage 仓储适配成 app 层直接可用的接口
gateway / runtime
└─ 通过依赖注入、facade、observer、event store 使用 infra
核心思想是:
storage包只负责“如何存”和“存什么”。app.infra负责把底层仓储转换为应用层语义。gateway/runtime只依赖infra暴露出来的接口,不直接碰 ORM 和 SQL。
3. 数据库如何接入
3.1 配置入口
数据库配置由 store/config/storage_config.py 定义,外层应用配置由 store/config/app_config.py 负责读取。
配置来源有几个特点:
- 默认从
backend/config.yaml或仓库根config.yaml读取。 - 支持
DEER_FLOW_CONFIG_PATH指定配置文件。 - 支持在配置中使用
$ENV_VAR形式引用环境变量。 - 时区配置也会影响存储层时间字段的处理。
3.2 persistence 入口
存储层统一入口是 store/persistence/factory.py 里的 create_persistence()。
它会做三件事:
- 根据
StorageConfig生成 SQLAlchemy URL。 - 根据 driver 选择 SQLite / MySQL / PostgreSQL 的构建函数。
- 返回
AppPersistence,其中包含:checkpointerenginesession_factorysetupaclose
也就是说,应用启动时拿到的不是单一数据库连接,而是一整套“运行期持久化能力包”。
3.3 各数据库驱动的接入方式
驱动实现位于:
store/persistence/drivers/sqlite.pystore/persistence/drivers/mysql.pystore/persistence/drivers/postgres.py
三者的共同模式一致:
- 创建
AsyncEngine - 创建
async_sessionmaker - 创建 LangGraph 对应的异步 checkpointer
- 在
setup()中先执行 checkpointer 初始化,再执行MappedBase.metadata.create_all - 在
aclose()中按顺序关闭 engine 和 checkpointer
这说明当前包的初始化策略是:
- checkpointer 表和业务表一起由运行时启动时初始化。
- 业务表当前依赖
SQLAlchemy create_all()自动建表。 - 当前包内没有独立的 migration 编排入口作为主路径。
3.4 SQLite 的当前行为
SQLite 使用 StorageConfig.sqlite_storage_path 生成数据库文件路径,默认落到 .deer-flow/data/deerflow.db。
对于 SQLite,当前模型主键会退化为 Integer PRIMARY KEY,这是因为 SQLite 的自增主键对 BIGINT 支持不如 INTEGER PRIMARY KEY 直接。
4. 持久化模型如何定义
4.1 基础模型约定
基础定义位于 store/persistence/base_model.py。
这里统一了几件事:
MappedBase作为所有 ORM 模型的声明基类。DataClassBase让模型天然支持 dataclass 风格。Base额外带上created_time/updated_time。id_key统一主键定义。UniversalText统一长文本类型,兼容 MySQL 和其他方言。TimeZone统一时区感知的时间字段转换。
因此,包内新模型通常遵循这样的模式:
- 如果需要
created_time/updated_time,继承Base。 - 如果只要 dataclass 风格、不要
updated_time,继承DataClassBase。 - 主键统一使用
id: Mapped[id_key]。
4.2 当前已定义的业务模型
模型位于 store/repositories/models:
Run- 表:
runs - 用于保存运行元数据、状态、token 统计、消息摘要、错误信息等。
- 表:
ThreadMeta- 表:
thread_meta - 用于保存线程级元数据、状态、标题、所属用户等。
- 表:
RunEvent- 表:
run_events - 用于保存 run 产生的事件流和消息流。
- 通过
(thread_id, seq)唯一约束维护线程内事件顺序。
- 表:
Feedback- 表:
feedback - 用于保存对 run 的反馈记录。
- 表:
4.3 模型字段设计特点
当前模型有几个统一约定:
- 业务主标识使用字符串字段,如
run_id、thread_id、feedback_id,数据库自增id仅作为内部主键。 - 结构化扩展信息一般放在
metadataJSON 字段中,ORM 内部属性名通常映射为meta。 - 长文本内容统一用
UniversalText。 - 时间字段统一走
TimeZone,避免不同时区下行为不一致。
RunEvent.content 还有一个额外约定:
- 落库时如果
content是dict,会先序列化成 JSON 字符串。 - 同时在
metadata中写入content_is_dict=True。 - 读出时再按标记反序列化。
这让 run_events 同时兼容“纯文本消息”和“结构化事件内容”。
5. 数据库访问接口如何定义
5.1 contracts:仓储协议层
协议定义在 store/repositories/contracts。
这一层做了两件事:
- 用 Pydantic 模型定义输入对象和输出对象,例如
RunCreate、Run、ThreadMetaCreate、ThreadMeta。 - 用
Protocol定义仓储接口,例如RunRepositoryProtocol、ThreadMetaRepositoryProtocol。
这意味着上层依赖的是“协议”和“数据契约”,而不是某个具体 SQLAlchemy 实现。
5.2 db:数据库实现层
实现位于 store/repositories/db。
每个仓储实现都遵循同样的模式:
- 构造函数接收
AsyncSession - 用
select / update / delete执行数据库操作 - 把 ORM 模型转换成 contracts 层的 Pydantic 对象返回
例如:
DbRunRepository负责runs表的增删改查和完结统计更新。DbThreadMetaRepository负责线程元数据的查询、更新和搜索。DbRunEventRepository负责事件批量追加、消息分页、按线程或按 run 删除。DbFeedbackRepository负责反馈创建、查询、聚合前置数据读取。
5.3 factory:仓储构造入口
store/repositories/factory.py 提供了统一工厂函数:
build_run_repository(session)build_thread_meta_repository(session)build_feedback_repository(session)build_run_event_repository(session)
这样上层只需要拿到 AsyncSession,就可以构造对应仓储,而不需要直接依赖具体类名。
6. 对外接口是什么
如果只看 storage 包本身,它对外暴露的是两类接口。
6.1 持久化运行时入口
由 store/persistence/__init__.py 暴露:
create_persistence()AppPersistence- ORM 基础模型相关基类与类型
这是应用初始化数据库和 checkpointer 的入口。
6.2 仓储接口与工厂
由 store/repositories/__init__.py 暴露:
- contracts 层的输入输出模型
- 各仓储
Protocol - 各仓储 builder 工厂函数
这是应用按“仓储协议”接入业务持久化的入口。
换句话说,storage 包不会直接给 app 层一个 HTTP SDK,而是给它:
- 初始化能力
- session factory
- repository protocol + repository builder
7. app 层如何调用:通过 infra 接入
app 层没有直接操作 store.repositories.db.*,而是通过 app.infra.storage 做一层适配。
相关代码在:
backend/app/infra/storage/runs.pybackend/app/infra/storage/thread_meta.pybackend/app/infra/storage/run_events.py
7.1 为什么要有 infra 这一层
因为 app 层需要的不是“裸仓储接口”,而是“符合应用语义的持久化服务”:
- 自动开关 session
- 自动 commit / rollback
- 补充 actor / user 可见性控制
- 把底层 Pydantic 模型转换成 app 需要的字典结构
- 对齐运行时 facade、observer、router 的接口习惯
7.2 Run 的接入方式
RunStoreAdapter 用 session_factory 包装了 build_run_repository(session),向上暴露:
getlist_by_threadcreateupdate_statusset_errorupdate_run_completiondelete
这里的关键点是:
- 每次调用都会创建独立的
AsyncSession。 - 读操作和写操作分开管理事务。
- 会结合
actor_context做user_id维度的可见性过滤。 - 创建 run 时会先把 metadata / kwargs 做序列化,确保可安全落库。
7.3 Thread 的接入方式
线程元数据分成两层:
ThreadMetaStoreAdapter- 是 repository 级别的 session 包装器。
ThreadMetaStorage- 是面向 app 的更高层接口。
ThreadMetaStorage 额外提供了应用语义方法:
ensure_threadensure_thread_runningsync_thread_titlesync_thread_assistant_idsync_thread_statussync_thread_metadatasearch_threads
也就是说,app 层通常依赖 ThreadMetaStorage,而不是直接依赖底层仓储协议。
7.4 RunEvent 的接入方式
AppRunEventStore 是运行时事件存储适配器。它不是简单 CRUD 包装,而是面向运行时协议设计的:
put_batchlist_messageslist_eventslist_messages_by_runcount_messagesdelete_by_threaddelete_by_run
它额外做了线程可见性校验:如果 actor 有 user_id,则会先查询线程归属,再决定是否允许读写该线程事件。
8. app 启动时怎么装配 storage
应用启动装配发生在 backend/app/gateway/registrar.py。
init_persistence() 的流程是:
- 调用
create_persistence() - 执行
app_persistence.setup() - 用
session_factory构造:RunStoreAdapterThreadMetaStoreAdapterFeedbackStoreAdapterAppRunEventStore
- 再进一步构造
ThreadMetaStorage - 把这些对象注入到
app.state
因此对 app 而言,storage 并不是按“全局单例 repository”接入的,而是:
- 底层共享一个
session_factory - 上层通过适配器按调用粒度创建 session
- 最终挂在
FastAPI app.state中给路由和服务使用
9. gateway / service 如何使用这些能力
9.1 依赖注入
backend/app/gateway/dependencies/repositories.py 从 request.app.state 取出:
run_storethread_meta_repothread_meta_storagefeedback_repo
然后作为 FastAPI 依赖注入给路由层。
9.2 在线程路由中的调用
在线程路由 backend/app/gateway/routers/langgraph/threads.py 中:
- 创建线程时调用
ThreadMetaStorage.ensure_thread() - 查询线程时调用
ThreadMetaStorage.search_threads() - 删除线程时调用
ThreadMetaStorage.delete_thread()
因此线程 API 并不直接碰 ORM 表,而是通过 infra 层完成。
9.3 在 runs facade 中的调用
backend/app/gateway/services/runs/facade_factory.py 会把 storage 相关对象注入到 RunsFacade:
run_read_reporun_write_reporun_delete_repothread_meta_storagerun_event_store
然后再由:
AppRunCreateStoreAppRunQueryStoreAppRunDeleteStoreStorageRunObserver
这些 app 层组件去消费。
9.4 Run 生命周期如何回写数据库
StorageRunObserver 是一条很关键的链路。
它监听运行时生命周期事件,并把结果回写到持久层:
RUN_STARTED-> 更新 run 状态为runningRUN_COMPLETED-> 更新 run 完结统计;必要时同步 thread titleRUN_FAILED-> 更新 run 错误状态和错误信息RUN_CANCELLED-> 更新 run 为interruptedTHREAD_STATUS_UPDATED-> 同步 thread status
这说明 storage 包不负责主动监听运行时事件,但 app.infra.storage 已经把它接到了运行时 observer 体系中。
10. 如何与外部通信
这里的“外部通信”可以分成两类。
10.1 与数据库通信
storage 包通过 SQLAlchemy async engine 与 SQLite / MySQL / PostgreSQL 通信。
通信入口是:
create_async_engineAsyncSession- repository 中的
select / update / delete
LangGraph checkpointer 也通过各自的异步 saver 与数据库通信,但这部分被统一收敛在 persistence/drivers 中。
10.2 与应用外部接口通信
storage 包本身不直接暴露外部 API;对外通信由 app 层完成。
当前典型路径是:
- HTTP 请求进入 FastAPI 路由
- 路由从依赖注入中拿到
infra适配器 infra调用storage仓储- 数据结果再由路由转换为 API 响应
另外一条链路是运行时事件:
- runtime 产生 run lifecycle event 或 run event
- observer / event store 调用
infra infra调用storage- 数据写入数据库
所以更准确地说,storage 的“对外通信方式”不是自己发请求,而是作为应用内部的数据库边界,被 HTTP 层和 runtime 层共同消费。
11. 当前包的设计理念
从现有代码看,这个包的设计理念比较明确:
- 统一 checkpointer 和应用数据存储入口。
- 以 repository contract 隔离上层业务与底层 ORM。
- 用
infra适配层隔离 app 语义与 storage 语义。 - 优先采用异步 SQLAlchemy,适配现代 async 应用栈。
- 保持数据库方言兼容性,把差异尽量收敛在基础类型和 driver 构造层。
- 把 actor / user 可见性控制放在 app infra,而不是硬编码进底层 ORM 模型。
这意味着它不是一个“全功能业务数据层”,而是一个“可装配的底层持久化能力包”。
12. 作用范围与边界
当前 storage 包负责的范围:
- 数据库连接参数和初始化
- LangGraph checkpointer 接入
- ORM 基础模型约定
- DeerFlow 核心持久化模型
- 仓储协议与数据库实现
当前不负责的范围:
- FastAPI 路由协议
- 认证鉴权
- 业务工作流编排
- actor 上下文绑定
- SSE / stream bridge 的网络层通信
- 更高层的 facade 业务语义
这些职责分别落在 app.gateway、app.plugins.auth、deerflow.runtime 和 app.infra 中。
13. 一条完整调用链示例
以“创建 run 并在运行结束后更新状态”为例,链路如下:
HTTP POST /api/threads/{thread_id}/runs
-> gateway router
-> RunsFacade
-> AppRunCreateStore
-> RunStoreAdapter
-> build_run_repository(session)
-> DbRunRepository
-> runs 表
运行完成
-> runtime 产生 lifecycle event
-> StorageRunObserver
-> RunStoreAdapter / ThreadMetaStorage
-> DbRunRepository / DbThreadMetaRepository
-> runs / thread_meta 表
以“查询 run 的消息”为例:
HTTP GET /api/threads/{thread_id}/runs/{run_id}/messages
-> gateway router
-> 从 app.state 获取 run_event_store
-> AppRunEventStore
-> build_run_event_repository(session)
-> DbRunEventRepository
-> run_events 表
14. 总结
backend/packages/storage 当前在整个工程中的角色,可以概括为一句话:
它是 DeerFlow 的数据库和持久化能力底座,统一封装了数据库接入、ORM 模型、仓储协议、数据库实现以及 LangGraph checkpointer 接入;而 app 层通过 infra 适配器把这些底层能力转化成线程、运行、事件、反馈等上层语义,再通过 HTTP 路由和运行时事件系统对外提供服务。