DAY 16 / PHASE 2 · 应用与系统

Cost Engineering

Token 经济学 · Prompt Caching · Model Routing · Batch & 监控

2026-05-31 · BigCat

「这个模型贵」是错的归因;成本是个乘积,每一项都能压一个数量级。

// WHY THIS MATTERS

2026 年的真实账单结构:同一个 Sonnet,重度用户月账单从 $80 到 $8000 的差距,90% 不是用得多少,是工程做得粗细。Anthropic 官方文档把账单展开成五项乘积——uncached input、cache write、cache read、batch、output——任何一项忽略掉就少掉一次 5×~10× 的杠杆。Simon Willison 在 2024 年底总结过去一年最反直觉的趋势:「LLM 价格在崩塌」。但真正吃到红利的不是用便宜模型的人,而是会把 caching + routing + batch 三件事叠起来的人——三者乘起来理论可以做到原价的 5% 以下。这一期讲四件事:怎么把账单拆到 token 级、prompt caching 的工程化、什么时候该 route 到便宜模型、batch + 监控告警的最小工程。

// 01

Token 经济学:账单是乘积,不是模型名

论断:成本归因到模型名(「Opus 贵」)是 99% 团队的第一错;真正的归因单位是「每次调用的五项 token 分布」。

背景与原理

Anthropic API 的实际计费公式(官方 prompt caching 文档可验证):

cost = uncached_input × 1.0x ← 默认输入 + cache_write_5m × 1.25x ← 写入 5 分钟缓存 + cache_write_1h × 2.0x ← 写入 1 小时缓存 + cache_read × 0.1x ← 命中缓存(-90%) + output × out_rate ← 输出(通常是 input 的 5×) × (1 - 0.5 if batch else 0) ← Batch API -50%

这意味着三件反直觉的事:(1)模型名只决定 base rate,乘上 cache 命中率和 batch 比例后,"贵" 模型可能比 "便宜" 模型更省。(2)output 是真正的成本中心——多数模型 output 单价是 input 的 5 倍;让模型「少说话」(structured output、明确 max_tokens、禁止 chain-of-thought 当 final answer)的 ROI 大于换模型。(3)cache_write 比默认 input 贵 25%,所以「试一试 cache 看看」是负 ROI 的——必须先估命中率。

Simon Willison 维护的 llm-prices.com 是值得收藏的对照表。他 2024 年底有一条结论值得记:用 Gemini Flash 8B 给 68000 张照片生成 caption 总成本 $1.68——说明真正贵的从来不是「调 LLM」,是「用错档位的 LLM 调没必要调的内容」。

实战示例

把账单结构嵌进每次调用,三行代码做成本归因

import anthropic
client = anthropic.Anthropic()

def call_with_cost(messages, system, model="claude-sonnet-4-6"):
    r = client.messages.create(model=model, max_tokens=1024,
        system=[{"type":"text", "text":system,
                 "cache_control":{"type":"ephemeral"}}],
        messages=messages)
    u = r.usage
    # 单价来自 llm-prices.com 或自己维护的表;这里以 Sonnet 4.6 为例
    rate = {"in":3.0, "out":15.0, "cw5":3.75, "cr":0.30}  # $/M tok
    cost = (u.input_tokens         * rate["in"]
          + u.cache_creation_input_tokens * rate["cw5"]
          + u.cache_read_input_tokens     * rate["cr"]
          + u.output_tokens        * rate["out"]) / 1e6
    print(f"${cost:.4f} | hit={u.cache_read_input_tokens/(u.cache_read_input_tokens+u.cache_creation_input_tokens+1):.0%}")
    return r, cost

跑一周后看 p50 / p95 cost 分布,95% 的团队会发现:少数 outlier 请求(output 超长 / 没命中 cache)占总成本一半以上——这才是优化目标。

