目录

记一次小模型微调/蒸馏学习(Qwen3-0.6B 从收货地址中提取结构化信息)

无意间,看见阿里云提供了针对 Qwen3-0.6B 的蒸馏教程,本文章本质上是跟着阿里云提供的教程学习微调过程,官方教程地址

借用下阿里云教程的介绍:

大参数模型效果好,但成本高、响应慢。为了在保障效果的同时提升推理速度、降低成本,可首先借助大参数模型完成目标任务的数据生成,并使用这些数据微调小参数模型,使其在特定任务中达到接近大参数模型的表现,这一过程也被称为模型蒸馏。

本方案将以从一句话中提取结构化信息(如收件人、地址、电话)为例,演示如何通过模型蒸馏,让 Qwen3-0.6B 模型在此任务上达到大参数模型的表现。

方案信息

教师模型: GLM4.7
样本信息: 由 GLM4.7 生成约1500+条虚拟数据
微调框架: ms-swift
模型推理: vllm

效果

https://s3.aixfan.com/img/screenshot/2026/03/02/1772387128_186b.png https://s3.aixfan.com/img/screenshot/2026/03/02/1772388039_187b.png

改动部分

  1. 新增了postal_coderemark字段,但postal_code在样本训练时没有特地添加,而是在 system 提示词内加的,所以不一定准确,最主要的还是“从地址推邮编”本质是 地理编码/检索问题,仅靠 0.6B 蒸馏感觉还不如由应用层处理。
  2. 我重新试用GLM4.7生成了一批 2K 训练样本,没有使用阿里云提供的,主要目的是想提取备注。

样本重新生成

  1. 包含功能区/开发区/高新区/未来科技城/管委会等
  2. 包含自治区/自治州/盟/旗等多级行政
  3. 包含英文/拼音混排行政区(extracted 中要映射为中文规范名称)
  4. 包含括号备注/路线提示/near地铁口/(原xxx)/收件要求/门禁暗号/时间要求
  5. 电话号码带空格、全角破折号、+86、分机/转接/ext(phone 规范化且保留转接信息)
  6. 只有城市,没有省份的地址
  7. 只有某个区,但没有城市、省份的地址

教程

注意:为了方便起见,建议直接使用阿里云教程内的使用环境(老鸟略过),可领取试用点后可以使用9个小时的环境,主要是已经安装好了 CUDA 等驱动,显卡是A10,内存也给得挺大的。

下载训练数据

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"

安装依赖

  1. ms-swift 魔搭社区提供的训练框架,支持模型的下载、微调和权重合并,极大简化了微调流程。
  2. 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

参数解释(AI)

参数 理解 影响/直觉 常见注意点
--model Qwen/Qwen3-0.6B 选择要微调的“底座模型” 模型越大越吃显存/更强;0.6B 属于小模型 要确认与 tokenizer、架构兼容(一般没问题)
--train_type lora 用 LoRA 方式微调(只训练少量“外挂”参数) 显存更省、训练更快、也更不容易把底座“练坏” LoRA 能力上限受 rank 等影响
--dataset train.jsonl 训练数据文件 数据质量决定上限 格式不对/字段错会直接训练失败或学偏
--torch_dtype bfloat16 用 bfloat16 精度训练 更省显存,通常更稳(比 fp16 更不容易溢出) 需要硬件支持(多数新 GPU/部分 CPU 支持)
--num_train_epochs 10 数据集会被“完整看完”10遍 轮数越多越容易记住训练数据 小数据集 + 10 epoch 很容易过拟合
--per_device_train_batch_size 20 每张卡一次喂给模型 20 条样本 越大越吃显存;越大梯度越稳定 OOM(爆显存)就得降它或降 max_length
--per_device_eval_batch_size 20 验证时每次跑 20 条 影响验证速度/显存 eval 也可能 OOM
--learning_rate 1e-4 学习率:模型“改参数的步子有多大” 大=学得快但容易不稳定;小=稳但慢 LoRA 常用 1e-4 ~ 2e-4,但仍要看数据量/长度
--lora_rank 8 LoRA 的“容量/可学习空间” rank 越大越能学复杂变化,也越吃显存/更易过拟合 8 偏保守;16 常见;32 更强但更贵
--lora_alpha 32 LoRA 的“放大系数/强度” 影响 LoRA 更新对模型的作用力度 常见搭配:rank 8/16 配 alpha 16/32/64(看任务)
--target_modules all-linear 把 LoRA 挂到所有线性层上 学得更全面,参数更多,显存/时间更高 小模型+all-linear 一般可行,但有时会更易过拟合
--gradient_accumulation_steps 16 梯度累积:攒 16 次再更新一次参数 等价于“更大的总 batch”,但不额外吃太多显存 训练更慢(因为更新变少、步数变长),但更稳
--save_steps 1 每 1 step 保存一次 checkpoint 方便回滚/对比 会非常频繁写盘、训练变慢、占 I/O;一般不建议这么小
--save_total_limit 2 最多保留 2 份 checkpoint 防止硬盘爆炸 配合 save_steps=1 会疯狂覆盖/删写
--logging_steps 2 每 2 step 打一次日志 便于观察 loss 日志太密也会拖慢一点点
--max_length 2048 每条样本最长截到 2048 token 能学更长上下文,但显存/耗时暴涨 最吃显存的参数之一;很多地址抽取任务不需要这么长
--output_dir output 训练产物输出目录 保存 checkpoint、日志等 注意磁盘空间
--warmup_ratio 0.05 前 5% 训练步数学习率从小慢慢升到设定值 避免一上来学太猛导致发散 数据少时 warmup 过长会浪费步数;过短可能不稳
--dataloader_num_workers 4 用 4 个进程加载数据 加快数据读取/预处理 Windows/某些环境可能 worker 太多反而卡;也可能导致随机性差异

