大模型 token 到底怎么算:中文、代码、图片各占多少

调用接口时报 context length exceeded,多数人的第一反应是「我字数没超过啊」。问题就出在这:上下文窗口的单位是 token,不是字符,也不是字数。同一个 10 万字的中文文档,在不同模型上可能占 7 万 token,也可能占 14 万 token,一个塞得进去,一个直接报错。

一、先记住三个数

内容类型 大致换算 说明
英文 1 token ≈ 4 字符 ≈ 0.75 个单词 几乎所有分词器都接近这个值
中文 1 个汉字 ≈ 0.6~1.5 token 差异极大,取决于模型词表
代码 1 token ≈ 3 字符 缩进、括号、下划线都单独占位,比自然语言更费

中文这个区间之所以宽,是因为分词器根本不同:

  • 国产模型(Qwen、DeepSeek、GLM 等)词表专门为中文优化过,常用双字词能压成一个 token,1 个汉字大约 0.6~0.7 token
  • OpenAI 早期词表(cl100k_base,GPT-3.5/4 时代)中文覆盖差,一个汉字常被拆成一到两个 token,1 字约 1.2~1.5 token
  • OpenAI 新词表(o200k_base)改善明显,1 字约 0.8~1 token

所以「128K 上下文能装多少汉字」这个问题没有唯一答案:

词表类型 128K token 约等于
国产模型(中文优化) 18~21 万汉字
OpenAI 新词表 13~16 万汉字
OpenAI 早期词表 8~10 万汉字

网上流传的「128K 大约十万字」,用的是最保守的那一档。如果你用的是国产模型,实际容量比你以为的大一倍。

二、为什么不能按字数估

除了分词器差异,还有四个容易忽略的消耗项。

系统提示词和历史对话全程占位。 上下文不是「这一轮你输入的字」,而是「系统提示词 + 全部历史消息 + 本轮输入 + 预留给输出的空间」的总和。一个 2000 token 的系统提示词,聊到第 20 轮,它已经被计费 20 次,同时也一直占着窗口。

数字特别费。 分词器会按固定位数切数字,一串 11 位手机号经常被切成 4~6 个 token,一段 18 位身份证号可能 8~10 个。整篇都是数据表格的文档,token 密度比同字数的散文高不少。

emoji 和特殊符号很贵。 一个 emoji 普遍占 2~4 个 token,一段全是 emoji 的营销文案,token 数能到汉字的三倍。

JSON 和 Markdown 的符号也占 token。 每个 "、:、{、换行都是独立 token。这也是为什么把 JSON 压缩成一行再喂给模型,能省下可观的开销——结构和值都没变,只是去掉了给人看的空白。

三、图片不是按张算,是按块算

这是最容易算错的一块。以 OpenAI 的视觉模型为例:

模式 计费方式
低清(detail: "low") 固定 85 token,无论图多大
高清(detail: "high") 先缩放到最长边 2048,再按 512×512 切块,85 + 170 × 块数

一张 1024×1024 的图,高清模式下切成 2×2 共 4 块,计费 85 + 170×4 = 765 token。同样一张图,低清只要 85 token,差 9 倍。

所以「一次传 20 张截图让模型看」是很贵的操作:20 × 765 ≈ 15,300 token,还没开始提问就吃掉一万五。

各家规则不同(有的按像素面积折算,有的按图块),但规律一致:分辨率是成本的主要变量,不是文件大小。同一张 JPEG,压缩到 200KB 和 2MB,计费完全一样。

四、三种实测方法,别靠估

方法一:直接看接口返回的 usage

最准,因为这就是计费依据。任何一家 API 的响应里都有类似字段:

{
  "usage": {
    "prompt_tokens": 1284,
    "completion_tokens": 356,
    "total_tokens": 1640,
    "prompt_tokens_details": { "cached_tokens": 0 }
  }
}