失败模式:(1)只盯着「换便宜模型」,忽略 output cap。Output 砍半通常比换模型省得还多。(2)用 dashboard 上的「总花费」做决策——必须按「per-feature cost」和「per-request p95 cost」归因,不然永远不知道是哪类请求在烧钱。
进阶资源 · Anthropic Prompt Caching 文档, platform.claude.com/.../prompt-caching · Simon Willison LLM Pricing Calculator, llm-prices.com
// 02

Prompt Caching:拿到 90% 折扣的工程化

论断:caching 的难点不在 API,在「让前缀保持稳定」——99% 的 cache miss 来自工程师无意中破坏了 prefix invariance。

背景与原理

Anthropic prompt caching 的契约:用 cache_control: {type:"ephemeral"} 标记 breakpoint,从请求开头到该 breakpoint 的所有内容(tools → system → messages 顺序)作为一个 cache key。下次请求只要前缀字节完全相同,就以 0.1× input 价命中;任何一个字节变了,整段重写、付 1.25× 写入费。

这条契约带来三条铁律:(1)不变的放前面,变的放后面——tool schema、system prompt、long doc、few-shot 全部前置;user 当前请求放最后。(2)动态拼接是 cache killer——任何随时间变化的 token(当前时间、随机 ID、动态计数器)出现在前缀里,命中率直接归零。(3)breakpoint 上限 4 个——通常放在 tools 末、system 末、history 末,给当前 user 留空。Minimum cacheable 长度按模型,Sonnet 通常 1024 tokens,Opus 4096 tokens(来自官方文档);不到阈值不缓存,所以小请求 cache 是无效的。

┌─ 高命中率的 prompt 结构 ───────────────────────────┐ │ [tools schema ] ← cache_control ① 几乎不变 │ │ [system prompt ] ← cache_control ② 偶尔改版 │ │ [long doc / corpus ] ← cache_control ③ 按 doc id │ │ [conversation hist ] ← cache_control ④ 滚动追加 │ │ [user 当前请求 ] ← 不 cache,每次都新 │ └────────────────────────────────────────────────────┘ 命中率: 通常 70-95% 实际账单: 名义的 15-30%

实战示例

「滚动追加 history」是最值得注意的 pattern——多轮对话里把 cache_control 打在最新一轮的前一轮 assistant 消息上,让旧 history 全部命中:

messages = [
  {"role":"user",      "content":[{"type":"text","text":turn1_user}]},
  {"role":"assistant", "content":[{"type":"text","text":turn1_asst}]},
  {"role":"user",      "content":[{"type":"text","text":turn2_user}]},
  {"role":"assistant", "content":[{"type":"text","text":turn2_asst,
                                  "cache_control":{"type":"ephemeral"}}]},
  {"role":"user",      "content":[{"type":"text","text":turn3_user}]},   # 新
]

turn3 请求过来时,前面 4 条全部走 cache_read(0.1×),只有 turn3_user 是新 input。第 4 轮再请求时把 breakpoint 移到 turn3 末——这是滚动 cache 的标准做法,多轮对话账单瞬间砍掉 80%。5 分钟 TTL 内每命中一次会自动续期,所以高频会话不需要 1h cache(贵 2×)。

失败模式:(1)在 system prompt 里插入当前日期——每天 0 点 cache 全部失效。日期请放到 user message 里。(2)tool 定义里调一个返回时间戳的 helper——schema 字符串被 stringify 时变了,cache 全 miss。(3)小请求开 cache:不到 minimum 阈值不会缓存,只是白付 1.25× 写入费的风险(实际未达阈值时不收);但 1024/4096 阈值以下根本走不进 cache 路径。先用 cache_read_input_tokens / total_input 监控真实命中率,低于 40% 说明前缀不稳,要重构 prompt 结构,而不是关 cache
进阶资源 · Anthropic Prompt Caching with Claude, anthropic.com/news/prompt-caching · Anthropic Cookbook caching 示例, github.com/anthropics/anthropic-cookbook
// 03

