skip to content
tlj 的工程笔记
Table of Contents

先说清楚这是个什么项目。这是我实习期间独立负责的一个内容合规质检模型——微调 Qwen2.5-14B-Instruct,覆盖四个平台(小红书、抖音、视频号、公众号),输入一段文案加一批平台规则,输出是否违规、违规原文、并精确溯源到具体哪一条规则。项目要求溯源率 90%,是条硬指标。数据、训练、评估、部署全程我自己做,只有其中一个平台的数据集最初是同事做的,后来我也在它基础上改了、又自己做了另外几个平台的数据集。

整个项目跑了好几个星期、五六次微调实验。但越往后我越意识到,真正难的不是把溯源率做上去,而是搞清楚这个数字到底该怎么算——因为同一个模型,换个口径,溯源率能从 29% 一直滑到 96%。这篇就讲这条主线。它也是我做过最较真的一段工程:在”挑个好看的口径交差”和”把口径说清楚”之间,我反复选了后者。

系统长什么样

输出是结构化 JSON:is_compliant + violations[],每条违规含原文片段、规则标题、规则说明、修改建议。推理是三阶段:全量扫描候选 → 逐条复核 → 按引用去重。规则按平台和类别组织,每次只把相关的一批规则给模型看。

四个平台的规则结构差异极大,这决定了后面所有方案:小红书 49 条 / 14 类、抖音 107 条 / 21 类、视频号 135 条 / 11 类、而公众号 53 条全挤在 1 个类别里。这一个数据结构差异,让公众号的效果比别的平台差出一截。

第一批假指标:0.0137 的 loss 和 93% 的准确率

第一版(exp_005)train_loss 收敛到 0.0137,某次评估准确率 93%,都好得反常。两个都是假的:loss 假在 cutoff_len=1024 把长样本的助手输出截断了、那部分 token 不参与损失计算,平均被算低(放开到 4096 重训,同样数据 loss 变 0.245,这才是真的);准确率假在评估时 padding_side="left" 去头保尾,规则正文被砍、模型几乎没看规则就靠”多数合规”的先验蒙。

这两个坑的教训是:异常好的 loss 先查截断和 mask,高准确率先核对模型真正看到的输入。 但它们只是开胃菜。

一个伪装成召回问题的数据 bug

四个平台里视频号最反常,规则召回一度 0%。我第一反应是”标题格式不匹配”,还列表断言”训练、规则库、验证集引用了三个不同文档”。然后强迫自己重查——把来源全列出来比对——发现第一版结论是错的。

去看模型的实际预测才挖到真因:模型认对了违规类型(它知道这是赌博内容),但条目编号是从训练记忆里掏的,不是从 prompt 复制的。因为训练数据里同一个”赌博”违规被打了 4 个不同文档的冲突标题(“第七条”20 次、“第五条”20 次、“第八条”13 次、“第二条”10 次),模型学成了”赌博 → 随机吐一个”。这是个数据标注问题,伪装成了模型召回问题。小红书是一对一所以加前缀就好,视频号一对多所以加前缀没用。

实验一路试到 exp_010

中间的方案演进不是一帆风顺,是一版版逼出来的:

  • exp_007(单规则二分类 + RAG,16056 条)失败——RAG 召回只有 5-35%,正确规则没进模型视野。
  • exp_008(batch 格式对齐三阶段推理,QLoRA 4-bit,AWQ INT4 量化到约 10GB)成了最终部署版。
  • exp_009(加 Hard Negative 冲 90%)灾难性失败:平均召回从 79.6% 暴跌到 46%。根因抽 5 条训练样本人工看才发现——make_hard_neg.py 去重只比规则短名、没比完整标题,导致正确答案自己被当成了 Hard Negative 放进 prompt,模型收到”正确答案是这三条”+“这三条别选”的矛盾信号,学成了”什么都不选”。5 分钟脚本回滚到 exp_008。

每次重训前我都得先算成本(4096 cutoff 让吞吐慢约 7 倍、一轮两三小时),所以养成了”冲新高前先钉死可回退基线”的习惯。

然后是溯源率本身:同一个模型,29% 到 96%

冲到后期,“溯源率”这个数字开始崩塌——因为它完全取决于你怎么定义。同一个 exp_008,我算出过这些:

口径数字它到底在衡量什么
严格溯源率(子 agent 看全部 344 条规则当标准答案)28.87%要求穷举所有该命中的规则
实用合理率(子 agent 二元判”方向对且言之有理”)91.23%判断是否站得住
per-rule 漂移率(用自身测试集,endswith 匹配)93.6%主规则对不对
sample-level(识别到”有违规”的二分类)96.9%有没有抓到违规
selfcheck 自洽率(拿模型自己的预测当标准答案重跑)94%只衡量两次推理一不一致

