跳转至

09 · Token、Embedding、上下文窗口:三个词读懂计费单

先讲给朋友听(如果你只有一分钟)

Token 是模型世界的最小货币:它不按字收费,不按词收费,按"词碎片"收费。Embedding 是文本的 GPS 坐标:意思近的文本,坐标就挨得近。上下文窗口是模型的工作台:台面只有这么大,放不下的东西它会真的忘记——你塞给它的每一本"公司制度手册",本质都是在挑战台面尺寸。

你的 Aha Moment

先看一张真实的 API 计费单(以 DeepSeek-chat 价格为例,单价会变,以官网为准,请看结构不要背数字):

项目 数量 单价(示意) 费用
输入 tokens 1,000,000 ¥2/百万 ¥2
输出 tokens 100,000 ¥8/百万 ¥0.8

注意最后两列的关系:输出单价比输入贵几倍——因为模型吐字是逐个生成的,算力成本全在那。所以"省 token"的重点永远是压缩输出和复用输入,不是反过来。第 22 篇的三本账会回到这张表。

你第一次看到这个计费单时,脑子里一定飘过五个字:"token 是啥?"

以及那个经典翻车现场:一个新手把整份 30MB 的《员工手册》塞进 Prompt 问问题,一个晚上烧掉半个月工资——因为他没搞懂 token + 上下文窗口的关系。

Aha 在三个事实里:

事实一:模型一个汉字都不"认"。 你跟模型说的每一句话,先被切成 token 序列。"我喜欢Java"可能切成 ["我", "喜欢", "Java"],也可能切成 ["我喜", "欢", "J", "ava"]——取决于分词器。对模型来说,你的输入从始至终是一串整数 ID,[27686, 21764, 15496] 这种。中文没有空格,分词粒度比英文更粗,同样内容的中文通常比英文更烧 token(工程上可感知的 30%~50% 差异)。

事实二:token 是计费、上下文、截断的三料共用单位。 你付钱按 token,模型记忆容量按 token,超出窗口的内容按 token 被丢。读懂 token,一整月的 API 账单和"模型失忆"事故,你都能提前预算。

事实三: embedding 可以手算。 后面实验 2,你将用 6 行代码算出"猫"和"狗"的相似度是 0.99,"猫"和"汽车"是 0——那就是 RAG 检索的心脏。第 12 篇整篇都在讲"怎么用好这个距离"。

不讲术语的版本:三个概念各说一件小事

Token:不是字,是"高频碎片"。

模型通过 BPE 算法(Byte Pair Encoding)在训练前就学出了一张"碎片表":高频出现的字节组合会被合并成一个 token。结果就是:

  • unbelievable 在英文里大概率切成 ["un", "believ", "able"]——每个碎片都是高频老面孔
  • 英文平均 1 个 token ≈ 0.75 个单词,中文约 1 个 token ≈ 1~1.5 个汉字

为什么用碎片不用整词? 词汇量。整词方案,一个"碳排放权交易"就吃掉一个词表槽位,组合爆炸;碎片方案,没见过的新词也能切碎表示——这是"用有限覆盖无限"的经典工程哲学,跟你用字典树做前缀匹配一个思路。

上下文窗口:台面的大小,忘掉台面外的世界。

模型有一个硬限制:它"看得见"的 token 总数有限(GPT-3.5 是 4k,GPT-4o 是 128k,Claude 最新款 200k,国产长文本 1M 逐渐铺开)。这个数同时包含你的 Prompt 和它的回答——你塞 100k 制度原文进去,它回答只能用剩下那点。

⏱️ 时效提示(本书对会过时的数字的处理方式):上面这组上下文窗口数值写于 2026 年 10 月,保鲜期约 1 年——这个数字每年都在快速上涨。选型前请查各模型的官方 Model Card 确认当下值,不要照搬本书。完整时效性清单见 附录 B · AI 信息可信度分级手册。

