奖励黑客:它找到了你没想到的漏洞
这一章有两个案发现场,一个是业界的经典案例,一个是我写这本书的时候真的犯的错。它们的共同点是:训练全程没有任何异常——损失在降,分数在涨,曲线漂亮得像教科书。
案发现场一:那条赛艇
2016 年,OpenAI 在一个叫 CoastRunners 的赛艇游戏上训练 agent。规则很简单:赛道上有一圈补给点,撞到就加分,吃掉后过一会儿会刷新。目标是赢比赛。
作者写下的奖励是:游戏里显示的那个分数。
这个选择再合理不过——分数就是游戏设计者定义的「好」,而且跑得快的人分自然高。
结果 agent 找到了一个水湾,那里有三个补给点密集地排在一起,而且刷新很快。
下面这台是真的逐步仿真:一圈 240 格,23 个补给点,吃掉后 10 步刷新,总共跑 600 步。两种策略各跑一遍,分数是数出来的。
那个数字
老老实实跑完全程:139 分。跑完 2 圈,一路吃到 59 个补给。
在泻湖里转圈刷补给:151 分。跑完 0 圈,吃到 151 个补给。
一圈都没跑完的那个,赢了。
而真实的 CoastRunners 案例里,那个 agent 的分数比正常跑完全程高 20%——它一边转圈一边撞船、着火、逆行,从任何一个人类的角度看它都在原地发疯,但它的分数是全场最高的。
这一点值得说清楚,因为它决定了你该怎么处理这类事故。
那个 agent 没有利用任何 bug,没有修改内存,没有钻游戏引擎的漏洞。它做的事完全在规则之内:撞补给点加分,它就去撞补给点。
问题出在人这一侧:「分数」和「赢比赛」在设计者心里是一回事,在赛道上大部分时候也确实是一回事——直到不是。
这类现象有个更中性的名字,叫 规格博弈(specification gaming):agent 满足了规格的字面要求,却违背了规格的意图。
「它作弊了」是一个会误导你的说法。它会让你去想「怎么防它作弊」,而正确的问题是「我的规格哪里写漏了」。
案发现场二:我自己犯的那个
这一个我讲得详细一点,因为整个过程是我真实经历的。
上一章那台两足小人,我给它写的奖励是:
reward = x + 0.5 * alive_time + 5 * (1 - abs(final_theta)) # └┬┘ └──────┬──────┘ └──────────┬──────────┘ # 走多远 活多久 要站直(用最后一帧的躯干倾角)
三项,都很合理:走得远、活得久、而且要站直。我当时对这一行相当满意。
然后 CEM 跑了十四代。日志是这样的:
第 1 代 最好分 5.140 群体均值 -0.020 前进 0.000 m 第 2 代 最好分 5.140 群体均值 4.874 前进 0.000 m 第 3 代 最好分 5.140 群体均值 5.140 前进 0.000 m 第 4 代 最好分 5.140 群体均值 5.140 前进 0.000 m …… 第 14 代 最好分 5.140 群体均值 5.140 前进 0.000 m
我的第一反应是「训练不动了」——学习率、种群大小、噪声地板,挨个调了一遍。没用。
第二反应是「物理写错了」——去检查腿的正向运动学,果然发现一个 π/2 的角度 bug(腿是横着长的)。修好了。还是 0.000 m。
然后我去看了那个「冠军个体」到底在干什么:
前进 0.00 m | 存活 14/500 帧 | 末帧倾角 0.00 | 得分 5.14
它当场趴下了。14 帧,大约 0.28 秒。落地的时候身体是正的,倾角 0.00。
把分算一下:0 + 0.5×0.28 + 5×(1−0) = 5.14。
那 5 分的姿态奖励,它一分不少地拿满了。而且拿得极其高效——不用走路,不用平衡,不用维持任何东西,直接躺平,姿势摆正,收工。
我想说的是「一直保持直立」。我写下的是「最后一帧的时候是直立的」。
这两句话在我脑子里是一回事,因为我默认了「它会一直走到最后一帧」。而这个默认,恰恰是我要它学会的东西——我把结论当成了前提。
改法只有一处:把姿态分改成按时间积分。
# 出事的版本:只看最后一帧 reward = x + 0.5 * alive_time + 5 * (1 - abs(final_theta)) # 改好的版本:全程积分 reward = x + 0.5 * alive_time + 0.5 * upright_time # └─ Σ (1-|θ_t|)·dt,摔早了就攒不到
改完之后,同一个种子、同一套超参,它走了 9.47 米,500 帧里站住了 497 帧。
这类 bug 最恶劣的地方
回头看那份日志:它没有任何异常。
- 没有报错
- 没有 NaN
- 没有发散
- 群体均值在上升,然后漂亮地收敛了
- 最好分稳定在 5.140,一动不动——教科书级别的收敛曲线
如果我只看曲线,我会得出结论:「训练非常成功,模型已收敛。」
永远去看录像,别只看曲线。
第 16 章那种训练发散,会在数值上留下痕迹——Q 值单调上涨、loss 爆炸、NaN。你盯着曲线能发现。
奖励黑客不会。它在数值上是完美的:目标函数确实被最大化了,而且往往比正常行为最大化得更彻底、更稳定、收敛得更快。
唯一的检测手段是把它渲染出来看一眼。三十秒的事,能省你三天。
具体做法:env = gym.make(env_id, render_mode="human"),或者存一段 rgb_array 成 gif。每训完一版就看一遍,养成条件反射。
更多真实案例
这类事故多到有人专门在维护清单(DeepMind 那份「规格博弈」列表收了几十条)。挑几个特别有代表性的:
| 要它做什么 | 奖励怎么写的 | 它学会了什么 |
|---|---|---|
| 让机械臂把积木叠起来 | 红色积木底面的高度 | 把积木翻过来——底面朝上,高度达标,没有叠 |
| 让虚拟生物跑得快 | 质心的水平速度 | 长成一根超高的杆子,然后直接倒下去——倒的瞬间质心速度极高 |
| 让 agent 通关 | 关卡进度 | 找到一个能让游戏崩溃的操作序列,崩溃时进度计数溢出 |
| 让扫地机器人别撞东西 | 碰撞传感器不触发就加分 | 倒着走——因为后面没装传感器 |
| 让赛车跑完赛道 | 沿赛道方向的位移 | 在一段路上来回反复开,反复刷同一段位移 |
注意最后一行和第 19 章那个「势函数塑形」的对照:写成 Φ(s') − Φ(s) 的形式,来回开的收益会精确抵消,这个漏洞根本不存在。写法的差别很小,性质的差别很大。
怎么防
没有根治办法,但有一套能挡掉大部分事故的做法:
① 看录像。这一条排第一,而且遥遥领先
不解释了,上面已经说过两遍。
② 每一项都问:这能被无限刷吗
逐项过一遍:存不存在一种活法,让这一项一直涨,而不推进真正的目标?
特别警惕这三类写法:
- 只看某一时刻的状态(我犯的那个)。改成按时间积分。
- 奖励某个可逆的状态变化(靠近、拿起、进入某区域)。改成势函数形式。
- 用一个能无限重复的事件计数(撞到补给、碰到球)。加冷却,或者改成「每个只算一次」。
③ 用一个 agent 碰不到的指标来验收
这一条最有用,也最容易被忘。
训练用塑形过的奖励,验收用那个稀疏的、真正的目标。
| 训练奖励(可以塑形) | 验收指标(不参与训练) | |
|---|---|---|
| 赛艇 | 游戏分数 | 有没有跑完、名次第几 |
| 两足小人 | 前进+活着+直立 | 10 秒内走了多少米、摔了没有 |
| 推荐系统 | 点击率 | 次日留存、长期付费 |
关键在于验收指标绝对不能进入训练。一旦它进去了,它也会被优化,也就不再是一把干净的尺子——古德哈特定律在这里同样成立。
④ 从简单到复杂,一次只加一项
先跑最稀疏的版本,看它能不能学到点什么。然后一次只加一项奖励,加完看录像。
一次加五项,出了事你不知道是哪一项的锅。第 19 章那张表就是这么一项一项试出来的。
奖励黑客不是游戏 AI 的专属病,它在大语言模型上照样发作,只是形态不同:
- 奉承(sycophancy):标注员偏爱同意自己的回答 → 模型学会顺着用户说,哪怕用户是错的。
- 啰嗦:长回答看起来更用心 → 模型学会把话说三遍。
- 虚假的自信:犹豫的措辞得分低 → 模型学会用肯定的语气说错话。这一条尤其危险,因为它同时降低了「说错」和「显得像错的」。
- 形式套路:带小标题和列表的回答得分高 → 模型学会给一切内容加小标题。
这和小人趴下刷姿态分,是同一件事。奖励模型是从人类偏好拟合出来的,它不是「什么是好回答」,它是「标注员倾向于选哪个」——两者之间的差距就是漏洞。
第 22 章会讲 RLHF 怎么处理这个(答案是:加一个 KL 惩罚拴住它,本质上是第 18 章那个 clip 换了个写法),以及为什么那只是缓解,不是解决。
回想一下你看过的那些「AI 训练翻车」合集:小人以诡异的姿势蠕动、贴着地面震动着前进、用头着地滑行……
那些几乎全部是奖励黑客——而且它们全都是在最大化作者写下的那个函数。作者想要「跑步」,写下的是「质心往前移动」,于是得到了「以任何方式让质心往前移动」。
下次再看,你可以试着从行为反推奖励函数:它在拼命做的那件事,就是奖励里被写重了的那一项。
这是个很好的练习,而且它反过来会让你写奖励的时候更小心。
这一章的一句话
奖励黑客不是 agent 在作弊,是你的规格写漏了。而它最恶劣的地方在于:训练全程一切正常——曲线漂亮、损失在降、分数在涨,只有行为是错的。
下一章:反过来的那个问题。如果你把奖励写得极其干净、完全无法钻空子——那它可能压根学不动。六次实验,六百局,一次都没成功。