DeerFlow 动态加载 Skill 机制深度解析:从源码到实践

2 阅读10分钟

前言

在 AI Agent 系统中,如何高效地将领域知识和最佳实践注入到大模型中,是一个核心挑战。传统的做法是将所有知识硬编码到 System Prompt 中,但这会导致 Token 消耗过大、响应变慢,且难以扩展。

DeerFlow 采用了一套精妙的 渐进式动态加载机制:将 Skill 元信息轻量注入,详细内容按需加载。本文将深入源码,剖析这一机制的设计原理。


一、Skill 的本质:结构化知识单元

1.1 文件结构

每个 Skill 是一个包含 SKILL.md 的目录:

skills/
├── public/                    # 内置 Skill(只读)
│   ├── frontend-design/
│   │   ├── SKILL.md          # 主文件:元数据 + 指令
│   │   ├── references/       # 参考文档
│   │   └── templates/        # 模板文件
│   └── data-analysis/
│       └── SKILL.md
├── custom/                    # 用户自定义 Skill(可编辑)
│   └── my-workflow/
│       └── SKILL.md
└── .history/                  # 修改历史(版本追踪)

1.2 SKILL.md 格式

SKILL.md 使用 YAML Front Matter 定义元数据:

---
name: frontend-design
description: Frontend design and development workflows including React, Vue, and modern CSS
license: MIT
---

# Frontend Development Skill

## Best Practices
- Use TypeScript for type safety
- Implement component-based architecture
...

## References
- [React Patterns](./references/react-patterns.md)
- [CSS Guidelines](./references/css-guidelines.md)

关键设计

  • name + description 用于 LLM 语义匹配
  • 主体内容是详细的指令和最佳实践
  • 支持相对路径引用外部资源

1.3 Skill 数据模型

源码位置:backend/packages/harness/deerflow/skills/types.py

@dataclass
class Skill:
    """Represents a skill with its metadata and file path"""
    name: str
    description: str
    license: str | None
    skill_dir: Path
    skill_file: Path
    relative_path: Path  # 相对于分类根目录的路径
    category: str  # 'public' 或 'custom'
    enabled: bool = False  # 是否启用

    def get_container_file_path(self, container_base_path: str = "/mnt/skills") -> str:
        """获取 Skill 在容器中的完整路径"""
        return f"{self.get_container_path(container_base_path)}/SKILL.md"

二、Skill 加载流程:从文件系统到内存

2.1 入口函数:load_skills

源码位置:backend/packages/harness/deerflow/skills/loader.py:25-103

def load_skills(skills_path: Path | None = None, use_config: bool = True, enabled_only: bool = False) -> list[Skill]:
    """Load all skills from the skills directory."""
    
    # 1. 确定 skills 目录路径
    if skills_path is None:
        config = get_app_config()
        skills_path = config.skills.get_skills_path()
    
    skills_by_name: dict[str, Skill] = {}
    
    # 2. 扫描 public 和 custom 目录
    for category in ["public", "custom"]:
        category_path = skills_path / category
        for current_root, dir_names, file_names in os.walk(category_path):
            if "SKILL.md" not in file_names:
                continue
            
            # 3. 解析 SKILL.md 文件
            skill_file = Path(current_root) / "SKILL.md"
            skill = parse_skill_file(skill_file, category=category, relative_path=relative_path)
            if skill:
                skills_by_name[skill.name] = skill
    
    # 4. 从配置文件读取启用状态
    extensions_config = ExtensionsConfig.from_file()
    for skill in skills:
        skill.enabled = extensions_config.is_skill_enabled(skill.name, skill.category)
    
    # 5. 过滤已启用的 skills
    if enabled_only:
        skills = [skill for skill in skills if skill.enabled]
    
    return skills