更反直觉的:塞满 ≠ 全都记住了。 学界实验发现模型对台面中段内容的利用率显著低于开头和结尾(lost in the middle 现象)。所以"上下文窗口 1M"不是"1M 全都靠谱",工程上仍要精打细算——这就是为什么有 RAG:与其把整个图书馆摊在台面上,不如只放最相关的那三页。

Embedding:意思的 GPS 坐标。

把任意一段文本,变成一个向量(一串浮点数,比如 1536 维)。神奇的是:这些坐标的位置携带着语义。"猫"和"狗"的坐标很近,"猫"和"汽车"很远——因为 Embedding 模型(专门训练的小模型)把"常在相似上下文出现的词"映射到了相近的位置。

RAG 的全部魔法就一句话:把你的问题变成一个坐标,在文档库里找距离最近的几页书,连同问题一起交给大模型。 没有 Embedding,就没有语义搜索,只有关键词匹配——"年假"和"休假制度"这种问了白问的场景,将无解。

Java 老司机对照室

LLM 圈的黑话 你已经懂的概念 一句话翻译
token 流量计费单位 / 分包 按碎片收费,中英文价不同
tokenizer (BPE) 前缀树的变体 / 序列化编码 训练前学好的高频碎片表
上下文窗口 128k 环形缓冲区 / 内存上限 模型工作记忆的容量硬顶
lost in the middle 缓存局部性反例 台面中段是记忆洼地
Embedding 向量 特征向量 (feature vector) 语义被压进一串浮点数
余弦相似度 推荐系统 user/item 向量 RAG 检索的打分器
向量数据库 倒排索引的近亲 pgvector(PostgreSQL 的向量插件,13 篇主线)/ Milvus,支持 ANN 检索
Prompt 拼装成本 序列化开销 每轮对话都要重发历史,费 token

最后一行划重点,是无数新手的翻车点:LLM 是无状态的! 你以为的"连续对话",实际是每一轮都把完整历史重新发一遍。十轮对话的真实 token 消耗是平方级增长——第 22 篇工程化我们会用"历史摘要压缩"治这个病,现在先建立警觉。

动手实验室

实验 1:手写一个 BPE 分词器(玩具版)

不引任何库,看模型是怎么"造碎片"的:

def bpe_merge(text: str, merge_rule: str, merge_target: str) -> str:
    """一轮 BPE:把 text 中所有相邻的 (merge_rule, merge_target) 合并成一个新 token"""
    pair = merge_rule + " " + merge_target     # "low er"
    replacement = merge_rule + merge_target    # "lower"
    return text.replace(pair, replacement)

# 语料库:模型训练时看到的文本
corpus = "low low low er er new new er"

# 第一轮学习:最高频相邻对是 ("low", "er") 和 ("new", "er")
# 合并 low er -> lower(真实 BPE 用计数器选最高频,这里手工指定)
step1 = bpe_merge(corpus, "low", "er")
print("第1轮合并:", step1)

# 第二轮:new er -> newer
step2 = bpe_merge(step1, "new", "er")
print("第2轮合并:", step2)

# 现在 "lower" 和 "newer" 已经是词表里的单 token 了

跑完看输出,BPE 的思想一览无余:

  • 初始词表:字母级别(每个字母是一个 token)
  • 逐轮找出最高频的相邻组合,合并成新 token
  • 新 token 进词表;没见过的词?切成认识的碎片照样表示

真实的分词器(tiktoken、sentencepiece)就是这个循环跑几万轮的产物,词表 5 万到 20 万条。你刚才手工走的两步,就是它一生的浓缩。

想对比真实效果,运行 uv add tiktoken 后试试 tiktoken.get_encoding("cl100k_base"),数一数"我喜欢Java"花掉几个 token——和研究费用直接挂钩的那种刺激。

实验 2:手算 Embedding 相似度(RAG 的心脏)

import math

