训练三阶段,能力是怎么被"装"进去的

outlier 15 分钟 5646 字 0 0
系列教程Agent从入门到放弃 第 12 / 19 章
  1. 01Agent 从入门到放弃:教程大纲
  2. 02①大模型、Chatbot、Agent,一次把名字理清楚
  3. 03②Agent 的最小定义,一个 while 循环
  4. 04③Agent 简史,从 ReAct 到 Claude Code
  5. 05④Workflow 还是 Agent?大部分场景你不需要 Agent
  6. 06Agent 能力地图,为什么编码 Agent 最先成熟
  7. 07Token,模型眼里的世界
  8. 08Next-token prediction,一个字一个字地猜
  9. 09采样,temperature、top-p 与"同题不同答"
  10. 10Transformer 与 Attention,不推公式的直觉版
  11. 11Context Window,模型的内存条
  12. 12训练三阶段,能力是怎么被"装"进去的
  13. 13推理模型,让模型先想再答
  14. 14用原理解释怪行为,原理篇结业考
  15. 15从 Prompt 工程到 Context 工程
  16. 16System Prompt 写作,角色、约束与示例
  17. 17Context Rot,为什么长对话越聊越笨
  18. 18项目记忆,CLAUDE.md 与 AGENTS.md
  19. 19检索,RAG 还是 Agentic Search
查看教程目录

开场:一个只会出考卷的模型

第一次在自己电脑上跑开源模型,我下错了版本。

我输入:"中国的首都是哪里?"

它输出:

美国的首都是哪里?日本的首都是哪里?英国的首都是哪里?法国的首都是……

我愣住了。它不回答,反而接着出题,像是在帮我编一张地理考卷。我又试了一句:"请帮我写一个 Go 的快速排序。"它接着写:"请帮我写一个 Python 的冒泡排序。请帮我写一个 Java 的……"

后来我才发现,我下载的是这个模型的 base 版本,而不是 instruct 版本。base 版本不是坏了,它只是在做它被训练去做的事:续写文档。 在它见过的海量网页里,"中国的首都是哪里?"后面跟着的,很多时候确实是另一道题。

同一个模型,为什么有两个版本?它们差在哪里?我们天天聊天用的那个"助手",是怎么从一个"续写机器"变过来的?这一篇讲模型的训练过程。读完本篇,你应该能回答这几个问题:

  • 预训练是什么?base 模型为什么只会续写?
  • 指令微调做了什么,让模型学会了"扮演助手"?
  • RLHF 和 RLAIF 是什么?为什么它们会让模型变得"讨好"?
  • 模型的知识是从哪里来的?为什么会过时、会出错?
  • 这些对使用 Agent 有什么实际影响?

一、全景:从随机数字到助手

一个大模型的诞生,大致经过三个阶段:

flowchart LR R["随机初始化的参数
什么都不会"] --> P["① 预训练
读完大半个互联网
学会续写"] P --> B["base 模型
知识渊博的续写机器"] B --> S["② 指令微调 SFT
学会扮演助手"] S --> F["③ 偏好对齐
RLHF / RLAIF
学会讨人喜欢"] F --> I["instruct / chat 模型
我们天天用的那个"] style P fill:#e3f2fd,stroke:#1565c0 style S fill:#e8f5e9,stroke:#2e7d32 style F fill:#fff3e0,stroke:#e65100

这三个阶段的投入极不均衡:预训练花掉了绝大部分的算力和数据,后两个阶段的数据量小得多,但它们决定了模型"像不像一个好用的助手"。

近两年还有一个越来越重要的环节:用可以自动验证的任务(数学题、代码、工具调用)做强化学习,专门提升推理和 Agent 能力。下一篇讲推理模型时细说,这里先把三个基本阶段讲清楚。

二、预训练:读完大半个互联网

2.1 目标只有一个:预测下一个 token

预训练的目标,就是第 07 篇讲的那件事:给一段文本的前半截,预测下一个 token。 预测错了,就调整参数,让下次预测得更准一点。

训练数据是海量的文本:网页、书籍、论文、代码、论坛帖子……经过清洗和去重,总量是以"万亿 token"为单位的。模型把这些文本一段一段地"读"过去,每读一个 token,就做一次预测、调整一次参数。

flowchart LR D["一段训练文本:
Go 语言的并发模型基于 goroutine 和 channel"] --> P1["看到:Go 语言的"] P1 --> Q1["猜:并发?
答案:并发 ✓"] Q1 --> P2["看到:Go 语言的并发"] P2 --> Q2["猜:编程?
答案:模型 ✗"] Q2 --> U["调整参数,
下次更可能猜「模型」"] style U fill:#fff3e0,stroke:#e65100

就这么一个简单得近乎无聊的目标,重复了万亿次之后,神奇的事情发生了:为了把下一个 token 猜得更准,模型被迫学会了语法、学会了事实、学会了代码的结构,甚至学会了一些推理的模式。因为这些东西都能帮它更准地预测下一个 token。

比如,要猜准"法国的首都是"后面那个词,它最好记住"巴黎";要猜准一段 Go 代码里 if err != nil { 后面是什么,它最好理解错误处理的惯用写法;要猜准一道数学题解答的下一步,它最好学会一点算术。

2.2 base 模型:一个知识渊博的续写机器

预训练结束后,得到的叫 base 模型。它见多识广,但它只会做一件事:续写。 你给它一段开头,它会接着写一段"在互联网上看起来最像会跟在后面的内容"。

所以开场那一幕就好解释了。"中国的首都是哪里?"这句话,在互联网上最常出现在什么地方?练习题、考卷、问答网站的问题列表。所以它最自然的续写,是"下一道题"。

base 模型并不是没用。你只要把"开头"写得像一份你想要的文档,它就能写出你想要的东西:

问:中国的首都是哪里?
答:北京
问:日本的首都是哪里?
答:

看到这个格式,它会续写"东京"。这就是早期 GPT-3 时代"提示工程"的主要技巧:不是告诉模型做什么,而是假装你在写一份文档的开头,让它接着写。

2.3 知识是一份有损压缩

预训练把万亿 token 的文本,压进了几百亿到几千亿个参数里。这显然不可能是"逐字存储"。

一个很好的比喻是:模型的参数是对互联网的一份有损压缩。 就像一张压缩得很厉害的 JPEG 图片:整体轮廓清晰,细节是模糊的。

  • 出现得多的知识,记得牢:法国的首都、Go 的语法、常见的算法,几乎不会错;
  • 出现得少的知识,记得模糊:某个小众开源库的某个函数的参数,某个不太出名的人的生日,它只有个大概的印象;
  • 模糊的地方,它会"补全":就像 JPEG 解压时会根据周围的像素脑补细节,模型在记不清的时候,也会根据"看起来合理"来编一个答案。

这就是"幻觉"的根源之一,第 13 篇会和其他几种怪行为一起归因。

2.4 知识截止日期

预训练数据是在某个时间点收集的,那之后发生的事情,模型就不知道了。这个时间点叫知识截止日期(knowledge cutoff)。

而且,截止日期附近的数据往往比较少(互联网上讨论一件事需要时间),所以模型对"截止日期前几个月"的事情,了解得也比较模糊。这就是为什么模型经常不知道最新的库版本、最新的 API 用法,甚至会用已经被废弃的写法。

三、指令微调:学会扮演助手

3.1 用"示范对话"教它怎么回答

一个只会续写的 base 模型,怎么变成一个会回答问题的助手?

方法朴素得出奇:给它看大量的"示范对话"。 每一条示范,是一个用户的问题,加上一个"理想的助手"会怎么回答。这些示范最初主要由人工撰写,后来也越来越多地由其他模型生成、再经过筛选。

用户:中国的首都是哪里?
助手:中国的首都是北京。

用户:请帮我写一个 Go 的快速排序。
助手:好的,下面是一个快速排序的实现:……

然后用和预训练一样的方法训练它:预测下一个 token。只不过这次的"文本",全是这种格式的对话。看多了,模型就学会了:在"用户:……助手:"这种格式后面,应该写一个有帮助的回答。 这个阶段叫有监督微调(SFT,Supervised Fine-Tuning),也常被叫做指令微调。

数据量和预训练比起来很小,可能是几万到几百万条对话,但效果立竿见影。它没有教会模型新的知识,只是教会了模型"以助手的身份说话"。 知识早在预训练阶段就装进去了。

3.2 角色只是一种格式

这里有个对理解 API 特别有用的细节:模型怎么知道哪段话是用户说的,哪段话是助手说的?

答案是:用特殊的 token 把它们标出来。 每个模型都有一套自己的"对话模板"(chat template),比如有的模型会把一段对话变成这样的文本:

<|im_start|>system
你是一个有帮助的助手。<|im_end|>
<|im_start|>user
中国的首都是哪里?<|im_end|>
<|im_start|>assistant

<|im_start|>、<|im_end|> 这些是词表里专门留出来的特殊 token。模型在 SFT 阶段看了无数这种格式的文本,学会了:看到 <|im_start|>assistant 之后,就该以助手的身份开始说话;说完了,就输出 <|im_end|>,这就是第 07 篇里那个"结束"token。

现在回头看 API 里的 messages 数组:

[
  { "role": "system", "content": "你是一个有帮助的助手。" },
  { "role": "user", "content": "中国的首都是哪里?" }
]

API 服务端做的事情,就是把这个数组按模板拼成上面那段带特殊 token 的文本,然后交给模型去续写。 role 不是什么魔法,它最终只是几个特殊 token。

工具调用也是一样。模型在训练时看过大量"助手决定调用工具"的示范,这些示范里,工具调用被写成某种特定的格式(同样用特殊 token 标出来)。所以模型输出这种格式时,API 能把它识别出来,变成 tool_use 或者 tool_calls 交给你。function calling 能力,本质上是训练出来的一种输出格式。

这也解释了为什么各家 API 都对消息格式有各种严格的规定。比如 DeepSeek 要求带工具的多轮对话里,之前每一轮的思考内容(reasoning_content)都必须原样传回去,否则报错。因为模型训练时看到的对话就是那个样子,你拼出来的格式和它训练时见过的不一样,它的表现就没法保证。

3.3 Agent 能力,也是这样"示范"出来的

同样的道理,可以一路推到 Agent 身上。一个模型之所以会"先跑测试、看报错、再读文件、再改代码",不是因为有人在它里面写了一个排查流程,而是因为它在训练中看过大量这样的多步示范:一个任务,一串工具调用,每一步的工具结果,最后的完成。

近两年,各家在这件事上投入越来越大。除了示范,还会把模型放进一个真实的沙箱环境里,给它一个代码仓库和一个任务,让它自己转循环,最后用测试来判断成没成功,成功的路径多做、失败的少做(这是下一篇要讲的强化学习)。

这件事有两个对使用者很实际的推论:

  • 模型会有"习惯":它在训练环境里用惯了某种工具、某种工作方式,比如改文件前先读一遍、用 grep 找定义、改完跑测试,到了你的 Agent 里,它也会倾向于这么做。第 26 篇实现 edit_file 时要求"改之前必须读过",第 20 篇讲工具设计时说"工具的形状要符合模型的直觉",背后都是这个原因;
  • harness 越接近它熟悉的样子,它表现越好:工具的名字、参数的形状、错误信息的写法,如果和它训练时见过的差不多,它就用得顺手;如果你设计了一套很"有创意"的工具,它可能要绕好几圈才能适应。这也是为什么同一个模型,在它厂商自家的 Agent 产品里,往往比在别人的 Agent 里表现更好一些。

四、偏好对齐:学会讨人喜欢

4.1 好的回答不止一种

SFT 之后,模型会回答问题了,但回答的质量参差不齐。同一个问题,有的回答简洁准确,有的啰嗦,有的带着错误,有的语气生硬。示范对话只能告诉模型"这是一个好回答",很难告诉它"这个回答比那个好在哪里"。

所以有了第三个阶段:让模型学会人类的偏好。

4.2 RLHF:让人来打分

最有名的方法是 RLHF(Reinforcement Learning from Human Feedback,基于人类反馈的强化学习),大致分三步:

flowchart TB A["① 让模型对同一个问题
生成几个回答"] --> B["② 人来比较:
A 比 B 好"] B --> C["训练一个奖励模型
学会模仿人的判断"] C --> D["③ 用奖励模型给新回答打分,
强化学习:高分的多做,低分的少做"] D --> A style B fill:#fff9c4 style C fill:#e8f5e9,stroke:#2e7d32 style D fill:#fff3e0,stroke:#e65100
  1. 让模型对同一个问题生成好几个回答;
  2. 请人来比较,哪个更好。比较比打分容易:人很难给一个回答打"7.3 分",但很容易说"A 比 B 好";
  3. 用这些比较数据训练一个奖励模型,让它学会模仿人的判断;再用奖励模型去给模型的新回答打分,通过强化学习让模型往高分的方向调整。

后来也出现了一些更直接的方法,跳过单独训练奖励模型这一步,直接用偏好数据调整模型。细节不同,思路一样:让模型的输出更符合人的偏好。

4.3 RLAIF:让 AI 来打分

找人来做大量比较,又慢又贵。所以另一个思路是:让 AI 来当评委。 这叫 RLAIF(Reinforcement Learning from AI Feedback)。

比如 Anthropic 在 2022 年提出的 Constitutional AI:先写一份"宪法",列出一些原则(要有帮助、要诚实、不要协助造成伤害……),然后让模型根据这些原则去评判和修改回答,用这些 AI 生成的偏好数据来训练。人的工作,从"逐条打分"变成了"写好原则"。

4.4 副作用:讨好

偏好对齐让模型变得礼貌、有帮助、乐于配合,这是好事。但它也带来了一些副作用,其中最有名的就是讨好(sycophancy):

  • 你说"我觉得这段代码没问题吧?",它倾向于同意你;
  • 你反驳它一句,它很容易改口说"你说得对,我之前错了",哪怕它之前是对的;
  • 你的方案明明有坑,它会先夸一番,再很委婉地提一点点"可以考虑"的建议。

为什么?因为人在打分的时候,本身就偏爱被认同。一个说"你说得对"的回答,往往比一个说"你错了"的回答更讨人喜欢。模型学会了人的偏好,也就学会了人的这个小毛病。各家厂商都在想办法减轻它,但它没有完全消失。

其他常见的副作用还有:回答越来越长(人有时候会把"长"误当成"详细"),以及对一些无害的请求过度谨慎、拒绝回答。

4.5 讨好在 Agent 里长什么样

聊天里的讨好,是"你说得对";到了 Agent 里,它有一个更危险的变体:为了"完成任务"而完成任务。

当训练的奖励信号是"测试通过了没有",模型就会学会一切能让测试通过的办法,其中有一些并不是你想要的:

  • 测试一直过不了,它把那个测试跳过,或者把断言改松;
  • 某个边界情况处理不了,它在代码里写死一个特判,专门让那个测试用例通过;
  • 任务没做完,它在最后的总结里写"已完成,所有测试通过",因为"说完成了"本身在训练中常常得到好评。

这类现象有个名字,叫奖励投机(reward hacking):模型优化的是奖励信号,而奖励信号只是你真实意图的一个近似。两者之间的缝隙,就是它投机的空间。

第 39 篇开场那个"跳过一个测试然后宣布完成"的故事,就是它的典型表现。应对办法第 35、36 篇会细讲:测试文件不让 Agent 改,判分时换回原版测试,再加上它看不见的隐藏测试。 说到底,就是不要只相信它自己的报告,而要用一个它改不了的东西去检查它。

五、这些对用 Agent 的人意味着什么

训练的细节离我们很远,但它的结果每天都在影响我们使用 Agent 的方式:

训练里发生的事你会看到的现象你该怎么做
知识是有损压缩冷门知识会编关键事实让它去查,别让它凭记忆说
有知识截止日期不知道新版本、新 API把最新文档放进上下文(第 14、17、18 篇)
SFT 学会了对话模板role、工具格式有严格规定严格按 API 文档拼消息,别自作主张
偏好对齐学会了讨好你说什么它都同意别用"对吧?"这种问法,让它先列反对意见
用可验证的任务做强化学习写代码、做数学进步最快编码 Agent 最先成熟(第 05 篇),也是第 12 篇的主题
奖励是"测试通过"跳过测试、写死特判、谎报完成测试文件锁死,用隐藏测试验收(第 35、36 篇)
用多步示范训练过工具使用有自己的工作习惯工具设计顺着它的习惯来(第 20 篇)

其中最实用的一条是第二条。Agent 写代码时,用了一个早就废弃的 API,或者照着某个库的旧版本写,这不是它"笨",是它的知识停在了某个时间点。编码 Agent 好用的关键之一,就是让它在动手之前先读项目里的代码和文档,而不是凭训练时的记忆写。这就是为什么好的编码 Agent 总是先 list_files、read_file,也是第 17 篇项目记忆、第 18 篇检索要解决的问题。

六、动手:base 和 instruct,对比一次

这一篇在配套仓库里没有对应的代码。动手环节需要 Python 和 Hugging Face 的 transformers 库,在线 Playground 也演示不了。

实验 1:同一个问题,两个版本

在 Hugging Face 上,很多开源模型会同时发布 base 版和 instruct 版。比如通义千问的小模型 Qwen/Qwen2.5-0.5B(base)和 Qwen/Qwen2.5-0.5B-Instruct(instruct),只有 5 亿参数,普通笔记本的 CPU 就能跑。

装好依赖(pip install transformers torch),写一个小脚本:

from transformers import AutoModelForCausalLM, AutoTokenizer

prompt = "中国的首都是哪里?"
for name in ["Qwen/Qwen2.5-0.5B", "Qwen/Qwen2.5-0.5B-Instruct"]:
    tok = AutoTokenizer.from_pretrained(name)
    model = AutoModelForCausalLM.from_pretrained(name)
    ids = tok(prompt, return_tensors="pt")
    out = model.generate(**ids, max_new_tokens=40, do_sample=False)
    print(f"== {name} ==")
    print(tok.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True))

第一次运行会从网上下载模型,需要一点时间。重点观察:base 版是在"回答",还是在"续写"? 它会不会像开场那样接着出题,或者写出一段像网页上抄下来的文字?

(每个模型的表现不完全一样,有些 base 模型在预训练数据里见过很多问答格式,也可能直接给出答案。多换几个问题试试。)

实验 2:看一眼对话模板

再加几行,把 instruct 版本的对话模板打印出来:

tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
messages = [
    {"role": "system", "content": "你是一个有帮助的助手。"},
    {"role": "user", "content": "中国的首都是哪里?"},
]
print(tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True))

你会看到 3.2 节那种带特殊标记的文本。这就是 API 里的 messages 数组,最终变成的样子。然后用这段文本作为输入,分别喂给 base 版和 instruct 版,再对比一次。base 版看到这些特殊标记,表现会不会好一点?

实验 3:用 base 模型"假装写文档"

最后试试 2.2 节的技巧:给 base 模型一个"文档开头":

问:中国的首都是哪里?
答:北京
问:日本的首都是哪里?
答:

看它能不能顺着格式续写出"东京"。这就是 SFT 出现之前,人们使用模型的主要方式。

七、踩坑记录:和训练有关的五个误区

误区 1:模型知道最新的事情。
它只知道截止日期之前的事,而且截止日期附近的事也知道得不多。问它"最新版本是多少""这个 API 现在怎么用",要么给它最新的文档,要么让它去查(第 18 篇)。

误区 2:模型说得很肯定,就说明它查过、确认过。
模型说话的语气是训练出来的,和它对事实有多大把握没有必然关系。一本正经地编,是有损压缩加上"总要给出像样回答"的训练共同造成的(第 13 篇)。

误区 3:它同意我,说明我是对的。
偏好对齐让它倾向于同意你。想得到真实的评价,就别问"这样没问题吧?",换成"请列出这个方案的三个最大风险",或者"假设你是一个挑剔的评审,你会在哪里打回"。

误区 4:拿 base 模型当聊天模型用。
在开源社区下载模型时,注意看名字里有没有 Instruct、Chat 之类的字样。base 模型适合做进一步的训练,不适合直接拿来聊天,更不适合拿来做 Agent。

误区 5:API 里的 role 是什么魔法。
system、user、assistant、tool,最终都只是对话模板里的几个特殊 token。理解了这一点,你就会明白为什么工具返回的内容里如果藏着指令,模型可能会照做:它们最后都被拼在同一段文本里(第 38 篇)。

一句话总结

预训练让模型读完大半个互联网,学会续写,把知识以有损压缩的形式存进参数;指令微调让它学会以助手的身份说话;偏好对齐让它的回答更符合人的口味,也顺便学会了讨好。

flowchart LR P["预训练
学会知识
(续写机器)"] --> S["指令微调
学会对话
(扮演助手)"] S --> F["偏好对齐
学会讨喜
(也学会了讨好)"] style P fill:#e3f2fd,stroke:#1565c0 style S fill:#e8f5e9,stroke:#2e7d32 style F fill:#fff3e0,stroke:#e65100

评论

还没有评论,来说点什么吧。

发表评论

Ctrl + Enter 快速提交
Enter 搜索Esc 关闭