前言

上一篇文章,我们主要介绍了本地部署大模型时需要关注的模型版本、量化参数和推理框架。文章发布之后,我发现还有一个更基础的问题没有讲清楚:

我们下载下来的大模型,到底由哪些东西组成?

第一次接触大模型时,很多人会把模型简单理解成一个几十GB的权重文件。文件下载下来,启动一个服务,然后就可以聊天了。

但真正运行起来之后,你会发现还有一堆新概念:

  • 模型参数到底是什么?7B14B又代表什么?
  • 模型为什么不直接处理文字,还要先切成Token?
  • Context和上下文长度有什么关系?为什么内容太长会报错?
  • TokenizerChat Template分别负责什么?
  • Base模型和Instruct模型有什么区别?为什么有的模型适合聊天,有的模型却像是在续写文章?

这些概念看起来各自独立,实际上它们共同组成了一次完整的模型调用流程。

以前我们开发Java服务,一个请求大致会经过网关、Controller、Service、数据库,然后返回一个JSON。大模型的调用流程也可以用类似的方式理解:

用户消息
   │
   ├── Chat Template:把多轮消息组织成模型认识的格式
   │
   ├── Tokenizer:把文本转换成Token ID
   │
   ├── 模型参数:根据已有Token预测下一个Token
   │
   ├── Tokenizer:把Token ID转换回文本
   │
   └── 返回模型回答

所以,本篇文章不急着研究Transformer内部复杂的数学公式,先从一个Java开发者比较容易理解的角度,把模型的基本组成梳理清楚。

下面我们就从“模型参数”开始,一点一点来看。

一、先看懂一次模型调用

在展开每个概念之前,我们先把一次最简单的模型调用过程简化一下。

假设用户输入:

请介绍一下Redis的过期删除策略

模型并不是直接读取这句话,然后像人一样理解它。通常会经历下面几个步骤:

原始文本
    ↓
Chat Template(如果是聊天模型)
    ↓
Tokenizer
    ↓
Token ID序列
    ↓
模型参数进行计算
    ↓
预测下一个Token
    ↓
重复预测,直到生成结束
    ↓
Tokenizer解码
    ↓
最终文本

这里有一个很重要的认识:大语言模型的核心工作,是根据前面的Token预测下一个Token。

模型输出一段话,并不是一次性把整段话都想好,而是一个Token一个Token地生成。我们平时看到的是一整段自然语言,模型内部处理的却是数字序列。

后面介绍的参数、Token、Context和Tokenizer,都可以放回到这条链路中理解。

二、模型参数到底是什么?

1. 参数是模型学到的数字

模型参数通常也叫权重,英文是Parameters或者Weights

目前主流的大语言模型大多基于Transformer类架构,模型参数就是这套网络在训练过程中不断调整得到的一组数字。

在训练过程中,模型会不断读取大量文本,并调整内部的一组数字。训练完成后,这些数字就被保存成模型权重。我们下载的safetensors.bin或者量化后的模型文件,本质上保存的就是这些参数。

可以先把它简单理解成:

模型参数 = 模型在训练过程中学到的一组数字

这些数字并不是一张可以直接打开的知识表,也不是把所有文章原封不动地保存下来。它们分布在模型的各个网络层中,共同影响模型对下一个Token的预测结果。

2. 7B、14B表示参数数量

我们经常会看到下面这样的模型名称:

Qwen-7B
Llama-3-8B
某某-14B

这里的BBillion的缩写,表示十亿。

因此:

7B  ≈ 70亿个参数
14B ≈ 140亿个参数

参数量越大,通常意味着模型有更强的表达能力,但同时也意味着更大的模型文件、更高的显存要求和更慢的推理速度。

当然,这也不是绝对的。模型能力还和训练数据、模型结构、训练方法以及对齐方式有关。同样是7B模型,不同模型之间的实际效果可能差别很大。

3. 参数量和模型文件大小是什么关系?

模型参数数量只是一个数量,实际占用多大的空间,还取决于每个参数使用多少位来保存。

以70亿参数为例,简单估算一下:

