把 AI 账单砍掉 88%:独立开发者的多模型路由实战

进阶进行中LLM API昨天更新

适用场景: 有真实付费用户、AI 成本开始咬毛利的独立产品 前置条件: 你的产品已经在调某个 LLM API,手上能改到调用代码;本文示例用 Python + anthropic SDK

目标

8 月 8 日有个被广泛转发的案例:一位做 AI 写作工具的独立开发者,120 个付费用户、月收入 960 美元,把 AI 成本从 514 美元砍到 62 美元——降幅 88%,毛利率从勉强打平升到 94%,月活还涨到了 1500。他没换供应商、没砍功能,用的是三板斧:任务分级、模型路由、结果缓存。

这篇不讲空道理。我们拿一个具体的小服务——用户反馈处理(输入一条用户反馈,输出情感分类 + 一段客服回复草稿)——从「两步都用最贵的模型」一路改造到「分级路由 + 缓存」,代码首尾连贯,最后能整段跑出改造前后的成本对比。照着做,把同一套手法搬到你自己的产品上。示例用 Claude 系模型,思路对任何多档位的 LLM 供应商都通用。

改造前:朴素版在哪烧钱

大多数产品最初都这么写——所有请求都发给最强的模型,省事:

python
import anthropic

client = anthropic.Anthropic()

def text_of(resp):
    return next((b.text for b in resp.content if b.type == "text"), "")

# 客服话术规范:品牌语气、禁语、赔偿口径……真实场景通常几百到上千字
REPLY_SYSTEM = "你是资深客服。根据用户反馈写一段简短、诚恳的回复草稿。……"

def process_feedback_naive(text):
    # 第一步:分类。一个"正面/负面/中性"的判断,却在按旗舰价付费
    s = client.messages.create(
        model="claude-opus-5", max_tokens=16,
        system="你是分类器,只输出一个词:正面/负面/中性。",
        messages=[{"role": "user", "content": text}],
    )
    # 第二步:写回复
    r = client.messages.create(
        model="claude-opus-5", max_tokens=512,
        system=REPLY_SYSTEM,
        messages=[{"role": "user", "content": text}],
    )
    return text_of(s).strip(), text_of(r)

问题一眼可见:「把这段话分个类」这种简单活,也在按 Opus 的价格付费。 而这类简单请求,往往占了产品调用量的大头——这就是 88% 降幅的来源。下面三板斧逐个上。

第一板斧:任务分级

把服务里的每个 LLM 调用点按难度分三档。判断标准是「错一点会怎样」,不是「重不重要」:

  • 简单档:分类、打标签、抽取、格式化——输出短、规则清晰、错了重试即可;
  • 主链路档:大多数生成和推理——正文写作、总结、多步问答;
  • 高价值档:真正难、错了代价大的——复杂代码、深度分析、需要长链推理的。

我们这个服务:分类 = 简单档,回复 = 主链路档。 对照档位单价(Claude 系,每百万 token 输入/输出,以官方为准):

模型输入输出本服务用在
Haiku 4.5$1$5分类步
Sonnet 5$3$15回复步
Opus 5$5$25本例用不到,留给更难的任务

同一个分类请求,走 Haiku 比走 Opus 便宜 5 倍。

第二板斧:模型路由

把「哪档走哪个模型」抽成一张表,两步各自落档——分类下沉到 Haiku,回复放在 Sonnet:

python
MODEL_BY_TIER = {
    "simple": "claude-haiku-4-5",
    "main":   "claude-sonnet-5",
    "hard":   "claude-opus-5",
}

def classify(text):
    model = MODEL_BY_TIER["simple"]        # 简单档 → Haiku
    resp = client.messages.create(
        model=model, max_tokens=16,
        system="你是分类器,只输出一个词:正面/负面/中性。",
        messages=[{"role": "user", "content": text}],
    )
    return text_of(resp).strip()

def draft_reply(text, sentiment):
    model = MODEL_BY_TIER["main"]          # 主链路 → Sonnet
    resp = client.messages.create(
        model=model, max_tokens=512,
        system=REPLY_SYSTEM,
        messages=[{"role": "user",
                   "content": f"用户反馈(情感:{sentiment}):{text}"}],
    )
    return text_of(resp)

别把「省钱」凌驾到主链路质量上。用户直接看到的回复草稿留在 Sonnet,别为省钱降到 Haiku。降本降的是分类这种用户根本感知不到模型档位的步骤。

第三板斧:结果缓存

同样的输入不该付两次钱。两层缓存各管一段。

应用层缓存:完全相同的反馈直接命中本地缓存,连 API 都不发——用户反馈里重复内容很常见(「发货太慢」能收到几十条)。给分类步加一层:

python
import hashlib

_class_cache = {}   # 生产环境换成 Redis

