04-大模型智能体开发工程师:Tokenization与模型推理流程

系列文章导航:AI系列文章导航目录-持续更新中

第04课:Tokenization与模型推理流程

📝 本文摘要:本文详解Tokenization(BPE/WordPiece/Unigram三种主流方法)及其对模型"字数限制"、"数错字"和成本的实际影响,并逐步拆解从用户输入到模型输出的完整推理流程(Tokenization→特殊Token→Embedding→位置编码→Transformer层→采样→自回归→Detokenization),涵盖采样参数(Temperature/Top-p/Top-k)和KV Cache加速原理,附OpenAI API实战代码。
Tokenization是文本到模型的桥梁,理解它你才能理解:为什么模型会"数错字"?为什么不同模型的最大上下文不同?为什么Prompt要精炼?


一、什么是Tokenization

核心问题:模型不能直接处理文本,必须先把文本切分成一个个"Token",再映射为数字ID。

复制代码
"Hello World" → Tokenize → ["Hello", " World"] → [15496, 995]

Token不等于字符,也不等于词。它是介于字符和词之间的一个单位。


二、主流Tokenization方法

2.1 演进历史

复制代码
词级别分词 (Word-level)
  问题: 词表太大(英语就有几十万词),OOV问题(未登录词无法处理)

字符级别分词 (Character-level)
  问题: 序列太长,训练慢,语义信息弱(单个字符没意义)

子词分词 (Subword-level) ← 当前主流
  核心思想: 高频词保留,低频词拆分
  "unfriendly" → ["un", "friend", "ly"]

2.2 BPE(Byte Pair Encoding,字节对编码)⭐最主流

算法直觉

复制代码
初始: 每个字符(包括字节)是一个Token
反复: 找到最频繁的相邻Token对,合并为新Token
直到: 达到目标词表大小

示例:
初始: h e l l o   w o r l d
第1轮: 合并 "l"+"l" → "ll"
      h e ll o   w o r ll d
第2轮: 合并 "h"+"e" → "he"  
      he ll o   w o r ll d
...最终可能: "hello" "world" 各为一个Token

核心思想:高频词保留完整(如"hello"),低频词拆成常见子词(如"unfriendly"→"un"+"friend"+"ly")。这样词表不需要包含所有词,也能处理未登录词(OOV,Out-of-Vocabulary,词表中没有的新词)。

被谁用:GPT系列、LLaMA、Qwen、DeepSeek

2.3 WordPiece(词片分词)

与BPE类似,但合并标准不同:

  • BPE:选频率最高的对
  • WordPiece:选使得语言模型似然(LM Likelihood,语言模型概率)增加最多的对

被谁用:BERT、DistilBERT

2.4 SentencePiece / Unigram(句子片段/单字分词)

SentencePiece(Google开发的开源分词工具):一个分词框架,支持BPE和Unigram两种算法,特别适合多语言场景。

Unigram Language Model(单字语言模型分词):与BPE相反的方向:

  • BPE:从字符逐步合并(自底向上)
  • Unigram:从大词表逐步裁剪(自顶向下),删减使似然减少最少的子词

被谁用:T5(Text-to-Text Transfer Transformer)、ALBERT(A Lite BERT)、多语言模型


三、Tokenization的实际影响

3.1 为什么不同模型的"字数限制"不同

复制代码
同样1万字中文:
  GPT-4 (cl100k_base):     ~5000 tokens
  Llama 3 (128K词表):      ~3000 tokens  
  Qwen2.5 (151K词表):      ~2500 tokens

词表越大 → 编码越高效 → 同样字数消耗更少token → 上下文能装更多内容

3.2 为什么模型会"数错字"

复制代码
"strawberry" → GPT-4 tokenize → ["str", "aw", "berry"]
                                          ↑
                              "berry"是一个Token,模型看不到里面的3个r

所以模型说strawberry有2个r------因为Token内部对模型是不可见的