FP32:7B × 32 bit ÷ 8 ≈ 28 GB
FP16:7B × 16 bit ÷ 8 ≈ 14 GB
INT4:7B × 4 bit  ÷ 8 ≈ 3.5 GB

实际文件还会包含缩放因子、元数据以及其他辅助信息,所以最终大小会比理论值略有差异。

这就是为什么同一个模型,会同时提供BF16FP16INT4等不同版本。参数数量没有变,变化的是每个参数的保存方式。

上一篇文章已经详细介绍过量化版本,这里先记住一句话:

参数量决定模型大致的规模,数据类型和量化方式决定模型需要多少存储空间。

4. 参数越多,模型一定越好吗?

不一定。

参数更多,通常意味着模型容量更大,但模型最终效果还取决于很多因素。比如:

  • 训练数据的质量
  • 训练数据覆盖的领域
  • 模型结构和训练方法
  • 是否经过指令微调
  • 是否针对代码、数学或中文做过优化

所以,不能只看模型名称中的B就判断模型好不好。真正选择模型时,还需要看模型卡片、评测结果和自己的业务场景。

三、Token:模型处理文本的基本单位

1. Token不是简单的字符

人类阅读的是汉字、单词和句子,模型处理的是Token。

Token可以理解成模型处理文本时使用的基本单位,它可能是一个汉字、一个词、一个词的一部分,也可能是标点符号或空格。

例如,下面的切分只是一个示意:

我喜欢Java开发
   ↓
我 | 喜欢 | Java | 开发

英文单词可能会被进一步切分:

unbelievable
      ↓
un | believe | able

实际如何切分,取决于模型使用的Tokenizer。不同模型的词表不同,同一句话经过不同模型处理后,Token数量也可能不一样。

2. Token和Token ID

模型内部并不直接使用喜欢这样的文字,而是把每个Token映射成一个数字,这个数字就是Token ID。

文本:我喜欢Java
Token:我 | 喜欢 | Java
Token ID:1024 | 5831 | 27190

上面的数字只是示意,不同模型的实际ID并不相同。

模型真正接收到的内容,更接近下面这样:

[1024, 5831, 27190]

这和Java程序从网络接收字节数组有点类似。我们看到的是字符串,程序内部处理的却是经过编码后的字节。

3. 为什么Token很重要?

Token会影响很多事情:

  • 能不能放进模型的Context窗口
  • 一次请求占用多少显存和内存
  • 模型生成速度有多快
  • API调用的输入和输出成本
  • 文档切分时应该按照多大的长度分块

比如,同样是1000个汉字,经过不同模型的Tokenizer处理后,不一定都是1000个Token。代码、英文、数字和特殊符号混在一起时,Token数量通常还会继续变化。

所以,在做RAG、长文本问答或者上下文管理时,不能只按照字符数估算长度,最好实际使用目标模型的Tokenizer进行计算。

4. Token还包括特殊Token

除了普通文本,Tokenizer还会处理一些特殊Token,例如:

BOS:序列开始
EOS:序列结束
PAD:补齐长度
UNK:未知Token

不同模型支持的特殊Token不完全一样。聊天模型还可能有表示用户、助手、系统消息的控制Token。

这些特殊Token看起来不像正常文字,但它们会参与模型计算,也会占用Context长度。

四、Context:模型当前能看到多长的内容?

1. Context是什么?

Context通常翻译成上下文,指的是一次请求中,模型能够看到并参与计算的Token范围。

模型的上下文长度通常会以这些形式出现:

4K
8K
32K
128K

这里的K通常表示千级Token,而不是千个汉字。

如果一个模型支持32K Context,可以粗略理解成:一次请求中,输入内容、历史对话、系统提示词以及预留的输出空间,不能超过大约3.2万个Token。

2. Context不是永久记忆

这是一个非常容易混淆的概念。

用户和模型聊了十轮,不代表模型永久记住了前面所有内容。很多聊天系统只是把历史消息重新拼接到下一次请求中,作为新的Context发送给模型。

可以简单理解为:

第1次请求:当前问题
第2次请求:历史消息 + 当前问题
第3次请求:更多历史消息 + 当前问题

