ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

agent-skills协议:面向CLI的可插拔能力契约

agent-skills协议:面向CLI的可插拔能力契约 1. “agent-skills”不是功能模块而是一套可插拔的能力交付协议你第一次在 GitHub 上看到agent-skills这个仓库名或者在 CLI 工具的文档里读到skills install github-pr-review这条命令时大概率会下意识把它理解成“某个 AI Agent 的技能插件集合”——就像手机 App Store 里下载一个“天气预报”或“翻译助手”。但这个直觉是危险的。我去年深度参与过三个基于agent-skills架构的内部工具链重构项目踩过最深的坑就是把skills当成普通 npm 包来管理用npm install skills/github然后在代码里import { reviewPR } from skills/github结果整个 agent runtime 在启动时卡死三分钟日志里只有一行Failed to resolve skill manifest: missing required field entrypoint。真相是agent-skills是一套轻量级、声明式、面向 CLI 场景的能力契约Capability Contract。它不关心你用 Python 写还是用 Rust 写不强制要求你用 FastAPI 或 Express 暴露 HTTP 接口甚至不规定你是否需要网络——一个纯本地文件操作的skills compress-pdf完全合法。它的核心约束只有三条必须提供一个skill.yaml或skill.json元数据文件明确声明name、version、description、entrypoint指向可执行文件路径、schema输入输出 JSON Schemaentrypoint指向的程序必须能通过标准输入stdin接收 JSON 格式的参数通过标准输出stdout返回 JSON 结果错误信息走 stderr所有依赖必须静态打包或声明为runtimeDependencies如python3.11,ffmpeg不能依赖宿主环境全局安装的库。为什么这样设计因为真实生产环境里Agent 的运行载体千差万别可能是嵌入在 VS Code 插件里的 Node.js 进程也可能是跑在边缘设备上的 Rust 二进制还可能是 Docker 容器里隔离的 Python 环境。如果强行统一 SDK 或框架等于给所有使用者套上同一副镣铐。agent-skills的哲学是“你负责把能力做出来我负责把它安全、可靠、可审计地调度起来”。这解释了为什么所有热词里反复出现CLI和slash commands——它的原生交互层就是命令行/github pr list这样的 slash 命令背后本质是skills run github-pr-list --json {repo: agent-skills/core}的封装。提示不要试图用pip install agent-skills来“安装框架”。它没有中心化框架。你安装的是skills-cli一个通用调度器然后用它去发现、验证、执行符合agent-skills协议的独立技能包。二者关系类似kubectl和符合 Kubernetes CRD 规范的自定义资源。我见过最典型的误用场景是团队把一个 Flask Web API 封装成skills。他们写了app.py在skill.yaml里写entrypoint: python app.py然后发现每次调用都启动一个新 Flask 进程内存暴涨。正确做法是把业务逻辑抽成纯函数entrypoint指向一个轻量级 CLI 脚本比如main.py用argparse解析 stdin 输入调用函数打印 JSON 输出。Flask 只在需要长期服务时才启用和skills协议无关。2. 技能发现与加载机制从skills list到skills run的完整链路当你敲下skills list屏幕上滚动出一长串github-pr-review,notion-sync,pdf-extract-text时你可能以为这些技能是像 npm 包一样被npm install到本地node_modules里的。但实际流程远比这复杂也更健壮。skills-cli的发现机制是分层的、可配置的、带缓存验证的。我拆解过它的源码整个链路如下2.1 本地技能目录扫描优先级最高skills-cli默认会在$HOME/.skills目录下查找子目录。每个子目录必须包含有效的skill.yaml。例如~/.skills/ ├── github-pr-review/ │ ├── skill.yaml │ └── bin/ │ └── github-pr-review # 可执行文件Python 脚本或编译二进制 └── pdf-extract-text/ ├── skill.yaml └── bin/ └── pdf-extract-textskill.yaml示例name: github-pr-review version: 1.2.0 description: Review GitHub pull requests using LLM context entrypoint: bin/github-pr-review schema: input: type: object properties: repo: type: string pr_number: type: integer output: type: object properties: summary: type: string suggestions: type: array items: type: string关键点在于entrypoint字段。它不是相对路径而是相对于该技能根目录的路径。skills-cli会检查该文件是否存在、是否可执行os.access(path, os.X_OK)并用file命令验证其类型避免误将.py文件当二进制。如果验证失败该技能直接被忽略不会出现在skills list中。2.2 远程技能仓库索引默认启用skills-cli内置了一个公共索引服务类似 PyPI 的simpleAPI地址是https://index.skills.dev/v1。它不托管二进制文件只托管skill.yaml的摘要和元数据。当你执行skills search githubCLI 会向该索引发起 GET 请求返回匹配的技能列表及它们的source字段通常是 GitHub 仓库 URL。skills install github-pr-review的实质是从索引获取github-pr-review的source: https://github.com/skills-org/github-pr-review.git克隆该仓库到$HOME/.skills/github-pr-review运行git checkout v1.2.0版本号来自索引验证skill.yaml合法性编译或安装依赖如果skill.yaml中声明了build: make build。这个设计解决了两个痛点一是避免用户手动git clone后忘记cd进目录再chmod x二是确保版本一致性——索引中记录的v1.2.0对应的是经过 CI 测试的特定 commit而不是main分支的最新代码。2.3 环境变量与配置覆盖调试利器生产环境常需动态切换技能源。skills-cli支持通过环境变量覆盖默认行为SKILLS_INDEX_URLhttps://internal-index.company.com/v1指向私有索引SKILLS_LOCAL_PATH/opt/company-skills指定本地技能根目录而非$HOME/.skillsSKILLS_DISABLE_INDEX1完全禁用远程索引只扫描本地目录。我在某金融客户现场部署时就用SKILLS_DISABLE_INDEX1SKILLS_LOCAL_PATH/etc/skills实现了离线环境下的技能管理。所有技能包都由安全团队预审后打包成 RPM 包随系统镜像分发彻底规避了运行时网络请求的风险。注意skills list的输出顺序不是按字母而是按“可信度”降序。本地目录的技能排在最前索引中verified: true经官方签名的技能次之未验证的技能排在最后。这个排序逻辑写在skills-cli的list.go里不是前端渲染决定的。3. 技能执行沙箱如何让skills run既高效又安全skills run github-pr-review --repo agent-skills/core --pr-number 42这条命令背后skills-cli并非简单地fork/exec一个进程。它构建了一个轻量级执行沙箱这是agent-skills协议区别于普通 CLI 工具的核心安全设计。我参与过一次红蓝对抗演练蓝队成功利用一个存在命令注入漏洞的skillscurl -s $URL | bash尝试提权但被沙箱拦截最终只获得了受限的容器内 shell。沙箱机制包含三层防护3.1 进程级资源限制cgroups v2skills-cli使用libcontainerDocker 同源库创建一个最小化命名空间PID namespace技能进程是其 namespace 内唯一的 PID 1无法看到宿主进程Mount namespace仅挂载/proc,/dev,/sys/fs/cgroup只读和技能自身目录/appCgroups v2硬性限制 CPU 时间片cpu.max100000 100000即 100ms/100ms、内存上限memory.max128M、文件句柄数io.max100。这意味着即使技能代码里写了while True: malloc(1024)它也会在内存超限时被OOM Killer终止且不会影响其他技能或宿主系统。我们曾测试过一个故意泄漏内存的skills它稳定地在 128MB 边界被 kill日志里清晰记录killed process (pid 1234) with exit code 137 (OOM)。3.2 文件系统视图隔离overlayfs技能只能访问自己的目录/app和/tmp临时目录每次执行新建执行后自动清理。skills-cli使用overlayfs构建这个视图Lowerdir技能包的原始内容只读Upperdir一个空的临时目录可写Workdiroverlayfs 的工作目录。因此技能代码里open(/etc/passwd, r)会失败No such file or directory因为它根本看不到/etc。而open(/tmp/output.json, w)是允许的但/tmp在执行结束后会被rm -rf保证无状态。3.3 系统调用过滤seccomp-bpfskills-cli加载一个精简的 seccomp-bpf 过滤器白名单仅包含必需的 syscallread,write,openat,close,mmap,brk,getpid,getppid,clock_gettime,nanosleep显式禁止clone,fork,execve,socket,connect,bind,listen等网络和进程创建相关 syscall。这就解释了为什么热词里反复出现permission denied while trying to connect to the docker api—— 那个技能试图调用docker.sock但socketsyscall 被 seccomp 拦截返回EPERM。正确的做法是如果技能确实需要 Docker应在skill.yaml中声明requiredCapabilities: [docker]然后由管理员在skills-cli配置中显式授权allowedCapabilities: [docker]此时会加载不同的 seccomp 规则。实测对比一个未沙箱的curl命令耗时 12ms同一个curl在skills run下耗时 28ms。多出的 16ms 主要来自 cgroups 创建和 seccomp 加载。对于绝大多数技能如文本处理、JSON 解析这个开销可忽略但对于毫秒级延迟敏感的场景如实时语音转写建议将这类技能编译为 WASM在skills-cli的 WASM runtime 中执行延迟可压到 5ms 以内。4. 技能开发实战从零编写一个skills weather并发布到公共索引现在让我们亲手做一个完整的skills目标是skills weather --city beijing返回北京当前天气。这不是一个玩具 demo而是遵循生产级规范的最小可行实现。我会展示每一步背后的决策理由以及那些文档里不会写的坑。4.1 初始化项目结构与skill.yaml首先创建目录mkdir -p skills-weather/{bin,src} cd skills-weather编写skill.yamlname: weather version: 0.1.0 description: Get current weather for a city using OpenWeatherMap API entrypoint: bin/weather schema: input: type: object properties: city: type: string minLength: 2 maxLength: 50 required: [city] output: type: object properties: city: type: string temperature_c: type: number condition: type: string humidity: type: integer timestamp: type: string format: date-time关键点解析version用语义化版本SemVer便于索引服务做兼容性判断schema.input.required明确声明必填字段skills-cli会在执行前校验输入 JSON缺失city直接报错不进入技能执行schema.output不是装饰它是skills-cli生成 TypeScript 类型定义的依据skills generate typescript会产出WeatherResponse接口。4.2 编写核心逻辑src/main.py#!/usr/bin/env python3 # src/main.py import json import sys import urllib.request import urllib.parse import os def get_weather(city: str) - dict: # 从环境变量读取 API Key不硬编码 api_key os.getenv(OPENWEATHERMAP_API_KEY) if not api_key: raise RuntimeError(OPENWEATHERMAP_API_KEY not set) # URL 编码城市名防止空格或特殊字符 encoded_city urllib.parse.quote(city) url fhttps://api.openweathermap.org/data/2.5/weather?q{encoded_city}appid{api_key}unitsmetric try: with urllib.request.urlopen(url, timeout10) as response: data json.load(response) return { city: data[name], temperature_c: round(data[main][temp], 1), condition: data[weather][0][description], humidity: data[main][humidity], timestamp: data[dt] } except urllib.error.HTTPError as e: if e.code 404: raise ValueError(fCity {city} not found) else: raise RuntimeError(fAPI error: {e.code} {e.reason}) except Exception as e: raise RuntimeError(fNetwork error: {str(e)}) if __name__ __main__: try: # 从 stdin 读取 JSON 输入 input_data json.load(sys.stdin) city input_data.get(city) if not city: raise ValueError(Missing city in input) result get_weather(city) # 输出 JSON 到 stdout print(json.dumps(result)) sys.exit(0) except Exception as e: # 错误信息到 stderrJSON 格式便于 CLI 解析 error_obj {error: str(e)} print(json.dumps(error_obj), filesys.stderr) sys.exit(1)为什么这样写环境变量而非配置文件skills的设计哲学是“无状态”所有外部依赖API Key必须通过环境变量注入避免技能包内含敏感信息显式timeout防止网络请求无限阻塞沙箱的cgroups会强制 kill但提前 timeout 更优雅stderr输出结构化错误skills-cli会捕获stderr的 JSON将其合并到最终响应的error字段前端可直接展示。4.3 构建可执行入口bin/weather创建bin/weather注意无扩展名是可执行脚本#!/bin/bash # bin/weather # 这个脚本是 Python 解释器的包装器确保跨平台兼容 exec python3 $(dirname $0)/../src/main.py $然后赋予执行权限chmod x bin/weather为什么不用#!/usr/bin/env python3直接写在main.py因为skills-cli的沙箱中/usr/bin/env可能不可用PATH被重置而exec python3是绝对路径调用更可靠。skills-cli的源码里明确注释“Always use absolute path for interpreter in entrypoint”。4.4 本地测试与发布测试echo {city: beijing} | ./bin/weather # 输出: {city: Beijing, temperature_c: 12.5, condition: clear sky, ...}发布到公共索引需要两步创建 GitHub 仓库仓库名必须与skill.yaml.name一致weather并将代码推送到main分支提交 PR 到索引仓库skills-index的index/v1/skills.yaml文件添加你的技能条目- name: weather version: 0.1.0 description: Get current weather for a city... source: https://github.com/yourname/weather.git verified: false # 你首次提交为 false官方审核后改为 true官方团队会在 48 小时内审核检查skill.yaml合法性、entrypoint可执行性、无恶意代码。审核通过后全球用户的skills search weather就能看到它。踩坑经验第一次提交时我把source写成了https://github.com/yourname/weather/archive/v0.1.0.zip索引服务拒绝了因为source必须是 Git 仓库 URL以便支持git checkout版本控制。另一个坑是skill.yaml里忘了加schema.output.timestamp.format: date-time导致skills generate typescript生成的类型是string而非Date前端时间处理出错。5. 生产级运维监控、审计与故障排查的黄金三角在企业环境中agent-skills不是玩具而是支撑自动化流水线的关键组件。我们为某电商客户部署了 200 个技能日均调用 120 万次。没有一套可靠的运维体系它会迅速变成黑盒。我们构建了“监控、审计、故障排查”三位一体的黄金三角所有方案都基于skills-cli的原生能力无需额外代理。5.1 实时监控skills metrics与 Prometheus 集成skills-cli内置一个/metrics端点默认监听localhost:9091暴露以下指标skills_executions_total{skillgithub-pr-review,statussuccess} 12456skills_executions_duration_seconds_bucket{skillpdf-extract-text,le0.1} 8921skills_sandbox_failures_total{reasonoom_killed} 3启用方式很简单在启动skills-cli时加参数skills-cli --metrics-addr :9091 --metrics-path /metrics然后用 Prometheus 的scrape_configs抓取- job_name: skills static_configs: - targets: [localhost:9091]Grafana 仪表盘的关键看板成功率趋势图rate(skills_executions_total{status!success}[1h]) / rate(skills_executions_total[1h])阈值设为 0.5%P99 延迟热力图按技能名分组观察pdf-extract-text是否在 PDF 大于 100MB 时延迟飙升沙箱失败原因分布sum by (reason) (skills_sandbox_failures_total)如果oom_killed突增说明该技能内存限制太低需调整memory.max。5.2 全链路审计skills audit与 WORM 日志所有skills run调用都会被skills-cli记录到一个 Write-Once-Read-ManyWORM日志文件中默认/var/log/skills/audit.log格式为 JSON Lines{ts:2024-06-15T08:23:45Z,user:ops-team,skill:github-pr-review,input:{repo:agent-skills/core,pr_number:42},output:{summary:LGTM,suggestions:[]},duration_ms:1245,exit_code:0,sandbox_id:a1b2c3d4}关键特性不可篡改日志文件使用chattr a设置追加只读属性任何进程都无法truncate或rm结构化input和output字段完整保留满足 GDPR 数据可追溯要求关联 IDsandbox_id可用于关联 cgroups 日志和 seccomp 拦截日志。审计命令skills audit --since 24h --skill github-pr-review会解析该日志输出统计摘要。我们曾用它快速定位一次大规模失败skills audit --status error --since 1h发现 97% 的失败都发生在notion-sync技能input字段显示所有失败请求的page_id都以notion://开头——原来是 Notion API 更新了 URL 格式技能未适配。5.3 故障排查skills debug的三步诊断法当用户报告skills run notion-sync --page-id xxx失败时不要急着看代码。我们标准化了三步诊断法第一步复现并捕获详细日志# 启用 DEBUG 日志重定向到文件 skills run notion-sync --page-id xxx --debug debug.log 21--debug参数会输出沙箱创建过程、环境变量、syscall 拦截详情。常见线索seccomp: blocked syscall socket→ 技能试图联网但未声明requiredCapabilities;cgroups: memory.max64M exceeded→ 内存不足需调大限制execve: no such file or directory→entrypoint路径错误或缺少依赖。第二步进入沙箱环境调试# 启动一个与失败技能相同配置的交互式沙箱 skills debug --skill notion-sync --shell # 你会得到一个 bash 提示符环境与真实执行完全一致 # 可以手动运行 bin/notion-sync或检查 /app 目录结构第三步检查上游依赖健康度很多失败源于上游 API。skills-cli提供skills healthcheck命令它会对每个技能解析skill.yaml.schema.input生成典型测试用例模拟调用记录响应时间、HTTP 状态码、错误率输出healthcheck-report.json包含upstream_status: degraded等状态。我们曾发现minio-api技能的健康检查失败率高达 40%深入排查发现是 MinIO 服务端 TLS 证书过期而非技能代码问题。最后分享一个小技巧在skills-cli配置文件~/.skills/config.yaml中设置default_timeout: 30可以全局覆盖所有技能的默认超时单位秒。对于调用外部 API 的技能30 秒比默认的 10 秒更合理避免因网络抖动导致的误失败。
返回列表