从字符到向量,中间藏着钱的秘密

每一次 API 调用都按 token 计价。但 token 到底是什么?问十个开发者可能得到十个不同回答。这篇把这个每天和你账单打交道的概念讲清楚,顺带讲清楚"为什么中文更贵"这个不大不小的谜题,以及省 token 的几种真正有效的办法。

它不是字也不是词

一个 token 是模型认识的最小单位。它既不是一个字符,也不是一个完整的单词,而是介于两者之间的一种东西。用 OpenAI 的 tokenizer 看 "Hello, world!" 这句话,大约会被切成四个 token:Hello, world(注意前面带空格)、!。你可以去 platform.openai.com/tokenizer 自己试,把任意一段文字贴进去,它会把每个 token 染成不同颜色展示出来。第一次看的人通常会被"空格是 token 一部分"这种细节震撼一下。

至于为什么要搞出这种奇怪的切法,要先知道两个极端方案各自的问题。按字符切(char-level)看似最干净——词表只有几百个字符,什么新词都能拼出来,但代价是序列长得吓人,生成 "Hello" 要跑五步预测,训练和推理都慢得让人想放弃。按单词切(word-level)看起来优雅,但词表会膨胀到几十万并且每次遇到新词("ChatGPT"、"CRISPR")就傻眼,中文更惨因为根本没有天然空格分词。

BPE(Byte Pair Encoding)是现在大多数 LLM 采用的折中方案。它的思路朴素得近乎笨拙:从"一个字符一个 token"开始,在训练语料里反复找出最频繁相邻的字符对,把它们合并成一个新 token,直到词表达到预设大小(OpenAI 的 cl100k_base 是 10 万个,GPT-4o 用的 o200k_base 是 20 万个)。训练语料里高频出现的组合(比如 "ing"、"tion"、" the")最早被合并成独立 token,低频组合则可能永远保持字符状态。

这种设计带来一个自然的性质:常见词被切得少,生僻词被切得多。"hello" 在英文语料里是 1 个 token,"hypothalamic" 这种医学术语可能被切成 3-4 个。中文的情况更有意思一些——"你好世界" 用旧 tokenizer 大约要 5-6 个 token,比英文同义的 "hello world" 多出一倍。

为什么中文更贵

这里有三层原因交织。第一也是最根本的,早期 LLM 的训练语料以英文为主,BPE 的合并优先级由频率决定,所以英文的常见组合更早变成独立 token。第二,UTF-8 编码里一个汉字占 3 字节,而早期 tokenizer 是基于字节级 BPE,一个汉字被切成多个字节 token 的情况非常普遍。第三,空格是英文天然的边界,BPE 在英文里工作得特别舒服,中文没这种先天馈赠,分词全靠模型硬学。

结果就是同样的语义内容,中文消耗的 token 数大致是英文的 1.5 到 2 倍,账单也按同样比例涨。这是真实存在的"语言税",不是错觉。

好消息是 GPT-4o 引入的新 tokenizer 针对多语言做了大幅优化。"你好" 现在基本上 1 到 2 个 token 就能搞定,中文、日文、韩文的处理效率都显著提升。Claude 和 Gemini 也在各自改进。但需要注意便宜模型(各家的 mini / haiku 系列)有时仍然在用老 tokenizer,所以同一个厂家不同档位模型的 token 数可能相差不少,选型时要具体查文档。

想精确计算 token 数可以用工具。Node.js 项目里装个 gpt-tokenizerjs-tiktoken

import { encode } from 'gpt-tokenizer'

const text = '你好,world!'
const tokens = encode(text)
console.log(tokens.length) // 5

前端开发者有时会在客户端预估 token 数,用来在用户输入超过上下文窗口之前就给出警告——这是一个不错的 UX 细节。

Token 和钱的换算

把 Claude Sonnet 的价目作为参照:输入每百万 token 3 美金,输出每百万 token 15 美金。一个典型的对话大致长这样:系统提示 500、历史 10 轮对话 3000、本轮用户问题 100、模型回答 500。输入加起来 3600、输出 500,算下来一轮约 0.018 美金,一分多钱。

看起来不多,但如果你的产品每天有 1 万次这样的调用,一个月就是 5400 美金。如果这些对话是 RAG 场景,每次系统提示里还带着几千个 token 的检索材料,数字会翻好几倍。所以 token 管理不是抠门,是基础工程素养。

省 token 的几种真正有效的方法