只要内容还在Context窗口内,模型就可以参考;一旦超过窗口,就需要截断、摘要,或者通过RAG等方式重新取回相关内容。

所以,Context更像是一次请求携带的“工作区”,而不是数据库中的永久记忆。

3. Context长度包括哪些内容?

一次聊天请求的Context,通常可能包含:

系统提示词
+ 历史用户消息
+ 历史模型回答
+ 当前用户问题
+ RAG检索结果
+ 工具调用结果
+ 预留的模型输出空间

实际限制要看具体模型和推理框架的定义,但在使用时不要只盯着模型名称中的128K

如果输入已经占用了很长的Context,再设置一个很大的max_new_tokens,就可能超过模型允许的总长度。

4. Context越长越好吗?

也不是。

Context越长,模型一次能看到的信息越多,但同时也会带来几个问题:

  • 占用更多显存和内存
  • 首次处理请求的时间更长
  • 多轮对话速度变慢
  • 过多无关内容可能干扰模型回答

这和Java服务中的缓存有点类似。缓存不是越大越好,真正重要的是缓存的内容是否有用,以及系统是否承受得住它带来的资源开销。

五、Tokenizer:负责文本和Token之间的转换

1. Tokenizer做了什么?

Tokenizer可以理解成模型的编解码器,主要负责两件事:

编码:文本 → Token → Token ID
解码:Token ID → Token → 文本

在Transformers中,Tokenizer通常由下面这些文件组成:

tokenizer.json
tokenizer_config.json
special_tokens_map.json
vocab.json
merges.txt

不同模型使用的文件不一定完全相同,但作用都是为了告诉程序:文本应该如何切分,Token应该如何映射成数字。[2]

2. 一个简单示例

使用Transformers时,可以这样查看文本经过Tokenizer之后的结果:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("模型目录")

text = "请介绍一下Redis的过期删除策略"
encoded = tokenizer(text, return_tensors="pt")

print(encoded["input_ids"])
print(encoded["attention_mask"])

decoded_text = tokenizer.decode(encoded["input_ids"][0])
print(decoded_text)

这里的input_ids就是Token ID序列,attention_mask用于告诉模型哪些位置是真实内容,哪些位置是补齐出来的内容。

3. Tokenizer必须和模型匹配

Tokenizer不是一个可以随便替换的工具。

不同模型可能有不同的词表、切分规则和特殊Token。如果使用了不匹配的Tokenizer,程序有时会直接报错,有时虽然能够运行,但模型输出质量会明显下降。

这就像Java服务的序列化协议。服务端使用Protobuf,客户端却按照JSON去解析,最终肯定无法正确通信。

所以,下载模型时不要只下载最大的权重文件,Tokenizer相关文件同样是模型的一部分。

4. Tokenizer不是模型本身

Tokenizer只负责把文本转换成模型能够处理的数字,它本身不负责理解语义,也不会决定模型的知识和能力。

可以这样区分:

Tokenizer:负责“怎么把文字交给模型”
模型参数:负责“根据这些数字进行计算”

六、Chat Template:多轮消息如何交给模型?

1. 模型其实只看到Token序列

我们在程序中通常使用下面这种结构表示一段对话:

messages = [
    {"role": "system", "content": "你是一个Java技术专家"},
    {"role": "user", "content": "什么是Redis的缓存穿透?"}
]

但模型本身并不直接理解rolecontent这两个字段。最终,这些消息还是需要被转换成一串带有特殊控制Token的文本。

例如,某些模型可能使用:

<|system|>你是一个Java技术专家<|end|>
<|user|>什么是Redis的缓存穿透?<|end|>
<|assistant|>

也有一些模型使用:

[INST] 什么是Redis的缓存穿透? [/INST]

不同模型使用的格式可能完全不同。

2. Chat Template是什么?

Chat Template就是一套“消息格式化模板”,负责把下面这样的结构化消息:

system消息
user消息
assistant消息

转换成当前模型训练时使用的Token序列。[4]

在Hugging Face Transformers中,Chat Template通常保存在Tokenizer的chat_template属性中,可以通过apply_chat_template()使用。

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("模型目录")