跑一次真实请求,把 prompt_tokens 除以你的字数,就得到你这个模型、这类内容的实际系数。这比任何网上的换算表都可靠。

方法二:本地跑分词器

# OpenAI 系:tiktoken
import tiktoken
enc = tiktoken.get_encoding("o200k_base")   # 新词表
print(len(enc.encode("这是一段中文测试文本")))   # 8 个字 → 大约 8 个 token 以内

# 对比老词表
enc_old = tiktoken.get_encoding("cl100k_base")
print(len(enc_old.encode("这是一段中文测试文本")))  # 通常更多
# HuggingFace 系(Qwen / Llama / GLM 等本地模型)
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
print(len(tok.encode("这是一段中文测试文本")))

注意开源模型的分词器要跟实际部署的模型版本对齐,同系列不同代之间词表可能换过。

方法三:粗估,够用就行

写代码时不想引依赖,用这个:

// 粗略估算,误差 ±20%,只用于限流和预检
const estTokens = (text) => {
  const cjk = (text.match(/[\u4e00-\u9fa5]/g) || []).length
  const rest = text.length - cjk
  return Math.ceil(cjk * 0.7 + rest / 4)   // 国产模型系数
}

要更准就把 0.7 换成你实测出来的值。

顺手统计一下原文有多少字、多少行、多少标点,也能帮你反推 token:

👉 字数统计(在线、不上传)

五、中文里的「隐形杀手」

全角标点。 中文的 ,。;: 在多数词表里是独立 token,一篇 3000 字的文章光标点就有 300 个 token,占 10%。

生僻字和异体字。 「囧」「槑」这类字可能被拆成 3 个字节级 token。古籍、方言、日文汉字混排时尤其明显。

表格和列表。 Markdown 表格的每一根 | 都是 token。一个 10 列 × 50 行的表格,光竖线就 500 个 token。

空行和缩进。 代码块里每行前面的 4 个空格,如果被保留就是 1 个 token;80 行的代码块白送 80 token。这也是为什么让模型读代码时,把无关的缩进层级压平能省钱。

六、超限了怎么办:按代价从低到高

顺序 做法 代价 效果
1 压缩空白、去掉多余缩进 无 省 5%~15%
2 把 JSON 从美化版换成单行 无 省 10%~20%
3 用低清模式传图 精度略降 省 90%
4 历史对话做摘要,只保留最近 N 轮 少量信息损失 省 50%+
5 长文档先切块,相关块才送入 要写检索逻辑 按需
6 换更大窗口的模型 单价更高 直接解决

最不该做的两件事:把长文档截断一半就发(模型可能看不到关键结论),无限追加历史(越聊越贵,最后必然超限)。

七、两个常见误解

「max_tokens 设大一点,回答会更长」 —— 不是。它只是输出上限,模型该停还是会停。设成 4096 不会让它写满 4096,但如果设成 200,一段完整代码就会被硬切断。

「上下文窗口 = 输入 + 输出,所以输入最多能用满」 —— 不是。窗口是输入和输出的总和。128K 的模型,输入塞了 127K,输出只剩 1K,回答会突然断在一半。

八、速查

英文           1 token ≈ 4 字符
中文(国产)    1 字 ≈ 0.6~0.7 token
中文(OpenAI 新)1 字 ≈ 0.8~1.0 token
中文(OpenAI 旧)1 字 ≈ 1.2~1.5 token
代码            1 token ≈ 3 字符
数字            每 5~6 位约 1 token
图片(低清)    85 token / 张
图片(高清)    85 + 170 × 块数

真正的系数只能自己测一次:发一个请求,用 prompt_tokens 除以字数。测完记下来,以后所有估算都用这个数。


相关工具:字数统计|JSON 格式化|AI 工具入口合集

全部纯前端实现,输入的内容不上传服务器。

还有 66 个免费在线工具

纯前端实现,不用注册,数据不上传服务器。

浏览全部工具