流程要点

  1. 路径解析:支持配置文件指定或使用默认路径
  2. 目录遍历:递归扫描 public/custom/ 目录
  3. 文件解析:调用 parse_skill_file 提取元数据
  4. 状态注入:从 extensions_config.json 读取启用/禁用状态
  5. 过滤返回:可选只返回已启用的 Skill

2.2 解析器:parse_skill_file

源码位置:backend/packages/harness/deerflow/skills/parser.py:10-125

def parse_skill_file(skill_file: Path, category: str, relative_path: Path | None = None) -> Skill | None:
    """Parse a SKILL.md file and extract metadata."""
    
    content = skill_file.read_text(encoding="utf-8")
    
    # 提取 YAML Front Matter
    front_matter_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
    if not front_matter_match:
        return None
    
    # 解析 YAML(支持多行字符串)
    metadata = {}
    # ... 解析逻辑 ...
    
    # 提取必需字段
    name = metadata.get("name")
    description = metadata.get("description")
    
    if not name or not description:
        return None
    
    return Skill(
        name=name,
        description=description,
        license=metadata.get("license"),
        skill_dir=skill_file.parent,
        skill_file=skill_file,
        relative_path=relative_path or Path(skill_file.parent.name),
        category=category,
        enabled=True,  # 默认启用,实际状态由配置文件决定
    )

解析器特点

  • 使用正则提取 YAML Front Matter
  • 支持多行字符串(|> 语法)
  • 只提取元数据,不解析完整内容(减少开销)

2.3 配置管理:ExtensionsConfig

源码位置:backend/packages/harness/deerflow/config/extensions_config.py

class ExtensionsConfig(BaseModel):
    """Unified configuration for MCP servers and skills."""
    
    mcp_servers: dict[str, McpServerConfig] = Field(alias="mcpServers")
    skills: dict[str, SkillStateConfig] = Field(default_factory=dict)
    
    def is_skill_enabled(self, skill_name: str, skill_category: str) -> bool:
        """Check if a skill is enabled."""
        skill_config = self.skills.get(skill_name)
        if skill_config is None:
            # 默认启用 public 和 custom 类型的 skill
            return skill_category in ("public", "custom")
        return skill_config.enabled

配置文件示例 (extensions_config.json):

{
  "skills": {
    "frontend-design": { "enabled": true },
    "data-analysis": { "enabled": false },
    "deep-research": { "enabled": true }
  },
  "mcpServers": { ... }
}

三、缓存机制:性能优化的关键

3.1 全局缓存设计

源码位置:backend/packages/harness/deerflow/agents/lead_agent/prompt.py:14-19

# 全局缓存变量
_enabled_skills_lock = threading.Lock()
_enabled_skills_cache: list[Skill] | None = None
_enabled_skills_refresh_active = False
_enabled_skills_refresh_version = 0
_enabled_skills_refresh_event = threading.Event()

设计要点

  • 线程安全:使用 threading.Lock 保护并发访问
  • 延迟加载:首次请求时才加载
  • 后台刷新:避免阻塞主线程
  • 版本控制:支持缓存失效和重新加载

3.2 缓存读取流程

源码位置:backend/packages/harness/deerflow/agents/lead_agent/prompt.py:103-111

def _get_enabled_skills():
    """获取已启用的 skills(优先使用缓存)"""
    with _enabled_skills_lock:
        cached = _enabled_skills_cache

    if cached is not None:
        return list(cached)  # 缓存命中,直接返回

    _ensure_enabled_skills_cache()  # 缓存未命中,启动加载
    return []

3.3 后台加载机制

源码位置:backend/packages/harness/deerflow/agents/lead_agent/prompt.py:26-57

def _start_enabled_skills_refresh_thread() -> None:
    """启动后台线程加载 skills"""
    threading.Thread(
        target=_refresh_enabled_skills_cache_worker,
        name="deerflow-enabled-skills-loader",
        daemon=True,
    ).start()