messages = [
    {"role": "system", "content": "你是一个Java技术专家"},
    {"role": "user", "content": "什么是Redis的缓存穿透?"}
]

input_ids = tokenizer.apply_chat_template(
    messages,
    tokenize=True,
    add_generation_prompt=True,
    return_tensors="pt"
)

add_generation_prompt=True可以理解成在历史消息后面补上“轮到助手回答了”的提示,让模型继续生成助手内容。

3. 为什么Chat Template很重要?

因为聊天模型在训练时,就已经见过某种固定的对话格式。

如果推理时使用了错误的格式,模型通常不一定直接报错,但它可能无法正确识别谁在说话、哪里是问题、哪里应该开始回答,最终表现就是:

  • 回答格式混乱
  • 重复输出用户问题
  • 不按照指令执行
  • 直接继续续写文本

这和Java服务中请求报文格式不一致有点像。字段名写错了,服务未必立刻宕机,但业务结果肯定不对。

Hugging Face官方文档也特别强调了这一点:不同聊天模型可能使用完全不同的控制Token,应该优先使用模型自带的Chat Template。[4]

4. Chat Template和Prompt有什么区别?

Prompt是我们希望模型完成的任务内容,例如:

请介绍一下MySQL的MVCC机制

Chat Template是把系统消息、用户消息、历史回答等内容组织成模型规定格式的规则。

可以简单理解为:

Prompt:我们要告诉模型什么事情
Chat Template:我们用什么格式把这件事情告诉模型

所以,Prompt写得好不好,会影响模型回答;Chat Template用没用对,会影响模型能不能正确理解这次对话。

七、Base模型和Instruct模型

1. Base模型是什么?

Base模型也叫基础模型,通常是经过大规模文本预训练之后得到的模型。

它的核心能力是:根据前面的Token,继续预测后面的Token。

例如输入:

MySQL是一种

Base模型可能会继续生成:

关系型数据库管理系统,广泛应用于互联网业务中……

它更像一个“文本续写模型”,并不一定会严格按照用户的问题进行回答。

Base模型通常适合:

  • 继续预训练
  • 领域微调
  • 研究模型底层能力
  • 文本续写和补全

2. Instruct模型是什么?

Instruct模型是在Base模型基础上,继续使用指令数据进行微调,有些模型还会结合人类反馈或偏好数据进行进一步训练。

它的目标不只是“把句子续写下去”,而是更好地理解用户意图,并按照指令完成任务。

例如用户输入:

请介绍一下MySQL的MVCC机制,并列出它解决的问题。

Instruct模型通常会直接组织答案,而不是继续续写一段看起来像文档的内容。

3. Base和Instruct有什么区别?

可以先用下面这张表进行区分:

对比项 Base模型 Instruct模型
基础能力 预测和续写文本 理解指令并完成任务
训练阶段 主要完成预训练 在Base基础上继续指令微调
适合场景 继续训练、微调、研究 问答、聊天、内容生成
对话能力 通常需要自行组织格式 通常配套Chat Template
普通开发者使用 上手门槛相对高 更适合直接调用

这里的“更适合聊天”不代表Instruct模型拥有更多知识,而是它经过了更适合人机交互的训练。

可以把它理解成:

Base模型:知道如何继续写下去
Instruct模型:更知道应该按照你的要求写什么

4. 为什么下载聊天模型要优先选择Instruct?

如果只是想在本地搭建一个聊天机器人,通常优先选择模型名称中带有下面这些后缀的版本:

Instruct
Chat
Assistant

但这里也不能只看名字,最终还要查看模型卡片。不同厂商的命名方式可能不同,有的模型虽然没有写Instruct,但实际上已经做过指令微调。

上一篇文章提到的Base / Instruct,描述的是模型适合做什么;BF16 / INT4描述的是模型如何保存;AWQ / GPTQ描述的是如何量化。它们不是同一层面的概念。

八、把这些概念串起来

前面讲了很多名词,现在把它们放到一条完整链路里:

用户输入消息
    ↓
Chat Template
    ↓
按照模型约定组织system/user/assistant消息
    ↓
Tokenizer
    ↓
