记一次小模型微调/蒸馏学习(Qwen3-0.6B 从收货地址中提取结构化信息)
无意间,看见阿里云提供了针对 Qwen3-0.6B 的蒸馏教程,本文章本质上是跟着阿里云提供的教程学习微调过程,官方教程地址。
借用下阿里云教程的介绍:
大参数模型效果好,但成本高、响应慢。为了在保障效果的同时提升推理速度、降低成本,可首先借助大参数模型完成目标任务的数据生成,并使用这些数据微调小参数模型,使其在特定任务中达到接近大参数模型的表现,这一过程也被称为模型蒸馏。
本方案将以从一句话中提取结构化信息(如收件人、地址、电话)为例,演示如何通过模型蒸馏,让 Qwen3-0.6B 模型在此任务上达到大参数模型的表现。
方案信息
教师模型: GLM4.7
样本信息: 由 GLM4.7 生成约1500+条虚拟数据
微调框架: ms-swift
模型推理: vllm
效果

改动部分
- 新增了
postal_code、remark字段,但postal_code在样本训练时没有特地添加,而是在 system 提示词内加的,所以不一定准确,最主要的还是“从地址推邮编”本质是 地理编码/检索问题,仅靠 0.6B 蒸馏感觉还不如由应用层处理。 - 我重新试用GLM4.7生成了一批训练样本,没有使用阿里云提供的,主要目的是想提取备注。
样本重新生成
- 包含功能区/开发区/高新区/未来科技城/管委会等
- 包含自治区/自治州/盟/旗等多级行政
- 包含英文/拼音混排行政区(extracted 中要映射为中文规范名称)
- 包含括号备注/路线提示/near地铁口/(原xxx)/收件要求/门禁暗号/时间要求
- 电话号码带空格、全角破折号、+86、分机/转接/ext(phone 规范化且保留转接信息)
- 只有城市,没有省份的地址
- 只有某个区,但没有城市、省份的地址
教程
注意:为了方便起见,建议直接使用阿里云教程内的使用环境(老鸟略过),可领取使用点后可以使用9个小时的环境,主要是已经安装好了 CUDA 等驱动,显卡与内存也给得挺大的。
下载训练数据
mkdir /root/qwen-0.6b
cd /root/qwen-0.6b && \
# 下载训练数据 train.jsonl
curl -f -o train.jsonl "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250610/azvmpb/train_with_system.jsonl"
安装依赖
- ms-swift 魔搭社区提供的训练框架,支持模型的下载、微调和权重合并,极大简化了微调流程。
- vllm 用于部署微调后的模型,支持高性能推理服务,不仅方便验证微调效果,还可用于生成 API,供业务方直接调用。
pip3 install vllm==0.9.0.1 ms-swift==3.5.0
ps: 建议分开安装,不然会卡很久。
pip3 install vllm==0.9.0.1
pip3 install ms-swift==3.5.0
模型微调
cd /root/qwen-0.6b && \
# 下载微调脚本 sft.sh
curl -f -o sft.sh "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250623/cggwpz/sft.sh" && \
# 执行微调脚本
bash sft.sh
微调的核心代码为:
swift sft \
--model Qwen/Qwen3-0.6B \
--train_type lora \
--dataset 'train.jsonl' \
--torch_dtype bfloat16 \
--num_train_epochs 10 \
--per_device_train_batch_size 20 \
--per_device_eval_batch_size 20 \
--learning_rate 1e-4 \
--lora_rank 8 \
--lora_alpha 32 \
--target_modules all-linear \
--gradient_accumulation_steps 16 \
--save_steps 1 \
--save_total_limit 2 \
--logging_steps 2 \
--max_length 2048 \
--output_dir output \
--warmup_ratio 0.05 \
--dataloader_num_workers 4
会自动下载 Qwen3-0.6B,我本地用的3080 12G跑的,大概在10分钟的样子。


