有兴趣的朋友可以到我的知识星球“小龙虾孵化实验室”共同探索智能工具的实现与应用(落地与变现)。

目录

  1. 核心概念
  2. OpenClaw 技能加载机制
  3. 小龙虾技能加载机制
  4. 对比分析
  5. 最佳实践
  6. 故障排查

注:小龙虾是我开发的类OpenClaw系统


核心概念

什么是技能动态加载?

技能动态加载 = AI 启动时自动扫描技能目录,解析技能定义,生成工具列表,无需修改代码。

传统方式(静态)

// 硬编码在代码中
this.toolDefinitions = [
  { name: 'read_file', ... },
  { name: 'write_file', ... },
  // 添加新技能需要修改代码
];

动态加载

// 运行时生成
this.toolDefinitions = this.loadSkillsFromDirectory();
// 添加新技能只需复制文件到 skills/ 目录

技能文件格式(SKILL.md)

---
name: weather
description: Get current weather and forecasts (no API key required).
homepage: https://wttr.in/:help
metadata: {"clawdbot":{"emoji":"🌤️","requires":{"bins":["curl"]}}}
---

# Weather

Two free services, no API keys needed.

## wttr.in (primary)
```bash
curl -s "wttr.in/London?format=3"

Open-Meteo (fallback, JSON)

curl -s "https://api.open-meteo.com/v1/forecast?..."

**结构说明**:
- **Frontmatter**(YAML):技能元数据
  - `name` - 技能名称(工具调用名)
  - `description` - 技能描述(AI 理解用途)
  - `homepage` - 技能主页
  - `metadata` - 扩展配置
- **Markdown 正文**:技能使用说明、示例代码

---

## OpenClaw 技能加载机制

### 目录结构


~/.openclaw/
├── workspace/
│   └── skills/              # 技能目录
│       ├── weather/         # 天气技能
│       │   └── SKILL.md
│       ├── stock-monitor/   # 股票监控
│       │   └── SKILL.md
│       └── README.md
├── config/
│   └── openclaw.json        # 配置文件
└── ...

加载流程

┌─────────────────────────────────────────────────────────┐
│ 1. OpenClaw Gateway 启动                                │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 2. 扫描 skills 目录                                      │
│    for skill_dir in workspace/skills/                   │
│      if exists(skill_dir + '/SKILL.md'):                │
│        skills.append(parseSKILL(skill_dir))             │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 3. 解析 SKILL.md                                         │
│    - 读取 frontmatter(YAML)                           │
│    - 提取 name, description, metadata                   │
│    - 验证技能格式                                        │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 4. 注册技能到工具系统                                   │
│    tools.register({                                     │
│      name: skill.name,                                  │
│      description: skill.description,                    │
│      handler: loadSkillHandler(skill.path)              │
│    })                                                   │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 5. 生成系统提示词                                       │
│    prompt = `                                           │
│      你是阿财,老大的助手。                             │
│                                                         │
│      ## 你有以下工具                                     │
│      ${tools.map(t => `- ${t.name}: ${t.description}`)}│
│                                                         │
│      ...                                                │
│    `                                                    │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 6. 发送给大模型                                         │
│    - 大模型看到动态生成的工具列表                        │
│    - 根据需要调用工具                                    │
│    - Gateway 执行对应技能                                │
└─────────────────────────────────────────────────────────┘

核心代码(伪代码)

// OpenClaw Gateway
class SkillManager {
  constructor(workspace) {
    this.skillsDir = path.join(workspace, 'skills');
    this.skills = [];
  }
  
  async loadSkills() {
    // 扫描技能目录
    const dirs = await fs.readdir(this.skillsDir);
    
    for (const dir of dirs) {
      const skillPath = path.join(this.skillsDir, dir, 'SKILL.md');
      
      if (await fs.exists(skillPath)) {
        const skill = await this.parseSKILL(skillPath);
        this.skills.push(skill);
        
        // 注册工具
        this.tools.register({
          name: skill.name,
          description: skill.description,
          handler: this.createHandler(skill)
        });
      }
    }
  }
  
  async parseSKILL(path) {
    const content = await fs.readFile(path, 'utf-8');
    const parts = content.split('---');
    
    // 解析 YAML frontmatter
    const frontmatter = yaml.parse(parts[1]);
    
    return {
      name: frontmatter.name,
      description: frontmatter.description,
      path: path,
      body: parts[2]
    };
  }
  
  buildPrompt() {
    const lines = ['## 你有以下工具'];
    
    for (const skill of this.skills) {
      lines.push(`- ${skill.name}: ${skill.description}`);
    }
    
    return lines.join('\n');
  }
}

特点

特性 说明
技能存储 ~/.openclaw/workspace/skills/
加载时机 Gateway 启动时
技能格式 SKILL.md(YAML frontmatter + Markdown)
工具注册 自动注册到 Gateway 工具系统
提示词生成 运行时动态生成
技能执行 Gateway 统一调度

小龙虾技能加载机制

目录结构

simple-ai/
├── src/
│   ├── core/
│   │   ├── skill_loader.py      # 技能加载器
│   │   └── plugin_manager.py    # 插件管理器
│   └── assistant-v7.js          # AI 核心
├── skills/                      # 技能目录(与 OpenClaw 共享)
│   └── weather/
│       └── SKILL.md
└── ...

加载流程

┌─────────────────────────────────────────────────────────┐
│ 1. 小龙虾启动(node src/server.js)                  │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 2. 创建 AI 助手实例                                      │
│    const assistant = new SimpleAIAssistant(...)         │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 3. 调用异步初始化                                        │
│    await assistant.init()                               │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 4. 加载技能(调用 Python 加载器)                        │
│    spawn('python', ['skill_loader.py', skills_dir])    │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 5. Python 技能加载器执行                                 │
│    - 扫描 skills 目录                                    │
│    - 解析 SKILL.md                                       │
│    - 输出技能列表(JSON/文本)                          │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 6. Node.js 解析输出                                     │
│    - 提取技能名和描述                                    │
│    - 构建工具定义(JSON Schema)                       │
│    - 更新 this.toolDefinitions                          │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 7. 生成系统提示词                                       │
│    this.buildToolsPrompt()                              │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 8. 调用大模型 API                                        │
│    - 发送工具定义给千问 API                              │
│    - 接收工具调用请求                                    │
│    - 执行对应技能                                        │
└─────────────────────────────────────────────────────────┘

核心代码

Python 技能加载器(skill_loader.py)
class SkillLoader:
    def __init__(self, skills_dir):
        self.skills_dir = skills_dir
        self.skills = []
    
    def scan_skills(self):
        """扫描所有技能"""
        for item in os.listdir(self.skills_dir):
            item_path = os.path.join(self.skills_dir, item)
            
            if not os.path.isdir(item_path) or item.startswith('.'):
                continue
            
            skill_md = os.path.join(item_path, 'SKILL.md')
            if os.path.exists(skill_md):
                skill = self._parse_skill_md(skill_md)
                if skill:
                    self.skills.append(skill)
                    print(f"[OK] 加载技能:{skill['name']}")
        
        return self.skills
    
    def _parse_skill_md(self, path):
        """解析 SKILL.md"""
        with open(path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 提取 frontmatter
        if content.startswith('---'):
            parts = content.split('---', 2)
            frontmatter_str = parts[1].strip()
            frontmatter = self._parse_yaml(frontmatter_str)
            
            return {
                'name': frontmatter.get('name', ''),
                'description': frontmatter.get('description', ''),
                'path': path,
            }
        
        return None
    
    def build_tools_definition(self):
        """构建工具定义(JSON Schema)"""
        tools = []
        
        for skill in self.skills:
            tool = {
                'type': 'function',
                'function': {
                    'name': skill['name'],
                    'description': skill['description'],
                    'parameters': {
                        'type': 'object',
                        'properties': {
                            'city': {
                                'type': 'string',
                                'description': '城市名',
                                'default': 'Beijing'
                            }
                        }
                    }
                }
            }
            tools.append(tool)
        
        return tools
Node.js AI 核心(assistant-v7.js)
class SimpleAIAssistant {
  constructor(apiKey, workspace) {
    this.workspace = workspace;
    this.skillsDir = path.join(workspace, '..', 'skills');
    this.skills = [];
    this.toolDefinitions = [];
  }
  
  async init() {
    await this.loadSkills();
    await this.loadMemoryToCache();
  }
  
  async loadSkills() {
    log('正在加载技能...');
    
    // 调用 Python 技能加载器
    const result = await new Promise((resolve, reject) => {
      const pyProcess = spawn('python', [
        path.join(__dirname, 'core', 'skill_loader.py'),
        this.skillsDir
      ], {
        cwd: this.workspace,
        encoding: 'utf-8'
      });
      
      let output = '';
      pyProcess.stdout.on('data', (data) => { output += data; });
      pyProcess.on('close', (code) => {
        if (code === 0) resolve(output);
        else reject(new Error('加载失败'));
      });
    });
    
    // 解析输出,提取技能
    const skillMatches = result.match(/\[OK\] 加载技能:(.+)/g);
    if (skillMatches) {
      for (const match of skillMatches) {
        const skillName = match.replace('[OK] 加载技能:', '').trim();
        this.skills.push({ name: skillName });
      }
    }
    
    // 更新工具定义
    this.toolDefinitions = this.buildToolDefinitions();
    log(`加载完成:${this.skills.length} 个技能`);
  }
  
  buildToolDefinitions() {
    // 基础工具
    const baseTools = this.getBaseToolDefinitions();
    
    // 技能工具
    const skillTools = this.skills.map(skill => ({
      type: 'function',
      function: {
        name: skill.name,
        description: `调用${skill.name}技能`,
        parameters: {
          type: 'object',
          properties: {
            city: {
              type: 'string',
              description: '城市名',
              default: 'Beijing'
            }
          }
        }
      }
    }));
    
    return baseTools.concat(skillTools);
  }
  
  buildToolsPrompt() {
    const lines = ['## 你有以下工具'];
    
    // 基础工具
    const baseTools = this.getBaseToolDefinitions();
    for (const tool of baseTools) {
      lines.push(`- ${tool.function.name}: ${tool.function.description}`);
    }
    
    // 技能工具
    for (const skill of this.skills) {
      lines.push(`- ${skill.name}: 调用${skill.name}技能`);
    }
    
    return lines.join('\n');
  }
}

特点

特性 说明
技能存储 simple-ai/../skills/(可与 OpenClaw 共享)
加载时机 AI 启动时(异步)
技能格式 SKILL.md(与 OpenClaw 兼容)
工具注册 动态生成 JSON Schema
提示词生成 运行时动态生成
技能执行 Node.js 直接执行或调用 Python

📊 对比分析

架构对比

维度 OpenClaw 小龙虾
运行环境 Node.js Gateway Node.js + Python
技能目录 ~/.openclaw/workspace/skills/ simple-ai/../skills/
加载方式 直接扫描 调用 Python 加载器
工具注册 Gateway 内部注册 JSON Schema 生成
技能执行 Gateway 统一调度 直接执行/Python 调用
配置复杂度 中等 简单

优劣势分析

OpenClaw

优势

  • 统一调度,资源管理好
  • 技能执行环境隔离
  • 支持复杂技能编排
  • 有完整的技能市场

劣势

  • 架构复杂,学习成本高
  • 需要 Gateway 常驻运行
  • 技能执行有额外开销
小龙虾

优势

  • 架构简单,易于理解
  • 直接执行,响应快
  • Python + Node.js 灵活组合
  • 可与 OpenClaw 共享技能

劣势

  • 技能执行需要自己管理
  • 没有统一的调度系统
  • 技能市场依赖 OpenClaw

【注:现已接入腾讯的skills社区】


实践

1. 技能目录管理

推荐结构

skills/
├── weather/              # 按功能分类
│   └── SKILL.md
├── stock-monitor/
│   └── SKILL.md
└── README.md             # 技能说明文档

命名规范

  • 技能名用小写字母和连字符
  • 避免中文和空格
  • 描述要清晰简洁

2. SKILL.md 编写

好的示例

---
name: weather
description: 查询全球任何城市的当前天气和预报
homepage: https://wttr.in/
metadata: {"emoji":"🌤️"}
---

# Weather Skill

查询天气情况,无需 API 密钥。

## 使用方法

```bash
curl -s "wttr.in/Beijing?format=3"