会自动下载 Qwen3-0.6B,我本地用的3080 12G跑的,大概在10分钟的样子。 https://s3.aixfan.com/img/screenshot/2026/03/02/1772380928_183b.png

https://s3.aixfan.com/img/screenshot/2026/03/02/1772391032_194b.png

根据脚本内容,训练完成后,会合并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

https://s3.aixfan.com/img/screenshot/2026/03/02/1772380998_185b.png

调用方式

经过我测试,温度设置为 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客户端

添加模型提供方

https://s3.aixfan.com/img/screenshot/2026/03/02/1772389387_188b.png

新会话

https://s3.aixfan.com/img/screenshot/2026/03/02/1772389387_188b.png

https://s3.aixfan.com/img/screenshot/2026/03/02/1772389538_192b.png

总结

对于识别效果,个人感官如果总分给10分,我给8分,可能是0.6B参数量比较小,或者是训练样本的问题,对于 行政冲突、功能区映射与定位、复杂纠错与翻译,处理还是不行,而且还是建立在我重新使用新样本微调的情况下。
自带的样本微调出来只能给到6分左右。但处理常见的地址还是没有问题的。

其次未做 字段提取准确率、召回率、错误率 量化指标,也未对比原大模型(GLM4.7)的效果。

PS:从目前的学习体验来看,蒸馏的整体流程和深度学习模型训练其实有不少相似之处。基本思路都是基于一批样本数据进行训练,通过不断迭代和微调来优化模型效果,并结合召回率、准确率等指标对模型性能进行评估,然后再根据评估结果继续调整训练策略或模型结构。从实践角度来看,本质上也是一个“数据 → 训练 → 评估 → 调整”的循环过程,只是蒸馏更多是在已有模型能力的基础上进行知识迁移和压缩。

2026-03-04 更新

针对以上行政冲突等问题,我重新利用GLM4.7生成了2.8万条样本数据,其中包含

  • 行政区冲突样本
  • 缺失补全样本
  • 定位,新增了字段 zone (功能区),
  • 多地址/冲突
  • 非“路号”型地址:村镇组/队/屯/号;市场档口/铺位/摊位;或学校/医院/园区内部楼宇(楼/座/科室/院区等)
  • 数字混淆:取件码/门禁码 等
  • 多号码/分机
  • 英文/拼音片段
  • 行政区错别字
  • 偏远地区
  • 行政区缩写与别称
  • 模糊POI与硬门牌
  • 路名干扰

不再将 功能区 放入 remark 字段中,单独放入 zone 字段。但最好还是模型后置省市区数据库做校验。

训练参数重新调整为

swift sft \
  --model Qwen/Qwen3-0.6B \
  --train_type lora \
  --dataset 'train.jsonl' \
  --torch_dtype bfloat16 \
  --num_train_epochs 2 \
  --per_device_train_batch_size 24 \
  --per_device_eval_batch_size 24 \
  --learning_rate 5e-5 \
  --lora_rank 16 \
  --lora_alpha 32 \
  --target_modules all-linear \
  --gradient_accumulation_steps 4 \
  --save_steps 100 \
  --save_total_limit 3 \
  --logging_steps 10 \
  --max_length 512 \
  --output_dir output \
  --warmup_ratio 0.05 \
  --dataloader_num_workers 4;

目前发现不足:

1、 把地标当 name。 如: 内蒙古自治区呼和浩特市赛罕区敕勒川大街1号内蒙古自治区政府旁,"{""province"": ""内蒙古自治区"", ""city"": ""呼和浩特市"", ""district"": ""赛罕区"", ""zone"": """", ""specific_location"": ""敕勒川大街1号"", ""name"": ""内蒙古自治区政府旁"", ""phone"": """", ""postal_code"": """", ""remark"": """"}"

