结构化输出是大语言模型最常见的落地任务之一,但主流基准往往把它并入更宽泛的推理或信息抽取评分中,很少单独衡量。一个模型能否稳定返回格式合法、可解析的输出,往往直接决定它能否接入下游系统。
本文介绍一套完全公开、成本极低的微调方案:使用 TRL 库中的 GRPO(Group Relative Policy Optimization)对 LFM2.5-350M 进行微调,并在 IFStruct 基准上评估。整个流程约需 500 个训练样本、100 个训练步,免费层级的 Colab 或 Kaggle GPU 即可运行,完整代码已在 GitHub 开源。结果显示,即使经过轻度微调,IFStruct 得分也能从 22.6% 提升至 29.7%。

需要说明的是,本文描述的并非 IFStruct 官方博客中 RL 模型的训练流程。这套笔记本的目的不是复现基准榜单分数,而是展示针对特定任务的小规模微调,能让小模型的结构输出能力显著提升,接近更大模型的水平。
运行环境
本指南分为两部分,可在不同设备上运行:
- 微调部分需要 GPU,附带笔记本的体量适合免费层的 Colab 或 Kaggle GPU。
- 评估部分可通过 llama.cpp 在本地 MacBook 上完成(本文使用配备 Apple M5 Max、36GB 统一内存的 MacBook Pro),它对外暴露一个兼容 OpenAI 接口的服务器供 IFStruct 评估器调用。
环境依赖方面,Python 工具链使用 UV,模型服务使用 llama.cpp。按照 Liquid AI 的 llama.cpp 部署文档,通过 Homebrew 安装并验证:
brew install llama.cpp
llama-server --version
基线评估:LFM2.5-350M(Base 模型)在 IFStruct 上的表现
微调之前,先在 IFStruct 基准上测试 LFM2.5-350M,验证能否复现官方公布的 21.1% 得分。IFStruct 用于测试 LLM 输出的合法性与模式(schema)遵从性,基准数据开源在 Liquid4All/ifstruct,公开数据集可在 Hugging Face 的 LiquidAI/ifstruct-v1.0 获取。
git clone https://github.com/Liquid4All/ifstruct.git
为保证评估口径一致,本地使用 llama.cpp 的 BF16 GGUF 版本(LiquidAI/LFM2.5-350M-GGUF)启动基模型服务:
llama-server \
-hf LiquidAI/LFM2.5-350M-GGUF:BF16 \
-c 32768 \
-np 4 \
-ngl 99 \
--alias LiquidAI/LFM2.5-350M \
--host 127.0.0.1 \
--port 8080
其中 --alias 指定 IFStruct 发送到 OpenAI 兼容端点的模型名;-ngl 99 要求 llama.cpp 在可用时把所有层卸载到 GPU;-np 4 表示并行处理四个请求;-c 32768 为上下文窗口大小。
服务启动后,运行完整的 2000 样本基准:
uv run ifstruct-eval \
--model LiquidAI/LFM2.5-350M \
--base-url http://localhost:8080/v1 \
--api-key dummy \
--dataset data/test.jsonl \
--results-file results/lfm2.5-350m-llamacpp-base.json \
--n-threads 4 \
--max-tokens 2048 \
-v
基模型最终得分:
============================================================
Model: LiquidAI/LFM2.5-350M
============================================================
Overall: 452/2000 passed (22.6%)
Average latency: 1453ms
By format:
JSON: 180/1000 passed (18.0%)
YAML: 272/1000 passed (27.2%)
By top-level structure:
Wrapper key 288/1011 passed (28.5%)
Bare list 164/989 passed (16.6%)
IFStruct 发布博客报道的 LFM2.5-350M 得分为 21.1%,本地 llama.cpp/BF16 环境测得 22.6%,两者接近。后续微调对比均以本地测得的这一结果为基线,保证服务栈一致。
基于 TRL 的 GRPO 结构化输出微调
完整可运行的流程见附带笔记本,此处只说明关键环节。
训练数据
训练数据采用 nvidia/Nemotron-RL-instruction_following-structured_outputs,其中每个提示词都对应目标 JSON 模式和预期字段数量,共使用约 500 个样本。
由于 Nemotron 的数据分布与 IFStruct 评估集存在差异,训练提示经过增强以弥合两处差距:
- 40% 的样本加入「将输出返回在封闭的代码块内」的指令,让模型学会遵从格式指令,而不是始终输出裸 JSON。
- 单独划出的 20% 样本被改写为顶层数组任务(模式被包裹在含预期条目数的数组中),用于训练裸列表输出和条目数量遵从性。
模型与 LoRA
加载 LiquidAI/LFM2.5-350M 并挂载 LoRA 适配器。考虑到 LFM2.5 采用注意力与卷积混合架构,target_modules 指向 LFM 特有的模块:
lora_config = LoraConfig(
r=16,
lora_alpha=32,
bias="none",
task_type="CAUSAL_LM",
target_modules=[
"q_proj", "k_proj", "v_proj", "out_proj", "in_proj",
"w1", "w2", "w3",
],
)
实际训练的参数量约 600 万,占模型总量的 1.66%。
奖励函数
整个训练过程由三个奖励函数驱动,每个函数的取值范围均为 [0, 1]:
- 格式奖励:输出是否可解析且符合所要求的格式。完全匹配要求格式(含代码块围栏)得 1.0;格式错误但可解析得 0.2;完全不可解析得 0.0。
- field_count_reward:对象顶层字段数量是否符合预期。精确匹配得 1.0,偏差越大得分线性衰减。
- schema_validation_reward:输出能否通过行内 JSON 模式校验,按约束违例次数扣分,必填字段缺失给予部分信用。
三项奖励以加权求和合并,reward_weights 设为 [1.0, 0.5, 2.0]。
训练配置
训练共 100 步,每个提示词生成 8 个采样,配置适配 16GB 免费 GPU:
from trl import GRPOConfig
training_args = GRPOConfig(
output_dir="./outputs/lfm25-350m-nemotron-schema-grpo",
learning_rate=5e-5,
max_steps=100,
warmup_steps=10,
num_generations=8,
per_device_train_batch_size=4,
gradient_accumulation_steps=8,
steps_per_generation=2,
max_completion_length=1024,
mask_truncated_completions=False,
temperature=1.1,
beta=0.01,
reward_weights=[1.0, 0.5, 2.0],
logging_steps=1,
save_steps=100,
)
训练过程中三项奖励分量均稳步上升,与参考模型的 KL 散度在预热结束后收敛至零附近。
合并并导出模型
训练完成后,将 LoRA 适配器合并回基模型权重,保存为独立的检查点,以便转换为 GGUF 用于服务:
MERGED_DIR = f"{training_args.output_dir}-merged"
merged_model = trainer.model.merge_and_unload()
merged_model.save_pretrained(MERGED_DIR)
tokenizer.save_pretrained(MERGED_DIR)
微调后评估:IFStruct 复测
微调完成后重新运行 IFStruct 评估。合并后的检查点需先转换为 BF16 GGUF,转换脚本随 llama.cpp 源码发布:
git clone --depth 1 https://github.com/ggml-org/llama.cpp
pip install ./llama.cpp/gguf-py
mkdir -p models
python llama.cpp/convert_hf_to_gguf.py \
PATH_TO_YOUR_MERGED_MODEL \
--outfile ./models/lfm25-350m-grpo-bf16.gguf \
--outtype bf16
随后用转换出的 GGUF 启动微调模型服务:
llama-server \
-m ./models/lfm25-350m-grpo-bf16.gguf \
--alias lfm25-350m-grpo-structured-output \
-c 32768 \
-np 4 \
-ngl 99 \
--host 127.0.0.1 \
--port 8081
再次运行完整的 2000 样本基准:
uv run ifstruct-eval \
--model lfm25-350m-grpo-structured-output \
--base-url http://localhost:8081/v1 \
--api-key dummy \
--dataset data/test.jsonl \
--results-file results/lfm25-350m-grpo.json \
--n-threads 4 \
--max-tokens 2048 \
-v
微调模型得分如下:
============================================================
Model: lfm25-350m-grpo-structured-output
============================================================
Overall: 594/2000 passed (29.7%)
Average latency: 1518ms
By format:
JSON: 319/1000 passed (31.9%)
YAML: 275/1000 passed (27.5%)
By top-level structure:
Wrapper key 300/1011 passed (29.7%)
Bare list 294/989 passed (29.7%)
在同一 llama.cpp/BF16 服务栈上对比两次运行,GRPO 微调带来约 7 个百分点的整体提升,从 22.6% 升至 29.7%。分项来看,JSON 格式从 18.0% 提升到 31.9%,提升最为明显;裸列表输出(Bare list)从 16.6% 升至 29.7%,与带包装键的输出持平,验证了针对条目数量与列表形态的训练增强确实生效。错误类型方面,「必填字段缺失」仍是最主要问题(7331 次),但「未闭合代码块」「多余字段 notes」等格式类错误相比基线显著减少。
整个方案训练数据仅约 500 条、训练步数 100 步,在免费 GPU 上即可完成,说明即便是小规模、低成本的针对性强化学习微调,也能让小模型在结构输出遵从性这一关键能力上获得可观的收益。





