OpenClaw智能体技能开发指南:从SKILL.md编写到实战部署
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想象成智能体技能的“部署清单”和“配置总览”。它的核心作用有三个:
- 元信息声明 :告诉OpenClaw系统这个技能叫什么、是谁写的、版本是多少、简单描述是什么。
- 能力定义 :明确列出这个技能提供了哪些可被调用的“工具”(Tools)或“动作”(Actions),每个工具需要什么参数。
- 依赖与配置 :指明运行这个技能需要什么样的环境(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天。'"
}
}
}
关键点解析:
- 参数设计 :
start_date和end_date是必填项。country_code和custom_holidays是可选项,提供了灵活性。给end_date添加了明确的“包含”说明,避免了歧义。 - 描述细节 :在
country_code的描述中,我特意加入了“需要网络连接”的提示,这是一个重要的实施约束,提前告知了可能的风险。 - 返回结构 :不仅返回一个数字,还返回一个
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:目录位置 。确认技能文件夹是否放入了正确的、且已被OpenClaw配置加载的技能目录中。
- 检查点2:SKILL.md文件名与格式 。必须是大写的
SKILL.md,且是有效的Markdown格式。可以用python -m markdown SKILL.md简单测试是否能被解析。 - 检查点3:OpenClaw日志 。查看OpenClaw启动或重载技能时的日志,搜索
ERROR或skill关键词,通常会有明确的加载失败原因,如“Invalid tool definition”或“Module not found”。
-
工具显示但调用时报错
- 检查点1:参数匹配 。调用工具时传递的参数名、类型是否与SKILL.md中
parameters的JSON Schema定义完全一致?特别是日期格式YYYY-MM-DD。 - 检查点2:依赖缺失 。尽管SKILL.md声明了依赖,但OpenClaw环境可能未安装。你需要手动在OpenClaw的运行环境中安装
requirements.txt中的包。如果是Docker部署,可能需要重建镜像或进入容器安装。 - 检查点3:代码异常 。查看OpenClaw的实时日志,工具调用时的异常堆栈信息会打印在这里。重点关注
skill.py中calculate_business_days函数内部的错误。
- 检查点1:参数匹配 。调用工具时传递的参数名、类型是否与SKILL.md中
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的情况下调整配置,你可以:
- 在SKILL.md的Configuration部分 ,清晰地列出所有可配置项及其说明、默认值。
- 在
skill.py的类中 ,不要将配置在__init__中写死,而是保存self.config引用,并在每个工具方法中动态读取。或者,设计一个update_config方法供OpenClaw在配置变更时调用。 - 为配置提供验证 。在
__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开始,你的技能开发之路就成功了一半。剩下的,就是用扎实的代码去实现它,并用耐心的调试去完善它。当你看到自己开发的技能被智能体流畅地调用并解决了实际问题时,那种成就感会让你觉得这一切都是值得的。
更多推荐



所有评论(0)