2、行政区规范仍不稳定。如 广东省深圳市深汕特别合作区 输出 district="南山区"。

3、单元号符号丢失。如 2号楼-1单元 802 输出:2号楼1单元802(丢了 -)

4、地址片段粘连。观日路18号 B10 5层 → 输出 观日路18号B105层(B10 和 5层粘一起)

还是需要针对性生成200条样本左右,基于新的模型再次蒸馏进行纠正,这一块后续再做。

优点:

  • 对于一般的正常地址已经能做到不出错
  • 抗干扰与清洗能力
  • 复杂的逻辑拆分,对 zone(功能区/开发区)和 remark(备注)的拆分非常灵动。比如能把“与海德三道交汇处”准确放入备注,把“高新区”和“武侯区”分别归类。
  • 行政区划补全与规范,能将“新疆”自动扩写为“新疆维吾尔自治区”。

测试地址(虚拟,有难度的情况)(忽略json格式):

原文:浙江省舟山市普陀区朱家尖街道东沙路100号
解析结果: "{""province"": ""浙江省"", ""city"": ""舟山市"", ""district"": ""普陀区"", ""zone"": """", ""specific_location"": ""朱家尖街道东沙路100号"", ""name"": """", ""phone"": """", ""postal_code"": """", ""remark"": """"}"

原文:山东省潍坊市寿光市圣城街道圣阳街与幸福路交叉口
解析结果: "{""province"": ""山东省"", ""city"": ""潍坊市"", ""district"": ""寿光市"", ""zone"": """", ""specific_location"": ""圣城街道圣阳街与幸福路交叉口"", ""name"": """", ""phone"": """", ""postal_code"": """", ""remark"": """"}"

原文:河南新乡市长垣市蒲西街道蒲城大道1号行政中心
解析结果: "{""province"": ""河南省"", ""city"": ""新乡市"", ""district"": ""长垣市"", ""zone"": """", ""specific_location"": ""蒲西街道蒲城大道1号行政中心"", ""name"": """", ""phone"": """", ""postal_code"": """", ""remark"": """"}"

原文:湖北省神农架林区木鱼镇木鱼路1号神农架游客中心
解析结果: "{""province"": ""湖北省"", ""city"": """", ""district"": ""神农架林区"", ""zone"": """", ""specific_location"": ""木鱼镇木鱼路1号神农架游客中心"", ""name"": """", ""phone"": """", ""postal_code"": """", ""remark"": """"}"

原文:广东省深圳市深汕特别合作区鹅埠镇创新大道1号
解析结果: "{""province"": ""广东省"", ""city"": ""深圳市"", ""district"": ""南山区"", ""zone"": ""深汕特别合作区"", ""specific_location"": ""鹅埠镇创新大道1号"", ""name"": """", ""phone"": """", ""postal_code"": """", ""remark"": """"}"

原文:上海 浦东 张江高科(中区) 科苑路捌拾捌号 3#楼501 张三/前台收 13800138000(备注:别按门铃,电话可能打不通)
解析结果: "{""province"": ""上海市"", ""city"": ""上海市"", ""district"": ""浦东新区"", ""zone"": ""张江高科(中区)"", ""specific_location"": ""科苑路捌拾捌号3#楼501"", ""name"": ""张三"", ""phone"": ""13800138000"", ""remark"": ""前台收;别按门铃,电话可能打不通""}"

原文:北京市海淀区 上地十街10号院(辉煌国际)2号楼-1单元 802,李四 186-0000-1234;备用:010-6299 7788(快递别放驿站)
解析结果: "{""province"": ""北京市"", ""city"": ""北京市"", ""district"": ""海淀区"", ""zone"": """", ""specific_location"": ""上地十街10号院辉煌国际2号楼1单元802"", ""name"": ""李四"", ""phone"": ""18600001234"", ""postal_code"": """", ""remark"": ""备用号:010-62997788;快递别放驿站""}"

原文:广东省深圳市南山粤海街道 科技园(南区)科苑南路2666号 中国华润大厦T2 23F 2306室 收件:王五 13900001111 备注:到前台说“会议资料”
解析结果: "{""province"": ""广东省"", ""city"": ""深圳市"", ""district"": ""南山区"", ""zone"": ""科技园(南区)"", ""specific_location"": ""科苑南路2666号中国华润大厦T2 23F 2306室"", ""name"": ""王五"", ""phone"": ""13900001111"", ""remark"": ""到前台说“会议资料”""}"

原文:浙江杭州 余杭区(未来科技城)文一西路 969号 阿里巴巴西溪园区 3期 D区 5号楼 3-201 赵* 15100002222(门口保安不让进)
解析结果: "{""province"": ""浙江省"", ""city"": ""杭州市"", ""district"": ""余杭区"", ""zone"": ""未来科技城;阿里巴巴西溪园区"", ""specific_location"": ""文一西路969号3期D区5号楼3-201"", ""name"": ""赵*"", ""phone"": ""15100002222"", ""remark"": ""门口保安不让进""}"

原文:重庆 渝北 回兴(两路)金开大道 1881 号 融科金开中心 1栋 19-7 收:陈晨 133 3333 3333 备注:周末别送
解析结果: "{""province"": ""重庆市"", ""city"": ""重庆市"", ""district"": ""渝北区"", ""zone"": """", ""specific_location"": ""金开大道1881号融科金开中心1栋19-7"", ""name"": ""陈晨"", ""phone"": ""13333333333"", ""remark"": ""周末别送""}"

