trace 量产 + 仪表盘
昨天(Day 86)拿到公网 URL 并确认了「1 条」线上 trace 入库——但一条 trace 只能证明链路通,不能支撑任何容量/成本决策。今天的任务是量产 trace 并聚合成分布:对线上 URL 跑 50+ 次混合请求,把延迟样本喂进已建好的 summarizeLatency 算 p50/p95/p99,再用 dashboard.ts 把延迟/token/cost 切片画成一张概览图。在
阶段: B9 · 部署 + 韧性 + 可观测 + 独立红队(Day 81-90) 标签: #latency-percentiles #p95 #dashboard #tail-latency
今日导引(由浅入深)
昨天(Day 86)拿到公网 URL 并确认了「1 条」线上 trace 入库——但一条 trace 只能证明链路通,不能支撑任何容量/成本决策。今天的任务是量产 trace 并聚合成分布:对线上 URL 跑 50+ 次混合请求,把延迟样本喂进已建好的 summarizeLatency 算 p50/p95/p99,再用 dashboard.ts 把延迟/token/cost 切片画成一张概览图。在 B1→B18 曲线上,这是 P5 可观测从「单点埋点」走向「分布级指标」的一步,也直接为 B10(Day 91 起的延迟/吞吐指标体系 M4)铺路。今天的最小可判定产出:50+ 条线上 trace + 1 张 p95/cost dashboard 图——目前是「待云」(需公网 URL 跑量),但分位统计与面板逻辑代码已 ✅ 建好。
1. 机理精读
为什么是 p95,而不是 mean。 延迟分布几乎总是右偏长尾——大部分请求快,少数请求因 cold start、队列排队、上游抖动、长输出而极慢。mean 会被海量快请求拉低,掩盖最差用户体验;而真正决定用户「这服务卡不卡」的,是那 5% 最慢请求。p95 = 「95% 的请求比这快」,是服务 SLO 的主线口径;p99 进一步刻画极端尾部(常被「最重的客户」命中)。所以可观测的第一性原理是:报分位,不报均值。
p50/p95/p99 三件套各自回答不同问题。 p50(中位数)是「典型体验」;p95 是「SLO 承诺线」(多数 SLA 写成 p95 < X ms);p99 是「最坏可接受边界」(容量规划与告警阈值常挂 p99)。三者一起才构成完整的尾延迟画像——只看 p50 会对尾部盲目,只看 p99 会被偶发离群点过度惊吓。
分位是「分布的切片」,dashboard 是「多维切片的拼图」。 单维分位(按延迟)只是开始;生产 dashboard 要按 token、cost/run 等维度同时切片,才能支撑「容量×成本」联合决策。比如 p95 延迟高但 cost/run 低 → 可能是输出长、可优化采样;cost/run 高但延迟低 → 模型偏贵、考虑降级路由。dashboard.ts 的设计是把多条 eval report 聚合成一棵「North Star Metric + 叶子指标」的指标树——NSM 是 task autonomous completion,叶子含 partial-credit、cost/run、unknown rate、judge-human κ。
关键权衡:采样量 vs 统计可信度。 p95 要稳,样本不能太少——N=10 时 p95 几乎就是「第 10 个样本」,噪声极大。今天定 50+ 次混合请求正是为了让分位有最低限度的统计意义;但 50 对 p99 仍偏少(p99 需要 ~100+ 才稳),这是诚实的局限,dashboard 上要标注 N。
与相邻概念的边界。 分位统计 ≠ A/B 检验:stats.ts 里 summarizeLatency(延迟分布)与 pairedBootstrap(模型对比的 delta CI)是两套独立工具——前者描述单一系统的延迟形状,后者比较两个变体的质量差。今天只用前者。dashboard ≠ 告警:今天画的是「概览快照」,把分位接成阈值告警(p95 > X 触发)是后续 SLO 工作。
2. 代码走读:stats.ts + dashboard.ts
读 src/agent/eval/stats.ts 与 src/agent/eval/dashboard.ts:
percentile(sorted, p)(stats.ts:17):对已排序数组做线性插值分位——idx = (p/100)·(n-1),取floor/ceil两侧按权重w = idx - lo线性插值。单元素直接返回,空数组返回 NaN。这是「插值型」分位(非「最近秩」),对小样本更平滑。summarizeLatency(samplesMs)(stats.ts:76):先[...samplesMs].sort()(不可变排序),返回{ n, p50, p95, p99, mean }。注释直白写「p95/p99 surface the tail that the mean hides」——正是今天的机理核心。mean/sd(stats.ts:5/10):标准均值与样本标准差(n-1 分母,n<2 时 sd=0),供分布描述用。buildDashboard(reports)(dashboard.ts:36):把一组 eval report 聚合成DashboardSnapshot——runs(report 数)、totalTasks(各 report 的 n 求和)、nsm(task autonomous completion = completionRate 均值)、leaves(partial-credit mean / code-check pass / unknown rate / cost USD/run / judge-human kappa 的均值)。- 空报告守卫(dashboard.ts:37-45):
reports.length === 0时返回value: null的 NSM 并带 note「no eval reports yet — run `pnpm eval:agent`(needs an API key)」——诚实地把空状态画成空,而非编 0。 - 纯函数边界(dashboard.ts:3-4 注释):
buildDashboard只接收 reports,fs 读取放在scripts/build-dashboard.ts,保持本模块 bundler-safe / 无副作用。
手算:percentile 的插值分位(看 mean 如何骗人)
取一个右偏的延迟样本(ms,已排序,n=10):
[120, 130, 140, 150, 160, 170, 180, 190, 200, 2000](最后一个是 cold start 离群点)。
- mean = (120+130+…+200+2000)/10 = 3440/10 = 344 ms——看着「平均不到 350ms,还行」。
- p50:
idx = (50/100)·(10-1) = 4.5,lo=4, hi=5, w=0.5→160·0.5 + 170·0.5= 165 ms(典型体验,比 mean 小一半)。 - p95:
idx = (95/100)·9 = 8.55,lo=8, hi=9, w=0.55→200·0.45 + 2000·0.55=90 + 1100= 1190 ms(SLO 线,被那个离群点拉爆)。 - p99:
idx = 0.99·9 = 8.91,w=0.91→200·0.09 + 2000·0.91=18 + 1820= 1838 ms。
结论一目了然:mean=344ms 把「最慢 5% 用户等了 1.2 秒」完全藏住了。这正是 summarizeLatency 返回 p95/p99 而非只返回 mean 的理由——也是 percentile 用线性插值(而非「取第 ⌈p·n⌉ 个」)让小样本分位更平滑的体现。注意这里 n=10 时 p99 几乎就是「第 10 个样本本身」,说明小样本的 p99 极不稳——这是今天要采 50+ 而非 10 的直接动机。
3. 今日实战
- 采样:对 Day 86 的公网 URL 跑 50+ 次混合请求(不同 task、不同输入长度),每次用
performance.now()记开始/结束,收集线上 trace 与延迟数组。 - 算分位:把延迟数组喂
summarizeLatency(src/agent/eval/stats.ts),出{n, p50, p95, p99, mean}。 - 建面板:把这批 run 的 eval report 喂
buildDashboard(src/agent/eval/dashboard.ts),得到指标树快照(NSM + 叶子),fs 读用scripts/build-dashboard.ts。 - 画图取证:把延迟分位 + cost/run 渲成一张 p95/cost dashboard 图,标注 N=50+ 与 cost 锚点。
4. 今日实测 / 产出
summarizeLatency+dashboard.ts:已建(✅)(stats.ts:76 返回 p50/p95/p99/mean;dashboard.ts:36 指标树)。- 50+ 线上 trace + 延迟/成本图:待云(需公网 URL 跑量)。
- 可锚定的真实成本:$0.0139/run(V4-Flash)。
- 目标产出:50+ trace + 1 张 p95/cost dashboard 图。
诚实状态:分位统计与面板逻辑是 ✅ 已建的确定性代码;50+ 线上 trace 与图仍是「待云」,cost $0.0139/run 是既有实测、不臆造。
5. 常见误区 / 陷阱
- 只报均值掩盖尾延迟:mean 看着漂亮、p95 惨不忍睹是常态。SLO 必须挂分位,不挂均值。
- 样本太少就报 p99:N=50 对 p95 勉强、对 p99 偏少(需 ~100+)。报分位必须连 N 一起报,否则数字是噪声。
- 把空 dashboard 编成 0:没有 eval report 时若强行渲成
0%会误导决策;buildDashboard的空守卫返回null+ note 才是诚实做法。 - 用「最近秩」分位对小样本下结论:插值型分位(本仓实现)对小样本更平滑,但任何分位在小 N 下都不稳——别拿 50 个样本的 p99 当承诺线。
6. 学习资源(每条带 YYYY-MM)
- OpenTelemetry GenAI semantic conventions(2026-01)— gen_ai.* span 属性、延迟/成本维度
- vLLM PagedAttention 博客(尾延迟视角,2026 当周重验)— p95/p99 为何是服务主线指标
- Gil Tene «How NOT to Measure Latency»(经典演讲,2015-11;分位与协调遗漏,长期有效)
- Google SRE Book — Latency & SLO 章节(2016-03;p95/p99 与 SLO 制定原则,仍权威)
7. 指标树速览(dashboard 输出形状)
DashboardSnapshot
├─ runs = report 数
├─ totalTasks = Σ report.n
├─ nsm (North Star) = task autonomous completion(completionRate 均值)
└─ leaves
├─ partial-credit mean
├─ code-check pass (rate)
├─ unknown rate (rate)
├─ cost (USD/run) ← 锚点 $0.0139/run (V4-Flash)
└─ judge-human kappa ← 仍待 ≥50 手标,可能为 null
- NSM 单一、叶子多维:一个北极星(自主完成率)+ 一组叶子(质量/成本/校准),避免「指标过载」又不丢关键维度。
- 空状态诚实:无 report 时 NSM
value=null+ note 引导跑pnpm eval:agent,绝不编 0。 - κ 可能为 null:judge-human κ 需 ≥50 手标,未标时是 null(不是 0)——dashboard 如实留空。
cost/run 这一格怎么来的
dashboard 的 cost (USD/run) 叶子取各 report totalCostUsd 的均值。今天能锚定的真实成本是 $0.0139/run(V4-Flash)——它来自 token 计费:把 tokenizer 数出的 input/output token 乘以 V4-Flash 单价。这条成本叶子和延迟分位并列,正是「容量×成本」联合决策的来源:
- p95 高 + cost 低 → 输出长/采样可优化,先压延迟。
- p95 低 + cost 高 → 模型偏贵,考虑 fallback 到更便宜变体(Day 84 的 Pro→Flash 降级,质量代价 89.7%→79.3%)。
这把 Day 84 的「降级换可用性」和今天的「成本可观测」接上:dashboard 让「该不该降级」从拍脑袋变成看图。
8. 面试自测(30 秒口径)
- 为什么报 p95 不报 mean? 延迟右偏长尾,mean 被快请求拉低、掩盖最差 5% 体验;p95 才是 SLO 承诺线。
- p50/p95/p99 各答什么? 典型体验 / SLO 线 / 最坏可接受边界。
- 50 个样本够算 p99 吗? 不够——p99 需 ~100+ 才稳;报分位必须连 N 一起报。
- dashboard 没数据时画什么? 画 null + 引导跑 eval 的 note,不编 0。
SOTA检查 (2026-06 更新)
- 主流方案:p95/p99 尾延迟为服务 SLO 主线指标,2026 无过时——任何 LLM serving 容量/成本决策都以分位切片为底。
- 须复验:vLLM/SGLang 连续批处理对比文需执行当周重验版本(vLLM V1 引擎 / SGLang RadixAttention 的当周版本号);OTel gen_ai.* 部分属性名(cost vs usage.cost)仍在 stabilizing,对照最新 spec。
- 过时黑名单:避免只报均值掩盖尾延迟;避免用过少样本对 p99 下结论;避免假设 Langfuse/Phoenix SDK 版本(pin 当周)。
- 下次复查点:B10(Day 91 起 M4 延迟/吞吐体系)执行当周,重验 vLLM/SGLang 版本与 TTFT/TPOT 口径。
衔接
- 昨天:Day 86 — 部署上线 + 公网可观测(拿到公网 URL + 1 条线上 trace)
- 今天:量产 50+ trace,喂
summarizeLatency出 p50/p95/p99,dashboard.ts画 p95/cost 概览 - 明天:Day 88 — OWASP LLM Top-10 红队方法(从「观测自己」转向「攻击自己」,独立红队登场)