def _refresh_enabled_skills_cache_worker() -> None:
    """后台加载 worker"""
    while True:
        with _enabled_skills_lock:
            target_version = _enabled_skills_refresh_version

        try:
            skills = _load_enabled_skills_sync()  # 同步加载
        except Exception:
            skills = []

        with _enabled_skills_lock:
            if _enabled_skills_refresh_version == target_version:
                _enabled_skills_cache = skills  # 更新缓存
                _enabled_skills_refresh_active = False
                _enabled_skills_refresh_event.set()
                return
            # 版本不匹配,说明有新的刷新请求,继续循环

3.4 缓存刷新机制

源码位置:backend/packages/harness/deerflow/agents/lead_agent/prompt.py:75-88

def _invalidate_enabled_skills_cache() -> threading.Event:
    """使缓存失效,触发重新加载"""
    global _enabled_skills_cache, _enabled_skills_refresh_version

    _get_cached_skills_prompt_section.cache_clear()  # 清除 LRU 缓存
    with _enabled_skills_lock:
        _enabled_skills_cache = None
        _enabled_skills_refresh_version += 1
        # ... 启动后台刷新

使用场景

  • 用户通过 API 修改了 Skill 配置
  • 管理员更新了 extensions_config.json
  • 系统检测到 Skill 文件变更

四、提示词注入:轻量元信息策略

4.1 核心设计理念

传统做法(全量注入):

System Prompt 包含所有 Skill 的完整内容
→ Token 消耗巨大
→ 响应变慢
→ 难以扩展

DeerFlow 做法(渐进式加载):

System Prompt 只注入 Skill 元信息(name + description + location)
→ Token 消耗极小
→ LLM 按需读取详细内容
→ 支持无限扩展

4.2 提示词生成

源码位置:backend/packages/harness/deerflow/agents/lead_agent/prompt.py:554-568

def _get_cached_skills_prompt_section(...) -> str:
    """生成 Skill 提示词部分"""
    
    skill_items = "\n".join(
        f"    <skill>\n"
        f"        <name>{name}</name>\n"
        f"        <description>{description}</description>\n"
        f"        <location>{location}</location>\n"
        f"    </skill>"
        for name, description, category, location in filtered
    )
    
    return f"""<skill_system>
You have access to skills that provide optimized workflows for specific tasks.

**Progressive Loading Pattern:**
1. When a user query matches a skill's use case, immediately call `read_file` on the skill's main file
2. Read and understand the skill's workflow and instructions
3. The skill file contains references to external resources
4. Load referenced resources only when needed during execution

**Skills are located at:** {container_base_path}

<available_skills>
{skill_items}
</available_skills>
</skill_system>"""

4.3 实际注入内容示例

<skill_system>
You have access to skills that provide optimized workflows for specific tasks.

**Progressive Loading Pattern:**
1. When a user query matches a skill's use case, immediately call `read_file`...
2. Read and understand the skill's workflow...

**Skills are located at:** /mnt/skills

<available_skills>
    <skill>
        <name>frontend-design</name>
        <description>Frontend design and development workflows [built-in]</description>
        <location>/mnt/skills/public/frontend-design/SKILL.md</location>
    </skill>
    <skill>
        <name>data-analysis</name>
        <description>Data analysis and visualization workflows [built-in]</description>
        <location>/mnt/skills/public/data-analysis/SKILL.md</location>
    </skill>
    <skill>
        <name>deep-research</name>
        <description>Deep research and report generation [built-in]</description>
        <location>/mnt/skills/public/deep-research/SKILL.md</location>
    </skill>
</available_skills>
</skill_system>

Token 消耗对比

方式每个 Skill 消耗10 个 Skill 总消耗
全量注入~2000 tokens~20000 tokens
元信息注入~50 tokens~500 tokens
节省比例-97.5%

五、LLM 匹配机制:纯提示词引导

5.1 核心问题

LLM 如何知道应该使用哪个 Skill?

5.2 答案:没有代码层面的匹配

