给 Agent Skill 装上「黑匣子」:STOP 可观测性协议设计与实现

给 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)是一个开放规范,定义了:

  1. Skill 如何声明自己的能力(Manifest)
  2. 运行时如何输出执行追踪(Trace)
  3. 如何验证执行结果(Assertions)
  4. 如何渐进式采纳(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.yamlSKILL.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 成功的判断标准是:

  1. 没抛异常(弱信号)
  2. LLM 说成功了(不可靠)
  3. 人工检查(不可扩展)

有了断言,成功变成可机器验证的:

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 从黑盒变成白盒。

相关推荐
笨蛋不要掉眼泪1 小时前
Sentinel 流控规则详解:三种模式与三种效果实战指南
java·jvm·数据库·后端·sentinel
阿乐艾官2 小时前
【K8s思维导图及单节点容器启动流程】
java·容器·kubernetes
再难也得平2 小时前
[LeetCode刷题]1.两数之和(java题解)
java·算法·leetcode
yaoxin5211232 小时前
327. Java Stream API - 实现 joining() 收集器:从简单到进阶
java·开发语言
再难也得平2 小时前
[LeetCode刷题]283.移动零(通俗易懂的java题解)
java·算法·leetcode
linux_cfan2 小时前
2026版 WordPress 视频插件终极选型:知识付费创作者如何低成本打造专业在线课堂?
前端·javascript·音视频·html5
野犬寒鸦2 小时前
Java8 ConcurrentHashMap 深度解析(底层数据结构详解及方法执行流程)
java·开发语言·数据库·后端·学习·算法·哈希算法
百锦再2 小时前
Java IO详解:File、FileInputStream与FileOutputStream
java·开发语言·jvm·spring boot·spring cloud·kafka·maven
追随者永远是胜利者3 小时前
(LeetCode-Hot100)647. 回文子串
java·算法·leetcode·职场和发展·go