Model Routing & Cascading:让便宜模型先尝试

论断:单一模型策略是浪费——70% 的请求 Haiku 就够,但你得用工程手段把它路出去。

背景与原理

真实流量的难度分布是长尾的:大部分请求(分类、抽取、改写、简单代码)小模型就能做对,少数请求(推理、长上下文、复杂代码)才需要旗舰。两种工程化做法:

什么时候 routing 是负 ROI?两个红线:(1)routing 决策的成本 > 节省——给 Haiku 调用前再跑一次 LLM 分类,分类成本可能已经吃掉差价;(2)routing 错误代价 > 节省——把医疗诊断 route 到 mini 模型省下 $0.01,错一次赔 $10000。规则:routing 适用于高量、低单价错误代价、难度方差大的场景(客服、抽取、初筛、批量生成)。

实战示例

不要从训路由器开始——先用 LiteLLM 这种 open-source proxy 把多 provider 收拢到一个 OpenAI 兼容端点,再加 fallback 规则。10 分钟见效:

# litellm config.yaml — 把 80% 流量灌给便宜模型,错误时升级
model_list:
  - model_name: tier-cheap
    litellm_params: {model: claude-haiku-4-5-20251001, api_key: os.environ/ANTHROPIC_API_KEY}
  - model_name: tier-mid
    litellm_params: {model: claude-sonnet-4-6,         api_key: os.environ/ANTHROPIC_API_KEY}

router_settings:
  fallbacks:
    - tier-cheap: [tier-mid]   # 429 / 5xx / context-window 错误自动升级
  context_window_fallbacks:
    - tier-cheap: [tier-mid]   # Haiku 装不下时落到 Sonnet

# client 侧:只调 tier-cheap,复杂请求走显式 tier-mid
client.chat.completions.create(model="tier-cheap", messages=[...])

这套配置做了三件事:fallback(错误时升级)、context window fallback(装不下时升级)、统一 OpenAI 接口(client 代码不需要 if-else 分支)。配合 LiteLLM 的 cost tracking,能直接看每个 tier 的占比和单价——比从头训 router 务实得多。

失败模式:(1)在 hot path 用 LLM 当 router,每个请求多一次 LLM 调用,分类 latency + cost 吃掉所有节省。规则路由 / embedding 阈值优先。(2)cascade 时低档模型的 self-eval 信号不可靠(Day 9 讲过的 sycophancy)——别问模型「你确定吗」,问外部 judge 或用任务自带的 verifier。
进阶资源 · LMSys RouteLLM, lmsys.org/blog/routellm · arXiv 2406.18665 · LiteLLM Router 文档, docs.litellm.ai/docs/routing
// 04

Batch API + 成本监控告警

论断:能容忍小时级延迟的任务都该走 batch;不能监控的成本都会失控。

背景与原理

Anthropic Message Batches API 和 OpenAI Batch API 的契约几乎一致:异步提交、24 小时 SLA、所有 token 价 -50%,且能和 prompt caching 叠加(理论极限 ≈ 原价的 5%——0.5 × 0.1)。它的工程意义不是「便宜一半」,是把可异步的工作流和必须同步的工作流分开,前者天然便宜一半。

判断该不该走 batch 的三个问题:(1)用户在不在等结果?不在 → 可 batch。(2)下一步操作是否阻塞当前 trace?不阻塞 → 可 batch。(3)失败重试容忍 24h 吗?能容忍 → 可 batch。典型可 batch 场景:nightly eval、回填历史数据、文档批量摘要、嵌入计算、报告生成。不可 batch:用户实时对话、agent loop 内部 tool call、需要立刻反馈的 UI。

监控这一层经常被跳过——直到月底账单 3 倍才发现。最小可用监控只要三个信号:per-request cost、cache hit rate、model tier 分布。任何一个突变都对应一类 bug(prompt 改了破坏 cache / 上游误路由到旗舰 / output 失控)。

实战示例

