结构化输出的难点通常不在“能不能生成 JSON”,而在于模型能否持续遵守约束:字段完整、类型正确、枚举值合法,并且在输入稍微变化后仍然稳定。这个实验的核心思路,是围绕一个 3.5 亿参数模型设计专门的结构化输出奖励,再用 100 个 GRPO 步骤快速验证微调是否有效。
这里的 100 步更像一个小规模实验预算,而不是适用于所有任务的固定结论。它的价值在于缩短反馈周期:先用明确的奖励函数验证方向,再决定是否扩大数据量、训练步数或模型规模。
为什么 GRPO 适合这个任务
GRPO(Group Relative Policy Optimization)会针对同一个输入采样一组回答,然后根据组内相对奖励更新模型。对于结构化输出任务,这种机制很实用,因为一次采样可以同时暴露多种失败模式:
- 有的回答是合法 JSON,但缺少必需字段。
- 有的字段存在,但类型错误,例如把数字写成字符串。
- 有的格式正确,却违反业务规则。
- 有的回答内容合理,只是混入了 Markdown 代码围栏或额外解释。
奖励函数不必一开始就复杂。可以把“可解析”“字段完整”“类型正确”和“业务语义正确”拆成多个可观察信号,再按优先级组合。这样做比只用一个模糊的文本相似度分数更适合约束输出。
对于 350M 级别模型,训练成本相对可控,但容量也有限。因此训练目标应当收窄到一个清晰的输出协议,例如“根据工单文本返回固定 JSON”,而不是同时要求模型掌握复杂推理、长上下文和多种工具调用格式。
把结构化输出写成奖励函数
下面的示例不依赖深度学习框架,可以直接运行。它模拟一个训练前的奖励评估器:输入模型候选回答,检查 JSON、字段、类型、枚举值以及额外文本。实际接入 GRPO 框架时,可以把 structured_reward 作为每个 rollout 的奖励函数。
运行前只需要 Python 3.9 或更高版本:
python reward_demo.py
将下面内容保存为 reward_demo.py:
import json
from typing import Any, Dict
REQUIRED_FIELDS = {
"category": str,
"priority": int,
"action": str,
}
VALID_CATEGORIES = {"billing", "technical", "account"}
VALID_ACTIONS = {"reply", "escalate", "close"}
def structured_reward(text: str) -> float:
"""Return a 0..1 reward for a strict support-ticket JSON protocol."""
text = text.strip()
# Penalize common behavior that breaks JSON parsers downstream.
if text.startswith("```") or text.endswith("```"):
return 0.0
try:
value: Any = json.loads(text)
except json.JSONDecodeError:
return 0.0
if not isinstance(value, dict):
return 0.1
score = 0.2 # Valid JSON object.
if set(value) == set(REQUIRED_FIELDS):
score += 0.2
else:
return score
for field, expected_type in REQUIRED_FIELDS.items():
# bool is an int subclass, but is not a valid priority here.
if field == "priority":
type_ok = type(value[field]) is int
else:
type_ok = isinstance(value[field], expected_type)
if not type_ok:
return score
score += 0.25
if value["category"] in VALID_CATEGORIES:
score += 0.15
if value["action"] in VALID_ACTIONS:
score += 0.15
if 1 <= value["priority"] <= 5:
score += 0.05
return round(score, 2)
if __name__ == "__main__":
candidates = [
'{"category":"technical","priority":2,"action":"reply"}',
'```json\n{"category":"technical","priority":2,"action":"reply"}\n```',
'{"category":"other","priority":"high","action":"reply"}',
]
for candidate in candidates:
print(f"{structured_reward(candidate):.2f} {candidate}")
这个奖励函数体现了一个重要顺序:先保证机器可解析,再检查协议完整性,最后奖励业务约束。不要让“内容看起来合理”覆盖格式错误,否则模型可能学会输出一段人类可读但无法被服务消费的文本。
在真实训练中,可以为不同失败级别设置更平滑的分数。例如,合法 JSON 但缺字段的回答得到 0.2,字段完整但枚举值非法得到 0.65,完全符合协议的回答得到 1.0。组内相对比较会让模型更容易识别哪些候选更好。
100 步实验应该观察什么
短训练最适合回答三个问题,而不是证明模型已经完成生产级优化。
1. 格式错误是否快速下降
记录每个步骤中以下指标的变化:
- JSON 解析成功率。
- 必需字段完整率。
- 字段类型正确率。
- 枚举值合法率。
- 是否出现额外解释或 Markdown 围栏。
如果 100 步后只有训练集格式正确率上升,而稍有改写的输入仍然失败,说明模型可能只是记住了模板。此时应增加输入多样性,而不是继续盲目增加训练步数。
2. 奖励是否被投机利用
奖励函数存在漏洞时,模型会找到评分最高但业务无效的输出。例如,奖励只检查字段存在,模型就可能输出空字符串;只检查 JSON 合法,模型就可能使用无意义的默认值。
因此,验证集必须包含边界输入:缺少信息、冲突信息、未知类别、超范围优先级,以及很长或很短的文本。奖励函数也要加入业务有效性检查,必要时对空值、默认值和重复内容扣分。
3. 训练是否损害原有能力
350M 模型的参数容量有限,窄任务微调可能影响通用指令跟随能力。除了目标格式测试,还应保留一组未参与训练的通用指令、拒答和长文本样例,观察训练后是否出现明显退化。
可以把实验配置记录成简单的 YAML,便于复现和比较:
model: your-350m-instruction-model
algorithm: grpo
steps: 100
rollouts_per_prompt: 8
max_new_tokens: 160
learning_rate: 1.0e-6
reward:
parser: json
required_fields: [category, priority, action]
reject_markdown_fence: true
validation:
- json_parse_rate
- schema_valid_rate
- enum_valid_rate
- extra_text_rate
这里的字段名是实验配置示例,具体参数需要按所使用的训练库调整。rollouts_per_prompt 越大,组内比较通常越稳定,但显存和生成成本也会增加;max_new_tokens 则应贴合协议长度,避免模型通过无关长文本获得训练机会。
采用前的检查清单
短步数 GRPO 适合做方向验证:成本低、反馈快,能够迅速判断奖励设计是否真的推动了结构化输出。它并不能替代更大规模的验证,也不能保证模型永远生成合法 JSON。
落地时建议保留以下边界:
- 推理服务仍使用 JSON Schema、类型校验或解析重试做最终防线。
- 训练奖励和离线评估使用同一套严格协议,但不要只用训练奖励衡量效果。
- 验证集加入未见过的字段组合、输入改写和异常样例。
- 同时监控结构化输出成功率与原有指令能力,避免局部指标变好、整体体验变差。
- 先运行 100 步小实验,确认奖励没有漏洞,再扩大训练规模。
真正值得优化的不是“让模型看起来更像 JSON”,而是让模型在清晰协议、可测奖励和服务端校验共同作用下,稳定地产生下游系统能够信任的结构化结果。