deerflow-code/offline-backend-20260512/backend/docs/SKILL_EVOLUTION_ZH.md
2026-09-07 18:24:55 +08:00

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 开关门控。该开关不是单纯的全局开关 —— 它的生效值由两层配置深度合并得到:

  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):

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 的"已知不一致"说明。