技术思考问题4:模型跑分领先,团队就该全面切换吗?

别用榜单切模型:能力圈与机会成本驱动的路由决策

榜单说它最强,账单说它更贵,听谁的

[读者] 负责模型选型与 LLM 网关的架构师、技术负责人
[痛点] 新模型一发布就想切,手上却没有能挡住全量切换的证据
[现在读] 5.5 系列刚发布,榜单、单价、交付成本三个信号同时在变
[读完] 能把选哪个模型变成一次可评测、可灰度、可回滚的路由决策
[旧方案] 全量走单一模型 + 凭榜单与 token 单价拍板
    |
    v
[新需求] 新模型单价降、输出快、编码基准领先,业务要求尽快切
    |
    v
[冲突] 榜单是特定编码场景,单价不是交付成本,任务分布对不上能力圈
    |
    v
[后果] 切完成功率掉、人工返工涨、总成本更高,还得连夜回滚

我是老李,带一个面向企业客户的智能平台,网关后面挂着三类调用:客服问答、长链路工单、代码修复。这三类流量共用一套提示模板、一套工具定义、一套重试策略,历史上只换过一次底座模型。

这次是季度规划会。业务负责人把屏幕推过来,上面是一条发布信息:Anthropic 推出 5.5 系列首款模型 Opus 5.5,性能媲美旗舰 Fable 5.1,典型任务成本较 Opus 5 降 40%,输出提速超 30%;默认 100 万 token 上下文、12.8 万最大输出,思考模式常开;定价每百万输入/输出 token 为 $4/$20,缓存读取降 60%;编程能力上 CursorBench 4.0 超 GPT-5.6 Sol 约 11 个百分点、成本仅其 1/3,还有一次 9.5 小时完成 HAProxy 由 C 到 Rust 重写的记录;模型已上线 Claude API、AWS、谷歌云、Azure,Sonnet 5.5、Haiku 5.5 将在数周内推出。

这些数字来自本次话题提供的发布信息,对应官方页面是 https://www.anthropic.com/claude-opus-5-5 、https://www.anthropic.com/claude-sonnet-5-5 、https://www.anthropic.com/claude-haiku-5-5 。我在当前环境无法访问这三份页面逐一核验,因此定价口径、上下文上限、基准口径与可用区域,落地前需按当前官方文档核验。

会议室的结论很统一:单价降了 40%,输出快 30%,榜单第一,切。任务是下个迭代前把三类流量全量迁到新模型,并把成本账单砍下来。

我关心的不是它强不强,而是它强的地方和我们让它干的活,重合多大。

榜单领先 11 个百分点,就说明我们可以全量切吗?

每百万 token 单价降了,单次有效交付真的更便宜吗?

如果不能全量切,那到底怎么切,切错了怎么退?

01、故事

现场是三楼的小会议室,白板上写着三类任务和各自的一句话定义。

客服问答:输入是一句用户提问加一段知识库片段,输出是一条结论加引用。判定标准是结论正确、格式稳定、必须带引用。这类任务短、并发高、对稳定性敏感,对推理深度几乎没有要求。

长链路工单:输入是一串事件,模型要在多轮工具调用里做决策,中间状态越长越容易发散。判定标准是最终动作正确、步骤齐全、可审计。

代码修复:输入是一个失败用例加一段代码,输出是补丁。判定标准是补丁能让测试通过,而不是看起来对。这一类恰好是商业编码基准最擅长覆盖的场景,也是我们三类流量里最小的一块。

这三类任务放在同一个网关后面,用的是同一套提示、同一套工具、同一套重试。任务是:在不改动业务语义的前提下,让每类请求走到它在成本与成功率上最合适的模型,并且切错了能在分钟级退回来。

会上给了三个理由:单价降 40%,输出快 30% 以上,CursorBench 4.0 领先 GPT-5.6 Sol 约 11 个百分点且成本只有其 1/3。三条都成立。但三条回答的都是它行不行,没有回答它在我们这三类任务上行不行。

