CnOps 智能运维与可观测社区
首页开源项目实践文章视频课程常见问答开发者工具STAROps 专题
导航菜单
首页开源项目实践文章视频课程常见问答开发者工具STAROps 专题

CnOps 智能运维与可观测社区



愿景

CnOps 智能运维与可观测社区是一个以"智能运维与可观测"为核心的开放、包容、分享的技术社区,旨在聚集运维专家、开发者和爱好者,共同探讨、学习和分享可观测最佳实践与最新技术,与众多技术社区合作互动,共同探讨交叉领域的技术挑战,推动可观测领域的创新与进步。

内容社区

  • 实践文章
  • 视频课程
  • 开源项目
  • 常见问答
  • 开发者工具

友情链接

  • Prometheus
  • Grafana Lab
  • OpenTelemetry
  • LoongCollector

关注我们

阿里云云原生公众号阿里云云原生
阿里云可观测公众号阿里云可观测

Copyright © 2026 CnOps 社区. All rights reserved.

首页STAROps 专题编写 Skill 确定性脚本

编写 Skill 确定性脚本

#持续优化#持续优化

STAROps | 2026-07-31

查看对话回放内容演示

本规范定义 STAROps Skill 中数值计算的确定性脚本写法,保证同输入同输出。

模型推理在两类计算上不可靠:

  • 单位换算:同一会话内两遍可能给出 1024 进制(1.00GB)或 1000 进制(1.07GB)不同答案
  • 阈值与持续时间判断:可能错算持续时间或漏掉 NaN 值

脚本化把这两类计算固化下来,跨次结果可复现、可回放。

前提条件

  • 已开通 STAROps,账号可创建并运行数字员工 Skill。
  • 已具备 Python 3.8+ 与基础脚本编写能力。
  • 已识别 Skill 中包含的数值计算(单位换算 / 聚合 / 阈值判断 / Diff),并把它们与模型推理类任务(根因假设排序、跨域关联、经验综合)区分开。
  • 已了解 编写 STAROps 运维 Skill 中 7 要素中的「计算与脚本」「推理边界」要素。

规范要素

要素 含义 是否必须
数据驱动声明 业务脚本只声明配置(阈值、单位、比较方向),零计算逻辑 必须
公共引擎 所有计算集中在公共模块(查询、解析、评估、格式化) 必须
纯函数保证 数值计算函数无随机数、无当前时间依赖、无全局状态 必须
结构化输出 固定 JSON 结构 + 标准 status 枚举(pass / find_problem / no_problem_found / error) 必须
CLI 集成 标准参数(--region / --project / --time-range)+ 标准退出码 必须

「数据驱动声明 + 公共引擎」是承载本规范的架构:

  • 业务脚本:只写 InspectionCase 配置项(阈值、单位、比较方向),零计算逻辑
  • 公共引擎:承载 PromQL 拼接、阈值评估、持续时间累计、JSON 格式化等全部计算

新增一项巡检 = 新增一个 InspectionCase 数据项,无新增计算代码。下文「应用样例」每条都是这个架构下的一个切面。

应用样例

样例 1:单位换算

项 内容
正例 format_bytes(1073741824) 返回 "1.00GB";format_bytes(536870912) 返回 "512.0MB"。纯函数,无状态。
反例 让模型心算「1073741824 字节约等于多少 GB」。同一会话内问两遍,可能返回 1.07GB(1000 进制)或 1.00GB(1024 进制)。下游若用此值做阈值判断,结果不可复现。
期望输出 始终 1024 进制,精度固定: format_bytes(0) → "0B" format_bytes(2048) → "2KB" format_bytes(1073741824) → "1.00GB"
关键差异 正例固化进制与精度,反例依赖模型猜测。

参考实现(纯函数 + 标准 CLI):

查看脚本

python

#!/usr/bin/env python3
"""单位换算脚本模板:纯函数、同输入同输出、无副作用。"""

import argparse
import json

def format_bytes(value: float) -> str:
    """字节数 → 可读格式(纯函数)。"""
    if value >= 1073741824:
        return f"{value / 1073741824:.2f}GB"
    elif value >= 1048576:
        return f"{value / 1048576:.1f}MB"
    elif value >= 1024:
        return f"{value / 1024:.0f}KB"
    return f"{value:.0f}B"