参数说明

  • city: 城市名(英文或拼音)
  • format: 输出格式(3=简洁,T=详细)

**避免**:
- 描述过于简单("查天气")
- 没有使用示例
- 参数说明不清晰

### 3. 技能加载优化

**启动时预加载**:
```javascript
// 推荐:启动时加载
await assistant.init();

// 不推荐:每次请求都加载
async chat() {
  await this.loadSkills();  // 太慢!
}

缓存技能列表

# 技能变化时重新加载
if skills_changed:
    loader.scan_skills()
else:
    return cached_skills

4. 错误处理

技能加载失败

try {
  await this.loadSkills();
} catch (error) {
  log(`技能加载失败:${error.message}`);
  // 使用基础工具继续运行
  this.toolDefinitions = this.getBaseToolDefinitions();
}

技能执行失败

async executeSkill(skill, params) {
  try {
    return await skill.handler(params);
  } catch (error) {
    return `技能执行失败:${error.message}`;
  }
}

🔧 故障排查

问题 1:技能未加载

现象:AI 说"我没有这个工具"

排查步骤

  1. 检查技能目录是否存在

    ls ~/.openclaw/workspace/skills/
    
  2. 检查 SKILL.md 格式

    cat weather/SKILL.md
    
  3. 查看加载日志

    tail -f logs/info.log | grep "技能"
    
  4. 手动测试加载器

    python src/core/skill_loader.py ~/.openclaw/workspace/skills/
    

问题 2:技能加载报错

常见错误

  • output is not defined → Promise 作用域问题
  • EADDRINUSE → 端口被占用
  • ENOENT → 文件路径错误

解决方法

  1. 检查代码中的变量作用域
  2. 停止占用的进程
  3. 使用绝对路径

问题 3:技能执行失败

排查步骤

  1. 检查技能依赖(如 curl、python 包)
  2. 查看技能执行日志
  3. 手动测试技能命令
  4. 检查权限问题

总结

核心要点

  1. 技能即文件 - 每个技能是独立的 SKILL.md 文件
  2. 动态发现 - 启动时自动扫描,无需修改代码
  3. 提示词生成 - 运行时构建工具列表
  4. 零配置 - 用户安装技能后重启即可用

计划

  1. 技能热加载 - 无需重启即可加载新技能
  2. 技能依赖管理 - 自动安装技能依赖
  3. 技能沙箱 - 更安全地执行第三方技能
  4. 技能市场 - 一键安装社区技能

有兴趣的朋友可以到我的知识星球“小龙虾孵化实验室”共同探索智能工具的实现与应用(落地与变现)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