# A) Batch 提交 — 异步任务一行换 -50%
from anthropic.types.messages.batch_create_params import Request
batch = client.messages.batches.create(requests=[
    Request(custom_id=f"doc-{i}", params={"model":"claude-sonnet-4-6",
        "max_tokens":1024, "messages":[{"role":"user","content":doc}]})
    for i, doc in enumerate(docs)
])
# poll batch.id 直到 processing_status == "ended",再拉 results

# B) 监控三信号 — 接到任何 metrics backend 都行
def emit(usage, model, feature, cost):
    hit = usage.cache_read_input_tokens / max(1, usage.cache_read_input_tokens
                                              + usage.cache_creation_input_tokens
                                              + usage.input_tokens)
    metrics.histogram("llm.cost_usd",        cost, tags=[feature, model])
    metrics.histogram("llm.cache_hit",       hit,  tags=[feature, model])
    metrics.histogram("llm.output_tokens", usage.output_tokens, tags=[feature])

# C) 三条告警规则(PromQL / Datadog 都一样)
# 1) p95(llm.cost_usd) by feature  > 历史 3×              ← 突发烧钱
# 2) avg(llm.cache_hit) by feature < 0.4 持续 1h          ← 前缀被破坏
# 3) sum(cost) by model{tier=premium} / sum(cost) > 0.6  ← routing 失效

这套监控不到一天搭完,下次「不知道钱花哪了」的故障可以预防。把 batch 比例和 cache 命中率做成 dashboard 头牌指标,你会发现「优化 prompt」和「优化成本」常常是同一件事。

失败模式:(1)把 agent loop 的中间 tool call 丢进 batch——agent 是 sync 的,下一步依赖上一步,batch 直接破坏 loop。Batch 只适合独立、可并行、不阻塞的请求。(2)把成本归因到「model 维度」——决策的最小颗粒应该是feature 维度(搜索 / 摘要 / 写代码 / 分类),同一个模型在不同 feature 上的成本结构完全不同。
进阶资源 · Anthropic Introducing the Message Batches API, anthropic.com/news/message-batches-api · OpenAI Batch API 文档, platform.openai.com/docs/guides/batch

// 综合实战 · 一周把账单砍 70% 的复盘流程

把这四点串成一个真实可执行的复盘工作流——周一拉账单、周五看结果:

  1. Day 1 · 归因:把上月账单按 feature × model × cache-hit 三维度切。多数团队会发现 1-2 个 feature 吃掉 60%+ 总成本——优先打这两个,其它先别动。
  2. Day 2 · Output cap:检查 top-cost feature 的 max_tokens,多数是默认开到 4096 但实际只用几百。设到合理上限,立刻砍 20-40%。
  3. Day 3 · Cache 重排:检查 top-cost feature 的 prompt 结构,把不变内容前置、动态内容后置、加 cache_control。目标 hit rate ≥ 70%——再砍 50%+ input 成本。
  4. Day 4 · Tier 下移:用 50 条样本跑 Haiku vs Sonnet 对比,质量不降的 feature 直接降档;不能降的加 LiteLLM fallback。
  5. Day 5 · Batch 切分:识别能容忍 24h 的 job(eval / 回填 / 离线分析),切到 batch endpoint,自动 -50%。
  6. Day 5 · 监控接入:把上面三条告警接进 metrics backend——这是防止下个月又涨回去的唯一办法。

叠加完保守估计 60-80% 成本砍掉,且质量不降。最重要的副产品:你会培养出一种「看 prompt 就能估账单」的工程肌肉——这是 AI 超级个体的基本功之一。

// ENGLISH GLOSSARY

