12 KiB
技能自我进化(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 单个条目结构:
{
"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 开关门控。该开关不是单纯的全局开关 —— 它的生效值由两层配置深度合并得到:
- 全局基线:
config.yaml的skill_evolution段; - 每用户覆盖:
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 的完整副作用:
- 写
custom/{name}/SKILL.md - 追加变更历史
mark_agent_created(name)→.usage.json记source=agent_ensure_skill_db_registered()→ DBskills表插入归属行(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):
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 无此限制,可见全部。
十、已知限制
use_count永不增长:bump_use()已定义但全后端无调用点。"使用一个技能"的语义模糊(技能是 prompt 知识,不是工具),尚未接线。view_count(skill_view触发)与patch_count(回滚触发)是正常工作的。.usage.json全局单文件:不按用户分文件;因技能名全局唯一,一个技能只有一份 state/计数,这是设计取舍。- enabled 开关:
extensions_config.json的技能enabled是全局的(非 per-user),前端开关已移除,归档功能覆盖了"对 Agent 隐藏技能"的需求。 - 历史物理归档:早期 curator 曾把技能目录物理移到
custom/.archive/,这些遗留目录不会出现在新的归档抽屉里(列表 API 扫不到);新流程已统一为软归档。 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 的"已知不一致"说明。