晨光里的AI

动手落地AI

LoRA 微调法律大模型:模型最终「见」到的是什么样的数据?

把同一个 Qwen 法律模型微调项目跑三遍——WebUI、CLI Notebook、无框架手写——沿着数据从 JSON 到 Token ID 的完整流转,回答一个最基础的问题:模型到底看到了什么。

Written by 晨旭发布于 约 9 分钟同步发布

TL;DR

刚开始接触微调实战,第一步就被数据格式拦住了:LLaMA-Factory 要 ShareGPT 格式的 JSON, PyTorch 原生代码要自己写 Template 拼字符串,满屏的 System / User / Assistant 标签背后,模型真正见到的到底是什么? 这篇是把同一个项目由浅入深跑三遍之后的复盘:WebUI 先跑通,CLI 换成可复现的 yaml,最后拆掉框架自己拼数据流。 结论很朴素——所谓神秘模板只是字符串拼接,Chat Template 是渲染层,模型最终只认 Token ID 序列。 但框架依然值得用,因为它替你处理了 Mask 掩码、EOS 停止符和数据与模型的解耦。

目录

如果是刚开始接触大模型微调实战,不知道你是否会和我一样,第一步就被数据格式给拦住了。

我遇到的场景是这样的:我想学习微调一个法律垂直领域的 Qwen 模型。起初,我以为这很简单,准备好问答对喂给模型不就好了吗?但现实却不是这样:

  • LLaMA-Factory 说:你要把数据转成 ShareGPT 格式的 JSON。
  • PyTorch 原生代码说:你要自己写一个 Template 把它们拼成字符串。

我看着满屏的 SystemUserAssistant 标签,心中满是疑惑:模型真正「见」到的数据,到底是什么样的呢?

这篇文章,是我把同一个 Qwen 法律大模型微调项目,由浅入深跑了三遍之后的数据格式复盘:

  • 第一遍:WebUI 模式(完全按教程点点点,先跑通)
  • 第二遍:CLI Notebook 模式(把点击变成代码,实现工程复现)
  • 第三遍:无框架模式(拆掉 LLaMA-Factory 黑盒,自己拼数据流)

案例可能比较简单,但这个小案例帮助我理清了关于微调数据的流转过程,之后再去深入学习微调的时候,思路更清晰了些。

一、WebUI 模式:模板是这样生效的

第一遍,目标非常简单:跑通。

这个法律大模型项目是使用 LoRA 指令微调,数据是开源社区的 DISC-Law-SFT 法律数据集,目标是微调 Qwen-3/2.5 模型,让它从一个通用助手变成一个懂法的「法律顾问」。

1、数据变身

原始的法律数据是一个标准的 JSONL 文件,每一行有一个 input(用户的问题)和一个 output(法律依据和回答)。

但是对于 LLaMA-Factory 框架来说,不能直接给模型喂这种格式的数据进行微调。必须写一个脚本,把它转换成框架喜欢的 ShareGPT 格式:

[
{
"conversations": [
{ "from": "human", "value": "user instruction" },
{ "from": "gpt", "value": "model response" }
],
"system": "system prompt (optional)",
"tools": "tool description (optional)"
}
]

此时 input 变成了 human 的 value,output 变成了 gpt 的 value。

当时我只是觉得这是多此一举:为什么不直接读 input / output?非要包一层 conversations 列表呢?

2、下拉框里的秘密

接着,我在 WebUI 界面上进行配置的时候,最让我困惑的一步来了:在 Chat Template(对话模板) 这一栏,项目要求必须要选择 qwen

我当时想:「这个选项是啥意义呢?我不是已经转换好数据了吗?」

但我还是先照做了。点击开始训练,Loss 下降,模型训练成功。

这一遍跑完,虽然做出了模型,但对于数据,我的心里还是个黑盒:我输入的数据是 JSON 对象,模型见到的数据就是这样的吗?刚刚选择的 Chat Template 是什么意思呢?

二、CLI Notebook 模式:拥抱工程化

于是我想着是不是用代码跑一遍会更清晰一点。并且第一遍的 WebUI 虽然直观,但有一个致命缺点:不可复现。过了两天,我可能就会忘了当时学习率是填了 5e-5 还是 2e-4。

于是,我决定用 LLaMA-Factory CLI 把流程重写一遍。这一遍,不再依赖 UI 界面的选择、点击,而是把所有配置写进了 yaml 文件。

我发现,WebUI 里的那个 Chat Template 下拉框,在代码里变成了一行参数:

template: qwen
dataset_formatting: sharegpt

