1. 项目概述:为什么我们需要一份SKILL.md

如果你最近在折腾OpenClaw,想把一个想法变成一个能真正干活儿的智能体技能,那你大概率会卡在第一步:这个技能到底该怎么写?文档在哪?OpenClaw的官方文档可能还在快速迭代中,社区里的例子也五花八门,对于一个新上手的开发者来说,最头疼的不是代码逻辑,而是“从哪开始”以及“按什么标准来写”。这就是我动手写这份《从零开始写SKILL.md》的初衷。

简单说,SKILL.md就是OpenClaw智能体技能的“说明书”和“身份证”。它不是一个可有可无的文档,而是技能能被OpenClaw核心系统正确识别、加载、配置和调用的关键。没有它,你的技能代码写得再漂亮,也只是一个躺在文件夹里的孤立脚本。这份指南会带你走完从构思到落地的完整流程,我会把官方文档里没细说的、社区踩过的坑、以及我自己调试时总结的经验,都揉碎了讲给你听。无论你是想给OpenClaw加一个查询天气的技能,还是想做一个能自动处理工单的复杂Agent,这篇内容都能给你一套清晰、可复现的行动路线。

2. 核心设计:理解SKILL.md的骨架与灵魂

在动手写第一行代码之前,我们必须先搞清楚OpenClaw技能的核心构成。一个完整的技能远不止一个Python函数,它是一套遵循特定约定的文件集合,而SKILL.md是统领这一切的纲领性文件。

2.1 SKILL.md的核心作用与文件结构

你可以把SKILL.md想象成智能体技能的“部署清单”和“配置总览”。它的核心作用有三个:

  1. 元信息声明 :告诉OpenClaw系统这个技能叫什么、是谁写的、版本是多少、简单描述是什么。
  2. 能力定义 :明确列出这个技能提供了哪些可被调用的“工具”(Tools)或“动作”(Actions),每个工具需要什么参数。
  3. 依赖与配置 :指明运行这个技能需要什么样的环境(Python包、系统命令)、以及有哪些可供用户调节的设置项。

一个典型的技能项目文件夹结构如下:

my_awesome_skill/
├── SKILL.md          # 核心配置文件,本文重点
├── skill.py          # 技能的主要实现代码
├── requirements.txt  # Python依赖包列表
├── config.json       # (可选)技能级别的配置,如API密钥模板
└── README.md         # (可选)给人类开发者看的详细说明

SKILL.md是入口 。当OpenClaw启动时,它会扫描技能目录,读取每个技能文件夹下的SKILL.md文件,并据此来动态注册和加载技能。如果你的SKILL.md格式错误或关键信息缺失,技能加载就会失败,通常会在OpenClaw的日志里看到令人困惑的错误信息,比如找不到某个工具。

2.2 技能能力模型:Tool与Action的设计哲学

OpenClaw技能通过“工具”来暴露其功能。在SKILL.md中,你需要详细定义每一个工具。这不仅仅是起个名字那么简单,它涉及到智能体如何理解和使用你的技能。

一个工具定义通常包含:

  • name : 工具的唯一标识符,建议使用蛇形命名法,如 get_weather_forecast
  • description : 这是 最关键 的部分。描述必须清晰、无歧义,说明这个工具是做什么的,它会处理什么输入,产生什么输出。大语言模型(LLM)正是根据这个描述来决定在什么场景下调用这个工具。模糊的描述会导致智能体“误诊”或“拒诊”。
  • parameters : 定义输入参数的JSON Schema。这包括参数名、类型、是否必需、以及参数描述。

实操心得:描述字段的“艺术” 我踩过最大的坑就是工具描述写得太“程序员思维”。比如,最初我写了一个总结网页内容的工具,描述是“Fetch and summarize URL content”。结果智能体经常在用户根本没提供URL,只是问“今天新闻有什么”时,就试图调用这个工具,然后因为缺少参数而报错。 后来我把它改成:“当用户提供了一个明确的具体网页链接(URL)并希望了解其内容概要时,使用此工具获取该网页的文本并生成摘要。如果用户只是泛泛提问,没有给出具体链接,则不应使用此工具。” 修改后,工具的调用准确率大幅提升。所以,请站在智能体的角度,用自然语言清晰地划定工具的 边界和触发条件

3. 从零开始:手把手编写你的第一个SKILL.md