解决方案:让模型逐字符检查,或用代码执行来计数。

3.3 Token与成本

复制代码
API计费: 按1000 tokens收费
1个汉字 ≈ 1-2 tokens (取决于模型)
1个英文单词 ≈ 1-1.5 tokens

一段1000字中文 ≈ 1500 tokens
GPT-4o: $5/1M input tokens → 约0.75分钱
DeepSeek-V3: ¥1/1M input tokens → 约0.15分钱

四、完整推理流程详解

4.1 从用户输入到模型输出

复制代码
Step 1: 用户输入
  "讲一个笑话"

Step 2: Tokenization
  ["讲", "一个", "笑话"] → [2523, 10387, 33111]

Step 3: 添加特殊Token
  [BOS, 2523, 10387, 33111, EOS]
  BOS=Beginning of Sequence(序列起始符)
  EOS=End of Sequence(序列结束符)
  这些特殊Token告诉模型"文本开始了"和"文本结束了"

Step 4: Embedding
  每个Token ID → 查Embedding表 → d_model维向量
  [2523] → [0.12, -0.34, 0.56, ...]  (4096维)

Step 5: 位置编码
  加上位置信息: embedding + positional_encoding

Step 6: Transformer层(N层)
  每层: Causal Attention → Add&Norm → FFN → Add&Norm

Step 7: 输出logits
  最后一层 → 线性层 → 词表大小的向量 (如151936维)
  每个维度代表对应Token的概率(未归一化)

Step 8: 采样(Sampling,从概率分布中选择Token的过程)
  logits → softmax → 概率分布 → 选择下一个Token
  采样策略(Sampling Strategy):
    - Greedy(贪心解码): 选概率最高的,输出确定性最高,但多样性最低
    - Top-k: 从概率最高的k个Token中按概率采样
    - Top-p (Nucleus Sampling,核采样): 从累积概率达到p的最少Token集合中采样
    - Temperature(温度参数): 调整概率分布的"锐度"
      T→0: 分布更尖锐(几乎确定性),输出更确定但无聊
      T→∞: 分布更平坦(完全均匀),输出更随机但多样
      T=1: 原始概率分布

Step 9: 自回归
  把选中的Token加入输入,重复Step 4-8
  直到生成EOS或达到max_tokens

Step 10: Detokenization
  Token IDs → 文本
  [2523, 10387, 33111, 456, 789, ...] → "讲一个笑话:..."

4.2 采样参数详解

python 复制代码
# Temperature: 控制随机性
# T→0: 几乎确定性的输出(总是选概率最高的)
# T→∞: 完全随机
# T=1: 原始概率分布
# 推荐: 代码生成0.0-0.2, 创意写作0.7-1.0

# Top-p: 核采样
# 只从累积概率达到p的最少Token集合中采样
# top_p=0.9 → 选最少的Token使其概率和≥0.9

# Top-k: 只从概率最高的k个Token中采样
# top_k=50 → 只看前50个候选

# Frequency/Presence Penalty: 惩罚重复
# 减少"车轱辘话"的效果

4.3 KV Cache加速

复制代码
无KV Cache:
  生成第1个Token: 计算Token1的Q,K,V
  生成第2个Token: 重新计算Token1,2的Q,K,V  ← 浪费!
  生成第3个Token: 重新计算Token1,2,3的Q,K,V ← 更浪费!

有KV Cache:
  生成第1个Token: 计算Token1的Q,K,V,缓存K,V
  生成第2个Token: 只计算Token2的Q,K,V,Token1的K,V从缓存取
  生成第N个Token: 只计算TokenN的Q,K,V,前面N-1个从缓存取

效果: 推理从O(n²)降到O(n)

五、OpenAI API推理实战

5.1 基本调用

python 复制代码
from openai import OpenAI

client = OpenAI(api_key="your-key")

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "你是一个有帮助的助手。"},
        {"role": "user", "content": "讲一个笑话"}
    ],
    temperature=0.7,
    max_tokens=500,
    top_p=0.9
)

