GPT 只是在猜下一个词,为什么却像会思考:我把 ChatGPT 拆到了 Transformer
2 月 9 日晚上,我终于注册上了 ChatGPT,在“灵感”里只写了两句话:太不容易了,好用是好用,就是太贵了。
接下来的几天,我把能想到的问题都丢给它。让它解释指针,它能换一个比老师课件更顺的比喻;让它改一段 C 代码,它不只指出数组越界,还会补上原因;故意给它一个不存在的函数,它又能一本正经地编出参数和返回值。
这几种表现放在一起很别扭。它有时像一个耐心的助教,有时又像一个完全不懂装懂的人。最让我困惑的是,资料都说 GPT 的训练目标只是“预测下一个 token”。猜下一个词听起来像输入法联想,为什么规模放大之后,会像是在分析问题?
我没有办法看到 ChatGPT 内部的全部实现,OpenAI 也没有公开每个训练细节。但 Transformer、语言模型训练和 InstructGPT 的公开论文,已经足够把主要链路拼起来。
我先用几个笨实验确认,它不是在查固定答案
刚用上 ChatGPT 时,我以为它背后可能是一套很大的问答库。这个猜测很快就解释不通了。
我把熟悉的问题改得很奇怪,例如让它“用食堂排队解释进程调度,再指出这个类比哪里会失真”。如果只是从数据库里找现成答案,它很难刚好命中这种组合。它却能把时间片说成每个人轮流打饭,又提醒我真实进程有优先级、阻塞和多核执行,食堂比喻只能解释一部分。
我再要求它把刚才的解释缩成 100 字,改成大一学生口吻,最后用 C 伪代码表示。内容会跟着限制重新组织,不像从某个页面复制一段文字。
但换一个冷门库,让它给出准确 API,它可能生成一个看起来非常合理、实际上根本不存在的函数名。更奇怪的是,它不会在胡编时露出明显犹豫,语气和回答正确问题时差不多。
这让我换了一个理解方向:它保存的不是一张“问题到答案”的表,更像是从海量文本里压缩出一套语言和知识之间的统计关系。它能现场组合答案,也会现场组合错误。
GPT 读到的不是文字,而是一串 token 编号
程序不能直接把“为什么 0.1 + 0.2 不等于 0.3”送进神经网络。第一步要做分词,也就是 Tokenization,把文本切成模型词表中的 token。
token 不完全等于中文的一个字,也不等于英文的一个单词。常见片段可能独立成 token,生僻组合可能被拆得更碎。不同模型使用的词表和分词器也不同,所以同一句话在不同模型里可能占用不同数量的 token。
可以把过程粗略理解成:
原始文本:
为什么 0.1 + 0.2 不等于 0.3
分词器:
[为什么] [空格] [0] [.] [1] [空格] [+] ...
词表编号:
[18493, 220, 15, 13, 16, 489, ...]上面的切分和编号只是示意,不代表某个具体 GPT 模型的真实输出。重要的是,模型后面处理的是整数编号序列。
token 也解释了两个使用时很容易忽略的问题。
第一,上下文窗口按 token 计算,不按“文章有多少字”计算。代码、中文、英文和特殊符号的 token 密度不同,不能用固定字数准确换算。
第二,模型一次通常生成一个 token。我们在网页上看到答案一个片段一个片段往外出现,不只是为了做打字动画,而是生成过程本来就在不断预测下一个 token,再把刚生成的结果接回输入。
输入: 数组越界会导致
预测: 未定义
新输入: 数组越界会导致未定义
预测: 行为
新输入: 数组越界会导致未定义行为
预测: ,从外面看是一句话,从模型内部看是一连串重复的预测。
Embedding 把离散编号变成可以计算的方向
token 编号只是词表中的位置。编号 100 和编号 101 并不意味着两个 token 比编号 100 和 900 更相似。神经网络需要把每个编号查表转换成一个向量,这一步叫 Embedding。
假设为了演示,每个 token 只用 4 个数字表示:
“猫” -> [ 0.8, 0.2, -0.1, 0.6]
“狗” -> [ 0.7, 0.3, -0.2, 0.5]
“编译”-> [-0.4, 0.9, 0.7, -0.1]真实模型的向量维度大得多,单个维度也没有“动物”“动作”这样方便人类阅读的固定标签。语义分布在许多维度和网络层中。不过,从训练结果看,出现在相似上下文里的 token,向量关系往往也更接近。
向量化之后,文字才能参加矩阵乘法。模型不是把“猫”这个汉字当成图形理解,而是在不断变换它对应的数值表示,让这个表示逐渐包含当前句子需要的信息。
这里还有一个问题:同一个 token 放在不同位置,意义可能不同。“我打了他”和“他打了我”包含相同的几个字,顺序一换,关系完全反过来。Transformer 会加入位置信息,让模型知道每个 token 在序列里的位置。不同实现可以使用学习得到的位置表示或其他位置编码方法,不能简单说所有 GPT 都使用同一种方案。
自注意力让每个 token 判断“这句话里该看谁”
Transformer 最核心的结构是自注意力。第一次看公式时我觉得名字比内容吓人,后来把它理解成一个问题:当前 token 为了更新自己的含义,应该从前面的哪些 token 取信息,各取多少?
看一句有歧义的话:
小王把书放进箱子,因为它太重了。“它”可能指书,也可能指箱子。模型处理“它”时,需要结合“书”“箱子”“放进”和“重”等上下文判断指代。注意力机制会为 token 之间计算相关程度,再按权重汇总信息。
每个 token 的当前向量会经过三组线性变换,得到 Query、Key 和 Value:
Query: 我现在想找什么信息
Key: 我这里有什么信息,可以怎样被匹配
Value: 如果你关注我,我实际提供什么内容计算可以写成:
QK^T 计算查询和各个键的匹配分数,除以 \sqrt{d_k} 是为了避免维度较大时数值过大,softmax 再把分数变成总和为 1 的权重。最后用这些权重加权 Value。
一个极简示意如下:
当前 token: “它”
对前文的注意力权重:
小王 0.05
书 0.28
放进 0.07
箱子 0.46
因为 0.04
太重 0.10
加权汇总后,“它”的表示更偏向“箱子”相关信息真实模型不会只有一组注意力,而是多头注意力。不同头可以学到不同关系,有的偏向局部语法,有的关注远距离指代,也可能出现人很难直接命名的模式。每一层处理后,token 的表示都会变化;堆叠很多层之后,同一个字的向量已经不再只是词典含义,而是带着当前上下文。
因果遮罩决定了 GPT 只能从左往右预测
GPT 属于自回归语言模型。训练它预测某个位置时,不能提前偷看后面的正确答案,否则任务会变成抄答案。
Transformer 会使用因果遮罩,让第 i 个位置只能关注自己和它前面的 token:
可关注的位置
第 1 个 token: 1
第 2 个 token: 1 2
第 3 个 token: 1 2 3
第 4 个 token: 1 2 3 4如果句子是“C 语言中的指针保存地址”,模型在预测“地址”时可以看到“C 语言中的指针保存”,但不能看到训练文本里已经存在的“地址”。
这个限制贯穿生成过程。模型先根据已有上下文产生一个 token,再把它加入上下文,继续产生下一个。它没有先在脑子里写好完整文章再逐字念出来。所谓“前后规划”,也必须通过隐藏状态、上下文中的中间文字或额外推理过程逐步形成。
这解释了为什么长回答有时会开头承诺五点,写到后面却只剩四点。局部每一步都可能合理,但模型没有一个独立于文本之外、永远准确的文章大纲计数器。
一层 Transformer 不只有注意力
注意力负责交换序列中的信息,后面通常还有前馈网络,也就是对每个位置分别执行的多层感知机。再加上残差连接和归一化,构成一个 Transformer Block。
token 向量序列
│
▼
多头自注意力 <── token 之间交换信息
│
残差连接与归一化
│
▼
前馈网络 <── 对每个位置做非线性变换
│
残差连接与归一化
│
▼
下一层 Transformer Block残差连接会把一层的输入直接加到输出上,让深层网络更容易训练,也让信息不必每经过一层都彻底重写。前馈网络则提供非线性变换能力,否则许多线性层叠起来仍然只是一个更大的线性变换。
经过多层处理,最后一个位置的向量会被映射成词表中每个 token 的分数,也叫 logits。分数越高,模型越倾向把那个 token 当作下一步输出。
“预测下一个 token”到底怎样训练出知识
训练样本可以是一段普通文本:
操作系统通过页表把虚拟地址映射到物理地址。这一个序列可以同时提供很多预测任务:
操作 -> 预测“系统”
操作系统通过 -> 预测“页表”
操作系统通过页表 -> 预测“把”
...模型给正确 token 的概率越低,损失就越大。反向传播会计算参数应该怎样调整,优化器再一点点更新权重。海量文本经过很多轮这样的训练,模型为了降低下一个 token 的预测误差,不得不学习语法、常见事实、代码结构和概念之间的关系。
这件事最反直觉的地方是,训练目标看起来很窄,逼出来的内部能力却很广。想准确续写“如果链表当前节点为空,那么下一步应该”,模型最好理解链表和空指针;想续写一段 Python 函数,它得掌握缩进、变量作用域和常见算法;想接住一篇故事,它还要跟踪人物和前因后果。
我更愿意把它理解成压缩。模型不可能逐字保存所有训练文本,只能把反复出现的结构压进参数。压得越好,越能在新上下文里还原出合适的模式。不过,“压缩了知识”不等于内部有一套像数据库那样可查询、可校验的事实表。
我们知道公开原理,不等于知道 ChatGPT 的全部内部细节
GPT 名称来自 Generative Pre-trained Transformer,公开论文能解释主干结构。ChatGPT 使用了怎样的数据配比、过滤流程、参数规模和每个训练阶段,外部并不知道完整答案。讨论机制时应区分 Transformer 的通用原理、公开的 InstructGPT 方法,以及对具体产品实现的推测。
预训练模型会续写,不一定会听话
只做预训练的语言模型,目标是续写互联网文本。用户输入“请解释递归”,它未必天然知道自己应该像助手一样回答,也可能续写成论坛对话、文章标题或另一段提问。
让模型变成聊天助手,通常还要做指令微调。人工整理“指令和理想回答”样本,让模型学习用户提出任务后该怎样回应。这一步常叫 Supervised Fine-Tuning,也就是监督微调。
公开的 InstructGPT 流程还加入了人类反馈。标注者比较多个候选回答,训练一个奖励模型去预测人更喜欢哪一个,再用强化学习让语言模型更倾向产生高奖励回答。简化后的链路是:
大规模文本预训练
│ 学会语言、代码和知识模式
▼
人工示范监督微调
│ 学会按指令回答
▼
人类对候选结果排序
│ 训练奖励模型
▼
强化学习继续调整
│ 更符合有用、诚实、安全等偏好
▼
对话模型这能解释为什么 ChatGPT 的回答比普通续写模型更像助手:它不只是会生成文字,还被训练成偏好某种回答方式。
但“人更喜欢”与“事实绝对正确”不是同一个目标。一个措辞顺畅、结构完整的错误答案,可能比一句支离破碎但谨慎的回答更讨喜。训练会改善行为,却没有给模型接入一个永远正确的事实裁判。
温度不是让模型变聪明,而是改变抽样的胆量
模型得到 logits 后,要把它们转成概率并选择下一个 token。最简单的方式是总选概率最高的那个,但这样容易重复、死板,也不一定能得到整体最好的句子。
温度 temperature 会调整概率分布。可以用一段小程序观察:
import math
def softmax_with_temperature(logits, temperature):
scaled = [value / temperature for value in logits]
largest = max(scaled)
exps = [math.exp(value - largest) for value in scaled]
total = sum(exps)
return [value / total for value in exps]
logits = [4.0, 2.0, 1.0]
for temperature in (0.5, 1.0, 2.0):
probabilities = softmax_with_temperature(logits, temperature)
print(temperature, [round(value, 3) for value in probabilities])输出类似:
0.5 [0.98, 0.018, 0.002]
1.0 [0.844, 0.114, 0.042]
2.0 [0.629, 0.231, 0.14]温度低,最高分 token 更容易胜出,回答通常更稳定;温度高,低分候选也有机会被选中,结果更发散。它改变的是采样分布,不会给模型补充没学过的知识,也不会自动修复逻辑错误。
实际生成还可能使用 top-p 等策略,只在累计概率达到阈值的一组候选中采样。无论策略多复杂,下一步仍然来自概率分布。这就是同一问题重复询问可能得到不同答案的原因之一。
幻觉不是偶尔短路,而是训练目标留下的缺口
ChatGPT 最危险的地方,不是它明确说“不知道”,而是能把错误包装得很完整。
如果我问一个不存在的 Python 库函数,模型会根据真实函数常见的命名、参数和文档语气,拼出一个高度像真的答案。从语言建模角度看,它做得甚至不错:生成的文本符合上下文统计规律。问题是,我们问的不是“请写一段像 API 文档的文字”,而是“这个 API 事实上存在吗”。
模型的基础目标负责预测文本,不负责在输出前访问真实世界做验收。参数里的知识也有时间范围、覆盖偏差和压缩误差。只要上下文强烈暗示应该继续回答,它就可能选择一个语言上合理的后续。
这也是为什么让模型“不要胡编”只能降低风险,不能从机制上消灭幻觉。更可靠的办法是给它外部证据,要求引用检索结果;对代码实际运行测试;对日期、价格和 API 查官方文档;对高风险结论交给人确认。
流畅不是正确性的证据
大语言模型最擅长的就是生成流畅文本,所以不能再用“说得很专业”判断它是否可靠。越是具体的函数名、论文、命令和数字,越应该单独验证。
上下文像工作台,不是永久记忆
聊天时,模型能记得前面说过什么,是因为对话历史会作为上下文再次送入模型。它并不是在每次对话后修改参数,也不一定在下一次新会话里记得我。
系统指令
+ 用户第一轮问题
+ 助手第一轮回答
+ 用户第二轮问题
│
▼
一起组成新的输入上下文上下文窗口有长度上限。对话越来越长时,早期内容可能被截断、压缩或变得难以被模型准确利用。模型还可能注意到某个细节,却在后面生成时又偏离它。
这给我一个很实际的提示:提示词不是魔法咒语,它更像临时给模型铺工作台。任务目标、输入数据、限制条件和输出格式都放在工作台上,模型才有机会使用。信息互相矛盾或埋在很远的位置,结果就会不稳定。
真正的长期记忆需要外部系统保存,例如数据库、文件或向量检索。需要时把相关内容找出来,再放回当前上下文。模型参数、当前上下文和外部记忆是三件不同的东西。
会回答问题的模型,还不是能完成任务的 Agent
用了十天后,我开始觉得 ChatGPT 像一个被关在文本框里的大脑。它能告诉我怎样查天气,却不能真的读取我所在城市的实时天气;能写出删除文件的命令,却不会自己判断目录、执行命令并检查结果;能说“明天提醒你”,关闭页面后也不会在明天主动醒来。
一个只接收文本并生成文本的语言模型,缺少几样完成现实任务需要的东西:
| 能力 | 单独的语言模型有什么限制 |
|---|---|
| 感知当前状态 | 只能看到输入上下文,不自动知道文件、网页和系统状态 |
| 执行动作 | 输出的是文字,不能直接调用搜索、终端或业务 API |
| 保存长期状态 | 上下文有限,新会话不等于永久记忆 |
| 根据结果继续 | 一次回答结束后不会自动观察执行结果并修正计划 |
如果在模型外面加一个程序循环,情况就变了:模型先根据目标选择动作,程序执行工具,把结果返回给模型,模型再决定下一步。这个组合才开始接近 Agent。
用户目标
│
▼
语言模型决定下一步
│ 输出“调用什么工具、传什么参数”
▼
外部程序执行工具
│ 返回网页、文件、命令结果
▼
语言模型观察结果并继续
└─────────────── 循环,直到完成或停止模型仍是概率预测器,但它不再只能靠参数里的旧知识回答。搜索工具可以给它当前信息,计算器可以避免心算错误,终端可以把代码真正跑起来,数据库可以保存状态。
同时,风险也从“说错话”变成“做错事”。模型胡编一个命令,如果外层程序不检查就直接执行,后果比聊天窗口里答错一道题严重得多。工具权限、参数校验、执行超时和人工确认,会成为 Agent 设计的一部分。
我从“它真聪明”转向了“这一步由谁保证”
注册成功那天,我最直观的感受是终于用上了一个很厉害的新东西。把机制拆开后,我对它的兴趣反而更大,但评价方式变了。
看到一段好回答,我会问:这是模型参数里学到的模式,还是当前上下文提供的信息?看到一个事实,我会问:有没有外部来源能核对?看到一串推理,我会问:中间步骤真的成立,还是语言上接得顺?如果要让它执行动作,我还要问:谁限制权限,谁检查参数,失败后怎样停下来?
这些问题没有削弱 GPT 的价值。恰恰因为它能把自然语言变成一种灵活的计算接口,我们才有机会让普通人用一句话调用复杂系统。但自然语言天生有歧义,模型输出又带概率,真正可靠的应用不能只靠“它大多数时候看起来没问题”。
我下一步想做的事情也因此变得具体:不给模型假装无所不知,而是给它几件受控的工具,让它在需要时自己选择调用;把每一步动作和结果记下来;遇到写文件、发请求这类操作时要求确认。
那时我还没有写出一个像样的 Agent,但方向已经清楚了。语言模型负责理解和决策,普通程序负责连接真实世界,确定性的代码负责守住边界。把这三部分接起来,比继续收集所谓“万能提示词”更值得折腾。
继续阅读与自己动手验证
理解 Transformer 最直接的入口仍是 2017 年的论文 Attention Is All You Need。人类反馈训练可以读 Training language models to follow instructions with human feedback。
不想一开始就啃公式,也可以先做三个小实验:固定问题多问几次观察输出差异;故意询问不存在的 API 并核对文档;开一个新会话,看它是否还记得旧会话里临时约定的暗号。
这三个实验分别对应采样、幻觉和上下文记忆。它们比背一串术语更能说明 GPT 的能力边界,也为后面真正动手做 Agent 留下了问题:既然模型会犯错,外层程序应该怎样让错误停在文本里,而不是扩散到真实系统?