关键结论:DeerFlow 不做服务端预判,完全依赖 LLM 的语义理解能力。

原因

  1. 用户意图复杂多变,难以通过规则穷举
  2. LLM 具备强大的语义理解能力
  3. 提示词引导 + 元信息足够让 LLM 做出正确判断

5.3 匹配流程

┌─────────────────────────────────────────────────────────────┐
│  1. 后端注入 Skill 元信息到 System Prompt                    │
│     - name: "frontend-design"                               │
│     - description: "Frontend design and development..."     │
│     - location: "/mnt/skills/public/frontend-design/..."    │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  2. 用户输入                                                 │
│     "帮我开发一个 React 后台管理系统"                          │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  3. LLM 语义理解                                             │
│     - 分析: 用户要做前端开发                                  │
│     - 扫描: 查看所有 Skill 的 name 和 description            │
│     - 匹配: "frontend-design" 的描述最相关                   │
│     - 决策: 需要加载这个 Skill                               │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  4. LLM 调用工具                                             │
│     read_file("/mnt/skills/public/frontend-design/SKILL.md")│
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  5. 返回完整 Skill 内容                                       │
│     LLM 根据内容中的指令执行任务                              │
└─────────────────────────────────────────────────────────────┘

5.4 提示词引导的关键

源码位置:backend/packages/harness/deerflow/agents/lead_agent/prompt.py:557-562

**Progressive Loading Pattern:**
1. When a user query matches a skill's use case, 
   immediately call `read_file` on the skill's main file
2. Read and understand the skill's workflow and instructions
3. The skill file contains references to external resources
4. Load referenced resources only when needed during execution
5. Follow the skill's instructions precisely

这段提示词明确告诉 LLM:

  1. 何时加载:用户问题匹配 Skill 用途时
  2. 如何加载:调用 read_file 读取 Skill 文件
  3. 如何执行:按 Skill 指令执行,按需加载资源

六、Agent Loop:Skill 执行的核心机制

6.1 ReAct 循环

DeerFlow 基于 LangGraph 的 create_agent 实现,内部是一个 ReAct 循环(Reasoning + Acting):

┌─────────────────────────────────────────────────────────────┐
  before_agent (只执行一次)                                   
  - ThreadDataMiddleware: 创建线程目录                        
  - SandboxMiddleware: 获取沙箱实例                           
└─────────────────────────────────────────────────────────────┘
                              
                              
        ┌─────────────────────────────────────────┐
                                                 
            ┌─────────────────────────────────┐  
              before_model                     
              - ViewImageMiddleware            
            └─────────────────────────────────┘  
                                                 
                                                 
            ┌─────────────────────────────────┐  
              MODEL (LLM 推理)                 
              - 分析请求                        
              - 匹配 Skill                     
              - 决定 tool call                 
            └─────────────────────────────────┘  
                                                 
                                                 
            ┌─────────────────────────────────┐  
              after_model                      
              - ClarificationMiddleware        
              - LoopDetectionMiddleware        
            └─────────────────────────────────┘  
                                                 
                                                 
            ┌─────────────────────────────────┐  
              ToolNode (工具执行)              
              - read_file(skill)               
              - bash, write_file, ...          
            └─────────────────────────────────┘  
                                                 
                                                 
                  tool call?                    
                /            \                    
              Yes            No                  
                                               
               └──────────────┘                  
                                                
        └──────────────┼──────────────────────────┘
                       
                       
┌─────────────────────────────────────────────────────────────┐
  after_agent (只执行一次)                                    
  - SandboxMiddleware: 释放沙箱                              
  - MemoryMiddleware: 入队记忆更新                            
└─────────────────────────────────────────────────────────────┘

6.2 Skill 加载的具体执行流程

用户请求:"帮我开发一个 React 后台管理系统"

