跳到主内容
创建时间:最后更新:作者:白川

建立最小运行监控与诊断导出

将单次调试汇总成有界运行窗口,明确错误率、分位数、告警门槛和诊断数据边界。

建立最小运行监控与诊断导出

单次调试回答“这次为什么失败”,监控回答“类似失败是否正在增多”。两者需要共享身份和分类,却不能把所有事件原封不动地堆进一个全局数组。

这篇先做一个固定容量的运行窗口,再导出必要诊断摘要。示例不部署监控平台,也不会真的发送告警;它让每个统计值的分母与限制可检查。

本系列的独立示例位于仓库 columns/coding-agent/examples/debugging-observability/,固定使用 pi-agent-core、pi-ai、pi-schema、pi-telemetry 1.5.0。先在该目录执行 npm install,下文命令也从该目录运行。示例不需要模型密钥,不读取个人配置。

先定义一次运行的结果

本例把运行内出现工具错误记为 error,即使第二轮模型仍能正常结束;主动取消另记 cancelled。实际产品也可能允许工具重试恢复后按任务成功统计,但必须同时保留尝试失败指标,不能两种口径混用。

src/demo.ts 从聚合的 toolErrors 判断结果,不从事件尾部重新扫描,避免早期错误被容量限制丢弃后变成成功。

ts
import { timed } from "./metrics.js"
import { runMock } from "./mock-agent.js"
import { Monitor } from "./monitor.js"

const result = await timed(() => runMock(process.argv.includes("--fail-tool")))
const monitor = new Monitor()
const errors = result.value.timeline.toolErrors > 0
monitor.add({ outcome: errors ? "error" : "ok", durationMs: result.durationMs })
// 独立脚本自己的 stdout;不在 Pi 的 JSON/RPC stdout 内打印。
console.log(
  JSON.stringify(
    {
      ...result.value.timeline,
      requests: result.value.requests,
      toolExecutions: result.value.toolExecutions,
      monitor: monitor.summary(),
    },
    null,
    2,
  ),
)

有界窗口与告警门槛

ts
export interface RunMetric {
  outcome: "ok" | "error" | "cancelled"
  durationMs: number
}

export class Monitor {
  private readonly runs: RunMetric[] = []
  dropped = 0
  constructor(private readonly capacity = 100) {
    if (!Number.isSafeInteger(capacity) || capacity < 1) {
      throw new Error("capacity 必须是正整数")
    }
  }
  add(run: RunMetric) {
    if (
      !["ok", "error", "cancelled"].includes(run.outcome) ||
      !Number.isFinite(run.durationMs) ||
      run.durationMs < 0
    ) {
      throw new Error("无效运行指标")
    }
    this.runs.push({ ...run })
    if (this.runs.length > this.capacity) {
      this.runs.shift()
      this.dropped += 1
    }
  }
  summary(minSamples = 20, errorThreshold = 0.2) {
    if (
      !Number.isSafeInteger(minSamples) ||
      minSamples < 1 ||
      errorThreshold < 0 ||
      errorThreshold > 1 ||
      !Number.isFinite(errorThreshold)
    ) {
      throw new Error("告警配置无效")
    }
    const samples = this.runs.length
    const errors = this.runs.filter((run) => run.outcome === "error").length
    const cancelled = this.runs.filter((run) => run.outcome === "cancelled").length
    const durations = this.runs.map((run) => run.durationMs).sort((a, b) => a - b)
    const errorRate = samples === 0 ? undefined : errors / samples
    return {
      window: "last-N-runs",
      samples,
      errors,
      cancelled,
      dropped: this.dropped,
      errorRate,
      cancelRate: samples === 0 ? undefined : cancelled / samples,
      p95Ms: samples === 0 ? undefined : durations[Math.ceil(samples * 0.95) - 1],
      alert: samples >= minSamples && errorRate !== undefined && errorRate >= errorThreshold,
    }
  }
}

窗口是最近 N 次运行,不是最近五分钟。满后移除最早一条,dropped 表示被窗口淘汰的数量,不表示网络 exporter 丢包。要改成时间窗口,需要加入可信时间、过期策略及窗口样本缺失处理。

errorRate 的分母包含成功、失败与取消;cancelRate 另列。若团队选择排除取消,应调整并明确口径。p95 使用 nearest-rank,空窗口为缺失,样本不足时不触发阈值告警。默认 20 个样本和 20% 是教学配置,不是生产推荐阈值。

一份够用的诊断包

当前 demo 只导出固定结构:事件类型、序号、相对时间、调用 ID、计数和请求摘要。原始 messages 虽然存在于测试返回值中,但不进入 demo 输出。文本投影也只保留在本地内存,不附在诊断包中。

调用 ID、工具名和 runId 仍需要按数据政策处理:生产系统应使用宿主产生的不可猜测身份、已登记名称或映射值,避免把用户输入当字段内容。哈希不自动等于匿名化,字段白名单也不能阻止允许字段里被塞入秘密。

OpenTelemetry 的敏感数据处理建议强调从源头控制采集与后续处理。原始请求捕获应显式开启、限制范围和保留时间;默认诊断不复制 prompt、工具参数、源码、Authorization 或服务端错误全文。

指标标签与关联身份分开

工具类别、结果分类、固定 Provider 等适合有限集合分组。sessionId、runId、完整文件路径和任意错误文本会制造高基数,不适合作为每条时序指标的标签;用它们查询 trace 或日志,并在需要时关联到诊断包。

若只保留采样后的运行再计算错误率,采样偏向失败就会扭曲分母。可以让聚合指标先计数,再按策略保留部分详细 trace,或明确展示抽样口径。当前示例无采样,不宣称解决了分布式计数一致性。

接真实后端前补齐哪些部分

部分最小需要的行为当前示例
缓冲
容量、丢弃与错误计数
只有本地事件尾部和运行窗口
导出
异步写入、超时、后端失败隔离
独立脚本一次 JSON 输出
退出
限时 flush 与未送达计数
没有后台 exporter
告警
窗口、最小样本、阈值、去重与恢复
只计算 alert 布尔值
保留
数据分级、访问范围、到期清理
不创建持久化日志

Pi telemetry 的通用契约允许应用提供后端适配器,但不是内置的生产 Collector。适配器可接 OpenTelemetry 或其他后端,业务仍需自己的采集政策、导出预算与验证。

运行 npm test 可验证窗口淘汰、缺失值、分位数、样本门槛和诊断字段。它不证明线上告警送达、跨进程关联或真实成本准确。先让本地数字能解释,再把同一口径接到生产后端。

依据与版本

本地官方快照为 8efd314fa,相关包标记 1.3.0;配套代码另按已发布的 1.5.0 类型和运行行为核验。主分支文档持续变化,不能混作同一版本。