把更强翻译成对我们更合适,中间隔着三件事:任务分布对不对得上、失败代价扛不扛得住、切换成本付不付得起。

榜单是别人的尺
账本是自己的账
一刀切得快
回滚来不及

02、问题

旧方案是全量走一个模型:运维简单,提示只维护一份,出问题只查一条链路。它在业务量小、任务单一的时候是成立的。

新需求出现后,旧方案的两条支柱断了。第一条支柱是单价可比:新模型的输入/输出单价确实变了,但我们的单次请求里,输出长度、重试次数、上下文重放次数都会跟着模型行为变化,账单上的单价乘以用量,乘积不一定变小。第二条支柱是能力可比:编码基准覆盖的是代码修复,我们流量里最大的一块是客服问答和长链路工单,两者根本不在同一张卷子上。

业务影响是具体的。长链路工单判错动作,下游要人工兜;客服问答格式飘了,前端解析直接报错;代码修复看着对但测试不过,等于白花一次调用再加一次人工复核。

技术表现也一样具体:同一个提示模板在新模型上产出更长的推理文本,输出 token 上升;超过最大输出被截断的请求,重试后仍然截断;思考模式常开让一次调用的成本结构从按答案付费变成按过程付费。

可验证的完成标准,我列了六条。第一,三类任务在同一黄金样例集上的成功率不低于现网基线。第二,每个任务类型的单成功任务成本可计算、可对比。第三,P95 时延与重试次数在阈值内。第四,人工返工不上升。第五,能按租户与任务类型灰度,并在阈值触发时自动回滚。第六,高风险写操作必须有人工审核或确定性校验兜底。

这六条里,第一条和第六条是质量底线,第二条到第五条是工程底线。缺任何一条,全量切换都只是一次赌博。

成功要看结果
成本要看交付
时延要看长尾
返工要算人头

03、原理

先给两个定义,后面所有判断都从这里长出来。

能力圈:识别基准测评与自身业务真实可靠范围的差距。注意它衡量的不是我会什么,而是我在哪里判断可靠。放到模型选型上,至少要比较四处差异:任务分布、判定标准、上下文形态、失败代价。编码基准衡量的是特定编码场景下的一次通过率,而我们的客服问答衡量的是格式稳定与引用正确,长链路工单衡量的是多步决策的收敛性。三者交集很小。

机会成本:比较替换方案在质量、时间、风险与费用上的放弃代价。它不等于价格差,而是把这四项一起算进去的差。单价低 40%,但失败率高 5 个百分点、每次失败多花 30 分钟人工,机会成本可能是负的。

反直觉判断:单价下降不等于单次有效交付更便宜。原因有三层。第一层,思考模式常开意味着过程 token 计入输出计费,答案变短不代表账单变短。第二层,长上下文请求会重复计费,除非命中缓存,而我们的长链路工单恰恰是缓存命中率最难保证的场景。第三层,也是最容易被忽略的一层:失败任务的成本不在 token 账单里,在人工返工的工时里。

反直觉判断:更强的模型在弱任务上不一定更稳。能力圈的边界是可靠,不是上限。在结构化的短问答上,过深的推理会引入格式波动和更长的输出,稳定性反而可能下降。基准分数衡量的是上限,生产评测衡量的是可靠性下限,两者常常不同向。

由此得到本篇的成本口径:

单成功任务成本 = (Σ 每次调用的输入/输出 token 费用 + 工具调用费用 + 重试额外消耗)/ 成功任务数 + 单位成功任务分摊的人工返工成本

这个式子里,分子看得见,分母才决定性质。分母是成功任务数,不是调用次数。一个把所有请求都答完、但只答对一半的模型,单成功任务成本可能比一个准确率高但贵一点点的模型更高。

商业基准是线索,不是替代生产评测的证据。它告诉你哪个方向值得试,不告诉你该全量切。

圈内叫可靠
圈外叫运气
便宜是可能
省钱是结果

04、架构

[输入] 用户请求 / 工单事件
    |
    v
[模块] 路由决策器(任务分类 + 能力圈匹配 + 机会成本打分)
    |
    v
