模型作为组件:契约、失效模式与选型

系列精华 · 六篇正文每篇一页:给一个没有规格书的组件写规格书

arganzheng · 2026-10-04

这个系列的一句话主张

模型 API 没有供应商替你写的规格书,应用工程师要自己写——失效模式、契约、账、选型判据、客户端纪律,五部分缺一不可。

几篇怎么连起来

%%{init: {"flowchart": {"wrappingWidth": 170}}}%%
flowchart TB
    C1["01 失效模式"] --> C2["02 API 契约(一)"]
    C2 --> C3["03 API 契约(二)"]
    C2 & C3 --> C4["04 成本与延迟的账"]
    C1 & C4 --> C5["05 选型"]
    C1 & C2 & C4 --> C6["06 客户端工程"]

01 · 失效模式:把「模型会出错」拆成七条可检测的性质

七条失效性质,每条可检测——每条对应后面某一层的应对

flowchart LR
    CTX["组装上下文<br/>system·历史·检索·工具"] --> PRE["prefill<br/>读上下文"] --> SAMP["逐 token<br/>采样"] --> OUT["输出<br/>文本/JSON/tool_call"] --> ACT["执行动作"]

七条失效性质挂在哪一步

flowchart LR
    PRE["prefill"] & SAMP["采样"] & ACT["执行"]
    F4["④ 上下文标称 ≠ 有效"] -.-> PRE
    F5["⑤ 知识截止"] -.-> PRE
    F3["③ 指令遵循"] -.-> PRE
    F1["① 非确定性"] -.-> SAMP
    F2["② 幻觉"] -.-> SAMP
    F6["⑥ 供应商变更"] -.-> PRE
    F7["⑦ 越界"] -.-> ACT

    classDef stage fill:#fff7e0,stroke:#c98a00,stroke-width:2px,color:#222
    classDef fail fill:#fdecea,stroke:#c0392b,color:#222
    class PRE,SAMP,ACT stage
    class F1,F2,F3,F4,F5,F6,F7 fail

要点

每条失效性质都有一个落在纸面上的数

性质 一个数
非确定性 batch 不变性缺失:1000 次贪心 18 种结果 → 修好后 1 种;公开 API 做不到
上下文有效长度 NoLiMa 32K 处 11/13 模型掉到基线一半以下;GPT-4o 99.3% → 69.7%
幻觉的后果 Air Canada 812.02 加元——源材料正确、输出编造、公司仍负责
越界 PocketOS 9 秒删库;约 700 个 agent 入侵 Hugging Face

02 · API 契约(一):四家 API 的共同骨架

块列表进出;工具调用是协议不是功能;结构化输出保证语法不保证语义

%%{init: {"flowchart": {"wrappingWidth": 200}}}%%
flowchart LR
    App["应用"] -->|"req"| API["模型 API"]
    API -->|"asst + tc"| CHK{"校验·权限"}
    CHK -->|"执行"| Tool["工具"]
    Tool -->|"res/err"| App
    App -->|"hist+res"| API
    API -->|"ans"| Done(["结束"])
    CHK -->|"循环到无 tc<br/>或预算尽"| API

要点

同骨架不同实现——字段、状态、缓存顺序各异

项 OpenAI Anthropic
工具参数 arguments 是字符串 input 是对象
服务端状态 Responses 默认 store: true 30 天;Conversation 无 TTL 无状态
缓存顺序 tools → system → messages 同
变更 Assistants API 2026-08-26 关闭  

03 · API 契约(二):推理模型加了什么维度

思考按输出价计费且占 TTFT;effort 是倾向不是预算;思考是只搬运的跨轮状态