═══════════════════════════════════════════════════════════════
第 1 轮循环: 匹配并加载 Skill
───────────────────────────────────────────────────────────────
MODEL:
  LLM 思考: "用户要做 React 前端开发,需要加载 frontend-design skill"
  LLM 输出: tool_call(read_file, "/mnt/skills/public/frontend-design/SKILL.md")

ToolNode:
  执行 read_file → 返回 SKILL.md 完整内容
  
判定: 有 tool_call → 继续循环

═══════════════════════════════════════════════════════════════
第 2 轮循环: 根据 Skill 指令执行
───────────────────────────────────────────────────────────────
MODEL:
  LLM 思考: "根据 skill 指令,需要创建项目结构"
  LLM 输出: tool_call(bash, "npm create vite@latest admin-system -- --template react-ts")

ToolNode:
  执行 bash 命令 → 创建项目

判定: 有 tool_call → 继续循环

═══════════════════════════════════════════════════════════════
第 3 轮循环: 继续执行任务
───────────────────────────────────────────────────────────────
MODEL:
  LLM 思考: "安装依赖并配置 antd"
  LLM 输出: tool_call(bash, "cd admin-system && npm install antd @ant-design/pro-components")

ToolNode:
  执行命令 → 安装依赖

... 继续循环 ...

═══════════════════════════════════════════════════════════════
第 N 轮循环: 完成任务
───────────────────────────────────────────────────────────────
MODEL:
  LLM 思考: "任务已完成,总结结果"
  LLM 输出: 文本响应(无 tool_call)

判定: 无 tool_call → 退出循环

═══════════════════════════════════════════════════════════════
after_agent:
  - 释放沙箱
  - 将对话入队记忆更新
  
返回响应: "React 后台管理系统已创建完成..."

6.3 循环退出条件

条件结果
LLM 无 tool_call✅ 正常退出
LLM 调用 ask_clarification✅ 被拦截后退出
达到 recursion_limit❌ 报错退出
LoopDetection 检测到死循环⚠️ 强制中断

源码位置:backend/packages/harness/deerflow/agents/middlewares/loop_detection_middleware.py


七、架构全景图

┌─────────────────────────────────────────────────────────────────────────┐
│                           用户请求                                       │
│                    "帮我开发一个 Web 应用"                                │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                        后端服务 (FastAPI)                                │
├─────────────────────────────────────────────────────────────────────────┤
│  services.py:239-333                                                    │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  start_run()                                                     │   │
│  │  - 创建 RunRecord                                                │   │
│  │  - agent_factory = resolve_agent_factory() → make_lead_agent   │   │
│  │  - asyncio.create_task(run_agent(...))                          │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                        Agent 创建 (make_lead_agent)                      │
├─────────────────────────────────────────────────────────────────────────┤
│  lead_agent/agent.py:273-350                                            │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  1. 解析配置 (model_name, plan_mode, subagent_enabled)          │   │
│  │  2. 加载工具: get_available_tools()                             │   │
│  │  3. 构建中间件: _build_middlewares()                            │   │
│  │  4. 生成提示词: apply_prompt_template()                         │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                    ┌───────────────┴───────────────┐
                    ▼                               ▼
┌───────────────────────────────┐   ┌───────────────────────────────────────┐
│      get_available_tools()    │   │     apply_prompt_template()           │
│      tools/tools.py:35-137    │   │     lead_agent/prompt.py:674-724      │
├───────────────────────────────┤   ├───────────────────────────────────────┤
│  - 内置工具 (bash, read_file) │   │  - get_skills_prompt_section()        │
│  - MCP 工具 (延迟加载)        │   │    └─ _get_enabled_skills()           │
│  - 工具搜索 (tool_search)     │   │       └─ 检查缓存                     │
└───────────────────────────────┘   │       └─ load_skills() [缓存未命中]   │
                                    │           └─ 扫描 skills/ 目录        │
                                    │           └─ parse_skill_file()       │
                                    │           └─ ExtensionsConfig         │
                                    │  - 生成 <skill_system> XML            │
                                    └───────────────────────────────────────┘
                                                    │
                                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                        Agent 执行 (ReAct Loop)                           │