[数据/状态] 任务类型标签、租户策略、模型评分卡、回滚阈值
    |
    v
[处理] 模型调用 + 统一提示/工具/重试 + 确定性校验 + 人工审核闸门
    |
    v
[输出] 结构化结果 / 工单状态 / 路由与成本埋点

边界:路由决策器只做分类与选择,不做业务判断。它看到的输入是任务类型标签、租户标识、请求特征。它看不到用户隐私原文以外的语义判断,也不该看到。

第一层收益是把选型从一次性动作变成持续决策。模型换代是常态,路由策略是常驻资产。第二层收益是成本可归因:每个任务类型、每个租户各自的单成功任务成本独立可见,谁贵谁便宜不用吵。

代价也很明确。多了一条路由链路,就多了一个故障点;多了一套评分卡,就多了一份要维护的黄金样例;灰度期间同一类任务会走不同模型,结果一致性需要额外校验。

适用条件有四条:任务类型可以被稳定分类;每类任务有可判定的验证标准;有可观测的埋点与阈值;有能在分钟级生效的回滚通道。四条缺一条,先把缺的那条补上,再谈路由。

高风险写操作不进入自动路由的主路径,必须走人工审核或确定性校验闸门。这是架构上的硬边界,不是可调参数。

分类再匹配
打分再路由
闸门留一手
回滚有通道

05、实战一次

先把评测管道跑通,这是 V1 唯一的目标。它不产生选型结论,只产生能算结论的能力。

环境与版本:Python 3.11+,只用标准库(json、random、time、dataclasses、typing、argparse)。依赖:无。真实调用时替换一个客户端实现即可。

核心实现如下,文件名 harness.py:

from __future__ import annotations

import argparse
import json
import random
import time
from dataclasses import dataclass
from typing import Callable, Protocol


# ---------- 1. 三组黄金样例,各带确定性校验 ----------

@dataclass
class Sample:
    sid: str
    task_type: str
    prompt: str
    checker: Callable[[str], bool]


def check_qa(out: str) -> bool:
    return '7' in out and '退货' in out


def check_workflow(out: str) -> bool:
    try:
        data = json.loads(out)
    except json.JSONDecodeError:
        return False
    return data.get('action') == 'escalate' and len(data.get('steps', [])) >= 3


def check_codefix(out: str) -> bool:
    return 'def mean(' in out and 'len(xs) == 0' in out


QA1 = '客服:未拆封商品的退货期限是多少天?只回一句结论。'
QA2 = '客服:退货需要保留哪些凭证?只回一句结论。'
WF1 = '工单:用户连续三次反馈同一问题,请输出 JSON,含 steps 与 action。'
WF2 = '工单:拆包后发现破损,请输出 JSON,含 steps 与 action。'
CF1 = '修复 mean() 在空列表上抛异常的问题,输出完整函数。'
CF2 = '修复 mean() 对非数值输入的处理,输出完整函数。'

SAMPLES = [
    Sample('qa-001', 'qa', QA1, check_qa),
    Sample('qa-002', 'qa', QA2, lambda o: '发票' in o or '凭证' in o),
    Sample('wf-001', 'workflow', WF1, check_workflow),
    Sample('wf-002', 'workflow', WF2, check_workflow),
    Sample('cf-001', 'codefix', CF1, check_codefix),
    Sample('cf-002', 'codefix', CF2, check_codefix),
]

# ---------- 2. 三种任务各自统一的提示与回答(脚本化) ----------

REPLIES = {
    QA1: '未拆封商品自签收之日起 7 天内可退。',
    QA2: '需保留订单凭证与发票。',
    WF1: '{"steps": ["确认订单", "核对凭证", "升级人工"], "action": "escalate"}',
    WF2: '{"steps": ["确认订单", "核对凭证", "升级人工"], "action": "escalate"}',
    CF1: 'def mean(xs):\n    if len(xs) == 0:\n        return 0.0\n    return sum(xs) / len(xs)',
    CF2: 'def mean(xs):\n    if len(xs) == 0:\n        return 0.0\n    return sum(xs) / len(xs)',
}


