调度开销 —— 滚动 p95: 11ms. 数字为示意性建模结果,非实测。

先打分,再落档,接不住就抬。

三件事决定一条请求去哪:出门前算出来的难度指纹、在做决定那一刻就生效的路由预算、以及接住首选档判断失误的降级阶梯。这一页写的是你判断「这东西是不是真能工作」所需要的细节。

这里没有任何「自适应」式的含糊。页面上每个阈值都是你设的数,跟着它所属的阶梯一起做版本,并且在每次决策时原样回给你。

第 01 步 — 难度指纹

10 毫秒,五个维度,一个分数。

请求离开你的应用之前,一个自托管的小分类器在五个维度上给它打分——推理深度、上下文跨度、格式严格度、事实敏感度、拒答风险,各 0–100。五个维度合成一个难度分 D。

D 是路由器唯一的输入。不看用户是谁、不看他买的哪档套餐、不看他昨天问过什么。只看这一条请求本身,所以同一个问题永远得到同样的对待,事后也解释得清。

难度 D

D = 0.34R + 0.22C + 0.14F + 0.20K + 0.10X

权重和为 1.00。每个部署固定一套,跟着阶梯一起做版本。

五个维度

五个维度
维度量什么权重
R推理深度0.34
C上下文跨度0.22
F格式严格度0.14
K事实敏感度0.20
X拒答风险0.10

四个难度带

D 切成四个带。阶梯是写在带上的,不是写在原始分上——一个能在配置文件里读出来的阈值,比一个谁也想象不出来的小数好用。

四个难度带
难度带区间流量份额
D1 · 例行D < 2542%
D2 · 常规25–5534%
D3 · 复杂55–8019%
D4 · 高风险长链D ≥ 805%
三条请求,打完分FIG-03示意数据
五个权重固定,加起来是一打分耗时p95 9 msR推理深度w 0.34C上下文跨度w 0.22F格式严格度w 0.14K事实敏感度w 0.20X拒答风险w 0.10把这段话翻成英文R12C8F20K15X5难度 D12难度带D1例行落到S-1把这份 PDF 里的表格抽成 JSON带 schema 校验R34C62F96K55X12难度 D51难度带D2常规落到M-2根据这三份合同,我们能不能单方面终止R78C86F45K92X61难度 D76难度带D3复杂落到L-3
示意模型 · FIG-03 · 每条请求五个维度,权重如图;D 是加权和

分类器本身

一个 60M 参数的编码器,你自己跑。它不外呼,所以指纹只花一次本地推理,请求正文也不用为了打分而先离开你这边。

分类器本身
参数值
规模60M 编码器,自托管
微调集1,200,000 条打标请求
延迟(p50 / p95 / p99)5 / 9 / 14 毫秒
每次调用成本$0.00003
与人工标注一致性(κ)0.71
分档准确率(按流量加权)84.1%
它在哪判错按行归一化,各真实带内的流量占比 %FIG-03B示意数据
预测带真实带D1D2D3D4D191.27.90.80.1D29.479.810.10.7D31.114.677.27.1D40.22.418.379.1按行归一化到 100分带准确率(按流量加权)84.1%判低了 — 这些由阶梯兜住7.2%判高了 — 这就是多花的钱8.7%判高了 — 这就是多花的钱$0.439美元 / 千次11% 占调度后账单
示意模型 · FIG-03B · 按行归一化,各真实带内的流量占比 %

大约每六条判错一条。这不是脚注里的免责,这正是阶梯存在的理由。

欠路由
判得比实际低一档以上。这部分由阶梯接住——它是升级的主要来源。
过路由
判得比实际高一档以上。这部分没人接:钱白花了,约占账单 11%。

把阈值调激进,过路由会缩,升级率和延迟尾巴会涨。没有哪一档设置能让两边同时变好。这个旋钮在你手里,按端点设。

第 02 步 — 路由预算

预算不是报表,是约束。

每个端点声明一个「每千次调用多少美元」的上限。它在选路那一刻就生效,不是月底对账时才算——余量不够时,整条阶梯当天下压一级,而不是先超支再给你发邮件。

预算和看板的区别就在这儿。看板告诉你发生了什么,这个东西改变将要发生什么。

各端点的预算与实测

