分析耗时、Token 与运行成本
用单调时钟和尝试账本统计耗时与用量,区分首输出、并行、重试和缺失数据。
分析耗时、Token 与运行成本
任务用了十秒,模型请求只用了两秒,剩下的时间可能花在排队、工具、事件处理或界面上。只有一个总耗时,无法判断该优化哪里;只有 Token 总数,也不能判断重复请求是否被漏算。
这篇建立两个独立口径:时间按操作边界测量,用量按实际尝试记录。前者不能用日历时钟相减代替,后者不能反复累加同一次请求的累计值。
本系列的独立示例位于仓库 columns/coding-agent/examples/debugging-observability/,固定使用 pi-agent-core、pi-ai、pi-schema、pi-telemetry 1.5.0。先在该目录执行 npm install,下文命令也从该目录运行。示例不需要模型密钥,不读取个人配置。
先写清起点与终点
使用 performance.now() 测单进程经过时间,墙上时间用于检索“何时发生”,两者职责不同。并行工具的耗时之和可能大于任务总耗时,不能直接相加得出关键路径。
示例 mock 直接产生最终消息,没有真实流式首字,故不输出伪造的 TTFT。需要验证首文本时应注入 text_delta 序列或真实 Provider 流,并记录无文本响应时的缺失值。
同一次尝试保存最终值
src/metrics.ts 的账本按 attempt ID 覆盖更新,同一次尝试的累计 usage 只保留最终快照。
export interface Attempt {
id: string
durationMs: number
input?: number
output?: number
estimatedCost?: number
}
export class UsageLedger {
private readonly attempts = new Map<string, Attempt>()
constructor(private readonly limit = 100) {}
add(attempt: Attempt) {
if (!Number.isSafeInteger(this.limit) || this.limit < 1) {
throw new Error("limit 必须是正整数")
}
for (const value of [
attempt.durationMs,
attempt.input,
attempt.output,
attempt.estimatedCost,
]) {
if (value !== undefined && (!Number.isFinite(value) || value < 0)) {
throw new Error("指标必须是非负有限数")
}
}
if (!this.attempts.has(attempt.id) && this.attempts.size >= this.limit) {
throw new Error("尝试数量超过上限")
}
// 同一次尝试用最终值覆盖,避免累计 usage 更新被反复相加。
this.attempts.set(attempt.id, { ...attempt })
}
summary() {
const rows = [...this.attempts.values()]
const total = (key: "input" | "output" | "estimatedCost") => {
const values = rows.map((row) => row[key])
return {
known: values.reduce<number>((sum, value) => sum + (value ?? 0), 0),
missing: values.filter((value) => value === undefined).length,
}
}
return {
attempts: rows.length,
input: total("input"),
output: total("output"),
estimatedCost: total("estimatedCost"),
}
}
}
export async function timed<T>(operation: () => Promise<T>, clock = () => performance.now()) {
const started = clock()
const value = await operation()
return { value, durationMs: Math.max(0, clock() - started) }
}假设一次请求先报告 input=10,最终修正为 12,应计 12 而不是 22。另一次重试 input=2,逻辑任务可报告已知输入合计 14。测试还验证 output=0 和未返回 output 是两种情况。
这里的 timed 只返回成功操作的耗时,异常按原值继续抛出。若要记录失败耗时,需要在调用边界的 finally 中更新相同尝试记录,并保证遥测写入失败不覆盖原异常;不能把本函数当作已经完整实现的生产性能中间件。
缺失与零值不能合并
账本返回 known 和 missing。所有缺失都换成 0 会制造“这个错误请求没有成本”的错觉;Provider 在失败时可能没有返回用量,但服务端仍可能已经计费。
模拟流的 usage 固定为零,仅用于构造合法测试消息,不代表真实模型免费。真实数据应记录来源和模型计价版本,estimatedCost 是估算,不等于账单。图像、缓存、工具子调用和不同价格层也不能只用一个通用单价推导。
防止重试和子调用重复统计
统计请求尝试要给每次重试分配新 ID,任务聚合再把尝试关联起来。若宿主已经汇总了工具子调用用量,再将相同子调用逐项相加就会重复;先定义哪一层是权威明细,再决定是否采用宿主汇总。
缓存 read/write token 也是报告值,不能没有分母就称为缓存命中率。比较两个版本时应使用同一类任务和相同采集口径,同时观察最终质量;更快或更少 Token 未必意味着任务完成得更好。
一次慢请求与运行分布
本地 demo 的 monitor 使用整个应用等待区间,包含工具和事件处理。它只是一次运行的样本,没有负载、网络和远程模型,不能作为性能基准。
拿到多次实际样本后再看分位数、错误与重试分组。小样本 p95 可能就是最大值,应一起展示样本量、窗口和采集遗漏;下一篇将这些规则落实到最小运行监控。
依据与版本
本地官方快照为 8efd314fa,相关包标记 1.3.0;配套代码另按已发布的 1.5.0 类型和运行行为核验。主分支文档持续变化,不能混作同一版本。