手把手教学: 从零到冒烟跑通的完整操作步骤见 基于千问的黑盒蒸馏操作教学文档

一、 课程核心知识点与理论背景

课程内容概览:

  1. 蒸馏的核心知识
  2. 蒸馏的环境准备
  3. 蒸馏的实战演练

核心概念与目标:

  • 本质:知识转移,将大模型(教师)的知识迁移到小模型(学生)。
  • 特点:参数量更小,计算更高效。
  • 应用:模型压缩,边缘计算,低功耗场景。
  • 技术路线:大模型 → 生成数据 → 微调 → 小模型。
  • 方法分类全量微调(所有参数都调,资源消耗大) 和 高效微调(如LoRA/QLoRA)。
  • 蒸馏类型:本 SOP 采用的是黑盒蒸馏(Data Distillation),即通过 API 获取教师模型文本输出作为训练数据,而非对齐教师 logits/隐层。

蒸馏为何“突然火了”?
① 精力放在了应用上;
② 强大的基模开源了;
③ DeepSeek 公布了蒸馏技术方案(OpenAI 支持蒸馏但未公开技术方案)。

大模型强大的推理能力有何作用?
① 构建知识图谱;
② 作为 Agent 智能中枢;
③ 创建高质量合成数据,训练更强大的基模。

蒸馏的流程概述:

  • 教师模型(如 DeepSeek-R1) → 生成软标签,创建高质量数据集 → 学生模型基于这些数据学习。
  • 注:DeepSeek 公布了一系列蒸馏模型,如 1.5B, 7B, 8B, 14B, 32B, 70B, 671B。

DeepSeek 公布的模型蒸馏过程(三轮训练):

  1. 第一轮(Instruction 模型): 原始 Base 模型 → (借助推理问答数据进行冷启动有监督微调/SFT) → Instruction 模型。
  2. 第二轮(初级推理模型): Instruction 模型 → (借助 CoT 数据(思维链数据)进行强化学习 RL) → 初级推理模型。
  3. 第三轮(终极推理模型): 初级推理模型 → (借助推理问答数据,第二次有监督微调 SFT) → 终极推理模型。
    注:SFT 全称 Supervised Fine-Tuning。

二、 微调工具与硬件资源评估

微调工具推荐:

  1. Unsloth:核心技术优势:① 显著提升微调效率;② 降低了硬件需求;③ 开源免费。
  2. LLaMA-Factory:① 广泛支持;② 高效的微调方法;③ 多模态任务支持;④ 实验监控;⑤ 运行速度快。
  3. MS-Swift(阿里系工具)。

微调资源与显存估算(大致的 x3 关系):

模型参数量 Freeze LoRA QLoRA(INT8) QLoRA(INT4)
7B 20 16 10 6
13B 40 32 20 12
30B 80 64 40 24
70B 200 160 80 48
110B 360 240 140 -

硬件最低配置推荐:

  • RTX 4090:24GB
  • RTX 4080:12GB
  • RTX 3060:<12GB
  • A100:40GB
  • L40:48GB
  • H100:80GB

