从Token到Next Token:一文读懂大语言模型生成原理
这篇并不是又一个“用框架搭 AI 应用”的教程,而是回到一个最简单的起点:LLM 到底是怎么工作的?如果之前被各种概念绕晕过,那么这篇文章直接帮你把“Token”和“Next Token Prediction”这两个词吃透。项目标题“The Next Token – LLMs, from the Beginning”是一个很有代表性的观点,它把大语言模型从参数、注意力、微调这些复杂名词中拎出来,最终落回一句话:LLM 本质上只是在不断预测下一个 Token。
这次我们不做教学空谈,会直接结合 Python 代码、Hugging Face 生态和 OpenAI 兼容 API 的调用示例,把 Token 是什么、生成文本的循环长什么样、训练和推理怎么对接、显存和 token 计费怎么估算全部串起来。读完这篇文章,你至少能看懂一段 LLM 生成日志、估算一次 API 调用的费用,也能知道“为什么长文本容易爆显存”“temperature 调大为什么容易胡说八道”这些高频问题背后的原因。
1. 核心概念速览
| 能力维度 | 说明 |
|---|---|
| 研究对象 | 大语言模型(LLM)的底层生成机制 |
| 核心单位 | Token,模型实际处理的文本最小单元 |
| 生成方式 | 自回归,每次只预测下一个 Token |
| 训练阶段 | 预训练、监督微调(SFT)、偏好对齐(RLHF/DPO) |
| 关键参数 | 模型参数量、上下文窗口、temperature、top-k、top-p |
| 推理资源 | 权重显存 + KV Cache 显存,具体以模型和上下文为准 |
| 接口能力 | 主流平台均提供 token 计数、chat completion、批量请求 |
| 适合读者 | 刚接触 LLM 的开发者、想深入理解生成逻辑的算法工程师 |
| 不适合场景 | 想一键部署模型的零代码用户,或需要行业级安全认证的生产系统 |
这里先定一个基调:理解 LLM,不需要一开始把所有技术细节都背下来。只需要抓住“Token”和“下一个 Token”这条主线,后面的注意力机制、位置编码、RLHF 都可以慢慢补。本文所有代码示例以学习验证为主,不依赖特定硬件,普通开发机加少量 CPU/GPU 就可以跑通小模型实验。
2. Token:理解 LLM 的最小单位
2.1 为什么是 Token 而不是字
很多初学者会问:LLM 是不是按“字”来理解文本的?实际上不是。中文里“字”的粒度太小,英文里“单词”的粒度又太粗,而且不同语言混合使用的时候很难统一。Token 就是模型自己规定的一种“文本切分单元”,它可能是一个完整单词、半个单词、一个汉字、一个标点符号,甚至是一个字节。
举个例子,英文单词“unbelievable”可能被切分成“un”、“believ”、“able”这样几个 token;中文“今天天气不错”可能被切分成“今天”、“天气”、“不错”。具体怎么切,由分词器(Tokenizer)决定。模型在做生成的时候,并不直接看到原始字符串,而是看到一串 token 编号。这个编号到 token 的映射表,就是模型的词表(Vocabulary)。
这种切分方式带来的直接好处是:模型不需要为每个词汇单独学一个表示,而是可以通过更小的子词单元组合出大量词汇。对中文场景来说,一个常用汉字通常是一个 token,但某些生僻字或表情符号可能会被拆成多个 token,这也直接影响后续讨论的 token 计费。
2.2 一个 Token 大约是多少字符
这是实际开发里最常遇到的量化问题。不同模型的 Tokenizer 词表不同,但大体遵循一个经验值:一个英文 token 大约对应 3 到 4 个字符,一个中文 token 大约对应 1 到 2 个汉字。也就是说,1000 个英文字符大约对应 250 到 340 个 token,1000 个汉字大约对应 600 到 1000 个 token。
这个数字为什么重要?因为几乎所有大模型 API 都按 token 计费,而发一条请求时,用户输入和模型输出都会被计费。你不知道一段文本到底消耗多少 token,就没办法估算成本。另一层影响是上下文窗口容量:模型说“128K 上下文”,并不是说它能处理 128K 个汉字,而是 128K 个 token。所以在实际测试长文本能力时,需要先明确你的文本量换成 token 是多少。
2.3 用代码观察 Token
安装依赖:
pip install transformers接下来用 Hugging Face 的 AutoTokenizer 加载一个小模型分词器,直接观察中英文的 token 切分结果:
from transformers import AutoTokenizer # 这里加载的是一个很小的分词器,仅用于观察 token 行为 tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") text = "今天天气不错,适合学习 LLM。The next token is important." tokens = tokenizer.tokenize(text) ids = tokenizer.convert_tokens_to_ids(tokens) print("原始文本:", text) print("token 数量:", len(tokens)) print("token 列表:", tokens) print("token ids:", ids) # 直接编码 encoded = tokenizer.encode(text) print("encode 后的 ids:", encoded) print("decode 还原:", tokenizer.decode(encoded))运行后会看到中文被按字或词切成多个 token,英文和标点也有各自的编号。这里需要说明:BERT 和 GPT 的分词规则不完全一样,所以同一个句子在不同模型里的 token 数可能不同。真正做生成任务时,应该用你将要部署或调用的那个模型自带的分词器,而不是随意替换。
3. 生成原理:一切都在预测下一个 Token
3.1 自回归生成逻辑
LLM 的生成不是一次给出整段回答,而是一个循环:把当前已经生成的所有 token 作为输入,让模型计算下一个 token 的概率分布,然后按某种策略选出一个 token,拼到序列末尾,再送入模型继续预测。这个过程叫自回归生成(Autoregressive Generation)。
假设输入是“今天天气”,模型先预测下一个 token 是“不”的概率最高,于是文本变成“今天天气不”。接着再预测下一个 token,可能得到“错”,然后序列变成“今天天气不错”。每一步模型都在用自己的输出作为下一步的输入,直到遇到结束符(EOS)或达到最大生成长度。
有一个很容易被忽略的点:模型每一步都在“并行走一次前向计算”。也就是说,生成 100 个 token 就需要执行 100 次前向传播,而不是一次输出 100 个。这也是 LLM 生成速度相对较慢、且对算力要求高的核心原因。虽然现在有投机采样(Speculative Decoding)、批处理、KV Cache 等加速手段,但底层的自回归逻辑没有变。
3.2 最小生成循环
用 Hugging Face Transformers 跑一个最小的自回归生成示例。这里只需要 CPU 即可,模型很小,用来理解流程:
from transformers import AutoTokenizer, AutoModelForCausalLM # 使用一个小型中文模型,仅用于验证生成流程 model_name = "uer/gpt2-chinese-cluecorpussmall" tokenizer = AutoTokenizer.from_pretrained(model_name) # 部分模型没有 padding token,这里简单处理 tokenizer.pad_token = tokenizer.eos_token model = AutoModelForCausalLM.from_pretrained(model_name) prompt = "人工智能的下一步是" inputs = tokenizer(prompt, return_tensors="pt") # 直接生成 output_ids = model.generate( inputs.input_ids, max_new_tokens=30, do_sample=True, top_k=50, top_p=0.95, temperature=0.8, ) output_text = tokenizer.decode(output_ids[0], skip_special_tokens=True) print("输入:", prompt) print("输出:", output_text)这段代码已经包含 do_sample、top_k、top_p、temperature 等采样参数,它们会在后面的章节详细解释。此时只需要理解一件事:模型接收 input_ids,然后返回新的 id 列表,最后通过 decode 还原成文字。
如果你想观察“每一步选择哪个 token”,可以手动实现一个循环。这会更加直观:
import torch device = "cuda" if torch.cuda.is_available() else "cpu" model.to(device) model.eval() input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to(device) with torch.no_grad(): for _ in range(20): logits = model(input_ids).logits next_token_logits = logits[0, -1, :] # 用 temperature 缩放 scaled_logits = next_token_logits / 0.8 probs = torch.softmax(scaled_logits, dim=-1) # 概率最高的 token next_token_id = torch.argmax(probs).unsqueeze(0).unsqueeze(0) input_ids = torch.cat([input_ids, next_token_id], dim=-1) print(tokenizer.decode(next_token_id[0], skip_special_tokens=True), end="") print() print(tokenizer.decode(input_ids[0], skip_special_tokens=True))这个手写循环展示了最朴素的“贪心解码”:每次都取概率最高的 token。实际产品里不会只用贪心,因为贪心容易让回复单调、重复。于是就有了下一节要讨论的采样策略。
4. 训练流程:预训练、指令微调与对齐
理解生成原理后,再看训练就不难了。LLM 训练的第一阶段是预训练(Pretraining),目标非常简单粗暴:给模型喂海量文本,让它不断预测下一个 token。损失函数就是预测 token 和真实 token 之间的交叉熵。这个阶段让模型学会语言规律、知识体系和基本推理能力。
预训练结束后,模型已经能“接话”,但它不一定懂得如何回答问题,也可能输出无意义内容。于是有了第二阶段:监督微调(Supervised Fine-Tuning, SFT)。人工标注一批“用户输入-期望输出”对,比如“请介绍 LLM 的 token 概念”对应一段标准回答,再继续训练模型。这个阶段会让模型学会指令跟随的基本格式。
第三个阶段是对齐,常见方法包括 RLHF(基于人类反馈的强化学习)和 DPO(直接偏好优化)。目标是让模型输出符合人类价值观和偏好,减少有害内容、提高回答质量。通俗讲:预训练让模型“能说”,SFT 让模型“会回答”,对齐让模型“说得像正常人”。
从工程角度,这里给一个清晰结论:普通开发者不需要从零训练大模型。绝大多数场景是加载一个开源底座模型,用自己的领域数据做 SFT 或 DPO,或者干脆直接调用 API。开源的 7B 到 14B 模型,在消费级显卡上可以用 LoRA 等参数高效微调技术跑起来,但这是另一个话题。理解训练流程的目的,是为了知道模型能力上限在哪里,以及为什么模型会“一本正经地胡说八道”。
5. 推理采样:temperature、top-k、top-p
5.1 参数含义
生成时,模型输出的不是最终文本,而是下一个 token 的概率分布。如何从这个概率分布里挑 token,直接决定了回答质量。
temperature 控制概率分布的平滑程度。temperature 越低,高概率 token 的优势越明显,输出更确定;temperature 越高,低概率 token 也有机会被选中,输出更多样,但也更容易乱说。实际经验是:代码生成、数学推理用较低温度(0.1 到 0.3),创意写作、头脑风暴用较高温度(0.7 到 1.0)。
top-k 只保留概率最高的 k 个 token 作为候选,其余全部过滤。top-p 则按累计概率过滤,从概率最高的 token 开始累加,直到累计概率超过 p,然后在这个候选集合里重新归一化采样。top-k 和 top-p 可以一起用,也可以单独用。
下面是一个简单的参数对比表:
| 参数 | 作用 | 调低效果 | 调高效果 | 适用场景 |
|---|---|---|---|---|
| temperature | 整体概率分布平滑度 | 更保守、更确定 | 更多样、更发散 | 低:代码/数学;高:创意 |
| top-k | 候选 token 数量 | 候选变少,更稳定 | 候选变多,更多样 | 配合 top-p 使用 |
| top-p | 候选累计概率范围 | 候选变少,更稳定 | 候选变多,更多样 | 控制输出随机性 |
5.2 接口调用示例
如果你调用的是 OpenAI 兼容接口,一般会暴露 temperature、top_p、max_tokens 等参数。下面是一个通用请求模板,实际地址、模型名和密钥需要替换成你自己的服务:
import requests url = "http://127.0.0.1:8000/v1/chat/completions" payload = { "model": "your-model-name", "messages": [ {"role": "user", "content": "请用一句话解释什么是 Token?"} ], "temperature": 0.3, "top_p": 0.9, "max_tokens": 200 } headers = { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post(url, json=payload, headers=headers, timeout=60) data = response.json() print(data["choices"][0]["message"]["content"]) print("usage:", data.get("usage"))返回结果里的 usage 字段通常会包含 prompt_tokens、completion_tokens 和 total_tokens,这是做 token 计费和生产监控的重要数据。如果你自己部署模型,也可以参照这个结构封装一个简单的服务。
6. 上下文、KV Cache 与资源占用
6.1 上下文窗口
上下文窗口(Context Window)是模型一次最多能处理的 token 数量,包括输入和输出。常见的有 4K、8K、32K、128K、200K 等。超过这个限制,模型就会表现异常:早期输入可能被截断,或者明确报错。实际使用中,建议保留一定余量,不要把窗口完全占满。
为什么长上下文会消耗大量资源?因为 Transformer 模型在推理时,需要缓存已生成 token 的 Key 和 Value 矩阵,这个缓存叫 KV Cache。生成时每新增一个 token,KV Cache 就会变大一点。所以上下文越长,显存占用越高。这也是很多本地部署用户发现“单条短对话显存够,长对话突然 OOM”的原因。
6.2 显存估算
显存占用主要由三部分构成:模型权重、KV Cache、中间激活值。模型权重部分可以快速估算:一个 7B 模型用 FP16 加载,权重约占 14GB(7B 个参数,每个参数 2 字节)。用 INT8 量化后约 7GB,用 INT4 量化后约 3.5GB。
KV Cache 大小则取决于模型层数、注意力头数、隐藏层维度和当前上下文长度,不能简单按一个数字估算。实操建议是:先用默认配置跑通,再逐步增加上下文长度,用 nvidia-smi 实时观察显存曲线。下面给出一段常用的观察命令:
nvidia-smi --query-gpu=memory.total,memory.used,memory.free --format=csv watch -n 1 nvidia-smi6.3 生成速度观察
除了显存,生成速度也需要关注。LLM 推理的加载阶段是并行计算,速度较快;生成阶段是逐 token 自回归,速度较慢。可以用下面这段小逻辑统计每秒生成 token 数:
import time start = time.time() output_ids = model.generate( inputs.input_ids, max_new_tokens=50, do_sample=True, top_p=0.95, temperature=0.8, ) elapsed = time.time() - start generated_tokens = output_ids.shape[1] - inputs.input_ids.shape[1] print(f"耗时 {elapsed:.2f} 秒,生成 {generated_tokens} token,速度 {generated_tokens / elapsed:.1f} token/s")如果你在接入生产环境,记录生成速度和 token 数非常关键,这能帮你预估高峰期的并发能力和成本。
7. 接口 API、Token 计费与批量任务
7.1 Token 计费
几乎所有商业大模型 API 都按 token 计费,而不是按字符数。计费通常分输入和输出两个价格,输入便宜、输出更贵。原因很简单:输出端是逐 token 自回归生成的,计算量远大于输入端。
开发者在设计应用时,要考虑“一次请求花多少钱”。比如每 1000 个输入 token 花费 0.002 元、每 1000 个输出 token 花费 0.006 元(数字仅示例,实际价格以平台公布为准)。那么一次输入 2000 token、输出 500 token 的请求就是2 * 0.002 + 0.5 * 0.006 = 0.007元。积少成多,批量任务尤其要提前估算成本。
7.2 批量任务与重试策略
批量任务的典型场景是“给 10000 条文本做摘要”或“给一批文档做知识问答”。此时不能简单写一个 for 循环逐个请求,因为可能遇到限流、超时和单点失败。工程上建议:
- 把任务拆成文件或数据库中的一批记录,每条记录记录状态。
- 每条请求独立设置超时时间,失败后自动重试 2 到 3 次。
- 增加并发数时,分阶梯往上涨,观察接口返回的限流状态。
- 处理长文本时,分批或分页发送,避免超过上下文上限。
- 记录每次请求的 token 消耗,生成费用报表。
示例:一个简单的批量处理框架。
import time import requests tasks = ["task001", "task002", "task003"] results = {} def call_llm(text): # 这里替换成真实接口 url = "http://127.0.0.1:8000/v1/chat/completions" payload = { "messages": [{"role": "user", "content": text}], "max_tokens": 200, "temperature": 0.3 } for attempt in range(3): try: resp = requests.post(url, json=payload, timeout=30) resp.raise_for_status() return resp.json() except Exception as e: if attempt == 2: raise e time.sleep(2 ** attempt) for task in tasks: content = f"这是任务 {task} 的输入文本" try: result = call_llm(content) results[task] = result["choices"][0]["message"]["content"] print(task, "成功") except Exception as e: results[task] = f"失败: {e}" print(task, "失败")这个模板没有绑定具体平台,可以直接改造成自己的批量任务模块。关键点在于:状态要可追踪,失败要重试,并发要受限。
7.3 常见 API 错误
| 错误类型 | 现象 | 排查方向 |
|---|---|---|
| 上下文超限 | 输入 token 超过模型最大限制 | 截断、摘要或换更大窗口模型 |
| 限流 | 出现 429 Too Many Requests | 降低并发、增加退避时间 |
| 鉴权失败 | 403 / 401 | 检查 API Key、权限和地区支持 |
| 超时 | 长时间无返回 | 增加 timeout,或减小 max_tokens |
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 生成结果重复 | 温度过低或缺省 top_p | 检查采样参数 | 适当上调 temperature 到 0.7,或启用 top-k |
| 生成内容偏离主题 | 温度过高 | 查看输出概率分布 | 降低 temperature,或设更小 top-p |
| 输入过长报错 | token 超过上下文窗口 | 打印 usage 或 token 数量 | 截断、摘要、分块处理 |
| 显存不足 | 模型权重 + KV Cache 超限 | nvidia-smi 观察峰值显存 | 量化模型、减小上下文、换小模型 |
| 生成速度太慢 | 自回归 token 数多 | 测量 token/s | 减少 max_tokens、用批量推理、升级硬件 |
| 接口偶发失败 | 网络波动或限流 | 查看状态码 | 加重试、退避、错误日志 |
| token 数量统计不一致 | 用了错误的分词器 | 检查 tokenizer 是否匹配模型 | 使用模型自带 tokenizer |
排查原则是:先看日志和状态码,再复现最小用例,最后考虑参数调整。不要一上来就换模型,很多问题其实出在参数或输入处理上。
9. 最佳实践与后续学习路线
把“The Next Token”这条主线吃透之后,建议做三件事。
第一,用一个本地小模型把生成循环和采样参数跑通。不需要高配显卡,CPU 也能跑。重点观察温度变化对输出的影响,以及 token 数量如何变化。这一步能建立直觉。
第二,接一次真实 API。无论你用哪个平台,先打印一次请求的 usage 字段,记录输入和输出 token 数,自己算一下费用。然后尝试写一个小批量任务,加上重试和日志。这样你会对“token 计费”“上下文窗口”“限流”有真实感知。
第三,再回头看更高级的概念。注意力机制解决的是“模型如何关注前文中的关键 token”,位置编码解决的是“token 顺序怎么表达”,KV Cache 解决的是“生成加速”。有了主线,这些概念就不再是孤立的点,而是围绕同一个目标展开:更好地预测下一个 token。
下一步还可以学习这些方向:
- 用 Transformers 库加载一个 7B 模型,尝试本地推理。
- 用 LoRA 做领域微调,观察训练数据如何影响生成。
- 用 vLLM 或 FastChat 部署一个兼容 API 的服务,测试并发。
- 跑一遍向量检索和 RAG 流程,理解“外接知识”如何影响 token 上下文。
最后给一个实用建议:笔记里优先记 token 数量、显存占用、生成速度和失败重试策略,而不是只记概念。因为这些数字会直接决定你的本地部署方案和 API 成本。把这套实践跑下来,再回去看模型源码和论文,会顺畅很多。
