SFT 数据工程实战:从业务数据到高质量 JSONL 训练集(以“课程推荐 + 学习规划”为例)

大模型微调(SFT)能不能训练出“像样的业务助手”,关键往往不在模型和参数,而在数据工程:你给模型喂的样本是否足够清晰、足够一致、足够可验证,能否逼着模型学会“按规则办事”。本文用一个真实且常见的场景做贯穿:根据用户画像 + 候选课程(candidates),输出课程推荐 + 推荐理由 + 学习规划,并且必须满足强约束

作者:lh

大模型微调(SFT)能不能训练出“像样的业务助手”,关键往往不在模型和参数,而在数据工程:你给模型喂的样本是否足够清晰、足够一致、足够可验证,能否逼着模型学会“按规则办事”。

本文用一个真实且常见的场景做贯穿:根据用户画像 + 候选课程(candidates),输出课程推荐 + 推荐理由 + 学习规划,并且必须满足强约束:

  • 只能从 candidates 里选 goods_id
  • 不得编造课程名/讲师信息
  • 输出必须是严格可解析 JSON
  • 候选不足时要能合理拒答/追问

目标是:写完这篇,你就能搭一套稳定的 SFT 数据流水线,并且知道怎么做闭环把模型越训越“听话”。



1. 先定义任务边界:训练模型学什么、不学什么

SFT 最容易翻车的一点是:任务定义太泛。比如“根据画像推荐课程”,模型会倾向于发挥“通用聊天能力”,开始编课程、许诺收益、输出散文式理由。

解决方式是:把任务定义写成可判定、可校验的规则。

以本场景为例,建议你把训练目标约束成四件事:

  1. 选择:从 candidates 选 0~K 门课(K 固定或可变)
  2. 解释:理由必须引用画像信息与课程字段(course_name/brief/teacher/direction)
  3. 规划:学习路线按周拆解(week1~weekN 或 milestones)
  4. 拒答:当 candidates 不足/信息缺失时,输出拒答并给追问
核心原则:每条输出都要能被程序验证对错(至少结构正确、ID合法、字段齐全)。


2. 数据结构设计:为什么建议“输入里带候选,输出只给 JSON”

2.1 推荐的样本结构(instruction / input / output)

你现在用的 JSONL 三段式非常适合 SFT:

  • instruction:固定任务说明 + 规则(强约束)
  • input:业务上下文(user_profile、user_query、candidates、constraints)
  • output:严格 JSON(只允许某些字段)

这套结构的好处是:

  • 规则固定,模型更容易学到一致行为
  • 输入包含“可引用证据”(课程字段),减少胡编
  • 输出可解析可校验,容易做自动评测与回流

2.2 输出 JSON Schema(建议固定)

例如你可以强制只允许一个顶层字段:selected,里面每门课包含固定子字段:

{
  "selected": [
    {
      "goods_id": "123",
      "course_name": "…",
      "teacher": "…",
      "match_reason": "…",
      "learning_plan": [
        {"week": 1, "focus": "…", "tasks": ["…","…"]},
        {"week": 2, "focus": "…", "tasks": ["…","…"]}
      ]
    }
  ],
  "overall_advice": {
    "weekly_hours": 4,
    "risk_notes": ["…"],
    "next_step": ["…"]
  }
}

并在 instruction 明确:

  • 不得输出额外字段
  • goods_id 必须来自 candidates
  • course_name/teacher 必须与 candidates 中完全一致(或只输出 goods_id,名字由系统回填)
工程上更稳的一招:输出只给 goods_id + 理由 + 规划,课程名与讲师由你的后处理从库里回填,直接消灭“编课程名/编老师”的问题。


3. 样本生成主流程:先建“可控候选”,再生成答案

一个稳的 SFT 数据流水线通常分三步:

Step A:构造输入(可控、可验证)

  • 从你的课程库里,为每个用户生成 candidates
  • candidates 的数量与方向规则要可解释(比如 top_k、同方向优先、老师优先等)

Step B:生成输出(让模型学会“选择 + 解释 + 规划”)

  • 可以用更强的模型作为“数据老师”(teacher model)合成回答
  • 输出必须满足你的 JSON schema,且可解析

