建立最小运行监控与诊断导出
将单次调试汇总成有界运行窗口,明确错误率、分位数、告警门槛和诊断数据边界。
建立最小运行监控与诊断导出
单次调试回答“这次为什么失败”,监控回答“类似失败是否正在增多”。两者需要共享身份和分类,却不能把所有事件原封不动地堆进一个全局数组。
这篇先做一个固定容量的运行窗口,再导出必要诊断摘要。示例不部署监控平台,也不会真的发送告警;它让每个统计值的分母与限制可检查。
本系列的独立示例位于仓库 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 判断结果,不从事件尾部重新扫描,避免早期错误被容量限制丢弃后变成成功。
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,
),
)有界窗口与告警门槛
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,或明确展示抽样口径。当前示例无采样,不宣称解决了分布式计数一致性。
接真实后端前补齐哪些部分
Pi telemetry 的通用契约允许应用提供后端适配器,但不是内置的生产 Collector。适配器可接 OpenTelemetry 或其他后端,业务仍需自己的采集政策、导出预算与验证。
运行 npm test 可验证窗口淘汰、缺失值、分位数、样本门槛和诊断字段。它不证明线上告警送达、跨进程关联或真实成本准确。先让本地数字能解释,再把同一口径接到生产后端。
依据与版本
本地官方快照为 8efd314fa,相关包标记 1.3.0;配套代码另按已发布的 1.5.0 类型和运行行为核验。主分支文档持续变化,不能混作同一版本。