
1. 为什么我要折腾这件事前阵子接了个私活需求说起来特别简单把一堆非结构化的中文文本批量转成固定字段的 JSON。字段不多就七八个但文本来源五花八门——有客服聊天记录、有商品评价、有用户填的乱七八糟的备注。甲方一开始想的是写正则我看了眼样本直接劝退了。同一个意思能有十几种说法日期格式能给你整出“昨天下午”“3月5号”“上周五”三种写法正则写到明年也覆盖不全。那就上大模型呗。调 API 是最省事的路子但问题也很现实一是数据敏感甲方的聊天记录不想往外传二是量不小几十万条跑下来 API 费用顶得上我半个月工钱三是最要命的通用大模型输出 JSON 经常不老实你让它输出纯 JSON它偏要给你加一句“好的以下是转换结果”然后外面再套个 json 的代码块。解析的时候得写一堆容错逻辑烦得很。所以我就动了微调的念头。目标很明确拿一个开源的小参数模型用 LoRA 微调成一个“JSON 格式打印机”——你给它一段文本它只吐 JSON别的啥也不说。训练环境呢本地显卡太拉胯租云卡最划算。我蹲了几天价格抢到一张 4090 的按量实例算下来一小时不到两块钱整个微调跑完加上调试总共花了不到一顿早饭钱。这篇文章就把整个过程掰开揉碎讲一遍。从数据准备、环境搭建、LoRA 参数怎么调、到最后的推理部署和踩过的坑全部是实操记录。适合谁看如果你手头有类似的“文本转结构化”需求或者想入门大模型微调但不知道从哪下手这篇应该能帮你省下不少试错时间。不需要你有多深的深度学习背景会点 Python、能看懂命令行就行。2. 整体方案设计与选型思路2.1 为什么是 LoRA 而不是全量微调先说清楚 LoRA 到底是个啥。你可以把它理解成给模型“外挂”了一组小零件。全量微调是把模型里几亿甚至几十亿个参数全部重新调整一遍相当于把整栋楼拆了重盖LoRA 则是在原来的权重旁边加一对低秩矩阵只训练这对小矩阵原模型参数冻住不动。训练完之后这对小矩阵可能只有几十兆推理的时候把它“贴”回原模型上就行。这么做的好处太明显了。第一显存占用断崖式下降。全量微调一个 7B 的模型没有个 80G 显存的卡根本别想LoRA 微调同样的模型24G 显存的 4090 就能跑得很舒服。第二训练速度快因为要更新的参数少了几个数量级。第三产物小一个 LoRA 权重文件通常就几十到几百兆方便保存和切换。第四不容易把原模型的能力“训崩”这对我们这种只想要一个特定输出格式的场景特别重要——我可不希望模型除了吐 JSON 啥都不会了。当然 LoRA 也不是万能的。如果你的任务需要模型学到全新的知识体系比如让它掌握一门它完全没见过的语言那 LoRA 可能不够。但我们这个任务本质上是“格式对齐”——模型本来就会理解文本我只需要把它输出格式的习惯掰过来LoRA 绰绰有余。2.2 基座模型怎么选选基座模型我主要看三个维度参数量、中文能力、社区生态。参数量方面7B 是个甜点。再小的比如 1.5B、3B理解能力会打折扣遇到复杂一点的文本容易漏字段再大的比如 13B、70B训练和推理成本都上去了而且对于“格式对齐”这种任务大模型带来的收益并不明显。我最终选的是 Qwen 系列的 7B 版本中文理解能力在同量级里属于第一梯队而且对 JSON 格式本身就有不错的认知基础微调起来收敛很快。这里插一句选模型的时候一定要看它的 tokenizer 和上下文长度。我们的输入文本有些比较长客服聊天记录动不动就上千字所以上下文窗口至少得 4K 起步。Qwen 7B 支持 8K 上下文够用了。2.3 训练框架的选择现在主流的微调框架有几个LLaMA-Factory、Axolotl、Unsloth、PEFT 原生方案。我最后用的是 LLaMA-Factory理由很简单它对中文社区友好文档全配置文件写起来清晰而且内置了数据格式转换、训练监控、模型合并导出这一整套流程不用自己东拼西凑。Unsloth 我也试过速度确实快号称能省 40% 显存、提速 2 倍但它的依赖版本卡得比较死我在云环境上装的时候跟其他库冲突了好几次折腾了半天没搞定就放弃了。如果你环境比较干净Unsloth 是个好选择如果像我一样在共享云环境里跑LLaMA-Factory 的兼容性更稳。2.4 云 GPU 的租用策略4090 按量实例的价格波动挺大的不同时段、不同平台能差出一倍。我的经验是工作日上午的价格通常比晚上和周末便宜因为用的人少。另外尽量选“按量计费”而不是“包周包月”因为微调这种任务你实际跑的时间可能就几个小时包月纯属浪费。租的时候注意几个参数显存要 24G4090 标配、系统盘至少 50G模型文件加数据集加中间产物很容易就几十 G、带宽尽量选大一点下载模型的时候你就知道痛苦了。我那次租的实例下载一个 7B 的模型权重花了将近二十分钟带宽是瓶颈。提示租之前先确认实例上有没有预装 CUDA 和 PyTorch。有些平台的基础镜像很干净啥都没有你得自己从头装光配环境就能耗掉一两个小时。选那种预装了 PyTorch 2.x 和 CUDA 12.x 的镜像能省很多事。3. 数据准备微调成败的八成在这里3.1 数据格式的设计LoRA 微调的数据格式说白了就是一堆“输入-输出”对。输入是一段原始文本输出是目标 JSON。但具体怎么写里面有不少讲究。LLaMA-Factory 支持 alpaca 格式和 sharegpt 格式。我用的是 alpaca 格式结构长这样{ instruction: 将以下文本转换为JSON格式只输出JSON不要输出任何其他内容。, input: 客户说昨天下午三点左右下单的买了两件T恤一共花了198块收货地址是北京市朝阳区某某路。, output: {\order_time\:\昨天下午三点\,\product\:\T恤\,\quantity\:2,\total_price\:198,\address\:\北京市朝阳区某某路\} }注意几个细节。第一instruction 要写得极其明确“只输出 JSON不要输出任何其他内容”这句话必须加而且要在训练数据里反复出现让模型形成条件反射。第二output 里不要加任何 markdown 代码块标记就是纯 JSON 字符串。第三字段名要统一不能这条数据用order_time那条用orderTime模型会懵。3.2 数据量的把控很多人以为微调需要几万条数据其实对于格式对齐这种任务几百到一千条就够了。我这次准备了 800 条训练数据、100 条验证数据效果已经很好。但数据质量比数量重要得多。我见过有人拿 GPT 批量生成几千条数据结果里面字段名不统一、JSON 格式有语法错误的比比皆是训出来的模型输出也是乱七八糟。我的做法是先人工精标 100 条确保这 100 条覆盖了所有字段和常见的文本变体然后用这 100 条作为 few-shot 示例让大模型批量生成剩下的 700 条最后再人工抽查 200 条把有问题的挑出来改掉。3.3 数据增强的实操技巧原始数据里如果某些字段的出现频率很低模型就很难学会。比如“备注”这个字段800 条数据里只有 50 条有那模型很可能在推理时直接忽略它。解决办法是针对性增强把这 50 条里有备注的样本通过换说法、改语序、加干扰信息等方式扩增到 200 条左右。另一个技巧是加入“负样本”。什么叫负样本就是输入文本里根本不包含某个字段的信息那输出 JSON 里对应的值就应该是 null 或者空字符串。这样训练出来的模型才不会瞎编——你给它一段没有地址的文本它不会硬编一个地址出来。{ instruction: 将以下文本转换为JSON格式只输出JSON不要输出任何其他内容。, input: 用户反馈说收到的商品有破损要求退款。, output: {\order_time\:null,\product\:null,\quantity\:null,\total_price\:null,\address\:null,\feedback\:\商品破损要求退款\} }3.4 数据清洗的检查清单在把数据喂给模型之前我习惯跑一遍检查脚本确认以下几件事每条数据的 output 都是合法 JSON用json.loads()能解析通过字段名在所有数据中完全一致没有拼写差异没有空 input 或空 output训练集和验证集没有重复样本特殊字符引号、换行、反斜杠都正确转义了这个检查脚本很简单但能帮你省下大量“训练跑完了才发现数据有问题”的时间。4. 环境搭建与训练配置4.1 云实例的初始化实例开机之后第一件事是确认显卡驱动和 CUDA 版本nvidia-smi输出里能看到 4090 和 CUDA Version 就说明驱动没问题。然后确认 PyTorch 能不能识别显卡import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出 True 和 RTX 4090环境就基本 OK 了。接下来装 LLaMA-Factory。我建议用 pip 装别用源码编译省事pip install llamafactory如果网络慢可以加个国内镜像源。装完之后用llamafactory-cli version确认一下。4.2 模型下载Qwen 7B 的权重文件大概 15G 左右。下载方式有好几种我用的是 modelscope 的命令行工具pip install modelscope modelscope download --model Qwen/Qwen2.5-7B-Instruct --local_dir ./models/qwen2.5-7b下载过程中可以去做别的事但注意别让实例因为闲置被自动关机了。有些平台有闲置检测机制长时间没有 GPU 活动会回收实例。4.3 LoRA 训练参数详解这是整个流程里最核心的部分。LLaMA-Factory 的训练配置可以写在一个 YAML 文件里我把我用的配置贴出来然后逐项解释model_name_or_path: ./models/qwen2.5-7b stage: sft do_train: true finetuning_type: lora lora_target: all lora_rank: 16 lora_alpha: 32 lora_dropout: 0.05 dataset: json_converter template: qwen cutoff_len: 2048 max_samples: 1000 overwrite_cache: true preprocessing_num_workers: 4 output_dir: ./output/lora_json logging_steps: 10 save_steps: 100 plot_loss: true overwrite_output_dir: true per_device_train_batch_size: 2 gradient_accumulation_steps: 8 learning_rate: 1.0e-4 num_train_epochs: 3.0 lr_scheduler_type: cosine warmup_ratio: 0.1 bf16: true gradient_checkpointing: true逐项说几个关键参数。lora_rank是 LoRA 矩阵的秩决定了“外挂零件”的容量。太小了学不动太大了容易过拟合。8 到 32 之间是比较常见的范围我用的 16对于格式对齐任务足够了。lora_alpha通常设为 rank 的两倍这是个经验值。它相当于一个缩放因子控制 LoRA 权重对原模型的影响程度。lora_target设为 all 表示对所有线性层都加 LoRA这样效果最好但显存占用也会高一些。如果显存紧张可以只对 attention 层的 q_proj 和 v_proj 加。learning_rate设 1e-4 是 LoRA 微调的常用值。注意 LoRA 的学习率通常比全量微调高一个数量级因为要训练的参数量少需要更大的步长才能有效更新。per_device_train_batch_size设为 2配合gradient_accumulation_steps为 8等效 batch size 就是 16。4090 的 24G 显存跑 7B 模型的 LoRA 微调batch size 设 2 是比较稳妥的设 4 有 OOM 的风险。gradient_checkpointing一定要开它用计算时间换显存空间能显著降低显存占用。代价是训练速度会慢 20% 左右但总比 OOM 强。bf16设为 true4090 支持 bf16比 fp16 更稳定不容易出现梯度爆炸。4.4 数据集注册LLaMA-Factory 需要在dataset_info.json里注册你的数据集。在项目目录下找到这个文件加一行{ json_converter: { file_name: json_converter.json, formatting: alpaca, columns: { prompt: instruction, query: input, response: output } } }然后把你的数据文件放到data目录下文件名和file_name对应。4.5 启动训练一切就绪之后启动命令很简单llamafactory-cli train config/lora_json.yaml训练开始后终端会输出 loss 值。正常情况下loss 应该在前几十步快速下降然后逐渐趋于平缓。如果 loss 一直不降或者剧烈震荡那多半是学习率设大了或者数据有问题。我这次训练跑了大概 40 分钟3 个 epoch最终 loss 从初始的 2.3 降到了 0.15 左右。显存占用峰值在 18G 上下4090 完全 hold 得住。5. 推理验证与效果调优5.1 合并 LoRA 权重训练完成后output目录下会有一个adapter_model.safetensors文件这就是 LoRA 权重。推理的时候有两种方式一种是加载原模型再加 LoRA 适配器另一种是把 LoRA 权重合并进原模型导出一个完整的模型。合并的好处是推理时不用额外加载适配器速度快一点部署也简单。合并命令llamafactory-cli export config/merge_lora.yamlmerge 的配置文件里主要指定原模型路径、LoRA 权重路径和输出路径。5.2 推理测试合并完之后写个简单的推理脚本测试效果from transformers import AutoModelForCausalLM, AutoTokenizer model_path ./output/merged_model tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForCausalLM.from_pretrained(model_path, device_mapauto) instruction 将以下文本转换为JSON格式只输出JSON不要输出任何其他内容。 input_text 张三上周五买了一个蓝牙耳机花了299寄到上海浦东新区。 prompt f{instruction}\n\n{input_text} inputs tokenizer(prompt, return_tensorspt).to(model.device) outputs model.generate(**inputs, max_new_tokens256, temperature0.1) print(tokenizer.decode(outputs[0], skip_special_tokensTrue))注意temperature设得很低0.1 甚至 0。因为我们要的是确定性输出不需要模型发挥创造力。temperature 越高输出越随机JSON 格式跑偏的概率就越大。5.3 效果评估与迭代我拿 100 条验证集跑了一遍统计了几个指标指标数值说明JSON 解析成功率99%只有 1 条输出多了个尾部逗号导致解析失败字段完整率97%有 3 条漏了某个字段字段值准确率94%有 6 条字段值提取有偏差多余内容出现率2%有 2 条在 JSON 前后加了说明文字这个结果已经能满足业务需求了。对于那 1% 解析失败的在工程侧加个简单的容错处理就行比如去掉尾部逗号再解析。如果效果不达标可以从这几个方向调增加训练数据量、提高 lora_rank、增加训练 epoch、调整学习率。但注意别过拟合验证集 loss 开始上升的时候就该停了。6. 踩坑记录与常见问题排查6.1 训练相关的坑坑一loss 不下降。我第一次跑的时候 loss 卡在 2.0 附近死活不降。排查了半天发现是数据格式注册错了——dataset_info.json里的columns映射写反了把 input 和 output 搞混了。模型拿输出当输入学当然学不会。所以训练前一定要用小样本过一遍数据加载流程确认输入输出没搞反。坑二显存 OOM。4090 的 24G 显存跑 7B LoRA 按理说够用但我有一次把cutoff_len设成了 4096batch size 又设了 4直接爆了。解决办法就是降 batch size、开 gradient checkpointing、或者缩短 cutoff_len。cutoff_len 不是越大越好根据你实际数据的长度分布来定覆盖 95% 的样本就行。坑三训练速度异常慢。正常情况下 4090 跑 7B LoRA每秒能处理 3 到 5 个样本。如果你发现速度只有零点几先检查是不是没开 bf16再检查数据加载的preprocessing_num_workers是不是设得太小。还有一个容易被忽略的点云实例的 CPU 核心数如果太少数据预处理会成为瓶颈GPU 利用率上不去。6.2 推理相关的坑坑一模型输出带“尾巴”。就算训练数据里全是纯 JSON模型偶尔还是会加一句“希望这对你有帮助”之类的话。这是基座模型的“礼貌惯性”在作祟。解决办法有两个一是训练时加入更多“纯 JSON 输出”的样本让模型彻底改掉这个习惯二是在推理后处理里做截断找到第一个{和最后一个}把中间的内容抠出来。坑二JSON 里的中文被转义了。有时候模型会输出\u4e2d\u6587这种 Unicode 转义形式。这不是错误json.loads()能正常解析但可读性差。如果在意这个可以在训练数据里确保中文直接以中文字符形式出现模型会跟着学。坑三字段值类型不稳定。比如价格字段有时候输出数字198有时候输出字符串198。这是训练数据里类型不统一导致的。解决办法就是在数据准备阶段严格统一类型价格一律用数字文本一律用字符串。6.3 常见问题速查表问题现象可能原因排查方向loss 不下降数据格式错误、学习率过小检查数据加载、调大学习率显存 OOMbatch size 过大、cutoff_len 过长降 batch size、开 gradient checkpointing输出格式不稳定训练数据格式不统一统一字段名和值类型推理速度慢未合并 LoRA、未用量化合并权重、尝试 4bit 量化模型胡编字段值负样本不足增加字段缺失的样本训练中断后无法续训未保存 checkpoint设置 save_steps、从 checkpoint 恢复6.4 几个实用的避坑心得第一训练之前先用 10 条数据跑一个 epoch确认整个流程能跑通再上全量数据。这样出问题能快速定位不用等半天才发现。第二云实例上跑训练一定要把 checkpoint 保存到持久化存储或者定期下载到本地。我有一次实例被平台回收了训练了半天的成果全没了血的教训。第三LoRA 的 rank 不是越大越好。我试过 rank 64效果跟 rank 16 差不多但训练时间多了将近一倍显存占用也高了不少。对于格式对齐这种任务rank 8 到 16 完全够用。第四推理的时候 temperature 和 top_p 都要调低。temperature 设 0.1top_p 设 0.9 或者更低。这两个参数越高模型越“放飞自我”JSON 格式跑偏的概率就越大。第五如果你的数据里有些字段的值特别长比如地址、备注注意 cutoff_len 要留够空间不然长样本会被截断模型学不到完整的输出格式。7. 成本核算与部署建议7.1 实际花费拆解我这次的总花费算下来是这样的4090 按量实例租了 3 个小时包括环境搭建、调试、正式训练每小时不到 2 块总共不到 6 块。模型下载和数据集准备的流量费用忽略不计。也就是说整个微调项目的硬成本就是一顿早饭钱。当然这是理想情况。如果你第一次跑环境搭建和调试的时间可能会长一些按 5 个小时算也就 10 块钱左右。相比调用 API 跑几十万条数据的费用这个成本几乎可以忽略。7.2 部署方案的选择微调完的模型怎么用几种方案如果数据不敏感、量也不大可以直接调 API没必要自己部署。但如果像我一样数据不能出本地那就得自己部署。7B 模型合并 LoRA 之后用 4bit 量化部署一张 4090 或者甚至 3090 就能跑推理速度也够用。部署框架可以用 vLLM 或者 Ollama。vLLM 吞吐量高适合并发场景Ollama 部署简单适合个人使用。我本地用的是 Ollama把合并后的模型转成 GGUF 格式一条命令就能跑起来。7.3 后续扩展方向这套流程跑通之后扩展起来就很灵活了。比如你换了业务场景需要提取的字段变了只需要重新准备几百条数据再跑一次 LoRA 微调就行基座模型不用换。又比如你想同时支持多种输出格式JSON、XML、YAML可以在 instruction 里加一个格式参数训练数据里覆盖这几种情况一个模型就能全搞定。我个人在实际操作中的体会是LoRA 微调这件事门槛比想象中低很多。真正的难点不在技术而在数据。你把数据准备明白了字段定义清楚了格式统一了训练本身就是一个跑命令的事。反过来数据乱七八糟再好的参数配置也救不回来。所以如果你准备动手先把八成时间花在数据上剩下的交给 4090 就行。