Tiana随笔

推理预算:模型开始"思考"之后,延迟和成本该怎么算

· AI 与大模型

标签:#AI #大模型 #成本 #架构

推理预算:模型开始"思考"之后,延迟和成本该怎么算

思考型模型的账单波动能差十倍,延迟也从两秒变到四十秒。这篇给出一套任务分档的实操方法、分级路由代码、缓存与批处理两个省钱杠杆,以及怎么用评测脚本查表决策。

上半年我给博客加了个小功能,让 AI 帮我给文章生成标签。同一个提示词,我先用便宜那档模型跑了一遍,标签质量还行,就是偶尔会把"缓存"和"性能"两个标签同时打上,语义重叠。我又换成会"思考"的那档模型跑了一遍,结果它花了四倍的时间、八倍的钱,给出的标签里多了一个我确实没想到的角度,但同时把一个明显的技术名词归错了类。

那天我盯着两个结果想了一会儿,得出的结论不是"贵的好"或者"便宜的好",而是我一直在用一个很粗的方式分配算力。同一个提示词、同一个任务,我从来没想过"这个任务值不值得思考",默认全走最高的那档。

现在模型的推理预算已经变成一个可调参数了,有的叫思考等级,有的叫推理强度,有的干脆让你指定最大思考 token。参数能调,但没人告诉你该怎么调。这篇就写我这两三个月摸出来的一套判断方法,以及中间踩的坑。

先搞清楚钱花在哪儿了

会思考的模型和普通模型最大的结构性差异,是输出 token 的性质变了。

普通模型是"你问一句,它答一句",输入输出比例通常比较固定。思考型模型会在正式作答前先生成一大段内部推理,这段推理对多数 API 来说是要计费的输出 token,而且长度极不稳定:简单问题可能几十个 token,复杂问题几千个甚至上万。这就带来一个反直觉的现象:同一个接口,同一句提示词,账单可能差十倍,差在你控制不了的那部分。

延迟上的变化更直接。用户体感里有两段时间:发出请求到看到第一个字(首 token 时间),以及从第一个字到回答结束(流式生成时间)。普通模型的首 token 时间很短,输出也不长,所以总时长可以预期。思考型模型会让首 token 时间变得很长,因为那段时间它在内部推理,页面上什么都不显示。我一开始的写法是干等着,用户盯着转圈十秒,体验很差。后来改成先把"正在分析"这个状态流式推给前端,至少让人知道它没死。

还有一层容易被漏算:思考 token 会挤占上下文预算。如果你的逻辑是把历史对话整段带上,再叠加一个动辄几千 token 的思考输出,上下文窗口的消耗比预想快得多,得把这块算进去。

一个简单的三档判断法

我把任务按"思考的边际收益"分成三档,这是我现在用得最顺的一个框架。

不需要思考的:分类打标签、格式转换、字段抽取、简单翻译、摘要压缩、路由分发。这类任务的正确答案基本由输入决定,思考不会让它更对,只会更慢更贵。拿给文章打标签来说,它的本质是匹配,不是推理,用思考模型属于浪费。

思考有明确收益的:多步计算、代码逻辑设计、需要权衡的架构选择、带约束的排期方案、对长文的多角度审阅。这类任务的共同点是"答案不在输入里",需要模型自己推演几步。我给博客做重构方案时用它,收益很明显,它会主动指出我方案里两个模块的依赖方向反了。

必须思考但代价可接受的:数学证明、复杂并发问题、需要同时满足多个互相冲突约束的设计。这类任务少,出现频率低,该花就花,不用抠。

判断方法我自己总结成一句话:如果我把这个任务交给一个很聪明但只能看一眼、不许思考的同事,他能不能做对?能,就别让它思考;不能,才考虑加预算。这句话不一定严谨,但决策速度快,够用了。

预算怎么设,参数怎么用

设预算这件事,我的原则是分级路由加硬上限,不要把选择权留给默认值。

分级路由是指按任务类型直接指定档位。行业里现在比较通用的做法是把任务分成轻、中、重三档映射到不同的思考预算,重的走最高档,轻的直接走不带思考的模型或者把思考预算压到最低。我自己的实现大概长这样:

TIERS = {
    "classify": {"model": "small", "max_reasoning": 0},
    "extract":  {"model": "mid",   "max_reasoning": 0},
    "summarize": {"model": "mid",  "max_reasoning": 512},
    "design":   {"model": "flagship", "max_reasoning": 8000},
    "review":   {"model": "flagship", "max_reasoning": 8000},
}

def call(task_type, prompt, timeout=30):
    tier = TIERS[task_type]
    resp = client.chat(
        model=tier["model"],
        messages=[{"role": "user", "content": prompt}],
        max_reasoning_tokens=tier["max_reasoning"],
        timeout=timeout,
    )
    return resp

