《大模型原理通俗讲》系列 09|微调与对齐:从“会接龙”到“会聊天”
09|微调与对齐:从“会接龙”到“会聊天”
《大模型原理通俗讲》系列第 9 篇,共 16 篇。不需要数学基础,会用大模型聊天就能读。上一篇讲了预训练:模型怎么靠海量“猜下一个词”练出一身本事。这一篇回答:练完了,为什么它还不能直接当助手用?
上一篇的最后,我们说,预训练得到的叫基础模型,它擅长续写。
一个新问题就来了:你平时用的聊天产品,你问它问题,它会回答。可一个只会“续写”的模型,你问它问题,会发生什么?
我们的迷你模型(下面会有代码)给了一个很形象的答案:
你输入: 问 : 法国 的 首都 是 哪里 ?
基础模型:问 : 日本 的 首都 是 哪里 ? 问 : 意大利 的 首都 是 ……
它没有回答你,而是接着写出了更多的问题。因为在它读过的文字里,一个问题后面,常常跟着另一个问题(比如题库、问答列表)。对它来说,“续写”就是任务,“回答”不是。
读完这一篇,你能回答三个问题:
- 聊天助手和基础模型,差在哪里?
- “微调”教的是什么?
- “对齐”是在对齐什么?
一、从“会接龙”到“会聊天”:三个阶段
先给你一个比喻:一个读了万卷书的毕业生。
- 预训练 = 读书。 他博览群书,见多识广,但还不懂得怎么在工作里跟人打交道:别人问他问题,他可能滔滔不绝地讲起相关的另一个话题。
- 微调 = 岗前培训。 带他看几十个“标准答复”的例子:客户这样问,应该这样答,答完就停。他的“知识”没怎么变,但他知道怎么回答了。
- 对齐 = 带教老师的反馈。 同一个问题,他有时答得好,有时答得啰嗦,有时答得生硬。老师对比着告诉他:这样更好,那样不太好。慢慢地,他的回答越来越让人满意。
整个过程画出来,是这样:

注意,三个阶段调整的,是同一批数字(参数)。不是三个模型,而是同一个模型,一步步被调成了不同的“性格”。
二、先看清:“聊天”到底是什么
在讲微调之前,要先清楚一件事:你在聊天窗口里看到的“一问一答”,在模型眼里是什么?