├─────────────────────────────────────────────────────────────────────────┤
│  runtime/runs/worker.py:34-216                                          │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  before_agent: [ThreadData → Uploads → Sandbox]                 │   │
│  │                                                                  │   │
│  │  ┌───────────────────────────────────────────────────────────┐  │   │
│  │  │  LOOP:                                                     │  │   │
│  │  │    before_model → MODEL → after_model → ToolNode          │  │   │
│  │  │         ↑                                        │         │  │   │
│  │  │         └──────────── 有 tool call ─────────────┘         │  │   │
│  │  └───────────────────────────────────────────────────────────┘  │   │
│  │                                                                  │   │
│  │  after_agent: [Sandbox释放 → Memory入队]                        │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                           SSE 流式响应                                   │
│  - 实时推送 LLM 输出                                                     │
│  - 实时推送工具执行结果                                                  │
└─────────────────────────────────────────────────────────────────────────┘

八、性能优化要点

8.1 缓存策略

缓存类型位置生命周期刷新触发
全局 Skill 缓存_enabled_skills_cache应用启动后首次请求配置变更
提示词 LRU 缓存_get_cached_skills_prompt_section32 个签名Skill 变更
MCP 工具缓存mcp/cache.py应用启动MCP 配置变更

8.2 延迟加载

MCP 工具延迟加载tools/tools.py:108-117

if config.tool_search.enabled:
    registry = DeferredToolRegistry()
    for t in mcp_tools:
        registry.register(t)  # 不立即绑定到 LLM
    builtin_tools.append(tool_search_tool)  # LLM 通过搜索按需获取

Skill 延迟加载:只注入元信息,详细内容通过 read_file 按需读取

8.3 并发安全

  • 使用 threading.Lock 保护全局缓存
  • 使用 ContextVar 隔离请求级别的数据
  • 后台线程异步刷新,不阻塞主线程

九、最佳实践

9.1 编写高质量 Skill

好的 Description 是关键

---
name: api-design
description: RESTful API design patterns, OpenAPI specification generation, and backend architecture best practices for Node.js, Python, and Go
---

要点

  • 包含关键词(RESTful, OpenAPI, Node.js, Python, Go)
  • 描述清晰,便于 LLM 语义匹配
  • 包含适用场景

9.2 合理组织 Skill 内容

# API Design Skill

## Quick Start
1. Analyze requirements
2. Design endpoints
3. Generate OpenAPI spec

## Best Practices
- Use plural nouns for resources
- Version your APIs
- Implement proper error handling

## References
- [OpenAPI Spec](./references/openapi-template.yaml)
- [Error Codes](./references/error-codes.md)

要点

  • 开头给出快速指南
  • 详细内容放在后面
  • 外部资源使用相对路径引用

9.3 配置管理

{
  "skills": {
    "frontend-design": { "enabled": true },
    "deep-research": { "enabled": true },
    "deprecated-skill": { "enabled": false }
  }
}

要点

  • 禁用不常用的 Skill 减少 Token 消耗
  • 根据项目需求启用特定 Skill
  • 定期清理过时的 Skill

十、总结

DeerFlow 的动态加载 Skill 机制体现了几个核心设计思想:

  1. 渐进式加载:元信息轻量注入,详细内容按需读取
  2. LLM 自主决策:不做服务端预判,发挥 LLM 语义理解能力
  3. 缓存优化:多级缓存,避免重复加载
  4. 线程安全:完善的并发控制机制
  5. 可扩展性:支持无限扩展 Skill 数量

这套机制在 Token 效率、响应速度、可扩展性之间取得了良好的平衡,是 AI Agent 系统设计的优秀实践。


参考资料


本文基于 DeerFlow 源码深度分析,转载请注明出处。欢迎在评论区讨论交流!