# 假设一个二维世界里的词向量(真实模型是 1536 维,但几何直觉完全一样)
EMB = {
    "猫":   [1.0, 0.1],
    "狗":   [0.95, 0.2],
    "汽车": [0.1, 1.0],
    "虎":   [0.9, 0.9],
}

def cosine(a: list[float], b: list[float]) -> float:
    """余弦相似度:两个向量夹角的余弦值,1=同向(极像),0=无关,-1=反向"""
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(y * y for y in b))
    return dot / (na * nb)

print(f"猫 vs 狗:   {cosine(EMB['猫'], EMB['狗']):.3f}     # ≈0.99 近亲,高分")
print(f"猫 vs 汽车: {cosine(EMB['猫'], EMB['汽车']):.3f}     # 不同物种,低分")
print(f"猫 vs 虎:   {cosine(EMB['猫'], EMB['虎']):.3f}     # 沾亲带故但偏'猛兽'一侧")

数一下:猫-狗相似度 0.994,猫-汽车只有 0.198,亲疏立判。这就是 Embedding 空间的全部直觉——语义距离被翻译成了几何距离。

而你上一段读到的所有 RAG 教程里那些花哨术语(稠密检索、语义搜索、混合检索),内核就是这么朴素的一行余弦。第 13 篇会把这条公式放大到上千维、上万篇文档,几何直觉不变,量级吓人。

插一句名词(免得你在第 13 篇撞见它时一脸茫然):pgvector 是 PostgreSQL 的官方向量插件,让 PG 表里能直接存 vector(1024) 这种列、用一句 SQL 做余弦距离检索——相当于给 PG 装了个向量加速器,13 篇会手把手用它建库。

(不同 embedding 模型维度不同:BGE-M3 是 1024 维,text-embedding-v3 可选 1024/1536/2048——所以 Schema 里的列类型必须跟模型对上,这就是 pgvector 要写 vector(1024) 的原因。)

实验 3:台面不够用的物理体验

# 模拟"上下文窗口 = 128 tokens"的世界
CONTEXT_LIMIT = 128
# 手册全文约 3000 tokens,取其中约 40 字一段(保守按 1 汉字=2 token 估中文实情)
snippet = """
第三条 员工年假:入职满一年享有5天带薪年假,满十年10天,十五年15天。
年假最小申请粒度为半天,需提前一周在HR系统提交申请,由直属主管审批通过后生效。
第四条 病假:需提供二级以上医院诊断证明,病假期间工资按当地最低工资80%发放。
"""

snippet_tokens = len(snippet) * 2
print(f"手册占用: {snippet_tokens} tokens")
print(f"台面剩余: {CONTEXT_LIMIT - snippet_tokens} tokens")   # 负数!放不下

if snippet_tokens > CONTEXT_LIMIT:
    print("结论:模型会把前面内容忘掉/截断,你的问题答非所问")

跑出负数那一刻,"RAG 为什么存在"这个问题就有了物理答案:台面放不下,就别硬塞,该去检索相关页。 一张计费单 + 一个负数,两个实验把第 12 篇的门票给你买好了。

费曼复述卡

  1. 向财务/老板解释:为什么"同样的问题"用中文问比英文问贵?从 token 分词差异讲。
  2. 为什么说 LLM 是无状态的?连续对话十轮的真实 token 成本大概是什么量级?(提示:每轮重发全量历史)
  3. 用实验 2 的"猫-狗-汽车"坐标系,向同事解释 RAG 的检索内核:我们的问题是怎么变成坐标、文档是怎么被找到的?

三题过不了,回到"不讲术语的版本"重读上下文窗口那节。这三个词是 API 世界的"单位制"——不会换算单位,后面每一行代码你都看不懂它在花谁的钱。

今日一句话

Token 是模型的计价碎片,Embedding 是语义的 GPS,上下文窗口是台面——台面有限,所以要检索;检索靠坐标,所以有 RAG。


下一篇:10 · Transformer:一场关于"注意力"的注意力经济(返回 目录)