Step C:自动校验 + 回收 bad rows

  • 解析 JSON
  • 校验 goods_id 是否越界
  • 校验字段是否缺失/多余
  • 校验理由是否引用了课程字段(可做弱校验:包含 course_name/teacher/direction 关键词之一)

失败样本进入 bad_rows.jsonl,并记录失败原因(后面会讲闭环怎么做)。

关键顺序:先生成候选(输入),再生成回答(输出)。 因为你要保证“只能从 candidates 选”这一条,是输入侧先决定的。


4. 数据质量的“硬指标”:四个必须盯的数

SFT 数据工程一定要量化,不然你不知道训练好没好。

建议至少统计这四项:

  1. 可解析率:output JSON parse 成功占比
  2. 越界率:输出 goods_id 不在 candidates 的比例(越低越好)
  3. 结构合规率:字段缺失/多字段/类型错误比例
  4. 幻觉率:编课程名/编老师/课程不存在(如果你输出只给 goods_id,这项几乎为 0)

你会发现:

  • loss 下降不代表越界率下降
  • 越界率下降,业务才真的“能上线”


5. 负样本与拒答样本:让模型“知道什么时候该说不”

很多业务助手失败,是因为训练数据里只有“成功推荐”,没有“失败处理”。模型自然学不会拒答。

你至少要覆盖三类拒答:

5.1 candidates 为空 / 不足

  • 输出 selected: []
  • 给出原因(候选不足)
  • 给追问(需要补充目标/风险偏好/时间等)

5.2 画像信息缺失

  • 例如缺少目标、风险偏好、可用时间
  • 输出拒答并列出“需要补充的字段清单”

5.3 候选与约束冲突

  • 例如 directions 限制,但 candidates 里没有合规方向
  • 输出拒答 + 建议放宽约束或重新拉取候选
经验:拒答样本占比别太低。 很多项目里拒答/追问样本做到 10%~30%,模型行为会稳定很多。


6. 模板化不是坏事:让“输出风格一致”反而更好训

SFT 不怕“模板”,怕的是“每条样本输出风格都不一样”。

你可以把理由与规划写成固定结构:

  • 推荐理由:画像匹配点 + 课程覆盖点 + 风险提示
  • 学习计划:每周目标 + 关键任务 + 产出物

这样模型更容易学会“稳定输出”,而不是写成散文。



7. 闭环:bad_rows 不是垃圾,是下一轮训练的宝藏

建议你把失败样本分桶,比如:

  • json_parse_fail:格式炸了
  • out_of_candidates:越界
  • missing_field:字段不齐
  • hallucinated_course:编课程/老师
  • weak_reason:理由空泛不引用信息

每一桶失败样本,都能对应一个“补数据策略”:

  • 格式炸 → 加更多“严格 JSON”样本 + 加反例警告
  • 越界 → 在 instruction 强化 + 给更多“候选列表很长”的样本
  • 理由空泛 → 在输入中突出课程 brief + 强制理由引用字段

这套闭环跑 2~3 轮,你会明显看到:
可解析率上升、越界率下降、拒答更合理



8. 给你一条可直接用的样本(正例 + 拒答例)

正例(示意)

  • input:user_profile + candidates(包含课程名/teacher/direction/brief)
  • output:只选 candidates 内 goods_id,并引用课程字段写理由,给 4 周计划

拒答例(示意)

  • input:candidates 为空或只有 1 门但规则要求 3 门
  • output:selected 为空,给追问与下一步(例如“补充学习目标/风险偏好/每周时间”)
你可以把“示意样本”换成你真实课程库字段,文章就更有说服力。


结语:SFT 数据工程的本质,是把“业务规则”变成“可学习的监督信号”

大模型天然会“发挥”,而业务系统需要“可控”。SFT 数据工程做的事情就是:

  • 让输入提供足够证据
  • 让输出满足严格结构
  • 让每条样本都能被程序判对错
  • 让失败样本进入闭环持续修正

当你把这件事做扎实,微调只是水到渠成。