def classify(text):
    key = hashlib.sha256(text.encode()).hexdigest()
    if key in _class_cache:
        return _class_cache[key]           # 命中,零成本
    model = MODEL_BY_TIER["simple"]
    resp = client.messages.create(
        model=model, max_tokens=16,
        system="你是分类器,只输出一个词:正面/负面/中性。",
        messages=[{"role": "user", "content": text}],
    )
    label = text_of(resp).strip()
    _class_cache[key] = label
    return label

供应商 prompt caching:回复步每次都带同一大段 REPLY_SYSTEM 前缀(客服规范),把它缓存下来,命中部分只按约 0.1 倍计费。给稳定前缀打断点,每次变的内容放在断点之后:

python
def draft_reply(text, sentiment):
    model = MODEL_BY_TIER["main"]
    resp = client.messages.create(
        model=model, max_tokens=512,
        system=[{
            "type": "text",
            "text": REPLY_SYSTEM,                 # 每次都一样的固定前缀
            "cache_control": {"type": "ephemeral"},
        }],
        messages=[{"role": "user",                # 只有这里每次变
                   "content": f"用户反馈(情感:{sentiment}):{text}"}],
    )
    return text_of(resp)

prompt caching 有最低可缓存长度(Sonnet 约 1024 token)。REPLY_SYSTEM 只有几十字是不会真缓存的——真实客服规范往往几百上千字,正好够格。前缀太短时 cache_read_input_tokens 会一直是 0,别以为是配置错了。

拼成可运行的完整版

三板斧到这里各就各位,组装起来就是最终的 process_feedback

python
def process_feedback(text):
    sentiment = classify(text)               # 简单档 → Haiku + 应用层缓存
    reply = draft_reply(text, sentiment)     # 主链路 → Sonnet + prompt caching
    return sentiment, reply

classifydraft_reply 就是上面第二、三板斧写好的版本——所有片段最终都落在这一个函数里。这就是「代码用在哪」的答案:它们共同组成一条可运行的反馈处理管道。

验证:跑对照,把降本算出来

降本幅度不能靠感觉,跑一遍对照。加一个成本累加器,按 usage 折算实际花费:

python
PRICE = {  # 每百万 token(输入, 输出),美元
    "claude-haiku-4-5": (1, 5),
    "claude-sonnet-5":  (3, 15),
    "claude-opus-5":    (5, 25),
}

total = 0.0

def add_cost(model, u):
    global total
    pin, pout = PRICE[model]
    read  = u.cache_read_input_tokens or 0       # 缓存命中,约 0.1 倍
    write = u.cache_creation_input_tokens or 0   # 缓存写入,约 1.25 倍
    total += (u.input_tokens * pin
              + read * pin * 0.1
              + write * pin * 1.25
              + u.output_tokens * pout) / 1_000_000

classify(未命中缓存那条分支)和 draft_replycreate 之后各补一行 add_cost(model, resp.usage),朴素版的两个 create 之后同样补上。然后跑同一批反馈对照:

python
feedbacks = [
    "东西不错就是发货慢",
    "客服态度差,再也不买了",
    "东西不错就是发货慢",     # 故意重复:改造版这条分类会命中缓存
    "物流很快,会回购",
]

total = 0.0
for f in feedbacks: process_feedback_naive(f)
print(f"朴素版(全 Opus):     ${total:.4f}")

total = 0.0
_class_cache.clear()
for f in feedbacks: process_feedback(f)
print(f"改造版(分级 + 缓存): ${total:.4f}")

你会看到两行数字,差距一目了然——第 3 条重复反馈在改造版里分类零成本,回复步则吃到 prompt caching 的折扣。案例里 88% 的降幅,主体就来自「简单请求下沉到 Haiku」这一步,缓存是叠加的第二层。把这套对照套到你自己产品调用量最大的那条链路上,先改那一条,账单立刻见效。

反复发相同前缀却发现 cache_read_input_tokens 一直是 0,多半是前缀里混进了「隐形失效元凶」——datetime.now()、随机 UUID、未排序的 JSON 塞进了 system。任何一个字节变化都会让整段缓存作废,把这些动态内容挪到断点之后。

常见问题

降档会不会掉质量? 简单档任务在 Haiku 和 Opus 上表现基本无差——分类、抽取本就不吃智力。吃智力的任务(本例的回复步)留在 Sonnet/Opus 即可,分级的意义就是别让两类请求一刀切用同一个模型。

缓存会不会返回过期结果? 应用层缓存要给会变的内容设过期时间(实时数据别缓存);供应商 prompt caching 有自己的 TTL(默认 5 分钟),只影响计费不影响正确性。

多供应商路由值得做吗? 单一供应商内部的三档路由已经能吃掉大部分收益,实现也简单。跨供应商路由收益递减、复杂度陡增,先把本篇三板斧做透再考虑。

把 AI 账单砍掉 88%:独立开发者的多模型路由实战 | 资讯狗 | Zixungou