文本转换成Token ID
    ↓
Context窗口限制输入和历史消息长度
    ↓
模型参数进行计算
    ↓
预测并生成下一个Token
    ↓
重复生成,直到达到停止条件
    ↓
Tokenizer解码成文本
    ↓
返回最终回答

如果用Java开发者熟悉的方式类比:

大模型概念 可以类比成什么 主要作用
模型参数 程序运行时真正使用的核心数据 决定模型的计算和能力表现
Token 网络协议中的基本数据单元 模型实际处理的文本单位
Tokenizer 编解码器或序列化组件 文本和Token ID之间相互转换
Context 一次请求的工作区 决定模型当前能看到多少内容
Chat Template 请求报文格式 组织多轮消息和特殊控制Token
Base模型 基础运行版本 更偏向文本续写和后续训练
Instruct模型 面向业务场景的应用版本 更适合直接理解指令并回答

本地部署时可以按照这个顺序排查

如果模型可以启动,但回答结果不对,可以按下面的顺序检查:

1. 当前下载的是Base模型还是Instruct模型
2. Tokenizer文件是否完整,是否和模型匹配
3. Chat Template是否使用正确
4. system、user、assistant消息格式是否正确
5. Context长度是否超过模型限制
6. 是否把Prompt、历史消息和RAG内容拼得过长

如果模型启动就报错,优先检查模型文件、Tokenizer、推理框架和硬件是否匹配;如果模型能够运行但回答奇怪,优先检查模型类型和Chat Template。

九、下载模型时的最小检查清单

好了,前面的概念比较多,我们最后把它们收敛成一张清单。

在Hugging Face下载模型时,至少确认下面这些内容:

1. 模型参数量是多少,机器是否能够承受
2. 下载的是Base模型还是Instruct模型
3. 模型支持的Context长度是多少
4. Tokenizer文件是否完整
5. 是否提供Chat Template
6. 模型使用了哪些特殊Token
7. 推理框架是否支持当前模型格式
8. 实际请求的输入和输出Token是否会超过限制

还可以使用下面的代码简单查看Tokenizer和Chat Template信息:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("模型目录")

print("最大长度:", tokenizer.model_max_length)
print("聊天模板:", tokenizer.chat_template)
print("开始Token:", tokenizer.bos_token)
print("结束Token:", tokenizer.eos_token)

需要注意的是,tokenizer.model_max_length只是Tokenizer侧的配置,实际可用长度还要结合模型配置、推理框架和显存情况判断,不能只看这一个值。

结语

OK,看到这里,相信大家已经对大模型的基本组成有了一个整体认识。

这一章的概念比较多,但可以把它们简单记成下面几句话:

参数:模型训练后留下的一组数字
Token:模型处理文本时使用的基本单位
Context:一次请求中模型能够看到的内容范围
Tokenizer:文本和Token ID之间的转换工具
Chat Template:把多轮消息组织成模型熟悉的格式
Base模型:更偏向文本续写和后续训练
Instruct模型:更适合直接理解指令和进行对话

如果把一次模型调用比作Java服务处理请求,那么:

Tokenizer负责编解码
Chat Template负责组装请求报文
Context负责控制请求大小
模型参数负责真正的计算

后面我们再看到模型仓库中的7B128KInstructTokenizerChat Template时,就不会再觉得它们是一堆互不相关的名词了。

本篇文章依然只是入门扫盲,没有展开Transformer内部结构、Attention计算和训练过程。对于刚开始接触大模型的Java开发者来说,先把一次调用链路跑通,知道每个组件解决什么问题,才是当前最重要的事情。

等这些基础概念熟悉之后,再继续学习Prompt、RAG、LoRA微调以及模型服务化部署,理解起来会顺畅很多。

下一篇文章,我们再继续看看模型到底是如何完成一次推理的,敬请期待。

参考资料

[1] Vaswani 等,Attention Is All You Need

[2] Hugging Face,Tokenizer文档

[3] OpenAI,Understanding tokens

[4] Hugging Face,Chat templates

[5] Hugging Face,Causal language modeling

[6] Long Ouyang 等,Training language models to follow instructions with human feedback