很多线上“成本失控”并不是模型太贵,而是:
- 重复问题反复调用(没缓存)
- history/context/tools 越叠越长(token 膨胀)
- 简单任务也用强模型(单价不必要)
- 高峰排队导致超时/重试(成本放大器)
这篇直接给你一个工程可落地的三板斧组合:
- 缓存:少调一次就少花一次钱
- 摘要/压缩:把 token 控进预算
- 多模型路由:把请求派给“合适的模型”,并有回退兜底
并附一个可复制的 Python 骨架(你把 call_llm 换成真实调用即可)。
0)你必须先打点(否则降本无法验证)
建议日志至少有:
input_tokens/output_tokens/total_tokens(P50/P95)latency_ms(P50/P95)error_rate/retry_rate/timeout_ratecache_hit(按 cache 类型拆:response/tool/retrieval/semantic)route_model/route_reason/fallback_countprompt_version/strategy_version
只看平均值会被骗:预算与门禁建议用 P95。
1)缓存:四类缓存的边界(别只会“Redis 存一下”)
1.1 Response Cache(确定性响应缓存)
适用:输入高度重复、答案稳定(规则说明/模板问答/内部批处理)。
关键点:
- cache key = 规范化后的 messages + 参数 + 租户信息
- TTL + 版本号(规则更新要能失效)
- 必须租户隔离(防串数据)
1.2 Tool Cache(工具结果缓存)
工具调用往往更稳定、更容易缓存:配置、商品信息、政策条款、组织结构等。
加 TTL + 字段投影(只保留必要字段),通常能显著降 token。
1.3 Retrieval Cache(RAG 检索缓存)
缓存召回 chunk_id 列表或重排后的 top_n chunk_id 列表,减少检索/重排开销。
1.4 Semantic Cache(语义缓存)
省钱但有风险:相似问题复用答案。
建议只用于低风险任务,并且要有阈值 + 回退 + 监控。
2)摘要/压缩:三件事控 token(history/context/tools)
2.1 History:摘要 + 最近 N 轮
- 只在超预算时触发摘要(别每轮总结)
- 摘要可用更便宜的模型生成(任务简单)
2.2 Context:预算门禁 + 降级策略
把预算写死:context_budget_tokens/history_budget_tokens/max_output_tokens。
超预算时按顺序降级:
先砍 tools 冗余 → 再砍老 history(用摘要替代) → 最后才砍 context
2.3 Tools:字段投影/截断/摘要
不要把全量 JSON 塞进上下文:
- 字段投影(只保留必要字段)
- 长数组 TopN 截断
- 大文本条款摘要(保留条款编号/定位信息)
3)多模型路由:规则路由先落地,回退比路由更重要
第一版建议用规则路由就够用:
- 高风险(合规/财务/政策/支付):强模型
- 需要 citation:强模型(或强检索+中等模型)
- 低风险格式化任务(改写/摘要/分类/抽取):便宜模型
然后必须有回退:
- cheap → strong
- strong → 降级(模板/缓存/友好提示)
把 fallback_count 打点,否则你会以为省钱,实际一直在回退。
4)可复制 Python 骨架:把“压缩 + 路由 + 缓存”串起来
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
import hashlib
import json
import time
@dataclass
class ChatReq:
tenant_id: str
messages: List[Dict[str, str]]
task_tag: str # rewrite/summarize/classify/qa/policy...
require_citation: bool = False
max_output_tokens: int = 512
@dataclass
class ChatResp:
content: str
model: str
cached: bool
route_reason: str
fallback_count: int = 0
class SimpleTTLCache:
def __init__(self):
self._store: Dict[str, Tuple[float, Any]] = {}
def get(self, key: str) -> Optional[Any]:
v = self._store.get(key)
if not v:
return None
expire_at, payload = v
if time.time() > expire_at:
self._store.pop(key, None)
return None
return payload
def set(self, key: str, value: Any, ttl_sec: int):
self._store[key] = (time.time() + ttl_sec, value)
def stable_hash(obj: Any) -> str:
raw = json.dumps(obj, ensure_ascii=False, sort_keys=True).encode("utf-8")
return hashlib.sha256(raw).hexdigest()
def choose_model(req: ChatReq) -> Tuple[str, str]:
if req.task_tag in {"policy", "payment", "legal"}:
return "strong-model", "high_risk_task"
if req.require_citation:
return "strong-model", "need_citation"
if req.task_tag in {"rewrite", "summarize", "classify", "extract"}:
return "cheap-model", "low_risk_structured_task"
return "mid-model", "default"
def compress_messages(messages: List[Dict[str, str]], budget_chars: int = 6000) -> List[Dict[str, str]]:
# 占位示例:真实工程建议用 token 预算,并用摘要模型生成更准确摘要
total = sum(len(m.get("content", "")) for m in messages)
if total <= budget_chars:
return messages
tail = messages[-6:]
summary = {"role": "system", "content": "对话摘要:前文较长已省略(建议用摘要模型生成摘要)。"}
return [summary] + tail
def call_llm(model: str, messages: List[Dict[str, str]], max_tokens: int) -> str:
# TODO: 替换为真实调用(OpenAI兼容接口/自建服务都行)
return f"[stubbed] model={model}"
def chat(req: ChatReq, cache: SimpleTTLCache) -> ChatResp:
# 1) 控 token(压缩/摘要/预算)
messages = compress_messages(req.messages)
# 2) 路由(选模型)
model, reason = choose_model(req)
# 3) 响应缓存(必须租户隔离)
cache_key = stable_hash({
"tenant": req.tenant_id,
"model": model,
"messages": messages,
"max_output_tokens": req.max_output_tokens,
})
cached = cache.get(cache_key)
if cached:
return ChatResp(content=cached, model=model, cached=True, route_reason=reason)
# 4) 调用 + 回退
fallback_count = 0
try:
content = call_llm(model=model, messages=messages, max_tokens=req.max_output_tokens)
except Exception:
fallback_count += 1
content = call_llm(model="strong-model", messages=messages, max_tokens=req.max_output_tokens)
reason = f"{reason}+fallback"
# 5) 写缓存(TTL 需结合业务更新策略)
cache.set(cache_key, content, ttl_sec=600)
return ChatResp(content=content, model=model, cached=False, route_reason=reason, fallback_count=fallback_count)
5)上线 Checklist(按优先级)
- 先打点:token/延迟/错误率/重试率 + cache_hit + route/fallback
- 先门禁:max_input/max_output/context_budget/history_window
- 先止血:修重试(可重试/不可重试)
- 再缓存:response/tool/retrieval(先确定性缓存)
- 再路由:任务分级 + 默认/回退模型 + 回归样本守质量
资源区:多模型路由落地时,先把接入层统一(Key 分散是大坑)
路由/评测/A/B 需要频繁切模型。
如果每次切换都换 SDK/鉴权/网关,工程摩擦会很大。
更省事的方式是统一成 OpenAI 兼容入口(很多时候只改 base_url 与 api_key)。
举个例子:我会用 147ai 这类聚合入口做路由与对比(具体模型与参数以其控制台/文档为准):
- API Base URL:
https://147ai.com - 端点:
POST /v1/chat/completions - 鉴权:
Authorization: Bearer <KEY> - 文档:
https://147api.apifox.cn/