本文深入剖析开源 AI Agent 框架 DeerFlow 2.0 中「Skills 自动调用」机制的实现原理。从文件系统发现、元数据解析、System Prompt 注入,到 LLM 自主决策读取、渐进式加载执行,完整还原一条从用户请求到 Skill 执行的闭环链路。
源码版本:bytedance/deer-flow main 分支,commit
e93f6584
1. 核心设计理念:Prompt-Driven,而非 Tool-Driven
DeerFlow 的 Skill 系统采用了一种精妙的架构选择:Skills 不是注册为 LLM 可调用的 Tool,而是作为结构化知识注入到 System Prompt 中,由 LLM 自主判断何时读取和执行。
这一设计带来了三个关键优势:
- 零 Token 开销:未使用的 Skill 只在 Prompt 中占用几行元数据(名称 + 描述 + 路径),不会消耗上下文窗口
- 渐进式加载(Progressive Loading):Agent 按需读取完整内容,仅在匹配时才加载
- LLM 自主调度:不依赖硬编码的触发规则,由模型理解语义后自主决策
2. Skill 的定义与文件结构
每个 Skill 是一个文件目录,核心入口是 SKILL.md 文件:
skills/
├── public/ # 内置 Skills(只读,随项目发布)
│ ├── data-analysis/
│ │ ├── SKILL.md # 元数据 + 工作流指令
│ │ └── scripts/
│ │ └── analyze.py # 辅助脚本
│ ├── bootstrap/
│ │ ├── SKILL.md
│ │ ├── templates/
│ │ │ └── SOUL.template.md
│ │ └── references/
│ │ └── conversation-guide.md
│ └── deep-research/
│ └── SKILL.md
└── custom/ # 用户自定义 Skills(可编辑)
└── my-skill/
└── SKILL.md
SKILL.md 使用 YAML Frontmatter 定义元数据:
---
name: data-analysis
description: Use this skill when the user uploads Excel (.xlsx/.xls) or CSV files...
license: MIT
allowed-tools: [] # 可选:限制该 Skill 可用的工具列表
---
# 详细工作流指令(Markdown 正文)
## Step 1: Understand Requirements
...
关键字段说明:
| 字段 | 必填 | 作用 |
|---|---|---|
name |
是 | Skill 唯一标识,必须为 kebab-case(^[a-z0-9]+(?:-[a-z0-9]+)*$) |
description |
是 | 描述触发场景,LLM 据此判断是否匹配(最长 1024 字符) |
allowed-tools |
否 | 声明白名单工具列表,启用后 Agent 只能使用列表中的工具 |
license |
否 | 许可证信息 |
其中 description 的写法至关重要 — 它是 LLM 判断「用户请求是否匹配此 Skill」的唯一依据。以 data-analysis 为例:
"Use this skill when the user uploads Excel (.xlsx/.xls) or CSV files and wants to perform data analysis..."
项目内置了 21 个公共 Skill,涵盖数据分析、深度研究、前端设计、PPT 生成、播客生成等场景。
3. Skill 发现与加载流程
3.1 存储抽象层
DeerFlow 定义了 SkillStorage 抽象基类,采用模板方法模式:
SkillStorage (ABC) # backend/.../skills/storage/skill_storage.py
├── _iter_skill_files() # 抽象:遍历文件系统找 SKILL.md
├── load_skills() # 具体:编排发现→解析→过滤的完整流程
├── validate_skill_name() # 具体:名称格式校验
├── validate_relative_path() # 具体:路径安全校验
└── ...
LocalSkillStorage (具体实现) # backend/.../skills/storage/local_skill_storage.py
└── _iter_skill_files() # 递归扫描 skills/{public,custom}/ 目录
存储实现通过反射机制从 config.yaml 加载:
# config.yaml
skills:
use: "deerflow.skills.storage.local_skill_storage:LocalSkillStorage"
path: null # 默认 skills/ 目录
container_path: "/mnt/skills" # 沙箱容器内的挂载路径
3.2 文件系统扫描
LocalSkillStorage._iter_skill_files() 的核心逻辑:
def _iter_skill_files(self) -> Iterable[tuple[SkillCategory, Path, Path]]:
for category in SkillCategory: # PUBLIC, CUSTOM
category_path = self._host_root / category.value
for current_root, dir_names, file_names in os.walk(category_path):
dir_names[:] = sorted(name for name in dir_names
if not name.startswith("."))
if SKILL_MD_FILE not in file_names:
continue
yield category, category_path, Path(current_root) / SKILL_MD_FILE
这确保了嵌套目录中的 Skill 也能被发现,同时跳过隐藏目录(以 . 开头)。
3.3 元数据解析
parse_skill_file()(backend/.../skills/parser.py)使用正则提取 YAML Frontmatter:
front_matter_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
metadata = yaml.safe_load(front_matter_text)
return Skill(
name=metadata["name"], # 必填
description=metadata["description"], # 必填
license=metadata.get("license"),
skill_dir=skill_file.parent,
skill_file=skill_file,
category=category,
allowed_tools=parse_allowed_tools(metadata.get("allowed-tools")),
enabled=True, # 实际状态从 extensions_config.json 合并
)
Skill 是一个 @dataclass,核心属性:
@dataclass
class Skill:
name: str
description: str
license: str | None
skill_dir: Path
skill_file: Path
relative_path: Path
category: SkillCategory # PUBLIC 或 CUSTOM
allowed_tools: list[str] | None
enabled: bool
3.4 启用状态合并
Skill 的 enabled 状态存储在 extensions_config.json 中,与文件系统分离:
{
"skills": {
"data-analysis": { "enabled": true },
"bootstrap": { "enabled": false }
}
}
load_skills() 模板方法在扫描完成后合并状态:
extensions_config = ExtensionsConfig.from_file()
for skill in skills:
skill.enabled = extensions_config.is_skill_enabled(skill.name, skill.category)
4. System Prompt 注入:让 LLM 知道 Skills 的存在
这是整个机制最核心的环节。prompt.py 中的 get_skills_prompt_section() 将已启用的 Skills 注入到 Agent 的 System Prompt 中。
4.1 生成的 Prompt 结构
注入到 System Prompt 中的 XML 块如下:
<skill_system>
You have access to skills that provide optimized workflows for specific tasks.
Each skill contains best practices, frameworks, and references to additional resources.
**Progressive Loading Pattern:**
1\. When a user query matches a skill's use case, immediately call `read_file`
on the skill's main file using the path attribute provided in the skill tag below
2\. Read and understand the skill's workflow and instructions
3\. The skill file contains references to external resources under the same folder
4\. Load referenced resources only when needed during execution
5\. Follow the skill's instructions precisely
**Skills are located at:** /mnt/skills
<available_skills>
<skill>
<name>data-analysis</name>
<description>Use this skill when the user uploads Excel... [built-in]</description>
<location>/mnt/skills/public/data-analysis/SKILL.md</location>
</skill>
<skill>
<name>bootstrap</name>
<description>Generate a personalized SOUL.md... [built-in]</description>
<location>/mnt/skills/public/bootstrap/SKILL.md</location>
</skill>
</available_skills>
</skill_system>
关键设计点:
- Progressive Loading Pattern 明确告诉 LLM:不要一次性加载所有 Skill,而是按需读取
- 每个 Skill 只暴露 3 个字段:
name(名称)、description(描述)、location(路径) -
[built-in]/[custom, editable]标签标识 Skill 的可编辑性 - 路径使用容器虚拟路径
/mnt/skills/...,由 Sandbox 层自动映射到宿主机路径
4.2 System Prompt 中的强化提醒
在 <critical_reminders> 中还有关键指令:
<critical_reminders>
- Skill First: Always load the relevant skill before starting **complex** tasks.
- Progressive Loading: Load resources incrementally as referenced in skills
</critical_reminders>
这确保了 LLM 在处理复杂任务时会优先考虑加载 Skill,而非直接回答。
4.3 Prompt 缓存与性能优化
为了避免每次请求都重新生成 Skill 列表,DeerFlow 使用了 lru_cache + 后台线程的双重缓存:
LRU 缓存层(_get_cached_skills_prompt_section):以 Skill 签名元组为 key 缓存生成的 Prompt 片段,避免重复格式化。
后台线程缓存层(_enabled_skills_cache):
请求到来 → 检查缓存(_enabled_skills_cache)
├── 命中 → 直接返回
└── 未命中 → 触发后台线程加载 → 返回空列表(下次请求可命中)
缓存通过版本号(_enabled_skills_refresh_version)和线程锁实现并发安全。当 Skill 被创建/编辑/删除时,_invalidate_enabled_skills_cache() 会递增版本号并触发新一轮后台加载。
整个缓存策略确保:
- 请求路径永不阻塞在磁盘 I/O 上
- 配置变更在下一轮对话中生效
- 并发安全(
threading.Lock+WeakValueDictionary)
5. Agent 决策与执行:从 Prompt 到 Action
5.1 完整调用链路
用户输入: "帮我分析这个 Excel 文件"
│
▼
┌─────────────────────┐
│ 1\. Agent 分析请求 │ ← LLM 在 thinking 阶段识别意图
│ 识别: 数据分析 │
└──────────┬──────────┘
│
匹配 Skill description?
│ 是
▼
┌─────────────────────────────────┐
│ 2\. 调用 read_file 工具 │
│ read_file( │
│ "/mnt/skills/public/ │
│ data-analysis/SKILL.md" │
│ ) │
└──────────┬──────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 3\. 获取完整 Skill 工作流 │
│ 包含: 步骤、参数、示例 │
└──────────┬──────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 4\. 按需加载引用资源 │
│ 如 scripts/analyze.py │
└──────────┬──────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 5\. 执行 Skill 工作流 │
│ bash("python /mnt/skills/ │
│ public/data-analysis/ │
│ scripts/analyze.py │
│ --files ... --action ...") │
└──────────┬──────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 6\. 返回结果给用户 │
└─────────────────────────────────┘
5.2 LLM 的自主决策过程
LLM 在收到包含 Skill 列表的 System Prompt 后,面对用户请求时的推理过程:
Step 1 — 意图识别:
Thinking: 用户上传了一个 Excel 文件并要求分析数据。
这匹配了 data-analysis skill 的描述:
"Use this skill when the user uploads Excel files..."
Step 2 — 加载 Skill:
# Agent 自主调用 read_file 工具
read_file("/mnt/skills/public/data-analysis/SKILL.md")
Step 3 — 按工作流执行:
Thinking: Skill 指示我先 inspect 文件结构,然后用 SQL 查询。
Step 1: bash("python /mnt/skills/public/data-analysis/scripts/analyze.py --files ... --action inspect")
Step 2: bash("python /mnt/skills/public/data-analysis/scripts/analyze.py --files ... --action query --sql '...'")
整个过程中,没有任何硬编码的触发器或路由逻辑,完全依赖 LLM 的语义理解能力来匹配 Skill description 与用户请求。
6. Skill 进化系统:Agent 自主创建和修改 Skill
DeerFlow 还支持 Agent 在运行时自主创建和修改 Skill(通过 skill_manage 工具),实现 Skill 的自我进化。
6.1 配置开关
# config.yaml
skill_evolution:
enabled: true
启用后,System Prompt 中会注入额外的指令:
## Skill Self-Evolution
After completing a task, consider creating or updating a skill when:
- The task required 5+ tool calls to resolve
- You overcame non-obvious errors or pitfalls
- The user corrected your approach and the corrected version worked
- You discovered a non-trivial, recurring workflow
If you used a skill and encountered issues not covered by it, patch it immediately.
Prefer patch over edit. Before creating a new skill, confirm with the user first.
6.2 skill_manage 工具
Agent 通过 skill_manage 工具(backend/.../tools/skill_manage_tool.py)管理自定义 Skill,支持 6 种操作:
| Action | 功能 | 参数 |
|---|---|---|
create |
创建新 Skill |
name, content
|
edit |
全量替换 SKILL.md |
name, content
|
patch |
增量替换(推荐) |
name, find, replace
|
delete |
删除 Skill | name |
write_file |
写入辅助文件 |
name, path, content
|
remove_file |
删除辅助文件 |
name, path
|
每次写操作都通过 refresh_skills_system_prompt_cache_async() 触发缓存刷新。
6.3 安全防护
所有 Skill 写入操作都经过安全扫描(security_scanner.py):
result = await scan_skill_content(content, executable=executable, location=location)
if result.decision == "block":
raise ValueError(f"Security scan blocked the write: {result.reason}")
- 脚本文件(
scripts/目录下)扫描更严格 - 支持文件路径校验(不允许路径穿越
..) - 辅助文件只允许在
references/、templates/、scripts/、assets/子目录下
6.4 变更历史
每个自定义 Skill 都维护 JSONL 格式的变更历史:
custom/.history/my-skill.jsonl
每条记录包含:时间戳、操作类型、操作者(agent/human)、thread_id、前后内容、安全扫描结果。支持通过 API 回滚到任意历史版本。
7. 工具策略:Skill 级别的工具白名单
Skill 可以通过 allowed-tools 字段限制可用的工具集合。这在 tool_policy.py 中实现:
def filter_tools_by_skill_allowed_tools(tools, skills):
allowed = allowed_tool_names_for_skills(skills)
if allowed is None:
return tools # 无声明则允许所有工具
return [tool for tool in tools if tool.name in allowed]
这允许创建「受限 Skill」— 例如一个只能使用 read_file 和 bash 的 Skill,确保 Agent 在执行该 Skill 时不会调用不该调用的工具。
白名单逻辑是联合取并集:当多个已启用 Skill 声明了 allowed-tools 时,最终的工具集是所有声明列表的并集。
8. Agent 配置级别的 Skill 过滤
每个自定义 Agent 可以通过 config.yaml 限制可用的 Skill 列表:
# agent.py 中的 _available_skill_names()
def _available_skill_names(agent_config, is_bootstrap):
if is_bootstrap:
return {"bootstrap"} # Bootstrap Agent 只能用 bootstrap skill
if agent_config and agent_config.skills is not None:
return set(agent_config.skills) # 自定义 Agent 的白名单
return None # 默认 Agent 可用所有 Skill
在构建 Agent 时,这个列表会同时过滤 System Prompt 中的 Skill 列表和工具集:
system_prompt=apply_prompt_template(
available_skills=set(agent_config.skills) if agent_config.skills else None,
...
)
tools=filter_tools_by_skill_allowed_tools(tools, skills_for_tool_policy)
9. API 层:运行时管理
Gateway API 提供了完整的 Skill 管理接口(backend/app/gateway/routers/skills.py):
| 端点 | 方法 | 功能 |
|---|---|---|
GET /api/skills |
GET | 列出所有 Skills |
GET /api/skills/{name} |
GET | 获取 Skill 详情 |
PUT /api/skills/{name} |
PUT | 启用/禁用 Skill |
POST /api/skills/install |
POST | 从 .skill 归档安装 |
GET /api/skills/custom/{name} |
GET | 读取自定义 Skill 内容 |
PUT /api/skills/custom/{name} |
PUT | 编辑自定义 Skill |
DELETE /api/skills/custom/{name} |
DELETE | 删除自定义 Skill |
GET /api/skills/custom/{name}/history |
GET | 查看编辑历史 |
POST /api/skills/custom/{name}/rollback |
POST | 回滚到历史版本 |
每次修改后都会调用 refresh_skills_system_prompt_cache_async() 触发缓存刷新,确保下一次对话使用最新的 Skill 配置。
10. 关键源码文件索引
| 文件 | 职责 |
|---|---|
skills/public/*/SKILL.md |
Skill 定义(元数据 + 工作流) |
backend/.../skills/types.py |
Skill dataclass、SkillCategory 枚举 |
backend/.../skills/parser.py |
SKILL.md 解析器(Frontmatter 提取) |
backend/.../skills/storage/skill_storage.py |
SkillStorage 抽象基类(模板方法) |
backend/.../skills/storage/local_skill_storage.py |
本地文件系统实现 |
backend/.../skills/validation.py |
Frontmatter 校验 |
backend/.../skills/tool_policy.py |
Skill 级工具白名单过滤 |
backend/.../skills/security_scanner.py |
安全扫描 |
backend/.../tools/skill_manage_tool.py |
skill_manage 工具定义 |
backend/.../config/skills_config.py |
Skills 配置模型 |
backend/.../config/skill_evolution_config.py |
Skill 进化配置 |
backend/.../agents/lead_agent/prompt.py |
System Prompt 生成(Skills 注入) |
backend/.../agents/lead_agent/agent.py |
Lead Agent 工厂(Skill 过滤 + 绑定) |
backend/app/gateway/routers/skills.py |
REST API 路由 |
11. 架构总结
┌──────────────────────────────────────────────────────────┐
│ DeerFlow Skill System │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ SKILL.md │ │ SkillStorage │ │ Extensions │ │
│ │ (定义层) │───▶│ (发现层) │ │ Config │ │
│ │ 元数据+流程 │ │ 扫描+解析 │◀──│ (启用/禁用) │ │
│ └─────────────┘ └──────┬───────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ System Prompt│ │
│ │ (注入层) │ │
│ │ XML <skill> │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ LLM Agent (决策层) │ │
│ │ │ │
│ │ 用户请求 → 意图识别 → 匹配 Skill description │ │
│ │ → read_file 加载 SKILL.md → 按工作流执行 │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Tool Layer │ │
│ │ (执行层) │ │
│ │ bash/read/ │ │
│ │ write/skill │ │
│ │ _manage │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
核心洞察:DeerFlow 的 Skill 系统本质上是一个基于 Prompt 的语义路由器。它没有使用传统的规则引擎或意图分类器,而是将 Skill 元数据作为结构化知识注入到 LLM 的上下文中,让 LLM 自身成为调度器。这是一种「Prompt as API」的设计哲学 — 把 LLM 的语义理解能力当作基础设施来用,而非试图在其上叠加传统软件工程的路由逻辑。