《中华人民共和国刑法》第三百四十四条 危害国家重点保护植物罪 《中华人民共和国民法典》第一千二百五十四条 高空抛物责任 《中华人民共和国刑事诉讼法》第五十五条 证据裁判原则
PROJECT SHOWCASE · LLM FINE-TUNING & EVALUATION

教会小模型,
有据可依地回答法律问题

以 Qwen2.5-1.5B-Instruct 为基座、5,000 条指令做 LoRA 微调,在单张 Tesla T4 上走完「数据构建 → 训练诊断 → 双层引用评测 → GGUF 量化部署」的完整闭环, 并把业界单一的引用准确率拆解为可分别归因的两层。

LoRA r=32 · fp165,000 指令 · 3 epochs 单张 T4 · 3.5 GPU·hGGUF Q8_0 · 边缘可部署 MIT · 全程可复现
0%
引用格式合规率
基座 56.3% ↑ 40.7pt
0%
引用事实正确率
基座 2.3% ↑ 65.2pt
0tok/s
解码吞吐(T4 实测)
TTFT 均值 0.068s
0GB
部署体积(Q8_0)
7B 方案约 8GB
SCROLL↓
§ 01 · THE TRADE-OFF

为什么是 1.5B,而不是 7B

放弃 7B 不是算力妥协,而是一次显式的工程取舍:当预算只有一块 T4, 问题从"怎么把 7B 塞进去"变成"在边界内把每一步做到最好、并把边界本身量化出来"。

7B + QLoRA对照配方
企业级原配方 · 4-bit 量化训练
训练显存~10G(4-bit 压缩后)
T4 训练 5k 样本不可行 · OOM
部署体积~8 GB
解码速度~40 tok/s(文献量级)
1.5B + bf16 LoRA本方案 · 采用
无需量化训练 · 全部数字为实测
训练显存~9G(bf16 免量化)
T4 训练 5k 样本2h 28m · 实测
部署体积1.6 GB · 笔记本可跑
解码速度97.1 tok/s · 实测
取舍的代价也如实量化:1.5B 的法律知识容量弱于 7B——本文不试图证明它能替代 7B, 而是用双层评测把"微调教会了什么、教会不了什么"分别度量出来(见 §04–§06)。
§ 02 · THE PIPELINE

全流程四段流水线 DATA → TRAIN → EVAL → SERVE

两条贯穿原则:冒烟先行——用 0.5B + 500 条 + 2 分钟先打通全链路, 把环境坑消灭在最便宜的实验上,正式训练 2.5 小时零意外;每个结论都有对照—— 基座 zero-shot 与微调模型同脚本、同口径、同测试集。

STAGE 1
数据构建
  • DISC-Law-SFT 16.7 万条
  • 清洗 + 问题级去重防泄漏
  • 5,000 / 300 / 300 分层切分
  • 测试集引用覆盖 100%
STAGE 2
训练
  • 0.5B 冒烟 2min 验链路
  • 1.5B LoRA r=32 α=64
  • lr 2e-4 · cosine · 3ep
  • fp16 · gradient ckpt
STAGE 3
评测
  • 双层引用指标(§04)
  • ROUGE / BLEU / BERTScore
  • 基线 vs 微调对照
  • 错误构成 + 案例归因
STAGE 4
部署
  • LoRA merge 回基座
  • GGUF Q8_0 · 1.6GB
  • llama-server OpenAI API
  • TTFT / 吞吐 / P95 压测

训练诊断 · eval_loss 的 U 型曲线

300 条验证集 · 每 epoch 评估一次
0.532 0.501 ep1 · 0.5324 ep2 · 0.5008 ★ ep3 · 0.5088 epoch loss
epoch 2 触底、epoch 3 回升 +1.6% —— 小指令集上的轻微过拟合信号。 checkpoint-626(ep2)保留用于对照;结论:5k 规模下 2 epochs 已接近该模型最优。

训练账单

全部为 Colab T4 实测
正式训练2h 27m · 1.69 samples/s
批量推理(×2 模型 ×300 条)~43 min
数据构建 + 评测 + 导出~25 min
全流程 GPU 合计≈ 3.5 h · 0 成本
train_loss0.857 → 0.407
冒烟先行节省的是风险成本: 所有环境问题(模板、精度、路径)都在 2 分钟的 0.5B 实验里暴露并修复。
§ 03 · THE FUNNEL

数据工程 16.7 万条到 5,600 条

评测数字可信的前提是数据干净:去重防泄漏、测试集可计算、抽样可复现—— 三件事在漏斗里一次做完。

0DISC-Law-SFT 原始 · 347MB
↓ 长度过滤(问 10–512 / 答 20–1200 字)+ 去重
0清洗后 · 17.9% 带法条引用
↓ 分层抽样 · 单类别 ≤40% · seed=42
5,6005,000 / 300 / 300
  • ①问题级去重防泄漏:NFKC 归一化 → 去标点 → MD5,同题仅留一条,训练/测试天然隔离,杜绝"背答案"。
  • ②测试集引用覆盖率 100%:验证/测试集优先保留答案带《法律名》第X条引用的样本——引用指标才可计算。
  • ③确定性可复现:seed=42 + 脚本化构建,任何人重跑得到同一份数据。
  • ④诚实标注:语料实为判决预测 / 司法考试 / 咨询的任务混态(无类别字段),指标解读基于这一事实。
§ 04 · THE TWO-LAYER METRIC

把"引用准确率"拆成两层

业界惯用单一 Citation Accuracy,把"会不会引用"和"引得对不对"混为一谈。 拆成两层之后,微调的价值与模型的边界同时变得可度量——这是本项目最核心的方法改进。

Citation Accuracy

