主题
技能系统
本章目标:
- 讲清楚 skills 为什么是 DeerFlow「几乎能做任何事」的关键,以及渐进式加载(progressive loading)如何节省 context。
- 掌握 SKILL.md frontmatter 字段、
load_skills递归扫描、parser/validation/tool_policy 的边界与契约。- 能按 validation 代码约束写出一个合法的自定义 SKILL.md,并理解 .skill ZIP 安装与 LLM 安全扫描流程。
TL;DR
技能(Skill)是放在 skills/{public,custom}/<name>/SKILL.md 下的「带 YAML frontmatter 的 Markdown 工作手册」。系统启动时只把已启用技能的 name/description/容器路径注入系统提示词(几十 token 级别),Agent 在遇到匹配任务时才用 read_file 读完整 SKILL.md 及其引用资源——这就是渐进式加载,用极小 context 换「插件式」能力扩展。安装走 .skill ZIP:经过路径穿越/zip 炸弹防护解压后,先做 frontmatter 校验,再用 LLM 逐文件做安全扫描(allow/warn/block),通过才落盘到 custom/。allowed-tools 字段可把 Agent 工具集收窄到技能声明的白名单。
Overview
DeerFlow 的 Lead Agent 本身只有一组通用工具(bash、文件读写、子代理委派等)。要让它「会做 PPT」「会做学术综述」「会部署 Vercel」,如果把每种工作流的完整说明都塞进系统提示词,context 会瞬间爆炸,且大部分内容在单次对话里用不到。
技能系统解决的就是这个矛盾。它的核心设计是渐进式加载(progressive loading):系统提示词里只放一张「技能目录」,每项技能只有 name、description(用于触发匹配)和容器内的文件路径 prompt.py:605。提示词里明确告诉模型加载步骤:用户查询匹配某技能时,先 read_file 读它的主文件,再按需读同目录下被引用的外部资源 prompt.py:612-617。技能的全文、脚本、模板都不进 context,只在真正执行那个工作流时才被读入。
这套机制让 DeerFlow「几乎能做任何事」:任何可以写成 Markdown 流程 + 辅助脚本的能力,都能作为一个目录挂进来,无需改代码。skills/public/ 是平台内置只读技能,skills/custom/ 是用户通过 .skill 包安装或自建的可编辑技能 types.py:8-16。技能目录在沙箱容器内统一挂载到 /mnt/skills,所以提示词里给的路径就是 Agent 实际能 read_file 的路径 types.py:39-53。
Architecture
技能系统由「类型契约 + 解析器 + 校验器 + 存储后端 + 安装器 + 安全扫描器 + 工具策略 + 提示词注入」组成。存储层用抽象基类 SkillStorage 提供 load_skills 等模板方法流程,LocalSkillStorage 实现本地文件系统的原子操作,二者通过反射工厂 get_or_new_skill_storage 装配 storage/init.py:15-68。
Source
| 模块 | 路径 | 职责 |
|---|---|---|
| 类型 | skills/types.py | Skill dataclass、SkillCategory、容器路径推导 |
| 解析器 | skills/parser.py | 提取 frontmatter、allowed-tools 解析 |
| 校验器 | skills/validation.py | frontmatter 字段白名单与命名规则 |
| 存储层 | skills/storage/skill_storage.py | 抽象基类 + load_skills 模板流程 |
| 本地存储 | skills/storage/local_skill_storage.py | 文件遍历、安装、历史 JSONL |
| 安装器 | skills/installer.py | ZIP 安全解压、安全扫描调度 |
| 安全扫描 | skills/security_scanner.py | LLM 分类 + 鲁棒 JSON 解析 |
| 工具策略 | skills/tool_policy.py | 按 allowed-tools 收窄工具集 |
| 提示词注入 | agents/lead_agent/prompt.py | 缓存 + 生成 <skill_system> 段 |
Components / Subsystems
parser —— frontmatter 解析
职责:从 SKILL.md 顶部的 --- 围栏中提取 YAML,产出 Skill 对象。关键函数 parse_skill_file 用正则 ^---\s*\n(.*?)\n---\s*\n 抓 frontmatter 块,yaml.safe_load 解析,要求 name/description 均为非空字符串,任一不满足或 YAML 非 mapping 则返回 None 而不抛异常 parser.py:54-84。parse_allowed_tools 单独处理 allowed-tools:省略返回 None(legacy 放行),必须是字符串列表,空字符串元素会抛 ValueError parser.py:12-32。注意:parser 产出的 enabled 恒为 True,真实启用状态由 load_skills 从 extensions 配置二次合并 parser.py:105。
validation —— frontmatter 字段约束
职责:在写入/安装前校验 SKILL.md。ALLOWED_FRONTMATTER_PROPERTIES 是字段白名单:name、description、license、allowed-tools、metadata、compatibility、version、author,出现其它键直接判不合法 validation.py:15。_validate_skill_frontmatter 强制 name 为 hyphen-case(^[a-z0-9-]+$,不能首尾连字符或 --,≤64 字符),description 不能含尖括号且 ≤1024 字符 validation.py:70-86。
storage —— 发现与启用状态合并
职责:抽象 SkillStorage 提供 final 模板方法 load_skills:遍历 _iter_skill_files() 给出的每个 SKILL.md,逐个 parse_skill_file,以 name 去重,再从 ExtensionsConfig.from_file() 每次重新读取合并 enabled 状态(让别的进程改动立即生效),按 name 排序 skill_storage.py:212-246。LocalSkillStorage._iter_skill_files 用 os.walk(followlinks=True) 递归扫描 public/custom 两个 category 目录,跳过以 . 开头的子目录,只 yield 含 SKILL.md 的目录 local_skill_storage.py:63-74。自定义技能写入用临时文件 + replace 原子落盘 local_skill_storage.py:81-92。
installer —— .skill ZIP 安全安装
职责:解压并安全校验 .skill 包。safe_extract_skill_archive 三重防护:拒绝绝对路径/.. 穿越成员、跳过 symlink 成员、流式写入并对未压缩总量设 512MB 上限防 zip 炸弹 installer.py:80-121。_scan_skill_archive_contents_or_raise 强制扫描 SKILL.md,并扫描 scripts/(按可执行处理)及 references//templates/ 下的文本文件,禁止嵌套 SKILL.md installer.py:175-192。LocalSkillStorage.ainstall_skill_from_archive 串起全流程:校验 .skill 后缀 → 安全解压到临时目录 → frontmatter 校验 → 目标不存在检查 → 安全扫描 → 复制到 staging 再原子搬入预留目录 local_skill_storage.py:94-155。
security_scanner —— LLM 分类 + 鲁棒 JSON 解析
职责:用 LLM 把技能内容分类为 allow/warn/block。scan_skill_content 给模型一份 rubric(拦截 prompt 注入、角色覆盖、提权、数据外泄、不安全可执行代码),要求只回单行 JSON security_scanner.py:70-93。_extract_json_object 对 LLM 输出做鲁棒解析:先剥 ```json 围栏直接 json.loads,失败再做「字符串感知的大括号配平」逐字符提取首个完整 JSON 对象 security_scanner.py:24-67。保守失败策略:模型应答但解析不出 → block;模型调用失败 → 一律 block(可执行内容更严格)security_scanner.py:105-109。
tool_policy —— allowed-tools 工具收窄
职责:按已加载技能的 allowed-tools 声明收窄 Agent 工具集。allowed_tool_names_for_skills 取所有声明的并集;关键语义:只要有任一技能声明了 allowed-tools,没声明的 legacy 技能就不再贡献工具(而不是退回放行全部);全部技能都没声明时返回 None 表示 legacy allow-all tool_policy.py:13-36。filter_tools_by_skill_allowed_tools 据此过滤工具列表,在 Lead Agent 工厂中对内置/扩展工具生效 agent.py:411-436。
Data Flow
下面是「.skill 安装 + 安全扫描」端到端流程:
提示词注入侧:get_skills_prompt_section 取已启用技能,生成每项的 <skill> 标签(含 get_container_file_path 给出的 /mnt/skills/... 路径)拼成 <available_skills>,无技能且未开启 skill_evolution 时返回空串 prompt.py:626-656。已启用技能列表由后台线程异步刷新并缓存,配置或技能变更时 clear_skills_system_prompt_cache 使其失效 prompt.py:114-160。
Implementation Details
security_scanner 的鲁棒 JSON 解析是本系统最关键的健壮性代码,处理 LLM 输出格式不稳定:
python
def _extract_json_object(raw: str) -> dict | None:
raw = raw.strip()
fence_match = re.match(r"^```(?:json)?\s*\n?(.*?)\n?\s*```$", raw, re.DOTALL)
if fence_match:
raw = fence_match.group(1).strip()
try:
return json.loads(raw)
except json.JSONDecodeError:
pass
start = raw.find("{")
if start == -1:
return None
depth = 0; in_string = False; escape = False
for i in range(start, len(raw)):
c = raw[i]
if escape: escape = False; continue
if c == "\\": escape = True; continue
if c == '"': in_string = not in_string; continue
...解读:第一层剥 Markdown 代码围栏后直接 json.loads,覆盖「干净 JSON」与「围栏包裹 JSON」两种最常见输出。第二层是兜底——LLM 常在 JSON 前后加自然语言解说,所以从首个 { 开始做字符串感知的大括号配平:in_string/escape 标志确保字符串内的 {、}、引号不被误计深度,直到 depth 归零截取出首个完整对象 security_scanner.py:24-67。这与提交记录中「make security scanner JSON parsing robust for LLM output variations」对应。
速查表
SKILL.md frontmatter 字段矩阵:
| 字段 | 必填 | 类型/约束 | Source |
|---|---|---|---|
name | 是 | hyphen-case ^[a-z0-9-]+$,≤64,无首尾/连续连字符 | validation.py:70-75 |
description | 是 | 非空字符串,无 < >,≤1024 | validation.py:78-86 |
license | 否 | 任意,str().strip() 归一,空→None | parser.py:86-88 |
allowed-tools | 否 | 字符串列表,空列表=无工具,空元素报错 | parser.py:12-32 |
metadata | 否 | 白名单内,parser 不消费 | validation.py:15 |
compatibility | 否 | 白名单内 | validation.py:15 |
version | 否 | 白名单内 | validation.py:15 |
author | 否 | 白名单内 | validation.py:15 |
| 其它键 | — | 出现即判不合法 | validation.py:51-53 |
注:
public/内置技能(如 data-analysis、bootstrap)实际只用了name+description,见 data-analysis/SKILL.md:1-4。
扩展指南
最小自定义 SKILL.md 模板(放在 skills/custom/my-skill/SKILL.md):
markdown
---
name: my-skill
description: Use this skill when the user wants to ... (写清触发场景与能力边界,模型靠这段做匹配)
license: MIT
allowed-tools:
- bash
- read_file
- write_file
---
# My Skill
## Overview
一句话说明这个技能做什么。
## Workflow
1. 第一步……
2. 引用资源放在同目录:见 `references/guide.md`
3. 脚本放在 `scripts/` 下,由 bash 调用约束清单(均从代码读出):
name必须 hyphen-case^[a-z0-9-]+$、≤64 字符、不能首尾或连续连字符,且必须与目录名一致(安装时校验)validation.py:70-75、local_skill_storage.py:136-137。description不能含<或>,长度 ≤1024 validation.py:83-86。- frontmatter 键只能在白名单 8 项内,其它键导致校验失败 validation.py:15。
allowed-tools必须是字符串列表,不能有空字符串元素;省略=不限制 parser.py:21-31。- 文件必须以
---开头,frontmatter 必须是 YAML mapping validation.py:32-46。 - 辅助文件仅允许放
references/templates/scripts/assets子目录,且不允许嵌套 SKILL.md skill_storage.py:81-92、installer.py:187-188。
Configuration
| 配置项 | 来源/默认 | Source |
|---|---|---|
技能根目录 skills.path | 显式 path → DEER_FLOW_SKILLS_PATH → 项目根 skills/ → legacy repo 根 | skills_config.py:32-59 |
容器挂载路径 skills.container_path | 默认 /mnt/skills | skills_config.py:27-30 |
存储实现 skills.use | 默认 LocalSkillStorage,反射装配 | skills_config.py:19-22 |
| 启用状态 | extensions_config.json 的 skills 映射;未配置时 public/custom 默认启用 | extensions_config.py:190-204 |
| 安全扫描模型 | skill_evolution.moderation_model_name,空则用默认模型 | security_scanner.py:84-86 |
is_skill_enabled 的默认行为很关键:extensions_config.json 里没有某技能条目时,public 与 custom 技能默认启用;只有显式写 {"enabled": false} 才禁用 extensions_config.py:200-204。
Common Pitfalls / Tips
- frontmatter 解析失败是静默的:
parse_skill_file在 name/description 缺失或 YAML 错误时返回None,该技能直接从列表消失,不报错。新建技能后发现没出现在列表,优先用 validation 规则逐项核对 parser.py:74-84。 allowed-tools是全局耦合的:只要任意一个已启用技能声明了allowed-tools,所有没声明的技能就不再向工具集贡献工具。混用时务必给所有技能补齐声明,否则会意外丢失工具 tool_policy.py:26-36。- 安全扫描默认从严:LLM 不可用或输出无法解析时一律
block。离线/无模型环境下安装.skill会失败,这是设计预期而非 bug security_scanner.py:105-109。 - 不要在 public/ 下手改内置技能:它是只读类别;要定制同名技能应在
custom/下新建,系统会在ensure_custom_skill_is_editable给出明确提示 skill_storage.py:248-254。 - 启用状态每次都重读:
load_skills每次调用都重新读ExtensionsConfig.from_file(),外部进程改了启用开关无需重启即生效,但也意味着该文件 IO 会随调用频繁发生,提示词侧用缓存 + 后台线程刷新缓解 skill_storage.py:233-238。
References
- backend/packages/harness/deerflow/skills/parser.py —— frontmatter 与 allowed-tools 解析
- backend/packages/harness/deerflow/skills/validation.py —— 字段白名单与命名规则
- backend/packages/harness/deerflow/skills/installer.py —— ZIP 安全解压与扫描调度
- backend/packages/harness/deerflow/skills/security_scanner.py —— LLM 安全分类与鲁棒 JSON
- backend/packages/harness/deerflow/skills/tool_policy.py —— allowed-tools 工具过滤
- backend/packages/harness/deerflow/skills/storage/skill_storage.py —— 抽象存储与 load_skills 模板
- backend/packages/harness/deerflow/skills/storage/local_skill_storage.py —— 本地文件遍历与安装
- backend/packages/harness/deerflow/skills/types.py —— Skill 类型与容器路径
- backend/packages/harness/deerflow/agents/lead_agent/prompt.py —— 渐进式注入与缓存
- backend/packages/harness/deerflow/config/skills_config.py —— SkillsConfig 路径解析
Related Pages
| 章节 | 关系 |
|---|---|
| 06-MCP与Skills扩展配置.md | 上游:extensions_config.json 中 skills 启用开关与 MCP 的统一配置体系 |
| 20-工具系统与内置工具.md | 协作:allowed-tools 收窄的就是工具系统装配出的工具集 |
| 10-LeadAgent与Agent工厂.md | 下游:Agent 工厂调用 filter_tools_by_skill_allowed_tools 与技能提示词注入 |