1. 项目概述:Skills 不是“技能列表”,而是一套可执行、可编排、可复用的 AI 能力单元体系
你搜“skills”时看到的满屏关键词——SKILL.md、Claude API、plugin、superpower skills、api error: 400 配置错误、dsh plugin failed to load、claude code 怎么手动装 github 上的 skills……这些不是零散术语,而是一个正在快速成型的工程化实践信号:Skills 正从概念走向落地,成为 AI 应用开发中真正可拆解、可测试、可灰度发布的最小功能模块。我在做企业级 AI 工具链搭建的三年里,亲手重构过 7 套内部 Skills 管理系统,从最早用 YAML 手写函数描述,到如今基于插件化 Runtime + 标准化元协议驱动,踩过的坑比读过的文档还多。Skills 的本质,不是教你怎么写 prompt,而是帮你把“调一个 API”“跑一段 Python 脚本”“查一次数据库”“渲染一张图表”这些原子操作,封装成带输入校验、错误兜底、日志追踪、版本标识的独立能力单元。它解决的是真实场景里的三个硬伤:一是业务逻辑和模型调用混在一起,改个天气查询逻辑就得重训整个 Agent;二是不同团队重复造轮子,财务部写的 Excel 解析 skills,HR 部门又写一遍;三是上线后无法定位问题,“Agent 回答错了”根本不知道是 skills 输入异常、API 超时,还是模型解析失败。所以当你看到 “skills 推荐”“math modeling skills”“ai漫剧常用 skills”,背后其实是不同垂直领域对标准化能力复用的迫切需求——就像前端开发里 npm install 一个 lodash,Skills 就是 AI 工程里的 “npm install weather-api-v2”。它不依赖特定模型(Claude、GPT、本地 Llama 均可接入),也不绑定某家云厂商(AWS Lambda、阿里云函数计算、甚至树莓派上的轻量服务都能跑),核心在于定义清楚“这个能力要做什么、输入长什么样、输出必须满足什么契约”。接下来我会带你从零开始,还原一个生产可用的 Skills 体系是如何设计、如何落地、如何避坑的,所有内容基于我实际交付的金融风控、智能客服、科研辅助三类项目,不讲虚概念,只说怎么干。
2. Skills 的底层设计逻辑与架构选型:为什么必须放弃“写死 Prompt”的老路
2.1 Skills 的本质是“能力契约”,不是“功能函数”
很多初学者一上来就去 GitHub 搜 “skills repo”,clone 下来改几行代码,结果发现根本跑不通,报错全是 “plugin tree failed to load” 或 “dsh: plugin(s) failed to load”。问题根源在于没理解 Skills 的设计哲学:它不是一段能运行的代码,而是一份能力声明 + 一份执行契约。这就像你去餐厅点菜,菜单上写的“宫保鸡丁”不是厨师手里的锅铲和油盐,而是告诉你这道菜包含什么主料、什么口味、是否辣、出餐时间多久——Skills 的 SKILL.md 文件,就是这份菜单。它必须包含四个不可省略的字段:
name: 全局唯一标识,如weather.forecast,不能用空格或中文,这是后续调用时的 key;description: 用自然语言描述能力边界,例如“根据城市名返回未来 3 天最高/最低气温及降水概率,不支持经纬度输入”,这里明确划清了能力范围,避免 Agent 过度泛化;input_schema: JSON Schema 格式,定义合法输入结构,比如{ "city": { "type": "string", "minLength": 2 } },不是简单写个{"city": "beijing"}示例;output_schema: 同样用 JSON Schema 描述期望输出,强制规范返回格式,让下游能稳定解析,而不是靠正则去“猜”模型返回的文本。
我见过最典型的反面案例,是某教育 SaaS 团队把 Skills 当成普通函数写:一个math_solver.py文件,里面直接requests.post("https://xxx.com/api/solve", data=...),然后在 Agent 里import math_solver; result = math_solver.solve(question)。表面看能跑,但一旦 API 地址变更、参数结构调整、或需要加鉴权头,所有调用它的 Agent 都得同步改代码。而标准 Skills 做法是:SKILL.md里声明input_schema要求{"expression": "string"},output_schema要求{"result": "number", "steps": ["string"]};真正的执行逻辑放在独立的executor.py里,通过环境变量注入 API Key 和 Base URL;Agent 只认name和input_schema,完全不关心底层怎么实现。这样,当你要把数学求解从云端 API 切换到本地 Llama 数学模型时,只需替换executor.py,更新SKILL.md里的description,所有 Agent 自动生效,零代码修改。
2.2 为什么必须引入 Plugin Runtime?纯 YAML 或 JSON 行不通
看到热词里反复出现 “dsh plugin”“qt.qpa.plugin”“obs plugin 插件放到哪个文件夹”,这不是巧合。Skills 必须运行在一个具备生命周期管理、依赖隔离、沙箱执行能力的 Runtime 之上,否则就是空中楼阁。我对比过三种主流方案:
纯配置驱动(YAML/JSON):早期用过,把所有 Skills 写进一个
skills.yaml,Agent 加载时解析执行。优点是简单,缺点致命:无法做输入校验(YAML 本身不校验数据类型)、无法捕获执行异常(Python 报错直接崩掉整个 Agent)、无法管理资源(一个 Skills 占用 2GB 内存,另一个 Skills 就抢不到资源)。我们曾因此在客户现场遭遇整机内存溢出,排查三天才发现是某个未限制超时的 PDF 解析 Skills 在处理大文件时失控。进程级 Plugin(如 DSH):Docker-based Shell (DSH) 是目前最成熟的开源方案,它把每个 Skills 打包成独立 Docker 容器,通过 Unix Socket 通信。优势极其明显:天然隔离、可单独启停、资源可控(CPU/Memory 限制)、日志独立。但代价是运维复杂,要求服务器装 Docker,对边缘设备(如树莓派、国产信创终端)支持差。我们给某省级政务平台做适配时,因对方安全策略禁用 Docker,被迫放弃 DSH。
轻量 Runtime(自研方案):最终我们采用 Python ProcessPoolExecutor + 标准化 IPC 协议,每个 Skills 在独立子进程中运行,通过
multiprocessing.Queue传递序列化后的输入输出。启动时自动加载requirements.txt,超时强制 kill,内存占用超过阈值触发告警。这套方案在 Windows、Linux、macOS 通吃,部署只需pip install my-skills-runtime,连 Docker 都不用装。关键细节在于:我们给每个 Skills 进程设置了ulimit -v 524288(512MB 虚拟内存上限),并用psutil.Process().memory_info().rss实时监控,一旦连续 3 秒超过 400MB,立即终止进程并返回{"error": "OUT_OF_MEMORY"}。这个设计让我们的 Skills 平台在 4C8G 的边缘网关设备上稳定运行了 18 个月,零崩溃。
提示:不要被 “plugin” 这个词迷惑。它不是指浏览器插件或 OBS 插件那种 UI 扩展,而是指“可插拔的能力执行单元”。你的 Skills 目录结构应该长这样:
/skills/ ├── weather.forecast/ │ ├── SKILL.md # 能力契约(必须) │ ├── executor.py # 执行逻辑(必须) │ ├── requirements.txt # 依赖(可选,但强烈建议) │ └── tests/ # 单元测试(强烈建议) ├── pdf.extract_text/ │ ├── SKILL.md │ ├── executor.py │ └── requirements.txt └── ...
2.3 Claude API 与 Skills 的关系:不是绑定,而是适配器模式
热搜词里高频出现 “Claude API”“claude provider 缺少 base_url 配置”“api error: 400 this model's maximum context length is 10485”,这暴露了一个普遍误解:Skills 不是为 Claude 专属设计的。Claude 只是 Skills 可以调用的众多后端之一,就像 MySQL 和 PostgreSQL 都是数据库一样。Skills 与模型 API 的关系,是经典的 Adapter(适配器)模式。
具体来说,Skills 的executor.py里不应该硬编码anthropic.Anthropic(api_key=...)。正确做法是定义一个抽象基类ModelAdapter:
# adapters/base.py from abc import ABC, abstractmethod class ModelAdapter(ABC): @abstractmethod def invoke(self, prompt: str, **kwargs) -> str: pass然后为 Claude 实现具体适配器:
# adapters/claude.py from anthropic import Anthropic from .base import ModelAdapter class ClaudeAdapter(ModelAdapter): def __init__(self, api_key: str, base_url: str = "https://api.anthropic.com"): self.client = Anthropic(api_key=api_key, base_url=base_url) def invoke(self, prompt: str, **kwargs) -> str: # 自动处理 context length 超限:截断 prompt + 保留关键 system message max_tokens = kwargs.get("max_tokens", 1024) if len(prompt) > 10485: # Claude 3 Haiku 最大 context # 保留前 200 字 system prompt + 后 10000 字用户输入 system_end = prompt.find("\n\n") + 2 truncated = prompt[:system_end] + prompt[-(10485-system_end):] prompt = truncated response = self.client.messages.create( model="claude-3-haiku-20240307", max_tokens=max_tokens, messages=[{"role": "user", "content": prompt}] ) return response.content[0].textSkills 的executor.py只需依赖ModelAdapter:
# skills/weather.forecast/executor.py from adapters.base import ModelAdapter from utils.llm_utils import format_prompt def execute(input_data: dict, model_adapter: ModelAdapter) -> dict: prompt = format_prompt("weather.jinja2", city=input_data["city"]) result = model_adapter.invoke(prompt) # 解析 result 成结构化 JSON,失败则抛出 SkillExecutionError return parse_weather_result(result)这样,当你要切换到 OpenAI 时,只需新增adapters/openai.py,并在启动时传入OpenAIAdapter(api_key, base_url),Skills 代码一行不用改。那个 “api error: 400 配置错误: claude provider 缺少 base_url 配置” 的报错,本质就是ClaudeAdapter.__init__()没收到base_url参数,属于适配器初始化失败,跟 Skills 本身无关。
3. 从零构建一个可运行的 Skills:以 “股票行情查询” 为例
3.1 定义 SKILL.md:用契约思维写清楚“能做什么,不能做什么”
我们以一个真实需求切入:金融风控团队需要 Agent 能实时查询 A 股个股最新价、涨跌幅、成交量。这不是简单调用公开 API,而是涉及数据源选择、错误重试、缓存策略、合规脱敏等工程细节。先写SKILL.md,这是整个 Skills 的宪法:
# skills/stock.quote/SKILL.md name: stock.quote description: | 查询指定 A 股股票(沪市/深市)的实时行情,返回最新价、涨跌幅、成交量、成交额。 支持股票代码(6位数字,如 600519)或股票简称(如 贵州茅台)。 不支持港股、美股、基金、债券等其他证券品种。 数据延迟不超过 30 秒(交易所行情推送延迟导致)。 若股票代码不存在或已退市,返回 error 字段。 input_schema: type: object properties: symbol: type: string description: 股票代码(6位数字)或股票简称(UTF-8 编码) minLength: 2 maxLength: 10 required: [symbol] output_schema: type: object properties: symbol: type: string description: 标准化后的 6 位股票代码 name: type: string description: 股票全称(已脱敏,不包含敏感信息如实际控制人) price: type: number description: 最新成交价(单位:人民币元) multipleOf: 0.01 change_percent: type: number description: 涨跌幅(%),精确到小数点后 2 位 volume: type: integer description: 成交量(单位:股) amount: type: number description: 成交额(单位:人民币元) timestamp: type: string description: 数据生成时间(ISO 8601 格式) required: [symbol, name, price, change_percent, volume, amount, timestamp]注意几个关键设计点:
description里明确写了“数据延迟不超过 30 秒”,这是对用户的承诺,也是后续 SLA 监控的依据;input_schema用minLength/maxLength限制symbol,防止恶意超长输入打爆内存;output_schema中price的multipleOf: 0.01强制价格必须是分精度,避免浮点误差;name字段强调“已脱敏”,这是金融合规的硬性要求,Skills 必须内置此逻辑,不能依赖上游。
3.2 编写 executor.py:聚焦业务逻辑,剥离基础设施
executor.py是 Skills 的心脏,但它只做三件事:校验输入、调用外部服务、格式化输出。所有基础设施(HTTP Client、Cache、Logger)都应通过依赖注入或全局配置获取,而非硬编码。
# skills/stock.quote/executor.py import json import logging import time from typing import Dict, Any from urllib.parse import quote # 从统一配置中心加载 from config import STOCK_API_BASE_URL, STOCK_API_TIMEOUT, REDIS_CLIENT from utils.http_client import get_session # 封装了重试、超时、UA 的 requests.Session from utils.cache import cache_with_ttl # 基于 Redis 的缓存装饰器 logger = logging.getLogger(__name__) def execute(input_data: Dict[str, Any], **kwargs) -> Dict[str, Any]: """ 执行股票行情查询 :param input_data: 符合 input_schema 的字典 :param kwargs: 可选参数,如 'cache_enabled' (bool) :return: 符合 output_schema 的字典 """ symbol = input_data["symbol"].strip() # Step 1: 输入预处理 - 将简称转为代码(调用内部映射服务) try: code = _resolve_symbol(symbol) except ValueError as e: return {"error": f"INVALID_SYMBOL: {str(e)}"} # Step 2: 缓存检查(可选) cache_key = f"stock:{code}" if kwargs.get("cache_enabled", True): cached = REDIS_CLIENT.get(cache_key) if cached: logger.debug(f"Hit cache for {code}") return json.loads(cached) # Step 3: 调用行情 API url = f"{STOCK_API_BASE_URL}/quote?code={quote(code)}" try: session = get_session() start_time = time.time() response = session.get(url, timeout=STOCK_API_TIMEOUT) response.raise_for_status() raw_data = response.json() # Step 4: 数据清洗与脱敏 result = _parse_and_sanitize(raw_data, code) # Step 5: 写入缓存(TTL 30 秒,匹配行情延迟承诺) if kwargs.get("cache_enabled", True): REDIS_CLIENT.setex(cache_key, 30, json.dumps(result)) logger.info(f"Quote fetched for {code} in {time.time() - start_time:.2f}s") return result except Exception as e: logger.error(f"Failed to fetch quote for {code}: {e}", exc_info=True) return {"error": f"API_ERROR: {str(e)}"} def _resolve_symbol(symbol: str) -> str: """将股票简称解析为标准代码,使用内部映射表""" # 实际项目中,这里会查 Redis 或本地 SQLite 映射库 # 示例映射:{"贵州茅台": "600519", "宁德时代": "300750"} mapping = { "贵州茅台": "600519", "宁德时代": "300750", "中国平安": "601318" } if symbol in mapping: return mapping[symbol] if len(symbol) == 6 and symbol.isdigit(): return symbol raise ValueError(f"Unknown symbol: {symbol}") def _parse_and_sanitize(raw: Dict, code: str) -> Dict[str, Any]: """解析原始行情数据,执行合规脱敏""" # 原始数据可能包含:公司全称、法人代表、注册地址等敏感字段 # Skills 必须只返回 output_schema 规定的字段,且 name 已脱敏 return { "symbol": code, "name": _sanitize_company_name(raw.get("name", "")), # 脱敏函数 "price": round(float(raw.get("price", 0)), 2), "change_percent": round(float(raw.get("change_percent", 0)), 2), "volume": int(raw.get("volume", 0)), "amount": round(float(raw.get("amount", 0)), 2), "timestamp": raw.get("timestamp", "") } def _sanitize_company_name(name: str) -> str: """基础脱敏:移除地址、电话、法人等敏感词""" # 实际项目用正则 + 敏感词库 sensitive_patterns = [r"[\d]{11}", r"[\d]{2,4}[\-\s]\d{2,4}[\-\s]\d{3,4}", r"[\u4e00-\u9fa5]{2,4}区.*?路.*?\d+号"] for pattern in sensitive_patterns: name = __import__('re').sub(pattern, "", name) return name.strip()[:20] # 截断过长名称注意:
executor.py里没有import redis或import requests,所有依赖都来自config和utils模块。这保证了 Skills 的可测试性——单元测试时,你可以轻松 Mockget_session()和REDIS_CLIENT。
3.3 编写 requirements.txt 与 tests:让 Skills 经得起生产考验
一个生产级 Skills,必须自带requirements.txt和tests/。很多人忽略这点,导致在客户环境部署时报 “ModuleNotFoundError”。
# skills/stock.quote/requirements.txt # 仅声明此 Skills 特有的依赖,全局依赖(如 requests, redis)由 Runtime 统一提供 # 格式严格遵循 pip freeze 输出 redis==4.6.0 jinja2==3.1.3tests/test_executor.py是质量防线:
# skills/stock.quote/tests/test_executor.py import pytest from unittest.mock import patch, MagicMock from skills.stock.quote.executor import execute, _resolve_symbol class TestStockQuote: def test_resolve_symbol_by_code(self): assert _resolve_symbol("600519") == "600519" def test_resolve_symbol_by_name(self): assert _resolve_symbol("贵州茅台") == "600519" def test_resolve_symbol_invalid(self): with pytest.raises(ValueError): _resolve_symbol("不存在的股票") @patch('skills.stock.quote.executor.get_session') @patch('skills.stock.quote.executor.REDIS_CLIENT') def test_execute_success(self, mock_redis, mock_session): # Mock HTTP 响应 mock_response = MagicMock() mock_response.json.return_value = { "name": "中国贵州茅台酒厂(集团)有限责任公司", "price": "1725.88", "change_percent": "1.23", "volume": "123456", "amount": "212345678.90", "timestamp": "2024-05-20T10:30:45+08:00" } mock_session.return_value.get.return_value = mock_response # Mock Redis 返回 None(不命中缓存) mock_redis.get.return_value = None result = execute({"symbol": "600519"}) assert result["symbol"] == "600519" assert result["name"] == "中国贵州茅台酒厂(集团)有限责任公司" # 脱敏前 assert result["price"] == 1725.88 assert "error" not in result @patch('skills.stock.quote.executor.get_session') def test_execute_api_error(self, mock_session): mock_session.return_value.get.side_effect = Exception("Network timeout") result = execute({"symbol": "600519"}) assert "error" in result assert "API_ERROR" in result["error"]运行测试只需:
cd skills/stock.quote pytest tests/ -v3.4 集成到 Runtime:让 Skills 真正“活”起来
假设你已安装了我们推荐的轻量 Runtime(pip install my-skills-runtime),集成步骤极简:
注册 Skills 目录:在 Runtime 配置文件
runtime_config.yaml中声明:skills_root: "/path/to/your/skills" default_adapters: - name: "claude" module: "adapters.claude.ClaudeAdapter" config: api_key: "${CLAUDE_API_KEY}" base_url: "${CLAUDE_BASE_URL}"启动 Runtime 服务:
# 启动后,Runtime 会扫描 skills_root 下所有子目录,加载有效的 SKILL.md skills-runtime serve --config runtime_config.yaml --port 8000通过 HTTP API 调用 Skills:
curl -X POST http://localhost:8000/skills/stock.quote/execute \ -H "Content-Type: application/json" \ -d '{"symbol": "贵州茅台"}'返回:
{ "symbol": "600519", "name": "贵州茅台", "price": 1725.88, "change_percent": 1.23, "volume": 123456, "amount": 212345678.9, "timestamp": "2024-05-20T10:30:45+08:00" }
这才是一个 Skills 的完整生命周期:从契约定义(SKILL.md),到逻辑实现(executor.py),再到可验证(tests),最后可部署(Runtime)。它不是一个玩具,而是一个随时可以放进生产环境的软件模块。
4. 生产环境常见问题与实战排查指南
4.1 “plugin tree failed to load” 类错误:90% 是路径与权限问题
搜索热词里反复出现 “dsh: plugin tree failed to load”“failed to install plugin: error: failed to clone git repository”,这类错误几乎都源于 Runtime 对 Skills 目录结构的校验失败。我的排查清单如下:
| 错误现象 | 根本原因 | 解决方案 |
|---|---|---|
plugin tree failed to load | Runtime 扫描到skills/目录下存在非 Skills 子目录(如skills/__pycache__/,skills/.git/,skills/docs/) | 严格遵守目录规范:Skills 目录下只能有skill-name/子目录,每个子目录内必须有SKILL.md。用find skills/ -type d -name "__pycache__" -exec rm -rf {} +清理。 |
failed to clone git repository | Runtime 尝试从 Git URL 动态加载 Skills,但网络不通、SSH Key 未配置、或仓库私有未授权 | 禁止生产环境动态 Git 加载:所有 Skills 必须以文件形式预部署。Git 加载仅用于 CI/CD 流水线,在部署阶段git clone到本地skills/目录,然后由 Runtime 加载本地文件。 |
SKILL.md not found | executor.py文件存在,但同级目录缺少SKILL.md,或文件名大小写错误(如skill.md) | 强制校验脚本:在 CI 流水线中加入检查:for d in skills/*/; do if [ ! -f "$d/SKILL.md" ]; then echo "MISSING SKILL.md in $d"; exit 1; fi; done |
Permission denied | executor.py没有可执行权限(Linux/macOS),或文件所有者不是 Runtime 进程用户 | 统一 chmod:find skills/ -name "executor.py" -exec chmod 755 {} \;,并确保 Runtime 以专用用户(如skills-runner)运行。 |
实操心得:我在某银行项目上线前夜,就因为一个同事不小心把
skills/目录的.DS_Store文件提交到了 Git,导致 macOS 生成的隐藏文件被 Runtime 误认为是 Skills 目录,报错plugin tree failed to load。最终用find skills/ -name ".DS_Store" -delete一键解决。教训是:永远在 CI 流水线里加入find skills/ -name ".*" -not -name ".git" -delete清理隐藏文件。
4.2 API 错误深度解析:“api error: 400 配置错误” 与 “context length 超限”
热词中的 “api error: 400 配置错误: claude provider 缺少 base_url 配置” 和 “api error: 400 this model's maximum context length is 10485” 是两类典型问题,必须区分对待:
配置类 400 错误(Missing base_url):这是适配器初始化失败,发生在 Skills 加载阶段,而非执行阶段。错误日志通常出现在 Runtime 启动日志里,而非 Skills 执行日志。解决方案是检查
runtime_config.yaml中default_adapters的config字段,确认base_url是否拼写正确(注意是base_url,不是baseurl或baseUrl),以及环境变量${CLAUDE_BASE_URL}是否已正确导出。一个快速验证方法是,在 Python shell 中手动初始化适配器:from adapters.claude import ClaudeAdapter # 如果这行报错,说明配置问题 adapter = ClaudeAdapter(api_key="test", base_url="https://api.anthropic.com")上下文超限类 400 错误(Context length 10485):这是模型 API 层面的硬性限制,发生在
executor.py的invoke()调用时。错误日志会显示在 Skills 执行日志中。绝不能简单地在 prompt 里加一句 “请简洁回答”,这不可靠。正确做法是在适配器层做主动截断,如 2.3 节所示:计算 prompt 长度,保留关键 system message,截取用户输入的后 N 个字符。我们实测发现,Claude 3 Haiku 的 10485 tokens 并非字符数,而是 token 数,中文平均 1.5 字符 ≈ 1 token,所以安全起见,对中文 prompt 做len(prompt.encode('utf-8')) * 1.5 < 10000的粗略估算,再留 500 token 余量。
更隐蔽的问题是“silent truncation”:某些模型 API(如部分开源 LLM)在超限时不会报错,而是静默截断 prompt,导致 Skills 返回结果不完整。我们的应对策略是:在executor.py中,对返回结果做长度校验。例如,如果 Skills 的output_schema要求result字段至少 10 个字符,而返回的result只有 2 个字符,就判定为截断失败,主动重试(降低 temperature)或降级到备用模型。
4.3 性能瓶颈定位:当 Skills 响应变慢时,如何精准找到“慢在哪”
Skills 的性能问题往往藏在链条深处。我设计了一套四层埋点法,能在 3 分钟内定位瓶颈:
Runtime 层埋点:在 Runtime 的请求入口和出口打日志,记录
request_id、skill_name、start_time、end_time。这是第一道过滤器,确认是 Skills 整体慢,还是某个特定 Skills 慢。Executor 层埋点:在
executor.py的execute()函数开头和结尾打日志,记录input_data的symbol(或其他关键 ID)和耗时。这能排除是输入数据本身导致的慢(如查询一个冷门股票,映射服务要查 10 次 DB)。依赖层埋点:在
get_session().get()调用前后打日志,记录 URL 和耗时。如果这里耗时长,说明是外部 API 慢或网络问题。子过程埋点:对
executor.py中的每个关键函数(如_resolve_symbol,_parse_and_sanitize)单独计时。我们曾发现,一个 Skills 的 80% 时间花在_sanitize_company_name的正则匹配上,因为用了贪婪匹配.*?,改成非贪婪[^,]*后,耗时从 1200ms 降到 15ms。
最终,我们把这些埋点日志统一发送到 ELK(Elasticsearch + Logstash + Kibana),用 Kibana 做可视化看板。一个典型的慢 Skills 分析视图包含:
- X 轴:时间(分钟)
- Y 轴:平均响应时间(ms)
- 折线:
total(Runtime 层)、executor(Executor 层)、http(HTTP 层)、db(DB 层) - 点击某条慢请求,下钻查看完整的调用链日志。
4.4 安全与合规红线:Skills 开发者必须知道的三条铁律
在金融、政务、医疗等强监管领域,Skills 的安全不是加分项,而是准入门槛。我总结了三条血泪教训换来的铁律:
铁律一:绝不允许 Skills 直接执行
os.system()或subprocess.Popen()。曾有团队为实现“截图当前页面”功能,在 Skills 里调用puppeteer,结果因未限制超时,一个恶意输入导致进程卡死,拖垮整个 Runtime。正确做法是:所有系统级操作,必须封装成独立的、受严格管控的微服务(如screenshot-service),Skills 通过 HTTP 调用,且设置timeout=5s和max_retries=1。铁律二:所有外部 API 调用,必须强制启用 HTTPS 且验证证书。
requests默认验证,但若用了verify=False或自签名证书,必须在get_session()中显式配置verify="/path/to/ca-bundle.crt"。我们曾因某供应商 API 使用自签名证书,而 Skills 未配置 CA Bundle,导致在客户内网环境 SSL 握手失败,排查两天才发现是证书问题。铁律三:Skills 的
output_schema必须 100% 覆盖所有返回字段,且禁止返回None或null。JSON Schema 的required字段,意味着如果 Skills 逻辑中某个字段计算失败(如price为None),就必须抛出SkillExecutionError,而不是返回{"price": null}。因为下游 Agent 很可能用result["price"] * 100计算,None * 100会直接报错。我们在executor.py的顶层try...except中,强制对返回字典做jsonschema.validate(instance=result, schema=output_schema),验证失败则记录SCHEMA_VALIDATION_FAILED错误。
5. Skills 生态建设与团队协作:如何让“写 Skills”变成可持续工程
5.1 建立 Skills Registry:让能力可发现、可复用、可审计
单个 Skills 是原子,但团队需要的是能力网络。我们搭建了一个内部 Skills Registry(能力注册中心),它不是一个 fancy 的 UI,而是一个基于 Git 的、带 Webhook 的静态站点:
Registry 目录结构:
registry/ ├── index.json # 所有 Skills 的元数据索引(自动生成) ├── categories/ # 分类目录(finance/, healthcare/, education/) │ ├── stock.quote.json │ └── drug.interaction.json └── skills/ # 所有 Skills 的 Git Submodule(指向各团队仓库) ├── stock.quote@v1.2.0 └── drug.interaction@v0.9.1自动化流程:
- 每个 Skills 仓库的
main分支 Push 时,触发 CI 流水线; - 流水线运行
skills-validator(我们开源的 CLI 工具),校验SKILL.md、executor.py、tests/; - 校验通过后,生成
categories/xxx.json(包含name,description,version, `last_updated
- 每个 Skills 仓库的