答案是:一段文字。 产品会把整个对话,拼成一段带有“角色标记”的文字(这些标记怎么写,每个模型各不相同,图里只是示意):
- 最前面,可能有一行系统提示词,告诉它扮演什么角色;
- 然后是“用户”说的话;
- 最后留一个“助手”的位置,让模型从这里开始续写,一直写到“结束”标记为止。
所以,聊天的底层,仍然是第 01 篇的那个接龙。区别只是:被续写的这段文字,是一份“剧本”,轮到“助手”这一行时,模型写出的就是回答。
下一轮你再提问,前面所有轮的内容,会一起重新拼进去再读一遍(这就是后篇要讲的“上下文”)。
那“微调”要教的,就很清楚了:让模型认得这种剧本格式,轮到助手时,写出一个像样的回答,然后停下来。
三、第二阶段:微调(SFT)
微调,英文常叫 SFT(监督微调)。它做的事情很简单:
准备一批示范对话:一个问题,配一个理想的回答。然后,在这些示范上,继续做第 08 篇那一流程:看前文,猜下一个词,对答案,微调数字。
和预训练比,有几个不同:
- 数据少得多。 预训练是海量文本;微调通常只用少得多的示范对话(具体多少,各家差别很大)。
- 数据是“精挑细选”的。 每条都是一个问题加一个好回答,通常由人写,或者由人审核筛选。
- 练的是“回答的样子”。 常见做法是:只在“回答”那部分计算扣分,因为我们要它学的是怎么答,而不是怎么提问。
还有一个很重要的认识:
微调主要改变的是模型的“行为”:格式、语气、怎么开头、怎么结束。它需要的新知识很少,大部分的知识,来自预训练。
这是一个常见的看法,也有很多实践印证;但它不是绝对的规律。下面的小实验,就是为了让你亲眼看到这件事。
四、动手:用 3 条示范,教会一个小模型“回答”
我们做一个迷你版(需要 numpy)。它只有几十个词、几行语料,前面的套路都一样:向量、概率表、猜下一个词、对答案、微调数字。
流程是:
- 预训练: 先在一些杂乱的文字上训练:有陈述句(“法国的首都是巴黎”),也有问题列表(“问:法国的首都是哪里?问:日本的首都是哪里?……”)。
- 微调: 再只用 3 条示范对话(法国、日本、德国),继续训练。注意:示范里没有“意大利”。
- 对比: 同样问“法国”和“意大利”,看微调前后有什么不同。
# mini_sft.py —— 迷你"微调":先在杂乱的文本上预训练,再用几条示范对话微调,看行为怎么变
import numpy as np
BASE = ["法国 的 首都 是 巴黎 。", "日本 的 首都 是 东京 。", "德国 的 首都 是 柏林 。", "意大利 的 首都 是 罗马 。"] * 3 + [
"问 : 法国 的 首都 是 哪里 ? 问 : 日本 的 首都 是 哪里 ? 问 : 意大利 的 首都 是 哪里 ?",
"问 : 德国 的 首都 是 哪里 ? 问 : 意大利 的 首都 是 哪里 ? 问 : 法国 的 首都 是 哪里 ?",
"问 : 日本 的 首都 是 哪里 ? 问 : 德国 的 首都 是 哪里 ? 问 : 法国 的 首都 是 哪里 ?"]
SFT = [f"问 : {c} 的 首都 是 哪里 ? 答 : {a} 。 <结束>" for c, a in [("法国", "巴黎"), ("日本", "东京"), ("德国", "柏林")]] # 注意:没有"意大利"
vocab = sorted({w for s in BASE + SFT for w in s.split()} | {"好的", "不知道", ","}); idx = {w: i for i, w in enumerate(vocab)}; V = len(vocab)
# 让小模型也能"举一反三":给"首都名"这一类词一个共享通道(真实模型里,这种"同类相通"是自己学出来的,不用人指定)
Mc = np.zeros((1, V)); Mc[0, [idx[c] for c in ["巴黎", "东京", "柏林", "罗马"]]] = 1
def feats(prefix): # 模型"看到"的:前文里出现过哪些词 + 最后一个词是什么
f = np.zeros(2 * V)
for w in prefix: f[idx[w]] = 1
f[V + idx[prefix[-1]]] = 1
return f
def probs(W, prefix):
z = feats(prefix) @ W
p = np.exp(z - z.max()); return p / p.sum()
def train(W, texts, steps, lr): # 和第 08 篇一样:猜下一个词,对答案,微调一点点
xs, ys = [], []
for t in texts:
ws = t.split()
for i in range(1, len(ws)): xs.append(feats(ws[:i])); ys.append(idx[ws[i]])
X, y = np.array(xs), np.array(ys)
for _ in range(steps):
z = X @ W; p = np.exp(z - z.max(axis=1, keepdims=True)); p /= p.sum(axis=1, keepdims=True)
p[np.arange(len(y)), y] -= 1
g = X.T @ p / len(y)
W = W - lr * (g + g @ Mc.T @ Mc) # 同类的词(首都名)共享一部分更新
return W
def write(W, prompt, n=14): # 永远选最高的(第 07 篇的 T = 0)
ws = prompt.split()
while len(ws) < len(prompt.split()) + n and ws[-1] != "<结束>":
ws.append(vocab[int(probs(W, ws).argmax())])
return " ".join(ws[len(prompt.split()):])
if __name__ == "__main__":
base = train(np.zeros((2 * V, V)), BASE, 800, 2.0) # 预训练:杂乱的文本
tuned = train(base, SFT, 100, 2.0) # 微调:只用 3 条示范对话
for c in ["法国", "意大利"]:
q = f"问 : {c} 的 首都 是 哪里 ?"
print(f"{q}\n 预训练后:{write(base, q)}\n 微调之后:{write(tuned, q + ' 答 :')}\n")
运行结果(我实际跑过):
问 : 法国 的 首都 是 哪里 ?
预训练后:问 : 日本 的 首都 是 哪里 ? 问 : 意大利 的 首都 是
微调之后:巴黎 。 <结束>
问 : 意大利 的 首都 是 哪里 ?
预训练后:问 : 日本 的 首都 是 哪里 ? 问 : 法国 的 首都 是
微调之后:罗马 。 <结束>

关注下面三件事:
- 微调之前: 问“法国的首都是哪里”,它续写出更多问题。和文章开头说的一样。
- 微调之后: 它答出“巴黎。”,然后停下来(“结束”)。3 条示范,就教会了它“轮到回答时,该怎么答、答完就停”。
- 最有意思的:“意大利”从没出现在示范里,它却答对了“罗马”。 因为“意大利的首都是罗马”这个知识,早在预训练时就学到了;微调教它的,是“该把这个知识说出来”。 这正是前面说的:知识大头来自预训练,微调主要改变行为。
要说明下:为了让这个极小的模型也能“举一反三”,我给“首都名”这一类词加了一个共享通道(代码里的 Mc,让同类的词共享一部分更新)。我试过,去掉它,小模型答不出“意大利”,只会输出一个句号。真实的大模型里,这种“同类相通”不用专门设计,是训练中自己形成的。
五、第三阶段:对齐
微调之后,模型已经会回答了。那为什么还需要“对齐”?
因为会回答,不等于答得好。 示范对话是人写的,风格不一:有的简短,有的啰嗦;有的礼貌,有的生硬;甚至有的偷懒,直接说“不知道”。模型学了这些,就会写出什么风格都有的回答。
我们想要的,是“更有用、更诚实、更不越界”的回答(这是常见的说法,各家的具体准则不一样)。可这很难直接教:
- “什么叫更好”,很难写成规则。 你很难写一份清单,列出一个好回答的所有标准。
- 但人很擅长“比较”。 给你两个回答,让你选哪个更好,比让你亲手写出一个完美回答,容易多了。
所以,对齐的思路是:让人来比较,再让模型朝着“人更喜欢的方向”靠拢。