理论讲完了,我们直接进入实战。假设我们要开发一个“工作日计算器”技能,它能计算两个日期之间的工作日天数(排除周末和自定义节假日)。

3.1 初始化项目与SKILL.md骨架

首先,创建一个技能目录并初始化SKILL.md文件:

mkdir business_day_calculator
cd business_day_calculator
touch SKILL.md skill.py requirements.txt

接下来,打开SKILL.md,我们从最基础的元信息开始写起:

# Business Day Calculator

**Author:** Your Name
**Version:** 1.0.0
**Description:** 一个用于计算两个日期之间工作日(排除周末和自定义节假日)天数的工具。适用于项目排期、请假天数计算等场景。

这里, # 后面的标题就是技能的名称,它会在OpenClaw的技能列表里显示。Description要一句话概括技能的核心价值。

3.2 定义核心工具(Tools)

在元信息下方,我们定义技能提供的工具。这是SKILL.md的主体。

## Tools

### calculate_business_days
**Description:** 计算起始日期和结束日期之间的工作日数量。工作日默认排除星期六和星期日。用户可以提供一个可选的国家或地区代码,以自动排除该地区的公共节假日;也可以提供一个自定义的节假日日期列表进行排除。如果起始日期晚于结束日期,将返回负数。
**Parameters:**
```json
{
  "type": "object",
  "properties": {
    "start_date": {
      "type": "string",
      "format": "date",
      "description": "起始日期,格式为YYYY-MM-DD,例如:2023-10-01。"
    },
    "end_date": {
      "type": "string",
      "format": "date",
      "description": "结束日期,格式为YYYY-MM-DD,例如:2023-10-10。注意:结束日期当天是否计入取决于具体业务逻辑,本工具默认包含结束日期。"
    },
    "country_code": {
      "type": "string",
      "description": "可选。国家或地区代码(如'US'、'CN'),用于自动排除该地区的法定节假日。需要网络连接以获取节假日数据。",
      "default": null
    },
    "custom_holidays": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "date"
      },
      "description": "可选。自定义的节假日日期列表,格式为['YYYY-MM-DD', ...]。这些日期也将被排除在工作日之外。",
      "default": []
    }
  },
  "required": ["start_date", "end_date"]
}

Returns:

{
  "type": "object",
  "properties": {
    "business_days": {
      "type": "integer",
      "description": "计算得到的工作日天数。"
    },
    "detail": {
      "type": "string",
      "description": "计算过程的简要说明,例如:'从2023-10-01到2023-10-10,共10天,排除2个周末日(10-07,10-08),0个节假日,工作日为8天。'"
    }
  }
}

关键点解析:

  1. 参数设计 start_date end_date 是必填项。 country_code custom_holidays 是可选项,提供了灵活性。给 end_date 添加了明确的“包含”说明,避免了歧义。
  2. 描述细节 :在 country_code 的描述中,我特意加入了“需要网络连接”的提示,这是一个重要的实施约束,提前告知了可能的风险。
  3. 返回结构 :不仅返回一个数字,还返回一个 detail 字段用于解释。这对于调试和让用户(或智能体)理解结果非常有用,能增加技能的可靠性和透明度。

3.3 声明依赖与配置

工具定义好后,需要说明运行它需要什么。

## Dependencies
- **Python Packages:** 在 `requirements.txt` 中列出。
- **System Commands:** 无。
- **External APIs:** 如果使用`country_code`参数,需要能访问公共节假日API(例如`holidays`库的在线数据源或自定义API)。

## Configuration
以下配置可在OpenClaw的技能管理界面或配置文件中进行设置:
- `HOLIDAY_API_TIMEOUT`: 获取节假日API的超时时间(秒),默认5秒。
- `DEFAULT_COUNTRY_CODE`: 默认的国家代码,当用户未提供`country_code`参数时使用(谨慎设置,建议留空)。
- `WORK_WEEK_START`: 工作周起始日(0为周一,6为周日),默认0(周一)。
- `WORK_WEEK_END`: 工作周结束日(0为周一,6为周日),默认4(周五)。此配置可用于定义非标准工作周。

注意事项: DEFAULT_COUNTRY_CODE 是一个双刃剑。虽然方便,但如果设置了一个全局默认值,可能导致用户在不经意间使用了非预期的节假日规则。我的建议是,除非技能有非常明确的单一地域使用场景,否则不要设置全局默认值,强制用户在调用时显式指定或留空。

4. 技能实现:将SKILL.md与代码连接起来

SKILL.md定义了“契约”,而 skill.py 则是契约的“履行者”。两者必须严格对应。

4.1 编写skill.py核心逻辑

打开 skill.py ,实现我们在SKILL.md中定义的 calculate_business_days 工具。

import datetime
from typing import Dict, Any, List, Optional
import holidays
import logging

logger = logging.getLogger(__name__)

class BusinessDayCalculatorSkill:
    def __init__(self, config: Optional[Dict[str, Any]] = None):
        self.config = config or {}
        self.api_timeout = self.config.get('HOLIDAY_API_TIMEOUT', 5)
        self.work_week_start = self.config.get('WORK_WEEK_START', 0)  # Monday
        self.work_week_end = self.config.get('WORK_WEEK_END', 4)      # Friday
        # 注意:默认国家代码不在技能层设置,由调用参数决定
        logger.info(f"BusinessDayCalculatorSkill initialized with work week from {self.work_week_start} to {self.work_week_end}")

    def calculate_business_days(self, start_date: str, end_date: str,
                                 country_code: Optional[str] = None,
                                 custom_holidays: Optional[List[str]] = None) -> Dict[str, Any]:
        """
        实现SKILL.md中定义的工具逻辑。
        """
        try:
            # 1. 解析日期
            start = datetime.datetime.strptime(start_date, '%Y-%m-%d').date()
            end = datetime.datetime.strptime(end_date, '%Y-%m-%d').date()
            
            if start > end:
                # 如果开始日期晚于结束日期,交换并计算负数
                start, end = end, start
                reverse = True
            else:
                reverse = False

            # 2. 构建节假日集合
            holiday_set = set()
            # 2.1 添加自定义节假日
            if custom_holidays:
                for day_str in custom_holidays:
                    try:
                        holiday_set.add(datetime.datetime.strptime(day_str, '%Y-%m-%d').date())
                    except ValueError:
                        logger.warning(f"Invalid custom holiday date format skipped: {day_str}")
            
            # 2.2 添加国家法定节假日(使用holidays库,注意网络延迟)
            if country_code:
                try:
                    # 这里假设使用`holidays`库,它可能需要在线获取数据
                    country_holidays = holidays.CountryHoliday(country_code, years=range(start.year, end.year + 1))
                    for date_obj, _ in country_holidays.items():
                        if start <= date_obj <= end:
                            holiday_set.add(date_obj)
                except Exception as e:
                    logger.error(f"Failed to fetch holidays for {country_code}: {e}")
                    # 根据你的策略,可以选择抛出错误或仅记录警告并继续
                    # 这里我们选择记录错误并继续,不中断计算
                    pass

            # 3. 计算工作日
            business_days = 0
            current_day = start
            detail_days = []
            
            while current_day <= end:
                # 判断是否为工作日(根据配置的工作周)
                is_weekday = self.work_week_start <= current_day.weekday() <= self.work_week_end
                # 判断是否为节假日
                is_holiday = current_day in holiday_set
                
                if is_weekday and not is_holiday:
                    business_days += 1
                    detail_days.append(current_day.strftime('%Y-%m-%d'))
                
                current_day += datetime.timedelta(days=1)

            # 4. 处理反向计算
            if reverse:
                business_days = -business_days
                detail_msg = f"反向计算:从{end_date}到{start_date},工作日为{business_days}天。"
            else:
                total_days = (end - start).days + 1
                weekend_days = total_days - business_days - len(holiday_set) + (len([d for d in holiday_set if not (self.work_week_start <= d.weekday() <= self.work_week_end)]))
                detail_msg = f"从{start_date}到{end_date},共{total_days}天,排除{weekend_days}个周末日,{len(holiday_set)}个节假日,工作日为{business_days}天。"

            return {
                "business_days": business_days,
                "detail": detail_msg,
                "included_days": detail_days if not reverse else []  # 反向时不列出具体日期
            }

        except ValueError as e:
            logger.exception(f"Date parsing error: {e}")
            return {
                "business_days": 0,
                "detail": f"日期格式错误,请使用YYYY-MM-DD格式。错误信息:{e}",
                "error": True
            }
        except Exception as e:
            logger.exception(f"Unexpected error in calculate_business_days: {e}")
            return {
                "business_days": 0,
                "detail": f"计算过程中发生意外错误:{e}",
                "error": True
            }

# OpenClaw技能的标准入口点:必须提供一个`get_skill`函数
def get_skill(config: Dict[str, Any]):
    return BusinessDayCalculatorSkill(config)

4.2 编写requirements.txt

skill.py 中用到了 holidays 库,我们需要在 requirements.txt 中声明:

holidays>=0.36
python-dateutil>=2.8.2  # holidays库可能依赖

注意 :依赖版本尽量使用宽松的约束(如 >= ),避免与其他技能的依赖发生冲突。但也要确保最低版本能满足功能需求。

4.3 技能注册与OpenClaw的集成

写完代码后,如何让OpenClaw知道这个技能?关键在于 技能目录的放置 。你需要将整个 business_day_calculator 文件夹放到OpenClaw指定的技能加载路径下。这个路径通常在OpenClaw的配置文件(如 config.yaml )中通过 skill_directories 或类似的配置项设置。

例如,在OpenClaw的配置中:

skills:
  directories:
    - /path/to/openclaw/skills  # 官方或社区技能目录
    - /path/to/your/custom/skills  # 你的自定义技能目录

将你的 business_day_calculator 文件夹放入 /path/to/your/custom/skills 目录中。重启OpenClaw服务后,它应该能自动扫描并加载你的技能。你可以在OpenClaw的Web界面或通过相关API查看到新技能 Business Day Calculator 及其工具 calculate_business_days

5. 调试、测试与问题排查实录

技能部署后不工作?这是最常遇到的阶段。别慌,按照以下步骤系统性排查。

5.1 常见加载失败问题

  1. 技能在列表中不显示

    • 检查点1:目录位置 。确认技能文件夹是否放入了正确的、且已被OpenClaw配置加载的技能目录中。
    • 检查点2:SKILL.md文件名与格式 。必须是大写的 SKILL.md ,且是有效的Markdown格式。可以用 python -m markdown SKILL.md 简单测试是否能被解析。
    • 检查点3:OpenClaw日志 。查看OpenClaw启动或重载技能时的日志,搜索 ERROR skill 关键词,通常会有明确的加载失败原因,如“Invalid tool definition”或“Module not found”。
  2. 工具显示但调用时报错

    • 检查点1:参数匹配 。调用工具时传递的参数名、类型是否与SKILL.md中 parameters 的JSON Schema定义完全一致?特别是日期格式 YYYY-MM-DD
    • 检查点2:依赖缺失 。尽管SKILL.md声明了依赖,但OpenClaw环境可能未安装。你需要手动在OpenClaw的运行环境中安装 requirements.txt 中的包。如果是Docker部署,可能需要重建镜像或进入容器安装。
    • 检查点3:代码异常 。查看OpenClaw的实时日志,工具调用时的异常堆栈信息会打印在这里。重点关注 skill.py calculate_business_days 函数内部的错误。

5.2 编写技能单元测试(强烈推荐)

在技能目录下创建一个 test_skill.py ,可以极大提升调试效率。

import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from skill import BusinessDayCalculatorSkill

def test_calculate_business_days_basic():
    skill = BusinessDayCalculatorSkill()
    result = skill.calculate_business_days('2023-10-01', '2023-10-10')
    assert result['business_days'] == 8  # 10月1-10日,排除7、8日周末
    assert '工作日为8天' in result['detail']
    print("✓ 基础测试通过")

def test_calculate_business_days_with_holidays():
    skill = BusinessDayCalculatorSkill()
    # 假设10月2日是自定义节假日
    result = skill.calculate_business_days('2023-10-01', '2023-10-10', custom_holidays=['2023-10-02'])
    # 原本8天工作日,再排除10月2日(周一)
    assert result['business_days'] == 7
    assert '1个节假日' in result['detail']
    print("✓ 自定义节假日测试通过")

def test_calculate_business_days_reverse():
    skill = BusinessDayCalculatorSkill()
    result = skill.calculate_business_days('2023-10-10', '2023-10-01')
    assert result['business_days'] == -8
    assert '反向计算' in result['detail']
    print("✓ 反向计算测试通过")

if __name__ == '__main__':
    test_calculate_business_days_basic()
    test_calculate_business_days_with_holidays()
    test_calculate_business_days_reverse()
    print("所有测试通过!")

在技能目录下运行 python test_skill.py ,确保核心逻辑在独立环境下正确无误,这能帮你快速隔离是技能逻辑问题还是OpenClaw集成问题。

5.3 处理网络依赖与超时

我们的技能有一个潜在故障点:通过网络获取国家节假日数据。在 skill.py 中,我们用了 try-except 包裹了这部分代码,并记录了错误日志。这是一种 优雅降级 的策略:即使获取节假日失败,技能依然能基于周末和自定义节假日完成计算,只是结果可能不包含该国法定假日。 对于更关键的网络调用,你可以在SKILL.md的Configuration部分增加重试次数、备用API等配置项,并在代码中实现相应的容错逻辑。永远假设网络是不可靠的。

6. 进阶技巧:打造更健壮、易用的技能

当你掌握了基础技能开发后,下面这些技巧能让你的技能更上一层楼。

6.1 技能配置的动态化与持久化

上面的例子中,配置(如 WORK_WEEK_START )是在技能初始化时从 config 参数读取的。在OpenClaw中,这个 config 通常来自主配置文件或数据库。为了让用户能在不重启OpenClaw的情况下调整配置,你可以:

  1. 在SKILL.md的Configuration部分 ,清晰地列出所有可配置项及其说明、默认值。
  2. skill.py 的类中 ,不要将配置在 __init__ 中写死,而是保存 self.config 引用,并在每个工具方法中动态读取。或者,设计一个 update_config 方法供OpenClaw在配置变更时调用。
  3. 为配置提供验证 。在 __init__ update_config 中,验证传入的配置值是否合法(例如 WORK_WEEK_START 是否在0-6之间)。

6.2 工具描述的优化与智能体引导

工具描述是智能体能否正确使用的关键。除了前面提到的明确边界,还可以:

  • 提供示例 :虽然在SKILL.md的JSON Schema中不能直接写示例,但你可以在 description 字段的末尾用自然语言补充。例如:“例如,当用户问‘从国庆节到元旦有多少个工作日?’时,你可以询问具体的起止日期,然后调用此工具。”
  • 说明副作用 :如果工具会修改外部数据(如创建工单、发送邮件),必须在描述中明确指出。
  • 关联其他工具 :如果一个技能有多个相关工具,可以在描述中提示智能体。“此工具仅计算天数。如需生成详细的日期列表,请使用 generate_business_day_calendar 工具。”

6.3 技能的生命周期管理与状态保持

简单的计算技能是无状态的。但对于一些复杂技能,可能需要保持状态(如一个多轮对话的预约技能)。OpenClaw通常为每个用户会话或每个智能体实例化一个技能对象。这意味着:

  • __init__ 中初始化状态 :连接数据库、加载大模型、初始化缓存等。
  • 避免在工具方法中使用全局变量 :因为OpenClaw可能以多线程/异步方式调用,全局变量会导致竞争条件。状态应保存在技能实例的属性中( self.xxx )。
  • 清理资源 :如果技能持有需要关闭的资源(如网络连接、文件句柄),可以实现一个 __del__ 方法或提供一个 shutdown 工具供系统调用。

6.4 错误处理与用户反馈

永远不要让你的技能因为未处理的异常而崩溃。在 skill.py 中,每个工具方法都应该有完善的 try-except

  • 返回结构化的错误信息 :如上面的例子,在发生错误时,返回一个包含 error: True 字段和友好 detail 信息的字典。这比抛出一个让OpenClaw全局捕获的异常更好,因为智能体可以解析这个错误信息,并选择重试或向用户解释。
  • 日志分级 :使用 logging 模块记录不同级别的日志( INFO , WARNING , ERROR )。 INFO 用于记录正常操作, WARNING 记录可恢复的异常(如网络超时), ERROR 记录需要人工干预的严重错误。清晰的日志是线上排查问题的生命线。

开发OpenClaw技能是一个将你的想法转化为智能体实际能力的过程。SKILL.md是这一切的蓝图,它要求你以机器可读的方式严谨地定义契约,同时又需要用人类可理解的语言清晰地描述意图。从一份结构清晰、描述准确的SKILL.md开始,你的技能开发之路就成功了一半。剩下的,就是用扎实的代码去实现它,并用耐心的调试去完善它。当你看到自己开发的技能被智能体流畅地调用并解决了实际问题时,那种成就感会让你觉得这一切都是值得的。

更多推荐