
简介哈尔滨工业大学自然语言处理研究所于2023年3月6日发布的ChatGPT调研报告全文94页面向自然语言处理学习者、算法与产品从业者以及关注大模型技术演进的研究人员帮助读者在较短时间内建立对ChatGPT技术脉络、应用边界与产业影响的系统认知。整份资料以单个PDF文件打包压缩包约3.54MB篇章按主题顺序编排便于从原理到落地逐层阅读。内容共分七章先梳理自然语言处理发展史与ChatGPT技术脉络继而详解Transformer三类预训练语言模型、提示学习与指令精调、思维链及基于人类反馈的强化学习等核心算法再延伸到大模型并行训练、部署与模型压缩、相关数据集等工程议题并讨论客服问答、智能搜索、自动化办公等应用场景及数据隐私、算法偏见、就业冲击等风险挑战。已有1512人学习适合作为入门梳理与内部研讨的参考材料。1. 知识的三次搬家ChatGPT 凭什么用一句话就能调用知识很多人把 ChatGPT 的突破归因于“参数大”但更反直觉的一点是它的知识表示精度其实下降了。早期知识存在关系型数据库里字段严格、精确度极高代价是人必须学会 SQL 这类机器语言才能取用后来知识散落在互联网的文本、图片、视频里人用关键词去搜索精确度掉了一档调用自然度升了一档到 2018 年之后知识被打包进模型参数精确度再掉一档——模型会说错、会编——但调用方式变成了人类最自然的那一种直接说人话。这条“精确度换自然度”的交换曲线才是理解 ChatGPT 底层设计动机的起点也是后面所有架构选择、对齐训练、评估指标的根。搞懂它你才知道哪些问题是这套范式天生的短板而不是某个模型的 bug。2. 架构分岔口为什么对话场景最终只剩 decoder-only预训练语言模型在 2018 年前后分成三条技术路线三条路线的差别不在参数量而在每个 token 能看到多少上下文。这个差别决定了它们后来各自能长出什么能力也解释了为什么最后是 decoder-only 这一支跑出了对话式通用系统。2.1 三种骨架与它们各自擅长的任务编码器架构用双向注意力每个位置同时看到左右两侧天生适合做“理解”——文本分类、序列标注、语义匹配这类把一句话压成一个判断的任务它性价比极高。解码器架构用单向注意力只能看左边天生适合做“生成”——给定前文预测下一个 token。编解码架构则是前者的编码加后者的解码中间用交叉注意力把源端信息喂给目标端机器翻译、摘要这类“输入一段输出一段”的任务是它的主场。三者的能力边界由训练目标锁定。双向模型在预训练时做的是“挖空填词”它学到的是上下文约束下的词分布没有“继续往下写”的训练信号所以它很难自然地生成连贯长文单向模型做的是“下一个词预测”它必须把整段历史压进隐状态才能续写于是被迫学会了长程依赖建模。对话的本质是“给定历史生成下一句”这正好是单向目标的形状。报告里提到的参数增长曲线——从 1.2 亿到 15 亿再到 1750 亿——是沿着 decoder-only 这条路走的规模只是放大器方向早就定了。2.2 因果掩码一个矩阵决定模型能看见什么单向性在实现上不是靠循环而是靠一个掩码矩阵。注意力计算是softmax(QK^T/√d)V如果不加干预每个位置都会和所有位置算相似度那就退化成双向了。做法是把上三角区域填成负无穷softmax 之后这些位置的权重精确等于 0。import numpy as np def causal_mask(n): 构造 n×n 的因果掩码位置 i 只能看到 j i upper np.triu(np.ones((n, n)), k1) # 严格上三角为 1 return np.where(upper 1, -np.inf, 0.0) # 上三角置 -inf def masked_attention_weights(scores, mask): scores: (batch, heads, n, n)mask 广播到同样形状 scores scores mask # 未来位置变成 -inf scores scores - scores.max(axis-1, keepdimsTrue) # 数值稳定 e np.exp(scores) return e / e.sum(axis-1, keepdimsTrue) # 未来位置权重精确为 0n是序列长度k1保证对角线保留——第 i 个位置可以看自己这在训练时是必须的因为要预测的正是第 i 个位置的下一词。减最大值那一步是血泪经验exp(-inf)本身是 0 没问题但混合了大数值后容易出现inf - inf得到 NaN先减最大值再取指数可以规避。这个矩阵带来的一个副作用值得注意它把训练变成了“一次前向、n 个监督信号”。一段长度 n 的文本双向模型只能构造一个填词目标单向模型能构造 n 个下一词预测目标每个位置都是一个训练样本。这就是为什么 decoder-only 在同样语料上能吃进更多梯度信号也是它能靠海量网络文本堆出通用能力的工程前提。2.3 从 1.2 亿到 1750 亿规模放大的是同一种能力参数涨上去之后出现了一个不在原计划里的现象语境学习。模型在 1750 亿参数规模上只要在输入里给一个任务描述、再给一两个示例不做任何权重更新就能完成这个任务——比如输入“这句话的情感是 __”它能直接补出“褒义”。这在中小模型上几乎观察不到属于典型的涌现行为能力不是线性增长的跨过某个规模阈值才突然出现。但这套机制也有明确的天花板。报告里对 GPT-3 的评估结论很直接它并没有真正解决深度学习的鲁棒性差、可解释性弱、推理能力缺失在深层语义理解和生成上离人类认知还很远。换句话说decoder-only 加规模解决的是“知识装得下、话说得顺”没解决“说得对、想得清”。这两件事分别要靠后面两章的两套机制去补——一套管“听话”一套管“说人话且别乱说”。提示判断一个模型是不是 decoder-only最省事的办法不是看论文而是看它的注意力掩码是不是下三角。很多号称“自回归”的实现在推理时开了双向上下文窗口训练时却是单向的这种不一致会在长文本任务上暴露出来。3. 提示与指令同一套参数如何被“唤醒”成不同任务参数冻结、行为可变这件事在传统监督学习里是不可想象的。要让一个模型同时会翻译、会写代码、会做摘要常规做法是给每个任务训一个模型或者至少训一个多任务头的模型。而提示学习和指令微调走的是另一条路不改一个权重只改输入的形状。3.1 语境学习不改权重也能切换任务语境学习的机制可以这么理解预训练阶段模型见过海量文本其中天然包含了大量“任务描述 输入输出对”的结构——问答页、教程、代码注释、论坛问答都是这种形状。模型在预测下一个 token 时被迫学会了“识别当前文本处于哪种模式并延续这个模式”。所以当你给出一段格式正确的示例模型实际上是在做模式续写而不是在执行一个被显式编程的任务。这解释了它的两个特征。第一格式比语义更重要示例的分隔符、标签写法、顺序一致性对结果的影响往往大于示例内容本身这也是为什么同一个任务换一种模板写法效果能差出一大截。第二示例数量和顺序敏感给三个示例通常好过给一个但如果第三个示例是噪声反而会拉低整体表现——模型学到的是“这段上下文里的统计规律”它会忠实地延续你给的规律包括你无意中引入的偏差。从工程角度看语境学习把“任务适配”从训练时搬到了推理时代价是每次调用都要携带示例token 成本上升且行为不够稳定。要稳定就得动权重这就轮到指令微调。3.2 指令微调把“续写”掰成“应答”预训练模型的原始行为是“续写”你给它一个问题它可能补出另一个问题因为在语料里问题后面跟着问题的情况并不少见。指令微调要做的是把这个分布往“问题后面跟答案”的方向掰。做法是用大量“指令 输入 期望输出”的三元组做监督微调数据规模不需要很大但覆盖面要广、指令表述要多样。实现上有个容易翻车的细节loss 只能算在答案部分指令和输入部分要屏蔽掉。否则模型会分心去学“怎么把指令复述一遍”而不是“怎么回答”。IGNORE_INDEX -100 def build_labels(prompt_ids, answer_ids): prompt 部分不参与损失只监督 answer 部分 return [IGNORE_INDEX] * len(prompt_ids) list(answer_ids) def shift_for_causal_lm(labels): 因果语言模型位置 i 的输出预测位置 i1 的标签 return labels[1:], labels[:-1] # (labels, inputs) # 交叉熵默认会忽略 IGNORE_INDEX只对答案 token 求平均IGNORE_INDEX是约定俗成的哨兵值交叉熵损失函数遇到它会跳过该位置不计入分母。shift_for_causal_lm那一步是自回归训练的固定动作模型在第 i 个位置输出的分布对应的是第 i1 个 token 的标签忘了 shift 会得到“模型在学复读自己”的诡异曲线——训练 loss 一路下降推理时却只会重复输入。这个坑我在早期项目里踩过一次loss 曲线漂亮得像教科书上线效果惨不忍睹。指令微调的另一个作用是激发泛化。报告里给了一个很有说服力的例子指令集里 96% 以上是英语其余只覆盖二十来种语言但拿一个指令集中完全没出现过的语种做翻译模型仍然能给出正确结果。这在“一任务一模型”的微调范式下几乎不可能——小模型没见过这个语种的任务数据就是不会。大模型因为底座里已经装了跨语言的对应关系指令微调只是把“翻译”这个行为模式激活了。3.3 思维链为什么代码语料能喂出逐步推理思维链是这套体系里最不像“训练”出来的能力。它的做法极其简单在示例里把推理过程一步步写出来模型就会模仿着一步步输出而不是直接给结论。真正值得追问的是这种“分步”能力是从哪来的。报告给出的解释是代码预训练。ChatGPT 的底座在文本预训练之后又用约 159G 的代码语料做了继续预训练。代码有一个文本不具备的性质它天然是分步骤、分模块、按顺序执行的结构一行做完一件事函数调用有明确的输入输出契约。模型在海量代码上学“下一行该写什么”实际上是在学“下一步该做什么”。当这种能力迁移到自然语言任务上就表现为把一个大问题拆成若干子步骤再逐步求解。这个迁移带来了一个可观测的异常模型表现不再随规模平滑增长而是在某个点上出现激增打破了原先的缩放规律。原因不难猜——分步推理把一个需要“一步到位”的高难度预测拆成了若干“每步都不算太难”的预测单步正确率的乘积自然高于整体一次命中的概率。这也是为什么思维链对数学、多跳问答这类任务收益最大对简单的信息抽取反而几乎没有增益甚至会因为啰嗦而拖慢响应。但思维链不是万能药。它生成的推理步骤本身也可能是错的而且错得很流畅——模型会一本正经地推出一条看起来严密的错误链路。这一点在后面对齐阶段也没能彻底解决只是被部分缓解。4. RLHF奖励模型学的不是“对错”是排序偏好指令微调解决“听话”但解决不了“说得让人满意”。同样一个正确回答可以写得像说明书也可以写得像人话同样一个不该回答的问题模型可能老老实实答了。这类目标没法用交叉熵表达因为不存在唯一正确的标签。基于人类反馈的强化学习就是为这类“没有标准答案但有优劣之分”的目标设计的。4.1 三段式流水线SFT → 奖励模型 → PPO整条流水线分三步每步的产物都是下一步的输入。第一步是有监督微调用人工写的高质量回答把底座掰成会应答的形状得到一个初始策略。第二步是训练奖励模型让标注人员对同一个问题的多个回答做排序用这些排序数据训一个打分器它的任务是“看一眼问题和回答给一个分”。第三步是用强化学习优化策略让策略生成的回答在奖励模型那里拿高分同时不能偏离初始策略太远。这套思路不是 2022 年才有的。早在 2008 年就有人提出在强化学习框架里引入人类评价作为额外奖励信号动机很朴素纯环境奖励收敛慢、方向容易跑偏人工评价可以加快收敛、压低训练成本、修正优化方向。后来有人把人工反馈信号接进演员-评论家算法去调节优势函数再往后才有人把它和语言模型结合。这条技术线沉默了十年直到模型规模足够大、生成质量足够高人工排序才变得有意义——你没法对一个只会输出乱码的模型做“哪个回答更好”的排序。4.2 排序损失与 Bradley-Terry 假设奖励模型的训练目标不是回归一个绝对分数而是让“被选中的回答”分数高于“被拒绝的回答”。这背后是一个成对比较的概率假设两个回答的分数差经过 sigmoid 之后等于人类选择前者的概率。import torch import torch.nn.functional as F def pairwise_rank_loss(r_chosen, r_rejected, beta1.0): r_chosen / r_rejected: 奖励模型对两个回答的打分形状 (batch,) beta: 温度系数控制分数差的敏感度 diff beta * (r_chosen - r_rejected) # -log(sigmoid(diff)) softplus(-diff)数值上更稳 return F.softplus(-diff).mean()用softplus而不是直接写-logsigmoid是因为后者在diff为很大的负数时会下溢到-inf梯度直接炸掉。beta控制的是“分数差要多明显才算赢”调大它模型会更激进地拉开差距容易过拟合到标注噪声调小它区分度不够强化学习阶段拿到的奖励信号太弱。实践中这个值要根据标注一致性来定标注人员之间分歧大的任务beta就该小一些。这里有个容易被忽略的点奖励模型学的是标注人员的偏好分布不是客观正确性。标注人员倾向于给更长的回答打高分因为长回答看起来更全面、更用心。这个偏差会被奖励模型原样学走再在强化学习阶段被放大最终表现为模型普遍啰嗦、爱重复、喜欢加免责声明。这不是模型“变笨了”而是优化目标就是这么定的。4.3 KL 惩罚为什么不能让策略跑太远强化学习阶段的目标函数里除了奖励还有一个 KL 散度项用来约束当前策略不要偏离初始策略太远。原因很实际奖励模型只是人类偏好的一个粗糙近似它有盲区、有漏洞。如果放任策略去最大化奖励它很快会找到奖励模型的漏洞——生成一堆在打分器看来得分极高、在人看来一塌糊涂的文本。这在强化学习里叫奖励劫持是必然会发生的不是概率事件。KL 项的作用相当于给策略拴了根绳你可以往高分方向走但每一步都要付出“偏离原模型多远”的代价。系数调大模型保守、稳定但学不到多少新东西系数调小模型敢创新但容易跑飞、语言质量崩坏。这个权衡没有普适最优解只能针对任务调。RLHF 之后还有一条延伸路线值得关注把排序环节的人工标注换成模型标注。做法是先训一个模型学习人类对“无害性”的偏好打分模式再让它代替人对结果排序。这样能大幅降低标注成本、加快迭代速度代价是把人类的偏见换成了模型的偏见——而这个模型本身的偏见又来自它的人类训练数据。绕了一圈问题只是被搬了个位置没有被消掉。理解这一点很重要对齐不是一个可以“完成”的状态而是一个持续校准的过程。5. 排查清单显存、延迟和效果对不上时该看哪一层大模型的问题很少是单点的。同一个“效果不对”可能出在数据、训练、压缩、部署四个层面的任何一层。这一章把工程侧的账本和最常见的几类翻车场景拆开讲方便对照排查。5.1 并行与压缩的账本单卡装不下模型是常态所以训练侧有四类并行。数据并行最简单每张卡放一份完整模型喂不同数据梯度做全归约。它的瓶颈是模型必须能塞进单卡参数量一大就失效。流水并行把模型按层切成几段放到不同卡上前一段算完把激活值传给后一段代价是流水线气泡——卡与卡之间要等。张量并行把单层内部的矩阵乘切块分到多卡通信量最大但延迟低适合放在同一个节点内。专家并行则是把前馈层拆成多个专家每个 token 只路由到其中少数几个参数量能涨但计算量不涨。推理侧更关心的是压缩。量化把参数精度从 32 位浮点降到 16 位甚至 8 位整数显存直接减半甚至更多代价是精度损失通常需要校准集来补偿。剪枝删掉不重要的权重粒度从单个权重到整行整列再到整个通道粒度越粗越容易真正加速粒度越细越难在通用硬件上兑现收益。蒸馏让一个小模型去学大模型的输出分布学的是“软标签”而不只是答案适合把能力搬到端侧。稀疏化则是把大量冗余连接置零保留最重要的结构。显存估算有个粗略公式先用它判断方案可行性比跑一遍再 OOM 划算得多def train_mem_gb(n_params, bytes_per_param2, use_adamTrue): n_params: 参数量个 bytes_per_param: 权重精度fp16 为 2fp32 为 4 返回训练所需显存的粗略下限GB weights n_params * bytes_per_param grads n_params * bytes_per_param # 梯度通常与权重同精度 if use_adam: # fp32 主权重 一阶动量 二阶动量常按 12 字节/参数估 optim n_params * 12 else: optim 0 total weights grads optim return total / (1024 ** 3) print(train_mem_gb(175e9)) # 约 2.9 TB 量级单机不可能必须切分这个数字只算了权重、梯度和优化器状态还没算激活值。激活值随批大小和序列长度线性增长长上下文场景下它经常才是真正的显存杀手。所以我一般会把这个结果乘个 1.5 到 2 的系数再拿去和集群容量对账。反过来如果按这个公式算出来单卡能装下但实际跑起来 OOM那问题几乎一定在激活值或碎片上而不是参数本身。5.2 部署侧的三条常见误判第一条是关于缓存的。自回归解码每生成一个 token 都要重算前面所有位置的注意力为了避免重复计算工程上会把历史的键值缓存下来。这个缓存的大小与批大小、序列长度、层数、隐藏维度都成正比。很多人按“参数量除以精度”估算显存结果部署上去发现差了一大截差额基本都在这里。序列越长、并发越高这块开销越不能忽略。第二条是关于批处理的。把多个请求拼成一批能显著提升吞吐但会让首字延迟变差因为短请求要等长请求算完。线上系统通常要做连续批处理请求随到随进、算完即出而不是攒够一批再发。这个细节直接决定用户体验但在离线评测里完全看不出来。第三条是关于精度的。量化后的模型在通用对话上几乎无损但在数值推理、长链计算、代码生成这类任务上退化明显。判断方法不是看平均分而是分任务看如果只有某一类任务掉了大概率是量化误差在某些层被放大换个量化策略或对敏感层保留高精度就能救回来。5.3 五条踩坑记录现象一同一个问题换个说法答案从错变对。原因是对输入措辞敏感模型在两种表述下激活了不同的模式而采样又引入了随机性。解决办法不是反复重试碰运气而是固定一组改写做稳定性评测把“换个说法就翻车”的比例当成一个正式指标来跟踪高于阈值就说明这个场景还不适合直接上线。现象二回答语句通顺、条理清晰但事实是错的。原因是模型的知识以参数形式存储没有可追溯的证据链置信度和正确性不挂钩。解决办法是在生成之后加一层校验或者直接改成检索增强把事实性内容交给外部语料模型只负责组织和表达。什么时候必须上检索判断标准很简单——错了会不会造成实际损失。现象三专业术语翻译质量明显低于通用句子。原因是预训练语料以通用文本为主专业领域数据占比极低而且非英语语种在语料中的比例更小。解决办法是补领域数据做继续预训练或者用术语表做约束解码。指望靠提示词解决专业术语问题效果有限而且不稳定。现象四回答普遍偏长、爱重复、结尾喜欢加一句自我说明。原因是标注人员偏好更长的回答奖励模型学走了这个偏好强化学习阶段又把它放大了。解决办法是在奖励模型阶段就引入长度惩罚或长度归一化从源头校正而不是在解码阶段硬截断。现象五离线评测分数很好上线后延迟高、并发上不去。原因是离线评测通常是单条串行、短输入短输出而线上是长上下文、高并发、流式输出。解决办法是把压测条件对齐到真实分布重点看首字延迟和长尾延迟而不是平均吞吐。6. 取舍把知识放进参数还是放进检索这套范式最根本的取舍其实在第 1 章那条曲线上就已经埋下了。知识放进参数调用自然、泛化强但更新成本极高一次知识变更意味着重新训练或至少重新微调而且无法给出证据。知识放进检索库更新即时、可溯源、可审计但调用方式退回到关键词或向量匹配模型只负责组织语言理解和推理的深度受限于召回的片段。维度参数化知识检索式知识更新成本高需重训或微调低改库即可调用自然度高直接说人话中依赖召回质量可溯源弱无证据链强可给出来源时效性受预训练数据截止时间限制实时长尾覆盖靠规模稀疏知识易错靠索引覆盖可控实际系统里很少有纯粹的二选一。更常见的做法是分层稳定的常识和语言能力放参数里易变的、需要担责的事实放检索里模型做路由和整合。判断边界的一条经验是——如果这个知识点一年内会变或者错了要有人负责就别放进参数。另一组取舍在训练侧。通用大模型加提示适合任务边界模糊、数据稀缺、需要跨任务泛化的场景微调小模型适合任务定义清晰、有充足标注数据、对延迟和成本敏感的场景尤其是序列标注这类不需要生成能力的任务硬上大模型是纯浪费。报告中提到的一个判断我很认同在机器阅读理解这类“答案已经在上下文里”的任务上大模型的额外知识不但没用反而可能干扰它对上下文的忠实度。最后一组是成本与能力的取舍。千亿参数模型的训练成本、部署难度、单次调用开销和延迟决定了它不可能在所有场景里都划算。量化、剪枝、蒸馏、稀疏化这四条压缩路线本质上都是在用一点精度换大量的成本选哪条取决于你的瓶颈是显存、是延迟还是吞吐。我自己的习惯是任何一个要上线的生成式功能先做一个“最小对照实验”——同一批输入分别用大模型加提示、小模型微调、检索加模板三条路各跑一遍把准确率、延迟、单次成本三个数字并排放。做完这张表方案基本就定了剩下的争论都是情绪。这个习惯是从一次翻车里学来的当时凭直觉选了大模型上线三个月后成本超预算四倍而任务本身其实一个微调过的小模型就能做到九成效果。从那以后我每次做技术选型都强制走一遍这张对照表哪怕结论看起来显而易见。希望帮到你。本文还有配套的精品资源点击获取