class ModelClient(Protocol):
    def complete(self, prompt: str, *, model: str, max_tokens: int) -> tuple[str, int, int]:
        """返回 (文本, 输入token, 输出token)。接真实 API 时按此签名实现。"""


class ScriptedClient:
    """脚本化客户端:先把评测管道、重试与成本归集跑通。
    接真实模型时换成 HTTP/SDK 实现,接口签名保持不变。"""

    def __init__(self, replies: dict[str, str], latency_ms: int = 120,
                 flaky: set[str] | None = None, seed: int = 7) -> None:
        self.replies = replies
        self.latency_ms = latency_ms
        self.flaky = flaky or set()
        self.rng = random.Random(seed)

    def complete(self, prompt: str, *, model: str, max_tokens: int) -> tuple[str, int, int]:
        time.sleep(self.latency_ms / 1000.0)
        if any(k in prompt for k in self.flaky) and self.rng.random() < 0.5:
            raise TimeoutError('simulated upstream timeout')
        text = self.replies.get(prompt, '无法回答')
        text = text[: max_tokens * 4]
        tokens_in = len(prompt) // 3 + 32
        tokens_out = len(text) // 3 + 8
        return text, tokens_in, tokens_out


# ---------- 3. 统一的运行、重试与成本归集 ----------

@dataclass
class Run:
    sid: str
    task_type: str
    model: str
    ok: bool
    latency_ms: float
    tokens_in: int
    tokens_out: int
    retries: int
    rework_min: float


REWORK_MIN_ON_FAIL = 30.0  # 失败任务计入的人工返工分钟数,示例假设,按团队口径替换


def run_one(client: ModelClient, sample: Sample, model: str,
            max_tokens: int, max_retries: int) -> Run:
    started = time.perf_counter()
    retries = 0
    ok = False
    tokens_in = 0
    tokens_out = 0
    while retries <= max_retries:
        try:
            text, ti, to = client.complete(sample.prompt, model=model, max_tokens=max_tokens)
            tokens_in += ti
            tokens_out += to
            if sample.checker(text):
                ok = True
                break
        except Exception:
            pass
        retries += 1
    latency_ms = (time.perf_counter() - started) * 1000
    return Run(sample.sid, sample.task_type, model, ok, latency_ms,
               tokens_in, tokens_out, retries, 0.0 if ok else REWORK_MIN_ON_FAIL)


def cost_usd(run: Run, price_in: float, price_out: float, rework_usd_per_min: float) -> float:
    token_cost = run.tokens_in / 1e6 * price_in + run.tokens_out / 1e6 * price_out
    return token_cost + run.rework_min * rework_usd_per_min


def p95(values: list[float]) -> float:
    if not values:
        return 0.0
    ordered = sorted(values)
    idx = min(len(ordered) - 1, int(round(0.95 * (len(ordered) - 1))))
    return ordered[idx]


