做过大模型应用开发的同学,一定遇到过这样的场景:同样一个问题,模型有时三言两语就能答完,有时却绕了一大圈,生成了一大段看似合理但与答案关系不大的内容。在多轮对话、Agent 任务、批量推理场景中,Token 消耗直接跟成本挂钩。等到月底账单出来,才发现大量的预算都被无效推理吞掉了。
这个问题的本质,是我们在调用 LLM 时缺少“预算意识”。模型本身并不知道你的成本上限,它只会按照预设的max_tokens或者自身的习惯去生成内容。如果你不主动管理,就会陷入“钱花了、效果还一般”的尴尬局面。
本文将围绕Token-Budget-Aware LLM Reasoning,也就是“感知 Token 预算的 LLM 推理”这一主题展开。我会先解释为什么要关注 Token 预算,再拆解 Token 计费与推理成本模型,然后介绍目前主流的预算感知推理思路,最后给出一套可以在 API 调用中落地的完整方案。适合正在做 LLM 应用开发、Agent 编排、RAG 问答系统的开发者阅读。读完你可以掌握如何控制推理长度、如何按需分配预算、如何减少无效 Token 消耗,并具备排查超限报错的能力。
1. 背景与核心概念
1.1 什么是 Token-Budget-Aware LLM Reasoning
先看一个朴素的问题:大模型是逐词生成的,它的每一步都会产生 Token。Token 可以简单理解为“模型看清楚文字的基本单位”,英文单词可能拆成多个 Token,中文一个字或几个字也可能对应一个或多个 Token。你给模型的输入(Prompt)是 Token,模型吐出来的回答也是 Token。每一次推理,消耗的是“输入 Token + 输出 Token”的总和。
Token-Budget-Aware LLM Reasoning,直译是“具备 Token 预算感知能力的 LLM 推理”。它指的是在让模型进行多步推理、工具调用、Agent 决策等高消耗任务时,系统或模型本身能对可用的 Token 总量有清晰认知,并据此调整推理策略和输出长度。
传统做法是“一刀切”,不管问题复杂度如何,统一设置max_tokens=2048或者更大的值。这样带来的直接后果是:简单问题生成过多,浪费成本;复杂问题在生成过程中突然截断,丢失推理结果。预算感知的推理追求的是“按需分配”——简单问题少花 Token,复杂问题集中资源推理,同时防止因为上下文过长导致可用输出空间被挤占。
理解这个概念,需要先分清几个容易混淆的说法:
| 术语 | 含义 | 容易混淆点 |
|---|---|---|
| 上下文窗口 | 模型一次能处理的最大 Token 总数,包括输入和输出 | 不等于输出上限 |
| max_tokens | 单次请求允许生成的最大输出 Token 数 | 受上下文窗口约束 |
| Token 预算 | 你为某次任务规划的最大可消耗 Token 量 | 通常是“输入 + 输出”的总额 |
| 推理长度 | 模型生成答案时实际输出的 Token 数 | 受 max_tokens 和自停止机制共同影响 |
1.2 它解决什么问题
Token 预算感知的推理,核心解决三类问题:
第一,成本失控。LLM API 大多按 Token 计费,输出 Token 通常比输入 Token 更贵。如果模型总是生成冗长答案,调用频率高的生产系统每个月会产生高昂费用。我在实际项目里见过一个很典型的例子:同一个查询接口,优化前平均输出 800 Token,优化后平均输出 200 Token,成本直接降了一半以上。
第二,上下文溢出。复杂的多轮 Agent 任务会不断追加工具返回结果和中间推理内容。如果不做预算管理,上下文很快会达到窗口上限,后面的请求直接被拒绝,系统表现为“聊着聊着就报错”。
第三,推理质量不稳定。模型的推理质量并不总是随长度提升而提升。过长的思维链会产生冗余推理甚至幻觉,而预算不足又会导致推理中途被截断。只有把预算控制在一个合理区间,才能在成本和效果之间找到平衡点。
1.3 常见应用场景
预算感知推理在以下场景中尤其重要:
- 大规模批处理任务:比如用 LLM 批量打标、信息抽取、文本分类,每一条样本的预算都应该有上限。
- 多轮 Agent 系统:Agent 在做任务时需要反复调用工具、阅读返回结果、规划下一步,每一轮都消耗 Token,必须做总量控制。
- RAG 问答:检索回的文档块越多,输入 Token 越大;回答越长,输出 Token 越大。预算分配决定了最终的回答质量。
- 流式输出应用:实时输出场景需要根据预算判断何时结束生成,避免用户等待过长的无意义内容。
2. Token 机制与推理成本拆解
要理解预算感知,先得理解 Token 是怎么被消耗的。
2.1 一次请求的计费模型
以 OpenAI 兼容接口为例,一次完整请求的 Token 消耗可以用下面公式表示:
本次请求费用 = 输入 Token 数 × 输入单价 + 输出 Token 数 × 输出单价其中:
- 输入 Token 包括 system prompt、用户问题、历史消息、工具返回结果、检索到的文档块。
- 输出 Token 是模型生成的全部内容,包括思考过程、最终回答、也会把工具调用的参数补充进去。
大多数模型厂商对输入 Token 和输出 Token 采用不同单价。通常输出侧更贵,所以在预算管理中,控制输出长度通常是性价比最高的手段。
2.2 上下文窗口约束
模型的上下文窗口是固定值,比如 8K、32K、128K。每次请求,输入 Token + 输出 Token必须小于窗口大小。
这里有一个常见误区:很多开发者把max_tokens理解为“模型会生成这么多 Token”。实际上max_tokens是上限,不是目标。模型可能在生成 50 个 Token 后就遇到结束符,也可能一直生成到上限才被迫停止。
另一个常见误区是:上下文窗口大小不等于你可以自由使用的空间。max_tokens设得太大,留给输入的空间就会被压缩。例如 8K 窗口,输入用了 6K Token,那么max_tokens最多只能设 2K,否则会直接报错。
我们用一个简化表理解:
可用输出空间 = 上下文窗口 - 输入 Token 数2.3 Token 消耗的隐藏开销
除了显而易见的输入输出之外,以下场景也会悄悄消耗 Token:
- 多轮历史消息重复发送:每次请求都会把历史消息重新计费。
- 结构化输出:强制模型输出 JSON 时,字段名和格式本身会占用 Token。
- 思维链与推理过程:模型在给出答案之前会生成大量中间推理,这些内容也计入输出。
- 工具调用参数:Agent 调用函数时,函数名、参数名、参数值都由模型生成。
- 结束符和格式标记:有些模型的特殊标记同样占用 Token。
理解这些开销之后,你就能明白,预算感知并不是简单调小max_tokens,而是一套从输入到输出全链路的策略。
3. 预算感知推理的核心思路
预算感知推理不是某一个具体算法,而是一类方法与策略的集合。下面拆开讲它的核心思路。
3.1 自适应长度控制
最简单直接的思路是:不再固定max_tokens,而是根据问题类型动态调节。
例如:
- 生成式问答:输出预算 800 Token。
- 分类抽取:输出预算 300 Token。
- 复杂数学推理:输出预算 1200 Token。
- 代码生成:输出预算 1500 Token。
这种静态按类型分配的方式实现成本低,效果也不错。但缺点是不够灵活,同一类型内部也可能有复杂度差异。
更进阶的方式是“先探后答”,也就是让模型先判断一次问题的复杂度,然后再决定输出预算。但这会多消耗一次请求的成本,适合对成本不敏感但要求精准的场景。
3.2 元推理:先判断后生成
元推理(Meta-Reasoning)是指模型在正式推理之前,先对“解这道题需要多少步”进行预测。比如模型先输出一个“难度评估”和“预估推理步数”,再决定要用多长的思维链。
以数学推理为例,一道两步加减法显然不需要像证明题那样输出 50 行推理过程。如果模型能在推理初期判断出问题的难度等级,就可以在中间步骤更早地收敛到答案,减少冗余推理。
需要说明的是,这类能力很多依赖于模型本身的训练水平。一些开源模型已经可以通过指令提示的方式实现简单的元推理。对于 API 开发者来说,更多是在 Prompt 层级做约束。
3.3 强化学习与自适应计算
在模型训练层面,有一类研究将“Token 预算”作为奖励函数的一部分。当模型以更少的 Token 得到正确答案时,获得更高的奖励;当模型使用了过多 Token 却未提升正确率时,奖励被适当削减。
这类方法追求的是“在正确率不下降的前提下,最小化推理长度”。学术界一般称之为自适应计算时间(Adaptive Computation Time)或推理时计算优化。通俗地说,就是让模型学会“想多久才够”。
对于大部分应用开发者,不需要自己去训练这样的模型,但要意识到:不同模型在推理长度控制上差异很大,选型时可以把“Token 利用效率”作为评估指标之一。
3.4 与 ReAct、CoT、Agent 范式的关系
ReAct 提出将推理(Reasoning)和行动(Acting)交替进行,Agent 在每次行动后观察环境反馈,再生成下一步推理。这种范式非常消耗 Token,因为每个步骤都会产生推理文本、工具调用参数、观察结果和历史累积。
Chain-of-Thought(CoT)则强调在回答前生成逐步推理过程。CoT 能提升复杂任务正确率,但会显著增加输出 Token。预算感知的推理不是否定 CoT,而是让 CoT 的长度与任务复杂度匹配。
在实践中,这三者经常组合使用:系统先通过元推理判断任务难度,中等难度任务走 CoT,复杂任务走 ReAct + 多步工具调用。每一层的 Token 预算都独立规划,这就是预算感知在工程上的真正体现。
关于“llm agent”的广泛讨论中,一个重要命题就是 Agent 的成本控制。一个不感知 Token 预算的 Agent,可能在一次简单查询中反复调用多次工具,而一个预算感知的 Agent 会在第一次工具返回结果足够充分时,果断终止调用,直接生成最终答案。
4. 实战:在 API 调用中实现预算感知推理
下面进入代码实战。我会以 Python + OpenAI 兼容接口为例,演示如何在工程中实现预算感知推理。完整代码可以直接复制到你的项目中调整使用。
4.1 基础环境准备
建议环境:
- Python 3.10+
- openai 库 1.x 或使用 requests 直接调用
- 一个可用的 LLM API(本文不限定具体厂商,接口兼容即可)
安装依赖:
pip install openai如果使用的是第三方兼容接口,可以通过base_url指定服务地址:
from openai import OpenAI client = OpenAI( api_key="your-api-key", # 换成你的 Key base_url="https://your-endpoint/v1" # 兼容接口地址 )说明:不同厂商的 API Key 获取方式和接口地址不同,这里以“兼容 OpenAI 协议”为例,具体参数请按你的服务商文档调整。
4.2 项目结构与配置
建议把预算配置独立成文件,方便不同业务线复用。
token_budget_demo/ ├── config.py # 预算配置 ├── token_helper.py # Token 估算与统计 ├── llm_client.py # 请求封装 ├── budget_agent.py # 预算感知推理逻辑 └── main.py # 运行示例4.3 预算配置
先定义不同任务类型的预算。这里把预算分为“消耗上限”和“输出上限”两层:
# 文件路径:token_budget_demo/config.py TASK_BUDGET = { "qa_simple": { "max_output_tokens": 300, # 输出 Token 上限 "input_budget": 2000, # 输入 Token 预算 "temperature": 0.3, }, "qa_complex": { "max_output_tokens": 1000, "input_budget": 6000, "temperature": 0.5, }, "extract": { "max_output_tokens": 400, "input_budget": 3000, "temperature": 0.1, }, "agent_step": { "max_output_tokens": 600, "input_budget": 3000, "temperature": 0.2, }, } CONTEXT_WINDOW = 8000 # 模型上下文窗口,按实际模型调整这里有一个关键设计:input_budget是给历史消息和上下文预留的上限。如果输入超过这个值,系统会触发裁剪策略,而不是盲目地把所有内容塞给模型。
4.4 Token 估算工具
在实际请求前,我们可以通过字符数粗略估算 Token 数。不同模型的分词器不同,但估算值已经足够用于预算分配。如果你的服务商提供了count_tokens接口,优先用官方接口。
# 文件路径:token_budget_demo/token_helper.py def estimate_tokens(text: str, language: str = "zh") -> int: """ 粗粒度估算 Token 数。 英文一般 1 Token ≈ 4 字符; 中文一般 1 Token ≈ 1~2 个汉字。 这里采用保守估算,宁多勿少。 """ if not text: return 0 if language == "zh": return int(len(text) * 1.3) # 中文字符偏大估算 return int(len(text) / 3.5) # 英文按 3.5 字符 1 Token估算函数说明:
- 中文场景下,把字符数乘以 1.3 是为了覆盖标点、特殊符号和模型分词误差。
- 英文场景下,按 3.5 字符估算 1 Token。
- 保守估算的好处是,即使误差存在,也大概率不会超出窗口。
4.5 预算感知的请求封装
在请求封装中,我们不直接暴露max_tokens给业务方,而是通过预算配置统一控制:
# 文件路径:token_budget_demo/llm_client.py from openai import OpenAI from config import CONTEXT_WINDOW from token_helper import estimate_tokens class BudgetAwareLLMClient: def __init__(self, api_key: str, base_url: str = None): self.client = OpenAI( api_key=api_key, base_url=base_url, ) def _check_input_budget(self, messages, input_budget) -> None: total_input = sum( estimate_tokens(msg.get("content", "")) for msg in messages ) if total_input > input_budget: raise ValueError( f"输入 Token 估算 {total_input} 超过预算 {input_budget}," "请裁剪历史消息后再请求" ) def chat(self, messages, task_type: str) -> str: """按预算配置发起对话请求""" budget = TASK_BUDGET[task_type] # 1. 校验输入预算 self._check_input_budget(messages, budget["input_budget"]) # 2. 根据输入 Token 计算剩余可用输出空间 input_tokens = sum( estimate_tokens(msg.get("content", "")) for msg in messages ) remaining = CONTEXT_WINDOW - input_tokens max_output = min(budget["max_output_tokens"], remaining) if max_output <= 0: raise ValueError("上下文空间不足,无法生成输出") # 3. 发起请求 resp = self.client.chat.completions.create( model="your-model", messages=messages, max_tokens=max_output, temperature=budget["temperature"], ) return resp.choices[0].message.content这段代码做了三件关键事情:
- 请求前校验输入预算,防止历史消息无限膨胀。
- 结合上下文窗口计算真实可用的
max_tokens,避免 400 报错。 - 按任务类型分配不同的输出上限和温度参数。
4.6 多轮消息的预算裁剪
多轮对话场景中,历史消息是最容易失控的地方。这里实现一个简单的裁剪策略:优先保留最近的对话,同时保证 system prompt 不被裁剪。
# 文件路径:token_budget_demo/token_helper.py def trim_messages(messages, input_budget: int, language: str = "zh"): """ 裁剪消息直到估算 Token 数不超过预算。 系统消息和最后一轮用户消息始终保留。 """ if not messages: return messages system_msg = None if messages[0]["role"] == "system": system_msg = messages[0] messages = messages[1:] # 最后一条用户消息保留 last_msg = messages[-1] history = messages[:-1] trimmed = [] total_tokens = estimate_tokens(last_msg.get("content", ""), language) if system_msg: total_tokens += estimate_tokens(system_msg.get("content", ""), language) for msg in reversed(history): msg_tokens = estimate_tokens(msg.get("content", ""), language) if total_tokens + msg_tokens > input_budget: break trimmed.insert(0, msg) total_tokens += msg_tokens result = [] if system_msg: result.append(system_msg) result.extend(trimmed) result.append(last_msg) return result这个函数的策略是:
- 从旧到新扫描历史消息,只要加上当前消息不会超过预算,就保留。
- 一旦超出预算,就丢弃更早的历史。
- 永远保留 system 和最新用户消息。
对于更复杂的场景,建议按照消息权重裁剪:工具返回结果可以压缩成摘要,系统提示词中可以移除不必要的示例,中间推理过程不一定需要完整保留。
4.7 一个简单的预算感知推理代理
下面实现一个简化版的预算感知 Agent。它不调用外部工具,只模拟“先评估难度、再分配预算”的过程:
# 文件路径:token_budget_demo/budget_agent.py from llm_client import BudgetAwareLLMClient class BudgetAwareAgent: def __init__(self, client: BudgetAwareLLMClient): self.client = client def _assess_difficulty(self, question: str) -> str: """ 用一个小预算请求评估问题复杂度。 这里使用 qa_simple 类型,限制输出长度,节省成本。 """ messages = [ { "role": "system", "content": "你是一个任务难度评估器。" "只输出一个词:simple、medium 或 complex。", }, {"role": "user", "content": f"评估问题难度:{question}"}, ] result = self.client.chat(messages, task_type="extract") answer = result.strip().lower() if answer not in ("simple", "medium", "complex"): return "simple" return answer def answer(self, question: str) -> str: difficulty = self._assess_difficulty(question) # 根据难度映射到不同预算 task_type = { "simple": "qa_simple", "medium": "qa_complex", "complex": "qa_complex", }[difficulty] if difficulty == "simple": sys_prompt = "请用简洁准确的语言回答用户问题,不要展开无关内容。" elif difficulty == "medium": sys_prompt = "请先简要分析,再给出结论。控制推理长度。" else: sys_prompt = "请逐步推理。每一步都为目标服务,避免重复和无效思考。" messages = [ {"role": "system", "content": sys_prompt}, {"role": "user", "content": question}, ] answer_text = self.client.chat(messages, task_type=task_type) return { "difficulty": difficulty, "task_type": task_type, "answer": answer_text, }这个示例展示了一个非常重要的设计思想:用一次小预算请求确定后续大预算请求的参数。虽然多了一次请求开销,但能避免后面的请求因为预算不足而失败。
4.8 运行与验证
写一个简单的 main 入口:
# 文件路径:token_budget_demo/main.py from llm_client import BudgetAwareLLMClient from budget_agent import BudgetAwareAgent from token_helper import trim_messages def main(): client = BudgetAwareLLMClient( api_key="your-api-key", base_url="https://your-endpoint/v1", ) agent = BudgetAwareAgent(client) questions = [ "中国的首都是哪里?", "请证明根号2是无理数,并说明这个证明的历史背景。", ] for q in questions: result = agent.answer(q) print(f"问题:{q}") print(f"难度:{result['difficulty']}") print(f"答案:{result['answer']}") print("-" * 50) if __name__ == "__main__": main()预期输出格式:
问题:中国的首都是哪里? 难度:simple 答案:北京。 -------------------------------------------------- 问题:请证明根号2是无理数,并说明这个证明的历史背景。 难度:complex 答案:(逐步证明与背景说明,长度受 qa_complex 预算控制) --------------------------------------------------注意:这里的难点判断只是示例。实际项目中,难度判断可以基于问题长度、关键词、历史准确率、检索结果数量等多个特征综合计算,不一定都要靠模型来判断。
5. 评测:如何判断预算感知效果
预算感知推理的效果不能只看“省了多少 Token”,还要看“质量有没有下降”。
5.1 评估指标
建议关注以下指标:
Token 利用率
Token 利用率 = 有效回答 Token 数 / 总输出 Token 数如果一个回答中包含了大量与问题无关的推理和重复表述,这个指标就会偏低。
预算消耗率
预算消耗率 = 实际消耗 Token / 预算 Token这个指标反映的是“预算分配是不是合理”,过高说明预算紧张,过低说明预算浪费。
任务正确率
这是最关键的质量指标。省 Token 不能以牺牲正确率为代价。通常的做法是:先在一个验证集上跑出基准正确率,再在预算收紧后对比正确率变化。
最大超限率
统计一段时间内因为超出上下文窗口导致请求失败的比例。预算感知系统应该把最大超限率控制在较低水平。
5.2 一个简单的 A/B 测试思路
在切换预算策略之前,建议做一次 A/B 测试:
- 选取 200~500 条真实业务请求。
- 用“固定 max_tokens”策略跑一遍,记录正确率、成本、失败率。
- 用“预算感知策略”跑一遍,记录相同指标。
- 对比两组数据的差异,确认预算策略没有显著降低正确率。
如果你使用的是开源模型,也可以在自己的数据集上测试不同解码参数对推理长度和正确率的影响。
6. 常见问题与排查思路
下面整理一些在 Token 预算和 LLM 推理中常见的问题。排查思路以实际操作经验为主。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 请求返回 400 错误,提示超出 max_tokens | 输入 Token 数过大,可用输出空间不足 | 先计算输入 Token,再动态计算 max_tokens |
| 请求返回 400 错误,提示 messages 格式问题 | 消息多轮对话中 content 字段类型不一致 | 统一将 content 转为字符串,不要传 None |
| 回答被截断,没有生成完整结果 | max_tokens 设得太小,或模型生成到上限 | 增大输出预算,或使用流式响应做提前终止判断 |
| 多轮对话越来越慢、越来越贵 | 历史消息持续累积,不做裁剪 | 使用 trim 策略,按预算裁剪历史消息 |
| 同一问题有时答得好、有时答得差 | 温度参数过高,或输出预算不稳定 | 降低 temperature,固定预算分配策略 |
| Agent 反复调用工具,消耗大量 Token | Agent 缺少终止条件,没有判断结果足够 | 在系统提示词中约束工具调用轮数,代码层做最大步数控制 |
| 模型返回 JSON 格式解析失败 | 输出预算不足导致 JSON 被截断 | 预留 JSON 格式结尾的 Token 空间,或使用结构化输出 |
| 输入 Token 估算不准,导致请求超限 | 估算方法太粗糙 | 使用官方 tokenizer 接口,或接入 tiktoken 精确计数 |
6.1 错误示例:未考虑输入 Token
很多初学开发者会这样写:
resp = client.chat.completions.create( model="your-model", messages=[ {"role": "user", "content": long_text}, ], max_tokens=7000, )如果这个模型上下文窗口是 8K,而long_text已经占用了 8000 Token,这个请求必然报错。即使没有long_text那么长,max_tokens=7000也会挤占输入空间。
正确的做法是:
input_tokens = estimate_tokens(long_text) available = CONTEXT_WINDOW - input_tokens max_output = min(7000, available, TASK_BUDGET["qa_complex"]["max_output_tokens"])6.2 错误示例:多轮对话不裁剪
多轮对话中,有些开发者会把所有历史消息都传给模型:
messages = [] for turn in all_history: messages.append({"role": "user", "content": turn["user"]}) messages.append({"role": "assistant", "content": turn["assistant"]})随着对话轮次增加,消息会越来越长,最终超出上下文窗口。更合理的方式是设置一个max_history_tokens的阈值,超出的历史消息只保留摘要或直接丢弃。
6.3 排查清单
遇到 Token 相关报错,推荐按以下顺序排查:
- 确认模型上下文窗口的官方数值。
- 用官方 tokenizer 或估算工具计算当前消息的总 Token 数。
- 计算可用输出空间 = 上下文窗口 - 输入 Token。
- 检查
max_tokens是否小于等于可用输出空间。 - 检查历史消息裁剪逻辑是否生效。
- 查看模型返回
finish_reason字段,区分“正常结束”“达到 max_tokens”“触发生成停止”。
finish_reason是一个重要的调试信息:
stop:模型遇到停止符,正常结束。length:达到 max_tokens 上限,被强制截断。content_filter:命中了内容过滤规则。
如果你发现大量请求的finish_reason是length,说明输出预算不够,需要调整。
7. 最佳实践与工程建议
下面是我在项目落地中总结出来的一些可执行建议。
7.1 把预算配置当成一等公民
不要在每个请求里硬编码max_tokens,而是像前面示例那样,把预算配置独立出来,按任务类型管理。这样好处很多:后续调整成本策略只需改配置文件;新增任务时能参考已有预算标准;排查问题时可以快速定位是哪个任务消耗了过高预算。
建议的配置维度:
- 输出 Token 上限。
- 输入 Token 预算。
- 温度。
- 是否开启流式输出。
- 是否允许工具调用。
- 最大工具调用轮数。
7.2 统一使用官方 Tokenizer
估算 Token 只能用于粗粒度控制。如果服务商提供了精确计数接口,优先使用:
import tiktoken enc = tiktoken.encoding_for_model("your-model") tokens = enc.encode("你的文本") print(len(tokens))用官方 tokenizer 做精确计数,可以最大程度避免超限报错。特别是输入文本长度不稳定、用户输入不可控的场景,这一步必不可少。
7.3 流式输出与提前终止
在交互式应用中,建议使用流式输出。当预算充足但模型已经生成了满意答案时,客户端可以在stop前主动断开,避免多余的 Token 消耗。
stream = client.chat.completions.create( model="your-model", messages=messages, max_tokens=1000, stream=True, ) for chunk in stream: delta = chunk.choices[0].delta.content if delta: print(delta, end="")流式输出的另一层好处是:你可以边生成边判断质量。如果发现模型开始重复说废话,可以直接终止流,节省成本。
7.4 为 Agent 设置硬性步数上限
LLM Agent 是 Token 消耗大户。无论模型训练得再好,都建议在代码层设置最大步数。不要只依赖模型自己“想停止”,而是在循环里显式判断。
MAX_STEPS = 5 for step in range(MAX_STEPS): response = agent_step(messages) if response["is_final"]: break else: # 超出最大步数,强制结束 final_answer = build_fallback_response(messages)这样做的目的是防止 Agent 陷入无限循环。即使模型已经产生了“再调用一次工具”的想法,代码层也可以在步数耗尽后强制收尾。
7.5 对工具返回结果做压缩
RAG 和 Agent 场景中,工具返回的文档和结果经常是大段文本。把整段文本塞进上下文,会快速消耗预算。建议对工具返回做摘要或切片:
- 只保留与当前问题最相关的段落。
- 对长文档分段后按相关性排序,取 Top-K 段。
- 对工具返回的结构化数据,只保留关键字段。
- 对历史搜索结果做缓存,避免重复检索和重复计费。
这些措施本质上都是在“输入侧”降低 Token 消耗。
7.6 记录 Token 消耗日志
在生产系统里,建议记录每一次请求的 Token 使用情况:
log_data = { "task_type": task_type, "model": model, "input_tokens": input_tokens, "output_tokens": output_tokens, "max_tokens": max_output, "finish_reason": finish_reason, "latency_ms": latency_ms, "total_cost": cost, }有了这些日志,你可以分析哪些任务消耗了最多预算、哪些请求经常被截断、哪些模型在相同效果下更省 Token。这些数据反过来又能优化你的预算配置。
7.7 不要盲目追求“极简输出”
预算感知不等于把输出压得越短越好。过短的输出可能缺少必要解释,对用户体验有负面影响。正确做法是:
- 先用一段话描述回答底线。
- 根据任务类型设定“最小回答长度”和“最大回答长度”。
- 在两者之间,允许模型自由发挥。
例如金融客服场景,答案不能只说“可以”,至少要有结论、理由和风险提示。这类业务的预算下限要比闲聊场景高。
7.8 选型时关注 Token 利用效率
不同模型在处理同样任务时,生成的 Token 数量差异可能很大。有的模型倾向长输出,有的模型比较简洁。在模型选型阶段,可以固定一组测试问题,对比各模型的输出 Token 数和正确率,计算单位正确率下的 Token 成本。这个指标比单纯看模型价格更有参考价值。
8. 总结与下一步
Token 预算感知是 LLM 应用从“能跑”走向“能省、能稳、能上线”的关键能力。本文从 Token 计费模型出发,解释了预算感知推理的基本思想,介绍了自适应长度控制、元推理、强化学习等不同层面的实现思路,并给出了一个工程可用的 Python 示例,包括预算配置、Token 估算、消息裁剪和难度自适应 Agent。
下一步你可以从这几个方向继续深入:
- 学习自己的模型服务商的分词器规则,建立更精确的 Token 统计工具。
- 在 RAG 项目中加入输入预算裁剪与检索结果压缩,观察成本变化。
- 研究当前主流开源模型的上下文窗口与推理长度表现,建立成本基线。
- 如果你的业务依赖 Agent 多步决策,可以尝试把最大步数和单步预算组合起来,做一个更完整的总预算控制方案。
在实际项目中,Token 预算最怕的不是花得多,而是花得不明不白。把预算配置、日志统计、超限监控这三件事做好,你的 LLM 应用在成本控制上就已经超过了大多数团队。