这里的 max_reasoning 是一个心理安全线,不是精确控制。不同厂商对它的解释不一样,有的当硬上限截断,有的只是建议值,你要按自己用的接口文档确认。但设了它有个好处:万一模型在一个问题上钻牛角尖,至少账单是可预期的。

硬上限是指对整个任务的 token 消耗设上限并且做熔断。这条我吃过亏:有个批处理任务跑了两千条数据,其中十几条触发了模型的长时间推理,最后那一晚的账单比前一周加起来还高。现在我的批量任务都会先按条数估算预期消耗,超过阈值就停下来报警,而不是默默跑完。

超时兜底是另一件必做的事。思考型任务的耗时波动很大,同一个提示词可能三秒回,也可能四十秒。我现在的做法是超时之后不重试同一档位,而是降级到不带思考的模型重试一次。理由是超时通常意味着这个问题对当前预算来说太难,重试同样配置大概率还是超时,而降级之后至少能拿到一个够用的答案。这条降级链路救过我几次值班。

一个真会翻车的细节:别把思考过程直接给用户看

现在不少产品会把模型的思考过程展示出来,挺多人觉得这样有透明度、有信任感。我最初也这么干,把思考内容流式显示在页面上,结果遇到两个问题。

一是思考内容里会包含不完整和自相矛盾的推导,用户看到模型先怀疑 A 又否定 A,最后选 B,观感其实是负面的,它会削弱用户对答案的信任,而不是增强。二是部分模型在思考过程中会出现半成品的措辞,直接展示容易引起误会。

我现在的做法是把思考过程留在服务端,给用户展示的是"它正在检查三个条件"这种由我这边生成的进度提示,而不是原始思考文本。既有等待反馈,又不会被原始推理误导。这个选择有点反直觉,但用过一轮之后我觉得它是对的:用户关心的是结论和依据,不是草稿。

别凭感觉判断值不值

我见过最多的浪费是把思考模型用在简单任务上,见最多的坑是把思考模型用在简单任务上还觉得"效果确实好",因为他只对比了输出质量,没算延迟和成本,也没注意到输出质量的提升在统计上根本看不出来。

所以评测这件事绕不过去。我的做法和之前写过的自建评测集差不多:从真实使用里挑三十到五十个有代表性的输入,同一个提示词分别跑不同的档位,记录下来三个数字,正确率(人工判定或者用规则判定)、总耗时、总花费。

跑一次大概半小时,得到一张只属于你的对照表。我那份表上的结论挺反直觉:摘要任务里高档位的正确率比低档位高不到百分之二,而成本是六倍;字段抽取任务两者完全没差别;只有代码设计类任务高档位拉开明显差距。有了这张表,后面所有路由决策都不用再猜。

需要提醒的是这个表会过期。模型迭代快,我那份表大概两个月就要重跑一次,因为厂商调整默认行为、或者新版本上线,结论可能翻盘。把它当季报看,而不是当宪法看。

那张对照表我用一个函数固化,换模型版本就重跑一次:

def worth_it(acc_low, acc_high, cost_low, cost_high):
    gain = acc_high - acc_low
    times = cost_high / cost_low
    if gain < 0.03 and times > 2:
        return "不值,用低档位"
    if gain > 0.1:
        return "值,保留高档位"
    return "看场景,按路由分"


print(worth_it(0.91, 0.925, 1, 6))
print(worth_it(0.62, 0.83, 1, 6))

缓存和批处理:两个真的能省钱的杠杆

推理预算之外,还有两个杠杆几乎不影响质量,但能直接把成本压下来,值得一起说。

缓存分两种。一种是提示词缓存,把不常变的部分(系统提示、风格约定、参考文档)标记成可缓存区,命中之后这部分输入 token 的价格大幅下降。对思考型模型尤其有用,因为它们的系统提示通常更长,而系统提示恰恰是最稳定的那部分。另一种是结果缓存,把"输入完全相同且输出可复用"的请求结果存下来。我给文章生成摘要时用了结果缓存,改标点、纠错字这类小编辑不会让摘要失效,判断条件是内容相似度超过阈值就直接复用。

批处理是另一条路。多数厂商对批量提交(比如承诺二十四小时内返回)给折扣,折扣幅度通常在三到五成。适合的场景是离线任务:生成全部文章的摘要、补全历史文章的元信息、跑全量评测。这类任务不急,走批量通道几乎是纯赚。我那次批量补标签的任务从实时改成批量之后,账单少了一半多,代价是等了一个小时。

两个杠杆叠加起来,常见的效果是整个应用的 LLM 账单比"全走最高档位加实时调用"低一个数量级。当然前提是你得先把任务分档做对,否则缓存和批处理只是在一个错误的成本基数上打折。

提示词要配合预算,别和它打架