def format_percent(value: float) -> str:
    return f"{value:.2f}%"

def format_duration(seconds: float) -> str:
    if seconds >= 3600:
        return f"{seconds / 3600:.1f}h"
    elif seconds >= 60:
        return f"{seconds / 60:.1f}min"
    return f"{seconds:.0f}s"

def format_count(value: float) -> str:
    if value >= 1_000_000:
        return f"{value / 1_000_000:.1f}M"
    elif value >= 1_000:
        return f"{value / 1_000:.1f}K"
    return f"{value:.0f}"

CONVERTERS = {
    "bytes": format_bytes,
    "percent": format_percent,
    "seconds": format_duration,
    "count": format_count,
}

def main():
    parser = argparse.ArgumentParser(description="单位换算脚本")
    parser.add_argument("--value", type=float, required=True)
    parser.add_argument("--unit", choices=CONVERTERS.keys(), required=True)
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()
    formatted = CONVERTERS[args.unit](args.value)
    if args.json:
        print(json.dumps({"raw_value": args.value, "unit": args.unit, "formatted": formatted}, ensure_ascii=False))
    else:
        print(formatted)

if __name__ == "__main__":
    main()

样例 2:聚合计算

项 内容
正例 PromQL 在数据源层完成聚合: avg by (instance_id) (rate(rds_cpu_usage_total[3m])) / 100 * 100 脚本侧只取最终值:value = float(row["value"][1])。
反例 脚本拉取原始时间序列后自己算 avg。可能用错公式(算术平均 vs 加权平均)、漏掉 NaN 值,或对采样间隔做错误假设,结果与 PromQL 内置 rate 不一致。
期望输出 聚合在 PromQL 完成,脚本输出结构化结果: {"case_id": "rds_cpu_high", "value": 92.5, "threshold": 80.0, "compare": "gt"}
关键差异 正例聚合下沉到数据源,反例聚合上提到脚本层引入误差。

聚合必须留在脚本层时(如离线 batch 处理、数据源不支持百分位),按下面这套纯函数实现,保证可回放:

查看脚本

python

#!/usr/bin/env python3
"""聚合计算脚本模板:avg / max / min / P50 / P95 / P99,纯函数。"""

import argparse
import json
import sys
from typing import List, Dict

def calc_avg(values: List[float]) -> float:
    return sum(values) / len(values) if values else 0.0

def calc_max(values: List[float]) -> float:
    return max(values) if values else 0.0

def calc_min(values: List[float]) -> float:
    return min(values) if values else 0.0

def calc_percentile(values: List[float], p: float) -> float:
    """百分位数(纯函数):线性插值法,与 numpy.percentile 默认行为一致。"""
    if not values:
        return 0.0
    sorted_values = sorted(values)
    k = (len(sorted_values) - 1) * (p / 100.0)
    f = int(k)
    c = f + 1 if f + 1 < len(sorted_values) else f
    return sorted_values[f] + (k - f) * (sorted_values[c] - sorted_values[f])

def aggregate(time_series: List[Dict]) -> Dict:
    values = [point["value"] for point in time_series if "value" in point]
    return {
        "count": len(values),
        "avg": round(calc_avg(values), 2),
        "max": round(calc_max(values), 2),
        "min": round(calc_min(values), 2),
        "p50": round(calc_percentile(values, 50), 2),
        "p95": round(calc_percentile(values, 95), 2),
        "p99": round(calc_percentile(values, 99), 2),
    }