根据脚本内容,训练完成后,会合并LoRA权重,生成合并后模型在output/v0-xxx-xxx/checkpoint-50-merged目录下。
教程内容:
在output/v0-xxx-xxx路径下有 images 文件夹,打开 train_loss.png(反映训练集损失) 与 eval_loss.png(反映验证集损失),根据损失值的变化趋势初步判断当前模型的训练效果:
-
在结束训练前 train_loss 与 eval_loss 仍有下降趋势(欠拟合) 可以增加 num_train_epochs(训练轮次,与训练深度正相关) 参数,或适当增大 lora_rank(低秩矩阵的秩,秩越大,模型能表达更复杂的任务,但更容易过度训练)的值后再进行训练,加大模型的对训练数据的拟合程度;
-
在结束训练前 train_loss 持续下降,eval_loss 开始变大(过拟合) 可以减少 num_train_epochs 参数,或适当减小lora_rank的值后再进行训练,防止模型过度训练;
-
在结束训练前 train_loss 与 eval_loss 均处于平稳状态(良好拟合) 模型处于该状态时,您可以进行后续步骤。本方案的 train_loss 与 eval_loss 变化如下表所示:
验证微调后模型效果(可选)
# 进入 /root 目录
cd /root/qwen-0.6b && \
# 下载并执行验证脚本
curl -o evaluate.py "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250612/bzszyc/evaluate.py" && \
curl -o evaluate.sh "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250612/xtgxql/evaluate.sh" && \
bash evaluate.sh
部署为 API 服务
# 下载部署脚本 deploy.sh
curl -o deploy.sh "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250613/hbojjv/deploy.sh" && \
# 后台运行部署脚本
bash deploy.sh

调用方式
经过我测试,温度设置为 0 ,TOP P设置为 1 好一点。
1、curl
- Authorization需要输入Token
curl --location 'http://localhost:8000/v1/chat/completions' \
--header 'Authorization: Bearer TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"model": "Qwen3-0.6B-SFT",
"messages": [
{
"role": "system",
"content": "你是一个专业的信息抽取助手,专门负责从中文文本中提取收件人的JSON信息,包含的Key有province(省份)、city(城市名称)、district(区县名称)、specific_location(街道、门牌号、小区、楼栋等详细信息)、name(收件人姓名)、phone(联系电话)、postal_code(邮编)、remark(备注)。必须遵守的规则:1、如果用户输入的文本没有明确 province/city/district/name,则对应字段输出空字符串,不准编造。2、remark不允许出现 province/city/district/name/specific_location 字段内的内容"
},
{
"role": "user",
"content": "西安,长安区,郭杜街道,樱花一路,茅坡新城,6号楼2单元 301室,联系人:周杰,电话:18666667777 (备用:13588889999)"
}
],
"chat_template_kwargs": {
"enable_thinking": false
},
"guided_json": {
"title": "Labels",
"type": "object",
"required": ["province", "city", "district", "specific_location", "name", "phone"],
"properties": {
"province": {
"title": "Province",
"type": "string"
},
"city": {
"title": "City",
"type": "string"
},
"district": {
"title": "District",
"type": "string"
},
"specific_location": {
"title": "Specific Location",
"type": "string"
},
"name": {
"title": "Name",
"type": "string"
},
"phone": {
"title": "Phone",
"type": "string"
},
"remark": {
"title": "Remark",
"type": "string"
}
}
}
}'
2、Chatbox客户端
添加模型提供方

新会话