这一点挺反直觉:如果你的提示词里写着"请一步步思考,先分析再给结论",那么用思考型模型时,思考预算的收益会被稀释,因为你在外面又套了一层手工流程。原来那套思路在普通模型上是必要的补丁,现在反而变成了累赘。

我现在的写法是把这类指令删掉,只留目标、约束和输出格式。给个对比,同一个任务两种提示词:

旧写法:请你一步步思考,先分析文章涉及的主要技术领域,
再考虑哪些领域是核心、哪些是次要,最后从候选中挑出三个标签。
注意不要选语义重叠的标签。

新写法:为这篇文章生成三个标签,要求覆盖核心技术领域,
不要选择语义重叠的标签。输出 JSON 数组。

新写法短得多,但前提是你得把模型设成带思考预算的那档。如果把它发给普通模型,它的表现会不如旧写法,因为它没有内部推理那一步,需要靠提示词引导。所以提示词和推理预算是配套的,改了一个就要检查另一个。

还有个相关的取舍:输出格式约束。要求严格 JSON 输出会让模型的推理空间变小(它得同时满足格式和内容),我遇到过一次思考型模型因为要输出严格 JSON,推理被压缩得比较浅,答案质量反而不如自由文本输出。解法是分两步:先让它自由作答,再让它把结论格式化成 JSON。多一次调用,但质量差距明显。

三个常见误区

误区一:以为思考预算越高越好。 我在摘要任务上试过把预算拉到最高,质量提升微乎其微,而延迟从两秒变成十几秒。大预算只在任务难度真的够高时才有回报,超出那个点之后就是纯浪费。背后的道理不复杂:预算是给推理深度用的,任务本身不需要那么深的推理时,多出来的额度换不来信息。

误区二:只看平均延迟。 平均值会掩盖长尾。我一开始的监控只看平均响应时间,看着挺好,实际上每二十个请求有一个卡到三十秒,那 5% 的用户体验是全废的。现在我会同时看中位数和 P95,P95 才是决定用户是否骂人的指标。思考型模型的 P95 波动天然比普通模型大,这一点在做产品预期时要提前想清楚。

误区三:把思考预算当成稳定的成本项做预算。 它的单次波动太大,不适合做线性估算。我的做法是按"每个任务档位乘以预期调用次数"算上限,再留一个总熔断线,而不是拍一个精确到分的月度预算。精确预算在这种波动面前没有任何意义,只会给你虚假的安全感。

评测脚本该长什么样

最后给一份能直接改着用的评测骨架,跑一次就能得到属于自己的那张对照表:

import json, time

CASES = json.load(open("eval_cases.json"))  # [{"id":..., "prompt":..., "expect":...}]
TIERS = [("no_think", 0), ("low", 1024), ("high", 8000)]

report = {}
for name, budget in TIERS:
    ok, total_ms, total_tok = 0, 0, 0
    for case in CASES:
        t0 = time.time()
        resp = client.chat(model="flagship",
                           messages=[{"role": "user", "content": case["prompt"]}],
                           max_reasoning_tokens=budget)
        total_ms += (time.time() - t0) * 1000
        total_tok += resp.usage.total_tokens
        if judge(resp.text, case["expect"]):   # 规则判定或模型判定
            ok += 1
    report[name] = {
        "accuracy": round(ok / len(CASES), 3),
        "avg_ms": int(total_ms / len(CASES)),
        "tokens": total_tok,
    }

print(json.dumps(report, ensure_ascii=False, indent=2))

跑完你会得到三行数字,正确率、平均耗时、总 token。我建议把这份报告存进版本库,每次模型升级或者调整档位策略时重跑一遍,对比着看。这样"要不要给这个任务加预算"这个问题就从讨论变成了查表。整个过程最重要的不是脚本,而是那几十条评测用例的质量,它们得覆盖你真实遇到过的疑难输入,而不是随手写的漂亮例子。

结尾

回到开头那个打标签的例子,我现在的做法是:标签任务走不带思考的便宜模型,跑完之后用一个规则检查器做交叉验证(比如两个标签的语义相似度超过阈值就报出来),发现可疑的才升级到思考模型重跑。整体成本比全走高档位降了八成多,标签质量比全走低档位高一些,因为疑难的那几%被专门处理了。

这套思路用一句话概括:把思考预算当成一种稀缺资源来分配,而不是一个开关。任务分档、设硬上限、超时降级、用数据验证,四件事做完,账单和延迟都会变得可预测,而你付的每一分算力都花在真正需要推演的地方。

顺便说一句,会思考的模型让"提示词工程"这件事简单了一些:以前我要反复调整措辞引导它一步步来,现在可以简单描述目标,把"怎么想"交给它的推理预算。参数化了反而是好事,因为清晰的目标描述比精巧的措辞更容易复用,也更容易写进规格里。

相关文章