各端点的预算与实测
端点质量下限预算实测余量
/chat裁判分 p10 ≥ 74$4.20$2.8133%
/docqa裁判分 p10 ≥ 78$5.50$4.2223%
/extract结构通过率 ≥ 99.0%$4.80$3.6823%
/review裁判分 p10 ≥ 84$9.60$8.6510%

在这个示意部署里预算是硬上限,四个端点都跑在各自的上限以内。

决策树占全量流量的百分点FIG-04示意数据
第一个判断语义缓存命中?cos ≥ 0.94 && same tenant && same prompt_versionD142 pp例行D234 pp常规D319 pp复杂D45 ppC-08S-130M-24S-18M-222L-34M-25L-313L-3R1L-32L-3R30102030405060708090100占全量流量的百分点Σ 100 pp路径档位ppD1缓存命中C-08D1D1 → 预算充足S-130D1D1 → 事实敏感度 K ≥ 70M-24D2D2 → 格式严格度 F < 80S-18D2D2 → 默认M-222D2D2 → K ≥ 85 或 F ≥ 95L-34路径档位ppD3D3 → 预算余量 < 20%M-25D3D3 → 默认L-313D3D3 → R ≥ 85L-3R1D4D4 → 预算余量 < 20%L-32D4D4 → 默认L-3R3
示意模型 · FIG-04 · 叶子份额为占全量流量的百分点,加起来等于 100

路由器是一棵树,不是一个模型。每条请求都带着它走过的路径和每条边上的条件,也就是说,六周之后你还能在工单里跟它吵架。

缓存命中也算一个叶子,因为它同样是一次路由决策。

四条旁路规则

这四条压过决策树。它们存在的原因是:有些失败不值得聪明。

  • 格式严格度 ≥ 80,强制走带 schema 校验的档,校验不过即升级。
  • 事实敏感度 ≥ 70,直接禁用小型通用档。
  • 路由预算余量 < 20%,整条阶梯下压一级,直到自然日结束。
  • 拒答风险 ≥ 70,并行跑两档,取先到且过下限的那个。

第 03 步 — 降级阶梯

出问题要横着倒,不要竖着塌。

一条阶梯是 3–5 级有序候选链。每一级是「模型档 × 参数 × 提示版本」,带触发条件和最大等待。同一条阶梯两个方向都走:首选档不够就往上抬,预算或上游顶不住就往下压。

只有四件事会把请求推到下一级:超时、上游报错、结构校验不过、裁判分低于该端点的下限。没有别的,也没有情绪。

什么会把请求往上推

超时
这一级的最大等待到点。在途调用直接放弃,不等它自己回来。
上游报错
供应商返回 5xx 或 429,且该级允许的重试已经用掉。
结构校验不过
结构化输出没通过该端点的 schema。
低于下限
抽到的裁判分低于这个端点自己声明的下限。
一万条请求,跟到底每一万次进入FIG-05示意数据
每一万次进入最终给出了答案99.99%进入10,000语义缓存命中800进入路由9,200首选档一次通过8,455触发阶梯7458.1%02,0004,0006,0008,00010,000在触发阶梯的那些请求里斜纹 = 计为失败+1 档解决589+2 档解决127顶档解决25有损应答(Brownout)3硬失败1触发阶梯Σ 745一次命中率91.9%升级率8.1%升级后平均提分+12.0
示意模型 · FIG-05 · 每 10,000 次到达;各级解决数、有损应答与硬失败之和等于升级数

首选档一次接住 91.9% 的已路由调用。升级的 8.1% 主要是欠路由被纠正,升级后的答案平均涨 12 个裁判分——这就是「要阶梯而不是要一个更大的默认档」的全部理由。

阶梯的代价

中位延迟砍掉 58%,因为大多数请求本来就不需要旗舰档。P99 反而慢了 1%,因为升级的请求跑了两跳。两个数我们都印出来。如果某个端点吃不起这条尾巴,就让它并行跑两级,多花的那份钱买的是尾延迟。

  • −58%分位 P502,265ms → 955ms
  • +1%分位 P9913,332ms → 13,511ms
