用一句话说出 LLM 计算的是什么——并追踪这一个计算如何在循环中跑出整段文本。
后面的每一章都聚焦于某一个组件。不先建立全局图景,这些机器零件就无处安放。
术语表 · 10 个术语
- language model
- 一个函数:给定到目前为止的 token,为每个可能的下一个 token 的可能性打分。
- token
- LLM 读写的基本单位——一个子词片段,既不是字符,也不是完整的词。
- logits
- 模型输出的原始无界分数——词表中每个条目各一个——softmax 把它们变成概率之前的样子。
- softmax
- 把每个 logit 取指数,再除以它们的总和:e^(z_i) / Σ_j e^(z_j)。将一组原始分数变成总和为 1 的概率分布。
- forward pass
- 模型在一个 token 序列上的一次完整计算,产生下一个位置的 logits。
- weights
- 模型存储的 852,985,920 个数字(权重)——训练时定下,之后冻结。对每个请求都相同;它们就是模型本身。
- activations
- 前向传播中为你的提示词现场算出的中间向量(激活值),这趟计算一结束就被丢弃。
- autoregressive
- 一次生成一个 token,把每个输出再喂回去,作为下一步的输入。
- sampling
- 按 softmax 概率加权,随机挑出下一个 token。“贪心”(greedy)解码则永远直接取分数最高的那一个。
- generation loop
- 前向传播 → 采样一个 token → 追加 → 重复,直到遇到停止 token 或达到长度上限。
什么是大语言模型?
剥去神秘的外衣,大语言模型其实就是一个函数。你交给它一串 token——也就是到目前为止的文本——它会为词表中的每一个 token 返回一个分数:对“接下来是什么”的猜测。这就是全部计算。本课程余下的内容,要么讲这个函数是怎么构建的,要么讲它是怎么被使用的。
一次调用:输入 token,为每个候选的下一个 token 打分
具体来说,输入只是一个整数列表——每个 token 对应一个 id,由 分词器 产生——输出则是一个很大的小数数组,词表中每个词对应一个分数:
tokens: number[] // a list of integers, one id per token,
// e.g. [760, 7993, 7338, 383, 279] = "The cat sat on the"
│
▼ one forward pass
logits: Float32Array(248,320) // an array of decimals — one raw score per vocab word这些原始分数就叫 logits。把它们过一遍 softmax(归一化指数函数),就得到“接下来是什么”的概率分布——对 Qwen3.5-0.8B 来说,是覆盖它认识的全部 248,320 个 token 的分布。模型从不直接输出某个词;它为每个可能输出的词各打一个分,再由一条单独的规则挑出一个。
那个“过一遍 softmax”的步骤小到可以手算——下面就在一个只有五个 token 的玩具词表上演示,让你在把规模扩大到全部 248,320 个之前,先看清原始分数是怎么变成概率的:
真实模型会输出 248,320 个 logits——词表中每个 token 一个。把它们变成概率的变换与个数无关,所以这里只用五个来演示。拖动滑块缩放原始 logits:放大让分布向第一候选收尖,缩小则把它摊平。
| token | zi (logit) | exp(zi) | ÷ Σ | pi |
|---|---|---|---|---|
| "cat" | 2.40 | 11.023 | ÷ 19.064 | 0.578 |
| "dog" | 1.60 | 4.953 | ÷ 19.064 | 0.260 |
| "ran" | 0.70 | 2.014 | ÷ 19.064 | 0.106 |
| "the" | -0.30 | 0.741 | ÷ 19.064 | 0.039 |
| "." | -1.10 | 0.333 | ÷ 19.064 | 0.017 |
| Σ | — | 19.064 | 1.000 |
五个 token 的词表与手挑的 logits 仅为示意——不是模型的实时输出。
Sharpen ↔ flatten(锐化 ↔ 压平)滑块会在 softmax 之前对原始 logits 做缩放:往上拖,模型对自己的第一候选押得更狠(分布出现尖峰);往下拖,概率在各个选项之间摊得更平。这正是生成文本时 temperature (温度)所调的那个旋钮——在这里它表现为温度的倒数(数值越高分布越尖)。
两类数字:权重 vs 激活值
再往下走之前,先把这里出现的数字分进两个桶——分清楚了,后面的一切都会顺理成章。第一个桶是权重(weights):那 852,985,920 个存下来的数字,它们就是 Qwen3.5-0.8B 本身。它们在训练时被定下,从那以后一直冻结——推理时只读,而且无论是你的提示词还是别人的,它们都逐位相同。第二个桶是激活值(activations):前向传播专为你这串 token算出的中间向量,随着数据流过各层而产生。它们在你的请求到来时诞生,在这趟计算结束的那一刻就被丢掉。(有一小片会跨循环步幸存—— KV 缓存——但那只是对话内部的一项优化,不是对你的记忆。)
- 852,985,920 个数字
- 只在训练时学到一次
- 推理时只读——对每个请求都一模一样
示意图,只为呈现这组对比——并非模型的实时状态。
这个划分也是整门课的地图:训练( 训练一章)是改写左边那个桶的过程;而推理——本课程接下来做的一切——永远只是往右边那个桶里填数、再倒空。它还解释了一个常让人意外的事实:模型对每个用户都是同一件制品,调用与调用之间什么也不记得,因为你的提示词算出的任何东西都不会被写回权重。
这些数字从哪来?是“拟合”出来的,不是写出来的
那么,852,985,920 个具体的数字是从哪来的?不是谁一个个写出来的。传统软件是程序员亲手写明的规则:如果邮件里出现“免费领钱”,就标记为垃圾邮件。每一种行为都是某个人想清楚、再敲出来的一行代码。而大语言模型恰恰是反着造的。没有谁能手写出“任意句子里的下一个词是什么”这样一条规则——于是改用另一种办法:先把这些数字随机初始化,再给模型看海量真实文本,反复微调这些数字,让它的猜测一点点逼近真正出现的下一个词。行为从来不是被写明的;它是被拟合到样例上的。(这个拟合过程—— 训练一章——对模型里的每一个数字都是同一套思路,无论它落在哪一种层里。)
这件事最小的、能装进脑子里的版本,就是一条直线。直线 y = a·x + b 恰好有两个旋钮:斜率 a 和截距 b。给它一些散落的样例点,“训练”无非就是拧动这两个旋钮,直到这条线尽可能贴近这些点:
y = a·x + b——斜率 a 与截距 b 是仅有的两个旋钮
训练不断微调这两个数字,直到这条线尽可能贴近这些散点。
这就是全部的把戏,只是放大到了几乎难以置信的程度。一条直线有 2 个旋钮,Qwen3.5-0.8B 有 852,985,920 个——大约是它的 4.26 亿 倍(852,985,920 ÷ 2 = 426,492,960)。同样的动作,只是旋钮多得无法想象:不再是用一个斜率和一个截距去弯折一条线,而是去拟合数以亿计的数字,让那一个庞大的函数尽可能贴近“人类真正写出的下一个 token”——覆盖人们几乎所有写下来的东西。
生成循环
一次前向传播只给出一个 token 的预测。要写出一句话,就得循环调用这个函数——采样出一个 token,追加到末尾,再对这串稍微变长的列表跑一遍。(“采样”的意思就是:按概率加权随机挑一个 token——概率最高的那个通常会赢,但不是每次都赢。“贪心”模式则跳过掷骰子,永远拿第一名。)
tokens = tokenize(prompt)while (!done) {logits = model(tokens) // 一次前向传播next = sample(logits) // 挑出一个 tokentokens.push(next) // 追加,然后重复}
数字为示意,按脚本演示循环的形状——不是模型的实时输出。
LLM 是一个函数:输入一串 token,为词表中的每一个 token 输出一个分数。要写出不止一个 token,它就放进循环里跑:前向传播 → 采样一个 token → 追加 → 在变长了的列表上再跑一遍。“生成文本”的全部含义,就是这一个反复执行的步骤。
这就是“生成文本”的含义:它是自回归(autoregressive)的。每个新 token 都来自把整个模型在比上次长一个 token 的序列上重新跑一遍。(每一步都重跑全部计算听起来很浪费—— KV 缓存一章 会讲它是怎么被做便宜的。)
一口气看完整条流水线
在那一次前向传播内部,一个 token 的旅程是:文本 → token → 向量(嵌入)→ 一摞很深的注意力 + MLP 块 → 为每个 token 给出的最终分数(LM head)→ 采样出一个 → 追加,然后循环。(这摞“深堆叠”是混合的:24 层中有 6 层用的是注意力一章要讲的注意力;另外 18 层用一种更省算力的捷径,在 KV 缓存一章中介绍。)后面的每一章都会打开其中一个盒子; 架构一章 再把它们拼回到同一页上。
更妙的是,下面就是这趟旅程的可逐步播放版本——一个具体的 token,从原始文本一路走到下一个词,每一步都标出数据的形状:
模型读不了原始文本。分词器先把你的文本切成认识的片段,叫作 token——这里是 5 个——再查出每个 token 的 id(一个普通整数)。从这里开始,模型看到的只有这些数字。
前导空格是 token 的一部分。
仅为示意——真实前向传播的简化示意图;图中的数字和热力图只是真实 1024 维向量与 248,320 个 logits 的替身,并非模型实时输出。
为什么“预测下一个 token”就够了
这听起来简单得近乎无趣。关键在于它被要求达到的标准:要在全人类的文本上——文章、代码、对话、翻译、算术——把下一个 token 预测好,模型就不得不内化语法、事实和推理模式,因为正是这些东西让下一个 token 变得可预测。能力是把一个狭窄目标做到极致的副产品。
先把一句实话说在前面:我们刚才描述的是基础模型——纯粹的自动补全。从它跳到一个会听从你指令的 ChatGPT 式助手,是另一段训练的故事,留到后面的 “从基础模型到助手”一章再讲。
第二句实话:这种逐 token 的猜测读起来有多连贯,很大程度上取决于规模。要拟合的旋钮越多,越多的语法、事实和推理模式才能被捕捉进去——所以更大的模型往往能在更长的篇幅里不跑题。本课程里跑的这个模型是刻意做小的——0.85B 参数,专门挑成能完全跑在你浏览器标签页里的大小。它不是任何模型的缩水版或过时版:它就是 Qwen3.5-0.8B,发布于 2026 年,24 层里混合了 6 个 full-attention 层和 18 个更省算力的 GatedDeltaNet 层。但在这个尺寸下、又没有经过指令微调,它会跑偏是意料之中的——开头一句还像样,接着就飘到某个奇怪的地方去了。给你一个它所处量级的感觉:这个模型约 0.85B 参数,一个小型开源模型大概 ~1.5B,而最初的 GPT-3 约为 ~175B——比这里跑的这个模型又大了大约 200 倍(不是比那个 1.5B 的模型)。你可以亲眼看着我们这个 0.85B 模型跑偏,并用 采样一章里的温度控件去调节它跑得有多野。
- LLM 就是一个函数:输入一串 token,输出词表中每个可能的下一个 token 的分数。
- 文本是一次一个 token 生成的——同一个前向传播在循环里反复运行,每一轮多看到一个 token。
- 推理阶段的全部工作就是“预测下一个 token”;能力是这个目标逼着模型学会的东西。
在生成循环的动画里观察伪代码。当一个新 token 加入左侧的 token 条时,恰好是哪一行被高亮?