sync update from LAN repo (source master @ ca48c4ec)

This commit is contained in:
gozeros committed 2026-11-05 10:53:54 +08:00
1 parent 8bc8b0e082
commit ea9dcff9c0
873 files changed
+107866 -14011

No files matched your search

@@ -0,0 +1,94 @@
# b5-1 任务分析深链接接入
`goPath=b5-1` 是「任务分析」单智能体问答入口。接入方先把任务详情写入 DeerFlow,再让用户登录并跳转;页面不会再调用接入方的任务详情接口。
## 前置配置
在「业务链条管理」页面新增的 **任务分析**(业务码 `b5-1`)行中选择要使用的默认单智能体。深链接也可以用 `?agentId=` 直接指定本次问答使用的智能体(优先于配置行);两者都没有时,深链接不会进入问答页。
服务端无需为接入配置任何密钥。
## 写入或更新任务详情
```http
PUT /api/task-deeplink-details/b5-1/{id}
Content-Type: application/json
```
该接口为**开放写入**:不需要登录态,也不需要任何鉴权头,接入方服务端直接调用即可。写入者仅作审计记录。
```json
{
"taskName": "重点活动舆情研判",
"taskContent": "围绕活动期间的舆情风险、重点对象和处置建议开展分析。",
"overview": "活动前后七天的综合研判任务",
"type": "yuqing",
"extra": {
"sourceSystem": "B5",
"priority": "high",
"regionCodes": ["110000", "120000"],
"customField": {
"owner": "张三"
}
}
}
```
- `id` 与深链接中的 `taskId` 必须完全一致;存储键为 `(type, id, 业务类型)`,此处路径 `type` 固定为 `b5-1`。
- `taskName`、`taskContent` 必填;`overview`、`type`、`extra` 可选。
- `type` 是业务类型(≤64 字符),与深链接 `?type=` 对应:**同一个 id 可以存多个不同类型的详情快照**,各自独立版本号;不传 `type` 时写入无类型分区(与深链不传 `?type=` 的读取口径一致)。
- `extra` 必须是 JSON 对象,最大 64 KiB;用于接入方的扩展字段。
- 这是幂等接口,接入方不需要预先查询「是否已存」。每次直接 `PUT` 完整快照即可:
- 首次写入:创建记录;
- 任务名称、详情、简介或 `extra` 有任一变化:整体更新并递增 `version`;
- 内容完全相同:只按主键读取并返回现有记录,不更新数据库、不递增 `version` 或 `updatedAt`。
成功响应示例:
```json
{
"data": {
"type": "b5-1",
"id": "task-20261020-001",
"taskType": "yuqing",
"taskName": "重点活动舆情研判",
"taskContent": "围绕活动期间的舆情风险、重点对象和处置建议开展分析。",
"overview": "活动前后七天的综合研判任务",
"extra": {
"sourceSystem": "B5",
"priority": "high"
},
"version": 1,
"updatedAt": "2026-10-20T10:00:00+08:00"
},
"created": true,
"changed": true
}
```
`created` 表示本次是否首次创建;`changed` 表示本次是否实际写入。对同样内容的重试,响应为 `created: false`、`changed: false`。响应中的 `taskType` 回显写入时的业务类型(注意与固定为 `"b5-1"` 的 `type` 字段区分)。
## 用户跳转
写入成功后,引导用户访问登录页(认证参数按既有接入方式附带):
```text
/#/login?taskId=task-20261020-001&goPath=b5-1
```
可选参数:
| 参数 | 说明 |
| --- | --- |
| `agentId` | 直接指定本次问答使用的智能体 id,优先于「业务链条管理」b5-1 行的配置。 |
| `type` | 业务类型(≤64 字符,超长截断)。**详情快照与聊天记录都按 `(taskId, type)` 独立分区**:同一任务传入不同 `type` 时读取各自的详情、维护各自的对话历史,互不可见。 |
| `theme` | 页面主题色。**不传时默认深蓝色**;传入则按传入值解析明暗(`dark`/`dark-blue`/`深色`/`深蓝` → 深色,其余如 `lightblue` → 浅色)。主题只在任务分析页面与本次深链会话内生效,不影响用户在系统其它页面的外观偏好。 |
示例:
```text
/#/login?taskId=task-20261020-001&goPath=b5-1&agentId=analyst-01&type=yuqing
```
登录后前端按 `(taskId, type)` 读取本系统详情接口(对应 `GET /api/task-deeplink-details/b5-1/{id}?type=…`,需登录态),将任务名称和任务详情展示在左上角,标题固定为「任务分析」;`taskName`、`taskContent`、`overview` 与完整 `extra` 会作为每轮问答的隐藏任务上下文提供给智能体。聊天记录同样按 `(taskId, type)` 独立保存,不会混入其它任务深链、其它类型或通用聊天;未传 `type` 时归入该任务的无类型分区(与历史链接的行为一致)。
@@ -0,0 +1,36 @@
# PostgreSQL 聊天记录月度归档
## 数据范围与月份
页面月份来自业务数据库 `threads_meta.created_at`,按北京时间的**会话创建月份**归档。一次归档会搬迁该月会话的全部 LangGraph checkpoints、checkpoint blobs 和 pending writes;跨月继续的会话仍完整放在创建月份的 SQLite 文件中。系统线程和当前状态为运行中的会话会跳过。
聊天列表元数据仍留在原业务数据库(MySQL 配置不变)。LangGraph Store 中的用户记忆、智能体数据和其他应用表不属于聊天 checkpoint,不会被清理。
## 固定目录与现有 SQLite 配置
默认目录:`$DEER_FLOW_HOME/data/chat-archives/`;未设置 `DEER_FLOW_HOME` 时为项目目录下 `.deer-flow/data/chat-archives/`。可用 `DEER_FLOW_CHAT_ARCHIVE_DIR` 指定其他绝对路径。目录内容:
- `chat-YYYY-MM.db`:每个归档月份一个 LangGraph SQLite checkpoint 数据库。
- `catalog.db`:月份任务、会话到月份的映射、复制/删除/回滚状态。
- `.locks/`:Worker 与主服务共享的会话级跨进程读写锁。
归档 SQLite 是叠加的历史层,不会修改 `checkpointer.connection_string`、`sqlite_connection_string` 或旧的 `dual_read_sqlite_paths`。PG 仍是写入目标;查询某个 thread 时先读 PG,缺少 checkpoint 才按 `catalog.db` 找到相应月份 SQLite。历史会话页面和 LangGraph `history/state` 接口使用同一套回退逻辑。首次管理员启动归档任务后才创建索引;此前聊天写入不创建锁文件、不额外写归档数据。仅打开页面只查 `threads_meta` 月份汇总,不扫描 PG 检查点表,也不创建归档目录或索引。
如果启用 Worker 池,归档目录必须位于主服务和 Worker 共同挂载的**可写持久卷**。默认目录落在 `$DEER_FLOW_HOME/data` 下,标准 Docker 部署的 data volume 会共享该路径;自定义路径需要放入相同的共享卷。PostgreSQL Worker 检查会在卷未共享时拒绝启动,避免只在主容器可见的归档导致历史查询丢失。
## 操作流程
归档、清理、回滚通过 SQLite 原子任务认领,保证全局同一时间只运行一个月份任务,避免多个管理请求同时压满 PostgreSQL 和磁盘 I/O。
1. 管理员打开「设置 → 聊天记录归档」。页面按月聚合轻量会话元数据,不会打开页面就扫描 PostgreSQL 的 80GB checkpoint 表,也不会创建归档状态文件;同时显示归档卷可用空间。
2. 选择已经结束的月份执行「归档到 SQLite」。任务后台逐会话复制并核对 checkpoint id;进度写在固定目录,关页或服务重启后可重新发起继续处理。
3. 确认状态为「归档已校验」后,才可点击「删除已归档 PG 数据」。删除任务会在每个会话上取得共享卷锁,等待正在进行的 checkpoint 写入结束,再次复制并验证 SQLite,最后删除 PostgreSQL 中该 thread 的 checkpoints、blobs 和 writes。
4. 页面「回滚到 PostgreSQL」会把该月 SQLite checkpoint 重新写回 PostgreSQL 并校验;SQLite 副本会保留,历史读取可继续回退。
归档、清理、回滚均按会话运行,并限制并发到单个月任务。目标卷会保留至少 2 GiB 空间;低于阈值时任务暂停,复制校验失败时会保留该会话的 PostgreSQL 数据。清理后 PostgreSQL 可以复用空闲页,但普通 `DELETE` 不保证操作系统看到的数据库文件立即变小;系统不会自动执行可能带来明显 I/O 和锁表风险的 `VACUUM FULL`。
## 部署与验证
更新后台 `app/`、`packages/`、`tests/` 和前端 dist 后重启主服务。数据库不需要新增 PostgreSQL migration;首次任务会创建本地 SQLite 月库和归档目录。部署前确保该卷有足够空间容纳选中月份的副本,并将 `DEER_FLOW_HOME/data` 纳入备份。
回归测试覆盖归档复制、checkpoint 校验、PG 删除后的透明 SQLite 回退、回滚,以及 Worker 共享归档卷校验。线上执行前先在单个月份做归档和历史读取验收;归档按钮本身不会删 PG,删除操作须管理员单独点击。
@@ -82,6 +82,14 @@ python scripts/setup_force_situation_chain.py
python scripts/setup_biz_detail_assistant.py
```
`task-context` 与 `task-detail-update` 读取 `TASK_DETAIL_AGENT_USERNAME` 和
`TASK_DETAIL_AGENT_PASSWORD`,通过本 Gateway 的 `/api/v1/auth/login/local` 领取短期 JWT 后再调用
任务详情接口。因此部署时必须在**技能沙箱运行环境**中注入这两个变量和
`TASK_DETAIL_GATEWAY_URL`,并启用 `password_login_enabled`;不得把密码写入技能文件、SOUL、
命令行或检索输入。`setup_biz_detail_assistant.py` 同样改用账号密码登录,并可通过
`DEERFLOW_SETUP_BASE`、`DEERFLOW_SETUP_USERNAME`、`DEERFLOW_SETUP_PASSWORD` 覆盖本地默认值;
脚本只在正式密码登录被策略关闭(403)时回退旧用户名登录,其他失败均停止,不会绕过凭据校验。
业务链安装脚本默认只更新配置,不立即触发真实接口。需要立即执行时显式追加
`--trigger`。链路阶段为:接口数据采集 → 当前判断威胁接口采集 → 情报摘要与校验。
@@ -0,0 +1,164 @@
# 线上多人互相拖慢:源码排查与隔离复现
排查日期:2026-10-22。范围:当前业务源码、部署脚本和无网络的隔离探针。
未连接线上主机、模型或数据库,以下数值不是线上时延或容量结论。原始排查完成后,已按
本次授权落实下述源码优化;尚未部署到线上,未修改线上配置或数据。
## 已落实的优化
1. **同步 IO 隔离**:产物递归扫描、技能 ZIP 预览、内容探测和文本写入使用独立有界
executor(4 工作者、最多 8 个已提交作业),与登录密码校验的默认线程池隔离。
JSONL/本地事件 spool 使用另一组 2 工作者、4 个已提交作业;取消调用方不会提前归还
尚在执行的磁盘作业名额。权限检查仍在扫描/读写之前执行。
2. **完成 run 内存回收**:实际 task 完成后默认保留 300 秒,每 10 秒最多核对 200 项。
只有终态、错误和最终 metadata/kwargs 已在 RunStore 核对成功才移除实时记录;有未完成
task、存储失败或快照不一致则保留。不会删 SQL run 行、checkpoint、聊天记录或 SSE
缓冲。共享用户仍通过已授权的线程查询持久化 run;所有跨线程 run id 继续校验。
旧 run 即使比新 run 更晚收尾,也不能因新 run 已回收而重新覆盖线程状态。
3. **事件转存磁盘**:原默认 `memory` 后端工厂改用本地 SQLite spool,在
`runtime_home()/run-events/events.sqlite3` 保存完整内容,以事务分配递增 seq 并批量提交,
查询按页读取,不在内存永久堆积完成事件。没有自动 TTL 删除或消息截断;独立
`MemoryRunEventStore` 类保留供测试使用。显式 `db` 仍用业务 SQL/MySQL,`jsonl` 保留旧
目录/文件格式;没有迁移或删除这些历史存储。spool 及 WAL 需放在可写持久化本地卷。
4. **公平调度**:后台 subagent 默认全局 3、每用户 2;后台会商默认全局 4、每用户 2。
按用户轮转,排队不占执行工作者;队列有全局和每用户上限,满时明确报错。
subagent 超时包含排队时长。取消/超时后的实际协程必须结束才释放名额,避免旧任务
尚在写 checkpoint 时启动过多新任务。身份 context 继续传入执行线程和事件循环。
5. **会商延迟 checkpoint**:前台/后台内部建席位、总控线程只写业务元数据;init 不再
通过 HTTP 给空线程追加席位消息。清单随真实首轮输入一起保存;旧客户端未传
seat_meta 时仍从 thread 映射构造真实席位 ID。后续广播和历史 checkpoint 保留。
配置见 README 的 `runtime_contention` 示例,均为可选字段,重启生效。原
`ROUNDTABLE_BACKGROUND_SLOTS_PER_USER` 环境变量继续覆盖每用户会商额度(不超过全局额度)。
这些额度按 Gateway 进程计算;当前实时 run/bridge 仍在进程内,不能据此直接增 worker。
测试覆盖:真实 HTTP/SQL 权限与共享访问、run 回收后的历史和终态重连、Last-Event-ID 回放、
旧 run 收尾隔离、慢扫描期间另一用户创建线程、并发事件序号/分页/重启完整性、批次回滚与
磁盘失败后的 journal 重试、A 用户积压时 B 用户准入、排队超时/取消、真实子任务取消确认、
后台会商取消确认、会商 init 无 checkpoint 依赖和连续两轮消息完整性。使用本地 SQLite、
内存 saver 与受控协程,无线上 PG/MySQL/模型实测;不据此承诺线上时延或并发容量。
最终验证(2026-10-22):后端 747 passed / 1 skipped;前端 `pnpm test:thread-history`
35 passed;35 个新增/改动 Python 文件 Ruff 全部通过,改动的 harness 模块未引入
`app.*` 依赖,`git diff --check` 通过。后端测试入口:
```text
packages/harness/tests/
tests/test_position_roundtable_sessions.py
tests/test_roundtable_role_boundary.py
tests/test_roundtable_model_fallback.py
tests/test_roundtable_seat_modes.py
tests/test_roundtable_gather_phase.py
tests/test_model_concurrency.py
tests/test_account_authentication.py
tests/test_artifact_preview.py
tests/test_artifact_library.py
```
## 结论
2026-10-23 已在最新 master 上整合:保留任务深链独立卡片存储、task/agent/action
投影索引、长历史 slim_display 和前端检索进度分轮隔离。整合后扩大回归到后端
780 passed / 1 skipped(额外覆盖任务深链接口、详情分区、一键清空与索引迁移),
前端历史/恢复 37 passed,改动 Python 文件 Ruff 与 `git diff --check` 均通过。
前端全量 typecheck 仍报告既有的 81 项错误,本次修改的 hooks/background-history/
api-client 文件无类型错误;该检查未宣称全量通过。
不能只归因于 PG 数据量。优化前还存在事件循环上的同步文件扫描、未回收的运行记录和
内存事件、跨用户共享的子智能体队列、业务连接池与线程池竞争等路径。
业务表使用 MySQL 只隔离了数据库来源,没有隔离 Gateway 的 CPU、内存、线程池与业务连接池。
单 worker 本身不意味着不能并发;这里的风险是慢同步工作在唯一事件循环上执行。
现有 RunManager/MemoryStreamBridge 的实时控制状态在进程内,直接增加 worker 又会导致
重连、取消和 join 请求找不到原进程中的运行,不能当作当前架构的安全扩容方案。
## 优化前已确认的代码路径
| 优先级 | 位置与现象 | 对其他人的影响 | 适用边界 |
| --- | --- | --- | --- |
| P0 | `routers/artifacts.py::list_artifacts` 在 async 路由内直接调用同步 `collect_outputs_listing`,递归扫描、resolve/stat、排序 | 扫描期间同一事件循环无法及时处理其他请求和 SSE | 同步调用已复现;线上时长取决于文件量、磁盘和挂载 |
| P0 | `runtime/runs/manager.py` 定义了 cleanup,但当前应用调用链没有调用;`_runs` 保留完成记录、kwargs 和 task 引用 | 内存随历史运行累积;创建/限流/列表仍遍历全部记录,在事件循环上消耗 CPU | 当前代码路径确定;线上累计数/RSS 未取得 |
| P0 | 默认 `run_events.backend=memory` 的 `MemoryRunEventStore` 仅 append,没有自动 TTL/总量预算 | 工具输出、消息和 trace 长期积累,增加内存压力及扫描/GC 成本 | 线上若已使用 db/jsonl,此项不按默认内存后端判断 |
| P0 | `subagents/executor.py` 全进程 `_scheduler_pool(max_workers=3)`,调度线程等待执行 future 完成才释放 | 一名用户占用 3 个后台子任务时,另一人的子任务排队;排队尚未进入执行 timeout | 普通后台 subagent 路径;不能推广为所有模型调用最多 3 路 |
| P1 | 登录与 run、权限、菜单、任务等仓储注入同一个业务 SQLAlchemy session factory | 慢查询/锁等待占满 MySQL 池后,登录和菜单也要等连接;移走 PG 不能避免 | 连接池共享确定,线上是否已耗尽要观测 checkout/慢查询 |
| P1 | 密码校验、文档转换和多个文件/扫描任务都用默认 `asyncio.to_thread` executor | 长转换/扫描占满工作线程时,另一个人的密码验证或工具 IO 排队 | YAML 中 100 是同一总池上限,不是每人 100;AnyIO 另有自己的池 |
| P1 | 当前安装的 PG saver 实例在 `_cursor` 取连接前持有实例锁;SQLite saver 同样串行 | 某条慢 checkpoint 查询/写入使其他会话状态读写排队 | 上一轮无 PG 网络测试已验证;线上必须核对实际 saver/wheel 版本 |
| P1 | 会商内部 `_create_thread_direct` 和后台 `_create_thread` 仍直接写 `empty_checkpoint` | 批量建席位仍争用 saver;上一轮普通 POST 的延迟初始化没有覆盖这两个入口 | 见 `routers/multi_agent.py` 与 `roundtable_inprocess_gateway.py` |
| P1 | worker 同步递归 serialize,`services.format_sse` 在事件循环中同步 JSON 编码 | 大聊天状态/工具输出形成 CPU 和带宽尖峰,同进程其他请求延迟 | 不等于每个 token 都重发全量历史,取决于实际 stream mode 与事件大小 |
| P1 | 模型总量限流默认关闭、每用户额度默认 0;后台会商只有每用户 2 槽的局部限制 | 多人/多席位同时压向同一模型服务,没有为前台保留容量,上游排队和重试会放大等待 | 线上管理员设置可能覆盖默认;每用户会商限制不是全站后台上限 |
RunManager 的锁内没有等待数据库 IO,不能把它描述成“持全局锁等待 MySQL”;其已确认问题是
完成记录没有接入回收,以及锁内同步全表扫描随累计运行数增长。
MemoryStreamBridge 每个 run 的事件条数有 256 默认上限,结束 60 秒后也会 cleanup,
不能与无限累计的 MemoryRunEventStore 混淆。但它没有按字节限制,包含大 values 的活动运行
仍可能占用大量内存;连接复用/replay 也会再次编码事件。
## 其他需要对照线上负载的放大因素
- MySQL metadata SQL 条件下推后仍可能没有适用索引。共享 taskId/agent_id/actionId 的 JSON
条件、标题 contains 和排序应检查真实 EXPLAIN;不能把“只发一条 SELECT”视为“查询恒快”。
- `services._maybe_sync_legacy_agent_records` 的 TTL 为 3600 秒。过期请求需要等待同一个
同步任务,期间按智能体目录读文件并逐个调用业务库;冷启动/整点附近的突刺应核对
`[start-run-timing] legacy-agent sync`。正常 TTL 内已有快路径,不能说每次发送都会全扫。
- `load_system_settings()` 没有读缓存,直接读 JSON 并校验;模型准入与多个中间件会调用它。
文件放在慢共享盘时会增加同步 IO;若文件小且本机缓存命中,通常不是首要瓶颈。
- 产物预览仅在启用 API 加载、没有命中 write_file 流式路径且运行中时每秒刷新一次,loader
获取完整响应文本。它不是每秒全目录列表扫描。大报告多窗口预览可能放大磁盘/网络/前端
内存压力;通用文件下载已用 FileResponse,不应误报为所有下载都在路由内整文件读取。
- 模型服务自身的并发槽、GPU 显存、长上下文、重试、知识检索/外部技能接口需要独立观测。
网关请求已经发出而首 token 很晚,不能自动归因于数据库。
- 当前仓库没有足以代表该线上入口的 Nginx/Ingress 配置,不能判断线上 buffering、连接数、
超时、HTTP 版本或负载均衡是否正确。开发启动器含 reload,正式启动器默认单 worker;
应核对线上实际启动命令,不能从源码脚本推断线上一定在热重载模式。
- 模板同时存在 SQLite 和 MySQL+PG 配置,其中一个 postgres 模板 `use_postgres=false`。
必须以线上配置路径、环境覆盖和启动日志确认实际后端,不能只看 type=postgres。
## 隔离探针证据
探针仅调用实际代码中的本地对象、临时目录和 SQLite 测试池,没有网络/模型调用。
机器、操作系统、文件缓存等都会影响绝对数值,仅用来验证串扰机制。
| 场景 | 观察 |
| --- | --- |
| 真实产物列表函数,临时目录 1,200 文件 | 路由耗时 1,640ms,同事件循环的无关 2ms timer 迟到约 1,638ms |
| 同一函数/目录,作为对照放入 to_thread(未修改生产代码) | 扫描约 1,344ms,timer 迟到约 14ms;工作没有消失,但释放了事件循环 |
| 实际 format_sse,构造约 6.27MiB values | 编码约 47ms,timer 迟到约 45ms |
| RunManager 含 0 / 10,000 / 50,000 个已完成记录 | 全部仍被保留;启用两种准入计数时,新 run 准入中位约 0.016 / 3.195 / 15.376ms |
| MemoryRunEventStore 写入 1,000 个唯一 16KiB 假 trace | 1,000 个全部保留;tracemalloc 约增加 16.13MiB,没有自动过期 |
| 真实 subagent execute_async + 受控 future | 4 个独立任务只启动 3 个,第 4 个保持 pending;释放 1 个后才启动第 4 个 |
| 共享 SQLAlchemy 池测试,2 个连接均被占用 | 无关第三个连接等待超时;使用 SQLite QueuePool 验证池行为,不是线上 MySQL 测量 |
本次机器可复跑入口及原始结果(未提交到 Git,不包含凭证或用户消息):
- 根目录 `.runtime/concurrency_audit_20261022.py`
- 根目录 `.runtime/concurrency_subagents_20261022.py`
- 根目录 `.runtime/concurrency_audit_20261022.json`
## 线上最小观测集合
用同一北京时间窗口关联以下指标,再确定哪个是此次线上事件的主因:
1. Gateway 进程数、代码版本、实际数据库/checkpointer 类型,CPU/RSS/可用内存/容器重启数。
2. 事件循环 lag 与登录、菜单、普通小查询、run 创建的 P50/P95/P99。
3. MySQL 活跃连接、连接池借用等待/超时、慢查询和锁等待;PG 另记录 checkpoint 的锁等待、
借连接时间、SQL 时间、响应体大小。两层池分开,不只看数据库 CPU。
4. 运行中的前台/后台/子智能体数量、每用户占用、排队时间、executor 的排队/执行时间。
5. 模型请求发送时间、首 token 时间、完整生成时间、429/超时/重试和同一模型服务总请求数。
6. SSE 帧大小/编码耗时、产物扫描文件数/耗时、索引/入库/会商后台作业与故障窗口的重合度。
若登录和菜单也慢,先查事件循环、RSS/CPU、共享 MySQL 池和密码线程池;若这些正常而只有
出字慢,优先区分模型上游、checkpoint 与子任务排队。服务运行越久越慢且重启缓解时,
优先核对完成 run/内存事件数量,但重启缓解本身不是泄漏的充分证据。
## 建议处理顺序
1. **先切断已确认的串扰**:把产物同步扫描和 ZIP 读取等重 IO 移离事件循环,并给扫描并发
单独预算;补完成 run 回收和事件存储/保留策略。回收前保证持久化查询、取消和断线恢复兼容,
不删除仍在运行的任务或未持久化的数据。
2. **统一准入和公平调度**:后台任务全站额度 + 每用户额度 + 可观测队列;子智能体名额可配、
公平取用,排队超时从入队计算;为前台保留资源。会商内部创建统一采用验证过的延迟初始化。
3. **隔离资源**:认证轻任务与文档转换/扫描分 executor,业务轻查询与后台重查询设独立预算,
保持 MySQL 总连接数有界;按实际 EXPLAIN 补索引,验证 saver 的并发实现。
4. **最后做容量扩展**:实时流传输和运行控制共享化后,再验证多 worker/多实例;结合真实
模型容量做分级负载测试。单纯扩大线程池、连接池或 worker 数,不能消除上述等待链。
@@ -4,15 +4,15 @@
>
> 适用范围:通用问答、普通智能体问答中的 WeKnora 普通知识库
>
> 更新日期:2026-10-18
> 更新日期:2026-10-21
>
> WeKnora 行为核查基线:v0.7.2,commit 3d5d8bfcdfeeea266b292b71cea616847af28d0f
## 1. 目标
系统在原有经典检索之外增加“知识库智能检索”模式。经典模式完整保留。新模式的全局开关默认关闭,只有管理员能在设置页开启;关闭时新对话默认经典检索,开启后新对话默认智能检索,经典检索位于候选位置。已有线程继续按自身 metadata 恢复模式,不被全局开关批量改写。
系统在原有经典检索之外增加“知识库智能检索”模式。经典模式完整保留并始终作为新对话的默认模式,模式选择器中经典检索位于智能检索上方。新模式的全局开关默认关闭,只有管理员能在设置页开启;开关开启只表示向用户开放智能检索选项,用户可在需要更全面梳理时主动切换。已有线程继续按自身 metadata 恢复模式,不被全局开关批量改写。
开关和本功能新增配置只保存在数据库中,不写 `system_settings.json`、`config.yaml` 或其他配置文件。智能模式在一次用户提问内执行模型与工具的多轮循环。
用户只需直接提问,不需要在问题中说明先检索什么、再检索什么。每个新问题先执行原始文档向量/混合检索取得直接相关的分片、标题和实体,再用这些线索执行 Wiki 正则检索。Wiki 有命中时,模型只选择与回答有关的页面深读;模型未选择时,中间件读取排序第一的页面作为最低证据基线。长 Wiki 页面会使用 `next_cursor` 连续读取到 `complete=true`。之后只有在 Wiki 暴露出首轮 RAG 没有覆盖且回答确实需要的线索时,才继续改写查询、做原始切片正则核验或读取文档上下文。证据足够就停止检索并回答;Wiki 与原始文档冲突时,以原始文档为准。
用户只需直接提问,不需要在问题中说明先检索什么、再检索什么。每个新问题先执行原始文档向量/混合检索取得直接相关的分片、标题和实体;只有问题需要目录结构、概念关系、时间线或扩展线索,且当前 WeKnora 支持原生 Wiki 正则接口时,才用这些线索执行 Wiki 正则检索。Wiki 有命中时,模型只选择与回答有关的页面深读;模型未选择时,中间件读取排序第一的页面作为最低证据基线。长 Wiki 页面会使用 `next_cursor` 连续读取到 `complete=true`。之后只有在 Wiki 暴露出首轮 RAG 没有覆盖且回答确实需要的线索时,才继续改写查询、做原始切片正则核验或读取文档上下文。证据足够就停止检索并回答;Wiki 与原始文档冲突时,以原始文档为准。
检索结果是回答的背景资料,不替代模型原有的理解、归纳、写作和推理能力。模型可以使用稳定通用知识解释术语与背景,但必须把库内事实、基于材料的推断和一般知识区分清楚,不能用模型记忆编造知识库专属事实。
最终回答不要求带 [1]、[2] 等编号。
@@ -27,7 +27,8 @@
| 同时需要框架与事实 | 先原始 RAG,再用 Wiki 扩展 | 用新的原始查询或切片正则核验 Wiki 暴露的关键事实 |
原始知识搜索沿用 WeKnora 的 knowledge-search 接口。 该接口由 WeKnora 执行向量或混合召回、融合和排序。 精确词、代码、编号和固定短语由切片正则检索补充。
Wiki 搜索采用大小写不敏感的安全 POSIX 风格正则表达式。 Wiki 搜索结果只作为页面入口。 模型需要调用页面读取工具获得正文,不能仅凭标题或摘要完成事实回答。
经典检索同时使用两层语义召回:WeKnora 原生 knowledge-search 检索上传原文的分片,DeerFlow 本地 Wiki 向量索引检索生成后的 Wiki 页面章节,并与本地 Wiki 关键词结果融合。两套向量覆盖的语料和职责不同,前者提供原始事实细节,后者提供整理后的主题结构与概念关联,不属于对同一份内容的重复编码。当前经典检索仍实际消费本地 Wiki 向量,因此保留 DeerFlow Wiki 向量索引;关闭它会降低经典模式的 Wiki 语义召回。
智能检索的 Wiki 搜索只调用 WeKnora 原生 `/api/v1/knowledgebase/{id}/wiki/search`,采用大小写不敏感的安全 POSIX 风格正则表达式。多个已选知识库在受控并发下各发起一次原生请求,不再枚举 Wiki 目录或逐页读取正文来模拟正则检索。若当前 WeKnora 没有该接口或返回 404/405,本轮静默跳过 Wiki 正则通道,并从后续模型调用中移除 Wiki 搜索工具,防止更换 pattern 后重复探测;系统继续使用原文分片和模型原有能力回答。 Wiki 搜索结果只作为页面入口。 模型需要调用页面读取工具获得正文,不能仅凭标题或摘要完成事实回答。
原始切片命中后,模型需要调用文档分块工具补齐相邻内容或后续分页。 Wiki 与原始文档冲突时,系统提示模型优先采用能定位到原始文档切片的证据,并说明差异。 首轮无结果时,系统提示模型改写查询一次。
仍有明确证据缺口时,系统提示模型选择能弥补该缺口的检索面。没有新增收益时应停止检索。中间件会在同一用户回合对原始查询、正则、Wiki pattern、`page_ref+cursor` 和文档分页做确定性去重;Wiki 的新 `next_cursor` 属于有效续读,不视为重复。上游核查入口:
@@ -46,7 +47,7 @@ Wiki 搜索采用大小写不敏感的安全 POSIX 风格正则表达式。 Wiki
- smart_reasoning
请求省略该字段或传空值时,后端按 classic 处理。 前端每次发送都显式携带当前模式。 线程 metadata 只用于恢复前端控件,不是后端本轮执行的隐式输入。
这保证旧客户端和缺少合法模式 metadata 的旧会话继续使用经典模式。新对话在设置读取完成后按全局开关选择默认模式:开关开启时为 smart_reasoning,关闭或读取失败时为 classic。管理员切换全局开关不会批量改变历史会话。
这保证旧客户端和缺少合法模式 metadata 的旧会话继续使用经典模式。新对话始终默认 classic;全局开关开启后只增加 smart_reasoning 可选项,关闭或读取失败时仅提供 classic。管理员切换全局开关不会批量改变历史会话。
管理员关闭开关也不会删除历史对话。 历史线程若保存为 smart_reasoning,关闭全局开关后必须切回 classic 才能继续发送。 模式不写入全局浏览器偏好。
新会话页面间的临时交接使用现有页面状态和 sessionStorage。
@@ -58,7 +59,7 @@ Wiki 搜索采用大小写不敏感的安全 POSIX 风格正则表达式。 Wiki
| 字段 | 含义 |
|---|---|
| scope | 设置作用域,当前为 global |
| smart_reasoning_enabled | 是否允许使用智能模式,并决定新对话的前端默认模式 |
| smart_reasoning_enabled | 是否向用户开放智能模式选项;不改变新对话默认的经典模式 |
| revision | 设置修订号 |
| updated_by | 最近修改人 |
| updated_at | 最近修改时间 |
@@ -252,14 +253,14 @@ Gateway 在模型启动前使用 HTTP 错误阻止无效请求。 主要错误
## 14. 经典模式兼容
classic 继续调用 build_llmwiki_rag_context。 它保留原有一次性预检索、上下文注入和 llmwiki_search 工具。 经典模式原有编号引用行为不因本功能改变。
旧客户端不传 mode 时自动进入 classic。全局开关控制 smart_reasoning 是否允许使用,并决定新对话的前端默认模式;关闭开关不会影响 classic。
classic 继续调用 build_llmwiki_rag_context。 它保留原有一次性预检索、上下文注入和 llmwiki_search 工具。其原始分片通道使用 WeKnora 自身的向量/混合检索,Wiki 通道使用 DeerFlow 本地 Wiki 向量与关键词融合;两类语料互补,当前均保留。经典模式原有编号引用行为不因本功能改变。
旧客户端不传 mode 时自动进入 classic。全局开关只控制 smart_reasoning 是否作为可选模式开放;无论开关状态,新对话都默认 classic,关闭开关不会影响 classic。
本功能也不复用前端已有的 multi_round_rag execution profile。 multi_round_rag 的外部调用语义与本功能的知识库范围、权限和会话执行不同。 knowledge_retrieval_mode 与模型选择、推理强度和 execution_profile 相互独立。
## 15. 前端实现
管理员入口位于“设置 → 知识检索”。 设置页展示当前数据库开关和说明。 通用问答与智能体问答输入框都支持“知识库智能检索”切换。
管理员关闭全局开关时,新会话初始化为 classic;管理员开启后,新会话默认使用 smart_reasoning,classic 保留为候选。已有线程始终从 metadata 恢复各自保存的模式。前端每次发送前执行以下校验:
新会话始终初始化为 classic,模式选择器按“经典检索、智能检索”的顺序展示;管理员开启全局开关后才显示并允许用户主动选择 smart_reasoning。已有线程始终从 metadata 恢复各自保存的模式。前端每次发送前执行以下校验:
- 全局开关已读取并开启
- 知识库选择已加载完成
@@ -372,7 +373,7 @@ Middleware 测试:
2. 非管理员不能修改全局开关。
3. 关闭开关时,伪造 smart_reasoning 请求被后端拒绝。
4. classic 在开关关闭和开启时都可正常使用。
5. 全局开关开启时新会话默认 smart_reasoning,关闭时默认 classic;历史线程始终按 metadata 恢复显示。
5. 新会话始终默认 classic 且经典检索位于模式选择器上方;全局开关开启后用户可主动切换 smart_reasoning,历史线程始终按 metadata 恢复显示。
6. smart_reasoning 请求必须显式携带模式。
7. 智能模式没有普通库、混选助手库或越权时不能启动模型。
8. 每个工具调用重新校验当前权限。
@@ -0,0 +1,56 @@
# 后端性能日志排查
此功能不需要安装 `py-spy`、`perf` 等工具。后台会为业务 `/api/` 请求打印一行简洁耗时(健康检查和 worker 池控制接口除外);超过 500 ms、返回 5xx 或抛出异常的请求会附带分阶段细节。每 5 秒最多报告一次超过 200 ms 的事件循环卡顿。日志不记录请求体、聊天内容、查询参数、SQL 原文、账号信息或密钥。
## 查看慢接口阶段耗时
更新并重启后台后,在宿主机执行:
```bash
docker logs --since 10m cmzs-deerflow-deerflow-backend-1 2>&1 | grep 'PERF'
```
慢请求会有一行 `PERF http_request` 日志,主要字段:
- `total_ms`:完整请求耗时。
- `headers_ms`:服务开始返回响应头前的耗时;大通常说明服务端处理或等待较慢。
- `endpoint_ms`:接口函数本身的耗时。
- `outside_handler_ms`:接口函数以外的耗时,包括鉴权、中间件、依赖解析和响应序列化。
- `stages`:接口内部分阶段的耗时,单位为毫秒。
- `event_loop_lag_ms`:最近测得的主事件循环延迟。
- `node`、`worker_access_ms`、`worker_upstream_headers_ms`、`worker_fallback`:请求是否走子 work、代理耗时,以及是否因容量不足回退主服务。
- `api_inflight_start` / `route_inflight_start`:请求刚进入时正在处理的 API 总数、同一路由请求数;`*_end` 是该请求结束记录时仍在处理的数量。
- `db_pool`:慢请求、异常请求和并发达到 8 个时,记录业务数据库连接池已借出数、池大小和溢出连接数。用于判断首屏并发/连续刷新是否挤满 MySQL 连接池。
- `exception_type`:只记录异常类型,不记录可能含敏感数据的异常消息。
- `status=499`:浏览器刷新、切页或取消请求导致服务端提前结束,不表示接口内部失败。
接口 INFO 日志依赖后台日志级别允许 INFO。若 `config.yaml` 中设置为 WARNING,请临时调到 INFO;慢请求和 5xx 仍会以 WARNING/ERROR 级别记录。
重点接口的 `stages` 含义:
- `auth_options`:`load_system_settings` 是系统设置读取;`count_admin_users` 是业务库查询。
- `agents_list`:`agents_*_query_db` 和 `*_db` 是数据库调用;`agent_response_models`、`agents_filter_and_sort` 是进程内转换/排序;`agents_*_assembly` 汇总列表组装耗时。
- `llmwiki_knowledge_bases`:`weknora_knowledge_base_list_wait` 是等待 WeKnora 列表结果;`local_knowledge_base_query_db` 是本地数据库查询;`conversation_deposit_mapping_sync` 是会话知识库映射同步。
- `llmwiki_agent_knowledge_bases`:`actor_and_agent_access` 是用户与智能体权限查询;`load_agent_binding_config` 是读取智能体配置文件;`local_knowledge_base_mapping_query` 是按智能体绑定 ID 读取本地映射;`weknora_knowledge_base_list_wait` 是等待远端列表。
知识库列表接口(`/api/llmwiki/knowledge-bases` 和智能体绑定列表 `/api/llmwiki/agents/{id}/knowledge-bases`)会等待 WeKnora 列表请求,进程内缓存有效期为 15 秒。若 `PERF http_request` 的 `stages.weknora_knowledge_base_list_wait` 接近 16 秒,同时出现相近耗时的 `PERF weknora_knowledge_base_list upstream_ms=...`,就能确认主要耗时在 WeKnora 请求。`PERF weknora_http_request` 会把远端目录请求拆成 `auth_ms`(获取/刷新 WeKnora 登录凭据)与 `network_ms`(HTTP 请求等待);这样能看出慢在登录还是远端 API 网络/处理。若远端耗时很快但接口仍慢,则看其他阶段。智能体绑定接口另记录 `actor_and_agent_access`(权限与智能体查询)、`load_agent_binding_config`(配置文件读取)和 `local_knowledge_base_mapping_query`(本地数据库映射查询)。
ORM 数据库执行时间超过 500 ms 会打印 `PERF db_query`,包含请求 ID、数据库类型、SQL 操作类型、耗时和 SQL 指纹,不包含 SQL 语句和参数。请求 ID 与 `http_request` 行相同,可据此判断慢 SQL 属于哪个接口。SQL 错误只记录错误类型。可用 `DEERFLOW_SLOW_SQL_LOG_MS` 单独调整数据库慢查询阈值。
首屏或反复刷新若引起请求洪峰,可看同一时间 `api_inflight_start`、`route_inflight_start` 是否升高,以及 `db_pool.checked_out` 是否逼近 `size + max_overflow`。如果接口等待明显但没有慢 SQL,重点看 `event_loop_lag_ms` 和 `outside_handler_ms`;这表示等待可能发生在连接获取、文件/磁盘、鉴权中间件或其它同步代码中。
如果 `total_ms` 很高、接口 `stages` 都很低,或同时出现 `PERF event_loop_stall`,问题更可能在接口函数之外的中间件/同步代码,或事件循环被其他任务占用。可以在卡顿发生时抓当下正在执行的 Python 函数。
## 抓当前 Python 函数堆栈
后台启动时会注册标准库 `faulthandler` 的 `SIGUSR1` 处理器。先在宿主机查容器里的 Python 主机 PID,再发信号:
```bash
docker top cmzs-deerflow-deerflow-backend-1 -eo pid,ppid,comm,args
sudo kill -USR1 <上一步查到的Python主机PID>
docker logs --since 2m cmzs-deerflow-deerflow-backend-1 2>&1 | less
```
在 `Current thread` 段查看最忙线程当时的 Python 文件和函数;其它线程也会一起输出。请在 CPU 正高时连续抓 2 到 3 次,每次间隔几秒:同一个函数反复出现通常是持续热点,不同调用栈则可能是瞬时任务。
若运行环境已有程序占用 `SIGUSR1`,可在后台容器环境设置 `DEERFLOW_ENABLE_STACK_DUMP=0` 关闭此处理器。额外详细输出可临时设置 `DEERFLOW_PERF_DIAGNOSTICS=1`,只会逐次记录 `options`、`agents`、知识库列表和智能体绑定知识库接口;HTTP 慢请求阈值可通过 `DEERFLOW_SLOW_REQUEST_LOG_MS` 调整,默认 500 ms。修改环境变量后需重启后台。
@@ -0,0 +1,198 @@
# 创建线程延迟优化与业务库 / Checkpoint 分离
日期:2026-10-22。
## 本次实现
适配官方 [#2865](https://github.com/bytedance/deer-flow/pull/2865) 与
[#4230](https://github.com/bytedance/deer-flow/pull/4230),不切换部署数据库。
### 会话检索与总数
`ThreadMetaRepository.search/count` 共享 SQL 条件构建。字符串 / 空值 metadata 过滤和系统线程
排除在 SQLite / PostgreSQL 中先于分页执行;常见 MySQL 字符串任务、智能体和行动过滤
同样下推。无剩余 Python 条件时,search 只发一条分页 SELECT,count 只发一条 COUNT。
这会减少向应用搬运的行数;没有为 JSON 字段自动创建索引,是否还需要表达式索引应由线上
执行计划确定,不能由“已下推 SQL”推断“数据库不再扫描候选行”。
保持原有规则:
- 普通会话仍由 `resolve_user_id` 隔离,get / check_access / 更新 / 删除授权不变。
- 非空 `metadata.taskId` 过滤仍开启任务共享;taskId、agent_id、actionId 全部条件同时生效。
- 默认列表继续隐藏 truthy `system`、非空白 `task_analysis_seat` 和既有系统 thread_type。
- search/count 使用相同条件;先过滤再分页,updated_at 相同按 thread_id 倒序稳定排序。
- 保留本地 `md.get(key) == None` 同时命中缺失键和 JSON null 的语义;数值过滤
保留 Python 的 `True == 1 == 1.0`,字符串数字不与数值混用。
- 嵌套 dict/list、特殊键名、超出 int64 的过滤值继续精确做剩余 Python 过滤,绝不忽略。
独立 openGauss 方言暂留原扫描路径。PG / MySQL 数值(含布尔)过滤也留在兼容路径,避免
DECIMAL / NUMERIC 与 Python 浮点数语义不同或 DOUBLE 舍入大整数改变任务匹配结果;
同一次查询的字符串条件仍可下推。SQLite 数值比较可直接下推。
- MySQL 字符串 id 按二进制比较,避免数据库默认不区分大小写 / 尾随空格的排序规则扩大命中。
使用 MySQL 5.7 可用的 JSON 函数;Python 空白判定通过完整字符替换实现,不依赖 5.7 的
字节正则去判定中文。
### PG ORM 连接池
`database.command_timeout` 默认 30 秒,可设有限正数或 null(关闭命令 deadline)。
只有 asyncpg 驱动接收该参数,MySQL / SQLite / psycopg 不接收。
PG 的 `pool_recycle` 未配置时默认 300 秒;MySQL / openGauss 保留 1800 秒,显式值不变。
初始建连与“数据库不存在、建库后重建 engine”使用相同参数。
```yaml
database:
backend: postgres
postgres_url: $DATABASE_URL
pool_size: 5
max_overflow: 10
pool_timeout: 30
pool_recycle: 300
command_timeout: 30
```
`pool_timeout` 是等待 ORM 池连接的期限,`command_timeout` 是 asyncpg 命令期限。
它们不限制 Python saver 锁排队,也不作用于使用 psycopg_pool 的 LangGraph checkpoint。
业务库已经是 MySQL 时,这个 PG ORM 补丁不会改善独立 PG checkpoint 的等待。
## 先确定部署真实使用的配置
仓库中存在不同配置文件,不能把源码模板当成生产进程最终配置:
- `offline-backend-20260512/config.yaml`:database.backend=mysql,checkpointer.type=postgres,
但 use_postgres=false;未被环境变量覆盖时,实际 checkpoint 回退 SQLite。
- `offline-backend-20260512/backend/config.yaml`:database/checkpointer 均为 SQLite。
- legacy `checkpointer:` 优先于 `database:`,`DEER_FLOW_USE_PG_CHECKPOINTER` 又可覆盖
legacy use_postgres。还应核对启动器选择的配置路径与已发布的后端版本。
现有 `GATEWAY_OPERATION_TIMING operation=thread_create` 日志会输出 metadata_store、
checkpointer 类型和各阶段耗时。默认延迟初始化时 checkpointer=deferred,只有
metadata_lookup / metadata_create;显式立即初始化时才有 checkpoint_create。
先看实际类型是 AsyncPostgresSaver、AsyncSqliteSaver 还是迁移代理,再判断瓶颈属于哪个库。
业务 MySQL 与 checkpoint PG 即使用同一应用,也有独立连接池。
## 创建线程能否只写 MySQL
**仅在创建阶段跳过 PG checkpoint,技术上可行;完整问答只使用 MySQL,当前实现不支持。**
已经实现延迟初始化:`POST /api/threads` 默认只创建业务元数据,不获取/查询/写入
checkpointer。业务库为 MySQL 时,创建阶段只使用 MySQL。metadata 保留 taskId、agent_id、
actionId 等既有分区信息,owner/user_id 仍由服务端控制;幂等重试保持既有记录。
已授权、尚无 checkpoint 的会话读取返回空 state/history;首次 run 由图运行时正常创建
checkpoint,继续使用原持久化、恢复和审计路径。需要旧行为的集成调用方可显式传入
`initialize_checkpoint: true`,等待立即写入空 checkpoint。
`GET /api/threads/{id}` 有业务记录时默认只读业务库,values 为空,running 状态映射 busy。
需要完整图状态请调用 `/state`,或显式 `?include_state=true`。仅有 checkpoint 的旧会话
保留读取兜底;所有路径仍先通过原 owner/project 权限装饰器,任务共享语义不变。
单纯去掉 await 并后台 `aput(empty_checkpoint)` 会与首次 run 竞态,晚到的空 checkpoint
可能成为新 head,不能这样处理。延迟初始化也只是把一部分 checkpoint 成本移到首次 run,
并不保证更快出现首字,必须分别测创建延迟和首字延迟。
完整移除 PG 还需要实现或验证 MySQL checkpointer(当前只有 memory/sqlite/postgres),并迁移
checkpoint/blob/write、兼容父链、恢复、回滚和子图。直接改 backend=mysql 不是此功能。
改用 SQLite 也保留 saver 串行 / 单写者竞争,不能据此承诺多人使用更快。
## 最终 History 能否后台刷新
**已实现不阻塞输入的后台状态同步,使用 SDK 的外部 history 接口。**
原 hook `fetchStateHistory: { limit: 1 }`,`onFinish(state, runMeta)` 使用最终 state。
锁定版本 `@langchain/langgraph-sdk@1.9.28` 的 `stream.lgp.js` 在 onSuccess 中先 await
`history.mutate`,再调用 onFinish,随后才结束 loading。`fetchStateHistory:false` 时,
有参数的 onFinish 仍会要求刷新,并把 getHistory 换成 getState,后者也读取 checkpointer。
因此简单关 history 或给服务端加 BackgroundTasks,不能消除这段等待。
实现细节:
1. 后端主图明确 checkpoint_ns='',完整消费并关闭 alist 后再加工响应,避免序列化继续
占用 saver 实例锁;最新 head 返回完整 channel values,其余条目不重复消息。
2. 前端 `BackgroundHistory` 提供 SDK 外部 history。post-stream mutate 立即提交流式快照,
不等待网络;快照不携带旧 checkpoint id。服务器明确 end 后解除运行中,允许继续输入,
再在后台读取 limit=1 的最终历史;网络 EOF/错误仍使用原终态确认和重连流程。
3. thread id + run id + epoch + request id 共同保护写回。新一轮发送、切线程和卸载会使旧
请求失效,即使 fetch 忽略 abort 也不能回写;新线程采用服务器 id 不会误清空首轮流。
同一轮历史若缺少最新消息或内容尚为早期前缀,也不会覆盖已显示回答。
4. 每次后台读限制 15 秒,完成同步失败最多重试一次(间隔 500ms;401/403/404 不重试)。
失败保留流式正文并提示可继续提问;已有消息不会被清空。正常 end 的业务完成回调立即
使用本轮完整流快照,避免下一轮改变页面上下文而漏掉日志/归档;后台同步不重复回调。
断流恢复可能遗漏最终 SSE,其回调仍等待有效同步,失败时使用保留快照;下一轮正常
发送使用 checkpoint:null 跟随服务端最新 head,避免以旧快照分叉。
`isStateSyncing` 与运行中 loading 分开。EOF 若发生在完整 values 帧之间,保留 SDK
已积累的更新 token 消息,直到终态确认/恢复完成。
后台 history 仍消耗数据库和 saver 锁容量,能改善交互等待,但不能代替解决后端串行瓶颈。
## 登录与业务接口是否查询 PG
登录链路为 `routers/auth.py` → `get_local_provider()` → `LocalAuthProvider` →
`SQLiteUserRepository(sf)`。仓储类名为历史命名,实际 sf 来自
`init_engine_from_config(config.database)`,与 checkpoint 的独立连接池无关。
密码、用户名/口令、token 兑换、本地用户读取、session/token_version 和岗位授权同步
均使用业务仓储;token 兑换另外会请求上游身份接口,岗位同步也可能增加 MySQL 等待,
这些等待不能归因为 PG。CSRF、口令验证、白名单、单会话策略均未更改。
| 路径 | database.backend=mysql 时的数据来源 |
| --- | --- |
| 登录配置中的账号统计、登录/注册、身份查询、用户/岗位权限 | MySQL(策略配置仍可能来自配置文件) |
| 会话创建、搜索/count、已有元数据的线程信息 | MySQL;显式 eager/include_state 除外 |
| 智能体、任务、轻应用、菜单、业务映射等 SQL 仓储 | 共享业务 session factory → MySQL |
| state/history/messages、聊天执行/恢复/删除、从聊天提取资料 | checkpoint(配置为 PG 时访问 PG) |
| AI 写作等图执行的暂停/继续、管理员查看聊天问答明细 | 同属图状态或聊天内容,仍需要 checkpoint |
分库配置示例(不包含实际连接信息):
```yaml
database:
backend: mysql
mysql_url: $MYSQL_DATABASE_URL
checkpointer:
type: postgres
use_postgres: true
connection_string: $CHECKPOINT_POSTGRES_URL
```
本次未修改现有部署连接串、迁移或删除旧表。已有数据库不会因改配置自动迁移,
不能在未迁移业务数据时直接把线上 database.backend 从 postgres 改成 mysql。
尚未完全拆开的边界:LangGraph BaseStore 目前跟随 legacy checkpointer 配置,
旧线程元数据迁移和部分运行时 store 使用它;Gateway 启动也仍等待 checkpoint/store
初始化与历史迁移。因此“已运行服务的业务请求不查询 checkpoint”不等于“PG 故障不影响
Gateway 冷启动”。若需要故障域完全独立,应后续拆运行时初始化/部署并迁移旧 Store,
不能以切换到易失内存或跳过旧数据的方式冒充完成。配置文件与文件型资料也未迁入 MySQL。
## 验证范围
后端相关回归:**133 passed**;前端 `pnpm test:thread-history`:**35 passed**。
从 backend 目录执行(Windows 将 PYTHONPATH 分隔符换为分号):
```bash
PYTHONPATH=packages/harness:. .venv/bin/python -m pytest \
packages/harness/tests/test_thread_metadata_sql_filters.py \
packages/harness/tests/test_database_pool_deadlines.py \
packages/harness/tests/test_thread_operation_timing.py \
packages/harness/tests/test_thread_lazy_checkpoint.py \
packages/harness/tests/test_run_completion_timeout.py \
packages/harness/tests/test_run_stream_response_headers.py \
tests/test_account_authentication.py \
tests/test_gauss_database.py tests/test_ensure_orm_columns_sync.py -q
```
- 新增 SQLite 实际查询回归:标量 / 缺失值 / 嵌套兼容、系统排除、共享任务、agent/action
分区、普通用户隔离与修改权限、分页、标题/状态过滤,以及 1200 条噪声下只发单条查询。
- 新增 PG/MySQL SQL 编译与参数绑定、MySQL 二进制 id 比较、Gauss/服务端数值兼容回归。
- 新增 ORM 真实初始化路径的参数捕获:初次 / 建库重试、null 和自定义命令超时、MySQL /
SQLite 参数隔离、配置校验。
- HTTP + SQL + SQLite saver 验证持锁时 20 个创建请求完成、首轮真实 LangGraph 初始化与
第二轮续聊、元数据空线程读写、私有/任务共享权限、history 释放锁和取消关闭迭代器。
- 登录配置/密码登录/用户名口令登录/token 兑换/身份/用户列表使用实际 SQL 用户仓储,并安装
禁止访问 checkpoint 的桩,验证这些请求不会走聊天后端。
- 前端含实际安装 SDK 的 SSE 解码和 StreamManager 集成,验证 history 挂起不阻塞结束、
旧响应/新轮次/切线程/卸载隔离、异常/EOF恢复、超时/重试、旧副本保护和流式状态保留。
- 修改文件 Ruff 与 `git diff --check` 通过。前端全量 `pnpm typecheck` 仍有 81 条既有
类型错误;与 HEAD 基线比对并修正新增测试的类型后,本次改动文件无新增报错。
- 本机 Docker Linux daemon 未运行,未以真实 PG/MySQL 服务做集成或负载验证;不把 SQLite
查询与驱动配置测试当作线上延迟改善比例。发布前仍应在对应版本数据库做隔离集成验证。
- 额外尝试收集本机原有 `tests/test_gauss_mysql_compat.py` 时,因仓库缺少其导入的
`deerflow.persistence.gauss_mysql_compat` 模块失败;该本机测试文件未列入上述 133 项,
未为其添加与本次补丁无关的兼容模块。