JSONL 数据格式、提交与轮询:微软生成式 AI 入门课第 18 课微调

2026-08-18

你大概率是这么走到微调这一步的:写了很长的 system prompt,又在每次请求里塞三五个示例,输出格式才勉强稳住。塞例子这件事有两个绕不过去的限制——18-fine-tuning/README.md 在讲 few-shot 的局限时把它写得很直白:模型的 token 限制会卡住你能给的示例条数,而每次请求都带着这些示例又会推高 token 成本。微调这一课要解决的就是这个:把示例从 prompt 里搬到训练集里去。

这篇不复述课程正文(仓库 translations/ 下有各语种译本,简体中文那一份在 translations/zh-CN/,要读原文去那儿),只盯着作业那个 notebook 里真正要动手的三段:训练文件怎么写、任务怎么提交和轮询、以及 RESOURCES.md 那张表该怎么用。对应文件是 18-fine-tuning/python/openai/oai-assignment.ipynb,同目录下还有 training-data.jsonlvalidation-data.jsonl 两个样本文件。

先把 provider 路线搞清楚

这里有个第一次读会愣一下的地方。按 00-course-setup/03-providers.md 的约定,作业文件名里的标签决定它需要哪套凭据:oai 要 OpenAI 的 endpoint 和 key,aoai 要 Azure OpenAI 的,githubmodels 要 Microsoft Foundry Models 的(同一份文档写明 GitHub Models 将于 2026 年 7 月底退役)。可这一课的文件叫 oai-assignment.ipynb,代码里读的却是 AZURE_OPENAI_API_KEYAZURE_OPENAI_ENDPOINT

把 notebook 开头那段说明和代码放在一起看就明白了:它用的是 OpenAI Python SDK v1,但把 base_url 指到 Foundry 资源的 /openai/v1/ 端点上,notebook 自述「同一段代码只改 client 构造就能跑在 OpenAI 平台上」。文件名标签和实际读的环境变量对不上,这是我们把两处摆在一起看到的事实,仓库里没有找到解释这处命名的说明。你要做的是:照代码里实际读的变量名去填 .env,别照文件名猜。

前置条件(这一段别跳)

  • Python 版本:仓库根目录 .python-version 写的是 3.12.10(这是仓库当前的固定值,随仓库更新可能变动,动手前自己去这个文件看一眼)。
  • 依赖:requirements.txt 里是 openai>=1.12.0python-dotenv==1.2.2;notebook 自己的前置条件写的是 pip install "openai>=1.0" python-dotenv。两处下限不一样,按仓库 requirements.txt 那个更严的来。
  • 权限:notebook 的 Prerequisites 明确要求 Microsoft Foundry 项目上的 Foundry Owner 角色,理由是微调和部署都需要。这一条在真实环境里最容易卡住——权限不够时前面的代码都能跑,卡在建任务那一步。
  • 环境变量:根目录 .env.copy 是模板,00-course-setup/03-providers.md 给的动作是 cp .env.copy .env 之后填值,.env 已经在 .gitignore 里。这一课要用到的是 AZURE_OPENAI_API_KEYAZURE_OPENAI_ENDPOINT。模板里这两项本来就是 <add your Foundry resource key here> 这样的占位串,往外贴配置时保持占位形式即可——这是通用做法,不是这门课的官方要求。
  • 虚拟环境的两侧写法,00-course-setup/02-setup-local.md 分开给了:macOS / Linux 是 source .venv/bin/activate,Windows PowerShell 是 .\.venv\Scripts\activate

训练文件:一行一条 messages

样本文件里每一行是一个完整 JSON 对象,顶层只有 messages 一个键,里面是 system / user / assistant 三条消息。这是 training-data.jsonl 里的第一行,原样抄自仓库:

{ "messages": [{"role": "system", "content": "Elle is a factual chatbot that answers questions about elements in the periodic table with a limerick"}, {"role": "user", "content": "Tell me about Gallium"}, {"role": "assistant", "content": "Gallium, oh gallium, so light - Melts in your hand, oh what a sight - At 86 degrees - Its liquid with ease - And in semiconductors, it's out of sight"}]}