总结
对于识别效果,如果总分给10分,我给8分,可能是0.6B参数量比较小,或者是训练样本的问题,对于 行政冲突、功能区映射与定位、复杂纠错与翻译,处理还是不行,而且还是建立在我重新使用新样本微调的情况下。
自带的样本微调出来只能给到6分左右。
测试地址(虚拟):
raw_text:
上海 浦东 张江高科(中区) 科苑路捌拾捌号 3#楼501 张三/前台收 13800138000(备注:别按门铃,电话可能打不通)
raw_text:
北京市海淀区 上地十街10号院(辉煌国际)2号楼-1单元 802,李四 186-0000-1234;备用:010-6299 7788(快递别放驿站)
raw_text:
广东省深圳市南山粤海街道 科技园(南区)科苑南路2666号 中国华润大厦T2 23F 2306室 收件:王五 13900001111 备注:到前台说“会议资料”
raw_text:
浙江杭州 余杭区(未来科技城)文一西路 969号 阿里巴巴西溪园区 3期 D区 5号楼 3-201 赵* 15100002222(门口保安不让进)
raw_text:
重庆 渝北 回兴(两路)金开大道 1881 号 融科金开中心 1栋 19-7 收:陈晨 133 3333 3333 备注:周末别送
raw_text:
江苏省南京市鼓楼区 中山北路 8O 号(注意是字母O不是零)德基广场二期 A座 1608,周先生 18000000000(发顺丰)
raw_text:
四川成都 高新区(其实是武侯?不确定)天府二街 138号 腾讯大厦 B座 12层 1203 张老师 177-7777-7777 备注:到了打座机 028-88886666
raw_text:
湖北武汉 江岸区 台北一路 9号 云林街坊? 4栋2单元502(原502现504) 收件人:刘女士 15200003333(别写“台北路”会丢件)
raw_text:
陕西西安 雁塔 曲江新区(功能区)芙蓉西路 99号 曲江大悦城 写字楼 1号楼 1402 杨洋 18700004444 备注:停车场入口旁
raw_text:
福建 厦门 思明区 软件园二期 观日路 18号 B10 5层(前台转) 收:林- 13600005555(上午送)
raw_text:
山东省青岛市 市北区(原四方)黑龙江南路 2号 万科中心 A座 1301,收件:孙悟空 17000006666(公司名别写,写了会被退)
raw_text:
河南郑州 郑东新区(功能区)商务外环路 5号 国际金融中心 IFC 2号楼 9-903 赵六 15900007777 备注:门牌有两个,以“2号楼”为准
样本 1:行政区划逻辑陷阱(跨省/市冲突)
raw_text: "收件人:老王 电话:135****9988 地址:江苏省苏州市武侯区天府三街腾讯大厦A座18楼(如果有门禁请打手机,由于是旧地址,请务必送到现在的办公点)"
测试点:武侯区实际属于四川成都市,而非江苏苏州市。看模型是盲目相信省市,还是能识别出区县冲突并修正/置空。
样本 2:功能区深度映射(无直接行政区名)
raw_text: "西安高新区锦业路38号粤汉国际 B 座 2101 室,联系人:Jasmine,Tel:+86-029-88887777,备注:工作日派送"
测试点:西安高新区锦业路段实际行政隶属于“雁塔区”。看模型能否通过知识库映射出“雁塔区”。
样本 3:极简混乱 + 拼音混排 + 楼层细节
raw_text: "shanghai-pudong-beiboloo 518 hao, 3F room 302. 肖先生 13800001111 备注:别放菜鸟驿站!!"
测试点:拼音地址识别、路名纠正(beiboloo -> 碧波路)、楼层信息精准提取。
样本 4:偏远地区 + 多重备注 + 引导词干扰
raw_text: "收货信息如下:内蒙古 锡盟 东乌旗 乌里雅斯太镇 额尔敦路12号(原牧民服务中心旁) 乌兰其其格 139-4791-2233(备用号15047910000) 尽量下午送达,谢谢合作!"
测试点:盟、旗、镇的多层级处理;备用号码与主号码的剥离;行政简称(锡盟 -> 锡林郭勒盟)的还原。
样本 5:英文格式 + 商业体名称 + 纠错
raw_text: "Add: 5F, MixC Shopping Mall, No. 2888 Nanbin Road, Nanan District, CQ City. Rec: Mr. Chen, 18623456789."
测试点:直辖市缩写(CQ -> 重庆市)、商业体(MixC -> 万象城)的识别与 specific_location 的规范化
本机微调过程(可忽略)
这里记录一下我自己使用本机训练的过程。
运行环境:Windows10(WSL2)
显卡: NVIDIA GeForce RTX 3080 12G
CUDA Version: 12.9
Driver Version: 576.88
NVIDIA-SMI 576.88
python运行环境: uv
安装uv
curl -LsSf https://astral.sh/uv/install.sh | sh
安装完成后,可用以下命令验证安装
uv --version
创建虚拟环境
mkdir qwen-0.6b && cd qwen-0.6b
uv venv --python 3.12.7
默认会在当前目录创建 .venv 虚拟环境。
安装vllm
官方版本 根据自己的CUDA版本来。
uv pip install vllm==0.15.1 --extra-index-url https://wheels.vllm.ai/0.15.1/cu129 --extra-index-url https://download.pytorch.org/whl/cu129 --index-strategy unsafe-best-match
更多安装教程,可自行搜索(VLLM 部署的一些细节,关于CUDA对应版本的问题)
安装ms-swift
uv pip install ms-swift==3.5.0
脚本改动
将sft.sh、deploy.sh 执行的python脚本改为 uv 环境运行。如: sft.sh
#!/bin/bash
# =============================================================================
# Qwen3 0.6B 模型LoRA微调脚本
#
# 功能:
# 1. 自动下载Qwen3-0.6B模型
# 2. 使用物流填单数据集进行LoRA微调
# 3. 保存微调后的权重
# 4. 合并LoRA权重
# =============================================================================
if CUDA_VISIBLE_DEVICES=0 \
uv run swift sft \
--model Qwen/Qwen3-0.6B \
--train_type lora \
--dataset 'train.jsonl' \
--torch_dtype bfloat16 \
--num_train_epochs 10 \
--per_device_train_batch_size 20 \
--per_device_eval_batch_size 20 \
--learning_rate 1e-4 \
--lora_rank 8 \
--lora_alpha 32 \
--target_modules all-linear \
--gradient_accumulation_steps 16 \
--save_steps 1 \
--save_total_limit 2 \
--logging_steps 2 \
--max_length 2048 \
--output_dir output \
--warmup_ratio 0.05 \
--dataloader_num_workers 4; then
# 检查输出目录是否存在checkpoint
echo "检查训练结果..."
if [ -d "output" ] && [ "$(find output -name "checkpoint-*" -type d | wc -l)" -gt 0 ]; then
echo "✓ 找到训练checkpoint文件"
echo ""
echo "✓ 模型微调完成!"
echo "输出目录: output/"
else
echo "✗ 未找到训练checkpoint文件"
echo "训练可能未成功完成"
exit 1
fi
else
echo ""
echo "✗ 模型微调失败!"
echo "请检查错误信息"
exit 1
fi
echo "检测最新的checkpoint路径..."
LATEST_CHECKPOINT=$(find output -name "checkpoint-*" -type d | sort -V | tail -1)
if [ -z "$LATEST_CHECKPOINT" ]; then
echo "✗ 错误: 未找到checkpoint文件"
echo "请确保已完成模型微调,并且output目录中存在checkpoint文件"
exit 1
else
echo "✓ 找到checkpoint: $LATEST_CHECKPOINT"
fi
echo "开始合并LoRA权重..."
# 执行权重合并
# --ckpt_dir: checkpoint目录路径(需要根据实际情况修改)
# --merge_lora: 启用LoRA权重合并
if uv run swift export \
--ckpt_dir "$LATEST_CHECKPOINT" \
--merge_lora true; then
echo "✓ swift export 命令执行成功"
else
echo "✗ swift export 命令执行失败"
exit 1
fi
# 检查合并是否成功
echo "检查合并结果..."
MERGED_DIR="${LATEST_CHECKPOINT}-merged"
if [ -d "$MERGED_DIR" ]; then
echo "✓ 合并目录创建成功: $MERGED_DIR"
echo ""
echo "✓ LoRA权重合并完成!"
echo "合并后的模型路径: $MERGED_DIR"
else
echo "✗ 权重合并失败,未找到合并目录: $MERGED_DIR"
echo "请检查错误信息"
exit 1
fi
deploy.sh
此处我固定了API KEY。
#!/bin/bash
# =============================================================================
# vLLM API服务部署脚本
#
# 功能:
# 使用vLLM部署OpenAI兼容的API服务
#
# 可自定义参数:
# - --model: 模型路径(需要根据merge.sh的输出修改)
# - --host: 服务监听地址(0.0.0.0表示所有网络接口)
# - --port: 服务端口(默认8000)
# - --api-key: API访问密钥(每次运行时自动生成)
# - --gpu-memory-utilization: GPU显存使用率(0.8表示80%)
# - --max-model-len: 最大模型长度
#
# 安全提示:
# 1. API密钥每次启动时自动生成
# 2. 如果在生产环境使用,建议配置防火墙规则
# =============================================================================
# 生成随机API密钥
echo "生成API密钥..."
if API_KEY="sk-$(openssl rand -hex 24)"; then
echo "✓ API密钥生成成功"
else
echo "✗ API密钥生成失败"
exit 1
fi
API_KEY="sk-23faf2fd47f91300d7a6c6656128e7b53f036408195acc77"
# 自动检测最新的合并模型路径
echo "检测合并模型路径..."
MERGED_MODEL=$(find output -name "*-merged" -type d | sort -V | tail -1)
if [ -z "$MERGED_MODEL" ]; then
echo "✗ 错误: 未找到合并后的模型"
echo "请确保已运行merge.sh并成功合并LoRA权重"
echo "或者手动指定模型路径"
exit 1
else
echo "✓ 找到合并模型: $MERGED_MODEL"
fi
echo ""
echo "✓ 所有检查通过,启动vLLM API服务..."
echo ""
echo "服务配置:"
echo "- 模型路径: $MERGED_MODEL"
echo "- 监听地址: 0.0.0.0:8000"
echo "- API密钥: $API_KEY"
echo "- 模型名称: Qwen3-0.6B-SFT"
echo ""
echo "请保存上面的API密钥,用于客户端连接!"
echo ""
# 启动vLLM API服务器
echo "正在后台启动vLLM API服务器..."
# 启动服务器并获取进程ID
uv run python -m vllm.entrypoints.openai.api_server \
--model "$MERGED_MODEL" \
--host 0.0.0.0 \
--port 8000 \
--served-model-name Qwen3-0.6B-SFT \
--trust-remote-code \
--gpu-memory-utilization 0.8 \
--max-model-len 2048 \
--api-key "$API_KEY" > vllm.log 2>&1 &
VLLM_PID=$!
# 等待服务启动
echo "等待服务启动中..."
# 循环检查服务是否启动成功
MAX_WAIT=120 # 最大等待2分钟
WAIT_TIME=0
SERVICE_READY=false
while [ $WAIT_TIME -lt $MAX_WAIT ]; do
if kill -0 $VLLM_PID 2>/dev/null; then
# 进程存在,检查API是否可用
if curl -s http://localhost:8000/health > /dev/null 2>&1; then
SERVICE_READY=true
break
fi
else
echo "✗ vLLM进程意外停止"
break
fi
echo "等待中... ($WAIT_TIME/$MAX_WAIT 秒)"
sleep 5
WAIT_TIME=$((WAIT_TIME + 5))
done
# 检查服务是否正常运行
if [ "$SERVICE_READY" = true ]; then
echo ""
echo "✓ API服务启动成功!"
echo "✓ 服务进程ID: $VLLM_PID"
echo "✓ 日志文件: vllm.log"
# 注意事项说明
echo ""
echo "重要提示:"
echo "1. API密钥: $API_KEY"
echo "2. 服务地址: http://0.0.0.0:8000"
echo "3. 日志查看: tail -f vllm.log"
echo "4. 停止服务: kill $VLLM_PID"
echo "5. 如需外网访问,请配置防火墙规则"
echo ""
echo "服务部署完成!按 Ctrl+C 停止服务。"
# 示例:保存API密钥到文件
echo "$API_KEY" > api_key.txt
echo "✓ API密钥已保存到 api_key.txt"
# 示例:检查服务状态
echo ""
echo "服务状态检查:"
if curl -s http://localhost:8000/health > /dev/null 2>&1; then
echo "✓ API服务健康检查通过"
else
echo "⚠ API服务可能还在启动中,请稍后检查"
fi
# 信号处理函数
cleanup() {
echo ""
echo "正在停止vLLM服务..."
kill $VLLM_PID 2>/dev/null
wait $VLLM_PID 2>/dev/null
echo "✓ 服务已停止"
exit 0
}
# 注册信号处理
trap cleanup SIGINT SIGTERM
echo ""
echo "服务正在运行中... (使用 Ctrl+C 停止)"
echo "============================================"
# 保持脚本运行,等待用户中断
while true; do
if ! kill -0 $VLLM_PID 2>/dev/null; then
echo "✗ vLLM服务意外停止,退出..."
exit 1
fi
sleep 10
done
else
echo ""
echo "✗ API服务启动失败"
echo "请检查日志文件: vllm.log"
exit 1
fi