这一遍最大的收获是「工程复现能力」:

  • 把数据处理脚本集成到了 Notebook 里,一键运行就能生成 train.json,并能逐步查看中间产物。
  • 把训练参数固化成了代码,能更清楚知道各个参数的真面目。无论之后换哪台服务器,只要运行这个 Notebook,结果都是一样的,并且方便项目迁移。

但这依然没有解决我最开始的疑问:模型到底「见」到的是什么样的数据?CLI 依然在调用 LLaMA-Factory 的内部函数,template: qwen 依然是个黑盒在后台悄悄工作。

三、无框架模式:看见本质

于是我决定不用 LLaMA-Factory,从另一个手写微调逻辑的 Notebook 项目迁移到这个项目中。

这一步是揭开秘密的时刻。我发现,之前以为的清洗数据,仅仅是冰山一角。要想真正理解模型是怎么学习的,需要看清数据从「文本到向量」的每一步转变。

1、并没有「神秘模板」,只有「字符串拼接」

我查找了关于 Qwen 的 Chat Template 原型资料。原来,当我在 WebUI 里选择 qwen 时,框架在后台偷偷帮我把 ShareGPT 里的 JSON 列表,拼接成了一个超长的字符串:

<|im_start|>user
请解释什么是合同的要约与承诺?<|im_end|>
<|im_start|>assistant
要约是……承诺是……<|im_end|>
<|endoftext|>

其中 <|im_start|><|im_end|> 只是特殊的占位符,告诉模型这句话哪里开始、哪里结束。

模型根本看不懂 JSON 对象,也看不懂 from: human 这种 key。它只认识这一长串拼好的纯文本。

2、Tokenizer:人类语言与机器语言的「翻译官」

但是,显卡是不认识中文汉字的,也不认识 <|im_start|> 这种符号。这时候 Tokenizer(分词器)就起到作用了。这也是我在 WebUI 模式下完全忽略的一步:

input_ids = tokenizer.encode(text)

它把上面的长文本切碎,并在字典里查找对应的编号(ID)。比如「请解释」变成了 ID 10567,特殊的 <|im_start|> 变成了 ID 151644。最终,数据变成了这样一串整数列表:[151644, 872, 356, ... 151645]

但,模型真的只看数字吗?

在工程代码里,数据处理到「整数列表」这一步确实就结束了。但在模型内部,还有一个极其迅速的转换过程:Embedding(词向量化)。模型手里拿着这些 ID(比如 151644),去查一张巨大的字典表,把每个 ID 换成了一组包含几千个浮点数的向量(比如 [0.012, -0.98, 0.33...])。

  • ID 只是索引(像图书馆的索书号)
  • Embedding 才是内容(像书本里的知识)

但对于做微调数据处理来说,只要把数据正确变成了 ID,剩下的查表工作交给显卡自动完成就好了。

3、如果想换一个「花样」呢?

既然看透了本质——模型只关心它见到的 Token 序列是否符合预测规律——那是不是意味着,我可以完全抛弃 Qwen 的官方模板,自己造一种格式呢?

当然可以。只要保证「训练时喂给它的格式」和「推理时问它的格式」保持一致就行。在无框架模式下,我尝试抛弃了所有模板库,手写一个最朴素的拼接函数:

def formatting_func(example):
# 手动定义一种简单的「指令-回复」格式,完全不用 Qwen 的特殊符号
text = f"### Instruction:\n{example['instruction']}\n\n### Response:\n{example['output']}"
return text

这里没有复杂的 <|im_start|>,只有很直白的 ### Instruction:。那么模型认识这个吗?它当然认识。只要把这串文本 Token 化,再告诉模型「### Response: 后面的是要学习的答案」,它就能照样学会。

这就像教鹦鹉说话,你可以教它「Hello」,也可以教它「你好」,只要你重复的次数够多,规则够统一,它都能学会。

总结一下:数据的三层模型

第一层:数据的语义形态(Human Readable) → ShareGPT / Messages

[{"from": "user", "value": "..."}]

本质:这是给人看的,也是给框架看的。它定义了「谁说了什么」,但还没有定义「怎么拼成字符串」。

第二层:渲染层 → Apply Chat Template

核心逻辑是 Jinja2 模板引擎。本质上这是一个翻译官:如果用 LLaMA-Factory,那么框架就是这个翻译官(根据所选的 qwen 选项);如果用原生 Trainer,那么手写代码的人就是翻译官(formatting_func 函数)。

第三层:数据的物理形态(Model Readable) → Raw Text / Token IDs

<|im_start|>user\n你好<|im_end|>\n...
[151644, 872, 356, ... 151645]

本质:这是模型真正看进去的东西。模型不关心你用什么框架,它只关心它收到的 Token 序列长什么样。

既然手写也能跑,为什么还要用 LLaMA-Factory?

1、Mask 机制