def main():
    parser = argparse.ArgumentParser(description="聚合计算脚本")
    parser.add_argument("--metric", required=True)
    parser.add_argument("--input", help="输入 JSON 文件路径(默认 stdin)")
    args = parser.parse_args()
    data = json.load(open(args.input) if args.input else sys.stdin)
    if args.metric not in data:
        print(json.dumps({"error": f"metric '{args.metric}' not found"}))
        sys.exit(1)
    result = aggregate(data[args.metric])
    result["metric"] = args.metric
    print(json.dumps(result, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()

样例 3:阈值与持续时间判断

项 内容
正例 数据声明 + 公共评估函数: InspectionCase(case_id="rds_cpu_high", threshold=80.0, duration=300, compare=CompareOp.GT, data_format="percent") 评估走 calc_sustained_seconds(series, threshold=80.0, compare=CompareOp.GT),纯函数遍历时间序列。
反例 Skill 只写「如果 CPU 使用率超过 80% 且持续 5 分钟,则判定为异常」,让模型从原始数据计算。模型可能错算持续时间(如把采样间隔误读为秒数),或把瞬时尖刺也算成持续超阈值。
期望输出 公共引擎产出单项结果(节选): {"case_id": "rds_cpu_high", "status": "find_problem", "duration_seconds": 360, "total_entities": 12, "abnormal_count": 1}
关键差异 正例阈值与持续时间下沉到数据声明,反例让模型做数值判断。

calc_sustained_seconds 与 evaluate 核心实现(纯函数 + 时间戳与采样间隔自动推断):

查看脚本

python

#!/usr/bin/env python3
"""阈值+持续时间判断脚本模板:纯函数。"""

import argparse
import json
import sys
from typing import List, Dict

def calc_sustained_seconds(points: List[Dict], threshold: float, compare: str) -> int:
    """计算连续超阈值的最长持续秒数(纯函数)。"""
    if not points or len(points) == 1:
        return 0

    # 推断时间戳除数(毫秒 vs 秒)
    ts_divisor = 1000 if points[0]["timestamp"] > 1e12 else 1

    # 推断采样间隔
    timestamps = [p["timestamp"] for p in points]
    diffs = sorted([
        timestamps[i] - timestamps[i - 1]
        for i in range(1, len(timestamps))
        if timestamps[i] > timestamps[i - 1]
    ])
    if not diffs:
        return 0
    expected_gap = diffs[len(diffs) // 2]
    gap_tolerance = int(expected_gap * 2.0)

    def is_breach(value: float) -> bool:
        if compare == "gt":  return value > threshold
        if compare == "gte": return value >= threshold
        if compare == "lt":  return value < threshold
        if compare == "lte": return value <= threshold
        return False

    max_sustained = 0
    run_start_ts = points[0]["timestamp"]
    prev_ts = points[0]["timestamp"]

    for point in points[1:]:
        ts, value = point["timestamp"], point["value"]
        if ts - prev_ts > gap_tolerance:
            run_start_ts = ts
            prev_ts = ts
            continue
        if is_breach(value):
            current = (ts - run_start_ts) // ts_divisor
            max_sustained = max(max_sustained, current)
        else:
            run_start_ts = ts
        prev_ts = ts

    return max_sustained

def evaluate(time_series: List[Dict], threshold: float, duration: int, compare: str) -> Dict:
    if not time_series:
        return {"status": "no_data", "message": "无数据"}
    sustained = calc_sustained_seconds(time_series, threshold, compare)
    status = "find_problem" if sustained >= duration else "pass"
    return {
        "status": status,
        "threshold": threshold,
        "duration_required": duration,
        "duration_sustained": sustained,
        "compare": compare,
    }

def main():
    parser = argparse.ArgumentParser(description="阈值+持续时间判断脚本")
    parser.add_argument("--threshold", type=float, required=True)
    parser.add_argument("--duration", type=int, required=True)
    parser.add_argument("--compare", choices=["gt", "gte", "lt", "lte"], default="gt")
    parser.add_argument("--input", help="输入 JSON 文件路径(默认 stdin)")
    args = parser.parse_args()
    time_series = json.load(open(args.input) if args.input else sys.stdin)
    result = evaluate(time_series, args.threshold, args.duration, args.compare)
    print(json.dumps(result, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()

样例 4:变更前后 Diff

项 内容
正例 脚本计算差值: python3 diff.py --before before.json --after after.json
反例 让模型对比两份报告,口头说「CPU 使用率有所上升」。跨次回归对比时无法量化波动,告警阈值无法用脚本回放验证。
期望输出 结构化 diff 结果: {"metric": "rds_cpu_usage", "baseline": 45.0, "current": 57.5, "delta_absolute": 12.5, "delta_percent": 27.8, "direction": "up"}
关键差异 正例 diff 可复跑可回归,反例无法量化。

参考实现(聚合结果两两比对,纯函数):

查看脚本

python

#!/usr/bin/env python3
"""Diff 计算脚本模板:两次聚合结果两两比对,纯函数。"""

import argparse
import json
import sys
from typing import Dict

def calc_change(before: float, after: float) -> Dict:
    absolute = after - before
    relative = (absolute / before * 100) if before != 0 else 0.0
    return {
        "before": before,
        "after": after,
        "absolute_change": round(absolute, 2),
        "relative_change_percent": round(relative, 2),
    }

def diff_aggregations(before: Dict, after: Dict) -> Dict:
    keys = set(before.keys()) & set(after.keys())
    keys.discard("metric")
    changes = {}
    for key in keys:
        if isinstance(before[key], (int, float)) and isinstance(after[key], (int, float)):
            changes[key] = calc_change(before[key], after[key])
    return changes

def main():
    parser = argparse.ArgumentParser(description="Diff 计算脚本")
    parser.add_argument("--before", required=True)
    parser.add_argument("--after", required=True)
    parser.add_argument("--metric", required=True)
    args = parser.parse_args()
    before_data = json.load(open(args.before))
    after_data = json.load(open(args.after))
    if args.metric not in before_data or args.metric not in after_data:
        print(json.dumps({"error": f"metric '{args.metric}' not found in both files"}))
        sys.exit(1)
    changes = diff_aggregations(before_data[args.metric], after_data[args.metric])
    print(json.dumps({"metric": args.metric, "changes": changes}, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()

样例 5:输出标准化

项 内容
正例 顶层固定 schema: {"total_cases": 7, "passed": 5, "find_problem_cases": 1, "no_problem_found": 0, "errors": 1, "has_find_problem": true, "results": [...]} status 枚举只有 4 个值:pass / find_problem / no_problem_found / error。
反例 每次输出格式不同,有时返回 Markdown,有时返回自然语言,字段名跨次漂移。下游解析脚本会因字段缺失或键名漂移直接报错,跨次趋势对比也无法做。
期望输出 单项结果 schema 含必填字段: {"case_id": "rds_cpu_high", "item": "RDS CPU 使用率过高", "severity": "P1", "status": "find_problem", "duration_seconds": 360, "total_entities": 12, "abnormal_count": 1, "abnormal_resources": [...]}
关键差异 正例结构与字段名跨次稳定,反例结构跨次漂移导致下游解析失败。

进阶要素

架构模式:数据驱动声明 + 公共引擎

以巡检类 Skill 为例,脚本文件的分工:

文件 职责 计算逻辑
{skill}_common.py 公共引擎 查询、解析、评估、格式化、聚合
{skill}-core-inspection.py 业务声明 若干 InspectionCase 数据项,零计算逻辑
{skill}-performance-inspection.py 业务声明 若干 InspectionCase 数据项,零计算逻辑
{skill}-security-inspection.py 业务声明 若干 InspectionCase 数据项,零计算逻辑
{skill}-logs-inspection.py 业务声明 若干 InspectionCase 数据项,零计算逻辑(SLS SQL 日志查询)

新增巡检项 = 新增一个 InspectionCase 数据项,不写新的计算代码。

确定性验证方式

同参数执行两次,diff 必须无差异:

bash

python3 core-inspection.py --region <region> --project <project> --metricstore <metricstore> --time-range last_1h > /tmp/run1.json
python3 core-inspection.py --region <region> --project <project> --metricstore <metricstore> --time-range last_1h > /tmp/run2.json
diff /tmp/run1.json /tmp/run2.json

任何 diff 输出都意味着脚本未达成纯函数保证,需要排查随机数、当前时间依赖或全局状态。

常见问题

哪些计算不需要脚本化?

模型推理(根因假设排序、跨域关联、经验综合)不需要脚本化。脚本化只针对数值计算:单位换算、聚合、阈值判断、Diff。

公共引擎和业务脚本的分界线在哪?

公共引擎承载「怎么算」(查询、解析、评估、格式化),业务脚本承载「算什么」(巡检项配置、阈值、单位)。判定标准:业务脚本里不出现 if/else 数值判断逻辑。

纯函数为什么禁用「当前时间依赖」?

datetime.now() 等当前时间调用会导致同输入跨次输出不一致,破坏可回放性。需要时间窗口时通过 CLI 参数(如 --time-range)显式传入。

业务脚本能否调用三方库?

可以,但必须是确定性库(同输入同输出)。避免依赖随机数、机器学习推理类库;调用日志检索或指标查询等远端服务时,参数固定即输出固定的接口可以使用。

相关入口

  • 返回 STAROps 最佳实践首页
  • 打开 STAROps Playground
  • 进入 STAROps 控制台

文章大纲