给 Agent Skill 装上「黑匣子」:STOP 可观测性协议设计与实现
Agent Skill 生态正在爆发,但 Skill 执行过程是黑盒。STOP(Skill Transparency & Observability Protocol)是一个开放规范,让 Skill 的能力声明、执行追踪、结果验证变得标准化和可观测。本文介绍 STOP 的设计思路、规范细节,以及 CLI 工具和 Runtime SDK 的实现。
目录
- [问题:Skill 是黑盒](#问题:Skill 是黑盒 "#%E9%97%AE%E9%A2%98skill-%E6%98%AF%E9%BB%91%E7%9B%92")
- [STOP 是什么](#STOP 是什么 "#stop-%E6%98%AF%E4%BB%80%E4%B9%88")
- 四层规范设计
- [1. Manifest:能力声明](#1. Manifest:能力声明 "#1-manifest%E8%83%BD%E5%8A%9B%E5%A3%B0%E6%98%8E")
- [2. Trace:执行追踪](#2. Trace:执行追踪 "#2-trace%E6%89%A7%E8%A1%8C%E8%BF%BD%E8%B8%AA")
- [3. Assertions:断言验证](#3. Assertions:断言验证 "#3-assertions%E6%96%AD%E8%A8%80%E9%AA%8C%E8%AF%81")
- [4. Levels:渐进式采纳](#4. Levels:渐进式采纳 "#4-levels%E6%B8%90%E8%BF%9B%E5%BC%8F%E9%87%87%E7%BA%B3")
- [CLI 工具:stop-cli](#CLI 工具:stop-cli "#cli-%E5%B7%A5%E5%85%B7stop-cli")
- [Runtime SDK:stop-runtime](#Runtime SDK:stop-runtime "#runtime-sdkstop-runtime")
- 实战示例
- 总结
问题:Skill 是黑盒
AI Agent 的能力越来越依赖 Skill(技能插件)。OpenClaw 的 SundialHub 上已经有 4 万多个 Skill,各种 Agent 框架也在构建自己的 Skill 生态。
但有一个根本问题:Skill 执行过程完全不透明。
你调用一个 Skill,它做了什么?调了哪些 API?读了哪些文件?成功还是失败?你不知道。
这带来几个实际痛点:
- 调试靠猜 --- Skill 失败了,你只能翻日志祈祷能找到线索
- 信任是二元的 --- 要么完全信任一个 Skill,要么完全不用
- 组合很脆弱 --- 串联多个 Skill 时,没有 stderr,出错了不知道断在哪
- 安全审计靠人工 --- 没有标准方式知道一个 Skill 实际做了什么
这就像早期的微服务------没有 tracing、没有 metrics、没有 health check,出了问题全靠经验和运气。
后来 SRE 领域发展出了可观测性三支柱(Logs、Metrics、Traces),微服务的运维才变得可控。
STOP 要做的,就是把这套方法论搬到 Skill 层。
STOP 是什么
STOP(Skill Transparency & Observability Protocol)是一个开放规范,定义了:
- Skill 如何声明自己的能力(Manifest)
- 运行时如何输出执行追踪(Trace)
- 如何验证执行结果(Assertions)
- 如何渐进式采纳(Levels)
核心设计原则:
- 最小侵入 --- L0 只需要一个 YAML 文件,零运行时开销
- 渐进式 --- 从声明到追踪到断言,按需逐步加
- 标准化 --- 基于 OpenTelemetry 的 span 模型,可对接现有基础设施
- 平台无关 --- 不绑定任何特定 Agent 框架
项目地址:github.com/echoVic/sto...
四层规范设计
1. Manifest:能力声明
Manifest 是 STOP 的基础------一个 skill.yaml 文件,声明 Skill 的输入、输出、使用的工具、副作用等。
把它理解为 Skill 的 package.json,但关注点是可观测性和信任,而不是依赖管理。
yaml
sop: "0.1"
name: juejin-publish
version: 1.2.0
description: 发布 Markdown 文章到掘金
inputs:
- name: article_path
type: file_path
required: true
description: Markdown 文章路径
constraints:
pattern: "\\.md$"
outputs:
- name: article_url
type: url
description: 发布后的文章链接
guaranteed: true
- name: article_id
type: string
description: 掘金文章 ID
guaranteed: true
tools_used:
- exec
- web_fetch
- read
side_effects:
- type: filesystem
access: read
paths: ["${inputs.article_path}"]
- type: network
description: POST 请求到掘金 API
destinations: ["juejin.cn"]
requirements:
env_vars: [JUEJIN_SESSION_ID]
有了这个文件,你立刻能知道:
- 这个 Skill 需要什么输入(一个 .md 文件路径)
- 它会产生什么输出(文章 URL 和 ID)
- 它用了哪些工具(exec、web_fetch、read)
- 它有什么副作用(读文件 + 网络请求到 juejin.cn)
- 它需要什么环境(JUEJIN_SESSION_ID 环境变量)
这就是 L0 的全部------一个 YAML 文件,零运行时改动。
skill.yaml 和 SKILL.md 是互补关系:
| 维度 | SKILL.md | skill.yaml |
|---|---|---|
| 受众 | Agent(LLM) | Runtime(机器) |
| 格式 | 自由 Markdown | 结构化 YAML |
| 用途 | 教 Agent 怎么用 | 告诉 Runtime 做了什么 |
2. Trace:执行追踪
Trace 是 Skill 的「飞行记录仪」------记录运行时发生了什么、什么顺序、花了多久、是否成功。
采用 OpenTelemetry 的 span 树模型:
less
Trace
└── Root Span (skill execution)
├── Span: read article.md
├── Span: exec python3 publish.py
│ └── Span: POST juejin.cn/api
└── Span: assertions check
每个 span 的结构:
typescript
interface Span {
span_id: string;
trace_id: string;
parent_span_id?: string;
start_time: string; // ISO-8601
end_time: string;
duration_ms: number;
kind: SpanKind; // skill.execute | tool.call | file.read | http.request | ...
name: string;
status: "ok" | "error" | "skipped";
attributes: Record<string, any>;
}
Trace 输出为 NDJSON 格式(每行一个 span),存储在 .sop/traces/ 目录:
jsonl
{"trace_id":"t_abc","span_id":"s_001","kind":"skill.execute","name":"juejin-publish","status":"ok","duration_ms":3420}
{"trace_id":"t_abc","span_id":"s_002","parent_span_id":"s_001","kind":"file.read","name":"read article","duration_ms":12}
{"trace_id":"t_abc","span_id":"s_003","parent_span_id":"s_001","kind":"tool.call","name":"exec: python3 publish.py","duration_ms":3100}
{"trace_id":"t_abc","span_id":"s_004","parent_span_id":"s_003","kind":"http.request","name":"POST juejin.cn/api","duration_ms":2200}
关键设计决策:
- NDJSON 而非 JSON --- 流式写入,不需要等执行完才输出
- 兼容 OpenTelemetry --- 可以直接转发到 Jaeger、Grafana 等
- 敏感数据脱敏 --- 不记录凭证、文件内容,只记录元数据
3. Assertions:断言验证
Assertions 回答一个关键问题:「这个 Skill 真的成功了吗?」
没有断言时,Skill 成功的判断标准是:
- 没抛异常(弱信号)
- LLM 说成功了(不可靠)
- 人工检查(不可扩展)
有了断言,成功变成可机器验证的:
yaml
assertions:
pre:
- check: file_exists
path: "${inputs.article_path}"
message: "文章文件必须存在"
- check: env_var
name: JUEJIN_SESSION_ID
message: "需要掘金 Session ID"
post:
- check: output.article_url
matches: "^https://juejin\\.cn/post/\\d+$"
- check: output.article_id
not_empty: true
支持的检查类型:
| 类型 | 用途 |
|---|---|
env_var |
环境变量是否存在 |
file_exists |
文件是否存在 |
file_not_empty |
文件是否非空 |
file_matches |
文件内容是否匹配正则 |
tool_available |
工具是否可用 |
output.* |
输出字段验证(matches/equals/not_empty/greater_than) |
duration |
执行时间是否在限制内 |
custom |
自定义脚本验证 |
基于历史断言通过率,还可以计算 Trust Score:
| 分数 | 标签 | 含义 |
|---|---|---|
| 0.95+ | ✅ Trusted | 稳定通过所有断言 |
| 0.80-0.94 | ⚠️ Unstable | 偶尔失败 |
| < 0.80 | 🔴 Unreliable | 频繁失败 |
Skill 平台(如 SundialHub)可以展示 Trust Score,帮用户选择可靠的 Skill。
4. Levels:渐进式采纳
STOP 不要求一步到位,定义了四个等级:
| 等级 | 名称 | 你需要做什么 | 你能获得什么 |
|---|---|---|---|
| L0 | Manifest | 写一个 skill.yaml | 静态分析、依赖审计、副作用可见 |
| L1 | Trace | Runtime 自动输出(无需 Skill 作者改动) | 执行时间线、工具调用审计 |
| L2 | Assertions | 在 skill.yaml 里加断言规则 | 自动成功验证、Trust Score |
| L3 | Full | 定义自定义指标和基线 | 成本追踪、异常检测、SLA 监控 |
决策树:
个人/内部 Skill? → L0
需要调试失败? → L1
需要用户/平台信任? → L2
生产环境大规模运行? → L3
L0 的成本是零------只需要一个 YAML 文件。 这是刻意设计的,降低采纳门槛。
CLI 工具:stop-cli
为了让开发者快速上手,我们提供了 stop-cli:
bash
# 安装
npm install -g stop-cli
# 或直接用 npx
npx stop-cli init
stop init
交互式生成 skill.yaml:
bash
$ stop init
🛑 stop init --- Generate skill.yaml
Skill name (kebab-case) (my-skill): juejin-publish
Version (1.0.0): 1.2.0
Description: Publish markdown articles to Juejin
Author: echoVic
Observability level (L0/L1/L2/L3) (L0): L2
Tools used (comma-separated): exec,read,web_fetch
✅ Created skill.yaml
stop validate
校验 skill.yaml 是否符合规范:
bash
$ stop validate
✅ skill.yaml is valid
如果有问题会明确报错:
bash
$ stop validate bad-skill.yaml
❌ Missing required field: version
❌ Input "foo": unknown type "invalid_type"
❌ Side effect: unknown type "banana"
⚠️ name should be kebab-case: "BAD_NAME"
3 error(s), 1 warning(s)
校验内容包括:
- 必填字段(sop、name、version、description)
- 名称格式(kebab-case)
- 输入/输出类型合法性
- 副作用类型合法性
- 可观测性等级合法性
${inputs.x}插值引用检查
Runtime SDK:stop-runtime
stop-runtime 是给 Agent Runtime 集成用的 SDK,提供三个核心能力:
bash
npm install stop-runtime
Manifest 加载
typescript
import { loadManifest, parseManifest } from 'stop-runtime';
// 从文件加载
const manifest = loadManifest('./skill.yaml');
// 从字符串解析
const manifest = parseManifest(yamlString);
Assertion Runner
typescript
import { runAssertions } from 'stop-runtime';
// 跑 pre-checks
const preResults = runAssertions(manifest.assertions.pre, {
env: process.env,
inputs: { article_path: './article.md' },
tools: ['exec', 'read', 'web_fetch'],
}, 'pre');
// 跑 post-checks
const postResults = runAssertions(manifest.assertions.post, {
outputs: {
article_url: 'https://juejin.cn/post/123456',
article_id: '123456',
},
duration_ms: 3420,
}, 'post');
// 检查结果
for (const r of postResults) {
console.log(`${r.check}: ${r.status}`); // output.article_url: pass
}
每个 assertion 结果包含:
typescript
interface AssertionResult {
check: string; // 检查类型
status: 'pass' | 'fail';
severity: 'error' | 'warn';
message?: string;
value?: any;
}
Tracer
typescript
import { createTracer } from 'stop-runtime';
const tracer = createTracer(manifest);
// 记录工具调用
const spanId = tracer.startSpan('tool.call', 'exec: python3 publish.py');
// ... 执行工具 ...
tracer.endSpan(spanId, 'ok', { 'tool.name': 'exec' });
// 记录 HTTP 请求
const httpSpan = tracer.startSpan('http.request', 'POST juejin.cn/api', spanId);
tracer.endSpan(httpSpan, 'ok', { 'http.status_code': 200 });
// 完成并输出
tracer.finish('ok');
// 导出 NDJSON
console.log(tracer.toNDJSON());
// 或写入文件(.sop/traces/)
tracer.writeTo();
实战示例
以 juejin-publish Skill 为例,完整的 STOP 集成流程:
1. 创建 manifest(L0)
bash
cd skills/juejin-publish/
stop init
# 填写信息,生成 skill.yaml
2. 添加断言(L2)
在 skill.yaml 中加入 assertions 部分(见上文示例)。
3. Runtime 集成
typescript
import { loadManifest, runAssertions, createTracer } from 'stop-runtime';
async function executeSkill(skillDir: string, inputs: Record<string, any>) {
const manifest = loadManifest(`${skillDir}/skill.yaml`);
const tracer = createTracer(manifest);
// Pre-checks
const preResults = runAssertions(manifest.assertions?.pre ?? [], {
env: process.env,
inputs,
tools: ['exec', 'read', 'web_fetch'],
}, 'pre');
const preErrors = preResults.filter(r => r.status === 'fail' && r.severity === 'error');
if (preErrors.length > 0) {
tracer.finish('error');
throw new Error(`Pre-check failed: ${preErrors.map(e => e.message).join(', ')}`);
}
// Execute skill
const execSpan = tracer.startSpan('tool.call', 'exec: python3 publish.py');
const outputs = await runPublishScript(inputs);
tracer.endSpan(execSpan, 'ok');
// Post-checks
const postResults = runAssertions(manifest.assertions?.post ?? [], {
outputs,
}, 'post');
const status = postResults.some(r => r.status === 'fail' && r.severity === 'error') ? 'error' : 'ok';
tracer.finish(status);
tracer.writeTo();
return { outputs, assertions: postResults, traceId: tracer.traceId };
}
执行后,.sop/traces/ 目录下会生成 trace 文件,可以用来调试、审计、或对接监控系统。
总结
STOP 协议的核心思路很简单:把 SRE 的可观测性方法论搬到 Agent Skill 层。
- L0 Manifest --- 一个 YAML 文件,让 Skill 从黑盒变成白盒
- L1 Trace --- 执行追踪,知道发生了什么
- L2 Assertions --- 断言验证,知道是否真的成功
- L3 Full --- 指标 + 异常检测,生产级监控
工具已经可用:
bash
# CLI
npx stop-cli init
npx stop-cli validate
# SDK
npm install stop-runtime
项目地址:github.com/echoVic/sto...
这是一个早期规范(0.1.0-draft),欢迎参与讨论和贡献。Skill 生态需要可观测性,就像微服务需要 tracing 一样。
如果你也在做 Agent 相关的开发,欢迎试用 STOP 并提 Issue/PR。让我们一起把 Skill 从黑盒变成白盒。