29% 和 96% 是同一个模型。差别全在口径,而其中两个口径尤其值得说清楚。

94% 自洽率是个盲点

最唬人的是那个 94% 的”自洽率”——拿模型自己的预测当标准答案再跑一遍,看一致不一致。它高达 94%,但它衡量的是可复现性,不是准确率:temperature=0 下模型输出本来就稳,连没微调的基座 Qwen2.5-14B 自己跟自己比也能到 90%+。所以这个 94% 根本证明不了微调起作用。我当时差点拿它当成绩,是被一句话点醒的——“你这个 94% 是自己跟自己比,那没微调的模型不也 90%+ 吗?“我把这个盲点如实写进了报告,没拿它充数。

28.87% 不是我的错——我拉了个更强的模型来证明

那最严的 28.87% 呢?它是让一个独立的子 agent 看全部 344 条规则、穷举出”应该命中的所有规则”当标准答案算出来的。这么低,第一反应是”模型太差”。

但我没有就此认栽,也没有偷偷换个好看的口径糊弄过去。我做了个对照实验:把当前最强的 DeepSeek-v4-pro 拉来,用完全相同的口径(看全集、子 agent 当标准答案)跑同一批题。 结果它的严格溯源率是 31.22%,我的模型是 32.92%——几乎打平。

这一下就说清楚了:28-32% 是这个口径本身的天花板,不是我的模型差。 根子在 RAG——子 agent 看的是全部 344 条规则,而模型的检索只召回 top-K(5-10 条),穷举口径下必然多漏。给业界最强的推理模型看全集,它也只能到 31%。一个数字低,先别急着怪模型,去问这个口径对任何模型是不是都这么低。

测试集本身也会骗你

口径之外,标准答案(GT)本身也有问题。后来有人拿另一份 Excel 测试集来测,测出来分很低。我用子 agent 把那 1653 条逐条审了一遍,发现它的 GT 硬错率 约 25%——平台标错、规则名是 LLM 编的(“无资质经营”这种规则在真实规则库里根本不存在)、什么都往”过度营销”这个万能筐里塞。

关键洞见是:用一份错的 GT 测出来的低分,衡量的是”被测模型和生成这份 GT 的那个 LLM 像不像”,不是真实能力。 如果我的模型更精准(把一条文案识别成”虚假医疗”而不是含糊的”过度营销”),它和那份糙 GT 的一致性反而更低、分数反而更难看。我把这点整理成可以直接转交的证据链——不是为了甩锅,是因为”拿错数据测出的低分恰好可能说明模型更好”这件事,本身就得讲清楚。

那真实水平到底是多少

把口径都摆清楚之后,我没有报那个最好看的数字。对外该说的是:识别”有没有违规”约 96.9%、识别得”实用合理”约 91.23%、而”穷举溯源到每一条规则”约 29%(受 RAG 召回天花板限制,业界最强模型同口径也是这水平)。 每个数字都标明口径。评估报告里我写的结论是”现有数据和检索架构下,穷举口径的工程天花板就在这,到不了 90%”。

一个溯源率,如果不写清楚”按什么算的、标准答案哪来的、这个口径对别的模型是不是也这样”,就是个能从 29 滑到 96 的橡皮筋。

这个项目教给我的

  • 没有单一指标可信,要顺着输入查到数据。loss 低可能是被截断算小的;准确率高可能是评估格式喂错;“0% 召回”可能是训练标签冲突让模型背答案;“94% 自洽”可能只是模型跟自己比。
  • 自己的第一个分析也会骗你。视频号那次我第一版结论是错的,靠采样得来的。下结论前问一句:这是我看到的,还是我以为的?
  • 一个数字低,先怀疑口径,再怀疑模型。用同样口径拉个更强的模型一起跑,立刻能分清是”模型不行”还是”这个口径对谁都这么低”。
  • 指标的口径就是诚实本身。最该警惕的不是低指标,是那个你为了好看而悄悄选松的口径。把口径写清楚、敢说”自洽率证明不了能力”、敢说”这份测试集的 GT 是错的”,比把数字做高更重要。

这个模型最难的从来不是”判得出违规”,而是”指得准是哪一条、并且老老实实说清楚指得有多准”。挡在 90% 前面的,多数时候不是模型不够强,是数据没干净、指标在骗人、口径在放水。