先打分,再落档,接不住就抬。
三件事决定一条请求去哪:出门前算出来的难度指纹、在做决定那一刻就生效的路由预算、以及接住首选档判断失误的降级阶梯。这一页写的是你判断「这东西是不是真能工作」所需要的细节。
这里没有任何「自适应」式的含糊。页面上每个阈值都是你设的数,跟着它所属的阶梯一起做版本,并且在每次决策时原样回给你。
第 01 步 — 难度指纹
10 毫秒,五个维度,一个分数。
请求离开你的应用之前,一个自托管的小分类器在五个维度上给它打分——推理深度、上下文跨度、格式严格度、事实敏感度、拒答风险,各 0–100。五个维度合成一个难度分 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 < 25 | 42% |
| D2 · 常规 | 25–55 | 34% |
| D3 · 复杂 | 55–80 | 19% |
| D4 · 高风险长链 | D ≥ 80 | 5% |
分类器本身
一个 60M 参数的编码器,你自己跑。它不外呼,所以指纹只花一次本地推理,请求正文也不用为了打分而先离开你这边。
| 参数 | 值 |
|---|---|
| 规模 | 60M 编码器,自托管 |
| 微调集 | 1,200,000 条打标请求 |
| 延迟(p50 / p95 / p99) | 5 / 9 / 14 毫秒 |
| 每次调用成本 | $0.00003 |
| 与人工标注一致性(κ) | 0.71 |
| 分档准确率(按流量加权) | 84.1% |
大约每六条判错一条。这不是脚注里的免责,这正是阶梯存在的理由。
- 欠路由
- 判得比实际低一档以上。这部分由阶梯接住——它是升级的主要来源。
- 过路由
- 判得比实际高一档以上。这部分没人接:钱白花了,约占账单 11%。
把阈值调激进,过路由会缩,升级率和延迟尾巴会涨。没有哪一档设置能让两边同时变好。这个旋钮在你手里,按端点设。
第 02 步 — 路由预算
预算不是报表,是约束。
每个端点声明一个「每千次调用多少美元」的上限。它在选路那一刻就生效,不是月底对账时才算——余量不够时,整条阶梯当天下压一级,而不是先超支再给你发邮件。
预算和看板的区别就在这儿。看板告诉你发生了什么,这个东西改变将要发生什么。
各端点的预算与实测
| 端点 | 质量下限 | 预算 | 实测 | 余量 |
|---|---|---|---|---|
| /chat | 裁判分 p10 ≥ 74 | $4.20 | $2.81 | 33% |
| /docqa | 裁判分 p10 ≥ 78 | $5.50 | $4.22 | 23% |
| /extract | 结构通过率 ≥ 99.0% | $4.80 | $3.68 | 23% |
| /review | 裁判分 p10 ≥ 84 | $9.60 | $8.65 | 10% |
在这个示意部署里预算是硬上限,四个端点都跑在各自的上限以内。
路由器是一棵树,不是一个模型。每条请求都带着它走过的路径和每条边上的条件,也就是说,六周之后你还能在工单里跟它吵架。
缓存命中也算一个叶子,因为它同样是一次路由决策。
四条旁路规则
这四条压过决策树。它们存在的原因是:有些失败不值得聪明。
- 格式严格度 ≥ 80,强制走带 schema 校验的档,校验不过即升级。
- 事实敏感度 ≥ 70,直接禁用小型通用档。
- 路由预算余量 < 20%,整条阶梯下压一级,直到自然日结束。
- 拒答风险 ≥ 70,并行跑两档,取先到且过下限的那个。
第 03 步 — 降级阶梯
出问题要横着倒,不要竖着塌。
一条阶梯是 3–5 级有序候选链。每一级是「模型档 × 参数 × 提示版本」,带触发条件和最大等待。同一条阶梯两个方向都走:首选档不够就往上抬,预算或上游顶不住就往下压。
只有四件事会把请求推到下一级:超时、上游报错、结构校验不过、裁判分低于该端点的下限。没有别的,也没有情绪。
什么会把请求往上推
- 超时
- 这一级的最大等待到点。在途调用直接放弃,不等它自己回来。
- 上游报错
- 供应商返回 5xx 或 429,且该级允许的重试已经用掉。
- 结构校验不过
- 结构化输出没通过该端点的 schema。
- 低于下限
- 抽到的裁判分低于这个端点自己声明的下限。
首选档一次接住 91.9% 的已路由调用。升级的 8.1% 主要是欠路由被纠正,升级后的答案平均涨 12 个裁判分——这就是「要阶梯而不是要一个更大的默认档」的全部理由。
阶梯的代价
中位延迟砍掉 58%,因为大多数请求本来就不需要旗舰档。P99 反而慢了 1%,因为升级的请求跑了两跳。两个数我们都印出来。如果某个端点吃不起这条尾巴,就让它并行跑两级,多花的那份钱买的是尾延迟。
- −58%分位 P502,265ms → 955ms
- +1%分位 P9913,332ms → 13,511ms
阶梯就是一个文本文件
这是一个端点的全部配置。导出去它就能在别处跑;没有哪段状态藏在我们的控制台里、等着你自己反推。
阈值、等待、Brownout 触发条件都在同一个文件里,跟它点名的提示版本一起做版本。
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 个。