%%{init: {"flowchart": {"wrappingWidth": 200}}}%%
flowchart TB
    IN["输入:完整历史<br/>(含上轮 thinking)"] --> THINK["思考阶段<br/>thinking 不可见<br/>输出价计费 · 占 TTFT"]
    THINK --> OUT["可见输出<br/>文本 / tool_call"]
    OUT --> STATE["返回状态<br/>thinking block 等"]
    STATE --> NEXT["下轮必须携带<br/>(DeepSeek 例外)"]
    EFF["effort"] -.->|"控制思考量级"| THINK
    NEXT -.-> IN

    classDef stage fill:#fff7e0,stroke:#c98a00,stroke-width:2px,color:#222
    classDef ctl fill:#eef6ff,stroke:#5b8fd6,color:#222
    class IN,THINK,OUT,STATE,NEXT stage
    class EFF ctl

要点

effort 一档 6 倍账单;历史也受校验约束

量 数
同任务 low → high 账单 $0.0075 → $0.0445(6 倍)
历史校验 校验 thinking block 之前的历史——编辑历史受约束
Chat Completions 自 GPT-5.4 起不支持带 effort 的工具调用
DeepSeek reasoning_content 不能送回

常见误区

  • 「把模型名从 4.6 改成 5 就迁移完了」——thinking 默认开启、采样参数 400、tokenizer 多 30%、max_tokens 含思考

04 · 成本与延迟的账

输出价是输入的 4–6 倍、缓存读 0.1× 一次回本;TTFT 三段、总时长由输出主导

%%{init: {"flowchart": {"wrappingWidth": 170}}}%%
flowchart TB
    Q["排队"] --> P["prefill<br/>∝ 未缓存输入"]
    P --> T["思考<br/>∝ 思考 token"]
    T --> F(["首个可见 token<br/>(TTFT)"])
    F --> D["decode<br/>每 token 一步"]
    D --> E(["最后 token<br/>(总时长)"])

    classDef seg fill:#fff7e0,stroke:#c98a00,stroke-width:2px,color:#222
    classDef mark fill:#f0f0f0,stroke:#888,color:#222
    class Q,P,T,D seg
    class F,E mark

要点

输出 4–6×、缓存读 0.1×——输出主导账单

价 相对输入价
输出 4–6×
缓存读 0.1×(Fable 5.1 0.025×、DeepSeek 0.02×)
缓存写 1.25×(1 小时 TTL 2×)
长上下文 OpenAI > 272K:整个请求输入 2×、输出 1.5×
峰时 / Batch DeepSeek 峰时 ×2;Batch API 50%

多轮对话的输入账

每轮输入的构成:命中与写入

  • 多轮不缓存输入二次增长;开缓存后每轮只付写入部分,长会话总输入约降到 15%

05 · 选型:榜单只缩范围

榜单只缩范围——选型用自己的评测集 30–50 条;级联不漏难题、弃用周期也是维度

%%{init: {"flowchart": {"wrappingWidth": 170}}}%%
flowchart TB
    A["20+ 候选 → 硬约束"] -->|"榜单"| E["短名单"]
    E --> F["自家评测集"]
    F --> G{"过?"}
    G -->|"否"| H["放宽约束"]
    G -->|"是"| I["按每任务成本排"]
    I --> K["选定 + 替代模型"]

    classDef step fill:#fff7e0,stroke:#c98a00,stroke-width:2px,color:#222
    classDef dec fill:#eef6ff,stroke:#5b8fd6,color:#222
    class A,E,F,I,K,H step
    class G dec

要点

30–50 条自测集选型;弃用通知最短 4 天

量 数
Arena 失真 Llama 4 前 27 个私测变体;两家各约 20% Arena 数据 vs 83 个开源模型共 29.7%
大小模型价差 5–20 倍;70% 简单请求级联省 63%
自托管 满载 $0.8 / 百万;利用率 30% 时翻三倍 + Infra 团队
弃用通知 OpenAI 3–6 个月、Anthropic ≥ 1 年、DeepSeek 4 天

常见误区

  • 「路由比级联省钱所以用路由」——只省 2–3 个百分点却会漏难题

06 · 客户端工程:调用侧要处理哪些失败

错误只重试「再试可能不同」的四类;超时分四层;幂等责任在工具侧

