在 LLM 应用从聊天问答走向复杂任务求解之后,Token 就不再只是单纯的计费单位,而成为推理质量与成本之间的核心约束。Token-Budget-Aware LLM Reasoning(Token 预算感知的大模型推理)要解决的核心问题是:如何让模型在明确推理预算内给出答案,而不是对每道题都无条件地思考到最深。这类思路既希望保留长链推理在高难度问题上的准确率优势,又希望避免简单问题被过度思考带来的成本与延迟浪费。接下来从概念、机制、代码实现、评测方法和调优排查几个层面展开,适合正在做 LLM 应用、Agent 编排或推理成本优化的开发者阅读。
1. 先理解 LLM 推理中的 Token 预算问题
1.1 什么是推理 Token 与 Token 预算
用一句通俗的话说:模型在给出最终答案之前,通常会在内部先“想”一段话,这段思考过程会以 token 为单位被切分、计算和计费。技术上的定义是,token 是模型文本处理的最小单元,一个 token 可能是一个英文单词的一部分、一个中文汉字或一个标点;而推理 token 是模型在思维链(Chain of Thought)阶段生成的中间文本。
Token 预算就是对这段“思考过程”设置一个明确上限。它可以是提示词里的一句话,也可以是 API 参数里的一个整数,还可以是流式生成过程中的一个计数变量。设置预算的目的不是让模型闭嘴,而是让模型在一开始就意识到“资源有限,必须把推理用在关键步骤上”。
这里容易误解的点是:预算不应当只是一个“输出上限”。如果模型每道题都把小预算用完,然后被硬生生截断,那不是预算感知,而是截断。预算感知的关键在于,模型能根据问题难度决定用多长的推理链,并且能判断当前预算是否够用。
1.2 为什么推理长度不是越长越好
很长一段时间里,大家都相信“思维链越长,准确率越高”。这个判断在大方向上有道理,但一旦落到工程里,就会出现三个现实问题。
第一是成本线性增长。按 API 调用计费时,输出 token 越多,单次请求费用越高。如果每天处理几十万次请求,哪怕每题只多出几百个 token,累积成本也会非常可观。
第二是延迟上升。长推理意味着生成更多 token,也就意味着用户等待更长时间。在实时交互和 Agent 工具调用场景中,延迟超过一定阈值,体验会急剧下降。
第三是过度思考可能降低准确率。对于“2 + 2 等于几”这类问题,模型生成 2000 个 token 的推导过程,并没有带来更多信息量,反而增加了自我重复、逻辑漂移的概率。在弱模型上,过长的思维链有时会把自己绕进去。
在 Agent 场景里,还有一个容易被忽略的问题:工具调用结果、历史消息都会累计进入上下文。如果一个步骤的推理占用了太多 token,留给工具返回内容和后续记忆的空间就会变小,甚至触发上下文窗口溢出。
1.3 Token-Budget-Aware 的核心思路
Token-Budget-Aware 推理不是一个单一的技巧,而是一套策略,核心可以拆成三点:
- 按问题难度分配预算,而不是所有问题用同一个固定上限。
- 在预算内完成推理,并通过置信度判断当前答案是否可靠。
- 预算不足时,有节奏地扩展预算,而不是一次性给到最大。
用一个生活类比:一个正常人在回答“今天星期几”和“请证明哥德巴赫猜想”时,思考时间的分配是完全不同的。预算感知推理就是在模仿这种“按需分配注意力”的行为。
| 方案 | 基本做法 | 结果特征 |
|---|---|---|
| 普通长链推理 | 不分难度,全部使用长推理 | 难题准确率高,简单题成本浪费 |
| 固定硬截断 | 统一设置很小的 max_tokens | 简单题快,难题大量截断 |
| 预算感知推理 | 小预算起步,按置信度升级 | 简单题省 token,难题仍有机会长推理 |
2. 预算感知推理的四个关键机制
2.1 难度估计:先判断该花多少 Token
预算感知的第一步,是推断当前问题的难度。常见做法有三种。
第一种是让模型自评。在提示词里要求模型先简短判断题目难度,再决定推理详略。这种方式实现最简单,但模型的自评并不总是可靠,需要配合后续校验。
第二种是外部启发式判断。比如按问题类型、关键词、长度,或是否有数学符号、代码片段来分配初始预算。例如选择题给 200 token,证明题给 800 token。这种方式可控性强,但规则需要人工维护。
第三种是“小预算起步,按需扩展”。不管什么题目,都先给一个偏小的默认预算,如果答案置信度不足或结果被截断,再逐步扩大。这种方式不需要提前判断难度,更适合通用方案,也是后文示例采用的方式。
2.2 预算内生成:让推理在限制下完成
预算内生成指的是在生成过程中持续让模型感知到限制。限制可以来自三个层面。
提示词层面:在系统提示词和用户问题里明确写出“本次推理总 token 数不要超过多少个”。模型对 token 数量的感知是近似值,但强调“用尽量少的步骤完成推导”通常能显著压短输出。
API 参数层面:通过max_tokens或max_completion_tokens做硬性上限。这是最可靠的约束,但设置得太小会导致答案被截断。
生成过程层面:在流式输出中实时统计已生成 token,接近阈值时提前停止。这种方法需要精确计数,而且如果推理正好进行到一半被掐断,效果会比让模型自然收尾差。
2.3 置信度校验:怎么判断答案是否值得信任
预算扩展不能盲目进行,必须有一个“当前答案是否可信”的判断依据。常用方法有三种。
第一种是让模型口头表达置信度。在提示词中要求模型在答案末尾输出“非常确定 / 比较确定 / 不确定”。这种方法简单,但模型自报的置信度不等于真实概率。
第二种是读取 logprobs。通过 API 返回的对数概率,观察答案关键 token 的置信度。该方法更客观,但只对部分模型和部分 API 开放。
第三种是自一致性(Self-Consistency)。让模型用相同的较小预算采样多个答案,如果多数答案一致,就认为结论可靠;如果分歧很大,再升级预算。
需要特别说明:自一致性本身会成倍增加 token 消耗。因此预算感知场景下,通常是“先在小预算下采样少量几次,不一致再扩大预算”,而不是对所有问题都采样大量答案。
2.4 预算扩展:从“小预算”到“大预算”的退避策略
当一次小预算推理没有得到可信答案时,系统应该扩大预算重试。推荐的做法是按倍数递增,例如 300 → 600 → 1200,同时设置最大轮数和全局预算上限。
这里有两个容易犯的错。
第一,重试次数没有上限。如果模型一直不自信,循环会无限叠加成本,最终比一次性长推理还要贵。
第二,只看答案内容不看终止原因。如果 API 返回的finish_reason是length,说明输出是因为达到上限被截断,而不是模型自然写完。截断的答案即使看起来完整,也应当触发预算升级或重试。
注意:Token 预算不是简单的“输出长度限制”。预算感知的核心是让模型在“预算内作答”和“预算不足时升级”之间做出合理决策,而不是被 max_tokens 一截了之。
3. 实验环境与 Token 计量准备
3.1 依赖与运行环境
实验阶段不需要复杂框架,一个 Python 脚本就能完成。你至少需要三样东西:一个可调用的模型接口、一个 token 计数工具、一份带标准答案的评测题目。
| 组件 | 说明 | 示例 |
|---|---|---|
| Python | 脚本运行环境 | Python 3.10 及以上 |
| API 客户端 | 调用远端模型 | openai 等官方 SDK |
| 本地推理引擎 | 私有化运行模型 | vLLM、Ollama、transformers |
| Token 计数 | 统计输入输出 token | tiktoken、huggingface-tokenizers |
| 评测集 | 带标准答案的题目 | GSM8K、MATH,或自建题目集 |
如果使用本地模型,还需要注意运行精度对上下文的影响。fp16、bf16 与 fp32 会占用不同的显存,KV Cache 大小也直接影响能容纳的 token 总数。推理精度问题属于另一个技术话题,但在这里需要意识到:本地环境下,Token 预算不仅影响成本,还直接影响显存中能存放的推理长度。
3.2 Token 计数:不要用 len(text) 估算
很多人在控制预算时,习惯用len(text)计算“长度”,这在中文场景下会严重失真。token 是模型分词后的结果,不同分词器对同一段文本的分法完全不同。一般来说,英文约 0.7 到 1 个 token 对应一个单词,中文一个汉字可能对应 1 到 2 个 token,具体取决于模型和 tokenizer。
推荐使用 tiktoken 进行离线估算:
import tiktoken def count_tokens(text: str, model: str = "gpt-4o") -> int: # 不同模型默认的 tokenizer 可能不同,调用前先确认模型是否受支持 encoding = tiktoken.encoding_for_model(model) return len(encoding.encode(text)) text = "请计算 12 乘以 7,并给出详细推导过程。" print(count_tokens(text, "gpt-4o"))要注意,encoding_for_model并不能覆盖所有模型,尤其是部分开源模型没有直接映射。遇到这种情况,可以退化为tiktoken.get_encoding("o200k_base")或cl100k_base,再按实际模型分词器校准。
3.3 选择一个可评测的题目集
预算感知是否有效,必须用带标准答案的题目集检验。数学推理是最合适的场景,因为答案可判定,且推理长度差异明显。常用评测集包括 GSM8K、MATH 等,中文场景也可以使用自建的数学应用题集。开源社区中 DeepSeekMath 相关评测集也经常被用来衡量模型数学推理能力,这类题目集覆盖从简单四则运算到复杂证明的多个难度段,用来测试预算分配策略非常合适。
生产项目里,更建议先自建一份“小样本三档题目集”:10 道简单题、10 道中档题、10 道难题,每道题都带标准答案和期望难度标签。这份小集子用于策略调优,比直接在大评测集上反复试验更节省成本。
4. 用提示词实现基础版 Token 预算控制
4.1 显式预算提示模板
先写一个最基础的单次生成提示词,把预算要求写进用户消息。注意预算提示要放在问题前面,并且明确说明“推理与输出总长度都不要超过预算”。
请回答下面的问题。 要求: 1. 你用于推理和最终回答的总 token 数不要超过 {budget}。 2. 先判断题目难度:简单题直接给关键步骤,复杂题再逐步展开。 3. 推理要精简,不要重复已经表达过的观点。 4. 最终答案要单独成段,方便程序提取。 问题:{question}这里的关键是,模型并不能像程序一样精确数出自己的 token 数。预算提示的本质是通过“语言指令”约束模型的输出详略程度,而不是做精确计量。实际效果取决于模型的指令遵循能力,能力越强的模型,越能理解“尽量减少冗余推理”的含义。
4.2 最小闭环:预算内作答、置信度评估、必要时升级
下面用一个 Python 示例实现一个最小的自适应闭环。为了不绑定具体模型供应商,代码把“如何调用模型”抽象成一个generate回调函数。
from dataclasses import dataclass SYSTEM_PROMPT = ( "你是一个需要在预算内完成推理的助手。" "你必须用尽量少的 token 给出正确结果," "简单题直接推导,复杂题再逐步展开。" ) def build_user_prompt(question: str, budget: int) -> str: return ( f"请回答下面的问题。\n" f"要求:你用于推理和输出的总 token 数不要超过 {budget}。\n" f"问题:{question}\n" ) def is_confident(answer: str) -> bool: # 简易置信度判断,生产环境建议换成结构化置信度字段或 logprob low_confidence_markers = ["不确定", "无法判断", "可能", "大概"] return not any(marker in answer for marker in low_confidence_markers) @dataclass class SolveResult: answer: str used_budget: int rounds: int finish_reason: str def adaptive_solve( question: str, generate, base_budget: int = 300, max_budget: int = 1200, max_rounds: int = 3, ) -> SolveResult: budget = base_budget used_budget = 0 finish_reason = "" final_answer = "" for round_idx in range(max_rounds): final_answer, used_budget, finish_reason = generate( build_user_prompt(question, budget), max_completion_tokens=budget, ) # 只有自然结束且模型自信时,才认为当前预算足够 if finish_reason == "stop" and is_confident(final_answer): return SolveResult(final_answer, used_budget, round_idx + 1, finish_reason) budget = min(budget * 2, max_budget) return SolveResult(final_answer, used_budget, max_rounds, finish_reason)这个示例把“要不要升级预算”的判断拆成了两个条件:一是模型是否自然结束(finish_reason == "stop"),二是答案是否表现出足够自信。两个条件任何一个不满足,都会触发预算翻倍重试。
4.3 把它接到真实模型调用上
上面的generate回调需要绑定真实模型。以 OpenAI 风格 API 为例,伪代码如下。不同供应商的参数名可能不同,落地前以官方文档为准。
def build_openai_generate(client, model: str): def generate(prompt: str, max_completion_tokens: int): resp = client.chat.completions.create( model=model, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": prompt}, ], max_completion_tokens=max_completion_tokens, ) message = resp.choices[0].message finish_reason = resp.choices[0].finish_reason return message.content, resp.usage.completion_tokens, finish_reason return generate完成绑定后,adaptive_solve("一个三角形的三个角分别是 60、60、60 度,它是什么三角形?", gen)就会先以 300 token 预算尝试,如果答案自信且自然结束,就只花一次请求;如果模型不确定,就会升级到 600、1200 再试。
注意:不要用字符长度代替 token 长度。中文场景下,
len(text)可能只有 token 数的一半甚至三分之一。预算控制必须基于 token 计数,否则上限会失真。
5. 在 API 层面对预算做硬约束
5.1 理解 max_tokens 与 max_completion_tokens
提示词只能“软约束”,真正能防止超支的是 API 参数。这里最容易踩坑的是参数名的历史差异。
在较早的 OpenAI Chat Completions 接口中,输出上限参数是max_tokens。而在新的 reasoning 模型接口中,推理 token 和最终回答 token 会合并计算,参数名改为max_completion_tokens。如果使用了带内部思考过程的推理模型,max_tokens可能只覆盖最终输出部分,导致思维链部分占用额外 token,最终结果被意外截断。
| 参数 | 适用场景 | 说明 |
|---|---|---|
max_tokens | 传统对话补全模型 | 只限制接口返回的生成 token |
max_completion_tokens | 新版模型及推理模型 | 包含推理过程和最终输出 |
budget_tokens | 部分推理模型接口 | 专门限制推理阶段预算,具体以官方文档为准 |
建议在项目里统一封装一个get_output_limit(model)方法,根据模型类型返回正确的参数名,避免混用。
5.2 流式输出与实时预算监控
交互式场景通常使用流式输出。流式场景里,每个 chunk 不一定正好是一个 token,因此不能简单地对 chunk 数累加就算 token 数。正确做法是先把文本累积起来,流结束后再用 tokenizer 统计。
def stream_generate(client, model, messages, budget): collected = [] finish_reason = "" stream = client.chat.completions.create( model=model, messages=messages, max_completion_tokens=budget, stream=True, ) for chunk in stream: if not chunk.choices: continue delta = chunk.choices[0].delta if delta and delta.content: collected.append(delta.content) if chunk.choices[0].finish_reason: finish_reason = chunk.choices[0].finish_reason text = "".join(collected) return text, finish_reason流式输出来得早并不意味着推理已经完成。不要看到第一个finish_reason就立即返回,要综合判断流是否完整结束,并检查完整文本中是否出现“答案被截断”的异常特征。
5.3 用 finish_reason 判断是否被截断
API 返回的finish_reason是预算系统最关键的信号之一。
stop:模型自然生成了结束标记,答案完整。length:输出达到上限,被强制截断。content_filter:内容被过滤,需要检查输入或输出是否触发拦截。
在预算感知系统中,finish_reason == "length"必须触发处理逻辑。可以打印如下形式的信息:
print({ "finish_reason": finish_reason, "answer_length": len(answer), "estimated_tokens": count_tokens(answer), })出现length时,推荐策略是“小幅度扩展预算并重试一次”,而不是直接把截断答案交给下游。尤其是数学推导和代码生成场景,截断的答案往往缺少关键结论,即使前半部分看起来正确也不能使用。
6. 评测:判断“预算感知”是否真的有效
6.1 指标设计
引入预算控制之后,评测不能再只看准确率。项目里应同时记录准确率和 token 效率。
| 指标 | 计算方式 | 说明 |
|---|---|---|
| 准确率 | 正确题目数 / 总题目数 | 模型真实表现 |
| 平均 token 消耗 | 总生成 token / 题目数 | 成本侧核心指标 |
| 每题正确 token 消耗 | 正确题目总 token / 正确题目数 | 衡量“有效成本” |
| 过度思考率 | 较高预算下准确率未提升的题目占比 | 判断哪些题被浪费了预算 |
| 平均响应延迟 | 请求耗时均值 | 用户可感知指标 |
其中“每题正确 token 消耗”最值得关注。它衡量的是“为了做对一道题,平均要花多少 token”,能直接反映预算分配是否合理。
6.2 对比实验设计
要证明预算感知有效,至少做三组对比:
- 基线 A:不限制预算,使用默认长推理。
- 基线 B:统一硬上限,比如固定 800 token。
- 方法 C:自适应预算,300 起步,最多 1200。
每组使用相同模型、相同种子、相同题目集,记录准确率和总 token 消耗。预期结果应该是:C 的准确率接近或略低于 A,但 token 消耗明显下降;C 的准确率高于 B,尤其在难题上。
| 方案 | 准确率 | 平均 token | 每题正确 token |
|---|---|---|---|
| 基线 A:不限制 | 待记录 | 待记录 | 待记录 |
| 基线 B:固定 800 | 待记录 | 待记录 | 待记录 |
| 方法 C:自适应 | 待记录 | 待记录 | 待记录 |
6.3 防止评测偏差
评测时最容易出现三类偏差。
第一,只测难题。如果测试集全是高难度问题,预算感知在简单题上的节省就体现不出来,反而会得出“自适应不如固定长推理”的错误结论。
第二,只测简单题。忽略了难题上可能存在的准确率回退。
第三,没有记录finish_reason。如果固定上限方案大面积length截断,准确率低是截断导致的,而不是预算策略本身的问题。评测报告里必须同时给出每个方案的length比例。
注意:评测时一定要记录
finish_reason、输入 token 数和输出 token 数。否则你无法区分“模型答错”和“答案被截断”这两种完全不同的失败模式。
7. 常见问题与排查路径
7.1 五个高频问题及处理方案
问题 1:答案戛然而止,明显没写完。
常见原因是max_completion_tokens设置过小,输出被截断。检查响应里的finish_reason,如果是length,就需要增大预算,或精简提示词减少冗余要求。
问题 2:提示词里写了预算,但模型仍然输出很长。
提示词中的 token 数字只是自然语言约束,模型无法精确计数。处理方式:使用 API 硬上限;在提示词中补充“先用一句话给出推导,再用最终结论”的格式要求;或增加一两条精简短回答的 few-shot 示例。
问题 3:自适应循环升级多轮后,总 token 反而高于一次性长推理。
这说明退避策略设置不合理。可能原因是基础预算太小、重试次数太多。解法:提高基础预算、降低重试次数、增大每轮扩展系数,并把“全局总预算”纳入循环终止条件。
问题 4:Agent 场景中上下文越来越大,还没到最终答案就超出上下文窗口。
工具调用结果、历史消息都会占用上下文,预算感知不能只约束模型输出。解法:控制每轮写入上下文的工具结果长度,必要时做历史摘要,同时把“每轮推理预算 + 工具返回预算”合并计算。
问题 5:评测时发现准确率下降,但不知道是预算导致的还是模型随机性导致的。
大模型输出有采样随机性。至少要同配置跑 2 到 3 次取均值,并固定seed或采样参数,才能区分“策略导致的差异”和“随机波动”。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 输出被截断 | max tokens 过小 | 查看 finish_reason | 增大预算或精简提示词 |
| 预算提示不生效 | 模型无法精确计数 | 观察输出长度 | 加硬上限、加 few-shot |
| 多轮重试总成本偏高 | 基础预算过小或重试过多 | 统计每轮 token | 调大基础预算、限制轮数 |
| 上下文溢出 | 工具结果累积 | 查看上下文占用 | 压缩工具输出、做历史摘要 |
| 评测结果波动大 | 采样随机性 | 多次运行对比 | 固定 seed,多轮取均值 |
7.2 排查顺序建议
当一个请求表现异常时,按下面顺序排查:
- 先看提示词是否把任务描述清楚,模型是否理解“预算内作答”的约束。
- 再看参数名和参数位置,确认当前模型使用的是
max_tokens还是max_completion_tokens。 - 检查
finish_reason,判断是自然结束还是长度截断。 - 统计输入 token 与输出 token,确认预算是按 token 计算,而不是按字符串长度计算。
- 在 Agent 场景中,检查上下文里累积的工具返回内容是否挤占了推理空间。
- 最后才考虑更换提示词模板或调整重试策略。
8. 落地建议与扩展方向
8.1 学习环境、测试环境与生产环境的差异
在本地实验时,可以只写一个 Python 脚本,把预算和重试逻辑都写死在代码里。进入生产环境后,这些配置必须外置化,并补充监控和熔断。
| 维度 | 学习/开发环境 | 生产环境 |
|---|---|---|
| 配置 | 写死在代码里 | 配置中心或环境变量外置 |
| 日志 | 打印答案和 token 数 | 结构化日志,记录每次调用的预算和 finish_reason |
| 监控 | 无 | 按模型、按场景统计 token 消耗与成本 |
| 异常处理 | 直接抛异常 | 截断重试、预算熔断、默认兜底答案 |
| 缓存 | 无 | 相同问题缓存答案,减少重复推理 |
8.2 发布前检查清单
每次把预算感知逻辑交给下游前,建议逐项确认:
- [ ] 是否按 token 计量,而不是按字符长度或 chunk 数量?
- [ ] 是否正确区分了输入 token 与输出 token?
- [ ] 是否根据模型类型选择
max_tokens或max_completion_tokens? - [ ] 是否记录了
finish_reason,并处理了length截断? - [ ] 自适应重试是否有最大轮数和全局预算上限?
- [ ] 不同难度题目在测试集中是否都有覆盖?
- [ ] Agent 场景是否同时计算了工具返回内容占用的 token?
- [ ] 评测报告是否包含准确率、平均 token、每题正确 token 和延迟?
8.3 扩展方向
预算感知可以往三个方向延伸。
第一个方向是 Agent 与 ReAct 模式。ReAct 让模型在“推理”和“行动”之间交替,每多一次工具调用,上下文就多一层。预算感知在这里不仅约束单次推理长度,还要约束整个 Agent 轨迹的 token 消耗,比如限制工具调用次数、压缩工具输出、在关键节点插入摘要。
第二个方向是数学推理评测。GSM8K、MATH 以及 DeepSeekMath 这类数学评测集,答案判定明确,推理长度差异大,非常适合用来量化预算策略的成本收益。很多数学推理优化工作都会把“在保持准确率的前提下压低推理 token”作为重要目标。
第三个方向是推理效率的模型侧优化。社区里讨论的 LLM Wiki 思路关注的是知识如何被组织成可检索、可复用的页面,这和每次推理的 token 预算是互补关系:知识组织得越好,模型需要临时推理的内容就越少。更进一步,可以在训练阶段引入“长度感知奖励”,让模型在强化学习时同时优化正确率和 token 效率,使模型天然倾向于用简洁路径解题。
预算感知推理真正要打磨的,是做权衡的能力:知道什么时候应该省,知道什么时候必须加预算,也知道什么时候应该停下不再重试。建议先从一份十道题的样本集开始,记录每道题的准确率、token 消耗和截断次数,用数据决定下一步的预算参数,而不是靠感觉调数字。