在 SFT(有监督微调)中,有一个核心原则:模型不应该学习「用户的提问」,只应该学习「助手的回答」。

这意味着,我们给模型的虽然是一整段对话,但在计算 Loss 时,必须把 User 部分遮住。在代码层面,这需要生成一个和 input_ids 等长的 labels 列表,把不需要学习的 Token 位置全部填为 -100(PyTorch 中忽略计算的标记):

{
"input_ids": [151644, 101, ... (用户问题) ... 151645, 151644, 202 ... (模型回答) ... 151645],
"labels": [-100, -100, ... (-100) ... -100, 151644, 202 ... (模型回答) ... 151645]
}
  • 用户提问区:Label 全被设为 -100,在 PyTorch 计算 Loss 时会被自动忽略
  • 模型回答区:Label 对应原本的 Token ID

想象一下多轮对话(User → Bot → User → Bot)中,需要精准计算每一个字符的起止索引:第 0 到 50 个 token 是提问(设为 -100),第 51 到 120 个是回答(保留),第 121 到 150 个又是提问(设为 -100)……只要算错一个索引,模型就会把用户的指令当成答案背诵下来,导致逻辑错乱。

LLaMA-Factory 会自动解析 ShareGPT 中的 from: humanfrom: gpt 标签,在拼接字符串的同时自动生成完美的 Mask 掩码。我们只需要关注内容本身,不需要关心索引。

2、避免模型停不下来

每个模型都有自己的结束符(EOS Token):Qwen 的结束符是 <|im_end|>,Llama 3 的是 <|eot_id|>

如果在手动拼接字符串时,不小心漏掉了结尾的这个特殊符号,或者用错了符号,训练出来的模型就会变成一个话痨。因为它微调时没学到「什么时候该闭嘴」,所以在推理时,它说完答案后会继续自言自语,直到显存爆掉或达到最大生成长度。

框架会自动读取 tokenizer_config.json 中的配置,保证每一个样本的末尾都正确添加了该模型的专属停止符。

3、数据与模型的解耦

这是工程化思维的体现:

  • ShareGPT(JSON) 保存的是数据逻辑,定义了「谁说了什么」
  • Template(Jinja2) 保存的是模型实现,定义了「这句话怎么拼」

使用框架的最大意义在于解耦。比如今天用 Qwen,明天想换 Llama 3 试试效果。手写模式需要去代码里把所有的 <|im_start|> 替换成 <|begin_of_text|>,甚至要重写整个拼接逻辑,改动量大且容易遗漏。而框架可以让数据文件一行都不用动,只需要在训练参数里把 template: qwen 改成 template: llama3

最后

写到这里,觉得好像所谓的黑科技,拆解到最后,往往都是最朴素的字符串拼接和矩阵运算。

想到一句古话:「万物之始,大道至简,衍化至繁。」

好像确实如此。

核心结论

标注「判断」「假设」的是我的看法而非事实;标注「已推翻」的保留在这里,不删除。

  • 所谓「神秘模板」并不存在。在 WebUI 里选择 qwen 时,框架只是在后台把 ShareGPT 的 JSON 列表拼接成了一个带特殊占位符的超长字符串。模型看不懂 JSON,也看不懂 from: human 这种 key。#

    把握较大

  • 微调数据有三层形态:语义层(ShareGPT / Messages,给人和框架看)、渲染层(Chat Template,Jinja2 引擎负责翻译)、物理层(Raw Text 与 Token IDs,模型真正读到的东西)。搞混哪一层,格式问题就永远说不清。#

    判断 · 把握较大

  • 完全可以抛弃官方模板自己造格式,只要保证「训练时喂的格式」和「推理时问的格式」一致就行。就像教鹦鹉说话,教 Hello 还是教你好都行,重复够多、规则够统一就能学会。#

    把握较大

  • 手写也能跑,但框架真正的价值在两个容易翻车的细节:自动生成 Loss 的 Mask 掩码(把用户提问位置填 -100),以及自动补上该模型专属的 EOS 停止符。索引算错一位,模型就会把指令当答案背诵。#

    判断 · 把握较大

  • 用框架最大的工程意义是解耦:ShareGPT 保存数据逻辑,Template 保存模型实现。换模型时数据文件一行都不用动,只改 template: qwen 为 template: llama3。#

    判断

引用本文

晨旭,《LoRA 微调法律大模型:模型最终「见」到的是什么样的数据?》,晨光里的AI,2025-12-26

[晨旭:《LoRA 微调法律大模型:模型最终「见」到的是什么样的数据?》](https://chenxu.xin/writing/lora-legal-finetune-data)

带进你的 AI 继续追问

这篇文章有一份干净的 Markdown 原文,可以直接交给任何模型读,不用复制粘贴。