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

> 作者：晨旭｜发布：2025-12-26｜系列：动手落地AI
> 来源：https://chenxu.xin/writing/lora-legal-finetune-data

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

---

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

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

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

我看着满屏的 `System`、`User`、`Assistant` 标签，心中满是疑惑：**模型真正「见」到的数据，到底是什么样的呢？**

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

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

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

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

第一遍，目标非常简单：跑通。

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

### 1、数据变身



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

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

```json
[
  {
    "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 下拉框，在代码里变成了一行参数：

```yaml
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 列表，拼接成了一个超长的字符串：

```text
<|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 模式下完全忽略的一步：

```python
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 的官方模板，自己造一种格式呢？

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

```python
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

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

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

**第二层：渲染层** → Apply Chat Template

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

**第三层：数据的物理形态（Model Readable）** → Raw Text / Token IDs

```text
<|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 中忽略计算的标记）：

```python
{
  "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: human` 和 `from: 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。（把握较大）
  永久链接：https://chenxu.xin/writing/lora-legal-finetune-data#no-magic-template-just-string
- 微调数据有三层形态：语义层（ShareGPT / Messages，给人和框架看）、渲染层（Chat Template，Jinja2 引擎负责翻译）、物理层（Raw Text 与 Token IDs，模型真正读到的东西）。搞混哪一层，格式问题就永远说不清。（判断 · 把握较大）
  永久链接：https://chenxu.xin/writing/lora-legal-finetune-data#data-has-three-layers
- 完全可以抛弃官方模板自己造格式，只要保证「训练时喂的格式」和「推理时问的格式」一致就行。就像教鹦鹉说话，教 Hello 还是教你好都行，重复够多、规则够统一就能学会。（把握较大）
  永久链接：https://chenxu.xin/writing/lora-legal-finetune-data#template-can-be-arbitrary
- 手写也能跑，但框架真正的价值在两个容易翻车的细节：自动生成 Loss 的 Mask 掩码（把用户提问位置填 -100），以及自动补上该模型专属的 EOS 停止符。索引算错一位，模型就会把指令当答案背诵。（判断 · 把握较大）
  永久链接：https://chenxu.xin/writing/lora-legal-finetune-data#framework-value-is-mask-and-eos
- 用框架最大的工程意义是解耦：ShareGPT 保存数据逻辑，Template 保存模型实现。换模型时数据文件一行都不用动，只改 template: qwen 为 template: llama3。（判断）
  永久链接：https://chenxu.xin/writing/lora-legal-finetune-data#framework-decouples-data-from-model

---

本文出自晨光里的AI（https://chenxu.xin），作者晨旭。
引用时请保留来源链接。文中标注「判断」「假设」的部分是作者的个人看法，不是事实。
