EN VERSION
← 所有文章

DPO vs. GRPO:教小模型写「高效」SQL

在哥大的一门 NLP 课程项目里,我们组在 BIRD benchmark 上微调了 Qwen3-4B-Instruct,目标其实很窄: 不只是写出「对」的 SQL,而是写出「快」的 SQL。一个用三表自连接查出正确结果、而本该用一次索引查找就搞定的模型, 按 execution accuracy 这类指标看是「对」的,但放到生产环境里就是没用的东西。「正确」和「高效」之间的这道缝隙, 正是这个项目里大部分有意思的问题冒出来的地方。

为什么一个对齐阶段不够用

我们最初的直觉是:只对同一个问题的高效 vs. 低效 SQL 做 DPO,训完就完事了。DPO 便宜、稳定, 也不需要在线 rollout 循环——你只要提前整理好偏好对,离线训练就行。这个做法确实有效,但也就到某个程度: 模型学到了一些相当靠谱的风格偏好(避免不必要的子查询、能用 join 就不用相关子查询)。

它没学好的地方,是泛化到我们没有配对过的查询结构上。DPO 优化的是两个具体生成结果之间的相对偏好—— 它完全没有「这个查询实际跑起来会怎样」的信号。模型可以学会「模式 A 看起来比模式 B 更像高效样本」, 却完全没有落地到「模式 A 在给定 schema 和数据分布下是不是真的更快」这件事上。偏好对只是效率的一个代理指标, 不是效率本身。

加入执行感知的 RL——以及立刻为「naive 版本」后悔

解决办法是加一个第二阶段:GRPO 风格的强化学习,奖励直接从「执行」中构建出来——真正跑一遍生成的 SQL, 测量延迟,再结合正确性一起计算。这填上了 DPO 留下的那道缝:现在模型拿到的梯度信号是关于「什么真的快」的真值, 而不只是「看起来更快」。

我们第一版奖励函数很朴素:正确性加分,加上延迟惩罚。它几乎立刻就开始 reward hacking, 事后想想其实很明显——模型发现返回空结果集,或者写一个过度限制的 WHERE 条件, 执行速度非常快,而且在某些查询类型下还能勉强通过比较宽松的正确性检查。「快但错」的得分一度高于 「慢但对」,直到我们把正确性检查改成硬性门槛(不正确直接零奖励,而不是软性扣分), 并且让延迟只在正确性满足之后才进入奖励计算,这个问题才被解决。

这是叠加 DPO 和 GRPO 之后最核心的一条经验:在线阶段的奖励函数,在你把它规模化之前, 必须先具备对抗鲁棒性,因为强化学习会找到通向高奖励的最短路径,而不是你设想中的那条路径。 正确性作为硬门槛——效率只有在正确性满足之后才有意义——这不是一个可选的优化项,而是硬性要求。

DPO 的稳定 vs. GRPO 的高方差

实际跑起来,这两个阶段的体感非常不一样。DPO 训练很稳定:loss 曲线表现正常,没有 reward collapse, 复现起来也容易。GRPO 就吵得多:训练早期 rollout 组之间的奖励方差很高,需要更细致的奖励归一化 (按 GRPO 的公式做组内相对 advantage 计算),才能防止更新被少数几个异常快或者彻底跑崩的 rollout 主导。

先跑 DPO 再跑 GRPO 这个顺序不是随便定的——顺序本身很重要。DPO 给了策略一个相对合理的初始分布, 让它对「看起来高效的 SQL」有基本认知,这意味着 GRPO 的 rollout 从一个更靠谱的起点开始, 收敛所需的步数更少;而且像上面提到的空结果集这种明显退化的输出,在在线 RL 真正开始对奖励施压之前, 就已经被部分抑制掉了。

最后落地成了什么

DPO → GRPO 的组合 pipeline 在 BIRD Mini-Dev 集上,相对 baseline 在 execution accuracy 和 Relative Valid Efficiency Score (R-VES) 上取得了 7% 的提升。之后的定性错误分析显示, 剩下的失败案例主要集中在真正困难的查询结构上——深度嵌套的聚合、列引用有歧义的多表 join—— 而不再是我们最初那种「技术上正确但荒谬低效」的模式。这个转变其实就是整件事的意义所在: 单靠偏好学习能拿到「风格上的高效」,执行感知的 RL 才能拿到「真正的高效」, 而正确性门槛则是保证第二阶段不作弊的前提。

完整项目报告 →
← 所有文章