目录
-
- 摘要
- [1. 引言:技能系统------AI Agent 的能力放大器](#1. 引言:技能系统——AI Agent 的能力放大器)
- [2. OpenClaw 技能系统概述](#2. OpenClaw 技能系统概述)
-
- [2.1 技能系统的设计哲学](#2.1 技能系统的设计哲学)
- [2.2 技能系统架构](#2.2 技能系统架构)
- [2.3 技能目录结构](#2.3 技能目录结构)
- [2.4 技能生命周期](#2.4 技能生命周期)
- [2.5 技能发现与调用机制](#2.5 技能发现与调用机制)
- [3. 需求分析与设计](#3. 需求分析与设计)
-
- [3.1 需求分析:从用户场景出发](#3.1 需求分析:从用户场景出发)
- [3.2 架构设计:模块化与可扩展性](#3.2 架构设计:模块化与可扩展性)
- [3.3 接口设计:参数与返回值](#3.3 接口设计:参数与返回值)
- [4. 脚本编写规范](#4. 脚本编写规范)
-
- [4.1 脚本模板与结构](#4.1 脚本模板与结构)
- [4.2 编码规范](#4.2 编码规范)
- [5. 参数解析与验证](#5. 参数解析与验证)
-
- [5.1 命令行参数解析](#5.1 命令行参数解析)
- [5.2 参数验证策略](#5.2 参数验证策略)
- [6. 错误处理机制](#6. 错误处理机制)
-
- [6.1 错误分类与处理策略](#6.1 错误分类与处理策略)
- [6.2 异常处理实现](#6.2 异常处理实现)
- [6.3 重试机制](#6.3 重试机制)
- [7. 日志记录最佳实践](#7. 日志记录最佳实践)
-
- [7.1 日志级别与使用场景](#7.1 日志级别与使用场景)
- [7.2 日志配置](#7.2 日志配置)
- [7.3 结构化日志](#7.3 结构化日志)
- [8. 实战案例:天气查询技能](#8. 实战案例:天气查询技能)
- [9. 技能开发最佳实践总结](#9. 技能开发最佳实践总结)
-
- [9.1 开发流程清单](#9.1 开发流程清单)
- [9.2 常见问题与解决方案](#9.2 常见问题与解决方案)
- [10. 总结](#10. 总结)
- 参考资料
摘要
技能系统是 OpenClaw 框架最具魅力的特性之一,它让 AI Agent 的能力边界变得无限可扩展。通过编写自定义技能,开发者可以为 AI 注入专业领域知识、对接外部 API、实现复杂业务逻辑,打造真正懂业务的智能助手。本文将从技能开发的全流程视角出发,系统讲解需求分析、架构设计、脚本编写、参数验证、错误处理、日志记录等核心环节,并以一个完整的天气查询技能为实战案例,手把手带你完成从零到一的技能开发之旅。无论你是 OpenClaw 的初学者还是希望深入定制能力的进阶开发者,本文都将为你提供详实的实践指南。
1. 引言:技能系统------AI Agent 的能力放大器
在 AI Agent 的技术演进中,"技能"(Skill)是一个革命性的概念。它解决了通用大模型与特定领域需求之间的鸿沟问题:大模型拥有强大的语言理解和推理能力,但缺乏专业领域的知识和工具;技能系统则为模型提供了"专业工具箱",让 AI 能够胜任各种垂直场景的任务。
OpenClaw 的技能系统设计理念可以概括为"简单、灵活、可组合 "。简单------一个技能只需要一个 SKILL.md 文件和一个脚本目录;灵活------支持 Python、Shell 等多种脚本语言,可以对接任何外部 API;可组合------多个技能可以协同工作,构建复杂的工作流。
本文将带你深入了解 OpenClaw 技能开发的方方面面。我们将从技能系统的整体架构讲起,分析技能的生命周期和调用机制;然后详细讲解技能开发的各个关键环节,包括需求分析、设计模式、脚本规范、参数处理、错误处理、日志记录等;最后,我们将通过一个完整的天气查询技能案例,将所有知识点串联起来,让你真正掌握技能开发的实战技能。
2. OpenClaw 技能系统概述
2.1 技能系统的设计哲学
OpenClaw 的技能系统遵循"约定优于配置"的设计哲学。开发者只需要遵循简单的目录结构和文件规范,就可以创建功能完备的技能,无需繁琐的配置文件或注册流程。这种设计大大降低了技能开发的门槛,让开发者能够专注于业务逻辑本身。
技能系统的核心设计原则包括:
声明式定义 :技能的功能、触发条件、参数规范都通过 SKILL.md 文件声明式地定义,AI 模型通过阅读这些声明来理解技能的用途和使用方法。
脚本驱动:技能的实际执行由脚本完成,支持 Python、Shell、Node.js 等多种语言。脚本接收结构化参数,输出结构化结果,与 AI 模型解耦。
自描述性:技能的描述信息不仅是给开发者看的,更是给 AI 模型看的。好的技能描述能够让 AI 准确判断何时使用该技能、如何传递参数。
可扩展性:技能系统支持技能之间的组合和依赖,复杂功能可以通过组合简单技能来实现。
2.2 技能系统架构
OpenClaw 的技能系统采用分层架构设计,从 AI 决策层到脚本执行层,每一层都有明确的职责边界。这种设计既保证了灵活性,又确保了安全性和可维护性。
图:OpenClaw 技能系统分层架构,展示了从 AI Agent 到具体技能的调用链路
从架构图可以看出,OpenClaw 的技能系统分为四个核心层次:AI Agent 决策层负责理解用户意图并选择合适的技能;Skill Registry 层维护技能元数据和权限验证;Skill Execution 层负责实际的脚本执行;External Resources 层则是技能访问的外部资源。
2.3 技能目录结构
一个标准的 OpenClaw 技能目录结构如下:
my-skill/
├── SKILL.md # 技能定义文件(必需)
├── scripts/ # 脚本目录
│ ├── main.py # 主脚本
│ └── utils.py # 辅助模块
├── tests/ # 测试目录(可选)
│ └── test_main.py
└── README.md # 说明文档(可选)
其中,SKILL.md 是技能的核心定义文件,包含技能的名称、描述、参数说明、使用示例等关键信息。scripts/ 目录存放实际执行的脚本文件。这种结构简单清晰,便于开发者快速上手。
2.4 技能生命周期
技能从创建到执行,经历以下生命周期阶段:
图:OpenClaw 技能开发的五个核心阶段,从创建到部署的完整流程
匹配
不匹配
创建技能目录
编写 SKILL.md
开发脚本
本地测试
部署到 OpenClaw
AI 发现技能
用户请求触发
AI 调用技能
其他处理
脚本执行
返回结果
AI 整合响应
理解技能生命周期对于开发高质量技能至关重要。在创建阶段,开发者需要设计合理的目录结构和文件组织;在定义阶段,需要编写清晰准确的 SKILL.md;在开发阶段,需要实现健壮的脚本逻辑;在执行阶段,需要处理各种边界情况和错误。
2.5 技能发现与调用机制
OpenClaw 的技能发现机制基于目录扫描。系统启动时,会扫描 skills/ 目录下的所有子目录,识别其中的 SKILL.md 文件,并将技能信息加载到内存中。AI 模型在处理用户请求时,会根据技能的描述信息判断是否需要调用某个技能。
Script Executor Skill Registry AI Agent 用户 Script Executor Skill Registry AI Agent 用户 "帮我查一下北京今天的天气" 分析意图 查询匹配的技能 返回 weather 技能信息 生成调用参数 执行 weather 技能 运行 Python 脚本 返回天气数据 整合数据生成回复 "北京今天晴,气温 15-25°C..."
这个时序图展示了技能调用的完整流程。关键点在于:AI 模型负责理解用户意图并选择合适的技能,技能脚本负责执行具体操作并返回结果,AI 模型再根据结果生成自然语言回复。这种分工让每个组件都专注于自己擅长的领域。
3. 需求分析与设计
3.1 需求分析:从用户场景出发
技能开发的第一步是需求分析。好的需求分析能够帮助我们明确技能的目标用户、使用场景、功能边界和性能要求。在分析需求时,建议从以下几个维度进行思考:
用户画像:谁会使用这个技能?他们的技术水平如何?他们期望什么样的交互方式?
使用场景:用户在什么情况下会触发这个技能?是主动调用还是被动触发?使用频率如何?
功能边界:技能应该做什么?不应该做什么?哪些功能是核心的,哪些是可选的?
数据来源:技能需要访问哪些数据源?是本地文件、外部 API 还是数据库?
错误处理:可能出现哪些异常情况?如何向用户反馈错误信息?
以天气查询技能为例,我们可以进行如下需求分析:
| 维度 | 分析结果 |
|---|---|
| 用户画像 | 普通用户,技术水平不限,期望自然语言交互 |
| 使用场景 | 查询当前天气、未来预报、空气质量等 |
| 功能边界 | 仅支持天气查询,不支持天气预警推送 |
| 数据来源 | 第三方天气 API(如 wttr.in、Open-Meteo) |
| 错误处理 | 城市 not found、API 超时、网络异常等 |
3.2 架构设计:模块化与可扩展性
完成需求分析后,下一步是进行架构设计。好的架构设计应该遵循以下原则:
单一职责:每个模块只做一件事,降低耦合度。
开放封闭:对扩展开放,对修改封闭。新功能通过扩展实现,而不是修改现有代码。
依赖倒置:高层模块不依赖低层模块,两者都依赖抽象接口。
对于技能开发,我们推荐采用分层架构:
基础层 (utils.py)
业务层 (main.py)
表现层 (SKILL.md)
技能描述
参数定义
使用示例
参数解析
业务逻辑
结果封装
API 客户端
日志工具
错误处理
这种分层架构将技能的不同关注点分离到不同层次,使得代码更加清晰、易于维护。表现层负责与 AI 模型的交互,业务层负责核心逻辑处理,基础层负责通用的基础设施功能。
3.3 接口设计:参数与返回值
技能的接口设计直接影响 AI 模型的使用体验。好的接口设计应该遵循以下原则:
参数最小化:只要求用户提供必要的参数,可选参数提供合理的默认值。
语义清晰:参数名称和描述要清晰明确,避免歧义。
类型明确:明确参数的类型(字符串、数字、布尔值等),便于 AI 模型正确传参。
返回结构化:返回结构化的数据,便于 AI 模型理解和处理。
以下是天气查询技能的接口设计示例:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
city |
string | ✅ | - | 城市名称,支持中文和英文 |
days |
number | ❌ | 1 | 预报天数,范围 1-7 |
units |
string | ❌ | metric | 温度单位:metric(摄氏)或 imperial(华氏) |
返回值设计:
json
{
"code": 0,
"message": "success",
"data": {
"city": "北京",
"current": {
"temp": 22,
"condition": "晴",
"humidity": 45,
"wind": "东北风 3级"
},
"forecast": [
{"date": "2026-03-20", "high": 25, "low": 15, "condition": "多云"}
]
}
}
4. 脚本编写规范
4.1 脚本模板与结构
一个规范的技能脚本应该包含以下结构:
python
#!/usr/bin/env python3
"""技能名称 - 简短描述
详细描述技能的功能、使用场景和注意事项。
Dependencies:
pip install requests # 如果有外部依赖
Environment:
API_KEY: API 密钥(如果需要)
"""
import argparse
import json
import logging
import sys
from typing import Optional
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description='技能描述')
parser.add_argument('--param1', required=True, help='参数1说明')
parser.add_argument('--param2', default='default', help='参数2说明')
return parser.parse_args()
def main():
"""主函数"""
args = parse_args()
try:
# 业务逻辑
result = do_something(args.param1, args.param2)
# 输出结果
print(json.dumps({
"code": 0,
"message": "success",
"data": result
}, ensure_ascii=False))
except Exception as e:
logger.exception("执行失败")
print(json.dumps({
"code": 1,
"message": str(e),
"data": None
}))
sys.exit(1)
if __name__ == "__main__":
main()
这个模板包含了技能脚本的各个关键组成部分:文档字符串、导入声明、日志配置、参数解析、主函数和错误处理。遵循这个模板可以确保脚本的一致性和可维护性。
4.2 编码规范
技能脚本应该遵循 Python 社区的编码规范(PEP 8),主要包括:
命名规范:
- 函数和变量使用 snake_case:
get_weather_data - 类名使用 PascalCase:
WeatherClient - 常量使用全大写:
MAX_RETRY_COUNT
文档规范:
- 模块顶部添加文档字符串,说明模块用途
- 函数添加文档字符串,说明参数、返回值和异常
- 复杂逻辑添加行内注释
代码风格:
- 每行不超过 100 字符
- 使用 4 空格缩进
- 函数之间空两行
- 导入按标准库、第三方库、本地模块分组
5. 参数解析与验证
5.1 命令行参数解析
Python 的 argparse 模块是处理命令行参数的标准工具。它支持必填参数、可选参数、默认值、类型转换等功能。
python
import argparse
def parse_args():
"""解析命令行参数
Returns:
argparse.Namespace: 解析后的参数对象
"""
parser = argparse.ArgumentParser(
description='天气查询技能 - 获取指定城市的天气信息'
)
# 必填参数
parser.add_argument(
'--city',
required=True,
type=str,
help='城市名称,支持中文(如"北京")或英文(如"Beijing")'
)
# 可选参数 - 整数类型
parser.add_argument(
'--days',
type=int,
default=1,
choices=range(1, 8),
help='预报天数,范围 1-7,默认为 1'
)
# 可选参数 - 枚举类型
parser.add_argument(
'--units',
type=str,
default='metric',
choices=['metric', 'imperial'],
help='温度单位:metric(摄氏度)或 imperial(华氏度)'
)
# 开关参数
parser.add_argument(
'--verbose',
action='store_true',
help='输出详细日志'
)
return parser.parse_args()
# 使用示例
if __name__ == "__main__":
args = parse_args()
print(f"城市: {args.city}")
print(f"天数: {args.days}")
print(f"单位: {args.units}")
print(f"详细模式: {args.verbose}")
上述代码展示了 argparse 的常用功能。required=True 指定必填参数,default 设置默认值,choices 限制可选值范围,action='store_true' 创建开关参数。type=int 自动将输入转换为整数类型。
5.2 参数验证策略
参数验证是保证技能健壮性的重要环节。我们建议采用"尽早验证,明确反馈"的策略,在脚本入口处完成所有参数验证。
python
import re
from typing import Optional
def validate_city(city: str) -> str:
"""验证城市名称
Args:
city: 用户输入的城市名称
Returns:
验证通过的城市名称(标准化后)
Raises:
ValueError: 城市名称无效
"""
if not city or not city.strip():
raise ValueError("城市名称不能为空")
# 去除首尾空格
city = city.strip()
# 长度限制
if len(city) > 50:
raise ValueError("城市名称过长,请输入有效的城市名")
# 字符验证(支持中英文、空格、连字符)
pattern = r'^[\u4e00-\u9fa5a-zA-Z\s\-]+$'
if not re.match(pattern, city):
raise ValueError(
f"城市名称包含无效字符: {city}。"
"请使用中文或英文名称,如'北京'或'Beijing'"
)
return city
def validate_days(days: int) -> int:
"""验证预报天数
Args:
days: 用户输入的天数
Returns:
验证通过的天数
Raises:
ValueError: 天数无效
"""
if not isinstance(days, int):
raise ValueError(f"天数必须是整数,当前类型: {type(days)}")
if days < 1 or days > 7:
raise ValueError(
f"预报天数必须在 1-7 之间,当前值: {days}"
)
return days
def validate_all_params(city: str, days: int, units: str) -> dict:
"""验证所有参数
Args:
city: 城市名称
days: 预报天数
units: 温度单位
Returns:
验证通过的参数字典
Raises:
ValueError: 任一参数无效
"""
return {
'city': validate_city(city),
'days': validate_days(days),
'units': units if units in ['metric', 'imperial'] else 'metric'
}
这段代码展示了参数验证的实现方式。每个验证函数专注于一个参数,验证失败时抛出 ValueError 并提供明确的错误信息。validate_all_params 函数整合所有验证逻辑,作为参数验证的统一入口。
6. 错误处理机制
6.1 错误分类与处理策略
技能执行过程中可能遇到各种错误,合理分类和处理这些错误对于提升用户体验至关重要。我们将错误分为以下几类:
| 错误类型 | 示例 | 处理策略 |
|---|---|---|
| 参数错误 | 城市名称为空、天数超范围 | 立即返回错误,提示正确格式 |
| 网络错误 | API 超时、连接失败 | 重试机制,提示用户稍后重试 |
| 业务错误 | 城市 not found、无数据 | 返回友好提示,建议替代方案 |
| 系统错误 | 内存不足、权限问题 | 记录日志,返回通用错误信息 |
6.2 异常处理实现
python
import json
import logging
import sys
from enum import Enum
from typing import Optional, Any
logger = logging.getLogger(__name__)
class ErrorCode(Enum):
"""错误码枚举"""
SUCCESS = 0
INVALID_PARAM = 1001
CITY_NOT_FOUND = 1002
API_ERROR = 2001
NETWORK_ERROR = 2002
INTERNAL_ERROR = 9999
class SkillError(Exception):
"""技能执行错误基类"""
def __init__(
self,
code: ErrorCode,
message: str,
detail: Optional[str] = None
):
self.code = code
self.message = message
self.detail = detail
super().__init__(message)
def to_dict(self) -> dict:
"""转换为字典格式"""
result = {
"code": self.code.value,
"message": self.message
}
if self.detail:
result["detail"] = self.detail
return result
class ParameterError(SkillError):
"""参数错误"""
def __init__(self, message: str, detail: Optional[str] = None):
super().__init__(ErrorCode.INVALID_PARAM, message, detail)
class CityNotFoundError(SkillError):
"""城市未找到错误"""
def __init__(self, city: str):
super().__init__(
ErrorCode.CITY_NOT_FOUND,
f"未找到城市: {city}",
f"请检查城市名称是否正确,或尝试使用英文名称"
)
class APIError(SkillError):
"""API 调用错误"""
def __init__(self, message: str, status_code: Optional[int] = None):
detail = f"HTTP 状态码: {status_code}" if status_code else None
super().__init__(ErrorCode.API_ERROR, message, detail)
def output_result(
code: int = 0,
message: str = "success",
data: Any = None
):
"""输出结构化结果
Args:
code: 状态码,0 表示成功
message: 状态消息
data: 返回数据
"""
result = {
"code": code,
"message": message,
"data": data
}
print(json.dumps(result, ensure_ascii=False))
def handle_error(error: Exception):
"""统一错误处理
Args:
error: 异常对象
"""
if isinstance(error, SkillError):
logger.warning(f"技能错误: {error.message}", extra={"detail": error.detail})
output_result(
code=error.code.value,
message=error.message,
data={"detail": error.detail} if error.detail else None
)
else:
logger.exception("未预期的错误")
output_result(
code=ErrorCode.INTERNAL_ERROR.value,
message="内部错误,请稍后重试",
data=None
)
sys.exit(1)
这段代码定义了完整的错误处理体系。ErrorCode 枚举定义了所有可能的错误码,SkillError 及其子类定义了各类错误,handle_error 函数提供统一的错误处理入口。这种设计使得错误处理更加规范和可维护。
6.3 重试机制
对于网络请求等可能临时失败的操作,实现重试机制可以显著提高技能的可靠性。
python
import time
import functools
from typing import Callable, Type, Tuple
logger = logging.getLogger(__name__)
def retry(
max_attempts: int = 3,
delay: float = 1.0,
backoff: float = 2.0,
exceptions: Tuple[Type[Exception], ...] = (Exception,)
):
"""重试装饰器
Args:
max_attempts: 最大尝试次数
delay: 初始延迟(秒)
backoff: 延迟增长因子
exceptions: 触发重试的异常类型
Returns:
装饰后的函数
"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
current_delay = delay
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_attempts:
logger.warning(
f"第 {attempt} 次尝试失败: {e},"
f"{current_delay:.1f}秒后重试"
)
time.sleep(current_delay)
current_delay *= backoff
else:
logger.error(
f"已达到最大重试次数 {max_attempts},"
f"最后错误: {e}"
)
raise last_exception
return wrapper
return decorator
# 使用示例
@retry(max_attempts=3, delay=1.0, exceptions=(APIError,))
def fetch_weather_data(city: str) -> dict:
"""获取天气数据(带重试)"""
# API 调用逻辑
pass
这个重试装饰器支持配置最大尝试次数、初始延迟、延迟增长因子和触发重试的异常类型。使用指数退避策略(exponential backoff)避免对 API 造成过大压力。
7. 日志记录最佳实践
7.1 日志级别与使用场景
合理的日志记录是技能可维护性的重要保障。Python 的 logging 模块提供了五个标准日志级别:
| 级别 | 数值 | 使用场景 |
|---|---|---|
| DEBUG | 10 | 详细调试信息,仅开发环境使用 |
| INFO | 20 | 关键流程信息,如技能启动、参数解析完成 |
| WARNING | 30 | 警告信息,如使用了默认值、API 响应较慢 |
| ERROR | 40 | 错误信息,如 API 调用失败、数据解析错误 |
| CRITICAL | 50 | 严重错误,如无法恢复的系统错误 |
7.2 日志配置
python
import logging
import sys
from datetime import datetime
from pathlib import Path
def setup_logging(
name: str,
level: int = logging.INFO,
log_file: str = None
) -> logging.Logger:
"""配置日志系统
Args:
name: 日志记录器名称
level: 日志级别
log_file: 日志文件路径(可选)
Returns:
配置好的 Logger 对象
"""
logger = logging.getLogger(name)
logger.setLevel(level)
# 日志格式
formatter = logging.Formatter(
fmt='%(asctime)s | %(name)s | %(levelname)s | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 控制台处理器
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(level)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# 文件处理器(可选)
if log_file:
log_path = Path(log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(level)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
# 初始化日志
logger = setup_logging(
name='weather_skill',
level=logging.INFO,
log_file='/var/log/openclaw/skills/weather.log'
)
7.3 结构化日志
对于复杂技能,建议使用结构化日志(JSON 格式),便于日志分析和监控。
python
import json
import logging
from datetime import datetime
class StructuredFormatter(logging.Formatter):
"""结构化日志格式化器"""
def format(self, record: logging.LogRecord) -> str:
log_data = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"line": record.lineno
}
# 添加额外字段
if hasattr(record, 'extra_data'):
log_data["data"] = record.extra_data
# 添加异常信息
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
return json.dumps(log_data, ensure_ascii=False)
def log_with_data(
logger: logging.Logger,
level: int,
message: str,
**kwargs
):
"""带额外数据的日志记录
Args:
logger: 日志记录器
level: 日志级别
message: 日志消息
**kwargs: 额外数据
"""
extra = {'extra_data': kwargs} if kwargs else {}
logger.log(level, message, extra=extra)
# 使用示例
log_with_data(
logger,
logging.INFO,
"天气查询成功",
city="北京",
response_time_ms=150,
api_provider="wttr.in"
)
# 输出: {"timestamp": "2026-03-19T15:30:00Z", "level": "INFO", ...}
8. 实战案例:天气查询技能
8.1 需求分析
我们将开发一个完整的天气查询技能,功能需求如下:
核心功能:
- 查询指定城市的当前天气
- 支持未来 1-7 天的天气预报
- 支持摄氏度和华氏度切换
数据来源:
- 使用 wttr.in 免费 API,无需 API Key
- 备选方案:Open-Meteo API
用户体验:
- 支持中英文城市名
- 友好的错误提示
- 结构化的返回数据
8.2 SKILL.md 编写
markdown
---
name: weather
description: "Get current weather and forecasts via wttr.in or Open-Meteo. Use when:
user asks about weather, temperature, or forecasts for any location. NOT for:
historical weather data, severe weather alerts, or detailed meteorological analysis.
No API key needed."
---
# Weather Skill
Get current weather and forecasts for any location worldwide.
## When to Use
✅ **USE this skill when:**
- User asks about current weather in a city
- User wants weather forecast for upcoming days
- User needs temperature, humidity, or wind information
❌ **DON'T use this skill when:**
- User asks about historical weather data
- User needs severe weather alerts
- User wants detailed meteorological analysis
## Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| city | string | Yes | - | City name (Chinese or English) |
| days | number | No | 1 | Forecast days (1-7) |
| units | string | No | metric | Temperature unit: metric/imperial |
## Commands
### Basic usage
\`\`\`bash
python3 scripts/weather.py --city "北京"
\`\`\`
### With forecast
\`\`\`bash
python3 scripts/weather.py --city "Beijing" --days 3
\`\`\`
### Fahrenheit units
\`\`\`bash
python3 scripts/weather.py --city "New York" --units imperial
\`\`\`
## Response
\`\`\`json
{
"code": 0,
"message": "success",
"data": {
"city": "北京",
"current": {
"temp": 22,
"condition": "晴",
"humidity": 45,
"wind": "东北风 3级"
},
"forecast": [...]
}
}
\`\`\`
## Notes
- No API key required
- Supports Chinese and English city names
- Rate limit: ~1000 requests/day
8.3 完整脚本实现
python
#!/usr/bin/env python3
"""Weather Skill - Get current weather and forecasts
A skill for querying weather information using wttr.in API.
Supports Chinese and English city names, temperature unit conversion,
and multi-day forecasts.
Dependencies:
pip install requests
Author: OpenClaw Team
"""
import argparse
import json
import logging
import re
import sys
from dataclasses import dataclass
from typing import Optional, List
from enum import Enum
import requests
# 日志配置
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s | %(levelname)s | %(message)s'
)
logger = logging.getLogger(__name__)
class ErrorCode(Enum):
"""错误码枚举"""
SUCCESS = 0
INVALID_PARAM = 1001
CITY_NOT_FOUND = 1002
API_ERROR = 2001
NETWORK_ERROR = 2002
@dataclass
class CurrentWeather:
"""当前天气数据"""
temp: int
feels_like: int
condition: str
humidity: int
wind_speed: int
wind_direction: str
visibility: int
@dataclass
class ForecastDay:
"""预报数据"""
date: str
max_temp: int
min_temp: int
condition: str
precipitation: float
@dataclass
class WeatherResult:
"""天气查询结果"""
city: str
current: CurrentWeather
forecast: List[ForecastDay]
def validate_city(city: str) -> str:
"""验证城市名称"""
if not city or not city.strip():
raise ValueError("城市名称不能为空")
city = city.strip()
if len(city) > 50:
raise ValueError("城市名称过长")
pattern = r'^[\u4e00-\u9fa5a-zA-Z\s\-]+$'
if not re.match(pattern, city):
raise ValueError(f"城市名称包含无效字符: {city}")
return city
def validate_days(days: int) -> int:
"""验证预报天数"""
if not isinstance(days, int) or days < 1 or days > 7:
raise ValueError("预报天数必须在 1-7 之间")
return days
def fetch_weather(city: str, days: int = 1) -> dict:
"""从 wttr.in 获取天气数据"""
url = f"https://wttr.in/{city}"
params = {
"format": "j1",
"lang": "zh"
}
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
return response.json()
except requests.Timeout:
raise RuntimeError("API 请求超时,请稍后重试")
except requests.HTTPError as e:
if e.response.status_code == 404:
raise ValueError(f"未找到城市: {city}")
raise RuntimeError(f"API 错误: {e}")
except requests.RequestException as e:
raise RuntimeError(f"网络错误: {e}")
def parse_weather_data(data: dict, units: str = 'metric') -> WeatherResult:
"""解析天气数据"""
current = data.get('current_condition', [{}])[0]
forecast_list = data.get('weather', [])
# 解析当前天气
temp = int(current.get('temp_C', 0))
feels_like = int(current.get('FeelsLikeC', 0))
if units == 'imperial':
temp = int(temp * 9/5 + 32)
feels_like = int(feels_like * 9/5 + 32)
current_weather = CurrentWeather(
temp=temp,
feels_like=feels_like,
condition=current.get('lang_zh', [{}])[0].get('value', current.get('weatherDesc', [{}])[0].get('value', '未知')),
humidity=int(current.get('humidity', 0)),
wind_speed=int(current.get('windspeedKmph', 0)),
wind_direction=current.get('winddir16Point', 'N'),
visibility=int(current.get('visibility', 0))
)
# 解析预报
forecast = []
for day_data in forecast_list[:7]:
max_temp = int(day_data.get('maxtempC', 0))
min_temp = int(day_data.get('mintempC', 0))
if units == 'imperial':
max_temp = int(max_temp * 9/5 + 32)
min_temp = int(min_temp * 9/5 + 32)
hourly = day_data.get('hourly', [{}])[0]
forecast.append(ForecastDay(
date=day_data.get('date', ''),
max_temp=max_temp,
min_temp=min_temp,
condition=hourly.get('lang_zh', [{}])[0].get('value', '未知'),
precipitation=float(hourly.get('precipMM', 0))
))
return WeatherResult(
city=data.get('nearest_area', [{}])[0].get('areaName', [{}])[0].get('value', city),
current=current_weather,
forecast=forecast
)
def output_result(code: int, message: str, data: Optional[dict] = None):
"""输出结构化结果"""
result = {"code": code, "message": message, "data": data}
print(json.dumps(result, ensure_ascii=False, default=str))
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='天气查询技能')
parser.add_argument('--city', required=True, help='城市名称')
parser.add_argument('--days', type=int, default=1, help='预报天数 (1-7)')
parser.add_argument('--units', default='metric', choices=['metric', 'imperial'], help='温度单位')
args = parser.parse_args()
try:
# 参数验证
city = validate_city(args.city)
days = validate_days(args.days)
logger.info(f"查询天气: city={city}, days={days}, units={args.units}")
# 获取数据
raw_data = fetch_weather(city, days)
# 解析数据
result = parse_weather_data(raw_data, args.units)
# 输出结果
output_result(
code=0,
message="success",
data={
"city": result.city,
"current": {
"temp": result.current.temp,
"feels_like": result.current.feels_like,
"condition": result.current.condition,
"humidity": result.current.humidity,
"wind": f"{result.current.wind_direction} {result.current.wind_speed}km/h"
},
"forecast": [
{
"date": f.date,
"high": f.max_temp,
"low": f.min_temp,
"condition": f.condition
}
for f in result.forecast[:days]
]
}
)
logger.info(f"查询成功: {result.city}")
except ValueError as e:
logger.warning(f"参数错误: {e}")
output_result(ErrorCode.INVALID_PARAM.value, str(e))
sys.exit(1)
except RuntimeError as e:
logger.error(f"运行时错误: {e}")
output_result(ErrorCode.API_ERROR.value, str(e))
sys.exit(1)
except Exception as e:
logger.exception("未预期的错误")
output_result(ErrorCode.NETWORK_ERROR.value, "网络错误,请稍后重试")
sys.exit(1)
if __name__ == "__main__":
main()
上述代码实现了一个完整的天气查询技能脚本。代码结构清晰,包含参数验证、API 调用、数据解析、错误处理等完整功能。使用 dataclass 定义数据结构,使代码更加类型安全和可读。错误处理覆盖了参数错误、API 错误、网络错误等各种情况,并提供了友好的错误提示。
8.4 测试验证
完成脚本后,需要进行充分的测试验证:
bash
# 测试基本功能
python3 scripts/weather.py --city "北京"
# 测试英文城市
python3 scripts/weather.py --city "London" --days 3
# 测试华氏度
python3 scripts/weather.py --city "New York" --units imperial
# 测试错误处理
python3 scripts/weather.py --city ""
python3 scripts/weather.py --city "北京" --days 10
9. 技能开发最佳实践总结
9.1 开发流程清单
| 阶段 | 关键任务 | 交付物 |
|---|---|---|
| 需求分析 | 用户画像、使用场景、功能边界 | 需求文档 |
| 架构设计 | 模块划分、接口定义、数据流 | 架构图 |
| SKILL.md | 描述、参数、示例、响应格式 | SKILL.md |
| 脚本开发 | 参数解析、业务逻辑、错误处理 | Python 脚本 |
| 测试验证 | 单元测试、集成测试、边界测试 | 测试报告 |
| 部署上线 | 目录部署、权限配置、日志配置 | 部署文档 |
9.2 常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| AI 不调用技能 | 描述不够清晰 | 优化 description,添加触发词 |
| 参数传递错误 | 类型不匹配 | 检查 SKILL.md 参数定义 |
| 脚本执行失败 | 依赖未安装 | 添加依赖说明,使用虚拟环境 |
| 返回结果混乱 | 输出格式不规范 | 统一使用 JSON 输出 |
| 日志丢失 | 日志配置错误 | 检查日志路径和权限 |
10. 总结
本文从技能系统的设计哲学出发,系统讲解了 OpenClaw 技能开发的完整流程。核心要点如下:
需求分析是基础:好的技能始于清晰的需求分析。理解用户画像、使用场景和功能边界,才能设计出真正有用的技能。
架构设计是骨架:分层架构将技能的不同关注点分离,使代码更加清晰、易于维护。表现层、业务层、基础层各司其职,协同工作。
脚本规范是保障:遵循编码规范、参数验证、错误处理、日志记录等最佳实践,能够显著提升技能的健壮性和可维护性。
实战出真知:通过天气查询技能的完整实现,我们将理论知识转化为实践能力。从 SKILL.md 编写到脚本实现,从参数验证到错误处理,覆盖了技能开发的各个环节。
技能系统是 OpenClaw 最具魅力的特性之一,它让 AI Agent 的能力边界变得无限可扩展。希望本文能够帮助你开启技能开发之旅,为 AI 注入更多专业能力,打造真正懂业务的智能助手。
思考题:
- 在你的业务场景中,有哪些功能适合封装为 OpenClaw 技能?
- 如何设计技能的描述信息,让 AI 能够准确判断何时使用该技能?
- 如果要开发一个需要用户授权的技能(如访问私有 API),你会如何设计授权流程?