云平台推荐:

  • 除 AutoDL (https://www.autodl.com/) 外,还可使用 魔搭社区 (https://www.modelscope.cn/)。

三、 环境准备与模型下载

网络加速(AutoDL 必做):访问 GitHub 或 Hugging Face 前,先执行:

source /etc/network_turbo

注意:开启后 pip 源等 GitHub/HF 资源可能变慢,建议仅在 clone/下载模型时使用。

1. 下载 LLaMA-Factory 并安装依赖

工作目录(实测): /root/autodl-tmp/Distill(原 SOP 写 /autodl-tmp/DeepSeek-R1-Distill,AutoDL 实际挂载点为 /root/autodl-tmp/

# 创建并进入工作目录
mkdir -p /root/autodl-tmp/Distill
cd /root/autodl-tmp/Distill

# 克隆 LLaMA-Factory 仓库(--depth 1 只下载最新版本)
source /etc/network_turbo
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git

# 进入 LLaMA-Factory 目录安装依赖
cd LLaMA-Factory
pip install -e ".[torch,metrics]"

实测问题与解决:

问题 现象 解决方案
torchaudio 版本冲突 OSError: libcudart.so.13: cannot open shared object file pip install 会拉取 torchaudio 2.11.0(需 CUDA 13),与系统 PyTorch 2.8.0+cu128 不兼容。执行:pip install "torchaudio==2.8.0" --index-url https://download.pytorch.org/whl/cu128
OMP 警告 libgomp: Invalid value for environment variable OMP_NUM_THREADS 不影响运行,可忽略;或 export OMP_NUM_THREADS=1

2. 下载开源模型 Qwen2.5-1.5B-Instruct

(注:Model ID 非常重要,需精确填写)
首先在魔搭社区搜索 Qwen2.5-1.5B-Instruct 获取可复制的精确 ID:Qwen/Qwen2.5-1.5B-Instruct

创建 download.py 并填入以下内容:

from modelscope import snapshot_download

model_id = "Qwen/Qwen2.5-1.5B-Instruct"
local_dir = "/root/autodl-tmp/Distill/models"
model_dir = snapshot_download(model_id, cache_dir=local_dir)
print(f"Model downloaded to: {model_dir}")

注:需先安装必要的依赖:pip install modelscope(LLaMA-Factory 安装时已包含)

实测下载路径:

/root/autodl-tmp/Distill/models/models/Qwen--Qwen2.5-1.5B-Instruct/snapshots/master

ModelScope 会在 cache_dir 下自动创建 models/Qwen--... 嵌套目录,配置训练时须使用完整绝对路径


四、 蒸馏数据集的生成与评估策略

核心思路: 让教师模型(DeepSeek-R1)回答这些数学/逻辑问题,然后使用这些高质量数据去教学生模型(1.5B)。

NuminaMath-CoT
只取 problem

DeepSeek-R1 API
deepseek-reasoner

DeepSeek-V3 裁判
deepseek-chat

Distil-data-17k-train.arrow

0. 配置 API Key

在项目根目录创建 .env(勿提交到公开仓库):

DEEPSEEK_API_KEY=sk-your-key-here

或临时导出:

export DEEPSEEK_API_KEY=sk-your-key-here

1. 步骤一:NuminaMath + DeepSeek-R1 生成软标签

  • 使用数据集:NuminaMath CoT (Hugging Face: AI-MO/NuminaMath-CoT)
  • 字段说明problem(题目,送给 R1)、solution(标准解答,留给 V3 裁判)、source(数据来源)
  • 原则:只将 problem 发给 R1,不把 solution 给教师模型
  • 教师模型deepseek-reasoner,提取 reasoning_content(思维链)+ content(最终答案)
  • 脚本step1_generate_r1.py
source /etc/network_turbo   # 加载 NuminaMath 需要
cd /root/autodl-tmp/Distill
python step1_generate_r1.py --num-samples 5   # 冒烟 5 条;正式设 17000

中间产物/root/autodl-tmp/data/r1_raw_outputs.jsonl

每条记录包含:problemground_truth_solutionreasoning_contentcontentassistant_text(思维链+答案合并,供训练使用)。

R1 API 调用核心代码(已封装在脚本中):

from openai import OpenAI
client = OpenAI(api_key="...", base_url="https://api.deepseek.com")
response = client.chat.completions.create(
    model="deepseek-reasoner",
    messages=[
        {"role": "system", "content": "你是一个数学推理助手,请仔细思考并给出解答。"},
        {"role": "user", "content": problem},  # 仅传 problem,不传 solution
    ],
    stream=False,
    timeout=180,
)
msg = response.choices[0].message
reasoning = msg.reasoning_content   # 思维链
answer    = msg.content             # 最终回答

2. 步骤二:DeepSeek-V3 评判与数据清洗

  • 裁判模型deepseek-chat(DeepSeek-V3)
  • 逻辑:将 R1 的 assistant_text 与 NuminaMath 的 solution(标准解答)一并交给 V3,判断最终答案是否数学等价
  • 保留judge_correct: true 的样本,转为 sharegpt 格式写入 arrow
  • 脚本step2_judge_v3.py
python step2_judge_v3.py

中间产物/root/autodl-tmp/data/v3_judged_outputs.jsonl(含评判理由)
最终产物/root/autodl-tmp/data/Distil-data-17k-train.arrow

3. 一键运行完整流水线

source /etc/network_turbo
export DEEPSEEK_API_KEY=sk-your-key-here   # 或写入 .env
cd /root/autodl-tmp/Distill
python run_distill_pipeline.py --num-samples 5

数据格式(sharegpt):

{
  "system": "你是一个智能助手",
  "conversations": [
    {"from": "user", "value": "<NuminaMath problem>"},
    {"from": "assistant", "value": "<R1 reasoning_content>\\n\\n<R1 content>"}
  ]
}

冒烟实测结果(2026-07-08):

  • NuminaMath 加载 5 条,R1 成功 4 条(1 条超时,已加重试机制)
  • V3 评判 4/4 全部通过
  • 最终 arrow 含 4 条真实蒸馏数据(含 R1 思维链)

实测问题与解决:

问题 现象 解决方案
NuminaMath 下载慢 HF 首次加载卡顿 source /etc/network_turbo;使用 streaming=True 逐条读取
R1 请求超时 Request timed out step1 已加 timeout=180 和 3 次重试;复杂题可适当增大 timeout
V3 评判路径错误 AttributeError: 'str' object has no attribute 'open' distill_utils.load_jsonl 已支持 str/Path
冒烟样本量 5 条耗时约 1-2 分钟 冒烟 --num-samples 5;正式 --num-samples 17000 预计数小时+大量 API 费用

五、 全量微调实战配置与执行

1. 硬件与镜像要求

  • 推荐服务器配置:L20 显卡 * 1片,20核心,100GB 内存,30GB 显存,50GB SSD。
  • 镜像:PyTorch 2.1.0 / 3.10 (Ubuntu 22.04 / 12.1)。
  • 预估耗时:约 3.87 小时 / 整体 15 小时左右。
  • 实测环境:RTX 3090 24GB,冒烟 2 step 约 15 秒(DeepSpeed ZeRO-3 offload)。

2. 修改 LLaMA-Factory 的 dataset_info.json

文件路径:LLaMA-Factory/data/dataset_info.json(非根目录下的 dataset_info.json

添加自定义数据集配置:

"Distil": {
    "file_name": "/root/autodl-tmp/data/Distil-data-17k-train.arrow",
    "formatting": "sharegpt",
    "columns": {
        "messages": "conversations",
        "system": "system"
    },
    "tags": {
        "role_tag": "from",
        "content_tag": "value",
        "user_tag": "user",
        "assistant_tag": "assistant"
    }
}

3. 创建全量微调配置文件

LLaMA-Factory/examples/train_full/ 目录下创建 qwen2-full-sft.yaml

### model
model_name_or_path: /root/autodl-tmp/Distill/models/models/Qwen--Qwen2.5-1.5B-Instruct/snapshots/master
trust_remote_code: true

### method
stage: sft
do_train: true
finetuning_type: full
deepspeed: examples/deepspeed/ds_z3_offload_config.json  # 24GB 显卡必须加

### dataset
dataset: Distil
template: qwen
cutoff_len: 2048
max_samples: 1000
overwrite_cache: true
preprocessing_num_workers: 4
dataloader_num_workers: 2

### output
output_dir: /root/autodl-tmp/Distill/saves/qwen2.5-1.5b/smoke-sft
logging_steps: 1
save_steps: 100
plot_loss: true
overwrite_output_dir: true
save_only_model: true   # 避免保存 DeepSpeed 优化器状态(约 12GB)
report_to: none

### train
per_device_train_batch_size: 1
gradient_accumulation_steps: 1
learning_rate: 1.0e-5
max_steps: 2            # 冒烟测试用 2;正式训练改为 num_train_epochs: 3.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true
ddp_timeout: 180000000

关键配置说明:

  • template: qwen:Qwen2.5 系列使用 qwen 模板(非 qwen3
  • deepspeed: ds_z3_offload_config.json:24GB 显卡全量微调 1.5B 必须启用 ZeRO-3 + CPU offload,否则 OOM
  • save_only_model: true:仅保存模型权重,避免 DeepSpeed 优化器 checkpoint 占满磁盘
  • 正式训练时:删除 max_steps,改为 num_train_epochs: 3.0output_dir 改为正式路径

4. 运行微调命令

cd /root/autodl-tmp/Distill/LLaMA-Factory

# 单卡 24GB:必须加 DeepSpeed + torchrun
pip install deepspeed  # 若未安装
FORCE_TORCHRUN=1 NNODES=1 NODE_RANK=0 MASTER_PORT=29501 \
  llamafactory-cli train examples/train_full/qwen2-full-sft.yaml

注:原 SOP 预计 15 小时(17k 数据 × 3 epoch)。冒烟测试 2 step 约 15 秒即通过。

实测问题与解决:

问题 现象 解决方案
全量微调 OOM RTX 3090 24GB,CUDA out of memory 在 optimizer.step() 添加 deepspeed: examples/deepspeed/ds_z3_offload_config.jsonpip install deepspeed;使用 FORCE_TORCHRUN=1 启动
磁盘空间不足 训练完成后保存 checkpoint 失败,file write failed,磁盘 100% 设置 save_only_model: true;确保 SSD 至少预留 10GB(模型 3GB + checkpoint 3GB);清理 global_step* 优化器目录
训练后退出码 1 2 step 训练成功、model.safetensors 已保存,但 DeepSpeed 优化器写入失败 冒烟可通过;设置 save_only_model: true 后重跑可完全成功

冒烟实测结果:

loss: 0.9479 → 1.05(2 steps)
checkpoint: /root/autodl-tmp/Distill/saves/qwen2.5-1.5b/smoke-sft/checkpoint-2/model.safetensors

六、 微调后的模型效果验证

为了突出微调之后的作用,在测试阶段提出一个专业领域问题,如数学领域。

推理测试脚本(test_inference.py):

from transformers import AutoModelForCausalLM, AutoTokenizer
import os

MODEL_PATH = "/root/autodl-tmp/Distill/saves/qwen2.5-1.5b/smoke-sft/checkpoint-2"
BASE_PATH = "/root/autodl-tmp/Distill/models/models/Qwen--Qwen2.5-1.5B-Instruct/snapshots/master"
model_name = MODEL_PATH if os.path.exists(os.path.join(MODEL_PATH, "model.safetensors")) else BASE_PATH

model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto", device_map="auto", low_cpu_mem_usage=True)
tokenizer = AutoTokenizer.from_pretrained(model_name)

system_prompt = "你是一名助人为乐的助手。"
user_prompt = "请证明根号2是无理数。"
messages = [
    {"role": "system", "content": system_prompt},
    {"role": "user", "content": user_prompt}
]

text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
generated_ids = model.generate(**model_inputs, max_new_tokens=256)
generated_ids = [
    output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(response)
cd /root/autodl-tmp/Distill
python test_inference.py

冒烟实测: 模型成功加载微调 checkpoint,对「证明根号 2 是无理数」给出反证法推理回答,pipeline 端到端通过。


七、 Python 脚本设计与代码说明

本节详细说明项目中各 Python 文件的构建思路执行流程关键代码解释

7.1 整体架构

项目采用「公共工具层 + 分步脚本 + 流水线编排」三层结构:

环境与验证

蒸馏数据流水线

公共工具层

distill_utils.py

step1_generate_r1.py

step2_judge_v3.py

run_distill_pipeline.py

download.py

test_inference.py

LLaMA-Factory 训练

Distil-data-17k-train.arrow

设计原则:

原则 说明
关注点分离 数据生成(step1)、质量过滤(step2)、格式转换(utils)各自独立
中间产物可追溯 每步输出 jsonl,便于排查 R1 生成质量、V3 误判
冒烟/正式统一入口 run_distill_pipeline.py 通过 --num-samples 控制规模
配置外置 API Key 放 .env 或环境变量,不写死在代码里

脚本依赖关系:

run_distill_pipeline.py
  ├── step1_generate_r1.py  →  distill_utils.py  →  r1_raw_outputs.jsonl
  └── step2_judge_v3.py     →  distill_utils.py  →  v3_judged_outputs.jsonl
                                                    →  Distil-data-17k-train.arrow

download.py          →  学生模型权重(独立,训练前执行)
test_inference.py    →  微调后验证(独立,训练后执行)

7.2 distill_utils.py — 公共工具模块

构建思路: 将 step1 和 step2 重复使用的逻辑(API 鉴权、数据集加载、文件读写、格式转换)抽取到单一模块,避免代码重复,也方便后续扩展(如换数据集、换输出格式)。

7.2.1 路径常量
DATA_DIR = Path("/root/autodl-tmp/data")
R1_RAW_FILE = DATA_DIR / "r1_raw_outputs.jsonl"       # step1 输出
DISTIL_ARROW = DATA_DIR / "Distil-data-17k-train.arrow"  # step2 最终输出
SYSTEM_PROMPT = "你是一个智能助手"                     # sharegpt 格式的 system 字段

三个路径约定了整个流水线的数据落盘位置,LLaMA-Factory 的 dataset_info.json 直接指向 DISTIL_ARROW

7.2.2 get_api_key() — API Key 读取
def get_api_key() -> str:
    key = os.environ.get("DEEPSEEK_API_KEY")          # 优先读环境变量
    if not key and Path(".../.env").exists():         # 其次读 .env 文件
        for line in Path(".../.env").read_text().splitlines():
            if line.startswith("DEEPSEEK_API_KEY="):
                key = line.split("=", 1)[1].strip()
    if not key:
        raise RuntimeError("请设置 DEEPSEEK_API_KEY ...")
    return key

设计要点: 支持两种配置方式(环境变量 / .env),优先级明确;缺失时立即报错,避免跑到一半才发现鉴权失败。

7.2.3 get_client() — DeepSeek API 客户端
def get_client():
    from openai import OpenAI
    return OpenAI(api_key=get_api_key(), base_url="https://api.deepseek.com")

DeepSeek API 兼容 OpenAI SDK 格式,因此直接使用 openai 库,base_url 指向 DeepSeek 端点。step1 和 step2 共用同一个 client 工厂函数。

7.2.4 load_numinamath_samples() — 加载 NuminaMath 数据集
def load_numinamath_samples(num_samples: int):
    ds = load_dataset("AI-MO/NuminaMath-CoT", split="train", streaming=True)
    samples = []
    for i, row in enumerate(ds):
        samples.append({
            "id": i,
            "source": row.get("source", ""),   # 数据来源标签(如 cn_k12, olympiads)
            "problem": row["problem"],          # 仅取题目,送给 R1
            "solution": row["solution"],        # 标准解答,留给 V3 裁判,不给 R1
        })
        if len(samples) >= num_samples:
            break
    return samples

关键设计:

  • 使用 streaming=True:数据集约 86 万条、1.23GB,流式读取避免全量下载,冒烟时只取前 N 条。
  • 严格隔离 problem 和 solutionproblem 送给教师模型 R1,solution 仅用于 V3 评判,确保是「模型教模型」而非泄露答案。

NuminaMath-CoT 字段说明:

字段 用途 传给谁
problem 数学题目(英文) DeepSeek-R1
solution 标准 CoT 解答 DeepSeek-V3(评判用)
source 数据来源分类 日志记录
messages 预制对话格式 不使用(我们自己构建 sharegpt)
7.2.5 build_assistant_text() — 合并思维链与答案
def build_assistant_text(reasoning: str | None, content: str) -> str:
    reasoning = (reasoning or "").strip()
    content = (content or "").strip()
    if reasoning and content:
        return f"{reasoning}\n\n{content}"   # 思维链 + 最终答案
    return reasoning or content

DeepSeek-R1 返回两个字段:

  • reasoning_content:内部推理过程(思维链 CoT)
  • content:格式化后的最终回答

合并后作为 sharegpt 中 assistantvalue,让学生模型学到完整的推理链。

7.2.6 save_jsonl() / load_jsonl() — 中间产物读写
def save_jsonl(path, records):
    path = Path(path)                              # 兼容 str 和 Path
    path.parent.mkdir(parents=True, exist_ok=True) # 自动创建目录
    with path.open("w", encoding="utf-8") as f:
        for rec in records:
            f.write(json.dumps(rec, ensure_ascii=False) + "\n")  # 每行一条 JSON

选择 JSONL(每行一个 JSON 对象)而非单个 JSON 数组,好处是:

  • 追加写入方便(断点续跑时可扩展)
  • 单行损坏不影响其他记录
  • 大文件逐行读取,内存友好
7.2.7 save_sharegpt_arrow() — 导出 LLaMA-Factory 可读格式
def save_sharegpt_arrow(samples, output_file=DISTIL_ARROW):
    table = pa.Table.from_pydict({
        "system": [s["system"] for s in samples],
        "conversations": [s["conversations"] for s in samples],
    })
    with pa.OSFile(str(output_file), "wb") as sink:
        with ipc.new_file(sink, table.schema) as writer:
            writer.write_table(table)

将 sharegpt 格式转为 Apache Arrow IPC 文件。LLaMA-Factory 的 dataset_info.jsonformatting: "sharegpt" 配合列映射,可直接加载此 arrow 文件进行训练。


7.3 step1_generate_r1.py — 教师模型生成软标签

构建思路: 这是蒸馏流水线的第一步——黑盒蒸馏的核心。从 NuminaMath 取纯问题,让 DeepSeek-R1(教师)独立推理,产出带思维链的高质量回答。

执行流程:

加载 NuminaMath 前 N 条 problem
    ↓  逐条调用
DeepSeek-R1 API (deepseek-reasoner)
    ↓  提取
reasoning_content + content → 合并为 assistant_text
    ↓  连同 ground_truth_solution 一起
保存至 r1_raw_outputs.jsonl
7.3.1 call_r1() — 调用 R1 并重试
def call_r1(client, problem: str, max_retries: int = 3) -> dict:
    for attempt in range(max_retries):
        try:
            response = client.chat.completions.create(
                model="deepseek-reasoner",          # R1 推理模型
                messages=[
                    {"role": "system", "content": "你是一个数学推理助手,请仔细思考并给出解答。"},
                    {"role": "user", "content": problem},  # 只传题目,不传答案
                ],
                stream=False,
                timeout=180,                         # R1 推理较慢,给 3 分钟
            )
            msg = response.choices[0].message
            return {
                "reasoning_content": getattr(msg, "reasoning_content", None),
                "content": msg.content,
                "assistant_text": build_assistant_text(...),
            }
        except Exception as e:
            time.sleep(2 * (attempt + 1))          # 指数退避:2s, 4s, 6s
    raise last_err

设计要点:

  • deepseek-reasoner 是 DeepSeek-R1 的 API 模型名,区别于 V3 的 deepseek-chat
  • reasoning_content 通过 getattr 安全获取(兼容不同 SDK 版本)
  • 3 次重试 + 指数退避:R1 对复杂数学题推理耗时较长,实测有超时情况
  • timeout=180:冒烟中 id=2 曾因默认超时失败,增大后改善
7.3.2 main() — 主流程
def main():
    samples = load_numinamath_samples(args.num_samples)  # 1. 加载题目
    client = get_client()                                 # 2. 初始化 API
    records = []
    for sample in samples:
        r1 = call_r1(client, sample["problem"])           # 3. 逐条调用 R1
        records.append({
            "id": sample["id"],
            "source": sample["source"],
            "problem": sample["problem"],
            "ground_truth_solution": sample["solution"],  # 保留标准答案给 step2
            "reasoning_content": r1["reasoning_content"],
            "content": r1["content"],
            "assistant_text": r1["assistant_text"],
        })
        time.sleep(0.5)                                   # 限速,避免 API 限流
    save_jsonl(output, records)                           # 4. 保存中间产物

输出文件 r1_raw_outputs.jsonl 单条记录示例:

{
  "id": 0,
  "source": "synthetic_math",
  "problem": "Consider the terms of an arithmetic sequence: ...",
  "ground_truth_solution": "For an arithmetic sequence... \\boxed{\\frac{13}{6}}",
  "reasoning_content": "我们被问到...所以答案是13/6...",
  "content": "在等差数列中...\\boxed{\\dfrac{13}{6}}",
  "assistant_text": "<reasoning_content>\\n\\n<content>"
}

7.4 step2_judge_v3.py — V3 裁判与数据清洗

构建思路: 教师模型也会犯错。用 DeepSeek-V3 作为「评判器」,将 R1 的回答与 NuminaMath 标准解答对比,只保留数学上正确的样本。这是数据质量控制的关键环节。

执行流程:

读取 r1_raw_outputs.jsonl
    ↓  逐条调用
DeepSeek-V3 API (deepseek-chat) + 评判 Prompt
    ↓  解析 JSON 结果
judge_correct == true  →  转为 sharegpt 格式
    ↓
保存 v3_judged_outputs.jsonl(全部评判记录)
保存 Distil-data-17k-train.arrow(仅通过样本)
7.4.1 JUDGE_PROMPT — 评判提示词
JUDGE_PROMPT = """你是一个数学答案评判器。请判断「模型回答」与「标准解答」在数学上是否一致(最终答案正确即可,推理过程可不同)。

【题目】{problem}
【标准解答】{ground_truth}
【模型回答】{model_answer}

请只输出 JSON,不要输出其他内容:
{{"correct": true或false, "reason": "一句话说明"}}"""

设计要点:

  • 明确评判标准:最终答案正确即可,推理过程可不同(避免因表述差异误杀)
  • 要求 JSON 输出,便于程序解析
  • temperature=0:评判需要确定性,不需要创造性
7.4.2 parse_judge_result() — 容错解析
def parse_judge_result(text: str) -> dict:
    try:
        return json.loads(text)                    # 直接解析
    except json.JSONDecodeError:
        match = re.search(r"\{.*\}", text, re.DOTALL)  # 从混合文本中提取 JSON
        if match:
            return json.loads(match.group())
    return {"correct": False, "reason": "无法解析"}     # 解析失败视为不通过

V3 有时会在 JSON 前后加说明文字,正则提取 {...} 做容错。解析失败时默认不通过(保守策略,保证训练数据质量)。

7.4.3 to_sharegpt() — 格式转换
def to_sharegpt(record: dict) -> dict:
    return {
        "system": SYSTEM_PROMPT,
        "conversations": [
            {"from": "user", "value": record["problem"]},
            {"from": "assistant", "value": record["assistant_text"]},
        ],
    }

将通过评判的样本转为 LLaMA-Factory 要求的 sharegpt 格式:

  • from: "user" / from: "assistant" 对应 dataset_info.json 中的 user_tag / assistant_tag
  • value 对应 content_tag
7.4.4 main() — 主流程
def main():
    records = load_jsonl(args.input)               # 1. 读取 step1 产物
    for record in records:
        result = judge_one(client, record)          # 2. V3 逐条评判
        judged.append(result)
        if result["judge_correct"]:
            accepted.append(to_sharegpt(result))      # 3. 通过的转 sharegpt
    save_jsonl(JUDGED_FILE, judged)                 # 4. 保存全部评判记录
    save_sharegpt_arrow(accepted, output)            # 5. 导出 arrow 训练文件

冒烟实测: 4 条 R1 输出全部通过 V3 评判(4/4),说明 R1 在 NuminaMath 数学题上质量较高。


7.5 run_distill_pipeline.py — 流水线编排

构建思路: 最薄的一层编排脚本,用 subprocess 按顺序调用 step1 和 step2,自身不包含业务逻辑。

def main():
    steps = [
        [sys.executable, "step1_generate_r1.py", "--num-samples", str(args.num_samples)],
        [sys.executable, "step2_judge_v3.py"],
    ]
    for cmd in steps:
        subprocess.run(cmd, check=True, cwd="/root/autodl-tmp/Distill")

为什么用 subprocess 而非 import?

  • 每步独立进程,一步崩溃不影响另一步的中间产物
  • 可以单独重跑某一步(如 R1 完成后只需 python step2_judge_v3.py
  • 日志清晰,每步有明确的 [Step1] / [Step2] 前缀

使用方式:

# 冒烟(5 条,约 1-2 分钟)
python run_distill_pipeline.py --num-samples 5

# 正式(17000 条,数小时 + 大量 API 费用)
python run_distill_pipeline.py --num-samples 17000

7.6 download.py — 学生模型下载

构建思路: 独立脚本,与蒸馏流水线无关,负责从魔搭社区下载学生模型 Qwen2.5-1.5B-Instruct。

from modelscope import snapshot_download

model_id = "Qwen/Qwen2.5-1.5B-Instruct"
local_dir = "/root/autodl-tmp/Distill/models"
model_dir = snapshot_download(model_id, cache_dir=local_dir)
  • 使用 ModelScope 而非 HuggingFace:国内 AutoDL 环境下载更快,无需 network_turbo
  • cache_dir 指定缓存目录,ModelScope 会自动创建嵌套路径
  • 下载后须在训练 yaml 中使用完整绝对路径(见第三节实测路径)

7.7 test_inference.py — 微调效果验证

构建思路: 训练完成后的推理冒烟脚本,验证微调 checkpoint 能否正常加载并生成回答。

执行流程:

检查微调 checkpoint 是否存在
    ↓  存在则用 checkpoint,否则回退基座模型
加载 model + tokenizer
    ↓
构造 messages(system + user)
    ↓
apply_chat_template → tokenize → generate
    ↓
截断 input 部分,decode 输出

关键代码解释:

# 优先微调模型,回退基座模型
model_name = MODEL_PATH if os.path.exists(..., "model.safetensors") else BASE_PATH

# 使用 Qwen 聊天模板格式化输入
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

# 只解码新生成的 token(去掉输入部分)
generated_ids = [
    output_ids[len(input_ids):]
    for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
  • apply_chat_template:自动添加 <|im_start|>system 等特殊 token,与训练时格式一致
  • add_generation_prompt=True:在末尾添加 <|im_start|>assistant,提示模型开始生成
  • 截断输入 token:避免把 prompt 也 decode 出来

7.8 完整数据流与文件产物

NuminaMath-CoT (HF streaming)
    │
    │  problem only
    ▼
step1_generate_r1.py ──→ /root/autodl-tmp/data/r1_raw_outputs.jsonl
    │                        (含 problem, ground_truth, reasoning, content)
    │  assistant_text + ground_truth
    ▼
step2_judge_v3.py ──→ /root/autodl-tmp/data/v3_judged_outputs.jsonl
    │                    (含 judge_correct, judge_reason)
    │  仅 judge_correct=true
    ▼
Distil-data-17k-train.arrow ──→ LLaMA-Factory dataset_info.json (Distil)
    │
    ▼
qwen2-full-sft.yaml ──→ llamafactory-cli train ──→ checkpoint-N/
    │
    ▼
test_inference.py ──→ 验证生成质量

产物文件格式对照:

文件 格式 内容 下游消费者
r1_raw_outputs.jsonl JSONL R1 原始输出 + 标准答案 step2 输入
v3_judged_outputs.jsonl JSONL 评判结果 + 通过/拒绝理由 人工审查
Distil-data-17k-train.arrow Arrow IPC sharegpt 格式训练数据 LLaMA-Factory
checkpoint-N/model.safetensors SafeTensors 微调后模型权重 test_inference.py

7.9 扩展与正式训练建议

场景 修改方式
增大数据量 run_distill_pipeline.py --num-samples 17000
R1 超时频繁 增大 call_r1()timeout(如 300)或 max_retries(如 5)
断点续跑 step1 读取已有 jsonl 的 id 集合,跳过已生成条目(需自行扩展)
换评判模型 修改 step2_judge_v3.pymodel="deepseek-chat"
并发生成 在 step1 中用 concurrent.futures 并行调用 R1(注意 API 限流)
正式训练 yaml 中删除 max_steps,改 num_train_epochs: 3.0

八、 实施记录与文件清单(冒烟测试 2026-07-08)

文件 用途
SOP.md 本文档
.env DeepSeek API Key(本地配置,勿公开)
distill_utils.py 蒸馏流水线公共工具(API 客户端、arrow 保存等)
step1_generate_r1.py NuminaMath 问题 → DeepSeek-R1 生成软标签
step2_judge_v3.py DeepSeek-V3 评判 → 导出 arrow
run_distill_pipeline.py 一键运行步骤1+2
download.py ModelScope 下载 Qwen2.5-1.5B-Instruct
test_inference.py 微调后推理验证
generate_smoke_data.py 已废弃 早期模拟数据脚本,请用 run_distill_pipeline.py
LLaMA-Factory/ 微调框架(clone)
LLaMA-Factory/data/dataset_info.json 注册 Distil 数据集
LLaMA-Factory/examples/train_full/qwen2-full-sft.yaml 全量微调配置
/root/autodl-tmp/data/r1_raw_outputs.jsonl R1 原始输出(中间产物)
/root/autodl-tmp/data/v3_judged_outputs.jsonl V3 评判结果(中间产物)
/root/autodl-tmp/data/Distil-data-17k-train.arrow 蒸馏训练数据(最终产物)
saves/qwen2.5-1.5b/smoke-sft/checkpoint-2/ 冒烟微调产出

冒烟 Checklist:

  • LLaMA-Factory 克隆与安装
  • Qwen2.5-1.5B-Instruct 下载
  • NuminaMath-CoT 加载
  • DeepSeek-R1 生成软标签(4/5 条,含思维链)
  • DeepSeek-V3 评判清洗(4/4 通过)
  • Distil arrow 数据导出
  • dataset_info.json 配置
  • 全量微调 2 step 运行成功
  • 推理脚本输出正常

正式训练待办:

  • run_distill_pipeline.py --num-samples 17000 生成完整数据
  • 确保磁盘 ≥ 20GB 可用空间
  • max_steps: 2 改为 num_train_epochs: 3.0,预计耗时 ~15h
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