目录
技能系统是 OpenClaw 框架最具魅力的特性之一,它让 AI Agent 的能力边界变得无限可扩展。通过编写自定义技能,开发者可以为 AI 注入专业领域知识、对接外部 API、实现复杂业务逻辑,打造真正懂业务的智能助手。本文将从技能开发的全流程视角出发,系统讲解需求分析、架构设计、脚本编写、参数验证、错误处理、日志记录等核心环节,并以一个完整的天气查询技能为实战案例,手把手带你完成从零到一的技能开发之旅。无论你是 OpenClaw 的初学者还是希望深入定制能力的进阶开发者,本文都将为你提供详实的实践指南。
在 AI Agent 的技术演进中,"技能"(Skill)是一个革命性的概念。它解决了通用大模型与特定领域需求之间的鸿沟问题:大模型拥有强大的语言理解和推理能力,但缺乏专业领域的知识和工具;技能系统则为模型提供了"专业工具箱",让 AI 能够胜任各种垂直场景的任务。
OpenClaw 的技能系统设计理念可以概括为"简单、灵活、可组合 "。简单------一个技能只需要一个 SKILL.md 文件和一个脚本目录;灵活------支持 Python、Shell 等多种脚本语言,可以对接任何外部 API;可组合------多个技能可以协同工作,构建复杂的工作流。
本文将带你深入了解 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.1 需求分析:从用户场景出发
技能开发的第一步是需求分析。好的需求分析能够帮助我们明确技能的目标用户、使用场景、功能边界和性能要求。在分析需求时,建议从以下几个维度进行思考:
用户画像:谁会使用这个技能?他们的技术水平如何?他们期望什么样的交互方式?
使用场景:用户在什么情况下会触发这个技能?是主动调用还是被动触发?使用频率如何?
功能边界:技能应该做什么?不应该做什么?哪些功能是核心的,哪些是可选的?
数据来源:技能需要访问哪些数据源?是本地文件、外部 API 还是数据库?
错误处理:可能出现哪些异常情况?如何向用户反馈错误信息?
以天气查询技能为例,我们可以进行如下需求分析:
3.2 架构设计:模块化与可扩展性
完成需求分析后,下一步是进行架构设计。好的架构设计应该遵循以下原则:
单一职责:每个模块只做一件事,降低耦合度。
开放封闭:对扩展开放,对修改封闭。新功能通过扩展实现,而不是修改现有代码。
依赖倒置:高层模块不依赖低层模块,两者都依赖抽象接口。
这种分层架构将技能的不同关注点分离到不同层次,使得代码更加清晰、易于维护。表现层负责与 AI 模型的交互,业务层负责核心逻辑处理,基础层负责通用的基础设施功能。
3.3 接口设计:参数与返回值
技能的接口设计直接影响 AI 模型的使用体验。好的接口设计应该遵循以下原则:
参数最小化:只要求用户提供必要的参数,可选参数提供合理的默认值。
语义清晰:参数名称和描述要清晰明确,避免歧义。
类型明确:明确参数的类型(字符串、数字、布尔值等),便于 AI 模型正确传参。
返回结构化:返回结构化的数据,便于 AI 模型理解和处理。
以下是天气查询技能的接口设计示例:
city string ✅ - 城市名称,支持中文和英文
days number ❌ 1 预报天数,范围 1-7
units string ❌ metric 温度单位:metric(摄氏)或 imperial(华氏)
返回值设计:
{ "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.1 脚本模板与结构
一个规范的技能脚本应该包含以下结构:
#!/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.1 命令行参数解析
Python 的 argparse 模块是处理命令行参数的标准工具。它支持必填参数、可选参数、默认值、类型转换等功能。
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 参数验证策略
参数验证是保证技能健壮性的重要环节。我们建议采用"尽早验证,明确反馈"的策略,在脚本入口处完成所有参数验证。
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'^[一-龥a-zA-Zs-]+$' 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
这段代码展示了参数验证的实现方式。每个验证函数专注于一个参数,验证失败时抛出 ValueError 并提供明确的错误信息。validate_all_params 函数整合所有验证逻辑,作为参数验证的统一入口。
6.1 错误分类与处理策略
技能执行过程中可能遇到各种错误,合理分类和处理这些错误对于提升用户体验至关重要。我们将错误分为以下几类:
6.2 异常处理实现
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 重试机制
对于网络请求等可能临时失败的操作,实现重试机制可以显著提高技能的可靠性。
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.1 日志级别与使用场景
合理的日志记录是技能可维护性的重要保障。Python 的 logging 模块提供了五个标准日志级别:
7.2 日志配置
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 格式),便于日志分析和监控。
import json
import logging from datetime import datetime
class StructuredFormatter(logging.Formatter):
"""结构化日志格式化器""" def format(self, record: logging.LogRecord) -> str: log_data = # 添加额外字段 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.1 需求分析
我们将开发一个完整的天气查询技能,功能需求如下:
核心功能:
- 查询指定城市的当前天气
- 支持未来 1-7 天的天气预报
- 支持摄氏度和华氏度切换
数据来源:
- 使用 wttr.in 免费 API,无需 API Key
- 备选方案:Open-Meteo API
用户体验:
- 支持中英文城市名
- 友好的错误提示
- 结构化的返回数据
8.2 SKILL.md 编写
---
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
python3 scripts/weather.py --city "北京"
With forecast
python3 scripts/weather.py --city "Beijing" --days 3
Fahrenheit units
python3 scripts/weather.py --city "New York" --units imperial
Response
{ "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 完整脚本实现
#!/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'^[一-龥a-zA-Zs-]+$' 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 测试验证
完成脚本后,需要进行充分的测试验证:
# 测试基本功能
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.1 开发流程清单
9.2 常见问题与解决方案
本文从技能系统的设计哲学出发,系统讲解了 OpenClaw 技能开发的完整流程。核心要点如下:
需求分析是基础:好的技能始于清晰的需求分析。理解用户画像、使用场景和功能边界,才能设计出真正有用的技能。
架构设计是骨架:分层架构将技能的不同关注点分离,使代码更加清晰、易于维护。表现层、业务层、基础层各司其职,协同工作。
脚本规范是保障:遵循编码规范、参数验证、错误处理、日志记录等**实践,能够显著提升技能的健壮性和可维护性。
实战出真知:通过天气查询技能的完整实现,我们将理论知识转化为实践能力。从 SKILL.md 编写到脚本实现,从参数验证到错误处理,覆盖了技能开发的各个环节。
技能系统是 OpenClaw 最具魅力的特性之一,它让 AI Agent 的能力边界变得无限可扩展。希望本文能够帮助你开启技能开发之旅,为 AI 注入更多专业能力,打造真正懂业务的智能助手。
思考题:
- 在你的业务场景中,有哪些功能适合封装为 OpenClaw 技能?
- 如何设计技能的描述信息,让 AI 能够准确判断何时使用该技能?
- 如果要开发一个需要用户授权的技能(如访问私有 API),你会如何设计授权流程?
- OpenClaw 官方文档
- OpenClaw GitHub
- wttr.in API 文档
- Python argparse 官方文档
- Python logging **实践
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/256207.html