{"schema_version":1,"id":"article:lora-legal-finetune-data","kind":"article","slug":"lora-legal-finetune-data","title":"LoRA 微调法律大模型：模型最终「见」到的是什么样的数据？","author":"晨旭","url":"https://chenxu.xin/writing/lora-legal-finetune-data","date":"2025-12-26","updated":"2025-12-26","description":"把同一个 Qwen 法律模型微调项目跑三遍——WebUI、CLI Notebook、无框架手写——沿着数据从 JSON 到 Token ID 的完整流转，回答一个最基础的问题：模型到底看到了什么。","position":null,"group":null,"series":"动手落地AI","tags":["hands-on","ai-pm"],"publication_status":"public","maturity":"evergreen","body_sha256":"0191817cb72e585b80f643b68b4099341ee43bc45036cc1c98ceedc1c61813f4","paragraph_rule":"src/lib/paragraphs.mjs@1","paragraphs":[{"id":"p1","kind":"paragraph","text":"如果是刚开始接触大模型微调实战，不知道你是否会和我一样，第一步就被数据格式给拦住了。","raw":"如果是刚开始接触大模型微调实战，不知道你是否会和我一样，第一步就被数据格式给拦住了。"},{"id":"p2","kind":"paragraph","text":"我遇到的场景是这样的：我想学习微调一个法律垂直领域的 Qwen 模型。起初，我以为这很简单，准备好问答对喂给模型不就好了吗？但现实却不是这样：","raw":"我遇到的场景是这样的：我想学习微调一个法律垂直领域的 Qwen 模型。起初，我以为这很简单，准备好问答对喂给模型不就好了吗？但现实却不是这样："},{"id":"p3","kind":"list","text":"- LLaMA-Factory 说：你要把数据转成 ShareGPT 格式的 JSON。\n- PyTorch 原生代码说：你要自己写一个 Template 把它们拼成字符串。","raw":"- **LLaMA-Factory 说**：你要把数据转成 ShareGPT 格式的 JSON。\n- **PyTorch 原生代码说**：你要自己写一个 Template 把它们拼成字符串。"},{"id":"p4","kind":"paragraph","text":"我看着满屏的 System、User、Assistant 标签，心中满是疑惑：模型真正「见」到的数据，到底是什么样的呢？","raw":"我看着满屏的 `System`、`User`、`Assistant` 标签，心中满是疑惑：**模型真正「见」到的数据，到底是什么样的呢？**"},{"id":"p5","kind":"paragraph","text":"这篇文章，是我把同一个 Qwen 法律大模型微调项目，由浅入深跑了三遍之后的数据格式复盘：","raw":"这篇文章，是我把同一个 Qwen 法律大模型微调项目，由浅入深跑了三遍之后的数据格式复盘："},{"id":"p6","kind":"list","text":"- 第一遍：WebUI 模式（完全按教程点点点，先跑通）\n- 第二遍：CLI Notebook 模式（把点击变成代码，实现工程复现）\n- 第三遍：无框架模式（拆掉 LLaMA-Factory 黑盒，自己拼数据流）","raw":"- 第一遍：WebUI 模式（完全按教程点点点，先跑通）\n- 第二遍：CLI Notebook 模式（把点击变成代码，实现工程复现）\n- 第三遍：无框架模式（拆掉 LLaMA-Factory 黑盒，自己拼数据流）"},{"id":"p7","kind":"paragraph","text":"案例可能比较简单，但这个小案例帮助我理清了关于微调数据的流转过程，之后再去深入学习微调的时候，思路更清晰了些。","raw":"案例可能比较简单，但这个小案例帮助我理清了关于微调数据的流转过程，之后再去深入学习微调的时候，思路更清晰了些。"},{"id":"p8","kind":"heading","text":"一、WebUI 模式：模板是这样生效的","raw":"## 一、WebUI 模式：模板是这样生效的"},{"id":"p9","kind":"paragraph","text":"第一遍，目标非常简单：跑通。","raw":"第一遍，目标非常简单：跑通。"},{"id":"p10","kind":"paragraph","text":"这个法律大模型项目是使用 LoRA 指令微调，数据是开源社区的 DISC-Law-SFT 法律数据集，目标是微调 Qwen-3/2.5 模型，让它从一个通用助手变成一个懂法的「法律顾问」。","raw":"这个法律大模型项目是使用 LoRA 指令微调，数据是开源社区的 DISC-Law-SFT 法律数据集，目标是微调 Qwen-3/2.5 模型，让它从一个通用助手变成一个懂法的「法律顾问」。"},{"id":"p11","kind":"heading","text":"1、数据变身","raw":"### 1、数据变身"},{"id":"p12","kind":"paragraph","text":"原始的法律数据是一个标准的 JSONL 文件，每一行有一个 input（用户的问题）和一个 output（法律依据和回答）。","raw":"原始的法律数据是一个标准的 JSONL 文件，每一行有一个 `input`（用户的问题）和一个 `output`（法律依据和回答）。"},{"id":"p13","kind":"paragraph","text":"但是对于 LLaMA-Factory 框架来说，不能直接给模型喂这种格式的数据进行微调。必须写一个脚本，把它转换成框架喜欢的 ShareGPT 格式：","raw":"但是对于 LLaMA-Factory 框架来说，不能直接给模型喂这种格式的数据进行微调。必须写一个脚本，把它转换成框架喜欢的 ShareGPT 格式："},{"id":"p14","kind":"code","text":"[\n  {\n    \"conversations\": [\n      { \"from\": \"human\", \"value\": \"user instruction\" },\n      { \"from\": \"gpt\",   \"value\": \"model response\" }\n    ],\n    \"system\": \"system prompt (optional)\",\n    \"tools\": \"tool description (optional)\"\n  }\n]","raw":"```json\n[\n  {\n    \"conversations\": [\n      { \"from\": \"human\", \"value\": \"user instruction\" },\n      { \"from\": \"gpt\",   \"value\": \"model response\" }\n    ],\n    \"system\": \"system prompt (optional)\",\n    \"tools\": \"tool description (optional)\"\n  }\n]\n```"},{"id":"p15","kind":"paragraph","text":"此时 input 变成了 human 的 value，output 变成了 gpt 的 value。","raw":"此时 `input` 变成了 human 的 value，`output` 变成了 gpt 的 value。"},{"id":"p16","kind":"paragraph","text":"当时我只是觉得这是多此一举：为什么不直接读 input / output？非要包一层 conversations 列表呢？","raw":"当时我只是觉得这是多此一举：为什么不直接读 input / output？非要包一层 `conversations` 列表呢？"},{"id":"p17","kind":"heading","text":"2、下拉框里的秘密","raw":"### 2、下拉框里的秘密"},{"id":"p18","kind":"paragraph","text":"接着，我在 WebUI 界面上进行配置的时候，最让我困惑的一步来了：在 Chat Template（对话模板） 这一栏，项目要求必须要选择 qwen。","raw":"接着，我在 WebUI 界面上进行配置的时候，最让我困惑的一步来了：在 **Chat Template（对话模板）** 这一栏，项目要求必须要选择 `qwen`。"},{"id":"p19","kind":"paragraph","text":"我当时想：「这个选项是啥意义呢？我不是已经转换好数据了吗？」","raw":"我当时想：「这个选项是啥意义呢？我不是已经转换好数据了吗？」"},{"id":"p20","kind":"paragraph","text":"但我还是先照做了。点击开始训练，Loss 下降，模型训练成功。","raw":"但我还是先照做了。点击开始训练，Loss 下降，模型训练成功。"},{"id":"p21","kind":"paragraph","text":"这一遍跑完，虽然做出了模型，但对于数据，我的心里还是个黑盒：我输入的数据是 JSON 对象，模型见到的数据就是这样的吗？刚刚选择的 Chat Template 是什么意思呢？","raw":"这一遍跑完，虽然做出了模型，但对于数据，我的心里还是个黑盒：我输入的数据是 JSON 对象，模型见到的数据就是这样的吗？刚刚选择的 Chat Template 是什么意思呢？"},{"id":"p22","kind":"heading","text":"二、CLI Notebook 模式：拥抱工程化","raw":"## 二、CLI Notebook 模式：拥抱工程化"},{"id":"p23","kind":"paragraph","text":"于是我想着是不是用代码跑一遍会更清晰一点。并且第一遍的 WebUI 虽然直观，但有一个致命缺点：不可复现。过了两天，我可能就会忘了当时学习率是填了 5e-5 还是 2e-4。","raw":"于是我想着是不是用代码跑一遍会更清晰一点。并且第一遍的 WebUI 虽然直观，但有一个致命缺点：**不可复现**。过了两天，我可能就会忘了当时学习率是填了 5e-5 还是 2e-4。"},{"id":"p24","kind":"paragraph","text":"于是，我决定用 LLaMA-Factory CLI 把流程重写一遍。这一遍，不再依赖 UI 界面的选择、点击，而是把所有配置写进了 yaml 文件。","raw":"于是，我决定用 LLaMA-Factory CLI 把流程重写一遍。这一遍，不再依赖 UI 界面的选择、点击，而是把所有配置写进了 yaml 文件。"},{"id":"p25","kind":"paragraph","text":"我发现，WebUI 里的那个 Chat Template 下拉框，在代码里变成了一行参数：","raw":"我发现，WebUI 里的那个 Chat Template 下拉框，在代码里变成了一行参数："},{"id":"p26","kind":"code","text":"template: qwen\ndataset_formatting: sharegpt","raw":"```yaml\ntemplate: qwen\ndataset_formatting: sharegpt\n```"},{"id":"p27","kind":"paragraph","text":"这一遍最大的收获是「工程复现能力」：","raw":"这一遍最大的收获是「工程复现能力」："},{"id":"p28","kind":"list","text":"- 把数据处理脚本集成到了 Notebook 里，一键运行就能生成 train.json，并能逐步查看中间产物。\n- 把训练参数固化成了代码，能更清楚知道各个参数的真面目。无论之后换哪台服务器，只要运行这个 Notebook，结果都是一样的，并且方便项目迁移。","raw":"- 把数据处理脚本集成到了 Notebook 里，一键运行就能生成 `train.json`，并能逐步查看中间产物。\n- 把训练参数固化成了代码，能更清楚知道各个参数的真面目。无论之后换哪台服务器，只要运行这个 Notebook，结果都是一样的，并且方便项目迁移。"},{"id":"p29","kind":"paragraph","text":"但这依然没有解决我最开始的疑问：模型到底「见」到的是什么样的数据？CLI 依然在调用 LLaMA-Factory 的内部函数，template: qwen 依然是个黑盒在后台悄悄工作。","raw":"但这依然没有解决我最开始的疑问：模型到底「见」到的是什么样的数据？CLI 依然在调用 LLaMA-Factory 的内部函数，`template: qwen` 依然是个黑盒在后台悄悄工作。"},{"id":"p30","kind":"heading","text":"三、无框架模式：看见本质","raw":"## 三、无框架模式：看见本质"},{"id":"p31","kind":"paragraph","text":"于是我决定不用 LLaMA-Factory，从另一个手写微调逻辑的 Notebook 项目迁移到这个项目中。","raw":"于是我决定不用 LLaMA-Factory，从另一个手写微调逻辑的 Notebook 项目迁移到这个项目中。"},{"id":"p32","kind":"paragraph","text":"这一步是揭开秘密的时刻。我发现，之前以为的清洗数据，仅仅是冰山一角。要想真正理解模型是怎么学习的，需要看清数据从「文本到向量」的每一步转变。","raw":"这一步是揭开秘密的时刻。我发现，之前以为的清洗数据，仅仅是冰山一角。要想真正理解模型是怎么学习的，需要看清数据从「文本到向量」的每一步转变。"},{"id":"p33","kind":"heading","text":"1、并没有「神秘模板」，只有「字符串拼接」","raw":"### 1、并没有「神秘模板」，只有「字符串拼接」"},{"id":"p34","kind":"paragraph","text":"我查找了关于 Qwen 的 Chat Template 原型资料。原来，当我在 WebUI 里选择 qwen 时，框架在后台偷偷帮我把 ShareGPT 里的 JSON 列表，拼接成了一个超长的字符串：","raw":"我查找了关于 Qwen 的 Chat Template 原型资料。原来，当我在 WebUI 里选择 `qwen` 时，框架在后台偷偷帮我把 ShareGPT 里的 JSON 列表，拼接成了一个超长的字符串："},{"id":"p35","kind":"code","text":"<|im_start|>user\n请解释什么是合同的要约与承诺？<|im_end|>\n<|im_start|>assistant\n要约是……承诺是……<|im_end|>\n<|endoftext|>","raw":"```text\n<|im_start|>user\n请解释什么是合同的要约与承诺？<|im_end|>\n<|im_start|>assistant\n要约是……承诺是……<|im_end|>\n<|endoftext|>\n```"},{"id":"p36","kind":"paragraph","text":"其中 <|im_start|> 和 <|im_end|> 只是特殊的占位符，告诉模型这句话哪里开始、哪里结束。","raw":"其中 `<|im_start|>` 和 `<|im_end|>` 只是特殊的占位符，告诉模型这句话哪里开始、哪里结束。"},{"id":"p37","kind":"paragraph","text":"模型根本看不懂 JSON 对象，也看不懂 from: human 这种 key。它只认识这一长串拼好的纯文本。","raw":"**模型根本看不懂 JSON 对象，也看不懂 `from: human` 这种 key。它只认识这一长串拼好的纯文本。**"},{"id":"p38","kind":"heading","text":"2、Tokenizer：人类语言与机器语言的「翻译官」","raw":"### 2、Tokenizer：人类语言与机器语言的「翻译官」"},{"id":"p39","kind":"paragraph","text":"但是，显卡是不认识中文汉字的，也不认识 <|im_start|> 这种符号。这时候 Tokenizer（分词器）就起到作用了。这也是我在 WebUI 模式下完全忽略的一步：","raw":"但是，显卡是不认识中文汉字的，也不认识 `<|im_start|>` 这种符号。这时候 Tokenizer（分词器）就起到作用了。这也是我在 WebUI 模式下完全忽略的一步："},{"id":"p40","kind":"code","text":"input_ids = tokenizer.encode(text)","raw":"```python\ninput_ids = tokenizer.encode(text)\n```"},{"id":"p41","kind":"paragraph","text":"它把上面的长文本切碎，并在字典里查找对应的编号（ID）。比如「请解释」变成了 ID 10567，特殊的 <|im_start|> 变成了 ID 151644。最终，数据变成了这样一串整数列表：[151644, 872, 356, ... 151645]。","raw":"它把上面的长文本切碎，并在字典里查找对应的编号（ID）。比如「请解释」变成了 ID `10567`，特殊的 `<|im_start|>` 变成了 ID `151644`。最终，数据变成了这样一串整数列表：`[151644, 872, 356, ... 151645]`。"},{"id":"p42","kind":"paragraph","text":"但，模型真的只看数字吗？","raw":"但，模型真的只看数字吗？"},{"id":"p43","kind":"paragraph","text":"在工程代码里，数据处理到「整数列表」这一步确实就结束了。但在模型内部，还有一个极其迅速的转换过程：Embedding（词向量化）。模型手里拿着这些 ID（比如 151644），去查一张巨大的字典表，把每个 ID 换成了一组包含几千个浮点数的向量（比如 [0.012, -0.98, 0.33...]）。","raw":"在工程代码里，数据处理到「整数列表」这一步确实就结束了。但在模型内部，还有一个极其迅速的转换过程：**Embedding（词向量化）**。模型手里拿着这些 ID（比如 `151644`），去查一张巨大的字典表，把每个 ID 换成了一组包含几千个浮点数的向量（比如 `[0.012, -0.98, 0.33...]`）。"},{"id":"p44","kind":"list","text":"- ID 只是索引（像图书馆的索书号）\n- Embedding 才是内容（像书本里的知识）","raw":"- ID 只是索引（像图书馆的索书号）\n- Embedding 才是内容（像书本里的知识）"},{"id":"p45","kind":"paragraph","text":"但对于做微调数据处理来说，只要把数据正确变成了 ID，剩下的查表工作交给显卡自动完成就好了。","raw":"但对于做微调数据处理来说，只要把数据正确变成了 ID，剩下的查表工作交给显卡自动完成就好了。"},{"id":"p46","kind":"heading","text":"3、如果想换一个「花样」呢？","raw":"### 3、如果想换一个「花样」呢？"},{"id":"p47","kind":"paragraph","text":"既然看透了本质——模型只关心它见到的 Token 序列是否符合预测规律——那是不是意味着，我可以完全抛弃 Qwen 的官方模板，自己造一种格式呢？","raw":"既然看透了本质——模型只关心它见到的 Token 序列是否符合预测规律——那是不是意味着，我可以完全抛弃 Qwen 的官方模板，自己造一种格式呢？"},{"id":"p48","kind":"paragraph","text":"当然可以。只要保证「训练时喂给它的格式」和「推理时问它的格式」保持一致就行。在无框架模式下，我尝试抛弃了所有模板库，手写一个最朴素的拼接函数：","raw":"当然可以。只要保证「训练时喂给它的格式」和「推理时问它的格式」保持一致就行。在无框架模式下，我尝试抛弃了所有模板库，手写一个最朴素的拼接函数："},{"id":"p49","kind":"code","text":"def formatting_func(example):\n    # 手动定义一种简单的「指令-回复」格式，完全不用 Qwen 的特殊符号\n    text = f\"### Instruction:\\n{example['instruction']}\\n\\n### Response:\\n{example['output']}\"\n    return text","raw":"```python\ndef formatting_func(example):\n    # 手动定义一种简单的「指令-回复」格式，完全不用 Qwen 的特殊符号\n    text = f\"### Instruction:\\n{example['instruction']}\\n\\n### Response:\\n{example['output']}\"\n    return text\n```"},{"id":"p50","kind":"paragraph","text":"这里没有复杂的 <|im_start|>，只有很直白的 ### Instruction:。那么模型认识这个吗？它当然认识。只要把这串文本 Token 化，再告诉模型「### Response: 后面的是要学习的答案」，它就能照样学会。","raw":"这里没有复杂的 `<|im_start|>`，只有很直白的 `### Instruction:`。那么模型认识这个吗？它当然认识。只要把这串文本 Token 化，再告诉模型「`### Response:` 后面的是要学习的答案」，它就能照样学会。"},{"id":"p51","kind":"paragraph","text":"这就像教鹦鹉说话，你可以教它「Hello」，也可以教它「你好」，只要你重复的次数够多，规则够统一，它都能学会。","raw":"这就像教鹦鹉说话，你可以教它「Hello」，也可以教它「你好」，只要你重复的次数够多，规则够统一，它都能学会。"},{"id":"p52","kind":"heading","text":"总结一下：数据的三层模型","raw":"### 总结一下：数据的三层模型"},{"id":"p53","kind":"paragraph","text":"第一层：数据的语义形态（Human Readable） → ShareGPT / Messages","raw":"**第一层：数据的语义形态（Human Readable）** → ShareGPT / Messages"},{"id":"p54","kind":"code","text":"[{\"from\": \"user\", \"value\": \"...\"}]","raw":"```json\n[{\"from\": \"user\", \"value\": \"...\"}]\n```"},{"id":"p55","kind":"paragraph","text":"本质：这是给人看的，也是给框架看的。它定义了「谁说了什么」，但还没有定义「怎么拼成字符串」。","raw":"本质：这是给人看的，也是给框架看的。它定义了「谁说了什么」，但还没有定义「怎么拼成字符串」。"},{"id":"p56","kind":"paragraph","text":"第二层：渲染层 → Apply Chat Template","raw":"**第二层：渲染层** → Apply Chat Template"},{"id":"p57","kind":"paragraph","text":"核心逻辑是 Jinja2 模板引擎。本质上这是一个翻译官：如果用 LLaMA-Factory，那么框架就是这个翻译官（根据所选的 qwen 选项）；如果用原生 Trainer，那么手写代码的人就是翻译官（formatting_func 函数）。","raw":"核心逻辑是 Jinja2 模板引擎。本质上这是一个翻译官：如果用 LLaMA-Factory，那么框架就是这个翻译官（根据所选的 `qwen` 选项）；如果用原生 Trainer，那么手写代码的人就是翻译官（`formatting_func` 函数）。"},{"id":"p58","kind":"paragraph","text":"第三层：数据的物理形态（Model Readable） → Raw Text / Token IDs","raw":"**第三层：数据的物理形态（Model Readable）** → Raw Text / Token IDs"},{"id":"p59","kind":"code","text":"<|im_start|>user\\n你好<|im_end|>\\n...\n[151644, 872, 356, ... 151645]","raw":"```text\n<|im_start|>user\\n你好<|im_end|>\\n...\n[151644, 872, 356, ... 151645]\n```"},{"id":"p60","kind":"paragraph","text":"本质：这是模型真正看进去的东西。模型不关心你用什么框架，它只关心它收到的 Token 序列长什么样。","raw":"本质：这是模型真正看进去的东西。模型不关心你用什么框架，它只关心它收到的 Token 序列长什么样。"},{"id":"p61","kind":"heading","text":"既然手写也能跑，为什么还要用 LLaMA-Factory？","raw":"## 既然手写也能跑，为什么还要用 LLaMA-Factory？"},{"id":"p62","kind":"heading","text":"1、Mask 机制","raw":"### 1、Mask 机制"},{"id":"p63","kind":"paragraph","text":"在 SFT（有监督微调）中，有一个核心原则：模型不应该学习「用户的提问」，只应该学习「助手的回答」。","raw":"在 SFT（有监督微调）中，有一个核心原则：**模型不应该学习「用户的提问」，只应该学习「助手的回答」。**"},{"id":"p64","kind":"paragraph","text":"这意味着，我们给模型的虽然是一整段对话，但在计算 Loss 时，必须把 User 部分遮住。在代码层面，这需要生成一个和 input_ids 等长的 labels 列表，把不需要学习的 Token 位置全部填为 -100（PyTorch 中忽略计算的标记）：","raw":"这意味着，我们给模型的虽然是一整段对话，但在计算 Loss 时，必须把 User 部分遮住。在代码层面，这需要生成一个和 `input_ids` 等长的 `labels` 列表，把不需要学习的 Token 位置全部填为 `-100`（PyTorch 中忽略计算的标记）："},{"id":"p65","kind":"code","text":"{\n  \"input_ids\": [151644, 101, ... (用户问题) ... 151645, 151644, 202 ... (模型回答) ... 151645],\n  \"labels\":    [-100,   -100, ... (-100)    ... -100,   151644, 202 ... (模型回答) ... 151645]\n}","raw":"```python\n{\n  \"input_ids\": [151644, 101, ... (用户问题) ... 151645, 151644, 202 ... (模型回答) ... 151645],\n  \"labels\":    [-100,   -100, ... (-100)    ... -100,   151644, 202 ... (模型回答) ... 151645]\n}\n```"},{"id":"p66","kind":"list","text":"- 用户提问区：Label 全被设为 -100，在 PyTorch 计算 Loss 时会被自动忽略\n- 模型回答区：Label 对应原本的 Token ID","raw":"- 用户提问区：Label 全被设为 -100，在 PyTorch 计算 Loss 时会被自动忽略\n- 模型回答区：Label 对应原本的 Token ID"},{"id":"p67","kind":"paragraph","text":"想象一下多轮对话（User → Bot → User → Bot）中，需要精准计算每一个字符的起止索引：第 0 到 50 个 token 是提问（设为 -100），第 51 到 120 个是回答（保留），第 121 到 150 个又是提问（设为 -100）……只要算错一个索引，模型就会把用户的指令当成答案背诵下来，导致逻辑错乱。","raw":"想象一下多轮对话（User → Bot → User → Bot）中，需要精准计算每一个字符的起止索引：第 0 到 50 个 token 是提问（设为 -100），第 51 到 120 个是回答（保留），第 121 到 150 个又是提问（设为 -100）……**只要算错一个索引，模型就会把用户的指令当成答案背诵下来，导致逻辑错乱。**"},{"id":"p68","kind":"paragraph","text":"LLaMA-Factory 会自动解析 ShareGPT 中的 from: human 和 from: gpt 标签，在拼接字符串的同时自动生成完美的 Mask 掩码。我们只需要关注内容本身，不需要关心索引。","raw":"LLaMA-Factory 会自动解析 ShareGPT 中的 `from: human` 和 `from: gpt` 标签，在拼接字符串的同时自动生成完美的 Mask 掩码。我们只需要关注内容本身，不需要关心索引。"},{"id":"p69","kind":"heading","text":"2、避免模型停不下来","raw":"### 2、避免模型停不下来"},{"id":"p70","kind":"paragraph","text":"每个模型都有自己的结束符（EOS Token）：Qwen 的结束符是 <|im_end|>，Llama 3 的是 <|eot_id|>。","raw":"每个模型都有自己的结束符（EOS Token）：Qwen 的结束符是 `<|im_end|>`，Llama 3 的是 `<|eot_id|>`。"},{"id":"p71","kind":"paragraph","text":"如果在手动拼接字符串时，不小心漏掉了结尾的这个特殊符号，或者用错了符号，训练出来的模型就会变成一个话痨。因为它微调时没学到「什么时候该闭嘴」，所以在推理时，它说完答案后会继续自言自语，直到显存爆掉或达到最大生成长度。","raw":"如果在手动拼接字符串时，不小心漏掉了结尾的这个特殊符号，或者用错了符号，训练出来的模型就会变成一个话痨。因为它微调时没学到「什么时候该闭嘴」，所以在推理时，它说完答案后会继续自言自语，直到显存爆掉或达到最大生成长度。"},{"id":"p72","kind":"paragraph","text":"框架会自动读取 tokenizer_config.json 中的配置，保证每一个样本的末尾都正确添加了该模型的专属停止符。","raw":"框架会自动读取 `tokenizer_config.json` 中的配置，保证每一个样本的末尾都正确添加了该模型的专属停止符。"},{"id":"p73","kind":"heading","text":"3、数据与模型的解耦","raw":"### 3、数据与模型的解耦"},{"id":"p74","kind":"paragraph","text":"这是工程化思维的体现：","raw":"这是工程化思维的体现："},{"id":"p75","kind":"list","text":"- ShareGPT（JSON） 保存的是数据逻辑，定义了「谁说了什么」\n- Template（Jinja2） 保存的是模型实现，定义了「这句话怎么拼」","raw":"- **ShareGPT（JSON）** 保存的是数据逻辑，定义了「谁说了什么」\n- **Template（Jinja2）** 保存的是模型实现，定义了「这句话怎么拼」"},{"id":"p76","kind":"paragraph","text":"使用框架的最大意义在于解耦。比如今天用 Qwen，明天想换 Llama 3 试试效果。手写模式需要去代码里把所有的 <|im_start|> 替换成 <|begin_of_text|>，甚至要重写整个拼接逻辑，改动量大且容易遗漏。而框架可以让数据文件一行都不用动，只需要在训练参数里把 template: qwen 改成 template: llama3。","raw":"使用框架的最大意义在于解耦。比如今天用 Qwen，明天想换 Llama 3 试试效果。手写模式需要去代码里把所有的 `<|im_start|>` 替换成 `<|begin_of_text|>`，甚至要重写整个拼接逻辑，改动量大且容易遗漏。而框架可以让数据文件一行都不用动，只需要在训练参数里把 `template: qwen` 改成 `template: llama3`。"},{"id":"p77","kind":"heading","text":"最后","raw":"## 最后"},{"id":"p78","kind":"paragraph","text":"写到这里，觉得好像所谓的黑科技，拆解到最后，往往都是最朴素的字符串拼接和矩阵运算。","raw":"写到这里，觉得好像所谓的黑科技，拆解到最后，往往都是最朴素的字符串拼接和矩阵运算。"},{"id":"p79","kind":"paragraph","text":"想到一句古话：「万物之始，大道至简，衍化至繁。」","raw":"想到一句古话：「万物之始，大道至简，衍化至繁。」"},{"id":"p80","kind":"paragraph","text":"好像确实如此。","raw":"好像确实如此。"}],"authored_claims":[{"id":"no-magic-template-just-string","text":"所谓「神秘模板」并不存在。在 WebUI 里选择 qwen 时，框架只是在后台把 ShareGPT 的 JSON 列表拼接成了一个带特殊占位符的超长字符串。模型看不懂 JSON，也看不懂 from: human 这种 key。","stance":"fact","confidence":"high","supersedes":null,"note":null,"formed_at":"2025-12-26"},{"id":"data-has-three-layers","text":"微调数据有三层形态：语义层（ShareGPT / Messages，给人和框架看）、渲染层（Chat Template，Jinja2 引擎负责翻译）、物理层（Raw Text 与 Token IDs，模型真正读到的东西）。搞混哪一层，格式问题就永远说不清。","stance":"judgment","confidence":"high","supersedes":null,"note":null,"formed_at":"2025-12-26"},{"id":"template-can-be-arbitrary","text":"完全可以抛弃官方模板自己造格式，只要保证「训练时喂的格式」和「推理时问的格式」一致就行。就像教鹦鹉说话，教 Hello 还是教你好都行，重复够多、规则够统一就能学会。","stance":"fact","confidence":"high","supersedes":null,"note":null,"formed_at":"2025-12-26"},{"id":"framework-value-is-mask-and-eos","text":"手写也能跑，但框架真正的价值在两个容易翻车的细节：自动生成 Loss 的 Mask 掩码（把用户提问位置填 -100），以及自动补上该模型专属的 EOS 停止符。索引算错一位，模型就会把指令当答案背诵。","stance":"judgment","confidence":"high","supersedes":null,"note":null,"formed_at":"2025-12-26"},{"id":"framework-decouples-data-from-model","text":"用框架最大的工程意义是解耦：ShareGPT 保存数据逻辑，Template 保存模型实现。换模型时数据文件一行都不用动，只改 template: qwen 为 template: llama3。","stance":"judgment","confidence":null,"supersedes":null,"note":null,"formed_at":"2025-12-26"}],"claim_ids":["no-magic-template-just-string","data-has-three-layers","template-can-be-arbitrary","framework-value-is-mask-and-eos","framework-decouples-data-from-model"],"revision":"456abc0f849c4c1234d668dc5a3b31b79f4b975a313530e80989878a9364fbda"}