为什么你的 Prompt 太长:常见的 token 计数错误

一个 prompt 在编辑器里看起来很短,实际却仍然可能因上下文长度错误而失败。通常原因并不是模型“算错了”。而是你计算的数字并不是模型实际收到的完整请求。
开发者经常只统计可见的用户消息,按词数或字符数估算,然后把这个数字与模型的上下文窗口比较。但实际输入还可能包括 system 或 developer 指令、对话历史、检索到的文档、工具定义、JSON schema、附件,以及特定于模型的格式化内容。你还需要为输出留出空间。
更安全的规则是:
统计精确序列化后的请求,预留输出容量,然后验证线上端点报告的用量。
本指南解释最常见的 token 计数错误,并提供你可以借助 TokenTest 在端点上运行的实用测试。
“prompt 太长”通常是什么意思
在实践中,“prompt 太长”可能描述几种不同的失败:
- 完整输入超过了模型或端点接受的上下文长度。
- 输入本身能放下,但请求的输出让整个请求没有足够空间。
- 某个框架在你计数之后,静默添加了历史记录、检索上下文、工具或 schema。
- 路由器把你的请求解析到与预期不同的模型或 tokenizer。
- 你的本地估算与提供方的服务器端计费/计数方式衡量的不是同一件事。
- 从技术上说请求能放下,但被中间层截断、拒绝或以不同方式计费。
先把错误当作请求构造问题来处理。在缩短有用指令之前,先确定到底是哪一层在消耗预算。
错误 1:把 token 当作词或字符
Token 是 tokenizer 生成的文本片段。它们并不可靠地等同于词、字符、字节或行。
标点、空格、大小写、较长的标识符、数字、emoji、重音符号文本、中文文本和代码的拆分方式都可能不同。即使看起来相似的字符串,也可能产生不同的 token 数量。
例如,这些输入在视觉长度上相近,但结构差异很大:
Reset the user password after identity verification.
{"action":"reset_password","requires_identity_verification":true}
验证身份后重置用户密码。
一个固定的“每个 token 四个字符”或“每个单词一个 token”规则,可能足以在已知的英文语料上做粗略的早期估算。但它不适合用于强制执行生产环境限制。
更好的做法:在可用时,使用与目标模型对应的 tokenizer。对于结构化 API 请求,如果提供方提供了精确的服务端计数方法,就使用该方法。
错误 2:使用了错误的模型或 tokenizer
token 数量取决于 tokenizer。用一个方便的 tokenizer 来计数,却发送给另一个模型,会造成一种虚假的精确感。
这个问题会在以下情况下出现:
- 你更换了模型家族,但保留了旧的计数代码;
- 一个网关在一个名称下别名了多个上游模型;
- 路由器在不更改你的应用配置的情况下更改了解析后的模型;
- 本地库尚未识别较新的模型,并回退到默认编码;
- 你使用单个 tokenizer 比较两个提供商。
OpenAI 的 token 计数指南明确将模型映射到编码,并建议使用与模型相关的编码查找。其较新的 API 指南还将本地字符串 tokenization 与结构化 API 输入的精确计数区分开来。
更好的做法:在每次测量中同时记录 requested_model 和 resolved_model。在测试中固定 tokenizer 版本。如果解析后的模型发生变化,就让旧基线失效。
错误 3:只计算可见的用户 prompt
你在 prompt 编辑器中看到的文本,通常只是请求的一层。
一个真实的应用可能会发送:
system 或 developer 指令
+ 对话历史
+ 当前用户消息
+ 检索到的文档
+ 文件或图像输入
+ 工具定义
+ JSON schema
+ 响应格式说明
+ 提供商特定的消息格式化
= 实际呈现给模型的输入
如果你的界面显示的是 2,000 token 的用户消息,但你的框架附加了 8,000 token 的历史记录和检索内容,那么实际输入并不是 2,000 token。
这在聊天机器人中尤其常见。每一轮都可能重放更早的消息。一个十轮前还能正常工作的对话,即使最新的用户消息很短,也可能跨过限制。
更好的做法:建立一个组件级台账:
| 请求组件 | 是否计入? | 压缩规则 |
|---|---|---|
| System/developer 指令 | 是 | 去重重复的策略 |
| 对话历史 | 是 | 对旧轮次做摘要或窗口化 |
| 当前用户消息 | 是 | 保留实际任务 |
| 检索上下文 | 是 | 对片段排序、过滤并设上限 |
| 工具和 schema | 是 | 只加载相关定义 |
| 附件 | 是 | 使用提供商的计数方法 |
| 输出预留 | 是 | 为有效回答预留空间 |
不要把用户消息的计数标记为总 prompt 计数。
错误 4:忘记 tools、schema 和框架包装层
工具调用会增加令人意外的大量输入。函数名通常开销很小;冗长的描述、重复的示例、嵌套 schema、枚举以及响应格式定义则不是。
框架也可能把一个紧凑的配置转换成更大的线上负载。一个工具可能只在应用代码中定义一次,但会序列化到每一次请求中。
考虑一个拥有 30 个工具的支持代理。如果当前问题只需要订单查询和退款资格,那么发送全部 30 个定义会浪费上下文,并可能让工具选择更困难。
更好的做法:在 API 调用之前,立即捕获最终的请求负载。统计或测量那个版本,而不是更早的 prompt 模板。仅动态加载与当前路由相关的工具,并保持描述精确。
错误 5:把整个上下文窗口都用于输入
模型的上下文容量并不自动等同于输入额度。请求还需要为输出预留空间,而且某些系统可能会在整体预算中将额外生成 token 或推理 token 计入其中。
如果某个应用几乎用尽了可用上下文来放输入,然后又要求输出一段很长的回答,就会出现以下三种情况之一:
- 请求在生成前被拒绝。
- 答案在达到 token 限制时提前停止。
- 系统以某种会破坏任务的方式裁剪输入或输出。
更好的做法:在组装上下文之前先定义预算:
安全输入预算
= 支持的上下文容量
- 必需的输出预留
- 运行安全余量
输出预留应根据任务来定,而不是根据剩余空间来定。分类响应可能只需要很少的空间。代码补丁、结构化报告或多步骤分析可能需要更多。
错误 6:统计草稿,而不是生产请求
小的转换会不断累积:
- 模板展开变量;
- Markdown 变成转义后的 JSON;
- 日志添加时间戳和元数据;
- 检索会附加标题、URL、分隔符和引用;
- 由于状态 bug,聊天历史被重复了一遍;
- 在统计之后又附加了响应 schema;
- SDK 或提供方插入控制标记。
在这些转换之前进行统计,回答的是错误的问题。
更好的做法:在请求管道中尽可能靠后的时点进行统计。将被统计负载的哈希值与测量结果一起保存。如果在传输前哈希值发生变化,那么这个计数就是过时的。
错误 7:假设所有粘贴内容都有相近的 token 密度
开发者常常因为按字符数或行数比较文件,而修剪错了内容。
高 token 密度内容可能包括:
- 压缩后的 JavaScript 或 CSS;
- 生成的代码和 lockfile;
- 带有重复路径的长堆栈跟踪;
- 键名重复的 JSON;
- UUID、哈希、时间戳和编码数据;
- 多语言文本;
- 带有重复分隔符的表格;
- 重复同一事件数百次的日志。
相较之下,一段更长的英文自然语言说明,可能比一段更短但噪声很大的机器数据更省 token。
更好的做法:分别测量各个组件。先移除低价值、高 token 的内容:重复日志、无关帧、压缩后的资源、生成文件、过时的检索片段,以及无关的工具 schema。
错误 8:把本地计数当作最终用量
本地分词统计很有用,但它并不总是最终的运行时数字。
实时端点可能会计入结构化消息、缓存输入、图片、文件、工具、schema,或特定提供商的格式化方式。一个与 OpenAI 兼容的网关也可能报告不完整或内部不一致的 usage 字段。
请将这些度量分开:
| Measurement | What it tells you |
|---|---|
artifact_tokens_local |
粘贴的文档、代码或日志的大小 |
request_tokens_local |
组装后请求的本地估计值 |
input_tokens_reported |
实时端点报告的输入用量 |
cached_input_tokens |
单独报告的复用输入(如支持) |
output_tokens_reported |
生成输出的用量 |
total_tokens_reported |
提供商报告的总量(如果有) |
不要把缺失字段强行设为零。将缺失值存储为 null,因为“未报告”和“报告为零”是不同的事实。
用于调试长 prompt 的实用 TokenTest 工作流
TokenTest 不是模型特定 tokenizer 的替代品。它会在 tokenizer 之外验证实时端点:模型身份、usage 完整性、输入 token 行为、输出上限、缓存证据以及协议一致性。
TokenTest 产品手册 描述了用于检查输入 token 单调性、总 token 一致性、输出合理性、stop/token-limit 关联、流式 usage 以及缓存 token 行为的方法。在组装好真实请求后,请使用这些检查。
测试 1:短 prompt 与扩展 prompt 对比
使用相同的任务和模型发送两个请求:
Version A: current user task only
Version B: system prompt + history + retrieved context + current task
报告的输入用量应当朝着可信的方向上升。如果它保持不变、以可疑的固定量跳变,或者消失了,就要检查端点的 usage 报告。
测试 2:无工具与启用工具对比
同一条用户消息先在不带工具的情况下运行一次,再使用生产环境的工具 schema 运行一次。
记录:
- 请求和解析后的模型;
- 本地请求计数;
- 报告的输入、缓存输入、输出和总量;
- 结束原因或停止条件;
- 答案是否仍然完成了任务。
这可以揭示你的工具表面的实际成本。
测试 3:输出预留与停止行为
保持输入不变,改变输出限制。确认端点的停止信号和报告的输出用量表现一致。
一个 prompt 不能仅仅因为请求被接受就算生产安全。它必须留出足够空间来完成所需的回答。
测试 4:为缓存证据重复执行
当你的提供方支持 prompt 缓存时,请对同一个稳定前缀请求发送两次。检查是否出现与缓存相关的用量,以及总计账是否仍然一致。不要因为文本相同,就假设重复输入一定会被缓存。
你可以保留在 CI 中的请求清单
为每个 prompt fixture 使用一个小型清单:
{
"test_id": "support-agent-refund-01",
"requested_model": "your-model-id",
"resolved_model": null,
"payload_sha256": "replace-after-serialization",
"user_message_tokens_local": null,
"request_tokens_local": null,
"input_tokens_reported": null,
"cached_input_tokens": null,
"output_tokens_reported": null,
"total_tokens_reported": null,
"output_reserve": 1200,
"finish_reason": null,
"task_valid": null
}
在硬限制之下设置一个告警阈值。当 prompt 变更超出已批准的请求预算,或移除了输出预留时,让 CI 失败。然后在更改模型、网关、工具集或缓存行为之前,先运行一次实时的 TokenTest 评估。
若要了解更深入的预算工作流程,请阅读 面向生产环境 LLM 应用的上下文窗口规划开发者指南。若要针对具体工件进行缩减,请参见 用于代码 prompt 的 token 计数:文件、diff、日志和堆栈跟踪。
最终检查清单:为什么你的 prompt 太长?
在删除有用上下文之前,请验证以下所有内容:
- [ ] 你统计的是 token,而不是单词、字符、字节或行。
- [ ] 分词器与请求的模型和解析后的模型相匹配。
- [ ] 统计包含系统指令、历史记录、检索、工具和 schema。
- [ ] 计量是在最终请求序列化之后进行的。
- [ ] 输入预算为所需输出预留了足够容量。
- [ ] 高密度工件是单独测量的。
- [ ] 本地估算与端点报告的用量分开保存。
- [ ] 缺失的 usage 字段存储为
null,而不是 0。 - [ ] 更长的 prompt 会产生可信的输入 token 增长。
- [ ] 响应完成了任务,没有被截断。
最重要的修正很简单:你可见的 prompt 并不一定就是完整的 prompt。一旦你统计完整请求并验证端点的用量行为,“prompt 太长”就不再是一个模糊的模型错误,而会变成一个可测量的工程问题。
从实时的 TokenTest 评估控制台 开始,或者浏览 TokenTest 博客 上更多实用指南。