原文:江苏省南京市鼓楼区 中山北路 8O 号(注意是字母O不是零)德基广场二期 A座 1608,周先生 18000000000(发顺丰)
解析结果: "{""province"": ""江苏省"", ""city"": ""南京市"", ""district"": ""鼓楼区"", ""zone"": """", ""specific_location"": ""中山北路8O号德基广场二期A座1608"", ""name"": ""周先生"", ""phone"": ""18000000000"", ""postal_code"": """", ""remark"": ""发顺丰""}"

原文:四川成都 高新区(其实是武侯?不确定)天府二街 138号 腾讯大厦 B座 12层 1203 张老师 177-7777-7777 备注:到了打座机 028-88886666
解析结果: "{""province"": ""四川省"", ""city"": ""成都市"", ""district"": """", ""zone"": ""高新区"", ""specific_location"": ""天府二街138号腾讯大厦B座12层1203"", ""name"": ""张老师"", ""phone"": ""17777777777"", ""postal_code"": """", ""remark"": ""备注:到了打座机028-88886666""}"

原文:湖北武汉 江岸区 台北一路 9号 云林街坊? 4栋2单元502(原502现504) 收件人:刘女士 15200003333(别写“台北路”会丢件)
解析结果: "{""province"": ""湖北省"", ""city"": ""武汉市"", ""district"": ""江岸区"", ""zone"": """", ""specific_location"": ""台北一路9号云林街坊4栋2单元502"", ""name"": ""刘女士"", ""phone"": ""15200003333"", ""postal_code"": """", ""remark"": ""原502现504;别写“台北路”会丢件""}"

原文:陕西西安 雁塔 曲江新区(功能区)芙蓉西路 99号 曲江大悦城 写字楼 1号楼 1402 杨洋 18700004444 备注:停车场入口旁
解析结果: "{""province"": ""陕西省"", ""city"": ""西安市"", ""district"": ""雁塔区"", ""zone"": ""曲江新区"", ""specific_location"": ""芙蓉西路99号曲江大悦城写字楼1号楼1402"", ""name"": ""杨洋"", ""phone"": ""18700004444"", ""remark"": ""停车场入口旁""}"

原文:福建 厦门 思明区 软件园二期 观日路 18号 B10 5层(前台转) 收:林- 13600005555(上午送)
解析结果: "{""province"": ""福建省"", ""city"": ""厦门市"", ""district"": ""思明区"", ""zone"": ""软件园二期"", ""specific_location"": ""观日路18号B105层"", ""name"": ""林"", ""phone"": ""13600005555"", ""remark"": ""前台转;上午送""}"

原文:山东省青岛市 市北区(原四方)黑龙江南路 2号 万科中心 A座 1301,收件:孙悟空 17000006666(公司名别写,写了会被退)
解析结果: "{""province"": ""山东省"", ""city"": ""青岛市"", ""district"": ""市北区"", ""zone"": """", ""specific_location"": ""黑龙江南路2号万科中心A座1301"", ""name"": ""孙悟空"", ""phone"": ""17000006666"", ""postal_code"": """", ""remark"": ""公司名别写,写了会被退""}"

原文:河南郑州 郑东新区(功能区)商务外环路 5号 国际金融中心 IFC 2号楼 9-903 赵六 15900007777 备注:门牌有两个,以“2号楼”为准
解析结果: "{""province"": ""河南省"", ""city"": ""郑州市"", ""district"": ""郑东新区"", ""zone"": ""郑东新区"", ""specific_location"": ""商务外环路5号国际金融中心IFC2号楼9-903"", ""name"": ""赵六"", ""phone"": ""15900007777"", ""remark"": ""门牌有两个,以“2号楼”为准""}"

本机微调过程(可忽略)

这里记录一下我自己使用本机训练的过程。

ps: 后续涉及到执行python命令,请自行按照自己的python 环境管理器运行,例如我使用的uv,那么执行python前加一个前缀uv / uv run等。

运行环境:ubuntu 20.04.6(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