基于Datawhale的hello-agent开源项目做的一些笔记,内容仅供参考,原PDF以及代码可以去github仓库获取https://datawhalechina.github.io/hello-agents
在上一篇中,我们学习了如何通过提示工程(Prompt Engineering)与大模型高效沟通。
但你是否想过,当我们输入“你好”或“Hello”时,计算机究竟是如何“读懂”这些字符的?事实上,大模型根本不认识字,它只认识数字。将文本转化为数字序列的过程,就是本篇的主角——分词 (Tokenization)
这是大模型最底层、最基础,却也最容易被忽视的环节。很多时候模型表现出的“弱智”行为(比如算不对加法),根源都在这里。

3.2.2 文本分词 (Tokenization)

我们知道,计算机本质上只能理解数字。因此,在将自然语言文本喂给大语言模型之前,必须先将其转换成模型能够处理的数字格式。这个过程就叫做分词

分词器 (Tokenizer) 的作用,就是定义一套规则,将原始文本切分成一个个最小的单元,我们称之为词元 (Token)

3.2.2.1 为何需要分词?

早期的自然语言处理任务可能会采用简单的分词策略,但它们都有明显的缺陷 :

  • 按词分词 (Word-based):直接用空格分词。

  • 缺陷词表爆炸。英语单词变化太多(learn, learns, learned, learning…),如果每个变体都存,词表会大到无法管理。且无法处理未见过的词(OOV 问题)。

  • 按字符分词 (Character-based):切分成单个字母。

  • 缺陷语义缺失。单个字母 ‘a’ 或 ‘b’ 通常没有独立语义,模型需要花费大量精力去学习如何拼写,导致学习效率低下。

现代主流:子词分词 (Subword Tokenization)
为了兼顾词表大小和语义表达,现代大模型(如 GPT、Llama)普遍采用子词分词

核心思想:将常见的词(如 “agent”)保留为完整的词元,同时将不常见的词(如 “Tokenization”)拆分成多个有意义的子词片段(如 “Token” 和 “ization”)。


3.2.2.2 字节对编码 (BPE) 算法解析

字节对编码 (Byte-Pair Encoding, BPE) 是最主流的子词分词算法之一,GPT 系列就采用了它。

其核心思想是一个贪心的合并过程

  1. 初始化:将词表初始化为所有在语料库中出现过的基本字符。
  2. 迭代合并:统计所有相邻词元对的出现频率,找到频率最高的一对,合并成一个新的词元。
  3. 重复:重复第 2 步,直到词表大小达到预设阈值。
案例演示

假设我们的迷你语料库是 {"hug": 1, "pug": 1, "pun": 1, "bun": 1},目标是构建一个大小为 10 的词表。

步骤 最高频词元对 合并为新增词元 当前词表 (示例)
初始化 - - {h, u, g, p, n, b} (6个)
合并 1 u, g ug {ug, h, p, n, b} (7个)
合并 2 u, n un {ug, un, h, p, b} (8个)
合并 3 p, ug pug {pug, un, h, b} (9个)
合并 4 p, un pun {pug, pun, h, b} (10个)
代码实现:BPE 算法模拟

为了深入理解 BPE,我们用一段 Python 代码来模拟这个“迭代合并”的过程 。

import re, collections

def get_stats(vocab):
    """统计词元对频率"""
    pairs = collections.defaultdict(int)
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols)-1):
            pairs[symbols[i], symbols[i+1]] += freq
    return pairs

def merge_vocab(pair, v_in):
    """合并词元对"""
    v_out = {}
    bigram = re.escape(''.join(pair))
    # 使用正则匹配,注意负向预查防止错误匹配
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
    for word in v_in:
        w_out = p.sub(''.join(pair), word)
        v_out[w_out] = v_in[word]
    return v_out

# 准备语料库,每个词末尾加上 </w> 表示结束,并切分好字符
vocab = {'h u g </w>': 1, 'p u g </w>': 1, 'p u n </w>': 1, 'b u n </w>': 1}
num_merges = 4 # 设置合并次数

for i in range(num_merges):
    pairs = get_stats(vocab)
    if not pairs:
        break
    best = max(pairs, key=pairs.get)
    vocab = merge_vocab(best, vocab)
    
    print(f"第{i+1}次合并: {best} -> {''.join(best)}")
    print(f"新词表(部分): {list(vocab.keys())}")
    print("-" * 20)

运行结果解析
你会看到,算法首先发现了 ug 经常一起出现,于是合并成 ug。接着发现 un 经常一起出现,合并成 un。最终,原本零散的字符被组合成了我们认识的单词片段。

其他主流算法

  • WordPiece (BERT 使用):与 BPE 类似,但合并标准是看“能否最大化提升语言模型概率”,而不仅仅是频率。
  • SentencePiece (Llama 使用):最大的特点是将空格也视作一个普通字符(通常用下划线 _ 表示),这使得分词过程完全可逆。

3.2.2.3 分词器对开发者的意义

理解这些底层细节,对我们开发智能体有什么实际用处呢?非常大!

  1. 上下文窗口限制 (Context Window)
    模型的记忆限制(如 8K, 128K)是按 Token 数量计算的,而不是字符数。
  • 注意:中文通常比英文占更多的 Token(一个汉字可能对应 1-2 个 Token,而一个英文单词可能只是 1 个 Token)。精确计算 Token 数是管理长时记忆的基础。
  1. API 成本
    大多数模型 API(如 OpenAI, DeepSeek)都是按 Token 数量计费的。了解分词机制有助于预估成本。
  2. 模型表现的“诡异”陷阱
    你是否遇到过模型算不对 2+2
  • 原因:在某些分词器下,2+2(无空格)可能被切分成 ['2', '+', '2'],甚至可能被作为一个未见过的整体乱码处理;而 2 + 2(有空格)则会被清晰地切分。
  • 大小写敏感:一个词的首字母大写与小写,可能会被切分成完全不同的 Token ID,导致模型对它们的理解产生偏差。

💡 注解
在设计 Prompt 时,如果模型对某个词理解不到位,试着在它周围加空格,或者改变大小写,有时会有奇效。这本质上是在辅助分词器正确地切分语义单元。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