%%{init: {"flowchart": {"wrappingWidth": 180}}}%%
flowchart TB
    S(["业务层发起调用"]) --> CNT["数 token + 并发槽"]
    CNT --> SEND["发送(四层超时)"]
    SEND --> OK{"成功?"}
    OK -->|"是"| PARSE["解析 · 读 usage"]
    PARSE --> DONE(["返回 + 记 trace"])
    OK -->|"否"| CLS["错误分类<br/>(下图)"]

    classDef step fill:#fff7e0,stroke:#c98a00,stroke-width:2px,color:#222
    classDef dec fill:#eef6ff,stroke:#5b8fd6,color:#222
    classDef stop fill:#f0f0f0,stroke:#888,color:#222
    class CNT,SEND,PARSE,CLS step
    class OK dec
    class S,DONE stop

失败之后:先分类,再决定退避还是兜底

可重试走「退避 + 预算」,不可重试看有没有 fallback——两条路最终都回到发送或失败

%%{init: {"flowchart": {"wrappingWidth": 180}}}%%
flowchart TB
    CLS{"错误分类"} -->|"可重试"| BACK["退避<br/>指数 + 抖动"]
    BACK -->|"预算未耗尽"| SEND["回到发送"]
    BACK -->|"耗尽"| FB{"有 fallback?"}
    CLS -->|"不可重试"| FB
    FB -->|"换模型 / 降级"| SEND
    FB -->|"无"| FAIL(["失败返回 + 记 trace"])

    classDef step fill:#fff7e0,stroke:#c98a00,stroke-width:2px,color:#222
    classDef dec fill:#eef6ff,stroke:#5b8fd6,color:#222
    classDef stop fill:#f0f0f0,stroke:#888,color:#222
    class BACK,SEND step
    class CLS,FB dec
    class FAIL stop

要点

重试有预算、超时分四种、取消要传播

纪律 规则
重试 full jitter \(t_n = \min(t_{max}, \text{random}(0, t_0 2^n))\);重试预算 10%
insufficient_quota 也是 429,不可重试
超时 连接 / 首字节 / 字节间 / 总——一个总超时不够
流式 超过一分钟的生成一律流式
计数 Sonnet 5 同文本多 30% token;中文每字 1–2 token
取消 用户关页面要传播到上游,否则供应商继续生成并计费

五部分规格书

部分 一句话 篇
失效模式 七条性质,每条可检测 01
契约 块列表进出;工具调用是协议;思考是只搬运的状态 02、03
账 五项成本;读一次回本;TTFT 三段 04
选型判据 自己的评测集;级联;弃用周期 05
客户端纪律 四类错误、四层超时、预算、传播取消 06

常见误区(一)

  • 「temperature = 0 加 seed 就能复现」——公开 API 的 batch 随负载变
  • 「源材料没错机器人就不会说错」——幻觉是高概率续写不是转述
  • 「标称 1M 就能放 1M」——32K 处就掉一半
  • 「system prompt 权限更高」——只是一段文本
  • 「strict 模式不会有错误输出」——只保证形状
  • 「服务端状态省 token 钱」——只改变谁传历史

常见误区(二)

  • 「effort 越高越好」——账单 6 倍、可能过度思考
  • 「缓存写入加价所以不划算」——读一次回本
  • 「Arena 第一就是最好」——只用来缩范围
  • 「自托管 $0.8 比 API 便宜」——满载数字
  • 「429 了就重试」——insufficient_quota 不可重试
  • 「用户关了页面请求就结束了」——不传播则继续计费

下一步

  • 同一路线:《Prompt 与上下文工程》——契约之上,模型这一步该看到什么;《生产与运维》——客户端纪律的服务端一半
  • 往下:《vLLM 源码》——API 另一端的引擎;《高效推理与压缩》——为什么输出价是输入的 4–6 倍
  • 原文总纲:/model-as-a-component.html;通关自测在系列总结
← Arganzheng's Blog