单一数值。格式学会了、事实没学会,两种情况在这个指标里无法区分——小模型微调最需要回答的问题,恰恰被它藏住了。

拆解 ⟶
引用格式合规率度量「作答规范」

参考答案带引用时,预测也给出 《法律名》第X条 形式引用的比例。
回答的是:微调是否教会了模型"怎么答"

引用事实正确率度量「知识检索精度」

预测引用的法条集合与参考答案的重合度(Precision / Recall / 完全一致)。
回答的是:模型"引的对不对"——受预训练知识边界约束

配套口径:同脚本(eval/metrics.py)· 字符级 ROUGE-L / BLEU + BERTScore + 引用正则抽取 · 同 300 条 held-out 测试集。LLM-as-Judge 因无外部 API 预算列为升级项(§08)。

§ 05 · THE EVIDENCE

实验结果 BASELINE → FINE-TUNED

全部指标来自同一测试集的两次推理:基座 zero-shot vs 微调后(epoch 3)。

引用与语义指标

unit: % · baseline → fine-tuned
引用格式合规率56.3 → 97.0
引用事实正确率 P2.3 → 67.5
引用完全一致率1.7 → 60.3
BERTScore F174.2 → 82.2
基座 zero-shot微调后

生成质量

unit: score / 60 · baseline → fine-tuned
ROUGE-L32.4 → 49.4
BLEU(字符级)30.2 → 47.4
基座微调后

300 条测试集 · 引用构成

完全一致 181 · 60.3%
有出入 110 · 36.7%
9

「有出入」的构成见 §06 案例归因——其中一部分其实是评测集自身的标签噪声。

规模信号(0.5B 冒烟对照):0.5B 仅用 500 条训练 1 epoch, 引用格式合规率即达 76.7%(格式极易习得), 而事实正确率只有 41.7%(事实层依赖规模与数据量)——两层指标的分离再次得到印证。
§ 06 · THE CASE STUDY

同一案情,两个模型 相邻条文混淆现场

案

案情:被告人得知红豆杉具有防癌功效,与其姑父商量后在其山林内砍伐 3 棵红豆杉并运往县城加工家具。 经鉴定,涉案红豆杉均属国家Ⅰ级重点保护植物。问:可能构成何罪?

✗ 引错相邻条文
Qwen2.5-0.5B · 冒烟(500 条 / 1 epoch)
《刑法》第三百四十五条
盗伐林木罪——针对"林木"的一般条款。事实理解正确,条文检索偏了一条。
✓ 引对具体条文
Qwen2.5-1.5B · 正式微调(5,000 条 / 3 epochs)
《刑法》第三百四十四条
危害国家重点保护植物罪——针对"重点保护植物"的特别条款,与参考答案一致。
这是"相邻条文混淆"的典型现场——小模型知道发生了什么,却在相邻规范域间选错。 它是 §05 中 36.7%「有出入」错误的主体,也是 §08 把 RAG 注入法条库排在路线图第一位的原因。
鉴

另一类"错误"其实是评测集的错:一条涉外民事行为能力题,参考答案引用了《中华人民共和国民事法》第9条 ——这是一部不存在的法律。标签噪声意味着 67.5% 的事实正确率被低估, "评测集净化"因此进入路线图。

§ 07 · IN PRODUCTION

量化部署与压测 GGUF · llama.cpp · 30 QUERY LOAD TEST

LoRA 合并 → GGUF Q8_0(1.6GB,几乎无损)→ llama-server 提供 OpenAI 兼容 API, 附带 Gradio 前端。以下为 T4(CUDA 版 llama.cpp)30 条真实压测。

benchmark_latency.py — qwen2.5-1.5b-legal.Q8_0.gguf
$ python deploy/benchmark_latency.py --model ...Q8_0.gguf -n 30 --n-gpu-layers -1
ttft_mean_s = 0.068 ttft_p95_s = 0.104
decode_tok_s_mean = 97.1 range = 94 – 104
e2e_mean_s = 2.277 e2e_p95_s = 2.78 # 目标 < 2s:未达标
✓ 根因明确 # 256 tok ÷ 97 tok/s ≈ 2.6s,解码时长主导,与加载/调度无关
✓ 修复路径 # 限长 150tok ≈1.6s 达标 · 流式(TTFT 0.07s) · Q4_K_M 再提速~40%
首 token 延迟TTFT · mean / P95
0.07s
解码吞吐tok/s · CUDA on T4
97.1
部署体积GGUF Q8_0 · 近无损
1.6GB
⚖ 未达标也要写清楚

P95 端到端 2.78s > 2s 目标。根因是解码时长主导而非服务瓶颈, 三条修复路径均有量化预期——诚实的失败分析比漂亮的数字更可信。

§ 08 · BOUNDARIES & ROADMAP

已知边界,与下一步 WHAT'S NEXT

已知边界 LIMITS

L1知识检索精度天花板:相邻条文混淆是 1.5B 参数量下的硬边界,继续调参收益有限
L2任务混态:判决预测 / 法考 / 咨询混合训练,指标暂无法按任务分层归因
L3引用指标盲区:测不出选项/推理层面的对错,需 LLM-as-Judge 补齐
L4评测集标签噪声:部分"错误"来自参考答案本身,真实事实正确率被低估

路线图 PRIORITIZED

  • P0RAG 注入法条库
    直接攻击相邻条文混淆——预期收益大于一切调参
  • P1评测集净化
    人工复核 110 条「有出入」,拆分模型错 vs 标签错
  • P1LLM-as-Judge
    覆盖引用指标测不到的推理正确性
  • P2对照实验组
    ep2 vs ep3 · 任务分层评测 · 自建法条 QA · Q4/vLLM