两条延迟分布端到端延迟,毫秒 — 对数分桶FIG-06示意数据
调度后基线调用占比,%0102030400–250250–500500–1k1k–2k2k–4k4k–8k8k–16k端到端延迟,毫秒 — 对数分桶分位基线P502,265msP753,532msP905,911msP957,545msP9913,332ms调度后P50955msP751,927msP904,223msP957,257msP9913,511msP99 +1% · 斜纹 = 比基线更差调度自身开销指纹 / 决策与预算,p95 9 / 4 msP506msP9511msP9919ms
示意模型 · FIG-06 · 端到端延迟,对数分桶,两条分布画在同一根轴上

阶梯就是一个文本文件

这是一个端点的全部配置。导出去它就能在别处跑;没有哪段状态藏在我们的控制台里、等着你自己反推。

阈值、等待、Brownout 触发条件都在同一个文件里,跟它点名的提示版本一起做版本。

chat.ladder.yaml示意数据
endpoint: /chat
quality_floor:
  judge_p10: 74
route_budget:
  usd_per_1k_calls: 4.20
  hard_cap: true
ladder:
  - tier: C-0                 # 语义缓存
    when: { cache_cos: ">=0.94", same_prompt_version: true }
  - tier: S-1
    when: { D: "<25", K: "<70" }
    max_wait_ms: 1200
  - tier: M-2
    when: { D: "25..55", or_escalated_from: S-1 }
    max_wait_ms: 2500
  - tier: L-3
    when: { D: ">=55", or_escalated_from: M-2 }
    max_wait_ms: 6000
brownout:
  trigger: { upstream_5xx_or_429: ">8% over 60s" }
  action: { cap_output_tokens: 512, drop_top_tier: true, keep_serving: true }
  cooldown_min: 10

第 04 步 — 有损可用

宁可灯光变暗,不要全城断电。

上游限流或者倒了的时候,端点不会开始返回 5xx。它会砍掉顶档、压低输出上限,继续出答案——答案变差,是故意的,而且明说。

名字取自电网:供电不够时把城市调暗是设计好的反应,把城市断掉才是事故。

四个状态

四个状态
状态什么时候进做什么
normal滚动 60 秒窗口内,上游错误率低于触发线。完整阶梯、声明的预算、顶档可用。
watch错误率越过触发线,但窗口还没走完。用户侧什么都没变。路由器开始计数,检测时限的表也开始走。
brownout窗口走完仍在触发线之上——60 秒内 5xx + 429 超过 8%。砍掉顶档、输出上限压到 512、关掉深推理档。端点继续出答案。
recover上游回落到触发线以下。先按 10 分钟冷却继续用降级阶梯,然后按和换档一样的 5% → 25% → 100% 曲线把顶档放回去。

冷却存在的理由很朴素:刚缓过来的上游是最可能再倒一次的。状态之间来回跳的代价,比老老实实暗十分钟贵。

第 05 步 — 切换窗口

新档位要先在影子里活够久。

3% 的真实流量复制一份给候选档离线跑。这些答案不返回给任何人,只用来挪动一个质量分、收窄它的置信区间。

候选档要穿过三道门才能变成默认档。然后是放量,不是切换。

三道门

5,000 条影子样本
同一个端点上的 5,000 条。不是全站凑 5,000——端点之间不通用。
连续 7 天
足够装下一个周末和一个周一,这两者长得完全不一样。
置信区间下界不差过 0.5 分
95% 区间的下界,不得比现任档低 0.5 分以上。

5,000 不是为了 PPT 凑的整数。裁判分离散度大约 22 分,正好是在这个样本量上,95% 置信区间收窄到 ±0.6 分——「不低于现任 0.5 分」这条规则才有意义。

然后才放量

按 5% → 25% → 100% 放,一次只动一个端点,每一步都留回滚开关。这个示意部署的上季度:提了 3 个,回滚 1 个,下架 2 个。

切换窗口置信区间半宽,裁判分FIG-09示意数据
置信区间半宽,裁判分该样本数下的理论半宽σ ≈ 22±0±1±2±3±0.6切换门槛±3.1200±1.9500±1.41,000±1.02,0005,000±0.410,000±0.320,000同一端点上的影子样本数 — 对数刻度置信区间半宽,裁判分放量三步切换门槛 · 5,000 · 7 d15%225%3100%
示意模型 · FIG-09 · 95% 置信区间半宽随影子样本量收敛,裁判分 σ ≈ 22

下一页

这套方法到底跑出了什么。

看全部数字