大模型降本三板斧:缓存 / 摘要压缩 / 多模型路由(可复制 Python 骨架)

22 阅读5分钟

很多线上“成本失控”并不是模型太贵,而是:

  • 重复问题反复调用(没缓存)
  • history/context/tools 越叠越长(token 膨胀)
  • 简单任务也用强模型(单价不必要)
  • 高峰排队导致超时/重试(成本放大器)

这篇直接给你一个工程可落地的三板斧组合:

  1. 缓存:少调一次就少花一次钱
  2. 摘要/压缩:把 token 控进预算
  3. 多模型路由:把请求派给“合适的模型”,并有回退兜底

并附一个可复制的 Python 骨架(你把 call_llm 换成真实调用即可)。


0)你必须先打点(否则降本无法验证)

建议日志至少有:

  • input_tokens/output_tokens/total_tokens(P50/P95)
  • latency_ms(P50/P95)
  • error_rate/retry_rate/timeout_rate
  • cache_hit(按 cache 类型拆:response/tool/retrieval/semantic)
  • route_model/route_reason/fallback_count
  • prompt_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_urlapi_key)。
举个例子:我会用 147ai 这类聚合入口做路由与对比(具体模型与参数以其控制台/文档为准):

  • API Base URL:https://147ai.com
  • 端点:POST /v1/chat/completions
  • 鉴权:Authorization: Bearer <KEY>
  • 文档:https://147api.apifox.cn/