print(response.choices[0].message.content)

5.2 流式输出

python 复制代码
stream = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "写一首诗"}],
    stream=True  # 关键参数
)

for chunk in stream:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)

5.3 Token使用量

python 复制代码
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "你好"}]
)

print(f"Prompt tokens: {response.usage.prompt_tokens}")
print(f"Completion tokens: {response.usage.completion_tokens}")
print(f"Total tokens: {response.usage.total_tokens}")

📝 作业

作业1:用tiktoken计算Token数量

安装tiktoken,计算以下文本在不同编码下的token数量,观察差异。

python 复制代码
import tiktoken

text = "Hello World! 你好世界!这是一个测试。"

for encoding_name in ["cl100k_base", "o200k_base", "p50k_base"]:
    enc = tiktoken.get_encoding(encoding_name)
    tokens = enc.encode(text)
    print(f"{encoding_name}: {len(tokens)} tokens")
    print(f"  Token IDs: {tokens}")
    print(f"  Decoded: {[enc.decode([t]) for t in tokens]}")
    print()

参考答案(预期输出)

复制代码
cl100k_base: 约14 tokens
  中文每个字约1-2 tokens,英文每个词约1 token
  
o200k_base: 约10 tokens  
  GPT-4o的新编码,中文编码更高效
  
p50k_base: 约16 tokens
  旧版编码,中文编码效率低

作业2:用OpenAI API(或兼容API)完成一次完整的对话

要求:

  1. 设置system prompt
  2. 进行多轮对话
  3. 开启流式输出
  4. 打印token使用量

参考答案

python 复制代码
from openai import OpenAI

# 也支持任何OpenAI兼容API,只需修改base_url
client = OpenAI(
    api_key="your-key",
    # base_url="https://api.deepseek.com"  # DeepSeek
)

messages = [
    {"role": "system", "content": "你是一个Python编程专家,回答简洁。"},
    {"role": "user", "content": "什么是列表推导式?"}
]

# 第一轮
response = client.chat.completions.create(
    model="gpt-4o-mini",  # 便宜的模型用于练习
    messages=messages,
    temperature=0.3,
    max_tokens=300
)
assistant_msg = response.choices[0].message.content
print(f"助手: {assistant_msg}")
print(f"Tokens: {response.usage}\n")

# 第二轮
messages.append({"role": "assistant", "content": assistant_msg})
messages.append({"role": "user", "content": "给一个嵌套列表推导式的例子"})

# 流式输出
stream = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    stream=True
)

print("助手: ", end="")
for chunk in stream:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
print()

下一篇文章见:AI系列文章导航目录-持续更新中

相关推荐
视觉&物联智能3 小时前
【杂谈】-游戏生成数据:人工智能训练中极易被低估的核心资源
人工智能·游戏·ai·chatgpt·openai·agi·deepseek
JaydenAI4 小时前
[MAF的Agent管道详解-04]如何让LLM按照要求的结构输出数据?
ai·c#·agent·maf·agent pipeline
李姆斯4 小时前
给 AI Agent 造好用的锤子:复杂系统的 Tool 抽象设计
aigc·agent
HIT_Weston5 小时前
92、【Agent】【OpenCode】edit 工具提示词
人工智能·agent·opencode
Swift社区6 小时前
推动AI领导力:构建全栈开放的智能生态
人工智能·ai
@蔓蔓喜欢你7 小时前
团队协作工具:提升开发效率的利器
人工智能·ai
阿部多瑞 ABU7 小时前
ADRO实战:用渐进式诱导“聊出”TATP完整合成路线——某国产大模型红队测试实录
安全·ai
养肥胖虎8 小时前
完整学习LLM(二):大模型到底是什么
大模型·llm·学习路线
ToBeTuring8 小时前
openclaw和claude code的配置文件参考
ai·claude code·openclaw