最经典的做法叫 RLHF(人类反馈强化学习),大致分四步:
- 同一个问题,让模型写多个回答;
- 人来比较:哪个更好?排个序;
- 用这些比较,训练一个**“打分员”模型**,学会给回答打分(模仿人的偏好);
- 用打分员的分数,调整原来的模型:高分的回答,更容易被写出来。
还有一种更简单的做法叫 DPO:不单独训练打分员,直接用“哪个更好、哪个更差”的成对数据,调整模型。两者的细节不同,思想一致。
六、动手:用“挑最好的、再学一遍”,做一个迷你对齐
真实的 RLHF 太复杂,没法在一篇文章里手写。我们用最简单的版本,演示同一个思想:让模型写多个回答,挑出最好的,再让它学一遍。(这种“挑最好再学”的做法,实践中也有人用,但真实系统要复杂得多。)
设定:
- 微调用的示范,风格故意混杂:有的简短(“巴黎。”),有的礼貌(“好的,巴黎。”),有的偷懒(“不知道。”),比例大约是 5:3:2。
- “人的偏好”,我用一条规则代替:答对 +2 分,礼貌(含“好的”)+1 分,说“不知道”扣 2 分。
- 做法:每个问题,让模型抽签写 8 个回答(第 07 篇),挑得分最高的,再学一遍。
# mini_align.py —— 迷你"对齐":让人更喜欢的回答更容易出现(最简单的办法:多写几个、挑最好的、再学一遍)
import numpy as np
from mini_sft import BASE, V, vocab, probs, train # 沿用上一段的小模型(两个文件放在同一个文件夹)
CAP = {"法国": "巴黎", "日本": "东京", "德国": "柏林", "意大利": "罗马"}
STYLES = [("{a} 。", 5), ("好的 , {a} 。", 3), ("不知道 。", 2)] # 不同标注员的写法:简短、礼貌、偷懒,各有几条
MIXED = [f"问 : {c} 的 首都 是 哪里 ? 答 : " + s.format(a=CAP[c]) + " <结束>" for c in ["法国", "日本", "德国"] for s, n in STYLES for _ in range(n)]
sft = train(train(np.zeros((2 * V, V)), BASE, 800, 2.0), MIXED, 100, 2.0) # 预训练 → 微调(示范里什么风格都有)
rng = np.random.default_rng(0)
def sample(W, c): # 按概率抽签写一个回答(第 07 篇,T = 1)
ws = f"问 : {c} 的 首都 是 哪里 ? 答 :".split(); n0 = len(ws)
while ws[-1] != "<结束>" and len(ws) < n0 + 8:
ws.append(vocab[rng.choice(V, p=probs(W, ws))])
return " ".join(ws[n0:])
def score(c, ans): # 这里用规则当"人"的偏好:答对 +2,礼貌 +1,说不知道 −2
return 2 * (CAP[c] in ans) + ("好的" in ans) - 2 * ("不知道" in ans)
def report(W, name): # 每个问题各写 400 次,看看各种回答占几成
outs = [(c, sample(W, c)) for c in CAP for _ in range(400)]
kinds = {"礼貌且答对": lambda c, a: score(c, a) == 3, "简短但答对": lambda c, a: score(c, a) == 2, "说不知道": lambda c, a: score(c, a) == -2}
print(f"{name}:" + " ".join(f"{k} {sum(f(c, a) for c, a in outs) / len(outs):.0%}" for k, f in kinds.items()))
ital = [a for c, a in outs if c == "意大利"] # 「意大利」从没出现在示范里,单独看一眼
print(" 其中「意大利」:礼貌且答对 " + f"{sum(score('意大利', a) == 3 for a in ital) / len(ital):.0%}")
report(sft, "对齐之前")
best = []
for _ in range(10): # 每个问题写 8 个回答,挑得分最高的那个
for c in ["法国", "日本", "德国"]:
cands = [sample(sft, c) for _ in range(8)]
best.append(f"问 : {c} 的 首都 是 哪里 ? 答 : " + max(cands, key=lambda a: score(c, a)))
report(train(sft, best, 100, 2.0), "对齐之后") # 再学一遍"最好的"
(这段代码要和上一段 mini_sft.py 放在同一个文件夹里。)
运行结果(实测过,随机种子固定):
对齐之前:礼貌且答对 24% 简短但答对 41% 说不知道 16%
其中「意大利」:礼貌且答对 26%
对齐之后:礼貌且答对 79% 简短但答对 8% 说不知道 0%
其中「意大利」:礼貌且答对 68%
重点:
- 对齐之前: 礼貌且答对 24%,简短但答对 41%,说不知道 16%(剩下的是别的写法)。什么风格都有,和示范的混杂程度对得上。
- 对齐之后: 礼貌且答对升到 79%,说不知道降到 0%。回答的整体风格,朝着“得分高”的方向移动了。
- 而且“意大利”也变好了: 它从没出现在示范里,也没出现在挑选里,礼貌且答对的比例,仍然从 26% 升到了 68%。说明学到的是“什么样的回答更受欢迎”这种倾向,而不是死记某个问题的答案。
要说明白两点:
- “人的偏好”在这里是自定的规则(礼貌加分、说不知道扣分)。这也点出了对齐的一个真问题:模型对齐的,是“谁的偏好”? 真实系统里,是标注人员的判断,和制定准则的公司的规定。
- 这是极简版。 真实的 RLHF、DPO 要复杂得多,我们只是借这个小实验,让你看到“用偏好改变回答风格”这件事真的会发生。
七、对齐的局限,要知道
对齐很重要,但它不是万能的。几个值得知道的局限:
- 可能变得过于讨好。 因为“人更喜欢被认同”,有研究者观察到,经过对齐的模型,有时倾向于顺着用户的话说,哪怕用户错了。
- 可能过于谨慎。 为了“不越界”,有时会拒绝本来可以回答的问题。
- 不能消灭“胡说”。 对齐能让回答更礼貌、更自信,却不保证更真实。一个答得很有礼貌的错误答案,反而更容易让人相信(第 11 篇讲幻觉)。
- 不是万无一失。 有人会尝试各种办法绕过它的限制,所以安全是个持续的工作,不是一劳永逸的。
八、一个常被混淆的问题:提示词 vs 微调
你可能会问:我给模型写一段“你是一位专业的客服”,和微调,不都是在“教它怎么回答”吗?
区别很大:
- 提示词: 只是在对话的最前面加一段文字(上面“剧本”里的系统那一行)。模型里的数字一个都没变,换一个对话就没了。
- 微调: 真的改了模型里的数字,改完就一直在。
这个区别,后面会详细讲。现在你只需要记住:一个改的是“输入”,一个改的是“模型本身”。
九、毕业生这个比喻,到哪里为止
- 岗前培训并不是“完全不教新东西”。 微调也能让模型学到一点新东西,只是它的主要作用,是改变行为。
- 毕业生知道为什么,模型只是被调数字。 他明白“为什么这样答更好”;模型没有这种理解,只是数字被调成了倾向于某类回答。
- “对齐”不等于“有了价值观”。 它只是让模型更倾向于写出某种人们期望的回答。模型是不是“真的明白对错”,是另一个问题。
十、三个常见误解
误解 1:“聊天助手是另一种技术,和接龙不一样。”
不是。它的底层,仍然是同一个 Transformer,仍然是一个词一个词接龙。多出来的,是微调和对齐,让它学会了按“剧本”来答。
误解 2:“微调可以把新知识灌给模型。”
不太靠谱。微调主要改变的是行为和风格,很难可靠地“灌进”大量新事实。想让模型回答它没学过的资料,常用的办法是第 13 篇的 RAG。
误解 3:“对齐过了,就一定是准确、安全的。”
不是。对齐提高的是“更符合人的偏好”的可能性,不是保证。它可能更礼貌,但仍会出错,也仍可能被绕过。
十一、本篇要点
- 基础模型只会续写;要变成聊天助手,要再经过微调(教它怎么回答)和对齐(让回答更合人意)。
- 聊天的底层,仍然是接龙:对话被拼成一份“剧本”,模型从“助手”那一行开始续写。微调主要改变行为,知识大头来自预训练。
- 对齐靠“人来比较”,让更受欢迎的回答更容易出现。它有用,但有局限:可能讨好、可能过度谨慎、不能保证真实。
下一篇预告: 前面我们反复说“几十亿、上千亿个参数”“要很多 GPU”。这些数字到底是什么?为什么训练一个大模型这么贵?下一篇讲参数、算力与成本。
系列目录

文中的图、代码,欢迎自己动手改一改,比如把示范里的风格比例改一改,或者把打分规则换成“越短越好”,看看对齐之后回答会怎样变化。有哪里没讲明白,欢迎在评论区告诉我。
更多推荐
所有评论(0)