大模型应用最容易犯的成本错误,不是"选了贵模型",而是没有把一次请求到底花在什么地方说清楚。一个聊天窗口看起来只问了一句,但服务端可能携带了长系统提示词、十几轮对话、检索出的多段原文、工具定义、重试请求,以及一段很长的模型输出。最后看到月账单上涨,再去全局截断文本,往往同时伤害答案质量和用户体验。
成本控制应该从可观测开始:知道每个功能、每个租户、每个路由最终消耗了多少输入 token、缓存 token、输出 token、调用次数、失败重试和人工成本;再对症使用上下文压缩、缓存和模型路由。它不是"让模型回答短一点"的单点技巧,而是一套把质量、延迟、金额放在同一张表上决策的办法。
本文的代码只依赖 Python 3.11 标准库,便于接到任意兼容 JSON HTTP 接口的网关前后。价格不写死在代码里,也不列任何可能过期的单价;不同提供商、区域、批量模式和模型版本的计费都可能变化,部署前应以账户控制台和官方价格页为准。
一、先把成本公式拆开,避免"平均每次多少钱"的幻觉
一次 LLM 调用的直接费用通常至少由输入 token 和输出 token 组成。有些平台还区分缓存命中输入、推理 token、图像/音频 token、工具调用和存储。最通用的记录方式不是把它们强行合并,而是分别保存数量与当时生效的价格版本:
请求成本 = 输入token×输入单价 + 缓存输入token×缓存单价 + 输出token×输出单价 + 附加项
同一轮对话中,输入 token 的构成也要拆开:系统提示词、历史消息、用户新问题、检索片段、工具 schema、工具结果。只有把来源拆开,才知道该优化哪里。比如系统提示词只有 600 token,而每次都塞入 12 段检索正文,优先优化的是检索和重排;如果真正问题是模型不断输出冗长解释,则应该约束输出预算和任务格式。
成本不能只看成功调用。超时重试、429 退避后重放、流式中断、工具循环和不受控的用户复制粘贴,都可能消耗 token。特别是"失败后换模型重试"的逻辑,如果没有幂等标识和次数上限,会把一次请求放大成多个完整上下文。监控面板应该同时展示请求数、成功数、失败数、重试数和每种 token;只显示总金额会让根因消失。
#mermaid-svg-MiBi86e5dpx4VRnd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MiBi86e5dpx4VRnd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MiBi86e5dpx4VRnd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MiBi86e5dpx4VRnd .error-icon{fill:#552222;}#mermaid-svg-MiBi86e5dpx4VRnd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MiBi86e5dpx4VRnd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MiBi86e5dpx4VRnd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MiBi86e5dpx4VRnd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MiBi86e5dpx4VRnd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MiBi86e5dpx4VRnd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MiBi86e5dpx4VRnd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MiBi86e5dpx4VRnd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MiBi86e5dpx4VRnd .marker.cross{stroke:#333333;}#mermaid-svg-MiBi86e5dpx4VRnd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MiBi86e5dpx4VRnd p{margin:0;}#mermaid-svg-MiBi86e5dpx4VRnd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MiBi86e5dpx4VRnd .cluster-label text{fill:#333;}#mermaid-svg-MiBi86e5dpx4VRnd .cluster-label span{color:#333;}#mermaid-svg-MiBi86e5dpx4VRnd .cluster-label span p{background-color:transparent;}#mermaid-svg-MiBi86e5dpx4VRnd .label text,#mermaid-svg-MiBi86e5dpx4VRnd span{fill:#333;color:#333;}#mermaid-svg-MiBi86e5dpx4VRnd .node rect,#mermaid-svg-MiBi86e5dpx4VRnd .node circle,#mermaid-svg-MiBi86e5dpx4VRnd .node ellipse,#mermaid-svg-MiBi86e5dpx4VRnd .node polygon,#mermaid-svg-MiBi86e5dpx4VRnd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MiBi86e5dpx4VRnd .rough-node .label text,#mermaid-svg-MiBi86e5dpx4VRnd .node .label text,#mermaid-svg-MiBi86e5dpx4VRnd .image-shape .label,#mermaid-svg-MiBi86e5dpx4VRnd .icon-shape .label{text-anchor:middle;}#mermaid-svg-MiBi86e5dpx4VRnd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MiBi86e5dpx4VRnd .rough-node .label,#mermaid-svg-MiBi86e5dpx4VRnd .node .label,#mermaid-svg-MiBi86e5dpx4VRnd .image-shape .label,#mermaid-svg-MiBi86e5dpx4VRnd .icon-shape .label{text-align:center;}#mermaid-svg-MiBi86e5dpx4VRnd .node.clickable{cursor:pointer;}#mermaid-svg-MiBi86e5dpx4VRnd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MiBi86e5dpx4VRnd .arrowheadPath{fill:#333333;}#mermaid-svg-MiBi86e5dpx4VRnd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MiBi86e5dpx4VRnd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MiBi86e5dpx4VRnd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MiBi86e5dpx4VRnd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MiBi86e5dpx4VRnd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MiBi86e5dpx4VRnd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MiBi86e5dpx4VRnd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MiBi86e5dpx4VRnd .cluster text{fill:#333;}#mermaid-svg-MiBi86e5dpx4VRnd .cluster span{color:#333;}#mermaid-svg-MiBi86e5dpx4VRnd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MiBi86e5dpx4VRnd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MiBi86e5dpx4VRnd rect.text{fill:none;stroke-width:0;}#mermaid-svg-MiBi86e5dpx4VRnd .icon-shape,#mermaid-svg-MiBi86e5dpx4VRnd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MiBi86e5dpx4VRnd .icon-shape p,#mermaid-svg-MiBi86e5dpx4VRnd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MiBi86e5dpx4VRnd .icon-shape .label rect,#mermaid-svg-MiBi86e5dpx4VRnd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MiBi86e5dpx4VRnd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MiBi86e5dpx4VRnd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MiBi86e5dpx4VRnd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
用户请求
规范化与限额
精确缓存命中?
返回缓存结果
轻量分类/路由
选定模型与上下文预算
LLM 调用
记录 usage、延迟、版本
按质量与成本复盘
二、Token 统计必须落在服务端,而不是依赖前端估算
前端可以估计输入长度,但最终计费以服务端实际发送的内容和供应商返回的 usage 为准。模型对中文、英文、代码、JSON 的分词都不同,字符数除以四这种经验公式无法用于结算。最可靠的是:响应成功时读取 API 返回的 usage;失败时记录"未知 usage"与失败阶段,而不是把它记成零。
下面是一个可运行的账本模块。它使用 Decimal 处理金额,避免浮点数积累误差;价格表按版本命名,调用方必须显式传入,不会因为悄悄修改全局常量而重算历史成本。实际接入时,请将供应商响应映射到 Usage,并把事件写入受访问控制的数据库或日志管道。
python
# cost_ledger.py
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
@dataclass(frozen=True)
class Usage:
input_tokens: int
output_tokens: int
cached_input_tokens: int = 0
@dataclass(frozen=True)
class PriceCard:
version: str
input_per_million: Decimal
cached_input_per_million: Decimal
output_per_million: Decimal
def request_cost(usage: Usage, price: PriceCard) -> Decimal:
if min(usage.input_tokens, usage.output_tokens, usage.cached_input_tokens) < 0:
raise ValueError("token 数不能为负")
if usage.cached_input_tokens > usage.input_tokens:
raise ValueError("缓存输入不能超过输入 token")
normal_input = usage.input_tokens - usage.cached_input_tokens
total = (
Decimal(normal_input) * price.input_per_million
+ Decimal(usage.cached_input_tokens) * price.cached_input_per_million
+ Decimal(usage.output_tokens) * price.output_per_million
) / Decimal(1_000_000)
return total.quantize(Decimal("0.000001"), rounding=ROUND_HALF_UP)
if __name__ == "__main__":
card = PriceCard("example-2026-08", Decimal("2.00"), Decimal("0.20"), Decimal("8.00"))
usage = Usage(input_tokens=12_000, cached_input_tokens=10_000, output_tokens=500)
cost = request_cost(usage, card)
print(f"cost={cost}, price_version={card.version}")
assert cost > Decimal("0")
这个示例中的价格只是演示计算,不能拿去当任何平台的报价。值得保留的是字段设计:model、price_version、request_id、feature、tenant_id、input_tokens、cached_input_tokens、output_tokens、duration_ms、status。tenant_id 最好存内部不可逆 ID,避免把邮箱、手机号、原始问题带入成本明细。原始 prompt 如果出于调试必须保留,也应单独加密、限期保存,并设置更严格权限。
三、先消灭无意义上下文,通常比换模型有效
上下文是最容易被"默认堆大"的成本项。工程上常见的来源有四个:把整个聊天历史拼回去、RAG 直接塞 Top-K 原文、把全部工具描述发送给模型、将数据库记录序列化为大段 JSON。它们都可以通过更严格的边界缩小,而不需要牺牲任务能力。
聊天历史适合"窗口 + 摘要"的组合:保留最近几轮的原始消息,把稳定事实、用户偏好和已完成动作压成短摘要。摘要也不是越早越好;刚刚出现且尚未被确认的信息不宜压缩成结论,否则模型会把猜测当作事实。对客服和工作流类应用,最好把关键状态放在结构化字段中,让模型读取少量已验证的状态,而不是反复从自然语言历史里猜。
RAG 的核心不是多取,而是取对。先检索一个略宽的候选集,再用重排或规则过滤,只把最终需要的少数片段放进提示词。每段都应携带文档 ID、版本和位置,既便于答案引用,也能在检索差时定位问题。把不相关的十段文本塞给强模型,通常会增加成本、稀释注意力,并不比三段高相关文本更可靠。
工具 schema 同样值得看一眼。把几十个工具的完整参数和说明无条件放入每次请求,会让每轮都付出固定输入费用。按页面、角色或任务阶段只注册可能用到的工具;工具结果应转换为紧凑、带字段白名单的数据,而不是原样回传整个第三方 JSON。这里的"紧凑"不能以丢掉审计字段为代价:对执行性工具,仍要保留操作 ID、状态和可校验的摘要。
输出预算也要变成产品规则。写摘要时设置合理最大输出 token,结构化场景要求严格 JSON 并限制数组上限,检索问答要求直接回答而非复述整篇材料。仅写"请简洁"不可靠,应该同时用 API 的输出上限和后端响应大小上限兜底。注意不要把最大输出调得太低导致响应频繁因长度截断;截断同样会浪费已生成 token,并诱发用户重试。
四、缓存的本质是复用,先选正确的缓存层
缓存并不只有一种。最简单的是精确结果缓存:同一版本的任务、同一规范化输入、同一模型参数,直接复用完整答案。它适合翻译固定术语、文档摘要、分类结果和不会变化的公共问答;不适合带当前时间、用户权限、私人上下文或随机性很强的创作请求。
第二层是提示词缓存。部分模型平台会自动或通过稳定的缓存键复用相同前缀的计算,这要求把稳定的系统提示、工具定义、静态文档放在前面,把每次变化的用户输入放在后面。不要为了缓存命中把用户私密内容移动到共享前缀;缓存是否跨请求、保存多久、是否影响零数据保留资格,必须查看所用供应商的官方数据控制说明。以 OpenAI 为例,官方文档明确说明扩展提示词缓存涉及 GPU 本地 application state,并与 Zero Data Retention 的资格有关。
第三层是语义缓存:找"意思相近"的问题并复用旧答。它最容易省钱,也最容易制造错误。相似度高不代表权限、时间、地域、产品版本相同;"退款规则是什么"和"我这个订单能退款吗"不能共享答案。语义缓存只能用于低风险、答案稳定的场景,命中后还应检查命名空间、知识库版本、用户角色和时间窗口。
缓存键必须包含所有会改变结果的输入:功能版本、提示词版本、模型、温度/采样参数、知识库版本、用户可见范围,以及规范化后的问题。少一个字段都可能造成串答或陈旧答案。下面的代码给出一个内存级精确缓存键与 TTL 判断,用于理解原则;进程重启会丢失数据,多实例生产环境应使用带 TTL 的共享存储,并限制缓存容量。
python
# exact_cache.py
import hashlib
import json
import time
from dataclasses import dataclass
@dataclass(frozen=True)
class CacheScope:
feature_version: str
model: str
prompt_version: str
knowledge_version: str
user_scope: str
temperature: float
def cache_key(scope: CacheScope, question: str) -> str:
normalized = " ".join(question.split())
payload = {"scope": scope.__dict__, "question": normalized}
raw = json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
def is_fresh(created_at: float, ttl_seconds: int, now: float | None = None) -> bool:
if ttl_seconds <= 0:
return False
return (time.time() if now is None else now) - created_at < ttl_seconds
if __name__ == "__main__":
scope = CacheScope("faq-v3", "model-a", "p12", "kb-20260831", "public", 0.0)
assert cache_key(scope, "退款 规则?") == cache_key(scope, "退款 规则?")
assert not is_fresh(100.0, 10, now=110.0)
缓存穿透和缓存投毒也要防。不要允许用户指定任意 cache key;不要把含有鉴权信息、Cookie、完整身份证号的响应作为可共享值;对于未命中而且昂贵的请求,应有每用户速率限制与队列。缓存失效时优先返回明确的可重试状态,不要让所有相同请求同时穿透到模型端。
五、模型路由:把简单题交给合适的模型,而不是"降级碰运气"
模型路由的目标不是每次都猜到最便宜模型,而是在预先定义的质量底线内,把任务交给成本更合适的路径。一个健康的路由器至少知道任务类型、风险等级、上下文长度、是否需要工具、是否需要视觉能力、租户预算和当前模型健康状态。它不该只依据用户文字长短做判断。
推荐从确定性规则开始。比如:涉及付款、删除、医疗、法律等高风险请求必须走人工/受控流程;需要长文档、复杂代码或多步工具调用的任务走能力更强路径;固定分类、意图识别和简单改写走经过评测验证的轻量模型。规则的优势是可解释,出了问题能找到为什么路由到某模型。等收集到足够的真实样本后,再考虑用轻量分类器做辅助,但不要把"模型判断该用哪个模型"作为没有边界的递归调用。
python
# router.py
from dataclasses import dataclass
@dataclass(frozen=True)
class RequestMeta:
task: str
input_tokens_estimate: int
needs_tools: bool
risk: str # low / high
monthly_budget_remaining: bool
def choose_route(meta: RequestMeta) -> str:
if meta.risk == "high":
return "guarded-review" # 不由模型直接执行高风险动作
if not meta.monthly_budget_remaining:
return "budget-exhausted"
if meta.needs_tools or meta.input_tokens_estimate > 8_000:
return "capable-model"
if meta.task in {"classify", "rewrite", "extract"}:
return "economy-model"
return "capable-model"
if __name__ == "__main__":
assert choose_route(RequestMeta("classify", 100, False, "low", True)) == "economy-model"
assert choose_route(RequestMeta("rewrite", 50, False, "high", True)) == "guarded-review"
路由器的第一版不要自动回退重试。先让每条路由都跑一段时间,用离线样本对比质量、P95 延迟、成功率和成本,确认"economy-model"确实覆盖该任务,再逐步扩大流量。自动回退容易让故障被掩盖:轻量模型失败后大模型补救,用户看到答案,但账单变成两次调用。必须回退时,要记录 initial_route、fallback_route 和失败原因,并限制一次请求最多一次替代调用。
六、预算不是硬截断,而是分层的护栏
最小的预算体系可以分四层:单请求上限、单用户/租户日上限、功能月预算、全局熔断阈值。单请求防止异常长输入或工具循环;租户配额防止少数用户挤占资源;功能预算让团队知道哪项产品最耗钱;全局阈值是在供应商故障、代码循环或攻击时保护账户。
预算触发后的行为要事先定义。例如低优先级摘要任务可以排队到次日,高优先级业务请求可以改走人工队列或给出稍后重试提示。最糟的是静默把质量降到不可用,或者在余额耗尽时让所有请求直接报 500。对用户显示的提示不需要透露内部模型和价格,但应该清楚说明当前功能暂不可用、是否能稍后重试。
安全边界也在这里:不要把供应商 API Key 下发到浏览器,让前端直接调用模型;后端必须根据已认证身份决定可用模型、最大输入和预算。计量接口需要鉴权,避免普通用户通过查询接口推断其他租户的使用情况。对 webhook、文件上传和第三方工具返回值做大小限制,防止攻击者构造超大上下文逼迫服务花钱。
七、用实验而不是直觉验证节省是否真的成立
每个优化都应该有一个对照。先固定一份匿名化评测集,覆盖真实的短问答、长上下文、结构化输出和失败样本;再记录优化前后的正确性、格式合规率、人工升级率、输入输出 token、缓存命中率、P50/P95 延迟和成本。只看"每次少了多少 token"不够:如果回答变差导致用户追问两轮,端到端成本可能上升。
缓存实验至少需要观察命中率与陈旧答案率。路由实验至少比较每条路径的质量和升级率。上下文裁剪实验则要特别检查是否删除了决策所需的关键条件。真实生产数据可能涉及隐私,离线评测集应做脱敏或使用经过授权的样本;不要把生产对话原文复制到公开 notebook 或第三方评测服务。
八、把成本归因到功能,而不是归因到"AI"
一个产品里通常不止一种模型调用:注册时的意图识别、知识库问答、文档摘要、客服回复草稿、后台标签提取、运营内容生成,它们的价值和风险都不同。若所有调用只写进同一个"llm_cost"计数器,月底只能知道总额,无法回答"哪个功能的每成功任务成本在上升""哪一类用户造成大量重试"。更实用的做法是给每次调用附上稳定的 feature 和 operation:例如 doc_qa.answer、ticket.classify、report.summary。
归因粒度也不能过细。把每个页面按钮都做成独立标签,会让报表碎成无法比较的长列表;建议按用户感知的能力和账单责任归组。模型版本、提示词版本、知识库版本属于维度,不应取代功能名称。对 A/B 实验,额外写 experiment_id,这样才能知道成本变化来自流量结构,还是来自模型本身。
有了归因,报表不需要复杂到第一天就上 BI 系统。每天按功能输出请求数、成功率、输入/输出 token、缓存命中、重试数、总成本和每成功请求成本;每周再按租户、模型、版本比较趋势。突然的总成本上涨通常会在这些拆分中变得具体:可能是某个新功能忘了限制文件大小,也可能是某次提示词改动让输出长度翻倍。
九、重试、流式与工具循环是隐藏的三笔账
网络超时和限流不可避免,但重试必须有预算。对读操作可使用带随机抖动的有限次退避;对会产生副作用的工具调用,必须携带幂等键,避免"客户端没有收到响应"时重复扣款或重复创建记录。模型请求若超时,不意味着供应商一定没执行;所以重试前需要检查请求 ID、状态查询能力和业务幂等性。将同一段长 prompt 原样重放三次,会直接把输入成本乘三。
流式响应改善了感知速度,却不自动降低费用。用户在页面关闭或网络断开时,后端是否继续生成、是否主动取消、供应商取消后已生成 token 如何计量,都是需要用目标平台文档和实际观测确认的行为。服务端应把客户端断开事件传递给推理请求的取消机制,并记录 cancelled 状态;否则用户以为已经离开,后端还在继续烧 token。取消也要有边界:不要因为浏览器短暂重连就误杀真正需要完成的后台任务。
工具循环的成本更隐蔽。一轮 Agent 可能先读数据、再搜索、再调用模型总结、然后因格式错误再来一轮。每一步都看似合理,合起来却远超普通聊天。应设定每个请求的最大工具步数、最大总 token、最大墙钟时间,并在每一步记录原因。达到上限时返回可理解的失败状态,并把轨迹交给人工或异步队列,而不是允许模型无限自我修正。
十、缓存命中率不是越高越好
缓存命中率高,可能是系统设计好,也可能是缓存范围过宽导致不同用户拿到同一答案。评估缓存时至少同时看四个数:命中率、命中节省的 token/金额、命中答案的人工负反馈率、缓存陈旧或越权事件数。对于权限敏感的知识库,每个用户或角色应该有隔离命名空间;对公共 FAQ,可把命名空间扩大,但仍要把知识库版本纳入键。
TTL 的选择由数据变化速度决定,而不是由缓存服务器容量决定。汇率、库存、排班、价格等动态内容不适合长 TTL;固定术语解释、模型生成的离线摘要可更长。若数据源支持版本号或更新时间,优先使用事件驱动失效:资料发布新版本时清掉相关键,而不是等一整天。做不到精确失效时,宁可 TTL 保守一些,并在回答中带可校验的资料版本。
语义缓存还要防止对抗输入。攻击者可以构造接近热门问题的表达,试图命中不应共享的答案;也可以通过大量相似查询污染缓存。所有语义命中必须在权限、租户、文档版本和任务类型过滤之后才可复用。缓存值不应携带内部系统提示、工具中间结果或安全策略细节,命中时也不应跳过内容审核和最终输出校验。
十一、模型路由应当有可回退的发布方式
第一次接入轻量模型时,最稳妥的不是立刻把全部流量切过去,而是做"影子评测":正常用户仍收到原路径的结果,后台用脱敏或已获授权的相同输入让候选路径生成答案,只记录指标,不展示给用户。若数据策略不允许影子调用,就用离线评测集和人工小批量试用替代。无论哪种方法,都要提前计算额外调用带来的预算,影子流量本身也会花钱。
第二阶段可做小比例灰度,并设置一键回滚条件。回滚触发不只看服务异常,还应包括结构化输出失败率、用户追问率、人工升级率和高风险样本错误。路由规则版本必须可追溯,不能只在代码里写一个 if 后就忘了何时修改。对于不同租户,应保证实验分配稳定,避免同一个用户上午使用轻量模型、下午随机换成另一条路径,导致体验和投诉都难解释。
路由错误时要返回哪条路径也需设计。若任务是简单改写但轻量模型返回解析错误,有限次数内可走同能力模型的修复流程;若是高风险授权问题,不能回退到"更会说"的大模型直接给答案,只能回到受控拒答或人工。成本优化从来不应该越过授权与安全边界。
十二、一个可执行的月度复盘模板
月度复盘建议按"事实---变化---动作"写,不要只截图账单。事实部分列出总调用、总 token、总成本、成功任务数、每成功任务成本、P95 延迟、缓存命中率、重试和预算拒绝数。变化部分与上个月或上个稳定版本对比,并按功能列出上涨/下降最大的三项。动作部分只保留有负责人和验证方法的改动,例如"将文档问答的重排后片段上限从 8 调到 4,使用固定评测集核对证据覆盖与追问率"。
还要列出没有做的优化及原因。比如语义缓存因权限隔离尚未完成而暂缓,模型路由因评测集不足而继续使用单模型。这并不是效率低,而是防止团队半年后重复讨论同一个风险。成本工作最怕"看见一个技巧就上线",最终系统里堆满无法解释的阈值和缓存规则。
十三、从提示词模板开始做版本化
成本分析中经常出现一个尴尬现象:某天输入 token 明显上涨,团队却找不到是谁改了什么。原因是系统提示、工具描述、检索拼接模板和输出格式散落在代码各处,没有版本号。解决方法不复杂:将每次会进入模型的稳定模板集中管理,给它一个可读版本,例如 qa-prompt-2026-08-31;请求日志记录这个版本,模板修改必须走代码评审或配置发布。
版本化不仅为了归因,也为了缓存正确。提示词变了,旧缓存通常不应继续命中;检索拼接格式变了,token 变化需要单独解释。对于很长的规则或工具定义,先删掉重复说明和历史遗留字段,再讨论压缩措辞。节省几十个 token 的前提是不能削弱关键安全约束或参数验证说明;这类内容应该由代码而不是自然语言承担时,就迁移到程序校验。
还可以给每类任务做一个"输入 token 预算":系统模板、历史、检索、工具、用户输入各占多少上限。某个部分超标时记录原因并按明确策略截断或转异步,而不是让模型接口在最后才因上下文过长失败。预算表不需要精确到每一个字,它的价值是逼迫团队为上下文做取舍。
十四、成本优化的红线
有三件事不该为了省 token 做:删掉鉴权与审计信息、把敏感用户共享到同一缓存、让模型替代后端的输入验证。前两件会导致越权和数据泄露,最后一件会把节省的提示词成本变成安全事故。对外部 URL、文件和工具结果做大小限制、类型验证和权限校验,既是安全要求,也是成本护栏。
同样不要通过隐藏模型能力变化来"省钱"。如果用户原本获得带引用的文档问答,后来路由到不检索的轻量模型,应当由产品明确该场景的能力与降级策略。成本策略需要和用户价值对齐:减少无效冗余可以立即做;改变答案质量、时效或可追溯性,则必须经过评测与产品确认。
十五、先用一个小仪表盘把问题看见
成本治理的第一周不必建设庞大平台。一个按天汇总的表就足够:每行一个功能和模型组合,列出调用量、成功率、输入/缓存/输出 token、平均与 P95 延迟、重试次数、总成本及每成功任务成本。再加上预算剩余和异常备注,就能在增长开始时发现问题。只要原始事件含有 request_id、版本和时间戳,未来需要接入数据仓库或可视化工具时也不用重新埋点。
这张表的价值在于促成具体对话。成本上涨时先问"哪一列变了",而不是问"谁把模型用贵了";质量投诉时先看是否与路由版本、缓存版本或上下文长度相关。先把事实收集好,再做最小改动,通常比一口气重写提示词、换模型、加缓存更便宜也更安全。
小结
控制大模型成本的顺序是:先把 token 与失败成本记清楚,再削减无意义上下文,然后对稳定任务做带边界的缓存,最后用评测过的规则路由模型。缓存键和路由规则都要包含权限、版本与风险,不然省下的是一点账单,换来的是串答、陈旧数据或越权。价格会变,模型会变,但按请求记录用量、延迟、质量和版本的习惯不会过期。