LLM 应用上线的第一周往往没有人关心成本,第三个月账单寄来时才发现一个问答功能每月烧掉数千美元。成本的失控不是发生在单次调用,而是发生在规模放大之后:同样的功能,调用量从每天 100 次涨到 10 万次,每一次多花的几厘钱都会放大为巨额账单。LLM 成本治理的目标不是“省钱省到影响质量”,而是在质量不降的前提下,让每一分钱都花在刀刃上。本指南系统覆盖计费模型拆解、缓存命中、模型路由、输入压缩、批处理、成本预算与监控看板,给出可落地、可度量、可持续的成本治理体系。
一、先看懂 LLM 计费模型
1.1 Token 计费的本质
LLM 按 token 数 × 单价 计费,且输入与输出分开计价。价格差异是治理的前提:
| 计费项 | 含义 | 典型价格(相对量级) | 优化抓手 |
|---|---|---|---|
| 输入 token | Prompt + 历史 + 检索 | 低 | 压缩、缓存、路由 |
| 输出 token | 生成的内容 | 高(约 2-4 倍输入价) | 约束长度、结构化输出 |
| 缓存写入 | 首次写缓存 | 约 1.25 倍输入价 | 避免频繁变动前缀 |
| 缓存命中 | 命中已缓存前缀 | 约 0.1 倍输入价 | 最大化前缀稳定性 |
| 图片/音频输入 | 多模态按 token 折算 | 高 | 压缩、按需使用 |
ℹ️ 核心洞察:多数应用的账单里,输入 token 占大头——因为每次请求都重复携带 system、历史、检索结果。砍掉重复输入,比压缩输出见效更快。
1.2 成本公式与杠杆点
单次调用成本 = (输入 token × 输入单价)
+ (输出 token × 输出单价)
- (缓存命中 token × 命中折扣)
批量成本 = Σ 单次调用成本
# cost_formula.py — 按计费模型估算单次调用成本(美元)
def call_cost(input_tokens, output_tokens, cached_tokens,
price_in, price_out, cache_discount=0.1):
cached_charge = cached_tokens * price_in * cache_discount
fresh_input = max(0, input_tokens - cached_tokens) * price_in
return fresh_input + cached_charge + output_tokens * price_out
1.3 成本归因的四个维度
治理的前提是把账单拆开。按四个维度归因,才能知道“钱都花在哪”:
| 维度 | 拆解问题 | 数据需求 |
|---|---|---|
| 按功能 | 哪个功能最烧钱 | 请求打上 feature 标签 |
| 按用户 | 谁在消耗资源 | 用户维度记账 |
| 按模型 | 哪些请求被路由到贵模型 | 模型维度统计 |
| 按时间 | 高峰期的并发成本 | 时序聚合 |
二、Prompt Caching:最便宜的 token
2.1 缓存命中率是成本健康的第一个指标
前缀缓存只对相同前缀生效。缓存友好设计的核心是:把稳定内容放前缀,变化内容放尾部。
请求 1: [System 角色][知识库 v3][工具说明][历史 H][问题 1] ← 全价
请求 2: [System 角色][知识库 v3][工具说明][历史 H][问题 2]
└─────────── 命中缓存 ──────────┘ ← 只对尾部计费
# cache_design.py — 缓存友好的上下文组装
def cacheable_context(static: dict, dynamic: dict) -> list[dict]:
"""静态前缀(角色/知识库/工具)+ 动态尾部(查询/临时检索)。"""
prefix = [
{"role": "system", "content": static["role"]},
{"role": "system", "content": f"知识库:{static['kb']}"},
{"role": "system", "content": f"工具:{static['tools']}"},
]
tail = [{"role": "user", "content": dynamic["query"]}]
return prefix + tail
def cache_hit_rate(stats) -> float:
"""缓存命中率 = 缓存 token / 总输入 token。健康值:> 60%。"""
return stats["cached_input_tokens"] / stats["total_input_tokens"]
2.2 知识库版本化与缓存失效
知识库升级会让旧缓存失效。版本化前缀(如 KB:v3)把失效范围精确到版本:
def kb_key(version: str) -> str:
"""知识库版本前缀:升级时只 bump 版本号,精确失效。"""
return f"KB:{version}|"
def bump_kb(static: dict, new_version: str) -> dict:
"""升级知识库 → 返回新前缀,旧缓存自动失效。"""
return {**static, "kb_version": new_version}
2.3 缓存友好度的反模式
| 反模式 | 表现 | 后果 |
|---|---|---|
| 前缀掺动态内容 | system 里塞时间戳、随机 id | 缓存永远不命中 |
| 检索结果放前缀 | 每次检索都不同 | 前缀漂移,全价重算 |
| 每轮重排系统指令 | 指令顺序随机 | 前缀不稳定 |
| 无脑全量重发 | 历史不裁剪全带上 | 输入膨胀 |
三、模型路由:让便宜模型干简单的事
3.1 复杂度分级路由
不是所有请求都需要大模型。按任务复杂度路由,是成本杠杆里收益最大的一项:
# routing.py — 复杂度分级路由
import json
def classify_complexity(query: str) -> str:
"""小模型分类器判复杂度:simple / medium / complex。"""
resp = small_model.classify(query)
return resp["label"]
def route(query: str) -> str:
label = classify_complexity(query)
return {
"simple": "gpt-4o-mini", # 价格低 10-30 倍
"medium": "gpt-4o-mini",
"complex": "gpt-4o", # 只在难题上用
}[label]
一句话:小模型解决 70% 的简单请求,大模型专注 30% 的复杂请求,平均成本可以下降一半以上。
3.2 路由正确性必须监控
路由省钱的代价是误判:简单题送大模型是浪费,难题送小模型是答错。两类错误都要监控:
def monitor_routing(golden_cases, router, judge_fn) -> dict:
"""评估路由正确率:简单题是否被错误路由到贵模型。"""
misroute_expensive = 0
wrong_answers = 0
for case in golden_cases:
model = router(case["query"])
if case["difficulty"] == "simple" and model == "gpt-4o":
misroute_expensive += 1
if case["difficulty"] == "complex" and model == "gpt-4o-mini":
if judge_fn(case)["total"] < 3:
wrong_answers += 1
return {
"expensive_misroute_pct": misroute_expensive / len(golden_cases),
"wrong_answer_pct": wrong_answers / len(golden_cases),
}
3.3 级联路由:先便宜后兜底
没有把握时先试小模型,质量不达标再升级大模型。级联(Cascade) 是质量与成本的折中:
async def cascade_call(query: str, judge_fn) -> str:
"""先小模型,Judge 认为质量不足再升大模型。"""
answer = await small_model.answer(query)
if judge_fn(query, answer)["total"] >= 4:
return answer, "small" # 小模型已够好
answer = await large_model.answer(query)
return answer, "large" # 升级兜底
| 策略 | 成本 | 质量风险 | 适用 |
|---|---|---|---|
| 静态路由 | 低 | 中(误判) | 任务特征稳定 |
| 级联兜底 | 中 | 低 | 质量敏感、预算敏感 |
| 全量大模型 | 高 | 低 | 高质量无预算约束 |
四、输入压缩:砍掉重复与冗余
4.1 历史裁剪策略
对话历史的成本按轮次线性增长。滚动窗口 + 摘要控制历史规模:
# history_compress.py — 历史裁剪
def trim_history(messages, max_turns=6, summarizer=None):
"""超窗的旧消息压缩为摘要,保留关键决定。"""
if len(messages) <= max_turns * 2:
return messages
keep = messages[-max_turns * 2:] # 保留最近 N 轮
older = messages[:-max_turns * 2] # 更早的压缩
summary = summarizer(older) if summarizer else "【历史省略】"
return [{"role": "system", "content": f"早期对话摘要:{summary}"}] + keep
4.2 检索结果的压缩注入
RAG 场景检索回来的文档常常有大量冗余。注入前做片段级去重与摘要:
def compress_evidence(docs, budget_tokens) -> list[str]:
"""按 token 预算压缩检索片段:去重、截取、必要时摘要。"""
docs.sort(key=lambda d: d["score"], reverse=True)
result, used = [], 0
for d in docs:
snippet = trim_to_budget(d["text"], budget_tokens - used)
if not snippet:
break
result.append(snippet)
used += count_tokens(snippet)
return result
4.3 系统指令瘦身
System 指令写得多,每次请求都付钱。指令精简与指令分层(常驻最小核心,技能按需注入):
| 指令组织 | 形式 | 成本影响 |
|---|---|---|
| 全量常驻 | 所有规则塞 system | 每次全价 |
| 核心常驻 + 技能按需 | 角色常驻,技能检索注入 | 减少 30-50% 常驻 token |
五、批处理:摊薄固定开销
5.1 什么时候值得批处理
不要求实时响应的任务(离线标签、摘要、嵌入生成)可以合并请求:
# batching.py — 离线批处理调度
async def batch_offline_tasks(tasks, batch_size=20):
"""把 N 个小任务合并成大 batch,摊薄固定开销。"""
for i in range(0, len(tasks), batch_size):
batch = tasks[i:i + batch_size]
# 一次调用处理多个输入,共享前缀与模型加载成本
results = await model.batch_complete(
prompts=[t["prompt"] for t in batch])
yield from zip(batch, results)
| 场景 | 实时流式 | 离线批处理 |
|---|---|---|
| 延迟要求 | 秒级 | 分钟-小时级 |
| 调用形态 | 单请求并发 | 合并大 batch |
| 成本 | 高(重复固定开销) | 低(摊薄 + 批价折扣) |
| 适用 | 对话、搜索 | 标签、摘要、嵌入、审核 |
5.2 批处理的经济学
def batch_economics(n, per_call_overhead, batch_size):
"""对比逐条与批处理的成本。"""
single = n * (per_call_overhead + 1)
batched = (n / batch_size) * (per_call_overhead + batch_size)
saving = 1 - batched / single
return {"single": single, "batched": batched,
"saving_pct": saving * 100}
六、输出控制:避免钱花在废话上
6.1 max_tokens 与输出约束
输出按 token 计费且单价更高。显式约束长度与结构化输出能显著降本:
def constrained_answer(query: str, schema: dict) -> dict:
"""用结构化输出约束:只返回 schema 需要的字段。"""
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": query}],
response_format={"type": "json_schema", "schema": schema},
max_tokens=300, # 硬性上限,防止发散
)
return json.loads(resp.choices[0].message.content)
| 输出控制手段 | 效果 | 副作用 |
|---|---|---|
| max_tokens | 防止超长 | 可能截断 |
| 结构化输出 | 只生成必需字段 | 需定义 schema |
| 摘要指令 | 限制篇幅 | 可能丢细节 |
| 停止词 | 提前终止 | 依赖模型支持 |
6.2 相似响应的缓存(语义缓存)
对于 FAQ 类高频相似请求,可以在应用层做语义缓存,完全跳过模型:
def semantic_cache_lookup(query: str, threshold=0.95) -> str | None:
"""embedding 相似度命中历史答案,完全不再调用模型。"""
emb = embed(query)
hit = vector_db.search(emb, top_k=1)
if hit and hit[0].score >= threshold:
return hit[0].answer
return None
ℹ️ 核心洞察:语义缓存是“零成本”的终极手段——命中的请求完全不调用模型。命中率 10-20% 就值得建设。
七、成本预算与监控看板
7.1 三层预算模型
预算要按预算类型分别设定与追踪:
| 预算类型 | 维度 | 示例 | 超支动作 |
|---|---|---|---|
| 总额预算 | 月度总成本 | $5000/月 | 熔断降级 |
| 功能预算 | 单功能成本上限 | 问答 < $800/月 | 限流/降模型 |
| 单请求预算 | 每请求成本上限 | 平均 < $0.02 | 优化路由/压缩 |
7.2 成本看板指标
# cost_dashboard.py — 成本看板核心指标
def cost_dashboard(daily_stats: list[dict]) -> dict:
totals = sum_daily(daily_stats)
return {
"daily_cost": totals["cost"],
"cost_by_feature": group_by_feature(daily_stats),
"cost_by_model": group_by_model(daily_stats),
"avg_cost_per_request": totals["cost"] / totals["requests"],
"cache_hit_rate": totals["cached"] / totals["input_tokens"],
"token_to_value_ratio": totals["value_score"] / totals["cost"],
}
def cost_budget_check(daily: float, monthly_budget: float,
day_of_month: int, days_in_month: int) -> str:
"""月度预算按天线性预测,提前预警超支。"""
projected = daily * days_in_month / max(day_of_month, 1)
if projected > monthly_budget * 1.2:
return "CRITICAL"
if projected > monthly_budget:
return "WARN"
return "OK"
7.3 成本告警与自动化降级
预算超支时自动执行降级动作链,而不是干瞪眼:
def auto_remediate(budget_status: str):
"""按预算健康度逐级降级:先路由、再限流、最后熔断。"""
actions = {
"WARN": lambda: switch_to_mini_model(), # 全量走小模型
"CRITICAL": lambda: enable_semantic_cache_only(), # 优先命中缓存
"OVERDUE": lambda: reject_non_core_features(), # 只保核心功能
}
return actions.get(budget_status, lambda: None)()
7.4 成本与质量的联合门禁
省钱的每一个手段都可能伤质量。成本优化必须过质量门禁:
def cost_quality_gate(optimized, baseline, judge_fn, golden,
max_drop=0.03) -> bool:
"""成本优化方案与基线对比:质量下降超阈值则拒绝上线。"""
saving = 1 - estimate_cost(optimized, golden) / estimate_cost(baseline, golden)
drop = judge_avg(baseline, golden) - judge_avg(optimized, golden)
print(f"节省 {saving:.0%},质量下降 {drop:.3f}")
assert drop <= max_drop, "成本优化牺牲质量,拒绝上线"
return True
八、自托管与量化:另一个成本维度
8.1 托管 API vs 自托管的成本对比
当调用量足够大,自托管可能更便宜,但要算全成本:
| 成本项 | 托管 API | 自托管 |
|---|---|---|
| 单 token 价 | 固定单价 | 硬件折旧 + 电费 + 运维 |
| 规模弹性 | 好(按需) | 差(需预留容量) |
| 前期投入 | 无 | 高(GPU 采购) |
| 精度 | 高 | 可用低精度量化 |
| 盈亏平衡 | — | 高 QPS 才划算 |
8.2 量化与精度权衡
自托管常用 INT8 / FP16 量化降硬件成本,但精度有损:
| 精度 | 显存占用 | 质量损失 | 适用 |
|---|---|---|---|
| FP16 | 高 | 无 | 质量敏感 |
| INT8 | 中 | 轻微 | 生产常用 |
| INT4 | 低 | 明显 | 边缘、演示 |
一句话:量化是“用可接受的精度损失换显存与电费”,必须在评测集上量化损失后才能上线。
九、成本治理的落地路线图
9.1 分阶段建设
阶段一(1-2 周) 阶段二(2-4 周) 阶段三(1-2 月)
────────────────── ──────────────────────── ────────────────────────
· 全量埋点成本数据 · 接入前缀缓存 · 成本×质量联合门禁
· 四维归因(功能/用户) · 引入复杂度路由 + 级联 · 自动降级动作链
· 建立月度基线 · 历史裁剪与压缩 · 语义缓存 / 批处理
└ 能看见 └ 能省 └ 能自动、能止损
9.2 常见失败模式
| 失败模式 | 表现 | 规避 |
|---|---|---|
| 只优化不度量 | 不知道省了多少 | 每项优化前后对比 |
| 路由误判 | 简单题送贵模型 / 难题答错 | 路由质量监控 |
| 缓存设计反模式 | 前缀漂移永不命中 | 静态前缀 + 动态尾部 |
| 质量隐性退化 | 成本降了用户变少 | 成本×质量联合门禁 |
| 只看总额 | 不知道哪个功能烧钱 | 按功能归因 |
总结:成本治理的六字真经
| 杠杆 | 关键动作 | 预期收益 |
|---|---|---|
| 缓存 | 静态前缀 + 版本化 | 输入成本降 50-80% |
| 路由 | 复杂度分级 + 级联 | 平均成本降 30-70% |
| 压缩 | 历史裁剪 + 检索精简 | 输入降 30-60% |
| 批处理 | 离线任务合并 | 摊薄固定开销 |
| 输出约束 | max_tokens + 结构化 | 输出降 20-40% |
| 语义缓存 | 高频相似请求命中 | 完全零成本 |
LLM 成本治理的本质,是把“模型按 token 收费”的简单计费模型,转化为“在质量约束下对每个请求做成本决策”的工程系统。读懂计费模型、抓住缓存与路由两个最大的杠杆、用归因与预算看板让成本可视化、最后用成本×质量联合门禁守住底线——这四步做完,你的 LLM 应用才能跑得又省又稳,让每一笔 token 支出都对得起它的价值。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。