Token Economics
把 LLM 账单拆成 input/output/cache_write/cache_read/batch 五项乘积的成本分析框架。
Prompt Caching
对稳定前缀做服务端缓存,命中时 input 价 0.1×。Anthropic 通过 cache_control breakpoint 显式声明。
Cache Breakpoint
cache_control 标记的边界,从请求开头到该点的内容作为一个 cache key。每请求最多 4 个。
Prefix Invariance
cache 命中的必要条件——前缀字节必须完全一致。任何动态 token 都会破坏。
TTL (Time-To-Live)
cache 存活时间。Anthropic 提供 5 分钟(默认,1.25× 写入价)和 1 小时(2× 写入价)两档。
Model Routing
请求级模型选择,按预判难度路由到不同档位。代表工作:RouteLLM。
Model Cascading
失败时升级模型的串联策略,与 routing 互补——便宜模型先试,不行再升档。
Batch API
异步批量提交、24h SLA、所有 token 价 -50%。可与 caching 叠加。
Output Cap
max_tokens 限定输出长度的工程手段。Output 是成本中心(通常 5× input 价)。
Cost Attribution
把账单按 feature × model × cache-hit 拆解的归因方法。优于按 model 看总数。

// 深入思考

Cache write 是 1.25× 默认价。什么场景下开 cache 实际是负 ROI?怎么算盈亏平衡?
盈亏平衡点:cache write 多付 0.25×,cache hit 每次省 0.9×。所以「打平」需要至少 0.25/0.9 ≈ 0.28 次未来命中——即未来 5 分钟内还会访问 ≥1 次同前缀。结论:单次性请求(搜索、一次性 QA)开 cache 是负 ROI;多轮对话、agent loop、batch 处理同一文档都是正 ROI。判定标准:写入时能不能预测「这段 prefix 5 分钟内还会被请求」。不能预测就别开。
Output 单价通常是 input 的 5×。为什么 provider 这么定价?是不是说明 output 计算量也是 5×?
不完全是。技术上 output 比 input 贵的根因是自回归解码——每个 output token 都要跑一遍完整 forward pass(用上整段 KV cache),而 input 是一次 prefill 批量处理。但 5× 的 markup 包含了很多商业因素:output 量小所以单价容忍高、output 直接对应用户感知质量、output 是 provider 主要差异化。工程意义:与其换便宜模型,不如先降低 output 长度(structured output、严格 max_tokens、不让模型重复输入)。
RouteLLM 论文报告了 85% 降本。为什么实际部署中很少见到这个数字?
三个原因:(1)论文 baseline 是「100% 用 GPT-4」,真实团队早就在用 mid-tier 模型,节省空间小很多。(2)论文用 MT Bench 等 benchmark 评质量,真实场景的 edge case 分布更宽,router 准确率会降。(3)routing 决策本身有成本(embedding / 分类器 / latency),论文常常不算进去。所以实战中 30-50% 已经是好结果,期望 85% 会失望。但配合 cache + batch 叠加,整体降本 70% 是合理目标。
Batch API 看起来无脑该用——为什么很多团队明明能 batch 却不 batch?
三个隐性障碍:(1)心智成本:sync API 是 request-response,batch 是 submit-poll-collect,需要重写状态机和失败重试。(2)体验损失:用户习惯了「秒回」,把后台任务也包装成「立刻处理」是产品惯性。(3)编排复杂度:batch 适合「大量同构请求」,但真实工作流常常异构,强行 batch 反而拖慢关键路径。务实做法:识别 1-2 个明显可 batch 的场景(eval、回填),用最简 poll 模式接进去;不要一开始就上 Temporal 这种 workflow engine。
成本监控的最小信号是 cost、cache_hit、tier 分布。如果只能加一条告警,加哪条?
per-feature cache_hit_rate 突降。理由:成本绝对值会随业务增长正常波动,单次 outlier 是个体问题不是系统问题,tier 分布变化通常是有意调整。但 cache hit 突降几乎一定是 bug——某个工程师悄悄改了 system prompt、加了时间戳、调了 tool schema,这类改动不会触发任何 alert,但会在月底账单上反映成 5-10× 涨幅。Cache hit 是领先指标,cost 是滞后指标。

// 延伸阅读