def summarize(runs: list[Run], price_in: float, price_out: float,
              rework_usd_per_min: float) -> dict:
    by_type: dict[str, list[Run]] = {}
    for r in runs:
        by_type.setdefault(r.task_type, []).append(r)
    report: dict[str, dict] = {}
    for task_type, rs in by_type.items():
        ok_count = sum(1 for r in rs if r.ok)
        total_cost = sum(cost_usd(r, price_in, price_out, rework_usd_per_min) for r in rs)
        report[task_type] = {
            'n': len(rs),
            'success_rate': round(ok_count / len(rs), 4),
            'p95_latency_ms': round(p95([r.latency_ms for r in rs]), 1),
            'retries': sum(r.retries for r in rs),
            'cost_usd': round(total_cost, 6),
            'cost_per_success_usd': round(total_cost / ok_count, 6) if ok_count else None,
            'human_rework_min': round(sum(r.rework_min for r in rs), 1),
        }
    return report


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument('--model', default='opus-5-5')
    parser.add_argument('--max-tokens', type=int, default=2048)
    parser.add_argument('--max-retries', type=int, default=1)
    parser.add_argument('--price-in', type=float, default=4.0)   # 美元/百万输入 token
    parser.add_argument('--price-out', type=float, default=20.0)  # 美元/百万输出 token
    parser.add_argument('--rework-usd-per-min', type=float, default=1.0)
    args = parser.parse_args()

    client = ScriptedClient(REPLIES)
    runs = [run_one(client, s, args.model, args.max_tokens, args.max_retries) for s in SAMPLES]
    print(json.dumps(summarize(runs, args.price_in, args.price_out,
                               args.rework_usd_per_min),
                     ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()

启动:

python harness.py --model opus-5-5 --max-retries 1 --price-in 4.0 --price-out 20.0

请求:V1 用脚本化客户端发起,回答与耗时都由脚本给出,目的是验证指标计算链路,不是测量模型。

验证:输出中三个任务类型都要出现,且每个都有 cost_per_success_usd;把 --price-in/--price-out 改成官方定价后重跑,单成功任务成本中 token 部分应按比例变化,人工返工部分不变。如果这个比例关系不成立,说明成本归集写错了,先修代码再谈选型。

未在当前环境实测,以下为预期结果(示例输出,数据来自脚本,不代表任何真实模型表现):

{
  "qa": {
    "n": 2,
    "success_rate": 1.0,
    "p95_latency_ms": 120.0,
    "retries": 0,
    "cost_usd": 0.000020,
    "cost_per_success_usd": 0.000010,
    "human_rework_min": 0.0
  },
  "workflow": {
    "n": 2,
    "success_rate": 1.0,
    "p95_latency_ms": 120.0,
    "retries": 0,
    "cost_usd": 0.000030,
    "cost_per_success_usd": 0.000015,
    "human_rework_min": 0.0
  },
  "codefix": {
    "n": 2,
    "success_rate": 1.0,
    "p95_latency_ms": 120.0,
    "retries": 0,
    "cost_usd": 0.000040,
    "cost_per_success_usd": 0.000020,
    "human_rework_min": 0.0
  }
}

V1 跑通之后留下的问题是明确的:谁来提供真实的回答分布,谁来决定失败时的返工分钟数,谁来把这份报告和真实灰度对齐。这三个问题分别对应数据、口径和流程,解决了才轮到 V2。

样例要黄金
提示要统一
指标要可算
结论要可复

06、排查

灰度一开始只放了很小一部分流量,两条诊断链就浮出来了。下面所有数值都是示例埋点结构,用来说明判定口径,不是线上实测。

诊断链一。现象:长链路工单的成功率低于基线,P95 时延同时上升。怀疑:新模型多步决策能力不足。检查:把埋点按任务类型拆开,看输出 token 数、重试次数、被截断的请求数。证据:同一类任务的输出 token 数明显高于旧模型,被截断的请求在重试后仍然截断,重试本身又放大了总 token 消耗。根因:思考模式常开加上统一的重试与输出预算,等于让模型把过程写进了答案,而网关还在用旧的输出上限和重试次数。修复:按任务类型分别设定输出预算与重试次数,并记录截断计数作为一等指标。

错误尝试: 最初的反应是去调采样参数和提示措辞,认为这是提示不兼容。为什么错:这次观测到的变化量集中在输出长度与截断上,采样参数几乎不影响这两个量;调参数只会让指标看起来动了一点,却把真正的变量——输出预算与重试策略——继续藏着。

诊断链二。现象:客服问答的成功率上升,但整体成本没有下降。怀疑:新模型单价并不便宜。检查:把成本拆成 token 费用与人工返工两部分,并按任务类型分摊。证据:客服问答本身极短,单价变化在这类任务上的绝对影响很小;真正拉高总成本的是长链路工单的失败返工,而它被平均到了所有任务上。根因:成本口径错位——用全局平均掩盖了任务类型之间的结构性差异。修复:每个任务类型独立评分卡,单成功任务成本不跨类型平均。

诊断链三。现象:代码修复的通过率看起来不错。怀疑:模型编码能力确实强。检查:把代码修复的校验从字符串匹配换成真实跑测试用例。证据:一部分回答里函数体完整但边界条件没处理,靠关键词匹配会被判为通过。根因:判定标准太弱,评分卡在自欺。修复:代码修复类任务一律以测试执行为准,禁止用关键词判定通过。

三条链指向同一个结论:失败不在模型身上,在评测口径和策略配置上。模型换代暴露的往往是评测能力的缺口。

现象先看数
怀疑后动手
证据定根因
改了再验证

07、优化

V2 只做第 06 章证据支持的四件事,不加需求。

根因一:策略是全局默认值,不区分任务类型。修改:路由策略按任务类型配置输出预算、重试次数与校验方式。原因:三类任务的失败模式和成本结构本来就不同,用一套默认值等于把长任务的病传给短任务。新行为:短问答零重试、小输出预算;工单允许一次重试、中等预算;代码修复必须跑测试、允许两次重试。验证:用同一黄金样例集重跑,检查截断计数归零、重试次数落在配置范围内。

route_policy:
  version: 2
  tasks:
    qa:
      primary: <cheap-model-id>
      fallback: <strong-model-id>
      max_retries: 0
      max_output_tokens: 512
      verify: schema_only
      thresholds:
        success_rate_drop: 0.01
        rework_rate_up: 0.01
    workflow:
      primary: <strong-model-id>
      fallback: <secondary-model-id>
      max_retries: 1
      max_output_tokens: 4096
      verify: schema_only
      require_audit_when: write_action
      thresholds:
        success_rate_drop: 0.02
        rework_rate_up: 0.02
    codefix:
      primary: <strong-model-id>
      fallback: <secondary-model-id>
      max_retries: 2
      max_output_tokens: 8192
      verify: run_tests
      thresholds:
        success_rate_drop: 0.02
        rework_rate_up: 0.02
  canary:
    stages: [0.05, 0.25, 0.5]
    rollback_on: [success_rate_drop, rework_rate_up, cost_per_success_up]

根因二:成本口径是全局平均。修改:单成功任务成本按任务类型与租户独立计算,模型单价作为参数注入而不是写死在代码里。原因:只有分摊到具体任务类型,机会成本才可比较。新行为:每个租户能看到自己三类任务的单成功任务成本与成功率。验证:故意把某个类型的失败率调高,确认该类型的单成功任务成本上升而其他类型不受影响。

根因三:判定标准太弱。修改:代码修复以测试执行为准,工单以 JSON 结构与动作字段共同判定。原因:判定标准决定评分卡是否可信,弱的判定会把错误模型选成正确模型。新行为:评分卡一旦失败即阻断晋级。验证:构造两个只有边界条件不同的补丁,确认只有测试通过的那个被判成功。

根因四:切换是全局动作。修改:灰度按租户乘以任务类型的二维矩阵推进,每一档都带自动回滚阈值。原因:经验上不同租户的任务分布差异最大,按租户切比按全局切更安全。新行为:任一阈值触发即回到上一档,回滚不需要人工介入。验证:在预发环境手动注入一次成功率下跌,确认自动回滚在设定时间内生效。

V2 的验证全部依赖第 06 章的证据,没有引入任何新的测量数字。这一步的原则是:如果一条优化找不到对应的证据,它就不该出现在 V2 里。

根因若不改
参数全白调
成本要分摊
路由才说得清

08、演进

[同一输入] 工单事件
    |
    +--[V1] 全量单模型 / 省了运维,贵在返工
    |
    +--[V2] 按任务与租户路由 / 多一份评测与灰度成本
    |
[Trade-off]
得到什么:单成功任务成本与成功率按类型可解释,切换可回滚
失去什么:多一条路由链路、一份黄金样例、一套阈值维护
适用边界:任务可分类、可判定、可观测、可回滚,四条缺一不可

正确性上,V1 的判定是单一的,模型答完就算完成;V2 引入了分层判定,短任务看结构,长任务看动作,代码任务看测试。判定变强,正确性的置信度随之提高。

稳定性上,V1 的稳定性等于单个模型在全部任务上的稳定性,任何一次模型行为变化都会整体传导;V2 把影响面限制在单个任务类型或单个租户内,并且有阈值兜底。代价是同一类任务在不同灰度档位上会走不同模型,结果一致性需要额外核对。

复杂度上,V1 是一份配置,V2 是一份策略矩阵加一套评分卡。这个复杂度不是白来的,它换来的是可以回答为什么选它这个问题。

成本上,V1 的成本是单价乘以用量的直读结论,V2 的成本是单成功任务成本。前者易算但会骗人,后者难算但能决策。评测本身也要花调用费,这部分应该算进选型预算里。

适用范围上,V2 适合任务类型可枚举、失败可判定、有埋点和回滚通道的系统。如果业务还在探索期,任务类型每天在变,先把评测集稳定下来,再谈路由。

遗留问题有三个:黄金样例会过期,需要定期补充新样本;人工返工的分钟数现在是拍出来的,需要用真实工单回填;多模型并存带来的结果一致性问题,还没有完全闭环。

快是一种代价
稳是一种成本
两者都要付
只看谁先付

09、洞见

9.1 基准是别人的能力圈证据

反直觉判断:基准分数越高,越说明不了它在你的场景里可靠。CursorBench 4.0 超 GPT-5.6 Sol 约 11 个百分点,这句话成立的前提是任务分布与评测集相当。我们的客服问答、长链路工单和它几乎不重合,因此这条信息只能作为线索,用来决定先试哪个方向,不能作为全量切换的证据。能力圈的核心动作是把评测集与你的任务分布对齐,而不是把评测分数照搬。

9.2 便宜的单价是可能性,不是结果

反直觉判断:单价下降 40% 与成本下降 40% 是两件事,中间隔着输出长度、重试次数、缓存命中和人工返工。$4/$20 的每百万 token 定价只在 token 数量不变时才能直接换算成账单变化。而思考模式常开、长上下文重放、失败返工这三件事,恰好都会改变 token 数量或总成本。判断便宜与否的唯一口径是单成功任务成本,不是单价。

9.3 路由的本质是约束下的匹配

路由不是把请求分配给最强的模型,而是分配给在约束内完成任务最稳、最省的模型。约束包括时延、成本、合规和可审计性。同一份请求在不同租户下的最优解可能不同,因为它们的失败代价不同。把路由当成分流器会写出一堆 if-else,把它当成约束优化问题才会写出策略表。

9.4 回滚能力决定你能切多快

能切多快不取决于迁移脚本写得多好,取决于回滚通道有多可靠。如果回滚需要改代码发版,那灰度就只能小心翼翼;如果回滚是一次策略变更,灰度就可以更大胆。这也是为什么第 07 章把回滚阈值和灰度档位放在同一份配置里:它们本来就是一件事。

9.5 高风险写操作必须留在圈外

有些任务再省钱也不能交给自动路由:资金变更、权限调整、对外承诺。这类任务必须有人工审核或确定性校验兜底。承认这件事不丢人,它是能力圈定义的一部分。把不确定的部分明确排除在外,剩下的部分才能放心自动化。

基准是线索
生产是证据
能力有边界
机会有代价

10、系统落地

原来有什么:一个网关、一份提示模板、一个模型、一套重试策略。选型靠会议与榜单,切换靠发版。

本篇新增什么:一套离线对照评测管道,用来把单成功任务成本算出来;一份按任务类型与租户的二维路由策略;一组带自动回滚阈值的灰度档位;三道质量闸门,分别是结构校验、测试执行和写操作人工审核。

现在能做什么:面对任何一款新模型,可以先在黄金样例上跑出三类任务的成功率、P95 时延、重试次数和单成功任务成本,再判断它是否值得进入灰度;进入灰度后按租户与任务类型逐步放量,任一阈值触发即自动回退。

还缺什么:黄金样例的更新机制、真实返工数据的回填、多模型结果一致性的核对方式。这三样都不是代码问题,是流程问题。

下一步如何演进:一是把返工成本从估值改成实测值,让单成功任务成本更接近真实;二是把评测集按租户拆分,按租户维度衡量能力圈;三是当 Sonnet 5.5、Haiku 5.5 陆续可用时,把路由从两档扩展成三档,让短任务下沉到更便宜的模型上。

有评测才敢切
有灰度才敢放
有阈值才敢退
有证据才敢说

11、小结

Q1 → 榜单领先不等于可以全量切,那是别人题集上的能力圈证据
Q2 → 单价降 40% 不等于单成功任务更便宜,返工与重试才是成本大头
Q3 → 按租户与任务类型灰度路由,配阈值自动回滚,切错能退
状态 → 交付一套可离线评测、可灰度、可回滚的模型路由决策闭环

三类任务、一个网关、一份策略表。这套东西不保证每次都选到最强的模型,它保证每次选型都有证据,每次切换都有退路。

问题拆三问
答案落一处
路由有边界
成本有出处

12、作业

12.1 理解题:为什么单次调用更便宜,单成功任务成本反而可能更高?

参考答案:因为分母不同。单次调用成本只看这一次的 token 费用;单成功任务成本的分母是成功任务数。如果替换后成功率下降,失败任务会带来重试消耗和人工返工,这些成本被分摊到剩下的成功任务上,于是每个成功任务变得更贵。此外思考模式常开、长上下文重放会让同样的任务消耗更多输出 token,进一步推高分子。

12.2 实战题:给你三类任务各两条样例,如何在一小时内搭出一套能算单成功任务成本的评测脚本?

参考答案:只做四件事。第一,把样例写成数据结构,每条带任务类型和确定性校验函数。第二,把模型调用抽象成一个只有 complete 方法的接口,先用脚本化实现把管道跑通。第三,统一运行入口,统计成功与否、时延、输入输出 token、重试次数,失败任务计入可配置的返工分钟数。第四,按任务类型聚合出成功率、P95 时延和单成功任务成本,打印成 JSON。不要在第一步就接真实 API,先把指标算对。

12.3 排障题:灰度后长链路工单成功率下降、时延上升,你先看哪三个指标?

参考答案:先看输出 token 数,判断是否因为过程 token 计入输出导致长度暴涨;再看被截断的请求数与重试次数,判断是否触发了输出上限并形成了重试放大;最后看缓存命中情况,长上下文任务若未命中缓存,输入侧会重复计费并拖慢时延。三个指标共同指向输出预算与重试策略,而不是模型能力。

12.4 架构判断题:把回滚阈值写进路由策略配置,和写在代码里,差别在哪?

参考答案:写在代码里,回滚等于一次发版,灰度只能保守,切换速度受发布流程限制;写在配置里,回滚是一次策略变更,可以在阈值触发时自动生效,灰度的风险敞口变小,才敢把档位放大。回滚能力和灰度速度是同一件事的两面,所以它们应该放在同一份配置里维护,并且有明确的触发条件与生效路径。

会算才算懂
会停才算稳
会退才算快
会验才算真

13、思考

回到本篇的核心冲突:一边是榜单、单价和速度这类看起来确定的信号,一边是任务分布、失败代价和切换成本这类看起来模糊的现实。选型的技术含量,不在于会不会读榜单,而在于能不能把模糊的那一面量化成可比较的成本。

能力圈和机会成本这两个工具的价值,是把选型从立场之争变成口径之争。当所有人用同一套口径看同一组证据,争的就不再是谁更强,而是谁在约束内完成任务更稳、更省。

最后一条判断留给所有要切换模型的人:先问它在我们这儿算不算圈内,再问换掉它要付什么,最后才问它有多强。顺序颠倒,代价会自己找上门。

榜单不等于边界
单价不等于成本
切换不等于升级
路由不等于妥协

posted @ 2026-10-11 13:39  李福春  阅读(8)  评论(0)    收藏  举报