227 lines
12 KiB
Markdown
227 lines
12 KiB
Markdown
# 技能自我进化(Skill Self-Evolution)文档
|
|
|
|
> 本文描述 DeerFlow 中"技能(Skill)自我进化"的完整实现流程,以及与之配套的**用户分权模型**。
|
|
|
|
---
|
|
|
|
## 一、概述
|
|
|
|
DeerFlow 的技能是一份 `SKILL.md`(YAML frontmatter + Markdown 正文),作为**程序性知识**注入到 Agent 的 system prompt 中。"自我进化"指系统让技能库**自动维护、自动增长、自动收敛**的一整套机制:
|
|
|
|
- **增长**:Agent 在对话中解决了一个非平凡的工作流后,主动询问用户,把流程沉淀为一个新技能。
|
|
- **维护**:后台策展程序(curator)周期性巡检,把长期不用的技能标记为"过时""归档",并用 LLM 评审是否应合并重复技能。
|
|
- **收敛**:重复、过窄、过时的技能被合并进"伞技能"或归档,保持技能库可发现性。
|
|
|
|
整个系统**绝不删除技能**(除非用户/Agent 明确删除)——自动化的上限是"软归档",可随时恢复。
|
|
|
|
---
|
|
|
|
## 二、核心概念:三个维度
|
|
|
|
每个技能由三个正交维度描述:
|
|
|
|
### 2.1 来源 `source`
|
|
|
|
| 值 | 含义 | 产生方式 |
|
|
|---|---|---|
|
|
| `agent` | Agent 在对话中创建 | `skill_manage(action="create")` 工具 |
|
|
| `upload` | 用户手动上传 | `POST /api/skills/install-upload`(.zip/.skill 包) |
|
|
|
|
### 2.2 生命周期状态 `state`
|
|
|
|
| 值 | 含义 | 是否进 Agent prompt |
|
|
|---|---|---|
|
|
| `active` | 活跃 | ✅ 是 |
|
|
| `stale` | 过时(长期未使用,被 curator 标记) | ❌ 否 |
|
|
| `archived` | 归档(软归档,文件保留,可恢复) | ❌ 否 |
|
|
|
|
状态机:`active → stale → archived`,可由用户手动逆向恢复(`archived/stale → active`)。
|
|
|
|
### 2.3 归属与可见性
|
|
|
|
由数据库 `skills` 表记录:
|
|
|
|
| 字段 | 含义 |
|
|
|---|---|
|
|
| `name` (主键) | 技能名,全局唯一,等于目录名 |
|
|
| `owner_user_id` | 所有者;`NULL` = 内置/历史遗留技能 |
|
|
| `published` | 是否对其他用户可见 |
|
|
|
|
---
|
|
|
|
## 三、存储架构
|
|
|
|
技能数据分布在**三处**,文件系统是"真理之源",DB 与 `.usage.json` 是元数据。
|
|
|
|
```
|
|
deer-flow/skills/
|
|
├── public/{name}/SKILL.md # 内置技能,只读,所有人可见
|
|
├── custom/{name}/SKILL.md # 用户/Agent 创建的技能(所有用户平铺,不按用户分目录)
|
|
│ └── .history/{name}.jsonl # 每个 custom 技能的变更历史
|
|
└── .usage.json # 全局元数据(按技能名索引)
|
|
```
|
|
|
|
数据库 `skills` 表(`deerflow.persistence.skills`):记录归属与发布状态。
|
|
|
|
`.usage.json` 单个条目结构:
|
|
|
|
```json
|
|
{
|
|
"my-skill": {
|
|
"use_count": 0, "view_count": 3, "patch_count": 0,
|
|
"last_used_at": null, "last_viewed_at": "...", "last_patched_at": null,
|
|
"created_at": "...", "state": "active", "pinned": false,
|
|
"archived_at": null, "source": "agent"
|
|
}
|
|
}
|
|
```
|
|
|
|
> **关键设计**:custom 技能在文件系统里**不按用户分目录**,所有用户的技能平铺在 `custom/` 下,归属完全由数据库 `skills` 表管理。用户 A 发布技能后,用户 B 直接读取**同一个** `SKILL.md`,无文件复制。
|
|
|
|
涉及文件:
|
|
- 文件存储:`packages/harness/deerflow/skills/storage/local_skill_storage.py`
|
|
- DB 表:`packages/harness/deerflow/persistence/skills/`
|
|
- 元数据:`packages/harness/deerflow/skills/usage.py`
|
|
|
|
---
|
|
|
|
## 四、自我进化的两条路径
|
|
|
|
### 4.1 路径一:Agent 主动进化(对话中)
|
|
|
|
**触发**:由 `skill_evolution.enabled` 开关门控。该开关**不是单纯的全局开关** —— 它的生效值由两层配置深度合并得到:
|
|
|
|
1. 全局基线:`config.yaml` 的 `skill_evolution` 段;
|
|
2. 每用户覆盖:`users/{user_id}/skill_evolution.json`(用户可在自己的配置文件里覆盖该开关及 curator 各项参数)。
|
|
|
|
合并由 `load_skill_evolution_config_for_user(user_id)` 完成(见 `config/skill_evolution_runtime.py`):无 `user_id` 或无覆盖文件时返回全局配置,否则把 per-user 文件深度合并到全局之上。
|
|
|
|
开启后:
|
|
|
|
- **`skill_manage` / `skill_list` / `skill_view` 工具是否注入给 Agent** —— `tools/tools.py` 按 **per-user 生效配置** 判断(`load_skill_evolution_config_for_user(get_effective_user_id())`)。
|
|
- **system prompt 是否注入"技能自我进化"指引段**(`_build_skill_evolution_section`,见 `agents/lead_agent/prompt.py`)—— 当前按 **全局 `config.yaml`** 的 `skill_evolution.enabled` 判断,**尚未读取 per-user 覆盖**。
|
|
|
|
> ⚠️ **已知不一致**:工具门控走 per-user 配置,提示词门控走全局配置。若某用户仅在 per-user 文件里开启了开关(全局为 `false`),他会拿到 `skill_manage` 工具,但 system prompt 里没有自我进化指引段 —— LLM 有工具却缺少"何时/如何用"的指引。修复方向是让 `prompt.py` 也改用 `load_skill_evolution_config_for_user`,并把 prompt section 缓存 key 加上 `user_id` 防止跨用户串味。
|
|
|
|
**指引逻辑**(对齐 Hermes 风格):
|
|
|
|
- 完成非平凡任务后(5+ 工具调用 / 克服过非显然错误 / 用户纠正后的方案奏效 / 发现可复用工作流),Agent **主动询问用户**是否保存为技能。
|
|
- 用户同意 → `skill_manage(action="create", name=..., content=...)`。
|
|
- 一次性查询、闲聊、单工具简单回答 → 跳过,不打扰用户。
|
|
- 如果用了一个已有技能但发现它不完整 → 立即 `skill_manage(action="patch")` 修补,不等用户开口。
|
|
|
|
**`skill_manage` 工具**(`tools/skill_manage_tool.py`)的 action:
|
|
|
|
| action | 作用 |
|
|
|---|---|
|
|
| `create` | 新建技能(写文件 + `.usage.json`(source=agent)+ **DB 归属行**) |
|
|
| `edit` | 整体替换 SKILL.md |
|
|
| `patch` | 局部替换(优先,token 高效) |
|
|
| `delete` | 删除(清文件 + `.usage.json` + DB 行 + cascade 解引用) |
|
|
| `write_file` / `remove_file` | 管理技能的支持文件 |
|
|
| `list` / `view` | 只读别名,转发到 `skill_list` / `skill_view` |
|
|
|
|
**create 的完整副作用**:
|
|
1. 写 `custom/{name}/SKILL.md`
|
|
2. 追加变更历史
|
|
3. `mark_agent_created(name)` → `.usage.json` 记 `source=agent`
|
|
4. `_ensure_skill_db_registered()` → DB `skills` 表插入归属行(`owner_user_id` = 当前用户)
|
|
|
|
> **历史 bug 已修**:早期 create 只写文件,不写 DB,导致 Agent 创建的技能在 Web UI 列表里**看不见**(列表对 custom 技能强制要求 DB 归属行)。现已补齐。
|
|
|
|
**delete 的安全机制**:
|
|
- `_assert_agent_may_delete()` 在动文件**之前**做 DB 归属预校验 —— 非本人技能、内置技能、无归属记录的孤儿技能,一律拒绝。
|
|
- 删除后同步清理 `.usage.json`、DB 行,并 `cascade` 把该技能从所有引用它的 Agent 的 `config.yaml` 中剥离 + 通知。
|
|
|
|
### 4.2 路径二:Curator 后台策展(定时)
|
|
|
|
**入口**:`maybe_run_curator()`(应用启动时的异步入口)→ 遍历所有用户 → 对每个间隔已到的用户,启动一个后台线程 `_curator_thread_target(user_id)`。
|
|
|
|
**线程上下文**:线程入口用 `set_current_user(_CuratorUser(user_id))` 绑定用户上下文 —— 保证整轮策展(及其调用的工具)都能解析到正确的 `user_id`。
|
|
|
|
**`run_curator_review(user_id)` 三阶段**:
|
|
|
|
**阶段 1 — 年龄自动流转** `apply_automatic_transitions()`:
|
|
- 仅处理"该用户拥有 + 未被任何 Agent 引用 + 未发布"的 agent 创建技能。
|
|
- 跳过 `pinned=true` 的技能。
|
|
- 闲置 ≥ `stale_after_days`(默认 30 天)→ 标 `stale`。
|
|
- 闲置 ≥ `archive_after_days`(默认 90 天)→ 标 `archived`。
|
|
- 每条转换输出一行中文日志(含闲置天数与阈值)。
|
|
|
|
**阶段 2 — LLM 评审** `_run_llm_review()`:
|
|
- 用 LangChain `create_agent` 起一个 React Agent,绑定 `skill_list` / `skill_view` / `skill_manage` / `skill_archive` 工具。
|
|
- 挂载 `ToolErrorHandlingMiddleware` —— LLM 误调工具时异常转为 `ToolMessage`,LLM 可读错重试,不中断本轮。
|
|
- 提示词要求 LLM 找"前缀簇"(如 `code-*`、`search-*`),把重复/过窄技能合并进"伞技能",或归档无价值的。
|
|
- 每次调 `skill_archive(name, reason)` **必须传中文 `reason`**,记入日志供运维审计。
|
|
- LLM 产出结构化 YAML(`consolidations` / `prunings`),解析后逐条输出中文原因日志。
|
|
|
|
**阶段 3 — 落盘** 写 `.curator_state`:记录 `last_run_at`、`run_count`、`last_run_summary`。
|
|
|
|
**软归档原则**:无论年龄流转还是 LLM 评审,归档都**只翻转 `state` 字段,文件保留在 `custom/{name}/` 原处**。用户可在 Web UI 的归档抽屉一键恢复。不存在"物理移动到 .archive/ 目录"的破坏性操作。
|
|
|
|
涉及文件:`packages/harness/deerflow/skills/curator.py`
|
|
|
|
---
|
|
|
|
## 五、用户分权模型
|
|
|
|
技能系统是**多租户**的,隔离发生在"可见性层"与"改写权限层"。
|
|
|
|
### 5.1 可见性:用户能看到哪些技能
|
|
|
|
`GET /api/skills` 返回的技能,经过数据库过滤(`persistence/skills/sql.py` 的 `_visible_filter`):
|
|
|
|
```python
|
|
or_(
|
|
SkillRow.owner_user_id.is_(None), # 内置 / 历史遗留 —— 所有人可见
|
|
SkillRow.owner_user_id == user_id, # 自己创建的 —— 可见
|
|
SkillRow.published.is_(True), # 别人已发布的 —— 可见
|
|
)
|
|
```
|
|
|
|
即:用户 B 能看到用户 A 的技能,**当且仅当** A 把它 `published`。A 的私有技能对 B 不可见。
|
|
|
|
### 5.2 改写权限:谁能修改技能
|
|
|
|
所有写操作(改 `state`、`pinned`、`published`、编辑内容、删除)都经过权限助手(`app/gateway/routers/skills.py`):
|
|
|
|
- `_get_owned_skill_or_raise()` — 必须是 owner,否则 403(可见但非己有)/ 404(不可见)。
|
|
- `_get_editable_skill_or_raise()` — owner **或** admin;admin 可强制处理任何人的技能(含强制下架)。
|
|
- 内置技能(`owner_user_id IS NULL`)只有 admin 能改。
|
|
|
|
| 操作 | 权限要求 |
|
|
|---|---|
|
|
| 查看列表 / 详情 | 任何登录用户(经可见性过滤) |
|
|
| 改 state(归档/恢复) | owner 或 admin |
|
|
| 改 pinned(钉选) | owner 或 admin |
|
|
| 发布 / 取消发布 | owner 或 admin(admin 可强制下架他人技能) |
|
|
| 编辑内容 / 删除 | owner 或 admin |
|
|
| 安装到 public 目录 | 仅 admin |
|
|
|
|
### 5.3 Curator 的分权
|
|
|
|
- Curator 按 `user_id` 逐用户独立运行,**用户 A 跑策展绝不触碰用户 B 的技能**(`_list_user_owned_skill_names` 按归属过滤)。
|
|
- `.usage.json` / `.curator_state` 等元数据按技能名/用户名索引;curator 日志(`curator_log`)按用户分桶,Web UI 只展示本人的策展记录。
|
|
- Curator 永不处理:已发布的技能、被任何 Agent 引用的技能、`pinned` 的技能、内置技能。
|
|
|
|
### 5.4 Agent 的 `skills` 白名单(另一层隔离)
|
|
|
|
自定义 Agent 的 `.deer-flow/agents/{agent_id}/config.yaml` 有一个 `skills` 字段:
|
|
|
|
- 未声明 / `null` → 不限制(或按默认 Agent 行为)。
|
|
- `[]` → 显式不加载任何技能。
|
|
- `["a", "b"]` → 只加载这两个技能。
|
|
|
|
`skill_list` 工具会读取**当前 Agent** 的 `config.yaml`,只返回白名单内的技能;默认 Lead Agent 无此限制,可见全部。
|
|
|
|
---
|
|
---
|
|
|
|
## 十、已知限制
|
|
|
|
1. **`use_count` 永不增长**:`bump_use()` 已定义但全后端无调用点。"使用一个技能"的语义模糊(技能是 prompt 知识,不是工具),尚未接线。`view_count`(`skill_view` 触发)与 `patch_count`(回滚触发)是正常工作的。
|
|
2. **`.usage.json` 全局单文件**:不按用户分文件;因技能名全局唯一,一个技能只有一份 state/计数,这是设计取舍。
|
|
3. **enabled 开关**:`extensions_config.json` 的技能 `enabled` 是全局的(非 per-user),前端开关已移除,归档功能覆盖了"对 Agent 隐藏技能"的需求。
|
|
4. **历史物理归档**:早期 curator 曾把技能目录物理移到 `custom/.archive/`,这些遗留目录不会出现在新的归档抽屉里(列表 API 扫不到);新流程已统一为软归档。
|
|
5. **`skill_evolution.enabled` 门控不一致**:`skill_manage` 工具门控(`tools/tools.py`)读 **per-user 生效配置**,而 system prompt 指引段门控(`agents/lead_agent/prompt.py`)读 **全局 `config.yaml`**。两者应统一到 per-user(`load_skill_evolution_config_for_user`),且 prompt section 缓存需按 `user_id` 分键。详见第四章 4.1 的"已知不一致"说明。
|