这条元素周期表打油诗的内容是仓库里的示例值,用来演示格式,不是什么推荐配置。有几点是格式本身的硬要求,notebook 里都点了名:

  • 每条记录必须写在一行里,不能像格式化 JSON 那样折行。这是 JSONL 的定义决定的,notebook 特意加粗强调了一次,说明确实有人在这里翻过车。
  • 这一课用的是单轮格式。notebook 说明,如果你要训练多轮对话内容,得改用多轮示例格式,那种格式里带一个 weight 参数,用来标记哪些消息参与微调、哪些不参与。多轮格式的具体写法 notebook 没有展开,它指向的是 OpenAI 官方文档。
  • system 那条消息在训练集里长什么样,推理时就得原样再给一遍。README 的最佳实践里单列了这一条,notebook 最后测试环节又重复了一次:换一个 system message,行为就会变。

关于样本量:notebook 正文自称这个 full sample set 是 10 条,我数了一下 training-data.jsonl 确实是 10 行,validation-data.jsonl 是 5 行。notebook 同时说明这是为了让流程跑得完的刻意简化,真实场景需要多得多的样本。README 的最佳实践给了一个「先小规模验证再扩」的顺序,也强调了质量优先于数量、低质量样本要剔掉。

验证集不是可选项。README 把「始终附带 validation 文件」写进了格式要求那一条,理由是有了它平台才会报验证损失,你才看得见过拟合。

提交与轮询:这条链路上有哪些调用

上传两个文件走的是 Files API,purpose 固定为 "fine-tune"

training_response = client.files.create(
    file=open("./training-data.jsonl", "rb"), purpose="fine-tune"
)
validation_response = client.files.create(
    file=open("./validation-data.jsonl", "rb"), purpose="fine-tune"
)

training_file_id = training_response.id
validation_file_id = validation_response.id

注意路径是相对的 ./training-data.jsonl,也就是说 notebook 得在它自己那个目录下运行,从仓库根目录起 kernel 会找不到文件。

建任务这一步的参数,notebook 里给的是:

job = client.fine_tuning.jobs.create(
    model="gpt-4.1-mini",              # base model to fine-tune
    training_file=training_file_id,
    validation_file=validation_file_id,
    suffix="elements-limerick",        # helps you identify the resulting model
    seed=105,                          # makes the run reproducible
    extra_body={"trainingType": "GlobalStandard"},
)

modelsuffixseedtrainingType 的取值都是仓库里的示例值。suffix 按注释是用来在结果模型名里认出这次训练,seed 按注释是让这次运行可复现。注意 trainingType 不是直接写在参数位上,而是塞进 extra_body 里的;仓库里没有找到关于这处写法的解释。notebook 的注释说 "GlobalStandard" 是推荐档,需要区域数据驻留就换 "Standard",做便宜的实验用 "Developer"。README 那边把训练层分成三档:Standard 在资源所在区域训练、保证数据驻留;Global 会把数据与权重复制到训练区域;Developer 用的是空闲容量,没有延迟与 SLA 保证,作业可能被抢占再恢复。数据不能出区域的场景就别用后两档。顺带记一笔:README 里这一档写作 Global,代码里传的字符串是 GlobalStandard,两处的字面不一样,仓库里没有找到说明这处差异的文字,照代码里的字符串写就是了。

训练技术也是三种,README 建议从 SFT 起步:SFT 用输入/输出成对样本训练,DPO 用「更偏好 / 不偏好」的响应对做对齐,RFT 靠 graders 给出的奖励信号优化,README 直说 RFT 需要更多 ML 经验。这一课的作业走的是 SFT。

轮询侧,notebook 把 client.fine_tuning.jobs 上能用的方法列了出来:list(limit=<n>) 列最近的任务、retrieve(<job_id>) 取单个任务详情、cancel(<job_id>) 取消、list_events(fine_tuning_job_id=<job_id>, limit=<n>) 拉事件流、checkpoints.list(<job_id>) 列出可部署的 checkpoint。实际写起来是这样:

response = client.fine_tuning.jobs.retrieve(job_id)

print("Job ID:", response.id)
print("Status:", response.status)
print("Trained Tokens:", response.trained_tokens)

事件流那一段 notebook 的做法是取出 response.dataevents.reverse() 再逐条打印 event.message,并注明要反复重跑这个 cell 直到看见 The job has successfully completed。任务刚提交时平台会先校验训练文件格式,这一步过不去后面根本不会开始训练。

