返回 AICAP-180
B9 · Day 87部署 + 韧性 + 可观测 + 独立红队

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.tssummarizeLatency(延迟分布)与 pairedBootstrap(模型对比的 delta CI)是两套独立工具——前者描述单一系统的延迟形状,后者比较两个变体的质量差。今天只用前者。dashboard ≠ 告警:今天画的是「概览快照」,把分位接成阈值告警(p95 > X 触发)是后续 SLO 工作。

2. 代码走读:stats.ts + dashboard.ts

src/agent/eval/stats.tssrc/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,还行」。
  • p50idx = (50/100)·(10-1) = 4.5lo=4, hi=5, w=0.5160·0.5 + 170·0.5 = 165 ms(典型体验,比 mean 小一半)。
  • p95idx = (95/100)·9 = 8.55lo=8, hi=9, w=0.55200·0.45 + 2000·0.55 = 90 + 1100 = 1190 ms(SLO 线,被那个离群点拉爆)。
  • p99idx = 0.99·9 = 8.91w=0.91200·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. 今日实战

  1. 采样:对 Day 86 的公网 URL 跑 50+ 次混合请求(不同 task、不同输入长度),每次用 performance.now() 记开始/结束,收集线上 trace 与延迟数组。
  2. 算分位:把延迟数组喂 summarizeLatencysrc/agent/eval/stats.ts),出 {n, p50, p95, p99, mean}
  3. 建面板:把这批 run 的 eval report 喂 buildDashboardsrc/agent/eval/dashboard.ts),得到指标树快照(NSM + 叶子),fs 读用 scripts/build-dashboard.ts
  4. 画图取证:把延迟分位 + 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 红队方法(从「观测自己」转向「攻击自己」,独立红队登场)