GRPO的数据集,告别“标准答案”
第二步:构建数据集——GRPO的“原料”有何不同
在上一章中,我们为项目绘制了详细的“蓝图”,精确定义了我们的任务。现在,是时候准备“建筑材料”了——也就是我们的训练数据集。
如果你有监督微调(SFT)的经验,你可能会下意识地开始思考:“我该去哪里找成千上万个由DBA专家手写的、针对各种问题的最优SQL查询呢?”
请停下来。这正是GRPO训练的魅力所在:它将我们从对“完美标准答案”的无尽追求中解放出来。 GRPO的数据准备工作比SFT要简单得多,也更能体现我们作为AI训练专家的智慧。
1. 与监督微调(SFT)的根本区别
让我们通过一个直观的对比,来理解这两种方法在数据需求上的本质差异。
-
SFT 数据集格式:
一个典型的SFT数据点,是一个严格的“提示-完成”对 (Prompt-Completion Pair)。
{ "prompt": "问题:查询用户表中年龄最大的前3位用户的姓名。 Schema: CREATE TABLE users(id INT, name VARCHAR, age INT);", "completion": "SELECT name FROM users ORDER BY age DESC LIMIT 3;" }
这里的
"completion"
必须是一个高质量的、甚至是“黄金标准”的答案。模型的学习目标就是像素级地模仿这个答案。 -
GRPO 数据集格式:
而一个GRPO数据点,则是一个“提示-验证元数据”对 (Prompt-Verification Metadata Pair)。
{ "prompt": { "question": "查询用户表中年龄最大的前3位用户的姓名。", "schema": "CREATE TABLE users(id INT, name VARCHAR, age INT);", "constraints": ["禁止使用 OFFSET"] }, "expected_result": "[{\"name\": \"Alice\"}, {\"name\": \"Bob\"}, {\"name\": \"Charlie\"}]" }
看到了吗? 最大的区别在于,我们不再提供那个“完美的SQL”。取而代之的是一个名为
expected_result
的字段。这个字段存储的是当一个正确的SQL被执行后,应该返回的真实结果。在训练过程中,模型是看不到
expected_result
的。它只会接收prompt
,然后生成自己的SQL。我们的“自动裁判”(奖励函数)会执行模型生成的SQL,并将结果与expected_result
进行比对,以此来判断模型的回答是否“语义正确”。这才是整个流程中最精妙的地方。我们把对模型的评价标准,从**“你写得跟我给的范例像不像?”** 转变为了 “你写的代码,能不能解决实际问题?”
%%{ init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#ffffff', 'primaryTextColor': '#111827', 'nodeBorder': '#6b7280', 'lineColor': '#4b5563', 'fontSize': '14px' } } }%% graph TD subgraph "SFT 训练流程" A[Prompt] --> B{"SFT 模型"}; B --> C["生成的 SQL"]; D["黄金标准 SQL"] C -- 计算损失 (Loss) --> E{比较差异}; D -- 对比 --> E; E --> F[更新模型]; end subgraph GRPO 训练流程 G[Prompt] --> H{"GRPO 模型"}; H -- 生成 --> I["候选 SQL"]; J[预期结果集] I -- 执行 --> K[实际结果集]; K -- 比较 --> L{奖励函数}; J -- 对比 --> L; L -- 生成奖励信号 --> M[更新模型]; end %% 为高对比度和移动端可读性进行样式设计 classDef gold std fill:#fefce8,stroke:#facc15,stroke-width:2px; classDef reward fn fill:#fff1f2,stroke:#fb7185,stroke-width:2px; classDef model fill:#f0f9ff,stroke:#38bdf8,stroke-width:2px; classDef final_step fill:#f0fdf4,stroke:#4ade80,stroke-width:2px; class D,J gold; class L reward; class B,H model; class F,M final_step;
上图清晰地展示了两种模式的差异。SFT是静态的模仿,而GRPO是动态的、基于结果的验证与学习。
2. 数据集生成实践
理解了原理,我们就可以动手构建自己的GRPO数据集了。好消息是,我们不必从零开始。我们可以巧妙地利用现有的、为SFT准备的Text-to-SQL数据集(如 Spider, WikiSQL 等),通过一个自动化流程将其转化为我们需要的格式。
下面是具体的生成逻辑,我们将一步步实现它:
-
第一步:获取源数据
从现有的数据集中,我们可以轻松获得成千上万的(自然语言问题, Schema, 黄金SQL)
三元组。这里的“黄金SQL”虽然我们不直接用它来训练模型模仿,但它将作为我们生成expected_result
的关键工具。 -
第二步:执行黄金SQL,生成预期结果
这是最核心的转换步骤。我们需要搭建一个本地的数据库环境(例如,使用SQLite或Docker化的PostgreSQL)。然后,编写一个脚本,遍历所有的三元组:- 在数据库中根据
Schema
创建相应的表结构。 - (如果数据集提供)向表中填充样本数据。
- 执行
黄金SQL
。 - 获取 查询返回的结果。
- 在数据库中根据
-
第三步:序列化并存储结果
数据库查询返回的结果通常是表格形式的。为了方便后续进行精确的、自动化的比较,我们需要将其序列化为一个标准格式,比如JSON字符串。- 重要提示:SQL查询结果的行顺序通常是不保证的,除非使用了
ORDER BY
。为了确保比较的确定性,一个最佳实践是在序列化为JSON之前,对结果集(比如,一个字典列表)按照所有键进行排序,确保无论原始行顺序如何,最终的JSON字符串都是唯一的。 - 处理完后,
expected_result
字段的内容可能看起来像这样(一个排序后的JSON字符串):"[{\"name\": \"Alice\"}, {\"name\": \"Bob\"}, {\"name\": \"Charlie\"}]"
。
- 重要提示:SQL查询结果的行顺序通常是不保证的,除非使用了
-
第四步:(可选)丰富约束条件
为了让我们的模型训练任务更具挑战性,也更贴近真实世界的复杂需求,我们可以编写一个脚本,为每个问题自动、随机地生成一些额外的约束。- 例如:
- 随机挑选一个SQL关键字(如
JOIN
,GROUP BY
,WHERE
)并加入到约束列表["禁止使用 'JOIN'"]
。 - 随机设置一个执行时间上限
["执行时间不能超过 2 秒"]
。 - 要求必须使用某种语法结构
["必须使用 CTE"]
。
通过这种方式,我们可以极大地扩充训练数据的多样性,迫使模型学习在不同限制下灵活构建查询的能力。
- 随机挑选一个SQL关键字(如
- 例如:
-
第五步:组合成最终格式
最后,我们将所有处理好的部分组合成最终的数据格式,并保存为一个JSONL文件。{ "prompt": { "question": "找出上个月所有‘电子产品’类别中,销售额超过 $1000 且利润率最高的前五种商品。", "schema": "CREATE TABLE products(...); CREATE TABLE sales(...);", "constraints": ["禁止使用子查询"] }, "expected_result": "[{\"product_name\": \"SuperPhone X\"}, {\"product_name\": \"UltraLaptop Pro\"}, ...]" }
通过以上步骤,我们就能高效地、大规模地构建出适用于GRPO训练的数据集。我们没有花费任何精力去手写或校验“最优SQL”,而是建立了一套生成可验证事实的自动化流水线。这不仅效率更高,也让我们能将更多精力投入到更有创造性的工作中去,比如设计一个能够精确评估模型能力的奖励函数。
在下一章,我们将进入整个GRPO流程中最核心、最考验智慧的部分:设计奖励函数。它将是模型的“导航系统”,直接决定了我们最终能训练出一个怎样的“SQL专家”。