sync: 鍚屾灞€鍩熺綉鏈€鏂颁唬鐮?(source master @ b69568e4)

This commit is contained in:
gozeros committed 2026-10-21 14:51:31 +08:00
1 parent 4f7f07504a
commit 8bc8b0e082
1651 files changed
+278599 -11416

No files matched your search

@@ -0,0 +1,161 @@
# 智能体回答评价与用户反馈评分规范
## 1. 定位
本功能采集用户对**单条智能体回答**的直接评价,并聚合为智能体的“用户评价分”。
它回答的是:
> 用户是否认可这个智能体已经给出的回答,以及负面体验主要集中在哪些方面?
它不等同于完整的智能体能力评测。工具选择、Skill 增益、执行轨迹、事实基准、成本、稳定性和多智能体协作仍需独立的离线评测或运行指标,不能用本分数替代。
## 2. 单条回答的评价入口
每条可评价的最终 AI 回答提供两种选择:
- `回答优秀`:记为正向评价 `rating = 1`。
- `回答不佳`:记为负向评价 `rating = -1`,且必须至少选择一个原因。
负向原因采用固定枚举:
| 枚举 | 展示文案 | 统计归属 |
|---|---|---|
| `incorrect_or_incomplete` | 不正确或不完整 | 正确性、完整性、证据支撑 |
| `unmet_expectations` | 与期望不符 | 相关性、可执行性 |
| `slow_or_problematic` | 速度慢或存在问题 | 性能/可靠性问题分布 |
| `style_or_tone` | 风格或语气 | 表达体验问题分布 |
| `safety_or_legal` | 安全或法律疑虑 | 安全合规问题分布 |
| `other` | 其他 | 其他问题分布 |
用户可以补充自由文本详情。正向评价不携带负向原因;同一用户对同一条回答重复提交时更新原评价,不重复计数。
回答所属智能体必须由服务端根据 `run`、线程和消息记录确定,不能接受前端自行指定的归属。
## 3. 汇总指标
设:
- `P`:正向评价数;
- `Q`:负向评价数;
- `N = P + Q`:有效评价样本数;
- 贝叶斯先验:`alpha = 8`、`beta = 2`,即 10 个等效样本、80 分的中性偏正先验。
### 3.1 原始任务成功率
当 `N > 0` 时:
```text
task_success_rate = 100 × P / N
```
这是没有平滑的真实好评率,必须与样本数一起展示。
### 3.2 用户评价分
当 `N > 0` 时:
```text
score = 100 × (P + alpha) / (N + alpha + beta)
= 100 × (P + 8) / (N + 10)
```
该分数使用贝叶斯平滑,避免只有一两条反馈时直接出现 0 分或 100 分。`N = 0` 时返回 `null`,界面展示“暂无评价”,不得把先验 80 分当作真实成绩。
### 3.3 置信度
```text
confidence = N / (N + 10)
```
置信度只表示样本量相对先验的充分程度,不表示回答事实正确的概率。
### 3.4 分数档位
| 分数 | 档位 | 含义 |
|---:|---|---|
| `>= 90` | `excellent` | 优秀 |
| `>= 80` | `good` | 良好 |
| `>= 70` | `needs_attention` | 需要关注 |
| `< 70` | `poor` | 较差 |
| 无样本 | `insufficient_data` | 数据不足 |
## 4. 回答质量五维诊断
回答质量采用以下权重:
| 维度 | 权重 |
|---|---:|
| 正确性 `correctness` | 30% |
| 完整性 `completeness` | 20% |
| 证据支撑 `evidence_grounding` | 20% |
| 相关性 `relevance` | 15% |
| 可执行性 `actionability` | 15% |
对任一维度 `d`:
- 每条正向评价都是该维度的一条正向证据,记为 `S_d = P`;
- 只有映射到该维度的负向原因才构成该维度的负向证据,记为 `F_d`;
- `E_d = S_d + F_d` 为该维度证据数。
当 `E_d > 0` 时:
```text
dimension_score_d = 100 × (S_d + 8) / (E_d + 10)
```
没有证据时维度分返回 `null`。不能因为用户选择了“风格或语气”,就虚构一次“正确性”失败。
五维任务质量分按有证据的维度加权,并对可用权重重新归一化:
```text
task_score = Σ(weight_d × dimension_score_d) / Σ(weight_d for available dimensions)
```
性能、表达、安全和“其他”原因保持为独立问题分布,不混入五维任务质量分,但仍会影响总的用户评价分。
## 5. 展示要求
智能体详情至少同时展示:
- 用户评价分和档位;
- 有效样本数;
- 原始好评率;
- 置信度或明确的低样本提示;
- 五维质量诊断;
- 各类负向原因数量。
禁止只展示一个没有样本数和解释的总分。前端文案必须使用“用户评价分”或“回答评价”,不得命名为“智能体综合能力分”。
## 6. API 约定
消息级接口:
```text
GET /api/threads/{thread_id}/messages/{message_id}/feedback-context
PUT /api/threads/{thread_id}/messages/{message_id}/feedback
DELETE /api/threads/{thread_id}/messages/{message_id}/feedback
```
智能体聚合接口:
```text
GET /api/agents/{agent_id}/feedback-score
```
该聚合接口仅允许 `system_role=admin` 的管理员访问;正常鉴权模式下未登录返回 401、普通用户返回 403,关闭鉴权的部署沿用项目统一放行语义。消息级评价接口仍按线程所有权和当前用户隔离。
原有运行级反馈接口继续兼容,但新界面应优先使用消息级接口,避免前端猜测 `run_id`。
## 7. 后续完整评测体系
用户评价分适合生产环境的持续反馈闭环。上线准入和版本对比还应逐步补充:
- 规则校验与 Ground Truth;
- 正确性、引用和幻觉率测试集;
- Skill/Tool 选择准确率与调用成功率;
- 轨迹效率、异常恢复率和无效步骤率;
- Sub-agent 委派、信息保留和交接质量;
- Token、延迟、成本、稳定性与回归通过率。
完整评测应按任务类型和智能体版本保存测试集、运行轨迹及评分,不应把在线赞踩直接解释为上述工程指标。
@@ -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 分钟锁定),与本地密码登录共用同一限速器。
### 响应
@@ -0,0 +1,92 @@
# 商业情报监测接口、技能与业务链说明
## 组成
本功能全部使用项目级自定义资源:
- 自定义任务详情技能:`fs-task-detail-info`
- 自定义接口技能:`fs-timeline-info`、`fs-insight-info`、`fs-target-info`、`fs-connection-info`
- 当前判断独立接口技能:`fs-current-judgment-info`
- 自定义摘要技能:`fs-brief-info`
- 自定义编排技能:`fs-collect`
- 自定义业务链:`biz-intel-chain`
- 自定义页面助理:`biz-detail-assistant`,显示名“商业QB助理”
这些资源不写入内置技能或内置智能体目录。接口技能脚本第一行均为对应生产接口地址。
## 本地模拟接口
```powershell
cd F:\react01\deeflow-code\deerflow-server\offline-backend-20260512\backend
./devtools/mock_monitor_api/start-mock.ps1
```
新开一个 PowerShell,设置:
```powershell
$env:MONITOR_API_BASE_URL = "http://127.0.0.1:18080/consumer"
$env:MONITOR_AUTHORIZATION = "admin"
```
若要让定时业务链也访问模拟接口,需要在**启动 DeerFlow Gateway 的同一环境**中设置以上
两个环境变量,并保持模拟服务运行;只在另一个终端设置不会改变已经启动的 Gateway 进程。
模拟接口校验 `Authorization: admin`,提供任务详情、时间节点、热点/威胁/战机、实体及
打击/塑势衔接任务。任务名作为严格关键词时先返回空数组,用来验证技能会逐步扩大时间、
移除关键词并最终命中。
## 采集命令
只采集和转换,不写入:
```powershell
python skills/custom/fs-collect/scripts/fs_collect.py `
--group source --task-id 3 --dry-run
```
采集并原子合并到态势详情:
```powershell
python skills/custom/fs-collect/scripts/fs_collect.py `
--group source --task-id 003
```
当前判断写入后生成情报摘要:
```powershell
python skills/custom/fs-collect/scripts/fs_collect.py `
--group judgment --task-id 003
python skills/custom/fs-collect/scripts/fs_collect.py `
--group summary --task-id 003
```
脚本逐次输出接口方法、地址、Authorization 状态、查询参数或请求体、原始返回和放宽原因;
最后一行输出机器可读 JSON。
## 关键字段
- 时间节点:页面字段外保留 `recUuid` 和全部原始时间字段。
- 三类事件:保留接口 `id`,并复制为 `eventId` 供实体查询和追溯。
- 当前判断:独立调用 `eventMonitorByTime`,固定 `discriminant=威胁`,保留 `id/eventId`。
- 重要目标:保留 `eid/etype/text/imagePath/num/label/reason/score`。
- 衔接任务:标识字段是 `taskId`,不生成 `id`;类型只有 `打击/塑势`。
- 情报摘要:`sourceRefs` 保留所依据的事件 id、eid、recUuid、taskId 和判断 id。
## 自定义智能体与业务链注册
Gateway 启动后执行:
```powershell
python scripts/setup_force_situation_chain.py
python scripts/setup_biz_detail_assistant.py
```
业务链安装脚本默认只更新配置,不立即触发真实接口。需要立即执行时显式追加
`--trigger`。链路阶段为:接口数据采集 → 当前判断威胁接口采集 → 情报摘要与校验。
## 测试
```powershell
python -m pytest tests/test_force_situation_monitor_skills.py -q
```
@@ -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
@@ -123,6 +131,27 @@ models:
If your OpenRouter key lives in a different environment variable name, point `api_key` at that variable explicitly (for example `api_key: $OPENROUTER_API_KEY`).
**vLLM (per-model, do not change other entries)**:
`use` is per model. SiliconFlow / DeepSeek entries should stay on `langchain_openai:ChatOpenAI`. For a self-hosted vLLM reasoning model (thinking may arrive as `reasoning` on newer vLLM, or `reasoning_content` on older builds), set only that one entry to `VllmChatModel`:
```yaml
models:
- name: qwen3.6
display_name: qwen3.6
use: deerflow.models.vllm_provider:VllmChatModel
model: qwen3.6
api_key: $VLLM_API_KEY
base_url: http://127.0.0.1:8001/v1
supports_thinking: true
when_thinking_enabled:
extra_body:
chat_template_kwargs:
enable_thinking: true
```
The default `langchain_openai:ChatOpenAI` path also maps both thinking field names onto the UI's `reasoning_content`. Use `VllmChatModel` when follow-up turns must echo vLLM's original `reasoning` field.
**Thinking Models**:
Some models support "thinking" mode for complex reasoning:
@@ -270,21 +299,21 @@ skills:
# Host path (optional, default: ../skills)
path: /custom/path/to/skills
# Container mount path (default: /mnt/skills)
container_path: /mnt/skills
# Optional system-prompt routing rule for the es_query skill
es_query_routing:
enabled: true
prompt: |
当用户的问题需要基于 Elasticsearch Query DSL 灵活构造、调整或执行查询时,优先使用 `es_query` 技能。
执行前先读取该技能的 SKILL.md,并严格遵循其中的流程和参数。
```
`skills.es_query_routing.enabled` controls whether the routing rule is injected
into the overall agent system prompt. Edit `prompt` to tune the matching boundary
or invocation requirements. When disabled (the schema default), the block is not
injected. Custom agents receive it only when their explicit skill allowlist
contains `es_query`.
# Container mount path (default: /mnt/skills)
container_path: /mnt/skills
# Optional system-prompt routing rule for the es_query skill
es_query_routing:
enabled: true
prompt: |
当用户的问题需要基于 Elasticsearch Query DSL 灵活构造、调整或执行查询时,优先使用 `es_query` 技能。
执行前先读取该技能的 SKILL.md,并严格遵循其中的流程和参数。
```
`skills.es_query_routing.enabled` controls whether the routing rule is injected
into the overall agent system prompt. Edit `prompt` to tune the matching boundary
or invocation requirements. When disabled (the schema default), the block is not
injected. Custom agents receive it only when their explicit skill allowlist
contains `es_query`.
**How Skills Work**:
- Skills are stored in `deer-flow/skills/{public,custom}/`
@@ -6,11 +6,14 @@ DeerFlow 后端提供了完整的文件上传功能,支持多文件上传,
## 功能特性
- ✅ 支持多文件同时上传
- ✅ 可选地转换文档为 Markdown(PDF、PPT、Excel、Word)
- ✅ 文件存储在线程隔离的目录中
- ✅ Agent 自动感知已上传的文件
- ✅ 支持文件列表查询和删除
- ✅ 支持多文件同时上传,默认单条消息最多 20 个
- ✅ 上传策略持久化到数据库,管理员可在「设置 → 上传设置」实时调整
- ✅ 按文件类别设置格式与单文件上限,并校验扩展名和文件真实签名
- ✅ 可选地转换文档为 Markdown(PDF、PPT、Excel、Word)
- ✅ 文件存储在线程隔离的目录中
- ✅ Agent 自动感知已上传的文件
- ✅ 大文件采用流式统计和头部/中段/尾部采样,不把全文直接塞入模型上下文
- ✅ 支持文件列表查询和删除
## API 端点
@@ -22,7 +25,15 @@ POST /api/threads/{thread_id}/uploads
**请求体:** `multipart/form-data`
- `files`: 一个或多个文件
网关会在应用层限制上传规模,默认最多 10 个文件、单文件 50 MiB、单次请求总计 100 MiB。可通过 `config.yaml` 的 `uploads.max_files`、`uploads.max_file_size`、`uploads.max_total_size` 调整;前端会读取同一组限制并在选择文件时提示,超过限制时后端返回 `413 Payload Too Large`。
网关每次上传都会读取数据库中的全局上传策略;前端也读取同一策略并在选择文件时预校验。内置默认值如下:
| 类别 | 默认格式 | 默认单文件上限 |
|---|---|---:|
| 文档/文本 | `.txt`、`.md`、`.doc`、`.docx`、`.ppt`、`.pptx`、`.pdf` | 10 MiB |
| 图片 | `.jpg`、`.jpeg`、`.png` | 10 MiB |
| 表格 | `.xls`、`.xlsx`、`.csv` | 5 MiB |
单条消息默认最多 20 个文件、附件合计默认不超过 100 MiB。管理员可在「设置 → 上传设置」调整数量、总量、各类别格式和大小,以及大文件采样阈值与输出字符预算。服务端始终执行最终校验;超过限制返回 `413 Payload Too Large`,不支持或内容签名不匹配返回 `415 Unsupported Media Type`。整批文件先写入暂存目录并全部验证,任一文件失败时不会留下半批结果。
**响应:**
```json
@@ -35,10 +46,10 @@ POST /api/threads/{thread_id}/uploads
"path": ".deer-flow/threads/{thread_id}/user-data/uploads/document.pdf",
"virtual_path": "/mnt/user-data/uploads/document.pdf",
"artifact_url": "/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/document.pdf",
"markdown_file": "document.md",
"markdown_path": ".deer-flow/threads/{thread_id}/user-data/uploads/document.md",
"markdown_virtual_path": "/mnt/user-data/uploads/document.md",
"markdown_artifact_url": "/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/document.md"
"markdown_file": "document.pdf.md",
"markdown_path": ".deer-flow/threads/{thread_id}/user-data/uploads/document.pdf.md",
"markdown_virtual_path": "/mnt/user-data/uploads/document.pdf.md",
"markdown_artifact_url": "/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/document.pdf.md"
}
],
"message": "Successfully uploaded 1 file(s)"
@@ -50,23 +61,62 @@ POST /api/threads/{thread_id}/uploads
- `virtual_path`: Agent 在沙箱中使用的虚拟路径
- `artifact_url`: 前端通过 HTTP 访问文件的 URL
### 2. 查询上传限制
```
GET /api/threads/{thread_id}/uploads/limits
```
返回网关当前生效的上传限制,供前端在用户选择文件前提示和拦截。
### 2. 查询和管理上传策略
所有已登录用户都可读取当前策略:
```
GET /api/upload-policy
```
管理员原子替换或恢复默认策略:
```
PUT /api/upload-policy
POST /api/upload-policy/reset
```
策略首次启动时会将内置默认值写入数据库表 `upload_policy`,后续修改带递增 `revision` 和更新人/更新时间审计信息。各 Gateway worker 在读取或修改策略时同步刷新 harness 运行时副本。
兼容旧前端的线程级限制查询仍保留:
```
GET /api/threads/{thread_id}/uploads/limits
```
返回网关当前生效的分类限制、总量限制、大文件策略和策略版本。
**响应:**
```json
{
"max_files": 10,
"max_file_size": 52428800,
"max_total_size": 104857600
}
```
### 3. 列出已上传文件
"max_files": 20,
"max_file_size": 10485760,
"max_total_size": 104857600,
"max_files_per_message": 20,
"max_total_bytes": 104857600,
"groups": {
"document": {"max_bytes": 10485760, "extensions": [".txt", ".md", ".doc", ".docx", ".ppt", ".pptx", ".pdf"]},
"image": {"max_bytes": 10485760, "extensions": [".jpg", ".jpeg", ".png"]},
"spreadsheet": {"max_bytes": 5242880, "extensions": [".xls", ".xlsx", ".csv"]}
},
"large_file": {
"threshold_bytes": 1048576,
"max_result_chars": 12000,
"hard_max_result_chars": 32000
},
"revision": 1
}
```
### 3. 有界文本预览
```
GET /api/threads/{thread_id}/artifact-preview?path=/mnt/user-data/uploads/example.txt&max_chars=12000
```
小文件返回有界完整文本;大文件返回流式统计、开头/中段/结尾的确定性分块采样、截断标记和警告。浏览器预览不会调用无界的 `response.text()`。
### 4. 列出已上传文件
```
GET /api/threads/{thread_id}/uploads/list
```
@@ -89,7 +139,7 @@ GET /api/threads/{thread_id}/uploads/list
}
```
### 4. 删除文件
### 5. 删除文件
```
DELETE /api/threads/{thread_id}/uploads/{filename}
```
@@ -111,7 +161,7 @@ DELETE /api/threads/{thread_id}/uploads/{filename}
- Word (`.doc`, `.docx`)
- OpenDocument (`.odt`, `.ods`, `.odp`)
转换后的 Markdown 文件会保存在同一目录下,文件名为原文件名 + `.md` 扩展名。
转换后的 Markdown 文件会保存在同一目录下,文件名为完整原文件名再追加 `.md`,例如 `report.docx.md`。这样 `report.docx` 和 `report.pdf` 不会同时覆盖同一个 `report.md`。前端预览仍兼容升级前的旧侧车名称。
通用部署可以关闭自动转换,以避免在网关主机上对不受信任的 Office/PDF 上传执行解析。本仓库当前 `config.yaml` 和纯 DeerFlow 离线部署包默认设置为 `uploads.auto_convert_documents: true`,用于受信任的内网部署。
@@ -121,19 +171,13 @@ DELETE /api/threads/{thread_id}/uploads/{filename}
Agent 在每次请求时会自动收到已上传文件的列表,格式如下:
```xml
<uploaded_files>
The following files have been uploaded and are available for use:
- document.pdf (1.2 MB)
Path: /mnt/user-data/uploads/document.pdf
- document.md (45.3 KB)
Path: /mnt/user-data/uploads/document.md
You can read these files using the `read_file` tool with the paths shown above.
</uploaded_files>
```
```text
<uploaded_files>
{经过 JSON 转义和字符预算裁剪的文件元数据、统计与采样摘要}
</uploaded_files>
```
当前轮文件可附带短摘要;历史轮次只注入元数据;自动生成的 Markdown 侧车不会作为第二个独立附件重复注入。整个 `<uploaded_files>` 块受数据库策略中的常规/硬字符预算约束。
### 使用上传的文件
@@ -144,8 +188,10 @@ Agent 在沙箱中运行,使用虚拟路径访问文件。Agent 可以直接
read_file(path="/mnt/user-data/uploads/document.pdf")
# 读取转换后的 Markdown(推荐)
read_file(path="/mnt/user-data/uploads/document.md")
```
read_file(path="/mnt/user-data/uploads/document.pdf.md")
```
当文本达到大文件阈值时,`read_file` 不会一次性返回全文,而会逐块读取并产生:总字节数/行数、空行和乱码统计、最长行、内容哈希,以及开头/中段/结尾样本。需要进一步核实时应使用明确的行区间或有界 `grep`。超长单行也按固定大小片段处理,内存占用不会随单行长度无限增长。
**路径映射关系:**
- Agent 使用:`/mnt/user-data/uploads/document.pdf`(虚拟路径)
@@ -217,18 +263,20 @@ backend/.deer-flow/threads/
└── user-data/
└── uploads/
├── document.pdf # 原始文件
├── document.md # 转换后的 Markdown
├── presentation.pptx
├── presentation.md
├── document.pdf.md # 转换后的 Markdown
├── presentation.pptx
├── presentation.pptx.md
└── ...
```
## 限制
- 最大文件大小:100MB(可在 nginx.conf 中配置 `client_max_body_size`)
- 文件名安全性:系统会自动验证文件路径,防止目录遍历攻击
- 线程隔离:每个线程的上传文件相互隔离,无法跨线程访问
- 自动文档转换默认关闭;如需启用,需在 `config.yaml` 中显式设置 `uploads.auto_convert_documents: true`
- 数据库策略可配置范围带服务端硬上限:文件数最多 100、单文件最多 100 MiB、单次总量最多 200 MiB;反向代理的 `client_max_body_size` 还必须不低于所需请求大小
- 文件名安全性:系统会自动验证文件路径,防止目录遍历攻击
- 文件真实性:PDF、JPG、PNG、OLE 与 OOXML 会校验签名/容器结构,OOXML 同时限制条目数、解压总量和压缩比
- 线程隔离:每个线程的上传文件相互隔离,无法跨线程访问
- 自动文档转换由 `uploads.auto_convert_documents` 控制;转换在事件循环外执行、限制并发,提取的 Markdown 还有固定字符上限
- 模型安全硬边界:单次文件摘要/读取结果最多 32,000 字符,图片还会校验像素数并生成受限尺寸派生图
## 技术实现
@@ -314,9 +362,7 @@ async function listFiles(threadId: string) {
### 扩展功能建议
1. **文件预览**:添加预览端点,支持在浏览器中直接查看文件
2. **批量删除**:支持一次删除多个文件
3. **文件搜索**:支持按文件名或类型搜索
4. **版本控制**:保留文件的多个版本
5. **压缩包支持**:自动解压 zip 文件
6. **图片 OCR**:对上传的图片进行 OCR 识别
1. **批量删除**:支持一次删除多个文件
2. **文件搜索**:支持按文件名或类型搜索
3. **版本控制**:保留文件的多个版本
4. **图片 OCR**:对上传的图片进行 OCR 识别
@@ -0,0 +1,502 @@
# 知识空间优先检索、Wiki 扩展检索与全库摘要实现方案
> 状态:P0/P1 与普通 Wiki 的 P2 主链路已落地;助手关系图扩展和 P3 待实施
> 适用范围:普通 WeKnora/LLMWiki 知识库、助手知识库、通用问答与智能体问答
> 更新日期:2026-09-16
## 1. 结论
本次改造不应只是“向量搜不到时再补一个 LIKE”。需要同时修复检索范围、索引可用性判断、召回策略和全库认知四个层面:
1. **勾选任一知识空间后,本轮进入严格知识空间模式**:只允许检索已选普通/助手知识空间,不调用 `web_search`、`web_fetch`、图片搜索、外部检索技能或检索型 MCP;非检索工具仍可使用。知识空间没有结果时必须明确报告原因,不能偷偷转向互联网。
2. **把普通知识库和助手知识库统一成一个检索入口**:预检索和智能体二次检索必须覆盖同一批已选空间,使用同一套权限、诊断、引用和排序协议。
3. **保留最后一次成功索引继续服务**:一次增量同步失败或取消,不能让已经完整生成的向量立刻整体失效。
4. **默认采用混合召回**:向量、关键词/全文、标题/别名精确命中并行执行,经 RRF 融合;向量不可用或低置信时关键词结果仍可回答,但响应必须附带 `fallback_reason`,以便继续修复向量根因。
5. **利用 Wiki 图做有界扩展**:在首批命中页面上沿 `InLinks`、`OutLinks`、实体关系和目录父子关系扩展 1 跳,确有多跳意图或首轮低置信时再扩展第 2 跳,然后统一重排,而不是把整张图塞进上下文。
6. **为每个知识空间维护“全库摘要”**:系统持续生成草稿,用户拥有可编辑的发布版本;系统更新不能覆盖人工修改。回答“这个库是干什么的/都讲了什么”时优先读取该摘要,并附代表性页面引用。
优先落地顺序是:**P0 可观测性与索引保活 → P1 严格范围与混合召回 → P2 Wiki 图扩展 → P3 全库摘要维护 → P4 重排与质量评测**。
## 2. 已确认的现状与问题
### 2.1 当前请求链路
当前前端已经把两类选择写入线程 metadata 和运行 context:
- `llmwiki_knowledge_base_ids`
- `assistant_knowledge_base_ids`
Gateway 的 `app/gateway/llmwiki_rag.py::build_llmwiki_rag_context` 会在模型调用前做一次检索,并把最多 8 条资料放进隐藏提示和引用列表。普通知识库走 `WikiRetrievalService`,助手知识库走 `AssistantKnowledgeRepository.search_pages`。
但现有实现存在四个断层:
| 断层 | 当前行为 | 影响 |
|---|---|---|
| 工具范围 | `services.py` 只在未选择普通知识库时排除 `llmwiki_search`,选择知识空间时没有排除 Web/外部检索 | 模型仍可能越过已选空间调用互联网或其他检索 |
| 二次检索 | `llmwiki_search_tool.py` 只读取 `llmwiki_knowledge_base_ids` | 助手知识库只参与一次预检索,智能体无法围绕它继续分解查询 |
| 普通 Wiki 召回 | 完整索引只做向量;远端回退按用户原句查询 Wiki 页面列表 | 同义表达、宽泛问题、精确专名都可能漏召回 |
| Wiki 图 | 本地镜像只保存 `parent_slug`,`normalize.py` 没有保留 `InLinks` / `OutLinks` | 数据虽然 Wiki 化,检索仍是孤立页面向量,没有利用页面关系扩展 |
### 2.2 已确认的向量漏检机制
普通 Wiki 只有同时满足以下条件才会进入本地向量检索:
```text
sync state == idle
AND last_success_at 非空
AND stored embedding_fingerprint == 当前编码器 fingerprint
AND ready_page_count == local_page_count
AND failed_page_count == 0
AND(空库 OR vector_count > 0)
```
对应代码是 `local_index/retrieval.py::is_fully_vectorized`。这个判断把“最新同步是否成功”和“最后一个成功索引是否仍可服务”混成了一个状态。
2026-09-11 本地 SQLite 快照及 2026-09-07 后端日志已经出现以下实际情况:
| 库状态样例 | 页面/向量 | 当前结果 |
|---|---:|---|
| `对话沉淀` | 124 页 / 281 向量 | 最新状态 `failed: Sync cancelled`,已有向量被整体排除,退回 Wiki API |
| `111` | 13 页 / 36 向量 | 同样因同步取消不使用已有向量 |
| `admin的知识库` | 0 页 / 0 向量 | 远端知识库映射持续返回 404,确实不可检索 |
| 两个空/源库 | 0 页 / 0 向量 | Wiki 页面和向量都不存在,属于真实空结果 |
| 三个验收/图片库 | 19 页 / 101 向量 | `idle` 且指纹一致,可走本地向量 |
这证明目前至少存在三类“搜不到”,必须分别呈现:
1. **数据真空**:没有可检索正文页或向量;
2. **索引存在但被状态门禁禁用**:最后一次增量同步失败,旧成功快照也不能服务;
3. **映射失效**:DeerFlow 本地记录指向已删除或无权访问的 WeKnora 库。
还存在一个独立的召回截断:`LocalWikiIndexConfig.min_similarity` 默认是 `0.35`,`search.py` 在此阈值之前直接丢弃候选。`keyword_boost_enabled` 默认关闭;即使开启,它也只给已超过向量阈值的候选加分,并不是关键词兜底。因此,只要所有向量分数低于 0.35,结果就是 0。
助手知识库已有关键词回退,但只有当前编码器指纹、维度和来源的 `last_import_job_id` 同时匹配时才会走向量。当前快照中也存在 active 来源为 0 条向量的情况。现有返回协议没有说明某个来源究竟是“无向量、指纹不符、最新导入不匹配”还是“向量低于阈值”。
### 2.3 “Wiki 已生成”不等于“可向量检索”
完整链路实际是:
```text
原始文件存在
→ WeKnora Wiki 异步生成完成
→ DeerFlow 成功拉取所有 Wiki 正文页
→ 正文成功切段
→ 使用当前编码器生成向量
→ 指纹/维度与查询编码器一致
→ 最后成功索引快照被激活
→ 查询分数超过阈值
```
任一步失败都可能出现“页面在 Wiki 管理里看得到,但聊天搜不到”。后续 UI 和接口必须展示每一层的数量与状态,不能只显示一个模糊的“已 Wiki 化”。
## 3. 外部开源方案调研与选型
### 3.1 可借鉴的项目
1. **腾讯 WeKnora(优先复用)**
当前上游已经把 Wiki 页面组织成有向图,页面包含 `InLinks` / `OutLinks`,并提供 `/wiki/graph`、`/wiki/search`、页面编辑、版本历史和回滚接口。上游说明还明确区分 `summary`、`entity`、`concept`、`index` 等页面类型。参考:[WeKnora Wiki 官方文档](https://github.com/Tencent/WeKnora/blob/main/website-docs/03-features/14-wiki.md)、[WeKnora 官方仓库](https://github.com/Tencent/WeKnora)。
2. **HippoRAG / HippoRAG 2(Wiki 扩展检索算法参考)**
HippoRAG 将稠密召回、知识图和 Personalized PageRank 组合起来,目标就是跨文档关联与多跳检索。这与“先命中 Wiki 页面,再沿关联页面扩展”的需求最接近。参考:[HippoRAG 官方仓库](https://github.com/OSU-NLP-Group/HippoRAG)、[NeurIPS 2024 论文](https://proceedings.neurips.cc/paper_files/paper/2024/file/6ddc001d07ca4f319af96a3024f6dbd1-Paper-Conference.pdf)。
3. **Microsoft GraphRAG(全库摘要参考)**
GraphRAG 的知识模型包含实体、关系、层级社区和 Community Report;Global Search 基于社区报告做全局问题汇总。这适合借鉴到“这个知识库整体讲什么”,但完整 GraphRAG 索引成本较高,不建议当前直接替换现有 WeKnora/本地向量链路。参考:[官方索引数据流](https://github.com/microsoft/graphrag/blob/main/docs/index/default_dataflow.md)、[官方 Global Search 示例](https://microsoft.github.io/graphrag/examples_notebooks/global_search/)。
4. **LightRAG(备选实现参考)**
LightRAG 使用图结构与向量的双层检索,并强调增量更新。它可作为独立图检索服务的备选,但当前系统已经有 WeKnora Wiki 图和助手库实体关系,再引入一套完整存储会造成双份索引、权限和一致性成本。参考:[LightRAG 官方仓库](https://github.com/HKUDS/LightRAG)。
### 3.2 选型结论
第一阶段**不整体引入 GraphRAG、HippoRAG 或 LightRAG 运行时**,而是:
- 复用 WeKnora 已生成的 Wiki 页面、链接图和现有 DeerFlow 助手知识图;
- 采用 HippoRAG 的“检索种子 + PPR/图扩散”思想实现轻量有界扩展;
- 采用 GraphRAG 的“分层摘要/全局摘要”思想维护全库摘要;
- 保留一个实验适配器,后续用离线评测决定是否接入完整框架。
这样能避免重复抽取同一批实体关系,也符合当前离线部署和 Harness/App 分层约束。
## 4. 目标行为
### 4.1 严格知识空间模式
只要本轮 `llmwiki_knowledge_base_ids` 或 `assistant_knowledge_base_ids` 至少有一个有效 ID,就设置:
```json
{
"retrieval_scope": "selected_knowledge_spaces",
"external_retrieval_allowed": false
}
```
必须保证:
- 允许:统一知识空间检索、读取命中 Wiki 全文、文件读写、计算、制图等非外部检索能力;
- 禁止:`web_search`、`web_fetch`、`image_search`、其他公网/内网检索工具、外部检索技能、检索型 MCP;
- 子智能体继承同一限制;
- 即使 0 命中也不能自动 Web fallback;
- 用户需要联网时,应先取消知识空间选择,或未来显式切换“知识空间 + 联网”模式,不能由模型自行决定。
只靠提示词不能作为保证。短期在 Gateway 合并 `excluded_tools`;中期给工具、技能和 MCP 增加 `retrieval_channel` 能力标签,在工具装配层统一 fail-closed 过滤,并在调用网关再次校验。
### 4.2 统一检索工具
用 `knowledge_space_search` 替代仅支持普通库的 `llmwiki_search`:
```json
{
"query": "聚焦后的检索问题",
"intent": "fact | overview | multi_hop | compare",
"selected_spaces": [
{"kind": "llmwiki", "id": "..."},
{"kind": "assistant", "id": "..."}
],
"top_k": 8,
"expand_wiki": "auto"
}
```
工具内部从 runtime context 获取白名单,模型不能通过参数扩大范围。预检索也调用同一个服务,避免两套算法和返回结构再次漂移。
统一结果至少包含:
```json
{
"results": [],
"mode": "hybrid_graph",
"diagnostics": {
"vector_candidates": 0,
"lexical_candidates": 7,
"graph_candidates": 11,
"vector_status": "threshold_filtered",
"top_raw_vector_score": 0.31,
"threshold": 0.35,
"fallback_reason": "VECTOR_BELOW_THRESHOLD"
}
}
```
## 5. 混合检索与 Wiki 扩展算法
### 5.1 查询路由
先做轻量意图识别,不调用外部检索:
- `overview`:“这个知识库做什么”“全部讲了什么”“有哪些主题”;
- `fact`:单事实、定义、专名、编号;
- `multi_hop`:关系链、原因链、跨页面综合;
- `compare`:多个对象/方案比较。
同时生成 1~4 个聚焦查询,包括原句、去停用词版本、实体/别名版本。保留原句用于引用追踪。
### 5.2 第一阶段召回
在每个已选空间内并行执行:
1. **向量召回**:页面正文 section 的 BGE-M3 向量;先取 raw top-N,再判断低置信,不能先丢掉诊断信息;
2. **关键词/全文召回**:标题、别名、heading、正文、实体名;中文使用分词或 CJK 2/3-gram,支持精确短语;
3. **结构命中**:slug、目录路径、实体 ID、来源文档名精确匹配;
4. **概览命中**:直接读取已发布的全库摘要,不把“知识库干什么”送去做普通相似度查询。
推荐默认并行混合,而不是只有 0 条时才执行关键词。向量擅长同义表达,关键词擅长专名、编号和缩写;两者用 Reciprocal Rank Fusion 合并,避免直接比较不可校准的原始分数:
```text
score_rrf(d) = Σ 1 / (k + rank_channel(d))
```
标题/别名精确命中可作为独立 rank channel,不直接覆盖正文相关性。
### 5.3 Wiki 图扩展
同步时把 WeKnora `GET /wiki/graph` 或页面 `InLinks` / `OutLinks` 落到本地边表。助手库复用现有 `assistant_knowledge_entities`、`assistant_knowledge_relations` 及包内双向链接。
建议的节点与边:
| 节点 | 边 | 默认权重方向 |
|---|---|---:|
| Wiki 页面 | 显式 `[[wiki-link]]` / InLinks / OutLinks | 1.0 |
| 页面 ↔ 实体 | 页面提及或实体贡献 | 0.9 |
| 实体 ↔ 实体 | 已抽取关系 | 0.8 |
| 页面 ↔ 来源文档 | `source_refs` / `chunk_refs` | 0.7 |
| 页面 ↔ 目录父子 | `parent_slug` / folder | 0.4 |
在线流程:
1. 取混合召回 top 10~20 作为个性化种子;
2. `fact` 默认扩 1 跳,`multi_hop` 或低置信再扩 2 跳;
3. 每个种子的邻居上限、总节点上限和每库配额必须固定;
4. 用有界 PPR 或衰减传播得到图分;
5. 将向量 rank、关键词 rank、图 rank 用 RRF 融合;
6. 可选本地 reranker 对 top 30 重排;
7. 用 MMR/去重限制同一页面和同一主题占满上下文;
8. 最终只返回有正文且能追溯到原始页面/文档的 6~10 条证据。
图扩展只扩大候选池,不直接把关联页面当作事实证据。最终引用必须对应真实正文片段。
### 5.4 索引快照保活
同步状态拆成两组:
- `serving_snapshot_*`:当前对查询可见的最后成功版本;
- `sync_attempt_*`:最新一次同步任务的状态、错误和进度。
新同步在 staging revision 中完成,只有页面、向量、指纹和维度校验全部通过后才原子切换 serving revision。同步失败时继续使用旧 revision,同时返回 `index_stale=true` 和失败原因。只有从未成功生成过 serving snapshot 时才退回 Wiki API/关键词。
## 6. 全库摘要的持续维护与人工编辑
### 6.1 产品语义
每个知识空间维护两份摘要:
- **系统草稿**:随 Wiki 页面变化持续重算;
- **人工发布版**:用户可编辑,是聊天回答和知识库详情默认展示的摘要。
如果从未人工编辑,发布版自动跟随系统草稿;一旦人工编辑,后续系统更新只产生“待合并草稿”和差异提示,不能覆盖用户文字。用户可以逐段接受、全部替换、恢复历史版本。
### 6.2 摘要结构
摘要不是一段无来源的模型结论,建议存储结构化字段:
```json
{
"purpose": "这个知识空间解决什么问题",
"scope": ["覆盖范围"],
"key_topics": [{"name": "主题", "page_slugs": ["..."]}],
"key_entities": [{"name": "实体", "page_slugs": ["..."]}],
"time_coverage": "资料时间范围或未知",
"source_coverage": {"documents": 12, "wiki_pages": 63, "eligible_pages": 58},
"limitations": ["缺口、失败页面、过期风险"],
"summary_markdown": "面向用户的可编辑摘要"
}
```
`source_coverage` 和 `limitations` 由系统生成且不可由摘要模型伪造。
### 6.3 生成策略
借鉴 GraphRAG 的分层摘要,但复用现有 Wiki 分类:
```text
页面摘要(增量复用)
→ 按目录 / 实体社区 / 来源文档聚类
→ 主题摘要
→ 全库摘要草稿
→ 事实覆盖与引用校验
```
触发条件:文件导入完成、Wiki 页面创建/编辑/删除、索引 serving revision 切换。采用 30~120 秒 debounce,同一知识库合并任务;只重算受影响主题,最后合并全库草稿。
### 6.4 数据模型
新增:
```text
knowledge_space_profiles
kind, space_id, serving_revision
generated_json, generated_markdown, generated_at
published_markdown, published_version, edited_by, edited_at
source_revision, status, last_error
knowledge_space_profile_revisions
profile_id, version, source(system|user|merge|revert)
content_markdown, content_json, created_by, created_at
```
唯一键为 `(kind, space_id)`;更新使用 `published_version` 乐观锁。
### 6.5 API 与界面
建议统一接口:
- `GET /api/knowledge-spaces/{kind}/{id}/profile`
- `PATCH /api/knowledge-spaces/{kind}/{id}/profile`:人工编辑;
- `POST /api/knowledge-spaces/{kind}/{id}/profile/regenerate`:重算系统草稿;
- `POST /api/knowledge-spaces/{kind}/{id}/profile/accept-generated`:接受系统草稿;
- `GET /api/knowledge-spaces/{kind}/{id}/profile/revisions`
- `POST /api/knowledge-spaces/{kind}/{id}/profile/revert/{version}`
普通/助手知识库详情增加“知识库摘要”页签:显示覆盖数量、生成时间、是否落后于当前 Wiki、编辑、差异、重算和历史。问答遇到 `overview` 意图时读取发布摘要,再补充 3~5 个代表性正文引用。
## 7. 检索健康诊断
### 7.1 健康接口
新增:
```text
GET /api/knowledge-spaces/{kind}/{id}/retrieval-health
POST /api/knowledge-spaces/{kind}/{id}/retrieval-health/reindex
POST /api/knowledge-spaces/{kind}/{id}/retrieval-health/resync
```
返回至少包括:
- 远端文档数、Wiki 页面数、正文可检索页面数;
- active/draft/archived、summary/index/body 页面分布;
- 已切段页数、向量数、失败数;
- serving revision 与最新 sync attempt;
- 当前/已存 embedding model、fingerprint、dimensions;
- 空正文数、失效映射/权限状态、最后错误;
- 最后成功同步/向量化/摘要更新时间;
- 关键词索引和 Wiki 图边数量。
### 7.2 每次查询追踪
每次检索生成 `retrieval_trace_id`,记录:
```text
选中空间 → 权限通过数 → 查询变体 → 每通道候选数
→ 向量 top raw score / threshold → 图扩展数 → rerank 数
→ 最终引用数 → 实际模式 / fallback_reason → 各阶段耗时
```
0 结果必须归类为稳定错误码,例如:
- `SPACE_EMPTY`
- `REMOTE_MAPPING_NOT_FOUND`
- `WIKI_NOT_FINALIZED`
- `NO_SERVING_VECTOR_SNAPSHOT`
- `EMBEDDING_FINGERPRINT_MISMATCH`
- `EMBEDDING_DIMENSION_MISMATCH`
- `VECTOR_BELOW_THRESHOLD`
- `NO_ELIGIBLE_BODY_PAGE`
- `KEYWORD_NO_MATCH`
- `RETRIEVAL_PROVIDER_UNAVAILABLE`
前端不得把 `UNAVAILABLE` 显示成“库内没有相关资料”。管理员可以展开诊断并直接触发重同步/重建索引。
## 8. 后端改造位置
| 模块 | 改造 |
|---|---|
| `app/gateway/services.py` | 识别严格知识空间模式;统一过滤外部检索能力;策略传给子智能体 |
| `app/gateway/llmwiki_rag.py` | 改为调用统一 `KnowledgeSpaceRetrievalService`;附诊断和 trace id |
| `tools/builtins/llmwiki_search_tool.py` | 迁移为同时支持普通/助手库的 `knowledge_space_search`,保留兼容别名 |
| `local_index/retrieval.py` | serving snapshot 保活、普通库关键词召回、模式与 fallback 原因 |
| `local_index/search.py` | 返回 raw top score;阈值校准;向量与关键词并行融合 |
| `local_index/normalize.py` | 保留 `in_links`、`out_links`、folder/关系字段 |
| `persistence/llmwiki_index/*` | serving/staging revision、Wiki 图节点边、全文索引 |
| `persistence/assistant_knowledge/*` | 统一诊断协议;复用助手实体关系做图扩展 |
| 新 `knowledge_space_retrieval/` | 查询路由、hybrid/RRF、图扩展、上下文组装、trace |
| 新 `knowledge_space_profiles/` | 全库摘要任务、版本、人工编辑与 diff |
Harness 不导入 `app.*`;Gateway 负责用户和权限解析,把授权后的空间描述传给 Harness 服务。
## 9. 前端改造
1. 选择知识空间后,在选择器旁明确显示“仅检索已选知识空间,不联网”;
2. 检索过程显示“向量 / 关键词 / Wiki 关联扩展 / 重排”阶段及耗时;
3. 0 命中时展示具体状态和修复入口,不只显示空卡片;
4. 引用卡片标注 `向量命中`、`关键词命中`、`Wiki 关联扩展`,但不能暴露内部敏感路径;
5. 普通/助手知识库共用“知识库摘要”编辑、差异和历史组件;
6. 管理页面增加“检索健康”抽屉,普通用户看到简化状态,管理员看到完整诊断。
## 10. 性能设计
- 空间列表继续只读 metadata/聚合统计,不扫描 Wiki 正文和向量;
- 多空间权限校验改成批量查询,不按 ID 串行;
- 本地向量矩阵按 `(space_id, serving_revision, fingerprint)` 缓存;
- 查询向量按 `(fingerprint, normalized_query)` 短 TTL 缓存;
- 关键词索引增量维护,禁止在线全表 `%LIKE%` 扫描大库;SQLite 可用 FTS5/CJK n-gram 辅助表,PostgreSQL/高斯/MySQL 通过统一适配层接各自全文索引;
- Wiki 图常驻压缩邻接表,在线只取有界邻居;
- 页面全文和引用详情批量加载,避免 N+1 远端请求;
- reranker 只处理融合后的 top 20~30,超时则降级到 RRF,不阻断回答;
- 摘要生成完全后台化,聊天只读 serving profile。
建议验收预算(需用生产规模压测校准):热缓存本地检索 p95 ≤ 2 秒,图扩展自身 p95 ≤ 200 ms;远端回退必须有独立超时,不能拖住整个模型首 token。
## 11. 分阶段实施
### 当前落地状态(2026-09-16)
- 已落地 P0/P1 主链路:失败/取消同步保留最后成功向量快照、向量 raw-score 诊断、普通 Wiki
关键词并行召回 + RRF、低分回退原因、普通/助手二次检索统一、选库后的 Web/MCP/技能/子智能体
硬隔离,以及选库后的 4 个推荐问题。
- 已落地逐次查询追踪与同链路预览:`POST /api/llmwiki/retrieval-debug` 复用问答预检索,知识空间
「检索调试」页展示最终证据、通道候选数、阈值、降级和零结果原因;正常问答在 SSE `metadata`
与 `/api/threads/{thread_id}/reference-batches.retrieval_traces` 暴露相同 trace,F12 可直接核对。
后续轮次省略选库字段时沿用线程选择,显式空列表仍清空范围。
- 编码器已按部署配置切换为 SiliconFlow `BAAI/bge-m3`(1024 维),密钥只从环境变量读取;
供应商切换产生新 fingerprint,旧库将重建,期间由关键词/Wiki API 保底。
- 已落地 P2 普通知识库与助手知识库图扩展:普通库同步库内入链/出链、正文 wikilink 与 provider
graph;助手库沿导入实体关系并通过 `entity.wiki_page_id` 回到 Wiki 正文。两者都以混合召回页为
种子,默认 1 跳,显式多跳意图或低置信时最多 2 跳,并执行候选/种子/每库/最终上下文硬上限、
衰减传播排序、真实正文校验、授权二次过滤和可审计路径 trace。P3 可编辑全库摘要仍未实施,
不能把当前 overview 代表页聚合误称为“持续维护摘要”。
### P0:诊断与正确性保底
- 建 retrieval health/trace;
- 拆分 serving snapshot 与 sync attempt;
- 修复失效远端映射和同步取消导致的整库失效;
- 记录 raw top score、阈值和每库候选数。
验收:现有 124 页/281 向量的库即使新同步失败,仍从上一次成功 snapshot 返回结果,并标记 stale。
### P1:严格范围与混合召回
- 勾选知识空间后禁止所有外部检索通道;
- 合并普通/助手知识工具;
- 向量、关键词、标题/别名召回 + RRF;
- 0 结果返回明确 fallback reason。
验收:测试 spy 证明选中空间的整轮运行中 Web/外部检索调用次数为 0;向量低于阈值时关键词能召回精确专名。
### P2:Wiki 图扩展
- 已同步 WeKnora InLinks/OutLinks、正文 wikilink 和 provider graph;
- 已实现普通 Wiki 1~2 跳有界扩展、衰减传播、稳定融合、去重、每库配额与 trace;
- 已接入助手实体关系:只沿所选助手库内 active 的关系、两端实体和页面扩展,并阻断跨库追边;
- 后续继续以多跳评测集校准传播权重和配额。
验收:准备只能通过关联页找到答案的 1 跳/2 跳数据集;结果必须命中且引用真实正文。
### P3:全库摘要与编辑
- profile 表、后台生成、事件触发;
- 人工发布版、diff、历史、回滚;
- overview 查询优先走 profile。
验收:新增/编辑/删除 Wiki 后系统草稿更新;人工摘要不被覆盖;“知识库干什么”稳定返回摘要和代表性引用。
### P4:重排与质量评测
- 可选 BGE reranker;
- 建中文问答集、专名集、概览集、多跳集和负样本;
- 评估 Recall@K、MRR、nDCG、citation precision、0 命中率和 p95。
只有离线评测显著优于 RRF 基线,才默认开启 reranker 或引入完整外部 GraphRAG/HippoRAG 服务。
## 12. 必测用例
1. 普通库、助手库、二者混选时都严格限定到所选 ID;
2. 选择知识空间后直接/技能/MCP/子智能体均不能调用外部检索;
3. 同步取消但存在 serving snapshot 时继续向量检索;
4. 指纹或维度不匹配时返回关键词结果和明确诊断;
5. 所有向量低于阈值时返回 raw top score,不误报为空库;
6. 中文专名、英文缩写、编号、标题别名可被关键词召回;
7. summary/index 页面不作为普通事实的唯一证据;
8. Wiki 1 跳/2 跳扩展不越出已选知识空间;
9. 多知识空间有每库配额,单个大库不会吞掉全部 top-K;
10. 空库仍可进入管理页、编辑摘要说明和上传文件;
11. overview 问题不依赖与问句的向量相似度;
12. 人工编辑摘要后自动更新只生成待合并草稿;
13. 远端 404、超时、权限错误与真实 0 命中分别显示;
14. 引用可打开对应 Wiki 页面或源文档,且正文与答案事实一致。
## 13. 完成定义
以下条件全部满足才算完成,而不是“接口能返回 200”:
- 勾选知识空间后,外部检索调用为 0;
- 普通和助手知识空间共享一套检索语义;
- 每个 0 结果都有可解释原因;
- 旧成功索引不因新同步失败而下线;
- 向量、关键词和 Wiki 图扩展均有独立指标;
- “这个知识库干什么”读取可持续维护、可人工编辑的全库摘要;
- 摘要、混合检索和图扩展都有可追溯引用;
- 离线质量集和性能压测达到约定门槛后再发布。
@@ -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,400 @@
# 普通知识库智能推理检索模式设计
> 状态:已实现,与当前代码一致
>
> 适用范围:通用问答、普通智能体问答中的 WeKnora 普通知识库
>
> 更新日期:2026-10-18
>
> WeKnora 行为核查基线:v0.7.2,commit 3d5d8bfcdfeeea266b292b71cea616847af28d0f
## 1. 目标
系统在原有经典检索之外增加“知识库智能检索”模式。经典模式完整保留。新模式的全局开关默认关闭,只有管理员能在设置页开启;关闭时新对话默认经典检索,开启后新对话默认智能检索,经典检索位于候选位置。已有线程继续按自身 metadata 恢复模式,不被全局开关批量改写。
开关和本功能新增配置只保存在数据库中,不写 `system_settings.json`、`config.yaml` 或其他配置文件。智能模式在一次用户提问内执行模型与工具的多轮循环。
用户只需直接提问,不需要在问题中说明先检索什么、再检索什么。每个新问题先执行原始文档向量/混合检索取得直接相关的分片、标题和实体,再用这些线索执行 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 执行向量或混合召回、融合和排序。 精确词、代码、编号和固定短语由切片正则检索补充。
Wiki 搜索采用大小写不敏感的安全 POSIX 风格正则表达式。 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 的旧会话继续使用经典模式。新对话在设置读取完成后按全局开关选择默认模式:开关开启时为 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 工具。 经典模式原有编号引用行为不因本功能改变。
旧客户端不传 mode 时自动进入 classic。全局开关控制 smart_reasoning 是否允许使用,并决定新对话的前端默认模式;关闭开关不会影响 classic。
本功能也不复用前端已有的 multi_round_rag execution profile。 multi_round_rag 的外部调用语义与本功能的知识库范围、权限和会话执行不同。 knowledge_retrieval_mode 与模型选择、推理强度和 execution_profile 相互独立。
## 15. 前端实现
管理员入口位于“设置 → 知识检索”。 设置页展示当前数据库开关和说明。 通用问答与智能体问答输入框都支持“知识库智能检索”切换。
管理员关闭全局开关时,新会话初始化为 classic;管理员开启后,新会话默认使用 smart_reasoning,classic 保留为候选。已有线程始终从 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. 全局开关开启时新会话默认 smart_reasoning,关闭时默认 classic;历史线程始终按 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 个结果项;完整工具结果和回答证据仍按既有运行记录保存。
@@ -11,11 +11,14 @@ This directory contains detailed documentation for the DeerFlow backend.
| [CONFIGURATION.md](CONFIGURATION.md) | Configuration options |
| [SETUP.md](SETUP.md) | Quick setup guide |
## Feature Documentation
| Document | Description |
|----------|-------------|
| [STREAMING.md](STREAMING.md) | Token-level streaming design: Gateway vs DeerFlowClient paths, `stream_mode` semantics, per-id dedup |
## Feature Documentation
| 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 |
| [PATH_EXAMPLES.md](PATH_EXAMPLES.md) | Path types and usage examples |
| [summarization.md](summarization.md) | Context summarization feature |
@@ -143,6 +143,12 @@ sequenceDiagram
**`StreamBridge` 的存在价值**:当生产者(`run_agent` 任务)和消费者(HTTP 连接)在不同的 asyncio task 里运行时,需要一个可以跨 task 传递事件的中介。Queue 同时还承担断连重连的 buffer 和多订阅者的 fan-out。
### Gateway 多进程约束
当前 `MemoryStreamBridge` 和 `RunManager` 都只保存当前 Python 进程内的实时状态。首次 `POST /runs/stream` 会在同一个 uvicorn worker 中创建后台 run 并消费事件,因此首连可以正常工作;但响应里的 `Location`、显式 `/join` 和携带 `Last-Event-ID` 的恢复请求都是新的 HTTP 请求。多个 uvicorn worker 会独立调度这些请求,恢复请求一旦落到另一个 worker,该进程看不到原 worker 的 live run 和事件缓冲,只能把持久化 run 记录当作没有可恢复流并提前返回 `end`。持久化 checkpointer/RunStore 不能替代实时事件桥。
因此,使用内存 bridge 时 Gateway 必须运行 **1 个 worker**。两个生产离线启动脚本 `scripts/start_backend-online.sh` 与 `offline-backend-20260512/start_backend.sh` 都默认并校验 `GATEWAY_WORKERS=1`;显式配置更大值会拒绝启动,除非同时设置 `DEERFLOW_ALLOW_UNSAFE_MULTIWORKER=1` 确认接受丢失重连流的风险。真正支持多 worker 需要共享的 StreamBridge 与 live-run 控制面(发布、订阅、结束、取消和活跃 run 查询),不能只依赖负载均衡粘性或数据库中的最终状态。
---
## DeerFlowClient 路径:sync + in-process
@@ -0,0 +1,429 @@
# 任务分析定时 · 对外接口对接文档
> 面向外部任务系统(下称「对方系统」)的开发者。覆盖任务接入、时间线详情拉取、
> 总控问答、**定时数据交付**(任务 6 块 JSON)四组接口。智能体编排配置
> (选业务链 / 执行频率)在 DeerFlow 任务分析页操作,对方系统无需开发。
>
> 相关实现:`app/gateway/routers/task_analysis.py`(任务/绑定/运行记录)、
> `app/gateway/routers/task_analysis_qa.py`(问答)。
## 0. 通用约定
- **Base URL**:DeerFlow Gateway 地址(如 `http://<host>:8001`)。
- **鉴权**:所有接口需带 `Authorization: Bearer <token>` 请求头(token 获取方式见登录对接,本文不重复)。
- **Content-Type**:`application/json`(SSE 接口除外,响应为 `text/event-stream`)。
- **时间格式**:ISO 8601 字符串(含时区,如 `2026-09-10T10:14:55+08:00`)。
- **两类任务标识**:
- `source_task_id`:对方系统的任务号(创建时由你们传入);
- `task_id`:DeerFlow 内部任务 id(**创建接口的响应里返回**,问答接口使用它)。
创建任务后请保存响应中的 `task_id`;也可随时通过时间线详情接口重新取到。
## 1. 任务接入
### 1.1 创建任务
对方系统创建任务完成后调用,DeerFlow 落一条任务记录(初始未绑定分析方案)。
```
POST /api/task-analysis/external/tasks
```
请求体:
```json
{
"source_task_id": "TS-2026-0147",
"name": "东南亚舆论环境监测",
"description": "持续跟踪东南亚重点渠道舆论动向,输出结构化分析详情。"
}
```
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| source_task_id | string(≤128) | 是 | 对方任务号,全局唯一 |
| name | string(≤512) | 是 | 任务名称 |
| description | string(≤8192) | 否 | 任务简介。**定时执行时注入各智能体的任务上下文以此为准**(存量快照,执行时不会回拉) |
响应 `200`:
```json
{
"task_id": "ta-TS-2026-0147",
"default_chain_id": "ta-demo-chain"
}
```
- `task_id`:DeerFlow 内部任务 id,**问答接口需要它**,请保存。
- `default_chain_id`:DeerFlow 侧最近一次绑定使用的业务链 id,可作为你们界面上绑定流程的默认选项;无绑定为 `null`。
错误:`409` —— `source_task_id` 已存在。
### 1.2 删除任务
对方系统删除任务时同步调用,**彻底删除**:任务、绑定、全部执行记录、过程消息与时间线版本一并清除(不可恢复)。
```
DELETE /api/task-analysis/external/tasks/{source_task_id}
```
响应 `200`:`{"deleted": true, "task_id": "..."}`。错误:`404` —— 任务不存在。
## 2. 任务详情(时间线)
对方系统详情页的数据源。每次定时执行成功会落一期**时间线版本**(含各智能体产出),按时间倒序返回。
```
GET /api/task-analysis/external/tasks/{source_task_id}/detail?limit=20&offset=0
```
响应 `200`:
```json
{
"task": {
"task_id": "ta-TS-2026-0147",
"source_task_id": "TS-2026-0147",
"name": "东南亚舆论环境监测"
},
"binding": {
"chain_id": "ta-demo-chain",
"cron_expr": "0 9 * * *",
"schedule_type": "cron",
"enabled": true
},
"timeline": [
{
"id": "…",
"external_task_id": "ta-TS-2026-0147",
"run_id": "tar-…",
"blocks": {
"eightbf-roundtable-intent": {
"name": "8BF 任务理解",
"seat_agent_id": "eightbf-roundtable-intent",
"content": "(该智能体本次产出的 Markdown 正文)",
"format": "markdown"
},
"c0a8799da8724e179e086ea9ca723967": { "name": "策略分析师", "…": "…" }
},
"created_at": "2026-09-10T10:15:13+08:00"
}
]
}
```
- `binding`:当前绑定信息(未绑定为 `null`,此时 `timeline` 不会有新版本)。
`schedule_type` 为执行方式:`cron` = 按 `cron_expr` 频率定时执行;
`once` = 只执行一次(创建后立即执行,之后不再自动执行,`cron_expr` 不生效)。
- `timeline[].blocks`:**每个参与执行的智能体一块**,key 为该智能体的 agent_id,
`name` 为执行时的名称快照,`content` 为其交付正文(Markdown)。
按你们的详情分区规则渲染即可(如按 block 顺序分栏)。
- 分页:`limit`(默认 20)/ `offset`,按 `created_at` 倒序。
## 3. 总控问答(详情页对话)
详情页的对话入口:用户提问 → DeerFlow 总控按任务绑定的编排给对应智能体派活 →
智能体执行 → 总控汇总结论。一次问答会话记为一条「手动执行」记录,过程消息可查。
### 3.1 发起问答会话(init)
```
POST /api/task-analysis/tasks/{task_id}/qa/init
```
> 注意:此处 `{task_id}` 为 **DeerFlow 内部任务 id**(创建接口响应里的 `task_id`)。
请求体:空(无需参数)。
响应 `200`:
```json
{
"run_id": "tar-0743654e-…",
"coordinator": "roundtable-coordinator",
"thread_ids": {
"eightbf-roundtable-intent": "ta-seat-…-…",
"c0a8799da8724e179e086ea9ca723967": "ta-seat-…-…",
"roundtable-coordinator": "ta-seat-…-…"
},
"seats": [
{ "agent_id": "eightbf-roundtable-intent", "name": "8BF 任务理解" },
{ "agent_id": "c0a8799da8724e179e086ea9ca723967", "name": "策略分析师" }
]
}
```
- `run_id`:本次问答会话的运行记录 id(查历史 / 应用方向都用它)。
- `thread_ids`:**会话线程映射,请保存并在后续每轮 stream 原样传回**(会话不跨请求保存在服务端)。
- init 时已自动把任务上下文(名称/简介/编排/历次方向备注)注入总控,之后每轮消息传用户原话即可。
错误:`400` —— 任务未绑定分析方案 / 绑定链没有可调度席位。
### 3.2 发送消息(stream,SSE)
```
POST /api/task-analysis/tasks/{task_id}/qa/stream
Content-Type: application/json
Accept: text/event-stream
```
请求体:
```json
{
"run_id": "tar-0743654e-…",
"message": "请让两个席位各自分析当前的主要风险点,然后给我综合结论。",
"thread_ids": { "…init 返回的映射原样传回…" }
}
```
响应为 SSE 流,每帧 `data: {JSON}\n\n`。**帧类型**:
| 帧 | 触发时机 | 关键字段 |
|---|---|---|
| LangGraph 上游事件(无 `event` 字段的多种帧) | 总控正在生成(token 流) | 透传自运行时,可用于打字机效果 |
| `{"event": "leader", "status": [[seat_id, task], …], …}` | 总控一轮结束 | `status` 为派活表(二维数组)时表示已派活 |
| `{"event": "leader", "status": "clarification", …}` | 总控需要澄清 | `question` / `options`,前端展示追问,等用户补充后发下一轮 |
| `{"event": "leader", "status": [], "content": …}` | 总控给出结论(共识) | `content` 为结论正文 |
| `{"event": "seat", "agent": id, "name": …, "content": …}` | 某智能体执行完成 | `content` 为该智能体交付正文 |
| `{"event": "round_done", "consensus": true/false}` | 本轮结束 | `consensus=true` 会话已收口(run 置 succeeded);`false`(如澄清)可继续发下一轮 |
一轮完整时序:`leader(派活)→ 若干 seat(交付)→ leader(综合结论)→ round_done`。
约定:
- 澄清(`consensus=false, kind=clarification`):继续对**同一 run_id** 发下一轮 stream;
- 共识(`consensus=true`):run 已自动收口,如需再问请新开一次 `qa/init`;
- 客户端中断流不影响服务端继续执行(消息仍会落库,可从历史接口补读)。
错误:`404` 会话不存在;`409` 会话已收口(需重新 init)或 `thread_ids` 缺失(重新 init)。
### 3.3 应用调整方向(可选)
问答结束后,若用户确认「把本次调整应用到后续执行」,调用本接口把结论沉淀为
**执行方向备注**——该备注会注入此任务后续**每次定时执行**各智能体的任务上下文。
```
POST /api/task-analysis/qa/{run_id}/apply-direction
```
请求体:`{"note": "后续执行请重点关注台湾海峡北段动态,风险输出按高/中/低分级。"}`
响应 `200`:`{"applied": true, "direction_notes": ["…", "…"]}`(全部累计备注)。
### 3.4 对话历史查询
按运行记录查询完整过程消息(总控 + 各智能体),供会话回放或补读。
```
GET /api/task-analysis/external/runs/{run_id}/messages?seat=&limit=500
```
- `seat` 可选:传智能体 agent_id 只看该席位;传 `__coordinator__` 只看总控;不传看全部。
- 响应:`{"run": {"id", "trigger", "status", "started_at", "finished_at"}, "messages": […]}`
- 消息字段:`seq`(顺序)、`seat_agent_id`、`stage_index`、`event_type`
(`input` 输入 / `llm.ai` 智能体输出 / `artifact` 产物备注)、`content`。
## 4. 定时数据交付(任务 6 块数据)
按任务展示 **6 块结构化数据**(相当于 6 个页面分区,每块含多个部分),每块一个
JSON 对象。DeerFlow 只负责数据管道与调度:智能体在定时执行过程中把成果提交进
数据块,归配智能体 / 业务链的配置由管理方维护。
**口径要点**:
- 块 key 定死 `block_1`~`block_6`,标题默认「数据块一~六」,可经 4.4 修改;
存量 5 块任务在读接口 / 首次提交时懒补 `block_6`,已改过的标题不覆盖;
- payload 不做 schema 校验(结构会变),只要求是 **JSON 对象**;
- 不定席位↔块映射:多个智能体可各写一个块的不同部分,一个智能体也可写多个块;
- 同一块多次提交按「**顶层 key 合并、同 key 后写覆盖**」合并——某个部分一提交,
立刻体现在任务的当前数据里(实时更新);
- **不挂时间轴**:一个任务的数据只有一份「当前状态」,所有提交(定时轮次、
手动触发、对话修复)都合并进它,越提交越新;
- 调度不管上一轮跑没跑完:到期照常触发,**同任务允许多轮并行**,不跳过不顺延;
多轮提交按流水顺序收敛到同一份数据;
- 首次提交时若任务还没有数据承载记录,自动创建一条(`trigger=external`),
保证写入永远成功。
### 4.1 数据块定义列表
```
GET /api/task-analysis/external/tasks/{source_task_id}/blocks
```
响应 `200`:
```json
{
"task": {"task_id": "ta-TS-2026-0147", "source_task_id": "TS-2026-0147", "name": "东南亚舆论环境监测"},
"blocks": [
{"block_key": "block_1", "title": "数据块一", "sort_order": 1},
{"block_key": "block_2", "title": "数据块二", "sort_order": 2},
{"block_key": "block_3", "title": "舆情热点", "sort_order": 3},
{"block_key": "block_4", "title": "数据块四", "sort_order": 4},
{"block_key": "block_5", "title": "数据块五", "sort_order": 5},
{"block_key": "block_6", "title": "数据块六", "sort_order": 6}
]
}
```
固定 6 条,按 `sort_order` 升序返回;`title` 为修改后的标题(见 4.4)。
### 4.2 当前数据状态(兼容旧路径 periods)
```
GET /api/task-analysis/external/tasks/{source_task_id}/periods
```
时间轴已去掉:任务的数据只有一份当前状态,本接口固定返回**唯一一条**当前数据
承载记录(`periods` 数组长度恒为 1,`is_current` 恒为 `true`),供消费方感知
数据更新时间。响应 `200`:
```json
{
"task": {"task_id": "…", "source_task_id": "…", "name": "…"},
"current_period_index": 3,
"total": 1,
"periods": [
{
"period_index": 3,
"opened_at": "2026-09-14T15:00:00+08:00",
"trigger": "cron",
"run_id": "tar-…",
"is_current": true,
"blocks": {
"block_1": {"updated_at": "2026-09-14T15:03:12+08:00"},
"block_2": {"updated_at": null},
"block_3": {"updated_at": null},
"block_4": {"updated_at": null},
"block_5": {"updated_at": null},
"block_6": {"updated_at": null}
}
}
]
}
```
- 各块 `updated_at` 为 `null` 表示该块尚无数据;
- 首条承载记录由首次数据提交自动创建(`trigger=external`);存量任务沿用
历史期号(`period_index` 数值仅作标识,无时间轴含义)。
### 4.3 单期数据详情
```
GET /api/task-analysis/external/tasks/{source_task_id}/periods/{period}
```
`{period}` 传期号数字(如 `2`)或 `latest`(当前最新一期)。响应 `200`:
```json
{
"task": {"task_id": "…", "source_task_id": "…", "name": "…"},
"period": {
"period_index": 2,
"opened_at": "2026-09-14T12:00:00+08:00",
"trigger": "cron",
"run_id": "tar-…",
"is_current": false
},
"blocks": [
{
"block_key": "block_1",
"title": "数据块一",
"sort_order": 1,
"payload": {"热点列表": ["…"], "统计": {"提及量": 128}},
"updated_at": "2026-09-14T12:04:55+08:00"
},
{"block_key": "block_2", "title": "数据块二", "sort_order": 2, "payload": null, "updated_at": null}
]
}
```
- `blocks` 固定 6 条、按 `sort_order` 升序;尚未提交的块 `payload` / `updated_at`
为 `null`;
- `payload` 即该块的**合并结果**(多次提交按顶层 key 合并、同 key 后写覆盖),
结构由智能体产出决定,请按 JSON 对象渲染;
- `{period}` 推荐传 `latest`(当前数据);传数字期号可访问存量任务的历史
遗留数据(时间轴去掉前的旧期,仅供排查)。
错误:`404` —— 期不存在;`400` —— `period` 既不是数字也不是 `latest`。
### 4.4 修改块标题 / 排序(管理端)
块标题支持后续修改(页面 + API)。`{task_id}` 为 **DeerFlow 内部任务 id**(创建
接口响应里保存的那个)。
```
PUT /api/task-analysis/tasks/{task_id}/blocks
```
请求体(只传要改的块;块集合固定 6 条,不能增删):
```json
{
"blocks": [
{"block_key": "block_3", "title": "舆情热点"},
{"block_key": "block_1", "sort_order": 9}
]
}
```
响应 `200`:`{"blocks": […]}`(修改后的完整定义列表,形状同 4.1)。
错误:`400` —— `block_key` 不在 `block_1`~`block_6` 内。
### 4.5 演示数据(已预存)
开发环境已预存一个演示任务,可直接用于联调:
- 任务号 `source_task_id = BUSINESS-OVERVIEW-DEMO`;`block_1` 标题「商业态势总览」;
- 预存时按 13 期小时刻度写入(时间轴去掉前的演示口径);当前对外仅暴露最新一期
的 `block_1` payload = 页面整卡数据(`pageTitle` / `briefs` / `objectives` /
`events` / `businessNarratives` / `insights` / `chart` / `snapshotName` 等,
与页面 mock 快照同构);`block_2`~`block_6` 暂无数据;
- 预存脚本:`scripts/seed_business_overview_demo.py`(可重复运行,幂等重建)。
### 4.6 智能体数据提交(内部,供参考)
智能体侧由内置技能 `task-data-submit`(`skills/public/task-data-submit/`)调以下
接口完成入库,**对方系统无需调用**,此处仅供联调排查参考:
```
POST /api/task-analysis/data/submit
```
请求体:`{"task_id": "<source_task_id>", "block_key": "block_2", "data": {…JSON 对象…}}`。
响应 `200`:`{"source_task_id", "task_id", "block_key", "period_index", "updated_at", "keys"}`。
错误:`400` —— 块 key 非法 / payload 不是对象;`404` —— 任务不存在;`422` ——
请求体不是合法 JSON 对象。
## 5. 典型对接流程
```
对方系统创建任务
└─ POST /external/tasks ──► 保存 task_id(+ default_chain_id)
│
│ (DeerFlow 管理页配置:选业务链 + 执行频率 —— 无需开发)
▼
按 cron 自动执行(多轮并行,产出时间线版本)
├─ 详情页定期/手动 GET /external/tasks/{source_task_id}/detail ──► 渲染 blocks
└─ 数据交付(任务 6 块 JSON,见第 4 章)
├─ GET /external/tasks/{source_task_id}/periods ──► 当前数据状态(兼容路径)
└─ GET /external/tasks/{source_task_id}/periods/latest ──► 6 块完整 JSON
│
▼
详情页问答(可选)
├─ POST /tasks/{task_id}/qa/init ──► 保存 run_id + thread_ids
├─ POST /tasks/{task_id}/qa/stream(每轮,SSE 渲染对话与席位交付)
├─ (可选)POST /qa/{run_id}/apply-direction ──► 调整注入后续执行
└─ GET /external/runs/{run_id}/messages ──► 历史回放
对方系统删除任务
└─ DELETE /external/tasks/{source_task_id} ──► DeerFlow 侧级联清除
```
## 6. 错误码汇总
| 状态 | 场景 |
|---|---|
| 400 | 任务未绑定分析方案 / 链无席位 / 请求参数非法 / 数据块 key 非法 |
| 401 | 未带 token 或 token 失效 |
| 404 | 任务 / 会话 / 运行记录 / 期不存在 |
| 409 | `source_task_id` 重复创建;会话已收口或线程映射缺失;执行被跳过(业务链已删除/停用) |
@@ -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 子任务尚未结束时不提前运行重点过滤,全部结束后才清理重点范围外的自动生成页面。
@@ -0,0 +1,143 @@
# 上下文超限自动恢复与压测记录
验证日期:2026-09-11。
## 行为
- 模型管理的 `max_input_tokens` 保持可空,不增加 32000 或任何默认模型窗口。明确配置或已有模型 profile 时做预裁剪;两者均无时,首个请求保持原样。
- 主代理、子代理的最终 `ContextWindowGuardMiddleware` 在模型明确返回超限时,对实际发送的请求副本缩短后重试,不重新执行外层摘要、RAG 或记忆注入。原始会话历史不改写。
- 最多首次调用加四次恢复调用。恢复预算来自失败请求的估算值,每次降到约 35%;最后尝试最小请求。30 秒用于限制继续发起恢复尝试,单次调用的超时仍由模型配置控制。
- 不盲目缩减已较小的输出额度,避免推理模型耗尽额度却没有正文。恢复后仍只有思考内容或空白时,普通问答返回自然澄清。
- 不发送空消息列表;不静默删除结构化输出格式和强制工具约束。必要约束无法满足时保留内部失败标记,供业务调用方识别,不能伪造成功 JSON 或已执行的操作。
- 摘要限制针对最终渲染的请求(包括 XML 和摘要提示词)。使用已有摘要裁剪配置与摘要模型上限中较小者,不新增默认模型窗口;`trim_tokens_to_summarize: null` 现在会正确传递。
- 摘要超限也缩短重试;摘要失败时保留可用旧摘要、最近消息和真实用户问题,不缓存失败文案、不阻断主问答。内部摘要调用隔离父级流式回调,避免摘要文字混入回答。
- 上下文错误识别兼容实测 MiniMax 的 `invalid params, context window exceeds limit (2013)`。普通鉴权、额度、网络等错误仍由原错误处理流程负责。
## 自动回归
以下命令从 backend 目录运行:
```powershell
.venv/Scripts/python.exe -m pytest tests/test_context_recovery.py tests/test_context_recovery_transport.py tests/test_llm_error_handling_middleware.py tests/test_summarization_human_message.py tests/test_summarization_multiturn.py tests/test_model_management.py tests/test_writing_mode_report_middleware.py tests/test_roundtable_role_boundary.py -q -p no:cacheprovider
```
首轮结果:135 passed;补充多轮问答后的最终结果:**168 passed**。
覆盖内容:
- 未填写输入上限时不擅自预裁剪,显式上限和绑定模型 profile 生效。
- 中英文、表情、单条超长输入;持续超限;输出预算;极小无效预算。
- 50 组固定随机种子的历史消息、工具调用/结果、隐藏上下文和系统提示组合,验证长度、工具配对与原始历史不变。
- 同步/异步恢复、取消、LangGraph 中断、重试时限、熔断半开探针释放。
- 摘要输入超限、摘要不可用、失败不污染缓存、摘要消息流不泄漏。
- 真实 `PatchedChatOpenAI` 客户端 + 模拟 HTTP 400/SSE + LangGraph 的端到端请求链,验证只返回一次正文、不重复上下文注入、不泄漏超限异常。
Python 改动与新增测试、探针脚本通过 Ruff 检查。
## 真实模型联调
使用当前配置中的 MiniMax-M2.7,仅发送自动生成的合成材料,不使用用户会话或业务文档。40000 行输入的本地估算从 **1,840,102** 降到 **644,001**;这些是保守估算值,不是供应商统计的实际 token 数。
| 模式 | 首次调用 | 第二次调用 | 可见正文 | 耗时 |
| --- | --- | --- | --- | --- |
| 普通调用 | 真实上下文超限 2013 | 自动缩短后成功 | 验证通过 | 18.83 秒 |
| SSE 流式 | 真实上下文超限 2013 | 自动缩短后成功 | 验证通过 | 21.26 秒 |
可重复运行的脚本(真实请求会消耗已配置模型额度):
```powershell
.venv/Scripts/python.exe scripts/probe_context_recovery.py --live --model MiniMax-M2.7 --rows 40000
.venv/Scripts/python.exe scripts/probe_context_recovery.py --live --model MiniMax-M2.7 --rows 40000 --stream
```
探针只在进程内设置 90 秒请求超时与 1024 输出预算,不修改配置文件。初期以较小输出预算测试时发现只有思考、没有正文的问题;30 秒网络超时测试也曾超时,扩大探针超时后完成上述验证。生产调用仍使用自身超时配置。
## 多轮问答追加验证
当前配置的主动摘要开关为 `summarization.enabled: false`,前端默认 `contextCompressionEnabled: false`。前端开启“上下文压缩”后,通过 `summarization_enabled` 传到后端。触发条件为 **120000 token 或 160 条消息**,保留目标为 40000 token。未开启主动摘要时,不会因为对话轮次增加而调用摘要模型;上下文超限后的裁剪重试仍可独立工作。本次没有修改这些开关或生产阈值。
多轮测试发现并修复:
1. 消息数达到阈值但历史尚未达到 token 保留目标时,提前返回导致不触发摘要。现在先执行保留窗口调整,再判断是否有历史可摘要。
2. 反复从全量旧历史重新摘要,旧事实可能被摘要输入截断删除。现在使用“上次摘要 + 新增历史”增量合并,并保留完整原始消息。
3. 摘要提示词与历史拼接后整体截断,可能保留提示词却删除历史开头。现在先计算提示词与已有摘要开销,再给历史分配预算。
4. 历史回复的思考文本、历史 token 用量缩放、摘要输出的思考文本可能干扰事实提取。现在清理这些信息的临时副本,存储的原始聊天记录不改写。
5. 摘要模型可能执行历史中的“只回复已记住”。现在明确区分摘要任务和历史数据,过滤纯确认回复;摘要失败时保留尚未成功摘要的事实,由最终 guard 按实际需要裁剪,不把确认文案当记忆。
6. 补齐 fraction 阈值、复合条件和绑定模型 profile 的多轮处理。
| 测试 | 真正生成摘要的轮次 | 结果 |
| --- | --- | --- |
| 当前阈值的 105 轮模拟,逐轮重建 agent | 81、101 | 210 条消息完整,早期事实可回忆 |
| 小摘要预算的 24 轮模拟 | 5、8、11、14、17、20、23 | 多次合并后仍保留首轮事实 |
| 12 轮工具问答,同步/异步分别运行 | 多次触发 | 每轮工具恰好执行一次,48 条消息完整,调用与结果不拆散 |
| 真实 MiniMax 12 轮,第一次最终复测 | 5、8、11 | 正确回忆代号、更新后的预算和期限;59.02 秒 |
| 真实 MiniMax 12 轮,第二次独立复测 | 5、8、11 | 再次通过相同事实检查;68.16 秒 |
真实测试为加快验证,仅在探针进程内把摘要触发调为 8 条消息、保留 2 条消息,并使用 2048 输出预算;**未设置默认输入上限,也未修改配置文件**。第 9 轮清空进程摘要缓存,检查能否从会话检查点恢复。第 6、7、9、10、12 轮复用摘要,不额外调用摘要模型。每次真正摘要均检查 checkpoint 中的摘要覆盖范围确实更新,而不只检查触发事件。
首轮提供代号 `ORBIT-42`、预算 35 万元和期限 `2026-12-20`,第 6 轮把预算改为 48 万元,第 8 轮把期限改为 `2026-12-28`。两次最终复测第 12 轮均正确返回:
```json
{"code":"ORBIT-42","budget":48,"deadline":"2026-12-28"}
```
复测过程中曾出现“摘要更新了,但答案事实错误”的失败,未计为通过;上述两次成功记录均来自清理历史思考及用量缩放后的版本。逐轮输入场景、正文、摘要正文和缓存更新记录保存在:
- [第一次最终复测](multiturn-probe-20260911.json)
- [第二次独立复测](multiturn-probe-20260911-repeat.json)
复现命令(使用合成材料,会调用已配置模型):
```powershell
.venv/Scripts/python.exe scripts/probe_multiturn_compression.py --live --output docs/multiturn-probe-20260911.json
```
## 前端及验证边界
- 模型管理页已注明输入上限可留空;该 TSX 文件单独转换通过。
- `pnpm typecheck` **未通过**:未改动的 `message-list-item.tsx`、`core/blog`(缺少 nextra)、`GeneralQA.tsx` 等存在类型或依赖错误。本次不把前端全量检查记为通过。
- 真实供应商测试覆盖 MiniMax-M2.7,不能等同于所有供应商均已实测。主代理/子代理和摘要调用以外的独立模型调用不因该中间件自动获得保护;标题生成原本已有本地标题降级。
- 极端情况下会丢失部分材料并请求用户补充;恢复机制不保证缺失信息时仍能完成原任务,也不掩盖鉴权失败、额度不足或服务不可用。
## 部署版本兼容修复(2026-09-11)
前面的本机测试使用 LangChain 1.3.15,未覆盖部署的 1.2.15,这是此前验证的遗漏。
已在真实 1.2.15 包上复现 `_trigger_clauses` AttributeError:旧基类只提供 `_trigger_conditions`。
现在应用自行保存和计算触发条件,不再读取这两个版本相关的私有属性;字典条件的 AND、条件列表的 OR 和旧参数映射保持一致。
此前新增的 `internal_call_transformer` 依赖已移除。没有要求升级部署依赖,没有引入默认输入上限。
本次将用户提供的全部 20 个包安装到隔离目录,以 PYTHONPATH 优先加载,未替换本机虚拟环境。
其余未提供版本的依赖沿用本机,测试主机为 Windows;不把该结果宣称为生产服务器完整环境的启动验证。
- 用户版本组合(LangChain 1.2.15、Core 1.3.2、LangGraph 1.1.9 等):177 项通过,26.26 秒。
- 本机新版组合:同样 177 项通过,24.64 秒。
- 覆盖 Gateway / 主代理导入、105 轮问答、重复压缩、缓存恢复、同步/异步工具和消息流隔离。
- 新增删除基类两个私有触发属性后仍能正常判断阈值的回归测试,涵盖 tuple、OR、AND、混合和未启用情况。
- 用户版本组合下真实 MiniMax-M2.7 12 轮问答:第 5、8、11 轮摘要,第 9 轮清空进程缓存后正常恢复;最终准确返回代号 ORBIT-42、更新预算 48、更新期限 2026-12-28。54.53 秒,无属性错误。
- 所有被调用的摘要中间件方法均已检查能在 1.2.15 下解析;Ruff 通过。
[用户版本组合的真实多轮记录及 20 个版本号](multiturn-probe-langchain-1.2.15.json)。
## 隐藏压缩提示与连续问答复测
后台仅记录真实摘要日志,不再向 custom 流发送 `context_compacting` 通知;前端也忽略旧后台的同名事件,不弹 toast。摘要开关和压缩功能保留。
新增模型每轮上报 150000 输入 token 的 12 轮测试,使用 2000 token 摘要触发阈值:同步和异步均仅在第 2 轮摘要,后续复用;历史用量数据未修改。该测试在用户的 1.2.15 依赖组合和本机新版均通过。部署组合相关回归共 118 项通过(22.28 秒)。
尚未拿到用户连续弹提示时的后台 cache / counted_tokens / msgs 日志,未复现该会话的每轮重复摘要,不能把隐藏提示当作已修复重复摘要根因。
## 修复压缩后每轮重新摘要
已用过长摘要复现另一条真实缺陷路径:摘要加保留内容在生成当时就超过阈值,旧逻辑之后每轮都会把同一个超阈值基础内容再次送摘要模型。12 轮测试中第 2~12 轮均调用摘要。此前的简短摘要测试没有覆盖此边界;尚无生产会话日志,不能据此断言这是用户会话唯一原因。
修复:缓存可用且上次压缩结果本身就超阈值时,要求**压缩后新增内容**累计达到原触发阈值的 25% 才再摘要。正常压缩后低于阈值的路径保持原判定。这个比例用于避免无效重复摘要,不是模型输入上限;最终 ContextWindowGuard 的裁剪及超限重试继续工作。子集计数移除原请求 usage_metadata 的缩放影响,只修改临时副本。日志新增 waiting_for_growth,并区分 unusable 与 stale 缓存。
用户提供的 1.2.15 等 20 个包组合最终回归 **187 passed,26.59 秒**:
- 12 轮过长摘要 + 短追问:仅第 2 轮摘要,修复前为第 2~12 轮;同步/异步均通过。
- 36 轮持续增加材料:第 2、19、36 轮摘要;每轮清空进程缓存,仍可由 checkpoint 恢复,不是直接禁用后续摘要。
- 超长摘要 + 严格 800 token 测试模型 + 连续 8 轮:分别验证配置上限(预裁剪,无拒绝)和不配置(真实抛出 context_length_exceeded 后缩短重试),同步/异步四种组合均正常回答最新问题;原会话消息数完整。这里使用可控测试模型,未宣称所有供应商均实测。
- 既有 105 轮、工具执行、首次阈值、缓存恢复、旧版导入及摘要流隔离回归继续通过。Ruff 通过。
@@ -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 无状态运行
@@ -0,0 +1,133 @@
{
"model": "MiniMax-M2.7",
"trigger_messages": 8,
"keep_messages": 2,
"rows": [
{
"turn": 1,
"compression": "passthrough",
"history_messages": 2,
"has_cache": false,
"visible": "已记住。",
"elapsed_seconds": 6.34,
"summary_updated": false
},
{
"turn": 2,
"compression": "passthrough",
"history_messages": 4,
"has_cache": false,
"visible": "已记住。",
"elapsed_seconds": 2.16,
"summary_updated": false
},
{
"turn": 3,
"compression": "passthrough",
"history_messages": 6,
"has_cache": false,
"visible": "已记住。",
"elapsed_seconds": 1.45,
"summary_updated": false
},
{
"turn": 4,
"compression": "passthrough",
"history_messages": 8,
"has_cache": false,
"visible": "已记住。",
"elapsed_seconds": 2.77,
"summary_updated": false
},
{
"turn": 5,
"compression": "summarize",
"history_messages": 10,
"has_cache": true,
"visible": "项目代号为ORBIT-42,预算为35万元,截止日期为2026年12月20日。",
"elapsed_seconds": 18.06,
"summary_updated": true,
"summary_text": "## SESSION INTENT\nThe user is establishing project parameters for a knowledge base/Q&A system with report export capabilities. The AI is being used as a memory keeper to retain key project constraints and requirements for future reference.\n\n## SUMMARY\nProject ORBIT-42 is defined with the following parameters:\n- **Budget**: 35万元 (350,000 yuan)\n- **Deadline**: 2026-12-20\n- **Scope**: System must support document retrieval, question answering, and report export\n- **Data Constraint**: Real customer data cannot be uploaded; only synthetic test materials are permitted\n- **Acceptance Criteria**: Must verify citation sources, output format compliance, and interface stability\n\nNo implementation work has begun. This session consists entirely of requirements gathering and acknowledgment.\n\n## ARTIFACTS\nNone. No files, code, or resources have been created or modified during this session.\n\n## NEXT STEPS\n- Begin system architecture design and implementation planning\n- Create synthetic test data materials for development and testing\n- Implement core functionalities: document retrieval, Q&A engine, and report export module\n- Establish validation framework for citation checking, format compliance, and interface stability testing"
},
{
"turn": 6,
"compression": "reuse",
"history_messages": 12,
"has_cache": true,
"visible": "已更新。",
"elapsed_seconds": 2.14,
"summary_updated": false
},
{
"turn": 7,
"compression": "reuse",
"history_messages": 14,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 3.05,
"summary_updated": false
},
{
"turn": 8,
"compression": "summarize",
"history_messages": 16,
"has_cache": true,
"visible": "已更新。",
"elapsed_seconds": 14.64,
"summary_updated": true,
"summary_text": "## SESSION INTENT\n\n用户正在建立知识库/问答系统的项目参数,需要AI作为记忆 keeper 保留关键项目约束和需求供后续参考。\n\n## SUMMARY\n\n项目 ORBIT-42 的参数如下:\n\n- **项目代号**:ORBIT-42\n- **预算**:48万元(已从35万元更新)\n- **截止日期**:2026年12月20日\n- **项目范围**:系统需支持文档检索、问答、报告导出三大功能\n- **数据约束**:不得使用真实客户数据,仅可使用合成测试材料\n- **报告格式约束**:报告必须用中文撰写,且先写结论再写依据\n- **验收标准**:验证引用来源、输出格式合规、接口稳定性\n\n## ARTIFACTS\n\n无。暂无文件或代码被创建或修改。\n\n## NEXT STEPS\n\n- 开始系统架构设计与实现规划\n- 创建合成测试数据材料用于开发和测试\n- 实现核心功能:文档检索、问答引擎、报告导出模块\n- 建立验证框架用于引用检查、格式合规和接口稳定性测试"
},
{
"turn": 9,
"compression": "reuse",
"history_messages": 18,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 3.31,
"summary_updated": false
},
{
"turn": 10,
"compression": "reuse",
"history_messages": 20,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 2.72,
"summary_updated": false
},
{
"turn": 11,
"compression": "summarize",
"history_messages": 22,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 9.44,
"summary_updated": true,
"summary_text": "## SESSION INTENT\n\n用户正在建立知识库/问答系统的项目参数,需要AI作为记忆 keeper 保留关键项目约束和需求供后续参考。\n\n## SUMMARY\n\n项目 ORBIT-42 的参数如下:\n\n- **项目代号**:ORBIT-42\n- **预算**:48万元\n- **截止日期**:2026年12月28日(已更新)\n- **项目范围**:系统需支持文档检索、问答、报告导出三大功能\n- **数据约束**:不得使用真实客户数据,仅可使用合成测试材料\n- **报告格式约束**:报告必须用中文撰写,且先写结论再写依据\n- **验收标准**:\n - 验证引用来源、输出格式合规、接口稳定性\n - 必须覆盖多轮问答和长材料(新增)\n- **数据留存要求**:测试报告必须留存,不得删除已有聊天记录(新增)\n\n## ARTIFACTS\n\n无。暂无文件或代码被创建或修改。\n\n## NEXT STEPS\n\n- 开始系统架构设计与实现规划\n- 创建合成测试数据材料用于开发和测试\n- 实现核心功能:文档检索、问答引擎、报告导出模块\n- 建立验证框架用于引用检查、格式合规和接口稳定性测试\n- 设计聊天记录和测试报告的持久化存储方案"
},
{
"turn": 12,
"compression": "reuse",
"history_messages": 24,
"has_cache": true,
"visible": "```json\n{\n \"code\": \"ORBIT-42\",\n \"budget\": 48,\n \"deadline\": \"2026-12-28\"\n}\n```",
"elapsed_seconds": 2.02,
"summary_updated": false
}
],
"fact_check": true,
"summary_rounds": [
5,
8,
11
],
"reuse_rounds": [
6,
7,
9,
10,
12
],
"outcome": "passed",
"elapsed_seconds": 68.16
}
@@ -0,0 +1,133 @@
{
"model": "MiniMax-M2.7",
"trigger_messages": 8,
"keep_messages": 2,
"rows": [
{
"turn": 1,
"compression": "passthrough",
"history_messages": 2,
"has_cache": false,
"visible": "已记住",
"elapsed_seconds": 9.39,
"summary_updated": false
},
{
"turn": 2,
"compression": "passthrough",
"history_messages": 4,
"has_cache": false,
"visible": "已记住",
"elapsed_seconds": 2.08,
"summary_updated": false
},
{
"turn": 3,
"compression": "passthrough",
"history_messages": 6,
"has_cache": false,
"visible": "已记住",
"elapsed_seconds": 1.62,
"summary_updated": false
},
{
"turn": 4,
"compression": "passthrough",
"history_messages": 8,
"has_cache": false,
"visible": "已记住",
"elapsed_seconds": 3.27,
"summary_updated": false
},
{
"turn": 5,
"compression": "summarize",
"history_messages": 10,
"has_cache": true,
"visible": "项目代号为ORBIT-42,预算35万元,截止日期为2026年12月20日。",
"elapsed_seconds": 12.16,
"summary_updated": true,
"summary_text": "## SESSION INTENT\nEstablish and remember project ORBIT-42 parameters for an upcoming project session, including budget, deadline, scope, constraints, and acceptance criteria.\n\n## SUMMARY\n- Project Code: ORBIT-42\n- Budget: 35万元 (350,000 RMB)\n- Deadline: 2026-12-20\n- Scope: System must support document retrieval, Q&A functionality, and report export capabilities\n- Constraint: Real customer data cannot be uploaded; only synthetic test materials may be used\n- Acceptance Criteria: Must verify citation sources, output format compliance, and API stability\n\n## ARTIFACTS\nNone created or modified.\n\n## NEXT STEPS\nReady to begin project ORBIT-42 implementation. Next steps will involve setting up the system architecture, creating synthetic test datasets, and implementing the three core features (document retrieval, Q&A, report export) within the budget and timeline constraints."
},
{
"turn": 6,
"compression": "reuse",
"history_messages": 12,
"has_cache": true,
"visible": "已更新。",
"elapsed_seconds": 2.12,
"summary_updated": false
},
{
"turn": 7,
"compression": "reuse",
"history_messages": 14,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 2.84,
"summary_updated": false
},
{
"turn": 8,
"compression": "summarize",
"history_messages": 16,
"has_cache": true,
"visible": "已更新。",
"elapsed_seconds": 8.55,
"summary_updated": true,
"summary_text": "## SESSION INTENT\nEstablish and remember project ORBIT-42 parameters for an upcoming project session, including budget, deadline, scope, constraints, and acceptance criteria.\n\n## SUMMARY\n- Project Code: ORBIT-42\n- Budget: 48万元 (480,000 RMB)\n- Deadline: 2026-12-20\n- Scope: System must support document retrieval, Q&A functionality, and report export capabilities\n- Constraint: Real customer data cannot be uploaded; only synthetic test materials may be used\n- Additional Constraint: Reports must be in Chinese, with conclusions written first followed by supporting evidence/reasoning\n- Acceptance Criteria: Must verify citation sources, output format compliance, and API stability\n\n## ARTIFACTS\nNone created or modified.\n\n## NEXT STEPS\nReady to begin project ORBIT-42 implementation. Next steps will involve setting up the system architecture, creating synthetic test datasets, and implementing the three core features (document retrieval, Q&A, report export) within the budget and timeline constraints."
},
{
"turn": 9,
"compression": "reuse",
"history_messages": 18,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 1.51,
"summary_updated": false
},
{
"turn": 10,
"compression": "reuse",
"history_messages": 20,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 1.74,
"summary_updated": false
},
{
"turn": 11,
"compression": "summarize",
"history_messages": 22,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 10.69,
"summary_updated": true,
"summary_text": "## SESSION INTENT\nEstablish and remember project ORBIT-42 parameters for an upcoming project session, including budget, deadline, scope, constraints, and acceptance criteria.\n\n## SUMMARY\n- Project Code: ORBIT-42\n- Budget: 48万元 (480,000 RMB)\n- Deadline: 2026-12-28 (updated from 2026-12-20)\n- Scope: System must support document retrieval, Q&A functionality, and report export capabilities\n- Constraint: Real customer data cannot be uploaded; only synthetic test materials may be used\n- Additional Constraint: Reports must be in Chinese, with conclusions written first followed by supporting evidence/reasoning\n- Acceptance Criteria: Must verify citation sources, output format compliance, and API stability\n- Updated Acceptance Criteria: Must cover multi-turn Q&A and long materials testing\n- Data Retention Requirement: Must retain test reports; cannot delete existing chat records\n\n## ARTIFACTS\nNone created or modified.\n\n## NEXT STEPS\nReady to begin project ORBIT-42 implementation. Next steps will involve setting up the system architecture, creating synthetic test datasets, and implementing the three core features (document retrieval, Q&A, report export) within the budget and timeline constraints."
},
{
"turn": 12,
"compression": "reuse",
"history_messages": 24,
"has_cache": true,
"visible": "```json\n{\n \"code\": \"ORBIT-42\",\n \"budget\": 48,\n \"deadline\": \"2026-12-28\"\n}\n```",
"elapsed_seconds": 3.02,
"summary_updated": false
}
],
"fact_check": true,
"summary_rounds": [
5,
8,
11
],
"reuse_rounds": [
6,
7,
9,
10,
12
],
"outcome": "passed",
"elapsed_seconds": 59.02
}
@@ -0,0 +1,155 @@
{
"model": "MiniMax-M2.7",
"trigger_messages": 8,
"keep_messages": 2,
"rows": [
{
"turn": 1,
"compression": "passthrough",
"history_messages": 2,
"has_cache": false,
"visible": "已记住",
"elapsed_seconds": 4.78,
"summary_updated": false
},
{
"turn": 2,
"compression": "passthrough",
"history_messages": 4,
"has_cache": false,
"visible": "已记住。",
"elapsed_seconds": 3.19,
"summary_updated": false
},
{
"turn": 3,
"compression": "passthrough",
"history_messages": 6,
"has_cache": false,
"visible": "已记住。",
"elapsed_seconds": 1.91,
"summary_updated": false
},
{
"turn": 4,
"compression": "passthrough",
"history_messages": 8,
"has_cache": false,
"visible": "已记住。",
"elapsed_seconds": 1.39,
"summary_updated": false
},
{
"turn": 5,
"compression": "summarize",
"history_messages": 10,
"has_cache": true,
"visible": "项目代号为ORBIT-42,预算为35万元,截止日期为2026年12月20日。",
"elapsed_seconds": 13.5,
"summary_updated": true,
"summary_text": "## SESSION INTENT\nSet up and remember project ORBIT-42 parameters for future reference in this session.\n\n## SUMMARY\n- Project Code: ORBIT-42\n- Budget: 35万元 (350,000 RMB)\n- Deadline: 2026-12-20\n- Scope: System must support information retrieval, Q&A functionality, and report export\n- Constraint: Real customer data cannot be uploaded; only synthetic test materials may be used\n- Acceptance Criteria: Must verify reference sources, output format compliance, and interface stability\n\n## ARTIFACTS\nNone created or modified.\n\n## NEXT STEPS\nProject parameters are stored. Awaiting further instructions on ORBIT-42 project implementation."
},
{
"turn": 6,
"compression": "reuse",
"history_messages": 12,
"has_cache": true,
"visible": "已更新。",
"elapsed_seconds": 1.62,
"summary_updated": false
},
{
"turn": 7,
"compression": "reuse",
"history_messages": 14,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 1.31,
"summary_updated": false
},
{
"turn": 8,
"compression": "summarize",
"history_messages": 16,
"has_cache": true,
"visible": "已更新。",
"elapsed_seconds": 9.2,
"summary_updated": true,
"summary_text": "## SESSION INTENT\nSet up and remember project ORBIT-42 parameters for future reference in this session.\n\n## SUMMARY\n- Project Code: ORBIT-42\n- Budget: 48万元 (updated from 35万元)\n- Deadline: 2026-12-20\n- Scope: System must support information retrieval, Q&A functionality, and report export\n- Constraint: Real customer data cannot be uploaded; only synthetic test materials may be used\n- NEW Constraint: Reports must be in Chinese, conclusions first then basis\n- Acceptance Criteria: Must verify reference sources, output format compliance, and interface stability\n\n## ARTIFACTS\nNone created or modified.\n\n## NEXT STEPS\nProject parameters are stored. Awaiting further instructions on ORBIT-42 project implementation."
},
{
"turn": 9,
"compression": "reuse",
"history_messages": 18,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 2.24,
"summary_updated": false
},
{
"turn": 10,
"compression": "reuse",
"history_messages": 20,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 1.67,
"summary_updated": false
},
{
"turn": 11,
"compression": "summarize",
"history_messages": 22,
"has_cache": true,
"visible": "已记住。",
"elapsed_seconds": 10.02,
"summary_updated": true,
"summary_text": "## SESSION INTENT\nSet up and remember project ORBIT-42 parameters for future reference in this session.\n\n## SUMMARY\n- Project Code: ORBIT-42\n- Budget: 48万元\n- Deadline: 2026-12-28 (updated from 2026-12-20)\n- Scope: System must support information retrieval, Q&A functionality, and report export\n- Constraint: Real customer data cannot be uploaded; only synthetic test materials may be used\n- Constraint: Reports must be in Chinese, conclusions first then basis\n- Acceptance Criteria:\n - Must verify reference sources\n - Output format compliance\n - Interface stability\n - Must cover multi-turn Q&A and long documents\n - Must retain test reports, cannot delete existing chat records\n\n## ARTIFACTS\nNone created or modified.\n\n## NEXT STEPS\nProject parameters are stored. Awaiting further instructions on ORBIT-42 project implementation."
},
{
"turn": 12,
"compression": "reuse",
"history_messages": 24,
"has_cache": true,
"visible": "```json\n{\n \"code\": \"ORBIT-42\",\n \"budget\": 48,\n \"deadline\": \"2026-12-28\"\n}\n```",
"elapsed_seconds": 3.62,
"summary_updated": false
}
],
"fact_check": true,
"summary_rounds": [
5,
8,
11
],
"reuse_rounds": [
6,
7,
9,
10,
12
],
"outcome": "passed",
"elapsed_seconds": 54.53,
"tested_dependency_versions": {
"langchain": "1.2.15",
"langchain-anthropic": "1.4.1",
"langchain-core": "1.3.2",
"langchain-deepseek": "1.0.1",
"langchain-google-genai": "4.2.2",
"langchain-mcp-adapters": "0.2.2",
"langchain-openai": "1.2.1",
"langchain-protocol": "0.0.12",
"langfuse": "4.5.1",
"langgraph": "1.1.9",
"langgraph-api": "0.8.1",
"langgraph-checkpoint": "4.0.2",
"langgraph-checkpoint-postgres": "3.0.5",
"langgraph-checkpoint-sqlite": "3.0.3",
"langgraph-cli": "0.4.24",
"langgraph-prebuilt": "1.0.11",
"langgraph-runtime-inmem": "0.28.0",
"langgraph-sdk": "0.3.13",
"langsmith": "0.7.36",
"lark-oapi": "1.5.5"
}
}
@@ -1,6 +1,59 @@
# Conversation Summarization
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.
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
also exceed its real window because the final request contains more than chat
history: the system prompt, tool schemas, hidden memory/RAG context, and
historical reasoning are all sent to the provider. For that reason DeerFlow
also supports a model-specific, always-on hard limit:
```yaml
models:
- name: qwen-local
use: langchain_openai:ChatOpenAI
model: Qwen3.8-27B
# This is the safe INPUT ceiling, not the model's advertised total window.
# For a 200k deployment, 160k leaves room for output and tokenizer variance.
max_input_tokens: 160000
max_tokens: 8192
```
`max_tokens` limits generated output. `max_input_tokens` limits the complete
input request and is consumed only by DeerFlow; it is never sent to the
OpenAI-compatible endpoint. The final model middleware conservatively counts
the system message, ordinary and hidden messages, tool schemas, response
format, tool calls/results, and historical reasoning. If the request is over
budget it transiently removes the oldest complete turns, drops expendable
hidden context, and truncates oversized tool/message bodies as a last resort.
The checkpoint and the conversation displayed in the UI are unchanged.
For LangChain-compatible configuration, the same limit may be expressed as
`profile.max_input_tokens`. A top-level `max_input_tokens` is preferred in this
project because it is visible alongside `max_tokens` in model configuration.
Do not set `max_tokens: 200000`: that asks the provider to generate up to 200k
tokens and does not protect the input context.
## Overview
@@ -19,7 +72,7 @@ The summarization feature uses DeerFlow's `DeerFlowSummarizationMiddleware` (a s
LangChain's stock `SummarizationMiddleware` runs in `before_model` and persists a `RemoveMessage(REMOVE_ALL_MESSAGES)` to the checkpoint — which permanently deletes the original Q&A from both the model context *and* the conversation the frontend renders. DeerFlow overrides this:
- `before_model` / `abefore_model` are **no-ops** (the destructive path is disabled).
- Compression runs in `wrap_model_call` / `awrap_model_call`: for a single model call, the message list is replaced with `[summary, *recent]` via `request.override(messages=...)`. **Nothing is written back to state.**
- Compression runs in `wrap_model_call` / `awrap_model_call`: for a single model call, the hidden summary is merged into LangChain's dedicated `ModelRequest.system_message` slot and the ordinary message list is replaced with the recent tail. This guarantees exactly one leading system message even for strict Qwen-style or generic `langchain_chat` adapters. **Nothing is written back to state.**
- The full original messages stay in the checkpoint, so the frontend keeps showing the user's real conversation. The summary message is named `summary` and flagged `hide_from_ui`, and because it only ever lives in the transient request it never reaches the frontend at all.
- The generated summary is cached **per-thread** so we don't pay an extra LLM call on every model invocation. The cached summary is reused (no LLM call) until enough *new* messages accumulate to push the compressed context back over the trigger threshold (a sticky boundary mirroring the stock middleware's post-compaction behavior). The cache is bounded (oldest threads evicted first).
- **Threshold-first**: while the full current conversation is still under the trigger, the request is passed through untouched — no cache lookup, no summary substitution. Compression only ever engages once the real context crosses the configured trigger.
@@ -185,7 +238,7 @@ The default LangChain prompt instructs the model to:
- Messages to summarize (older messages beyond the `keep` threshold)
- Messages to preserve (recent messages within the `keep` threshold)
4. **Summary Generation**: The model generates a concise summary of the older messages
5. **Context Replacement (transient)**: The message list **handed to the model for this call** is rebuilt as `[summary, *recent]`. The persisted state is left untouched — old messages are **not** removed, so the frontend still renders the full original conversation.
5. **Context Replacement (transient)**: The request **handed to the model for this call** is rebuilt with one system message (agent prompt + hidden summary) and the recent non-system tail. The persisted state is left untouched — old messages are **not** removed, so the frontend still renders the full original conversation.
6. **AI/Tool Pair Protection**: The system ensures AI messages and their corresponding tool messages stay together
7. **Skill Rescue**: Before the summary is generated, the most recently loaded skill files (tool results whose tool name is in `skill_file_read_tool_names` and whose target path is under `skills.container_path`) are lifted out of the summarization set and prepended to the preserved tail. Selection walks newest-first under three budgets: `preserve_recent_skill_count`, `preserve_recent_skill_tokens`, and `preserve_recent_skill_tokens_per_skill`. The triggering AIMessage and all of its paired ToolMessages move together so tool_call ↔ tool_result pairing stays intact.
@@ -202,12 +255,14 @@ The middleware intelligently preserves message context:
- **Recent Messages**: Always kept intact based on `keep` configuration
- **AI/Tool Pairs**: Never split - if a cutoff point falls within tool messages, the system adjusts to keep the entire AI + Tool message sequence together
- **Summary Format**: Summary is injected as a HumanMessage with the format:
```
Here is a summary of the conversation to date:
[Generated summary text]
```
- **Summary Format**: Summary is merged into the leading SystemMessage with the format:
```
Internal conversation summary for continuity only...
<internal_context_summary>
[generated summary text]
</internal_context_summary>
```
## Best Practices
@@ -329,7 +384,8 @@ Summarization runs after ThreadData and Sandbox initialization but before Title
- Configuration is loaded once at startup
- Summarization is **non-destructive**: the summary replaces older messages only in the transient model request (`wrap_model_call`), never in persisted state. The checkpointer therefore keeps the full original conversation
- The per-thread summary cache lives in a **process-wide, thread-safe LRU** (`_GLOBAL_SUMMARY_CACHE`, bounded), **not** on the middleware instance. `make_lead_agent` rebuilds a fresh middleware every run, so an instance cache would be cold each turn and re-run a blocking summary LLM call on every turn of a long conversation; the module-level cache keyed by `thread_id` lets a summary survive across turns. It is a cost optimization only — a cold/stale cache simply re-summarizes, which is always correct
- The per-thread summary cache lives in a **process-wide, thread-safe LRU** (`_GLOBAL_SUMMARY_CACHE`, bounded), **not** on the middleware instance. `make_lead_agent` rebuilds a fresh middleware every run, so an instance cache would be cold each turn and re-run a blocking summary LLM call on every turn of a long conversation; the module-level cache keyed by `thread_id` lets a summary survive across turns. It is a cost optimization only — a cold/stale cache simply re-summarizes, which is always correct.
- Compression normalizes LangChain's split request representation before invoking the next middleware/model: the dedicated `system_message` contains all system context exactly once, while `messages` contains no `SystemMessage`. This makes the fix independent of the configured provider adapter.
## Example Configurations