sync update from LAN repo (source dhk-1 @ 3b1ec5b4)
This commit is contained in:
1 parent
da6521b6cb
commit
264e2200d6
995 files changed
+148105
-13399
No files matched your search
@@ -1,5 +1,36 @@
|
||||
# 登录鉴权扩展接口文档(auth_login)
|
||||
|
||||
## 账号、注册与单点总开关(2026-10-05)
|
||||
|
||||
管理员设置 `GET/PUT /api/system-settings/authentication`,请求和响应是完整对象:
|
||||
|
||||
```json
|
||||
{"password_login_enabled":false,"registration_enabled":false,"sso_enabled":true,"single_session_enabled":false}
|
||||
```
|
||||
|
||||
密码登录页面、自助注册与同账号单处登录默认关闭,外部系统免登默认开启,已有管理员保存的设置不变。至少保留一种登录方式;自助注册要求密码登录开启。无个人密码的存量用户使用原共享口令,未配置则使用 `123ewq`,因此关闭外部系统免登无需先给管理员设个人密码。未登录页面通过 `GET /api/v1/auth/options` 读取同四字段和 `needs_setup`(当前没有管理员),响应禁止缓存。这里只公开开关,不公开共享密码、个人密码或上游密钥。
|
||||
|
||||
| 接口 | 请求 | 成功响应 |
|
||||
| --- | --- | --- |
|
||||
| `POST /api/v1/auth/login/local` | form-urlencoded `username`、`password`;用户名或邮箱 | `UsernameLoginResponse` + HttpOnly Cookie |
|
||||
| `POST /api/v1/auth/register` | JSON `{username,password}`;兼容 `{email,password}`,两种账号字段只传一项 | `201` + `UsernameLoginResponse`,自动登录普通用户 |
|
||||
| `POST /api/v1/auth/initialize` | JSON `{email,password}`;只允许系统没有管理员时 | `201` + `UsernameLoginResponse`,创建首任管理员 |
|
||||
| `GET /api/v1/auth/me` | 当前会话 | `id,email,system_role,needs_setup,has_password,requires_current_password,uses_default_password` |
|
||||
| `POST /api/v1/auth/change-password` | JSON `{current_password,new_password}` | `message,access_token,token_type,expires_in` + 更新 Cookie |
|
||||
| `PUT /api/v1/auth/users/{id}/login-password` | 管理员 JSON `{password}`;也可用 `/reset-password` 别名 | 用户列表项;重置自己时附 `access_token,token_type,expires_in` |
|
||||
|
||||
`UsernameLoginResponse` 为 `access_token,token_type,expires_in,user_id,email,system_role,needs_setup,created,username`。前端收到改密/本人重置成功响应后必须同步替换本地 Bearer,否则会被旧版本 token 拒绝。新增 `auth_usernames` 表用数据库唯一键保护并发新建账号的用户名;新旧域名注册同名也只能成功一次。ORM 自动创建该表,历史用户在新建前按邮箱 local-part 检查。用户名单点在初次查询后发生并发注册/改密时,会再次校验最终账号凭据后才签发会话。
|
||||
|
||||
新密码为 8–255 字符,拒绝常见弱密码和首尾空格;所有用户都必须提供 `current_password`,无个人凭据时用原共享口令(未配置则 `123ewq`)首次设置,不能留空。用户名接受字母、数字、`.`、`_`、`-`,首尾须为字母或数字,也接受有效邮箱。用户名不区分大小写;已存在的相同邮箱 local-part 不得通过换域名重复注册或覆盖。重复返回 `409`,校验失败 `422`,关闭方式或越权返回 `403`,错误密码 `401`(改密码的错误当前密码为 `400`),登录限速 `429`。新注册沿用现有黑名单语义默认放行;已有封禁账号仍被拦截。
|
||||
|
||||
新密码与「登录口令」统一在 `password_hash`、`login_password` 保存同一版本化散列;两种登录入口接受同一密码。历史明文个人口令优先于旧 `password_hash`,可直接用于密码登录、URL 口令登录或验证改密,修改后即改存散列。管理员列表仅对历史明文保留旧兼容,任何散列均不返回,新增 `has_login_password` 表示是否已设置。管理员设置/重置不回显密码;已有密码不得清空。复制新登录链接时使用本次输入,之后需重新输入或重置。修改/重置原子递增 `token_version`,此前全部会话与旧密码失效;并发过期修改返回 `409`,普通审批不能覆写凭据或复活旧会话。
|
||||
|
||||
`sso_enabled=false` 后,用户名直登、上游 token、OAuth 及 `/api/parallel-agents/auth/token` 都拒绝登录。并行面板兼容入口现复用标准用户名登录的密码、限速和封禁校验,可附 `?password=`。关闭自助注册不影响已启用的单点对外部身份自动建普通用户。用户名含 `admin` 不再授予或提升管理员权限;历史已经是管理员的账号不变。首任管理员初始化使用独立单例 claim 与用户插入的同一数据库事务,跨工作进程并发也最多成功一次。部署无需人工增加列,ORM 启动自动创建 `auth_initialization` 表;管理员恢复仍使用 `python -m app.gateway.auth.reset_admin`,同样同步两套凭据。
|
||||
|
||||
启用 `password_login_enabled` 后,用户名/并行面板登录也必须验证个人密码或原共享口令(未配置 `auth_login.username_login.password` 时为 `123ewq`);个人凭据存在时共享口令不能替代。JWT 的 `auth_method=password/passphrase/sso` 仅由实际完成校验的登录路径显式写入。Gateway 与 LangGraph 同时拒绝缺少标记的历史/免密会话,要求重新登录;已验证的上游 SSO 仍受独立 `sso_enabled` 控制。开发环境 `DEER_FLOW_AUTH_DISABLED` 也不能绕过已开启的密码登录或单处登录限制。关闭密码登录时保留旧用户名单点及共享口令门行为;`create_access_token` 不会自动给未经校验的调用添加可信标记。
|
||||
|
||||
「单点登录(同账号仅一处在线)」由独立 `single_session_enabled` 控制,默认关闭;`sso_enabled` 仅表示既有外部单点/免登入口,两个功能分开。开启单处登录后,每次成功的账号密码、用户名口令、上游 token、并行面板、注册或初始化登录,都在数据库内原子递增该账号 `token_version` 后签新 JWT,旧会话立即返回 401,前端轮询 `/me` 后退出旧页面。失败登录不会踢人,关闭时允许多处会话并存;修改/重置密码仍始终撤销旧会话。并发登录通过同一行的原子递增保证最终仅最高版本会话有效;事务还比较已验证的密码/口令与权限快照,避免并发重置密码后旧凭据借新登录版本恢复。
|
||||
|
||||
在原有登录之上新增两种自定义登录模式,均由 `config.yaml` 的 `auth_login` 段控制。
|
||||
|
||||
## 配置
|
||||
@@ -33,7 +64,7 @@ auth_login:
|
||||
|
||||
## 功能一:用户名直登 + 口令门
|
||||
|
||||
在原 `POST /api/v1/auth/login/username` 上叠加一个共享口令校验。`require_password=false`(默认)时行为完全不变(免密直登 + 自动注册)。
|
||||
此前提为系统设置的 `sso_enabled=true`。账号已设置个人密码/口令时,始终校验该凭据;未设置个人凭据时,开启账号密码登录或 `require_password` 任一开关都会要求共享口令(未配置则为 `123ewq`)。
|
||||
|
||||
### 请求
|
||||
|
||||
@@ -44,8 +75,8 @@ Content-Type: application/json
|
||||
{ "username": "alice" }
|
||||
```
|
||||
|
||||
- `?password=`:仅当 `require_password=true` 时需要。值需等于 `username_login.password`;该项留空时默认口令为 `123ewq`。
|
||||
- 口令门是**单一共享口令**,只决定"放不放行",不影响登录成的是哪个账号。
|
||||
- `?password=`:账号已有个人密码/口令时必须匹配个人凭据;否则仅当 `require_password=true` 时需要匹配共享口令。
|
||||
- 共享口令仅适用于没有个人凭据的账号,不影响目标账号选择。
|
||||
- 校验失败按 IP 计入登录限速(5 次/5 分钟锁定),与本地密码登录共用同一限速器。
|
||||
|
||||
### 响应
|
||||
|
||||
@@ -4,29 +4,25 @@
|
||||
|
||||
## 前置配置
|
||||
|
||||
在「业务链条管理」页面新增的 **任务分析**(业务码 `b5-1`)行中选择要使用的单智能体。没有配置智能体时,深链接不会进入问答页。
|
||||
在「业务链条管理」页面新增的 **任务分析**(业务码 `b5-1`)行中选择要使用的默认单智能体。深链接也可以用 `?agentId=` 直接指定本次问答使用的智能体(优先于配置行);两者都没有时,深链接不会进入问答页。
|
||||
|
||||
服务端为接入写入端配置环境变量:
|
||||
|
||||
```text
|
||||
DEERFLOW_TASK_DEEPLINK_INGEST_KEY=<随机且足够长的共享密钥>
|
||||
```
|
||||
|
||||
该密钥只用于下面的 `PUT`;浏览器读取详情仍需正常登录态。
|
||||
服务端无需为接入配置任何密钥。
|
||||
|
||||
## 写入或更新任务详情
|
||||
|
||||
```http
|
||||
PUT /api/task-deeplink-details/b5-1/{id}
|
||||
Content-Type: application/json
|
||||
X-Integration-Key: <DEERFLOW_TASK_DEEPLINK_INGEST_KEY>
|
||||
```
|
||||
|
||||
该接口为**开放写入**:不需要登录态,也不需要任何鉴权头,接入方服务端直接调用即可。写入者仅作审计记录。
|
||||
|
||||
```json
|
||||
{
|
||||
"taskName": "重点活动舆情研判",
|
||||
"taskContent": "围绕活动期间的舆情风险、重点对象和处置建议开展分析。",
|
||||
"overview": "活动前后七天的综合研判任务",
|
||||
"type": "yuqing",
|
||||
"extra": {
|
||||
"sourceSystem": "B5",
|
||||
"priority": "high",
|
||||
@@ -38,8 +34,9 @@ X-Integration-Key: <DEERFLOW_TASK_DEEPLINK_INGEST_KEY>
|
||||
}
|
||||
```
|
||||
|
||||
- `id` 与深链接中的 `taskId` 必须完全一致;存储键为 `(type, id)`,此处 `type` 固定为 `b5-1`。
|
||||
- `taskName`、`taskContent` 必填;`overview`、`extra` 可选。
|
||||
- `id` 与深链接中的 `taskId` 必须完全一致;存储键为 `(type, id, 业务类型)`,此处路径 `type` 固定为 `b5-1`。
|
||||
- `taskName`、`taskContent` 必填;`overview`、`type`、`extra` 可选。
|
||||
- `type` 是业务类型(≤64 字符),与深链接 `?type=` 对应:**同一个 id 可以存多个不同类型的详情快照**,各自独立版本号;不传 `type` 时写入无类型分区(与深链不传 `?type=` 的读取口径一致)。
|
||||
- `extra` 必须是 JSON 对象,最大 64 KiB;用于接入方的扩展字段。
|
||||
- 这是幂等接口,接入方不需要预先查询「是否已存」。每次直接 `PUT` 完整快照即可:
|
||||
- 首次写入:创建记录;
|
||||
@@ -53,6 +50,7 @@ X-Integration-Key: <DEERFLOW_TASK_DEEPLINK_INGEST_KEY>
|
||||
"data": {
|
||||
"type": "b5-1",
|
||||
"id": "task-20261020-001",
|
||||
"taskType": "yuqing",
|
||||
"taskName": "重点活动舆情研判",
|
||||
"taskContent": "围绕活动期间的舆情风险、重点对象和处置建议开展分析。",
|
||||
"overview": "活动前后七天的综合研判任务",
|
||||
@@ -68,7 +66,7 @@ X-Integration-Key: <DEERFLOW_TASK_DEEPLINK_INGEST_KEY>
|
||||
}
|
||||
```
|
||||
|
||||
`created` 表示本次是否首次创建;`changed` 表示本次是否实际写入。对同样内容的重试,响应为 `created: false`、`changed: false`。
|
||||
`created` 表示本次是否首次创建;`changed` 表示本次是否实际写入。对同样内容的重试,响应为 `created: false`、`changed: false`。响应中的 `taskType` 回显写入时的业务类型(注意与固定为 `"b5-1"` 的 `type` 字段区分)。
|
||||
|
||||
## 用户跳转
|
||||
|
||||
@@ -78,5 +76,19 @@ X-Integration-Key: <DEERFLOW_TASK_DEEPLINK_INGEST_KEY>
|
||||
/#/login?taskId=task-20261020-001&goPath=b5-1
|
||||
```
|
||||
|
||||
登录后前端读取本系统详情接口,将任务名称和任务详情展示在左上角,标题固定为「任务分析」;`taskName`、`taskContent`、`overview` 与完整 `extra` 会作为每轮问答的隐藏任务上下文提供给配置的智能体。聊天记录按 `(taskId, 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,删除操作须管理员单独点击。
|
||||
+8
@@ -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`。链路阶段为:接口数据采集 → 当前判断威胁接口采集 → 情报摘要与校验。
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ models:
|
||||
use: deerflow.models.openai_codex_provider:CodexChatModel
|
||||
model: gpt-5.4
|
||||
supports_thinking: true
|
||||
supports_reasoning_effort: true
|
||||
|
||||
- name: claude-sonnet-4.6
|
||||
display_name: Claude Sonnet 4.6 (Claude Code OAuth)
|
||||
@@ -59,6 +58,15 @@ models:
|
||||
supports_thinking: true
|
||||
```
|
||||
|
||||
Whether a model exposes the per-chat reasoning-effort selector is managed by
|
||||
an administrator in **Model Management** and persisted in the
|
||||
`model_capabilities` database table. For upgrade compatibility, a legacy
|
||||
`supports_reasoning_effort: true` value is imported once at startup only when
|
||||
that model has no database row. Existing database values (including `false`)
|
||||
are never overwritten; after that first import the database is authoritative.
|
||||
Remove the legacy YAML field when convenient. Model Management also removes it
|
||||
the next time the model list is saved, and new configurations should not add it.
|
||||
|
||||
**Auth behavior for CLI-backed providers**:
|
||||
- `CodexChatModel` loads Codex CLI auth from `~/.codex/auth.json`
|
||||
- The Codex Responses endpoint currently rejects `max_tokens` and `max_output_tokens`, so `CodexChatModel` does not expose a request-level token cap
|
||||
|
||||
@@ -78,6 +78,8 @@ WeKnora 前端不做任何修改。DeerFlow 原“知识库资源管理”入口
|
||||
- 原模式:保留现有 LLMWiki iframe 页面;
|
||||
- WeKnora 模式:渲染 DeerFlow 原生 React 页面,提供个人库、公共库、直接发布和检索。点击卡片进入独立知识库路由,内容区复刻 WeKnora 的详情页结构,同时保留 DeerFlow 的导航、登录和权限壳。Wiki 库按 **Wiki → 文档 → 图谱** 排列并默认打开 Wiki;标准库不显示 Wiki 标签,按 **文档 → 图谱** 排列。Wiki 视图包含页面目录、搜索、页面正文和引用摘要;文档以卡片网格展示,点击后从右侧抽屉查看元数据、原文件与真实解析分块;图谱视图渲染 WeKnora Wiki 页面节点和边。新建知识库可选择“Wiki 知识库”或“标准知识库”,详情页支持文件、网页 URL、手工 Markdown 三种导入方式。问答选择器沿用 WeKnora 的紧凑搜索/分组/多选交互,颜色、间距和组件沿用 DeerFlow 设计系统;智能体创建和编辑页使用同一数据源的内嵌绑定面板。
|
||||
|
||||
Wiki 知识库可选填“参考目录”,支持逐行录入或上传 `.txt/.md`。参考目录只帮助生成模型理解用户期望的信息组织方式,不是目录白名单,也不是必须逐项套用的固定模板。WeKnora 可以根据原始资料新增、合并、拆分、改名、调整或省略目录;Gateway 不会为参考目录预建文件夹,也不会在生成后移动页面、删除目录或清理参考目录之外的有效结构。`directory_paths` 与兼容字段 `parent_directories` 均遵循这一语义。未填写时沿用原有自动 Wiki 生成方式;编辑时清空只表示不再向后续生成提供该参考,不撤销已有页面或目录。
|
||||
|
||||
问答检索命中以 DeerFlow 来源卡片展示。“查看原文”调用 DeerFlow 的 `/api/llmwiki/sources/context`,由服务端在重新校验 DeerFlow 用户权限后向 WeKnora 读取命中切片及相邻上下文,不向浏览器暴露 WeKnora 原始资源 ID。
|
||||
|
||||
## 主要接口
|
||||
|
||||
@@ -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 数,不能消除上述等待链。
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
# 普通知识库智能推理检索模式设计
|
||||
|
||||
> 状态:已实现,与当前代码一致
|
||||
>
|
||||
> 适用范围:通用问答、普通智能体问答中的 WeKnora 普通知识库
|
||||
>
|
||||
> 更新日期:2026-10-21
|
||||
>
|
||||
> WeKnora 行为核查基线:v0.7.2,commit 3d5d8bfcdfeeea266b292b71cea616847af28d0f
|
||||
|
||||
## 1. 目标
|
||||
|
||||
系统在原有经典检索之外增加“知识库智能检索”模式。经典模式完整保留并始终作为新对话的默认模式,模式选择器中经典检索位于智能检索上方。新模式的全局开关默认关闭,只有管理员能在设置页开启;开关开启只表示向用户开放智能检索选项,用户可在需要更全面梳理时主动切换。已有线程继续按自身 metadata 恢复模式,不被全局开关批量改写。
|
||||
开关和本功能新增配置只保存在数据库中,不写 `system_settings.json`、`config.yaml` 或其他配置文件。智能模式在一次用户提问内执行模型与工具的多轮循环。
|
||||
用户只需直接提问,不需要在问题中说明先检索什么、再检索什么。每个新问题先执行原始文档向量/混合检索取得直接相关的分片、标题和实体;只有问题需要目录结构、概念关系、时间线或扩展线索,且当前 WeKnora 支持原生 Wiki 正则接口时,才用这些线索执行 Wiki 正则检索。Wiki 有命中时,模型只选择与回答有关的页面深读;模型未选择时,中间件读取排序第一的页面作为最低证据基线。长 Wiki 页面会使用 `next_cursor` 连续读取到 `complete=true`。之后只有在 Wiki 暴露出首轮 RAG 没有覆盖且回答确实需要的线索时,才继续改写查询、做原始切片正则核验或读取文档上下文。证据足够就停止检索并回答;Wiki 与原始文档冲突时,以原始文档为准。
|
||||
检索结果是回答的背景资料,不替代模型原有的理解、归纳、写作和推理能力。模型可以使用稳定通用知识解释术语与背景,但必须把库内事实、基于材料的推断和一般知识区分清楚,不能用模型记忆编造知识库专属事实。
|
||||
最终回答不要求带 [1]、[2] 等编号。
|
||||
|
||||
## 2. 与原 WeKnora 逻辑的对应关系
|
||||
|
||||
本实现对齐 WeKnora 智能推理的搜索、深读和证据缺口循环。按当前验收要求,在 WeKnora 混合检索策略之上增加确定性的证据基线:先搜索原始分片,再结合原始命中的标题和实体搜索 Wiki,并至少深读一个相关 Wiki 页面。中间件会推迟越序的 Wiki 调用并补齐缺少的阶段,但不会为了走流程而读取全部 Wiki 命中页。基线完成后,后续轮次由模型根据问题类型和真实证据缺口决定。后续补强路径如下:
|
||||
|
||||
| 问题类型 | 固定首轮后的补强面 | 深读动作 |
|
||||
|---|---|---|
|
||||
| 概念、主题、关系、整体脉络 | Wiki 正则搜索 | 读取命中 Wiki 页正文 |
|
||||
| 数字、日期、编号、条款、原句 | 原始知识搜索或切片正则搜索 | 读取命中切片的邻近上下文或文档分块 |
|
||||
| 同时需要框架与事实 | 先原始 RAG,再用 Wiki 扩展 | 用新的原始查询或切片正则核验 Wiki 暴露的关键事实 |
|
||||
|
||||
原始知识搜索沿用 WeKnora 的 knowledge-search 接口。 该接口由 WeKnora 执行向量或混合召回、融合和排序。 精确词、代码、编号和固定短语由切片正则检索补充。
|
||||
经典检索同时使用两层语义召回: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` 属于有效续读,不视为重复。上游核查入口:
|
||||
|
||||
- Agent preset:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/config/agent_type_presets.yaml
|
||||
- Agent system prompt:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/config/prompt_templates/agent_system_prompt.yaml
|
||||
- Knowledge search tool:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/internal/agent/tools/knowledge_search.go
|
||||
- Chunk grep tool:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/internal/agent/tools/grep_chunks.go
|
||||
- Document reader:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/internal/agent/tools/list_knowledge_chunks.go
|
||||
- Wiki tools:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/internal/agent/tools/wiki_tools.go
|
||||
|
||||
## 3. 模式与开关
|
||||
|
||||
会话请求字段为 knowledge_retrieval_mode。 允许值只有:
|
||||
|
||||
- classic
|
||||
- smart_reasoning
|
||||
|
||||
请求省略该字段或传空值时,后端按 classic 处理。 前端每次发送都显式携带当前模式。 线程 metadata 只用于恢复前端控件,不是后端本轮执行的隐式输入。
|
||||
这保证旧客户端和缺少合法模式 metadata 的旧会话继续使用经典模式。新对话始终默认 classic;全局开关开启后只增加 smart_reasoning 可选项,关闭或读取失败时仅提供 classic。管理员切换全局开关不会批量改变历史会话。
|
||||
管理员关闭开关也不会删除历史对话。 历史线程若保存为 smart_reasoning,关闭全局开关后必须切回 classic 才能继续发送。 模式不写入全局浏览器偏好。
|
||||
新会话页面间的临时交接使用现有页面状态和 sessionStorage。
|
||||
|
||||
## 4. 数据库存储
|
||||
|
||||
全局开关表为 knowledge_retrieval_settings。 该表使用 scope 作为主键。 当前全局记录的 scope 为 global。
|
||||
主要字段如下:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| scope | 设置作用域,当前为 global |
|
||||
| smart_reasoning_enabled | 是否向用户开放智能模式选项;不改变新对话默认的经典模式 |
|
||||
| revision | 设置修订号 |
|
||||
| updated_by | 最近修改人 |
|
||||
| updated_at | 最近修改时间 |
|
||||
|
||||
数据库没有记录时,读取结果为 false。 迁移只创建表,不插入开启记录。 因此升级后功能保持默认关闭。
|
||||
迁移文件为 20261014_01_knowledge_retrieval_settings.py。 SQL 部署使用 SQLAlchemy Repository。 无数据库的单元测试使用同一 Store 接口的内存实现。
|
||||
生产功能不读取或写入本地配置文件。
|
||||
|
||||
## 5. 管理接口
|
||||
|
||||
读取接口:
|
||||
|
||||
GET /api/system-settings/ordinary-knowledge-retrieval
|
||||
|
||||
响应字段为 smart_reasoning_enabled。
|
||||
|
||||
更新接口:
|
||||
|
||||
PUT /api/system-settings/ordinary-knowledge-retrieval
|
||||
|
||||
请求字段为 smart_reasoning_enabled。
|
||||
|
||||
GET 供对话客户端和设置页读取当前状态。
|
||||
PUT 仅管理员可调用。
|
||||
|
||||
前端读取失败时按关闭处理,并允许用户重试读取。 后端每次启动智能模式请求时重新读取数据库开关。 前端隐藏控件不能替代后端校验。
|
||||
|
||||
## 6. 适用知识范围
|
||||
|
||||
智能模式只支持 WeKnora 普通知识库。 至少需要选择一个普通知识库。 选择助手知识库时拒绝智能模式。
|
||||
普通库与助手库混选时也拒绝智能模式。 智能体会话只能使用该智能体已绑定的普通知识库。 普通用户只能使用自己有读取权限的本地 mapping。
|
||||
管理员也必须通过现有 mapping 存储读取有效映射。 请求和工具参数始终使用本地 mapping ID。 模型不能直接传入远端 WeKnora knowledge base ID 扩大范围。
|
||||
越权、不存在和未绑定的知识库统一按不可访问处理。 智能模式启动前完成一次范围授权。 五个检索工具每次调用时再次读取当前用户、智能体绑定和 mapping 权限。
|
||||
因此管理员撤权或绑定发生变化后,后续工具调用会立即失败。 深读工具还会校验 chunk 或 document 实际所属的远端知识库。 只知道其他知识库的 chunk ID、document ID 或 page_ref 不能越权读取。
|
||||
|
||||
## 7. 请求执行流程
|
||||
|
||||
一次请求按以下顺序执行:
|
||||
|
||||
1. Gateway 解析本轮显式 knowledge_retrieval_mode。
|
||||
2. classic 直接进入原有经典检索链路。
|
||||
3. smart_reasoning 读取数据库全局开关。
|
||||
4. Gateway 校验普通库数量、助手库混选、智能体绑定和用户权限。
|
||||
5. Gateway 校验 WeKnora runtime 与 mapping Store 可用。
|
||||
6. Gateway 持久化本轮模式和知识库选择到线程 metadata。
|
||||
7. metadata 写入失败时,将尚未启动的 run 标记为失败,不启动模型。
|
||||
8. Gateway 把服务端确认后的模式和范围写入 RunnableConfig。
|
||||
9. 智能模式关闭经典的预检索提示和编号引用提示。
|
||||
10. Lead Agent 挂载五个智能检索工具和专用中间件。
|
||||
11. 专用中间件保证本轮先执行原始文档 RAG,再执行 Wiki 搜索;Wiki 有命中时至少完整读取一个相关页面。模型再根据 Wiki 暴露的真实证据缺口自主执行后续搜索和深读。
|
||||
12. 模型在证据充分或继续检索无收益时生成最终回答。
|
||||
|
||||
智能模式的 Gateway 上下文构建只做授权和范围准备。 它不在第一次模型调用前塞入固定数量的一次性检索片段。 检索决策由本轮模型和工具循环完成。
|
||||
服务端会覆盖调用方试图设置的 disable_tools。 服务端也会从 excluded_tools 中移除五个智能检索工具。 智能模式会排除经典 llmwiki_search。
|
||||
经典模式会排除五个智能检索工具。 两种模式不会同时暴露两套知识检索入口。
|
||||
|
||||
## 8. 五个真实工具
|
||||
|
||||
### 8.1 ordinary_knowledge_search
|
||||
|
||||
用途:对原始文档切片做向量或混合检索。 输入为 queries,允许 1 至 5 个非空查询。 单个查询最长 500 个字符。
|
||||
多个查询并行调用 WeKnora /api/v1/knowledge-search。 结果按 chunk 合并。 同一 chunk 命中多个改写时保留 matched_queries。
|
||||
同一 chunk 的分数取各次命中的最高值。 结果按分数倒序,最多返回 30 条。 某个查询失败时,其余查询结果仍可返回。
|
||||
所有查询都失败时返回结构化失败。
|
||||
|
||||
### 8.2 ordinary_chunk_regex_search
|
||||
|
||||
用途:在原始文档标题和切片中查找精确词、编号、代码或固定短语。 输入为一个大小写不敏感的安全 POSIX 风格正则表达式。 表达式最长 256 个字符。
|
||||
不接受 Python 专属扩展、反向引用和明显的嵌套量词风险模式。 每次正则执行有 50 毫秒硬超时。 首先调用 POST /api/v1/chunks/grep。
|
||||
原生响应必须声明 contract 为 weknora-v0.7.2-grep-v1。 合同不匹配按上游错误处理,不静默接受未知格式。 只有原生接口返回 HTTP 404 或 405 时,才进入兼容回退。
|
||||
兼容回退只使用现有 list_documents 和 list_chunks 接口,并按知识库、文档和切片页轮转扫描。
|
||||
一个大知识库不能独占全局扫描预算。 兼容回退最多扫描 500 个切片,最多返回 30 条结果。 兼容响应明确返回 compatibility_fallback 为 true。
|
||||
兼容响应的 contract 为 weknora-existing-api-compat-v1。 部分知识库失败时返回已成功范围的结果和结构化 warnings。 全部知识库失败时返回结构化失败。
|
||||
|
||||
### 8.3 ordinary_document_chunks
|
||||
|
||||
用途:读取命中切片的邻近上下文,或分页读取整篇文档的分块。 identifier_type 支持 chunk_id 和 knowledge_id。 chunk_id 模式先读取切片并核验所属知识库。
|
||||
校验通过后返回该切片前后各两个相邻切片。 knowledge_id 模式先核验文档所属知识库。 校验通过后按 page 和 page_size 读取分块。
|
||||
page 从 1 开始。 page_size 限制为 1 至 100。 返回 total,模型可继续翻页直到覆盖所需正文。
|
||||
|
||||
### 8.4 ordinary_wiki_search
|
||||
|
||||
用途:在 Wiki 页面中做正则搜索。 匹配字段包括:
|
||||
|
||||
- title
|
||||
- slug
|
||||
- summary
|
||||
- aliases
|
||||
- content
|
||||
|
||||
每个 mapping 单独选择读取通道。 本地镜像只有同时满足以下条件才被采用:
|
||||
|
||||
- 有 last_completed_at
|
||||
- state 为 idle
|
||||
- 没有 last_error
|
||||
- failed_page_count 为 0
|
||||
- local_page_count 等于 remote_page_count
|
||||
- 最近同步结果不是失败或部分完成
|
||||
|
||||
某个 mapping 的镜像不完整时,只将该 mapping 回退到 WeKnora provider。 其他镜像完整的 mapping 继续走本地通道。 全局最多扫描 500 个页面候选。
|
||||
扫描预算按 mapping 公平分配。 正文匹配范围对单页限制为前 50000 个字符。 最多返回 30 个页面入口。
|
||||
响应带 channel、coverage、partial、warnings 和 scan_truncated。 全部 mapping 都不可用时返回结构化失败。 检索成功但没有匹配时返回 ok 为 true、result_count 为 0。
|
||||
|
||||
### 8.5 ordinary_wiki_read_page
|
||||
|
||||
用途:读取 ordinary_wiki_search 命中的完整 Wiki 正文。 输入 page_ref 必须来自 Wiki 搜索结果。 page_ref 是范围引用,不是权限凭据。
|
||||
工具调用时仍会重新校验 mapping 权限。 读取通道与 Wiki 搜索相同:完整镜像优先,否则走 provider。 单次最多返回 30000 个字符。
|
||||
长页面返回以下续读字段:
|
||||
|
||||
- complete
|
||||
- next_cursor
|
||||
- content_offset
|
||||
- content_end
|
||||
- content_length
|
||||
- content_truncated
|
||||
|
||||
模型把 next_cursor 传回同一工具即可读取下一段。 cursor 与 page_ref 绑定。 把一个页面的 cursor 用到另一个页面会返回 INVALID_CONTINUATION_CURSOR。
|
||||
按顺序拼接各段 content_markdown 可以重建完整正文。 页面结果同时返回 title、slug、summary、aliases、links 和 source_refs。
|
||||
|
||||
### 8.6 检索进度与 `result_items` 合同
|
||||
|
||||
五个工具通过 LangGraph custom stream 发出 `knowledge_retrieval_progress` v1 事件。事件只展示实际查询、公开聚合统计和有界结果预览,不包含模型内部思考、系统提示、原始 provider 异常或未授权范围的数据。`started` 事件用于显示正在执行的步骤;成功的 `completed` 事件除 `result_count`、`partial`、`channel`、扫描数量等聚合字段外,还可包含:
|
||||
|
||||
```json
|
||||
{
|
||||
"result_items": [],
|
||||
"result_items_truncated": false
|
||||
}
|
||||
```
|
||||
|
||||
`result_items` 最多 20 项。每项公共字段如下:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `kind` | `wiki_page` 或 `document_chunk` |
|
||||
| `knowledge_base_id` | 本地、已选择且已授权的 mapping ID |
|
||||
| `knowledge_base` | 知识库显示名称 |
|
||||
| `title` | Wiki 页面或原始文档标题 |
|
||||
|
||||
Wiki 项还可包含 `page_ref`、`slug` 和最长 1200 字符的 `summary`。Wiki 读取项可附带最长 8000 字符的 `content_markdown`、`content_offset`、`content_end`、`content_length` 和 `content_truncated`。原始文档项可包含 `chunk_id`、`knowledge_id`、最长 1600 字符的 `content`、`content_truncated`,以及可选的 `chunk_index`、`score`、`is_requested`。超过 20 项或投影时丢弃了不能安全展示的结果时,`result_items_truncated` 为 true。
|
||||
|
||||
进度投影只从本次工具已经授权的结果生成。它不会复制远端 WeKnora/provider knowledge base ID,也不会把 provider 返回的任意未知字段透传给浏览器。缺少本地 mapping ID、mapping 不在本轮选择范围、或 `page_ref` 解码后与本地 mapping/slug 不一致的项会被丢弃。右侧详情接口还会根据当前用户重新授权本地 mapping ID,`result_items` 中的标识不能替代权限校验。
|
||||
|
||||
## 9. 多轮推理约束
|
||||
|
||||
智能模式在系统提示中加入所选知识空间约束。模型把本轮选择的普通知识库作为具体事实的首要来源,不能用 Web、MCP、外部技能或子智能体扩大检索范围。模型原有的理解、归纳、写作和推理能力仍然可用,也可以用稳定通用知识解释背景;但必须明确区分库内事实、基于材料的推断和一般知识,不能靠模型记忆补造知识库专属事实。用户明确要求只依据知识库时,按更严格范围回答。
|
||||
模型可以在五个工具之间多次切换。每个新问题必须先调用 `ordinary_knowledge_search`,一次可提交 1 至 5 个查询改写;再结合原始命中调用 `ordinary_wiki_search`。有 Wiki 命中时,优先保留模型选择的相关页面;模型未选择页面时,中间件读取排序第一的页面,不再强制读取最多 5 个命中的全部页面。即使模型试图越序或直接回答,中间件也会补齐这个最低证据基线。完成后,多轮次数由证据是否覆盖问题决定,不固定为 N 次,也不要求每轮把所有工具都调用一遍。
|
||||
精确命中后可调用 ordinary_document_chunks 深读。Wiki 命中后只深读确实与问题有关的页面。已选择的长 Wiki 页面由中间件携带 `next_cursor` 连续读取到 `complete=true`;失败、畸形结果或无法继续的空游标作为终止尝试处理,避免无限循环。
|
||||
同一回合中,相同 raw query、正则 pattern、Wiki pattern、`page_ref+cursor` 和文档分页会被过滤;`chunk_id` 上下文读取按切片本身去重,不能通过无效的分页参数重复执行。一个批次同时包含旧查询和新查询时,只删除旧查询并继续执行新的扩展查询。模型调用前还会收到不展示给用户的已执行动作清单;如果模型仍只返回重复调用且没有正文,中间件会移除五个智能检索工具并以 `tool_choice=none` 做一次隐藏的最终合成重试,要求基于本轮已有证据完整回答。该重试不会再次进入检索循环;重试仍无正文时才返回证据不足的安全说明。
|
||||
Wiki 与原始文档冲突时,以本轮原始文档切片为权威证据。知识库专属事实证据不足时必须明确说明缺少什么;一般背景可以由模型能力补充,但不能伪装成知识库结论。
|
||||
|
||||
## 10. 历史证据清理
|
||||
|
||||
完整聊天记录和工具轨迹仍保存在 checkpoint 中。 前端历史展示和审计数据不会被删除。 在新问题发给模型前,SmartReasoningRetrievalMiddleware 只重写本次模型请求。
|
||||
它找到最近一个真实用户消息作为当前轮边界。 边界之前的五类智能检索 tool call 和对应 ToolMessage 不再发给模型。 旧工具正文会被固定提示替代,说明历史检索不构成本轮证据。
|
||||
当前用户轮中的工具调用和结果保持不变。 非智能检索工具调用保持不变。 这样每个新问题都重新检索,同时保留完整持久化历史。
|
||||
|
||||
## 11. 40 次模型调用上限
|
||||
|
||||
智能模式对每个真实用户回合设置最多 40 次 Lead Agent 模型调用。 计数从最近一个真实用户消息之后开始。 前 39 次允许模型继续检索或回答。
|
||||
第 40 次被保留为最终合成调用。 进入第 40 次时,五个检索工具从请求中移除。 请求强制 tool_choice 为 none。
|
||||
系统追加隐藏指令,要求以本轮已获取证据为事实基础立即回答,并允许用通用知识解释背景但不得补造知识库专属事实。 如果模型仍返回工具调用,Middleware 会剥离工具调用并结束图执行。 如果第 40 次没有可用回答,返回固定的证据不足提示。
|
||||
达到上限后不会发生第 41 次模型调用。 该限制只挂载在 smart_reasoning 模式。
|
||||
|
||||
## 12. 回答与引用
|
||||
|
||||
智能模式将 rag_mode_enabled 强制设为 false。 它不注入经典的 RAG_CITATION_PROMPT。 它不挂载 RagCitationMiddleware。
|
||||
因此最终正文不强制出现数字编号引用。 模型可以自然提及文档名或 Wiki 页面名。 模型应先覆盖用户问题,再说明资料冲突或证据缺口。
|
||||
工具仍返回结构化知识库名、文档名、page_ref、chunk_id 和读取范围。 完整工具轨迹仍进入现有运行记录。
|
||||
|
||||
## 13. 失败语义
|
||||
|
||||
Gateway 在模型启动前使用 HTTP 错误阻止无效请求。 主要错误如下:
|
||||
|
||||
| 场景 | HTTP | code |
|
||||
|---|---:|---|
|
||||
| mode 不是允许值 | 422 | INVALID_KNOWLEDGE_RETRIEVAL_MODE |
|
||||
| 管理员未开启智能模式 | 409 | SMART_REASONING_DISABLED |
|
||||
| 设置 Store 不可用 | 503 | KNOWLEDGE_RETRIEVAL_SETTINGS_UNAVAILABLE |
|
||||
| 混选或只选助手知识库 | 422 | SMART_REASONING_ORDINARY_ONLY |
|
||||
| 没有选择普通知识库 | 422 | SMART_REASONING_SCOPE_REQUIRED |
|
||||
| WeKnora runtime 不可用 | 503 | SMART_REASONING_PROVIDER_UNAVAILABLE |
|
||||
| mapping Store 不可用 | 503 | SMART_REASONING_STORE_UNAVAILABLE |
|
||||
| mapping 不存在、越权或未绑定 | 404 | KNOWLEDGE_SCOPE_NOT_FOUND |
|
||||
|
||||
相关 metadata 写入失败时,尚未启动的 run 被标记为失败。 模型不会在模式或范围没有持久化的情况下启动。 工具失败使用统一 JSON 结构。
|
||||
成功响应包含 ok 为 true、operation、result_count 和 results。 全部检索通道失败时返回 ok 为 false 和安全错误信息。 部分范围失败时返回 ok 为 true、partial 为 true 和 warnings。
|
||||
成功执行但零命中不是系统错误。 零命中返回 ok 为 true、result_count 为 0,交给模型改写或切换检索面。 无效正则返回 INVALID_REGEX。
|
||||
正则执行超时返回 REGEX_TIMEOUT。 跨范围 chunk、document 或页面读取返回通用的不可访问错误。 未归一化异常会转换为统一失败,不向模型返回 Python 堆栈。
|
||||
每个工具输出有 48000 字符上限。 超过上限时按完整结果项裁剪,并标记 output_truncated。
|
||||
|
||||
## 14. 经典模式兼容
|
||||
|
||||
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。已有线程始终从 metadata 恢复各自保存的模式。前端每次发送前执行以下校验:
|
||||
|
||||
- 全局开关已读取并开启
|
||||
- 知识库选择已加载完成
|
||||
- 至少选择一个普通知识库
|
||||
- 没有选择助手知识库
|
||||
|
||||
校验失败时显示明确提示并停止发送。 后端仍重复执行全部权威校验。 输入框可显示历史线程的智能模式状态,便于用户切回 classic。
|
||||
前端不会在开关关闭时把 smart_reasoning 静默降级成 classic。
|
||||
|
||||
智能检索执行期间,“智能检索过程”保持展开,连续显示首轮原始文档搜索、Wiki 搜索、相关页面读取和按需扩展深读。检索结束后自动收起,只保留轮数、原始文档命中数和 Wiki 命中数摘要;用户可以随时手动展开或再次收起。历史消息重放时也默认收起。
|
||||
|
||||
展开后,每个检索步骤在“查询结果”中显示可查看的结果项。带有本地 mapping ID 和稳定页面/文档标识的结果可以点击,并在当前聊天页面右侧打开现有 WeKnora 来源抽屉:Wiki 项定位到 Wiki 页面,原始项定位到对应文档或切片。打开抽屉不会改变聊天主路由。旧历史轨迹若没有本地 mapping ID、Wiki slug、chunk ID 或 document ID 等稳定标识,只显示不可点击的标题摘要,前端不会按标题猜测来源。
|
||||
|
||||
## 16. 主要实现文件
|
||||
|
||||
后端设置与迁移:
|
||||
|
||||
- packages/harness/deerflow/persistence/knowledge_retrieval_settings/
|
||||
- packages/harness/deerflow/persistence/migrations/versions/20261014_01_knowledge_retrieval_settings.py
|
||||
- app/gateway/routers/system_settings.py
|
||||
- app/gateway/deps.py
|
||||
|
||||
Gateway 范围与运行配置:
|
||||
|
||||
- app/gateway/llmwiki_rag.py
|
||||
- app/gateway/services.py
|
||||
|
||||
工具与 WeKnora 适配:
|
||||
|
||||
- packages/harness/deerflow/tools/builtins/llmwiki_reasoning_tools.py
|
||||
- packages/harness/deerflow/integrations/weknora/client.py
|
||||
- packages/harness/deerflow/tools/tools.py
|
||||
|
||||
模型提示与运行保护:
|
||||
|
||||
- packages/harness/deerflow/agents/lead_agent/agent.py
|
||||
- packages/harness/deerflow/agents/middlewares/smart_reasoning_retrieval_middleware.py
|
||||
|
||||
前端:
|
||||
|
||||
- frontend-web/src/components/workspace/settings/knowledge-retrieval-settings-page.tsx
|
||||
- frontend-web/src/components/workspace/settings/settings-dialog.tsx
|
||||
- frontend-web/src/components/workspace/input-box.tsx
|
||||
- frontend-web/src/core/threads/knowledge-retrieval-mode.ts
|
||||
- frontend-web/src/core/threads/hooks.ts
|
||||
- frontend-web/src/core/threads/smart-knowledge-retrieval.ts
|
||||
- frontend-web/src/components/workspace/messages/smart-knowledge-retrieval-trace.tsx
|
||||
- frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer.tsx
|
||||
- frontend-web/src/pages/ChatPage.tsx
|
||||
- frontend-web/src/pages/AgentChatPage.tsx
|
||||
|
||||
## 17. 测试
|
||||
|
||||
设置和权限测试:
|
||||
|
||||
- tests/test_knowledge_retrieval_settings.py
|
||||
- tests/test_llmwiki_smart_reasoning.py
|
||||
|
||||
检索工具测试覆盖:
|
||||
|
||||
- 多查询合并和 matched_queries
|
||||
- 原始搜索全部失败与部分失败
|
||||
- 安全正则和执行超时
|
||||
- provider grep 原生合同校验
|
||||
- 404 和 405 时的兼容回退
|
||||
- 多知识库公平扫描
|
||||
- 兼容回退标记和结构化告警
|
||||
- 文档和切片跨范围拒绝
|
||||
- Wiki 按 mapping 选择本地或远端通道
|
||||
- Wiki 全部失败和部分失败
|
||||
- Wiki 长页面续读和 cursor 绑定
|
||||
- `result_items` 只包含本轮已授权的本地 mapping ID
|
||||
- provider knowledge base ID 和未知 provider 字段不会进入进度流
|
||||
- Wiki `page_ref` 与 mapping/slug 不一致时拒绝投影
|
||||
- 结果项数量和预览长度有界,并正确标记截断
|
||||
|
||||
Middleware 测试:
|
||||
|
||||
- tests/test_smart_reasoning_retrieval_middleware.py
|
||||
- 历史智能检索 call/result 配对清理
|
||||
- 当前轮证据保留
|
||||
- 非智能工具保留
|
||||
- OpenAI 原始 tool_calls 兼容
|
||||
- 第 40 次强制合成
|
||||
- 禁止第 41 次模型调用
|
||||
- 首轮原始 RAG 在 Wiki 搜索前执行,原始命中标题可参与 Wiki pattern 生成
|
||||
- Wiki 多页命中只读取模型选择的相关页面,不强制遍历全部页面
|
||||
- 同一轮对查询、正则、页面游标与文档分页去重,混合批次保留新的扩展动作
|
||||
- 长 Wiki 页面按 `next_cursor` 续读完成后才进入后续扩展或回答
|
||||
- 失败或无后续游标时安全终止,不形成注入循环
|
||||
|
||||
前端模式测试:
|
||||
|
||||
- frontend-web/src/core/threads/knowledge-retrieval-mode.test.ts
|
||||
- 模式归一化
|
||||
- 每次请求显式携带 mode
|
||||
- 管理员开关、选库状态和助手库混选校验
|
||||
- 检索中保持展开、完成和历史轨迹默认收起,并支持手动开合
|
||||
- 新轨迹结果可点击打开右侧来源抽屉
|
||||
- 缺少稳定标识的旧历史结果保持不可点击
|
||||
|
||||
2026-10-18 调整后的中间件专项为 34 passed;相关四组后端测试合计 145 passed。相关 Python 文件通过 Ruff。
|
||||
本次前端模式、目录解析、进度匹配、抽屉定位与检索轨迹专项测试为 31 passed,pnpm build 通过。
|
||||
|
||||
## 18. 验收标准
|
||||
|
||||
以下条件同时满足时,本功能可验收:
|
||||
|
||||
1. 新数据库无设置记录时,智能模式默认关闭。
|
||||
2. 非管理员不能修改全局开关。
|
||||
3. 关闭开关时,伪造 smart_reasoning 请求被后端拒绝。
|
||||
4. classic 在开关关闭和开启时都可正常使用。
|
||||
5. 新会话始终默认 classic 且经典检索位于模式选择器上方;全局开关开启后用户可主动切换 smart_reasoning,历史线程始终按 metadata 恢复显示。
|
||||
6. smart_reasoning 请求必须显式携带模式。
|
||||
7. 智能模式没有普通库、混选助手库或越权时不能启动模型。
|
||||
8. 每个工具调用重新校验当前权限。
|
||||
9. 用户只直接提问时,每轮新问题都依次完成原始 RAG、Wiki 搜索和至少一个相关 Wiki 页面的完整读取,并可按证据缺口继续扩展。
|
||||
10. 原始正则在 provider 不支持 grep 时自动使用有界兼容回退。
|
||||
11. Wiki 搜索按 mapping 独立选择完整本地镜像或 provider。
|
||||
12. 长 Wiki 页面能用 next_cursor 连续读完。
|
||||
13. 全部通道失败与零命中具有不同语义。
|
||||
14. 新用户轮不会把旧轮智能检索正文重新当作证据。
|
||||
15. 单个用户回合最多执行 40 次 Lead Agent 模型调用。
|
||||
16. 智能模式最终回答不强制编号引用。
|
||||
17. 智能检索过程在执行时展开,结束和历史重放时默认收起,并允许手动展开或收起。
|
||||
18. 带本地授权 mapping ID 和稳定来源标识的新检索结果可点击,并在聊天页右侧抽屉查看。
|
||||
19. 缺少稳定来源标识的旧历史结果不可点击,前端不按标题猜测来源。
|
||||
20. 进度流不泄露 provider knowledge base ID、未授权结果、提示词或模型内部思考。
|
||||
21. 设置、模式和 Wiki 生成相关新增配置均由数据库持久化,不写运行时配置文件。
|
||||
22. 同一用户回合不会重复执行相同 query、正则、Wiki `page_ref+cursor` 或文档分页;没有证据缺口时不会为了展示流程继续检索。
|
||||
23. 知识库证据作为事实基础,但不会关闭模型原有的归纳、解释和写作能力;通用背景与库内事实保持可辨认。
|
||||
|
||||
## 19. 已知边界
|
||||
|
||||
切片正则兼容扫描是有界扫描,不承诺遍历无限大的全部语料。 响应中的 scan_truncated 会明确表示达到扫描上限。 模型可据此改写查询,优先使用向量或混合检索缩小范围,再做精确深读。
|
||||
Wiki 搜索对候选页面和单页匹配正文也有明确上限。 Wiki 页面正文读取支持 continuation,因此已命中的长页面可以完整读完。 page_ref 和 continuation cursor 是内部不透明引用。
|
||||
它们用于绑定页面与续读位置,不替代每次工具调用的权限校验。 本实现不改变原有 WeKnora 数据管理、经典检索和现有会话记录。
|
||||
旧会话仍可展示当时保存的检索步骤和标题,但早期工具结果若没有本地 mapping ID 或稳定来源标识,就无法安全构造详情目标,因此保持不可点击。新产生的进度事件最多投影 20 个结果项;完整工具结果和回答证据仍按既有运行记录保存。
|
||||
@@ -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。修改环境变量后需重启后台。
|
||||
@@ -15,6 +15,8 @@ This directory contains detailed documentation for the DeerFlow backend.
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [ORDINARY_KNOWLEDGE_SMART_REASONING_RETRIEVAL_ZH.md](ORDINARY_KNOWLEDGE_SMART_REASONING_RETRIEVAL_ZH.md) | 普通知识库智能推理检索:管理员数据库开关、原文与 Wiki 首轮双检索、WeKnora 式多轮深读、权限与验收设计 |
|
||||
| [WIKI_HIERARCHY_AND_FOCUS_GENERATION_ZH.md](WIKI_HIERARCHY_AND_FOCUS_GENERATION_ZH.md) | Wiki 创建时按目录层级生成、按重点要求生成及异步协调设计 |
|
||||
| [KNOWLEDGE_SPACE_RETRIEVAL_AND_WIKI_EXPANSION_ZH.md](KNOWLEDGE_SPACE_RETRIEVAL_AND_WIKI_EXPANSION_ZH.md) | 知识空间严格优先检索、混合召回、Wiki 图扩展与可编辑全库摘要实现方案 |
|
||||
| [STREAMING.md](STREAMING.md) | Token-level streaming design: Gateway vs DeerFlowClient paths, `stream_mode` semantics, per-id dedup |
|
||||
| [FILE_UPLOAD.md](FILE_UPLOAD.md) | File upload functionality |
|
||||
|
||||
@@ -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 项,
|
||||
未为其添加与本次补丁无关的兼容模块。
|
||||
@@ -0,0 +1,253 @@
|
||||
# Wiki 知识库参考目录与重点要求生成设计
|
||||
|
||||
> 状态:已实现,与当前代码一致
|
||||
> 更新日期:2026-10-19
|
||||
|
||||
## 1. 目标与语义边界
|
||||
|
||||
创建或编辑 Wiki 知识库时提供两个互相独立的选填项:
|
||||
|
||||
- **参考目录**:管理员或知识库所有者给出一至三级目录,帮助生成模型理解期望的信息组织方式。它不是目录白名单,也不是必须逐项套用的固定模板。
|
||||
- **重点要求**:用户描述允许抽取、组织和呈现的重点范围。系统把它作为严格的内容范围约束,并在生成完成后清理重点范围之外的自动生成页面。
|
||||
|
||||
模型可以依据原始资料新增、合并、拆分、改名、调整或省略参考目录。内容与参考目录冲突时,以资料实际内容和清晰表达为准。系统不得为了匹配参考目录预建空目录、强制移动页面、删除目录或删除参考目录之外的有效结构。
|
||||
|
||||
两项均可留空。全部留空时沿用 WeKnora 原有自动生成方式;原有 `content_instructions` 和 `extraction_instructions` 高级指令继续有效。本功能的权威配置全部保存在数据库,不写入 `config.yaml`、JSON 或其他运行时配置文件。
|
||||
|
||||
## 2. 用户交互
|
||||
|
||||
### 2.1 参考目录
|
||||
|
||||
前端支持两种输入格式,最多三级,不能混用。
|
||||
|
||||
Markdown 标题:
|
||||
|
||||
```markdown
|
||||
# 政策法规
|
||||
## 国内政策
|
||||
### 2026 年
|
||||
## 国际规则
|
||||
# 操作指南
|
||||
## 申报流程
|
||||
```
|
||||
|
||||
每级两个空格的缩进文本:
|
||||
|
||||
```text
|
||||
政策法规
|
||||
国内政策
|
||||
2026 年
|
||||
国际规则
|
||||
操作指南
|
||||
申报流程
|
||||
```
|
||||
|
||||
无缩进的普通逐行文本按一级参考目录处理。用户也可以下载示例文件,修改后导入 `.md` 或 `.txt` 文件。表单实时显示树形预览、节点数、规范化长度和逐行错误,并明确提示模型可以按实际资料调整结构。
|
||||
|
||||
### 2.2 重点要求
|
||||
|
||||
重点要求是最多 2000 个字符的自由文本,例如:
|
||||
|
||||
```text
|
||||
重点提取适用对象、申请条件、时间期限、审批部门和例外情形;
|
||||
涉及不同年份时优先说明最新规则,并保留历史差异。
|
||||
```
|
||||
|
||||
重点要求限制自动生成页面的内容范围,同时不允许模型降低事实依据要求或凭空补全资料。只填写重点、不填写参考目录时,系统保留 WeKnora 自动生成的目录结构,只过滤与重点无关的自动生成页面。
|
||||
|
||||
### 2.3 编辑语义
|
||||
|
||||
- 两项都支持创建后修改。
|
||||
- 清空参考目录只表示不再向后续生成提供该参考,不删除或移动已经存在的远端目录和页面。
|
||||
- 清空重点要求后,后续生成不再追加重点范围提示,也不再运行重点页面过滤。
|
||||
- 保存时只更新请求中明确出现的 Wiki 配置字段,未出现字段保持原值。
|
||||
|
||||
### 2.4 生成后的智能检索与查看
|
||||
|
||||
参考目录和重点要求只影响 Wiki 的生成与整理,不要求用户在问答时重复这些指令。管理员开启普通知识库智能检索、且用户在会话中选择该模式后,用户可以直接提问;运行时先检索原始文档分片,再根据原始命中检索 Wiki 并读取相关页面,之后只针对证据缺口扩展检索。Wiki 与原始文档冲突时以原文为准。最终回答不强制输出 `[1]`、`[2]` 等编号或编号参考文献列表。
|
||||
|
||||
智能检索过程在执行时展开,完成后默认收起。用户展开后可以点击带稳定来源标识的 Wiki 页面或原始文档结果,在聊天页右侧来源抽屉查看详情。详情读取仍按当前用户和本地 mapping 重新鉴权。
|
||||
|
||||
## 3. API 合同
|
||||
|
||||
创建接口:
|
||||
|
||||
```http
|
||||
POST /api/llmwiki/knowledge-bases
|
||||
```
|
||||
|
||||
编辑接口:
|
||||
|
||||
```http
|
||||
PATCH /api/llmwiki/knowledge-bases/{mapping_id}
|
||||
```
|
||||
|
||||
两者的 `wiki_config` 使用同一结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"content_instructions": "原有内容生成要求,可选",
|
||||
"extraction_instructions": "原有信息抽取要求,可选",
|
||||
"directory_paths": [
|
||||
["政策法规"],
|
||||
["政策法规", "国内政策"],
|
||||
["政策法规", "国内政策", "2026 年"]
|
||||
],
|
||||
"focus_instructions": "重点说明适用对象和截止时间"
|
||||
}
|
||||
```
|
||||
|
||||
`parent_directories` 作为旧版一级目录兼容字段继续接受。无论使用 `directory_paths` 还是 `parent_directories`,目录都只是生成提示中的参考结构,不能触发远端目录预建或生成后目录整理。若同一请求同时提供两个字段,两者的根目录必须完全一致;新客户端统一使用 `directory_paths`。
|
||||
|
||||
知识库详情响应中的 `wiki_config` 返回:
|
||||
|
||||
- 原始内容指令、原始抽取指令;
|
||||
- `directory_paths`、兼容的 `parent_directories`;
|
||||
- `focus_instructions`;
|
||||
- 配置 schema、revision 和应用状态;
|
||||
- 参考目录提示写入 WeKnora 指令的覆盖状态及节点计数。
|
||||
|
||||
## 4. 参考目录规范化与校验
|
||||
|
||||
前后端执行一致的基础校验,后端是最终安全边界:
|
||||
|
||||
| 规则 | 限制 |
|
||||
|---|---:|
|
||||
| 最大深度 | 3 级 |
|
||||
| 最大节点数 | 100 |
|
||||
| 单个目录名 | 100 个字符 |
|
||||
| 规范化目录总长度 | 6000 个字符 |
|
||||
| 重点要求 | 2000 个字符 |
|
||||
|
||||
目录名称先执行 Unicode NFKC 规范化和首尾空白清理,并拒绝空名称、`.`、`..`、路径分隔字符、控制字符、同一父目录下的等价重复名称、跨级跳转以及非法缩进。后端将缺失的父路径自动补齐,并按父节点先于子节点的稳定顺序保存完整路径集合。
|
||||
|
||||
这些规则只保证参考数据能够稳定存储并安全写入提示词,不把参考目录变成业务上的硬约束。
|
||||
|
||||
## 5. 有效提示编译
|
||||
|
||||
内容生成指令依次合并:
|
||||
|
||||
1. 用户原始 `content_instructions`;
|
||||
2. 重点要求及“只生成直接相关且有资料支撑页面”的严格内容范围;
|
||||
3. 参考目录树及“可以按资料新增、合并、拆分、改名、调整或省略目录”的软性组织指引。
|
||||
|
||||
信息抽取指令依次合并:
|
||||
|
||||
1. 用户原始 `extraction_instructions`;
|
||||
2. 重点要求及“只抽取直接相关、可追溯事实”的严格内容范围。
|
||||
|
||||
原始指令和重点要求不会被静默截断。基础内容超过 4000 字符时返回 422。参考目录提示按父子顺序装入剩余预算;无法放入全部节点时,详情响应返回 `partial` 覆盖和实际节点数。未写入的节点仍保存在数据库中,但不执行任何本地目录补建或强制归档。
|
||||
|
||||
## 6. 生成与生成后处理
|
||||
|
||||
### 6.1 创建和编辑时不操作目录
|
||||
|
||||
创建或编辑知识库时,Gateway 只把编译后的提示传给 WeKnora,并把原始配置保存到本地数据库。无论输入来自 `directory_paths` 还是兼容字段 `parent_directories`,都不得:
|
||||
|
||||
- 预建参考目录骨架;
|
||||
- 因参考目录变更而移动 Wiki 页面或文件夹;
|
||||
- 删除参考目录之外的文件夹、目录页或普通页面;
|
||||
- 周期性扫描并强制恢复参考目录。
|
||||
|
||||
WeKnora 生成出的目录结构就是当前资料对应的有效结构,可在后续生成中自然调整。
|
||||
|
||||
### 6.2 重点范围过滤
|
||||
|
||||
只有非空的重点要求需要生成后协调。WeKnora 的知识库级 `is_processing` 可能早于 Wiki 生成结束变为 `false`,因此协调器还会检查每篇文档的 `pending_subtasks_count` 和 finalizing/processing 状态;确认全部结束后再执行重点判定。
|
||||
|
||||
重点判定遵循以下规则:
|
||||
|
||||
- 页面主体直接提供重点所需的事实、指标、事件、关系、责任或实施细节时保留;
|
||||
- 只作为背景出现、与重点无关的人物、机构、地点或主题自动生成页面可以删除;
|
||||
- 人工维护页不因重点过滤被删除;
|
||||
- 判定按有界批次执行,漏项会补判,不能把“没有返回”解释成删除;
|
||||
- 肯定决定可按配置签名和正文指纹写入页面元数据,供相同内容重跑复用;
|
||||
- 所有判定完成前不做删除;判定缺项、格式非法、标记写入失败或模型不可用时,本轮不做破坏性变更;
|
||||
- 重点过滤始终保留 WeKnora 已生成的目录结构,不移动页面,不删除文件夹或目录页。
|
||||
|
||||
参考目录本身不触发 mutation-settle 或周期协调;只有重点过滤和本地 Wiki 索引同步按各自条件调度。
|
||||
|
||||
## 7. 数据库存储
|
||||
|
||||
迁移 `20261014_02_llmwiki_generation_config.py` 在 `llmwiki_knowledge_bases` 增加:
|
||||
|
||||
| 字段 | 用途 |
|
||||
|---|---|
|
||||
| `wiki_directory_paths` | 完整一至三级参考目录路径数组 |
|
||||
| `wiki_focus_instructions` | 原始重点要求 |
|
||||
| `wiki_content_instructions_raw` | 原始内容生成要求 |
|
||||
| `wiki_extraction_instructions_raw` | 原始信息抽取要求 |
|
||||
| `wiki_generation_managed` | 是否由当前 Wiki 配置流程管理 |
|
||||
| `wiki_config_schema_version` | 当前 schema 版本;参考目录语义为版本 3 |
|
||||
| `wiki_config_revision` | 每次有效配置变更递增 |
|
||||
| `wiki_config_apply_status` | `active` 或 `not_applicable` |
|
||||
| `wiki_config_apply_error` | 保留的应用错误字段 |
|
||||
|
||||
旧字段 `wiki_parent_directories` 继续保存根目录投影,兼容既有数据和旧客户端,但不保留旧的强制目录行为。SQL、内存存储与映射响应使用相同字段语义。上述生成选项、原始指令、应用状态和 revision 均以数据库记录为权威来源;前端本地状态只用于未提交表单。
|
||||
|
||||
后台协调器会幂等升级版本 2 的受管 Wiki:从数据库保存的原始指令、重点要求和参考目录重新编译有效提示,远端更新成功后再写入版本 3 并增加 revision;失败时保留版本 2、记录错误并稍后重试。未知版本或缺少原始配置的旧记录不会被自动覆盖。
|
||||
|
||||
页面级重点保留判定存放在 WeKnora 的数据库页元数据中,不使用本地文件。它只缓存某一重点版本和正文指纹下的肯定决定,不替代管理员原始配置。
|
||||
|
||||
## 8. 一致性和失败处理
|
||||
|
||||
### 8.1 创建
|
||||
|
||||
创建顺序为:校验与编译、创建 WeKnora 知识库、写入本地映射。参考目录不增加远端目录初始化步骤。WeKnora 创建成功但数据库写入失败时,系统尝试删除新建的远端知识库,避免留下无本地映射的孤儿库。
|
||||
|
||||
### 8.2 编辑
|
||||
|
||||
编辑前读取当前远端配置和数据库原始配置:
|
||||
|
||||
- 参考目录或指令校验失败:不修改远端和数据库;
|
||||
- WeKnora 更新失败:数据库保持原值;
|
||||
- WeKnora 更新成功但数据库提交失败:尝试恢复 WeKnora 之前的名称、描述和两段有效指令,然后把错误返回调用方。
|
||||
|
||||
目录参考的保存失败不会留下预建目录,因为该流程不调用远端目录创建接口。
|
||||
|
||||
### 8.3 权限
|
||||
|
||||
沿用知识库原有授权边界:只有知识库所有者或管理员可创建和修改配置;公开浏览接口只返回调用者有权查看的内容。
|
||||
|
||||
## 9. 主要实现位置
|
||||
|
||||
后端:
|
||||
|
||||
- `app/gateway/wiki_generation_config.py`:重点规范化、参考目录提示与有效指令编译;
|
||||
- `app/gateway/wiki_directory_constraints.py`:输入规范化和重点范围过滤,不负责强制目录整理;
|
||||
- `app/gateway/routers/llmwiki.py`:创建、编辑、响应投影和补偿事务;
|
||||
- `app/gateway/llmwiki_index_scheduler.py`:重点过滤与本地 Wiki 索引的异步协调;
|
||||
- `packages/harness/deerflow/persistence/llmwiki/`:数据库和内存存储;
|
||||
- `packages/harness/deerflow/persistence/migrations/versions/20261014_02_llmwiki_generation_config.py`:数据库迁移。
|
||||
|
||||
前端:
|
||||
|
||||
- `src/core/llmwiki/directory-tree.ts`:参考目录文本解析、校验和格式化;
|
||||
- `src/strategy-components/api/llmwiki.ts`:API 类型和请求;
|
||||
- `src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeWorkspace.tsx`:创建、编辑、示例下载、文件导入和树预览。
|
||||
|
||||
## 10. 测试与验收
|
||||
|
||||
自动化至少覆盖:
|
||||
|
||||
- Markdown、缩进文本、路径规范化、重复和越界校验;
|
||||
- 重点指令、参考目录和原始高级指令的合并、4000 字符预算和目录提示覆盖计数;
|
||||
- 创建与编辑会持久化参考目录,但不调用远端文件夹创建、页面移动或目录删除接口;
|
||||
- 新字段和兼容字段都不会启动目录协调;
|
||||
- 重点页面过滤、失败前不破坏及幂等重跑;
|
||||
- 编辑时远端失败不写本地,数据库失败恢复远端指令;
|
||||
- SQL/内存存储字段往返、迁移单头和迁移兼容性;
|
||||
- 前端参考目录解析、示例文件和生产构建。
|
||||
|
||||
验收时至少使用下列组合:
|
||||
|
||||
1. 两项均为空,行为与原有 Wiki 创建一致;
|
||||
2. 只给参考目录,模型可以采用、补充、合并或调整目录,系统不创建固定骨架,也不移动或删除生成结构;
|
||||
3. 只给重点要求,只保留重点范围内且有来源依据的自动生成页面,同时目录保持不变;
|
||||
4. 两项同时填写,参考目录提供组织方向,重点范围独立生效;
|
||||
5. 使用兼容字段 `parent_directories` 创建或编辑,仍然只有参考语义;
|
||||
6. 编辑后 revision 增加,刷新页面仍从数据库恢复原始配置;
|
||||
7. 清空参考目录后不再向后续生成提供参考,已有目录和内容不被删除或移动;
|
||||
8. 重启服务或更换浏览器后,参考目录、重点要求、原始指令和 revision 仍从数据库恢复;
|
||||
9. 生成后的 Wiki 页面可进入智能检索,并可在右侧抽屉查看来源详情;
|
||||
10. 文档 Wiki 子任务尚未结束时不提前运行重点过滤,全部结束后才清理重点范围外的自动生成页面。
|
||||
@@ -370,7 +370,8 @@ client.get_memory()
|
||||
| POST | `/api/threads/{id}/runs/{rid}/cancel` | 取消运行 |
|
||||
| GET | `/api/threads/{id}/runs/{rid}/join` | 加入 SSE 流 |
|
||||
| GET | `/api/threads/{id}/runs/{rid}/messages` | 分页获取消息 |
|
||||
| GET | `/api/threads/{id}/messages` | 线程所有消息(含反馈) |
|
||||
| GET | `/api/threads/{id}/messages` | 从最新 checkpoint 读取按时间正序的完整消息字段 |
|
||||
| GET | `/api/threads/{id}/run-messages` | 跨运行读取 event-store 展示消息(含反馈) |
|
||||
|
||||
### 8.3 无状态运行
|
||||
|
||||
|
||||
@@ -2,6 +2,25 @@
|
||||
|
||||
DeerFlow includes automatic conversation summarization to handle long conversations that approach model token limits. When enabled, the system automatically condenses older messages while preserving recent context.
|
||||
|
||||
## Administrator recent-three-turn window
|
||||
|
||||
Administrators can enable a simpler deployment-wide sliding window in
|
||||
「设置 → 上下文管理」. It is stored as
|
||||
`system_settings.json -> conversation_context.recent_turn_window_enabled`, is
|
||||
disabled by default, and is exposed through
|
||||
`GET/PUT /api/system-settings/conversation-context` (write access is admin-only).
|
||||
|
||||
When enabled, every lead-agent model call receives only the latest three real
|
||||
user turns, counting the current request, plus the assistant messages and tool
|
||||
results that belong to those turns. Hidden business context immediately attached
|
||||
to the first retained turn and system instructions are preserved. Trimming is
|
||||
performed on `ModelRequest` only, so checkpoint history and the transcript shown
|
||||
to the user remain complete.
|
||||
|
||||
This mode takes priority over the per-chat 「上下文压缩」 option and does not run
|
||||
the summarization model. Turning it off restores the existing summarization
|
||||
behavior without losing earlier messages.
|
||||
|
||||
## Hard input limit (always-on safety net)
|
||||
|
||||
Summarization is a quality feature and may be disabled by the user. A model can
|
||||
|
||||
Reference in new issue
Block a user