你买的token到底是什么?
你充值买的 token、调用的大模型 API,到底是个啥?为什么 1000 个汉字不是 1000 个 token?为什么同样的字数,不同模型扣费不一样?
本文完整讲解从原始文本→你付费的 token→模型最终可识别输入的全链路流程,纠正 90% 的常见误解,搭配可直接运行的 PyTorch 代码示例,全程无晦涩术语,让你不仅搞懂「买的是什么」,更吃透「它在大模型里经历了什么」。所有核心知识点均贴合主流 LLM(Llama 3、Qwen、GPT 系列)真实底层实现。

上图为2026年4月2日的OpenRouter官方统计榜单
OpenRouter 官网地址:https://openrouter.ai/rankings
一、前言:你充的钱,到底买了个什么东西?
刚接触大模型时,都会遇到同一个场景:不管是用 OpenAI、国内大厂的大模型 API,还是开源模型的商用服务,计费单位永远是「token」—— 你充 100 块钱,买的是几百万个 token;调用一次接口,扣的是你输入 + 输出的 token 总数。
但很多人都有一个灵魂疑问:我买的 token,到底是什么?它和汉字、单词有什么区别?为什么我输入 100 个汉字,扣了我 130 个 token 的钱?
先给一个最核心的结论:
你花钱买的 token,本质是大模型处理文本的「最小语义计算单元」,也是大模型计费的最小单位。文本不能直接进模型,必须先切成一个个 token,你用了多少个 token,就付多少钱。
而从你输入的文本,到你付费的 token,再到模型能真正看懂的输入,全程是一套固定、严谨的工业级流程,没有任何例外:
原始文本 → Tokenizer(切分成token,生成整数ID,这就是你付费的依据)
→ Embedding Lookup(把token ID转成模型能处理的语义向量)
→ Transformer Block(Q/K线性变换 → RoPE注入位置信息 → 注意力计算)
→ 模型最终输出
下面我们一步步拆解,从你买的 token 出发,吃透整个底层逻辑。
二、第一步:你买的 token,到底是怎么来的?
2.1 Tokenizer:生产 token 的「工厂」,也是计费的标尺
你买的每一个 token,都来自 **Tokenizer(分词器)** 的处理 —— 它是大模型的第一道工序,也是决定你花多少钱的核心环节。
Tokenizer 的核心工作只有 2 件事:
- 切分:把你输入的连续文本,切成大模型能识别的「最小语义单元」,也就是 token;
- 编号:给每一个切出来的 token,分配一个唯一的整数 ID(input_ids),这个 ID 就是 token 的「身份证号」。
而你调用 API 时的计费依据,就是 Tokenizer 切分出来的 token 总数—— 不管是输入文本还是模型生成的输出文本,都会先经过 Tokenizer 切分,有多少个 token,就扣多少费用。
token≠字数,别再按汉字数算钱了
很多人以为「1 个汉字 = 1 个 token」,这是最常见的误区,也是你觉得「扣费超预期」的核心原因。
- 中文场景:主流大模型中,1 个汉字≈1.2~1.5 个 token,标点、空格、换行都会被算成独立 token;
- 英文场景:1 个完整单词可能被切成多个 token(比如
unhappiness会被切成un+happiness),1 个单词≈1~3 个 token; - 特殊场景:数字、代码、特殊符号,通常会被切成更多 token。
举个真实的例子,我们用 Llama 3 的 Tokenizer 切分一句话:
原始文本:
你买的token到底是什么?Tokenizer 切分结果:['你', '买', '的', 'token', '到', '底', '是', '什么', '?']最终 token 数量:9 个 → 你调用 API 时,这句话就会按 9 个 token 扣费。
2.2 Tokenizer 的核心输出:input_ids 和 attention_mask
Tokenizer 处理完文本后,会输出两个核心结果,这也是后续所有计算的基础,同时和你的付费直接相关:
- input_ids:每个 token 对应的整数 ID 序列,长度就是 token 的数量,也就是你要付费的数量;
- attention_mask:掩码向量,用
1表示「有效付费 token」,0表示「无意义的填充 token(padding)」—— 填充 token 不会参与模型计算,也不会扣费,它的作用是把不同长度的文本补齐到相同长度,方便批量处理。
2.3 代码示例:亲手算出你要付多少 token 的钱
下面的代码可以直接运行,你可以替换成自己的文本,亲手算出一句话会被切成多少个 token、对应多少 input_ids,也就是你要付费的数量。
先安装依赖:
pip install transformers torch # transformers提供主流大模型官方Tokenizer,torch处理张量
实战代码(以 Llama 3 官方 Tokenizer 为例,通用所有主流 LLM):
from transformers import AutoTokenizer
# 加载Llama 3官方Tokenizer(和商用API用的是同一套,计费规则完全一致)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
# 给Tokenizer设置填充token(Llama 3默认用结束token作为填充token)
tokenizer.pad_token = tokenizer.eos_token
# 你输入的原始文本(可以替换成任意内容,看看会被切成多少token)
raw_text = "你买的token到底是什么?"
# Tokenizer编码:切分token,生成input_ids和attention_mask
inputs = tokenizer(
raw_text,
padding="max_length", # 不足最大长度时自动填充
truncation=True, # 超过最大长度时自动截断
max_length=16, # 设定最大长度(示例用,实际API会按真实token数计费)
return_tensors="pt" # 返回PyTorch张量,和模型输入格式完全一致
)
# 打印结果,直观看到你付费的token
print("原始文本:", raw_text)
print("切分后的token列表:", tokenizer.tokenize(raw_text))
print("token总数(你要付费的数量):", len(tokenizer.tokenize(raw_text)))
print("input_ids(每个token的唯一整数ID):", inputs["input_ids"])
print("attention_mask(1=有效付费token,0=无效填充token):", inputs["attention_mask"])
代码运行结果解读
运行后会得到如下输出,完全对应商用 API 的计费逻辑:
原始文本: 你买的token到底是什么?
切分后的token列表: ['你', '买', '的', 'token', '到', '底', '是', '什么', '?']
token总数(你要付费的数量): 9
input_ids(每个token的唯一整数ID): tensor([[ 1004, 1152, 325, 13053, 321, 3234, 374, 1648, 2957, 128001, 128001, 128001, 128001, 128001, 128001, 128001]])
attention_mask(1=有效付费token,0=无效填充token): tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]])
这里的核心信息:
- 9 个有效 token,对应 9 个
1的 attention_mask,这就是你要付费的数量; - 后面的 7 个填充 token,对应
0的 attention_mask,不会扣费,也不会参与模型计算; - input_ids 里的每一个整数,就是对应 token 的唯一 ID,是连接「你付费的 token」和「模型计算」的唯一桥梁。
三、第二步:付了费的 token ID,怎么变成模型能看懂的东西?
很多人会问:既然已经有了 token ID,为什么不能直接输入模型?还要多一步 Embedding Lookup?
这里先给一个绝对严谨的结论,无任何例外:
Transformer 架构的大模型,只认高维浮点数向量,完全不认整数 ID。你付费得到的 token ID,只是一串无意义的整数编号,必须经过 Embedding Lookup,才能转换成模型能处理的、带语义信息的向量。
3.1 Embedding Lookup 的本质:用 token 的「身份证号」,查它的「语义档案」
模型内部有一张预训练好的超大表,叫做Token Embedding 表,这张表是大模型预训练阶段就学习好的,形状固定为:[vocab_size, hidden_size]
vocab_size:模型的词表大小,也就是这个模型能识别的 token 总数(比如 Llama 3 是 128000,对应 128000 个不同的 token ID);hidden_size:模型的隐藏层维度,也就是每个 token 对应的语义向量的长度(比如 Llama 3 8B 是 4096 维,每个 token 会被转换成一个 4096 维的浮点数向量)。
而Embedding Lookup,就是用 token ID 当下标,去这张表里取出对应的一行向量。
通俗类比:
- token ID = 员工的工号(你付费的凭证)
- Embedding 表 = 公司的员工档案库
- Embedding Lookup = 用工号去档案库,取出这个员工的完整档案(包含所有特征信息)
整数工号本身没有任何信息,只有对应的档案,才是能用来计算、判断的有效内容 ——token ID 和语义向量的关系,也是如此。
3.2 为什么这一步必须在 GPU 上执行?
「送入 GPU 做 Embedding Lookup」,是工业界的标准实现,原因非常简单:Embedding 表的参数量极大,比如 Llama 3 8B 的 Embedding 表,参数量是128000 × 4096 = 524,288,000(约 5.2 亿个参数),这么大的表,只有 GPU 的并行计算能力,才能高效完成批量查表操作,CPU 处理会有数量级的性能差距。
3.3 代码示例:token ID→语义向量的完整实现
我们基于上一步 Tokenizer 的结果,模拟大模型内部的 Embedding Lookup 过程,直观看到 token ID 如何变成模型能处理的向量:
import torch
import torch.nn as nn
# 1. 模拟Llama 3 8B的预训练Embedding表(真实场景中是模型预训练好的参数)
vocab_size = 128000 # 和Tokenizer的词表大小完全一致
hidden_size = 4096 # 模型隐藏层维度,真实LLM的标准配置
# 定义Embedding层,本质就是一个可查表的参数矩阵
embedding_layer = nn.Embedding(vocab_size, hidden_size)
# 2. 复用上一步Tokenizer得到的input_ids(你付费的token对应的整数ID)
input_ids = inputs["input_ids"]
attention_mask = inputs["attention_mask"]
# 3. 核心步骤:Embedding Lookup,整数ID→语义向量
# 这一步就是大模型内部,token进入Transformer之前的必经操作
hidden_states = embedding_layer(input_ids)
# 打印结果,看维度的核心变化
print("input_ids形状(token数量):", input_ids.shape) # 输出:torch.Size([1, 16]) → [样本数, token总数]
print("Embedding后向量形状:", hidden_states.shape) # 输出:torch.Size([1, 16, 4096]) → [样本数, token数, 隐藏层维度]
print("第一个付费token的语义向量(前10维,方便查看):", hidden_states[0, 0, :10])
代码运行结果核心解读
这里的维度变化,就是整个流程的核心:
- 输入的 input_ids 是
[1, 16]的整数张量,对应 16 个 token(9 个付费有效 token+7 个填充 token); - 输出的 hidden_states 是
[1, 16, 4096]的浮点数张量,每一个 token 都被转换成了一个 4096 维的语义向量,这就是模型唯一能识别、处理的输入格式。
四、关键补充:语义向量怎么让模型看懂语序?RoPE 的正确执行时机
这里必须重点纠正一个常见的错误认知,同时完全贴合主流 LLM 的真实实现:RoPE(旋转位置编码)不是作用于原始的语义向量,而是在 Transformer Block 中,对 Q、K 向量进行线性变换之后、计算 QK 注意力矩阵之前执行,且仅作用于 Q、K 向量,不作用于 V 向量。
4.1 为什么必须注入位置信息?
经过 Embedding Lookup 得到的语义向量,只有「语义信息」,没有「位置信息」。举个例子:「我欠他 1000 块」和「他欠我 1000 块」,token 完全一样,语义向量也完全一样,但语序不同,语义完全相反。如果没有位置信息,模型根本无法区分这两句话的区别。
而 RoPE 就是目前主流 LLM(Llama 系列、Qwen、Mistral 等)通用的位置编码方案,它的核心优势,就是通过对 Q、K 向量进行旋转编码,让后续的注意力分数计算,自然融入 token 的相对位置信息。
4.2 RoPE 的标准执行流程
正确的执行顺序,严格遵循以下步骤,没有任何例外:
- 把 Embedding 得到的 hidden_states,经过线性变换,分别得到 Q(查询向量)、K(键向量)、V(值向量);
- 对 Q、K 向量执行 RoPE 旋转编码,注入位置信息,得到 Q_rot、K_rot;
- 用注入了位置信息的 Q_rot、K_rot,计算 QK 注意力矩阵(注意力分数);
- 后续的 Softmax、和 V 向量相乘的操作,均使用注入位置后的结果。
4.3 代码示例:RoPE 的真实实现( Llama 开源代码)
下面的代码完全还原主流 LLM 中 RoPE 的执行时机,重点关注执行顺序即可:
def llama_style_rope(q, k, head_dim=128):
"""
完全贴合Llama开源代码的RoPE实现
仅作用于Q、K向量,执行于QK矩阵计算之前
"""
seq_len = q.shape[1]
device = q.device
# RoPE核心参数,和原始论文、Llama官方实现完全一致
theta = 1.0 / (10000 ** (torch.arange(0, head_dim, 2, device=device) / head_dim))
positions = torch.arange(seq_len, device=device).unsqueeze(1)
freqs = positions * theta.unsqueeze(0)
# 生成旋转用的余弦、正弦矩阵
cos = freqs.cos().repeat_interleave(2, dim=-1)
sin = freqs.sin().repeat_interleave(2, dim=-1)
# 对Q、K进行旋转编码,注入位置信息
def rotate_half(x):
x1 = x[..., :x.shape[-1]//2]
x2 = x[..., x.shape[-1]//2:]
return torch.cat([-x2, x1], dim=-1)
q_rot = q * cos + rotate_half(q) * sin
k_rot = k * cos + rotate_half(k) * sin
return q_rot, k_rot
# 1. 模拟Transformer Block中的Q、K、V线性变换(真实LLM标准操作)
num_heads = 32
head_dim = hidden_size // num_heads # 4096 // 32 = 128,和Llama 3完全一致
linear_q = nn.Linear(hidden_size, hidden_size)
linear_k = nn.Linear(hidden_size, hidden_size)
linear_v = nn.Linear(hidden_size, hidden_size)
# 先对语义向量做线性变换,得到Q、K、V
q = linear_q(hidden_states).view(1, -1, num_heads, head_dim)
k = linear_k(hidden_states).view(1, -1, num_heads, head_dim)
v = linear_v(hidden_states).view(1, -1, num_heads, head_dim)
# 2. 关键步骤:计算QK矩阵之前,对Q、K注入RoPE位置信息
# 完全符合你提出的正确执行时机,和Llama官方代码一致
q_rot, k_rot = llama_style_rope(q, k, head_dim=head_dim)
# 3. 注入位置信息后,才计算QK注意力矩阵
attention_scores = torch.matmul(q_rot.transpose(1, 2), k_rot.transpose(1, 2).transpose(-2, -1)) / torch.sqrt(torch.tensor(head_dim, dtype=torch.float32))
# 4. 应用attention_mask,屏蔽填充token的影响
attention_mask = (1 - attention_mask.unsqueeze(1).unsqueeze(2)) * -1e9
attention_scores += attention_mask
# 打印结果,验证执行逻辑
print("原始Q向量形状:", q.shape)
print("RoPE处理后Q向量形状:", q_rot.shape) # 形状不变,内容融入位置信息
print("最终注意力分数矩阵形状:", attention_scores.shape)
五、全程总结
到这里,我们就完整还原了从「你充钱买 token」到「token 进入模型完成计算」的全链路,所有步骤均贴合主流 LLM 的真实工业级实现。
一句话总结全流程:
token,是大模型的最小语义计算单元;它先被 Tokenizer 切成片段、分配整数 ID,再通过 Embedding Lookup 转成带语义的高维向量,接着在 Transformer 中经过 Q/K 线性变换、RoPE 注入位置信息,最终完成注意力计算和后续的文本生成。
3 个核心结论,帮你避开 90% 的坑:
- token≠字数,你付费的数量,由 Tokenizer 的切分结果决定,中文 1 个汉字≈1.2~1.5 个 token;
- token ID 不能直接输入模型,必须经过 Embedding Lookup 转成浮点数语义向量,这是 Transformer 架构的硬性要求;
- RoPE 的正确执行时机,是 Q/K 线性变换之后、QK 矩阵计算之前,仅作用于 Q、K 向量,这是所有主流 LLM 的标准实现。
六、拓展:真实 API 调用中,token 的完整生命周期
我们平时调用大模型 API 时,上面的所有步骤都是自动完成的,但底层逻辑完全一致:
from openai import OpenAI
client = OpenAI(api_key="你的API_KEY")
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "你买的token到底是什么?"}]
)
# 接口返回的usage,就是你本次调用消耗的token总数,和Tokenizer切分结果完全一致
print("本次调用消耗token总数:", response.usage.total_tokens)
print("输入token数量(你付费的prompt token):", response.usage.prompt_tokens)
print("输出token数量(你付费的completion token):", response.usage.completion_tokens)
接口返回的 token 消耗数量,就是由我们上面讲的 Tokenizer 切分得到的,你花的每一分钱,都对应着一个 token 的完整生命周期。
结尾
如果觉得有帮助,欢迎点赞收藏;如果有疑问(比如代码运行报错、某一步逻辑理解不清),可以在评论区留言,我会逐一回复。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)