生产级 LLM 应用的 Token 预算策略
大多数团队发现他们上下文管理问题的方式都如出一辙:一个在演示中表现良好的生产级智能体,在对话进行 15 轮后开始出现幻觉。日志显示 JSON 格式正确,模型返回了 200 状态码,且没有人修改代码。变化的是累积效应——工具结果、检索到的文档和对话历史悄无声息地填满了上下文窗口,直到模型需要在 80,000 个相关性参差不齐的 Token 上进行推理。
上下文溢出(Context overflow)是显而易见的故障模式,但“上下文腐化”(context rot)则更具隐蔽性。研究表明,在达到限制之前,LLM 的性能就已经开始下降。随着上下文的增加,模型会出现“中间迷失”效应(lost-in-the-middle effect):注意力集中在输入的开头和结尾,而中间的内容则变得不可靠。埋藏在 30 轮对话中第 12 轮的指令可能会实际上消失。模型不会报错——它只是悄悄地忽略了它们。
这不是通过升级到百万级 Token 上下文窗口就能解决的问题。大窗口只是推迟了问题,而不是消除了问题。在容量利用率达到 60-70% 时,即使是宣称支持 1M+ Token 的模型,性能也会开始下降。真实的生产系统需要明确的 Token 预算策略。
为什么 Token 成本的累积速度比你预想的快
假设一个生产级支持智能体拥有 3,000 Token 的系统提示词。每次查询增加 4,000 Token 的检索文档,500 Token 的对话历史(每轮都在增长),以及每步 200 Token 的工具调用结果。到第 8 轮时,每次调用的 Token 数将达到 12,000+。
按每 1K 输入 Token 0.05 美元计算,每月 100 万次 API 调用意味着仅系统提示词一项每月就要花费 15 万美元。加上检索和历史记录,在计算输出 Token 之前,你的花费将超过 50 万美元。不追踪这一指标的团队通常会被他们的第一份全额发票吓到。
累积问题在多智能体工作流中更加严重。一个拥有 20 次 LLM 调用的流水线,如果每个步骤都将结果传递给下一个步骤,在生成最终答案之前可能会累积 50,000+ Token。每一个中间步骤都要计费。每一个中间步骤都会影响质量。
解决方法不是随意削减上下文——那会导致其自身的问题。解决方法是将 Token 预算视为一等公民的工程关注点,并采用深思熟虑的分配策略。
使用特定模型的 Tokenizer,而非近似值
在管理 Token 预算之前,你需要准确的计数。“每个 Token 4 个字符”的启发式方 法错误率极高,足以影响生产环境。
不同的模型使用不同的 Tokenizer:
- OpenAI GPT-4o:使用
cl100k_base编码的tiktoken(较新的gpt-4o-2024-11-20使用o200k_base) - Anthropic Claude:使用
count_tokens()终端节点——它是免费的且不消耗速率限制 - Google Gemini:使用官方 SDK 中的
count_tokens()方法(基于 SentencePiece) - Meta Llama:使用 Llama 仓库中精确的 HuggingFace Tokenizer
使用 tiktoken 来计算 Claude 的 Token 可能会产生 30-50% 的误差。这个误差范围会耗尽你的安全缓冲并导致意外的截断。
准确的计数还需要考虑消息格式的开销。在 OpenAI Chat Completions API 中,每条消息会为 ChatML 包装器增加约 4 个 Token。函数调用模式(schema)会增加更多。生产环境的代码应该始终进行精确计算:
import tiktoken
def count_chat_tokens(messages: list[dict], model: str = "gpt-4o") -> int:
enc = tiktoken.encoding_for_model(model)
total = 3 # completion primer overhead
for msg in messages:
total += 4 # per-message wrapper
for key, value in msg.items():
total += len(enc.encode(str(value)))
return total
显式预留输出 Token。如果你的模型最多可以生成 2,000 个 Token 的输出,请将你的上下文预算设置为 context_limit - 2000。永远不要让输入占满整个窗口。
按优先级而非时间顺序分配预算
上下文管理的幼稚做法是按时间顺序:保留所有内容直到达到上限,然后丢弃最旧的内容。这种做法会失败,因为最旧的内容通常包含你的系统提示词、初始约束和关键指令。静默丢弃这些内容会破坏智能体的行为。
相反,应定义明确的预算层级:
| 层级 | 内容 | 预算 |
|---|---|---|
| 受保护 (Protected) | 系统提示词、当前查询 | 始终包含 |
| 高优先级 (High priority) | 当前工具结果、最新的检索文档 | 约占窗口的 30% |
| 中优先级 (Medium priority) | 最近的对话历史(最近 5 轮) | 约占窗口의 25% |
| 低优先级 (Low priority) | 较旧的对话历史 |