最有效的一招是开 prompt caching。OpenAI、Anthropic、DeepSeek 都支持这个机制:你把长系统提示、RAG 检索出的上下文、few-shot 示例这类"基本不变"的内容放在消息的开头,服务端会缓存住它对应的计算结果,下次相同前缀直接命中,只收 10% 的费用。Anthropic 需要显式加 cache_control: { type: 'ephemeral' },OpenAI 1024 token 以上的稳定前缀会自动缓存。对于频繁调用同一套系统提示的产品,这条能省出 80% 以上的输入 token 费用。实操中要记得:把变化的部分放后面、稳定的部分放前面,前缀哪怕改一个字符缓存就失效了。

其次是压缩对话历史。最朴素的做法——每次都把全部历史丢给模型——在几轮对话之后就会变成巨大浪费。合理的架构是保留最近 N 轮原文,更早的内容由模型自己生成一个摘要代替。摘要可以每 10 轮触发一次,prompt 写成"用 200 字总结以上对话中的关键信息、用户偏好、已达成的共识,以便后续继续对话"。这条能把长会话的平均 token 消耗砍半。

第三是选对模型。90% 的场景根本用不到旗舰模型。简单分类、意图识别、格式化、翻译、摘要这类任务,Haiku、mini、Flash 这些小模型完全够用,而且便宜 10 到 30 倍。一个常见做法是在前端根据任务类型做路由:简单任务走小模型,复杂推理才升级到旗舰。粗暴但有效:

function pick(task) {
  if (task.needsDeepReasoning) return 'claude-opus'
  if (task.needsCoding) return 'claude-sonnet'
  if (task.isClassification) return 'gpt-4o-mini'
  return 'gpt-4o-mini'
}

第四是设 max_tokens 的上限。不加限制时模型倾向于多说,因为 RLHF 训练里"信息更全的回答"经常被奖励。但用户多数情况下不需要那么长的回答。加一个合理的 max_tokens(比如 500),或者在 prompt 里直接说"用不超过 50 字回答",能显著降低输出消耗。这条对"客服机器人"、"分类 API" 这类场景尤其有效。

第五是结构化输出代替长文。同样的信息量,让模型输出 JSON 比让它写一段流畅文本省 3 到 5 倍 token。"请详细介绍这部电影,包括导演、主演、剧情、评价等"会得到一段 300 字的散文;换成"返回 {title, director, cast: string[], plot_oneline, rating}",token 数砍到三分之一还更便于解析。

第六是Batch API。非实时任务(离线生成、批量打标、周期性总结)走各家的 batch 接口,通常是正常单价的 5 折,交货 SLA 是 24 小时。这对日常调度任务非常划算,只要你能接受隔夜返回。

最后一招是让 embedding 代替 LLM。语义搜索、文本去重、相似度聚类这类任务根本不需要 LLM 出场。跑 embedding 算 cosine 相似度,成本是 LLM 调用的百分之一甚至千分之一。情感分类之类的简单任务也可以"embedding + 一个小分类器"搞定,训练成本几美金,推理几乎免费。能用 embedding 解决的问题都别上 LLM,这条能省不只是钱,还有延迟。

推理模型的隐藏账单

o1、o3、Claude 的 extended thinking 这类推理模型有一个容易忽视的隐藏成本:思考 token。模型在真正开始回答之前会先生成一大段内部思考链,这部分内容你看不到,但按输出价格结算。简单问题也可能消耗几千个 thinking token,一个看似"不长"的回答背后其实烧了不少钱。

所以日常任务不要盲目用推理模型。真正需要推理(复杂数学、多步规划、需要计划的编程任务)的时候它值回票价,分类、润色、总结这类直觉任务用普通模型就够了。很多人升级到 o1 之后账单翻几倍还以为是 bug,其实就是 thinking token 在默默烧钱。

Token 多 ≠ 模型记得住

一个常见误解:"上下文窗口有 200K token,我把整本书塞进去它就全记住了"。从"装得下"的角度说没错,但从"用得好"的角度看是另一回事。有个叫 Lost in the Middle 的著名研究(Liu 等人,2023)发现:模型对上下文的利用是 U 型的——开头和结尾能被良好检索,中间 40% 到 60% 的位置召回率显著下降。这意味着即便 200K 全塞满,真正被有效利用的区域也是前后两头,中间大段可能被"视而不见"。

实践中的推论是:关键指令、用户问题、最重要的参考资料要放在开头或结尾,不要埋在中间。而且长上下文并不等于 RAG 的替代品——在大型知识库场景下,先用 embedding 做相关性筛选、只把最相关的片段塞进去,比"反正窗口大直接全给"效果好得多,也便宜得多。

一些好用的工具

参考资料

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:番外 3:Token 的秘密——BPE、中文更贵、怎么省

本文链接:https://www.sshipanoo.com/blog/ai/ai-for-frontend/番外03-Token秘密/

本文最后一次更新为 天前,文章中的某些内容可能已过时!