以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。

边界:哪些地方文档没兜底

  • BOM 这条要求和仓库自带样本对不上。 README 的格式要求原文是训练与验证文件必须是 JSONL、UTF-8 with a BOM、且有大小上限(具体数值是平台限制,会变,以官方文档为准)。但我按字节读了仓库里的 training-data.jsonlvalidation-data.jsonl,两个文件开头都没有 BOM 字节。这两处摆在一起就是对不上的,仓库里没有找到关于这处差异的说明。要不要给你自己的文件加 BOM,请以平台官方文档为准;任何转换编码的具体做法都是通用做法,不是这门课的官方内容。
  • 不是所有模型都能微调。 README 列了几个可微调的基座模型,同时明确要求「始终去查当前的 fine-tuning models 列表」以确认支持的方法、区域和可用性。这份清单是会变的,别照文章里的名字硬填。
  • 持续微调有适用范围。 README 写的是可以把已经微调过的模型当基座再微调一轮,并括注了「supported for OpenAI models」——不是所有模型都吃这一套。
  • 超参没有给默认值。 notebook 讲监控指标那一段只说训练和验证曲线一旦分叉可能是过拟合,可以试更少的 epoch 或更小的 learning-rate multiplier;这两个超参具体默认是多少,仓库里没有找到说明。
  • Azure 侧必须先部署再调用。 notebook 明确写了,OpenAI 平台上可以直接拿微调后的模型 id 调用,Microsoft Foundry 上得先把模型部署出来,之后代码里传的是部署名而不是模型 id。部署好的自定义模型是按小时计费的,长期闲置的部署会被清理,具体规则以官方文档为准。
  • 作业本身的定位。 README 的 Assignment 一节写明:这些 tutorial 可能会在本仓库里复刻一份 Jupyter Notebook,但仅供参考,请直接使用原始来源以获得最新版本。

怎么确认这一趟走对了

按 notebook 的顺序,有四个可观察的落点:上传后两个文件 ID 都打印出来了;retrieve 返回的 status 会随任务推进变化,trained_tokens 有值;事件流里出现成功完成那句话;checkpoints.list(job_id) 能列出 fine_tuned_model_checkpointstep_number。指标侧 notebook 点名了四个:train_lossfull_valid_loss 应该往下走,train_mean_token_accuracyfull_valid_mean_token_accuracy 应该往上走。

最后一步是拿部署名调 Responses API:

deployment_name = "elements-limerick"  # the name you gave the deployment in Foundry

completion = client.responses.create(
    model=deployment_name,
    input=[
        {"role": "system", "content": "You are Elle, a factual chatbot that answers questions about elements in the periodic table with a limerick"},
        {"role": "user", "content": "Tell me about Strontium"},
    ],
    store=False,
)
print(completion.output_text)

notebook 建议在 Foundry playground 里把微调后的模型和基座模型并排放着对比,且用训练时那条 system message。这里再提醒一遍:store=False 是仓库示例里的写法,改不改要看你自己对请求留存的要求。

RESOURCES.md 该怎么翻

18-fine-tuning/RESOURCES.md 分成 Primary 和 Secondary 两节,不是随手堆的链接墙。Primary 那张表里,概念侧是 OpenAI 的 fine-tuning 指南和 Foundry 的「何时该微调」,流程侧是「Customize a model with fine-tuning」这篇端到端 how-to(数据准备、训练、checkpoint、部署都在里面),另外单列了持续微调、带工具(函数)调用样本的微调、可微调模型清单、以及技术与模态的总览。

Secondary 那节里对本文这条落点最相关的是 OpenAI Cookbook 的「Data preparation and analysis for chat model fine-tuning」——RESOURCES.md 对它的描述是:在微调前预处理并分析对话数据集,检查格式错误、给出基本统计、估算 token 数。你要是被格式问题卡住,先翻这一篇比盯着报错猜要快。Secondary 里另有 Hugging Face TRL、AutoTrain、Unsloth 三条,走的是开源模型本地微调的路子,和这一课云端提交任务的链路不是一回事,别混着看;同一节里还有 RAG 场景微调与实验跟踪的条目,属于另一个方向的延伸。


本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码, 因此不涉及运行结果、耗时与生成质量的任何描述。 该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。 文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。