最近在做智能客服的请求路由改造,核心诉求很简单:把线上问题在几百毫秒内分到对应处理链路,不需要模型给出长篇分析,只需要“快、准、稳”的结果。顺着这个方向翻了一圈,最终选定了 GitHub 上 17K Star 的开源项目 Laya 做 System 1 决策微调。网上关于 Laya 和 Jev 的讨论不少,标题虽然写的是“爆打 Jev”,但我的态度很明确:不要盲目站队,按场景选模型。这篇就把我从安装、数据构造、LoRA 微调、部署到实测的完整过程记录下来,包括踩过的坑和可以直接抄走的配置。
1. 为什么在决策场景我会先看 Laya
这一节先说清楚前提:我不打算说服所有人放弃 Jev,而是解释我在 System 1 决策场景下选择 Laya 的理由。理解这个取舍,比直接抄命令重要得多。
1.1 System 1 决策到底指什么
System 1 这个概念来自卡尼曼《思考,快与慢》,指人类大脑里快速、自动、不怎么费力的那套判断机制。放到大模型工程里,System 1 决策任务指的就是不需要深度推理链、不要求模型写出完整分析过程,但需要快速产出唯一结论的场景。
典型例子包括:
- 意图识别:用户说“我要退款”,模型直接判定为
refund,而不是输出一段“根据您的描述,我认为您可能需要进行退款申请,具体流程如下...”。 - 请求路由:把工单分到售前、售后、技术、投诉等不同队列。
- 工具选择:给智能体判断“这个问题应该查订单库还是查库存表”。
- 风险初筛:把高风险请求拦截下来交给人工复核。
- 标签分类:给文本打上多标签分类结果。
这类任务的共同点是:延迟敏感、并发量高、准确率要求不低但也没有严格到医疗/金融核心决策那种程度。用大参数模型做这些事,等于开着卡车去买菜——能到,但成本离谱。而专门为 System 1 场景微调过的小模型,反而能跑出更好的性价比。
1.2 Laya与Jev的取舍
我在选型前测过 Jev,也测过 Laya。Jev 的优势在于推理链路完整、可解释性强,适合需要“给出理由”的场景。但在我这个决策场景里,绝大多数请求根本不需要理由,只需要一个标签。把 Jev 的完整推理链跑完再提取结论,延迟翻了好几倍,对线上 SLA 很不友好。
Laya 的定位正好和 System 1 决策贴合:小参数量、短输入、单轮决策输出。我在本地 4090 上做了初步对比,微调后的 Laya 在意图分类测试集上的准确率和 Jev 相当,但 P95 延迟大约只有 Jev 的三分之一。这个结果决定了我最终选 Laya。
当然,如果你做的是复杂代码生成、长链条逻辑推理、深度问答这类 System 2 场景,那 Jev 可能更合适。选型不是比参数大小,而是看任务形态。
| 对比项 | Laya(决策微调后) | Jev(通用基座) |
|---|---|---|
| 体积 | 小,适合单卡部署 | 较大,通常需要多卡 |
| 推理链 | 单轮直接出结论 | 长推理链,可解释性强 |
| 延迟 | 低 | 高 |
| 适合任务 | System 1 决策、路由、分类 | System 2 深度推理、复杂生成 |
| 微调成本 | LoRA 单卡可跑 | 需要更多资源 |
| 许可证/生态 | 开源社区活跃 | 需要关注商用条款 |
2. 环境准备与安装:把基础环境一次弄干净
很多人微调失败不是模型问题,而是环境一团乱。这里分享一下我的安装过程,目标是:用一个干净的 conda 环境跑通 Laya 微调和部署,而不是污染系统全局 Python。
2.1 硬件与系统要求
我的环境如下,可以作为参考:
- 操作系统:Ubuntu 22.04
- GPU:NVIDIA RTX 4090 24GB(单卡)
- 驱动版本:535+
- CUDA:12.1
- Python:3.10
- 内存:64GB
如果你手中的显卡是 24GB 显存,微调 7B 量级的 Laya 用 LoRA 完全够用。如果显存只有 16GB,可以把per_device_train_batch_size调小,或者用 QLoRA 加 4bit 量化。如果你连 GPU 都没有,建议先用 Colab 或云 GPU 实例跑通再上生产。
2.2 安装步骤与注意事项
先用 conda 创建独立环境,避免和服务器上其他项目打架。我在这一步吃过亏,之前用全局 Python 环境装 torch 和 transformers,结果跟 TensorFlow 版本冲突,排查了大半天才发现是两个库的 ABI 不兼容。
conda create -n laya python=3.10 conda activate laya pip install --upgrade pip接下来安装 PyTorch。这里注意:不要直接裸装pip install torch,默认源装的可能是 CPU 版,或者和你 CUDA 版本不匹配。用官方推荐的搭配方式:
pip install torch==2.1.2 torchvision==0.16.2 torchaudio==2.1.2 --index-url https://download.pytorch.org/whl/cu121然后安装微调套件 LLaMA-Factory。这个工具的特点是把数据预处理、LoRA/QLoRA 微调、模型导出、推理部署都封在了一套 CLI 和 WebUI 里。对于做决策微调这种常规任务,能少写很多胶水代码。
git clone https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factory pip install -e .Laya 基座模型的权重下载,我建议走 Hugging Face 的镜像或者直接用 LLaMA-Factory 内置下载逻辑。模型体积不大,7B 版本大概 15GB 左右。下载完以后把路径记好,后续配置文件和命令行都要引用。
这里有个容易踩的坑:LLaMA-Factory 会根据模型名称自动推断模板(template)。不同基座模型的 prompt 模板结构差别很大,Laya 用的是和 Qwen 系兼容的 ChatML 格式。如果你在自定义数据集时擅自改了模板结构,后面训练出来会得到一堆乱输出。后面第三节会展开说。
3. 微调前的数据准备:System 1 决策数据集怎么构造
数据是微调的真正重心。很多人一上来就训练,结果模型输出一股“机翻味”,核心原因就是数据格式和任务定义不对。这节重点讲决策任务数据集的构造逻辑。
3.1 决策类数据的 ChatML 格式
微调用的是监督微调(SFT),数据要组织成“指令-回答”的结构。System 1 决策场景要求输出简洁、无废话,所以回答部分必须是一个直接可用的结果,而不是解释。
这里以“工单自动分类”为例,一条训练样本可以组织成:
{ "conversations": [ { "from": "system", "value": "你是工单分类助手,只需要输出一个分类标签:订单问题、物流问题、售后问题、其他。不要输出任何解释。" }, { "from": "human", "value": "我上周买的东西到现在还没发货,物流信息一直停在待揽收。" }, { "from": "gpt", "value": "物流问题" } ] }注意几个细节。system 提示词里明确写了“只要输出标签”,这个约束必须写清楚。人类输入是真实场景的用户话术,越口语化越好。模型输出是一个不多不少的分类结果。
我构造数据集的时候,会用脚本把历史工单标题和对应人工分类标签批量转成上述格式。重点是保证标签一致性和边界清晰,比如“退款”和“退货”容易被混为一谈,要在 system 提示词里做区分,或在数据里补充边界样本。
3.2 数据常见问题:刷掉这些坑
数据构造阶段我重复踩了几个坑,列出来给你们避雷:
- 回答太长:某个样本里模型回答写了三行解释,这会教坏模型,让它输出变得啰嗦。决策类回答必须强制单行短文本。
- 指令污染:把原始工单里的敏感信息原样写入,既没必要又可能造成隐私问题。文本要脱敏。
- 标签不均衡:订单问题占 80%,其他类型加起来才 20%,模型会学成“不管什么都输出订单问题”。建议先做标签分布分析,然后决定要不要过采样/欠采样或加权。
- system 提示词不统一:每条样本的 system 提示词建议全局统一,最多只做微调,不要每一条都不一样,否则模型会混乱。
- 输入输出没做长度控制:虽然 Laya 支持最长 8K 上下文,但决策任务不需要长文本。我给
cutoff_len设置 1024,把超长文本截断,效率更高。
另外一个建议:先只构造 500~1000 条高质量样本跑一轮小实验,看模型行为是否符合预期,再决定要不要扩大到几千几万条。很多决策任务两三千条就够用了,数据量不是越多越好,噪声样本多了反而拉低效果。
4. 用 LLaMA-Factory 做 LoRA 微调
环境装好、数据就绪之后,才进入正题:微调。这里我选择 LoRA,而不是全量微调,原因有几个,下面先讲原理再给配置。
4.1 为什么选 LoRA 而非全参
全量微调(Full Fine-tuning)会更新基座模型的全部参数,效果上限高,但显存开销和训练时间都非常可观。7B 模型全参微调在 4090 上几乎跑不起来,除非用多卡或者大量梯度累积,而且非常容易灾难性遗忘——模型在决策任务上变好,却把基础的指令理解能力搞坏。
LoRA 的做法是冻结基座模型全部参数,只在 attention 层的权重矩阵旁路插入低秩矩阵,训练时只更新这些低秩矩阵。实际效果上,LoRA 用很小的参数量就能贴近全参微调的效果,训练显存低、速度快、还能随时切换多个任务专用适配器。
对于 System 1 决策这类任务,LoRA 完全够用,因为任务本身不需要学习全新知识,只需要改变模型的输出风格和判断偏好。如果你显存十分紧张,还能用 QLoRA:把基座模型量化为 4bit 再训练 LoRA,16GB 显存也能跑 7B 量级。
4.2 配置文件与训练命令
LLaMA-Factory 的训练入口推荐用 YAML 配置文件,比命令行参数写一堆更清晰、好复用。我用的配置文件如下:
model_name_or_path: /data/models/Laya-7B template: qwen stage: sft finetuning_type: lora dataset: decision_sft cutoff_len: 1024 learning_rate: 2.0e-4 num_train_epochs: 3.0 max_samples: 100000 per_device_train_batch_size: 8 gradient_accumulation_steps: 4 lr_scheduler_type: cosine warmup_ratio: 0.1 logging_steps: 20 save_steps: 500 eval_strategy: steps eval_steps: 500 output_dir: outputs/lora-laya-decision几个关键参数解释一下:
template: qwen:我踩过最大的坑之一。Laya 基座用的是 Qwen 系模板,如果你填错模板(比如填成llama或chatglm),prompt 结构直接错位,训练不报错,但推理结果会非常离谱。这个参数务必和模型实际模板保持一致。cutoff_len: 1024:决策任务不需要长上下文,设 1024 足够。设太长会浪费显存,训练速度也慢。learning_rate: 2.0e-4:LoRA 微调常用 1e-4 到 5e-4 之间,我用 2e-4 起步。如果你的数据量少,建议降到 1e-4 并加长训练轮数,避免过拟合。per_device_train_batch_size: 8:在 4090 24GB 上跑 7B LoRA 是安全的。如果你报 OOM,先降到 4。gradient_accumulation_steps: 4:和 batch size 结合后,实际每次更新梯度的 batch 是 32,这个规模对决策数据集来说是合理的。
数据集要在 LLaMA-Factory 的data/dataset_info.json里注册:
{ "decision_sft": { "file_name": "decision_sft.json", "formatting": "sharegpt", "columns": { "messages": "conversations" }, "tags": { "role_tag": "from", "content_tag": "value", "user_tag": "human", "assistant_tag": "gpt" } } }注册好之后,执行训练:
llamafactory-cli train configs/lora_sft.yaml也可以在浏览器里操作:
CUDA_VISIBLE_DEVICES=0 llamafactory-cli webuiWebUI 对刚上手的人来说比较直观,但生产环境落脚本还是推荐 YAML 方式。
4.3 训练过程观察与调整
训练开始后,loss会逐步下降。我观察到的正常情况是:第 100 步左右 loss 从 1.2 左右降到 0.6 附近,第 500 步后开始缓慢下降并趋于平稳。如果你的 loss 在训练刚开始就非常低(比如 0.01),那大概率是数据有问题——模型只是背答案,没有学会决策逻辑。
训练轮数也别贪多。决策任务一般 2~3 个 epoch 足够。超过 5 个 epoch 以后,loss 虽然在降,但评测集准确率往往开始波动,这就是过拟合信号。LoRA 微调时要多盯 eval 指标,不要只盯训练 loss。
我当时跑了 3 个 epoch,配置了eval_strategy: steps,在每个 checkpoint 完成后跑一次验证集。最终选择验证集准确率最高的 checkpoint,而不是最后一个。
5. 部署与 System 1 决策实战
训练完成后,需要把 LoRA 适配器合并到基座模型里,或者保留适配器单独加载。下面讲两种方式,并给一段可以直接使用的调用代码。
5.1 导出与合并
LoRA 适配器训练完成后,我建议先合并导出再部署。合并后的模型就是一个正常模型文件,部署环节不依赖 LLaMA-Factory,也不用在每次加载时额外拼接适配器,省心很多。
导出的配置文件configs/lora_export.yaml如下:
model_name_or_path: /data/models/Laya-7B adapter_name_or_path: outputs/lora-laya-decision template: qwen finetuning_type: lora export_dir: outputs/merged-laya-decision export_size: 5 export_legacy_format: false执行导出:
llamafactory-cli export configs/lora_export.yaml导出后,模型存放在outputs/merged-laya-decision,可以直接被 vLLM、Ollama 或 Transformers 加载。
5.2 用 vLLM 部署并调用
我推荐用 vLLM 做在线部署,吞吐量比原生 Transformers 高很多,对 System 1 决策这种高频调用场景尤其重要。
pip install vllm vllm serve outputs/merged-laya-decision --max-model-len 4096 --gpu-memory-utilization 0.85 --port 8000启动完成后,用 Python 调用:
from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="EMPTY" ) def decide(user_input: str) -> str: response = client.chat.completions.create( model="outputs/merged-laya-decision", messages=[ {"role": "system", "content": "你是工单分类助手,只需要输出一个分类标签:订单问题、物流问题、售后问题、其他。不要输出任何解释。"}, {"role": "user", "content": user_input} ], temperature=0.2, max_tokens=50 ) return response.choices[0].message.content.strip() if __name__ == "__main__": test_cases = [ "发货三天没物流信息", "收到的商品有破损", "我要退款,不要了", "请问你们客服电话是多少" ] for case in test_cases: print(f"输入: {case}") print(f"决策: {decide(case)}")这里有几个细节。第一,temperature要调低,我设置 0.2。决策任务需要确定性输出,温度太高模型会随意跳跃。第二,max_tokens不用给大,决策回答一般 10 个 token 以内,给 50 足够,别让模型有空间生成长篇。第三,system 提示词必须和训练时保持一致,测试时如果换了提示词,效果一定会打折扣。
如果你不想上 vLLM,也可以导出为 GGUF 后用 Ollama 跑,适合边缘节点部署。这次我为了压延迟选了 vLLM,实测单请求差不多在几十毫秒到一两百毫秒之间,效果符合预期。
5.3 一套简单的效果评估方法
线上效果评估不要只看准确率,还要看延迟和输出稳定性。我在验证集上做了三类评测:
- 准确率:预测标签和真实标签的匹配比例。
- P95 延迟:压测时 95% 请求的响应时间。这个比均值更稳,均值容易被长尾拉高。
- 无效输出率:模型输出不在这几个合法标签里的比例。这个指标很关键,因为决策系统最忌讳模型说出“我觉得您应该联系物流公司”。
最终结果大致如下(仅代表我这组实验):
| 指标 | Laya(LoRA 微调后) | Jev(原版直出) |
|---|---|---|
| 准确率 | 94.2% | 94.8% |
| P95 延迟 | 130ms | 420ms |
| 无效输出率 | 0.4% | 2.1% |
| 部署卡数 | 1 张 | 2 张 |
单看准确率,Jev 略高一点点,但延迟和无效输出率不占优。决策系统需要的是短平快,如果有人拿“准确率高 0.6% 但延迟翻三倍”给你画饼,你可以把这篇丢给他看。
6. 常见问题与排坑速查
最后把这次实操中遇到的高频问题整理成速查表。这些问题看起来小,但每一个都能让你卡上半天。
| 症状 | 可能原因 | 解决方式 |
|---|---|---|
| 训练直接 OOM | batch size 过大或cutoff_len太长 | 降低per_device_train_batch_size到 4 或 2;或开启 QLoRA 4bit 量化 |
| 训练正常但输出无意义 | template填错,prompt 结构错位 | 确认 Laya 的模板是qwen,不要凭感觉填 |
| 模型输出一堆解释,而不是标签 | 训练数据里存在长回答样本;temperature 太高 | 清理数据;设置temperature=0.2,max_tokens=50 |
| 准确率上不去,loss 很快到 0 | 数据标签不均衡或存在重复样本 | 检查标签分布,做去重和过采样 |
| 模型在真实数据上效果好,在测试集上差 | 测试集分布和真实场景偏差大 | 重新划分训练/验证集,按时间切分更合理 |
| 合并导出后模型加载失败 | 导出路径或格式配置不对 | 清空export_dir后重跑,注意export_legacy_format: false |
| vLLM 部署后延迟很高 | GPU 利用率不足或并发配置太低 | 增大--max-num-seqs,并考虑开启--enable-prefix-caching |
这里单独补充两个经验。
首先是训练数据质量优先级高于数据量。我试过把数据从 2000 条扩到 10000 条,准确率反而掉了近两个点,原因是盲目从历史工单池里捞数据,捞进了大量低质量、重复、标签错乱的样本。后来我花了半天清洗数据,把数据集压缩回 2600 条,效果立刻回升。
其次是决策类任务一定要加一层非法输出兜底。无论模型训练得多好,总有概率输出不在预设范围内的内容。部署侧我建议在代码里加白名单校验,不是合法标签就统一走默认策略。这个兜底逻辑简单但极重要,能避免线上出现一堆奇怪的脏数据反馈到下游系统。
另外一个容易被忽略的点:微调后模型在 LLaMA-Factory 测试界面表现良好,不代表生产环境表现一致。测试界面往往不会并发压测。我建议部署后先用脚本跑几百条真实流量回放,对比标签分布是否合理,确认没问题再切线上。
我在实际项目中连续跑了两轮实验,最深的体会是:System 1 决策微调的成败,一半取决于数据构造,另一半取决于有没有把部署侧约束想清楚。模型本身反而不是瓶颈。选 Laya 还是 Jev,本质上是在延迟、成本和效果之间做权衡,没有绝对的黑白对错。如果你刚开始接触这类任务,建议先用小数据集把这条链路完整跑通,感受一下数据清洗、模板匹配、LoRA 调参、并发部署分别会有什么坑。跑通一次之后,后面想换基座模型、换任务场景都会顺手很多。