迭代记录我怎么修正自己的评测体系

项目:AI 内容生产 Agent 的质量评测体系 · v1.1 → v1.2

这一页记录一件我认为比"系统做得多完整"更重要的事: 把判分器当成被测对象,反过来审它自己。
结果发现 4 类问题——其中一个让安全红线在特定输入下静默失效, 一个是方法的固有缺陷(调参救不了)。四项现在都已修复或显式声明。
每一项都留了回归测试或对比验证,命令在页面底部。

先说结论

验证项修复前修复后
短稿违规是否触发否决否(红线被旁路)是 ✅
规则分取值数3 个7 个 ✅
严重度能否区分踩 1 条 = 踩 2 条1 条 59 / 2 条 54 ✅
C1 的召回盲区无度量可诊断 ✅
改写 vs 无关的区分余量−0.018(无法区分)+0.288 ✅
回归测试无3 组 ✅

发现 1 · 安全红线被静默关闭

C3 的"低置信度护栏",在短稿上把一票否决关掉了 最严重 已修复

判分器有个护栏:当输入明显不是纯口播稿时,不给出误导性的分数。判断条件是"违规比例 > 60%"。

// 原实现
low_confidence = total_numbers > 0 and violation_count / total_numbers > 0.6
if low_confidence:
    result["status"] = "结果不可用(低置信度)"
    result["veto"] = False   // ← 把一票否决关掉了

问题:样本量很小时,比例极易达到 100%。
一篇 300 字的正常短稿只要出现 1 个无出处数字 → 总数字 1、违规 1 → 比例 100% → 触发护栏。
结果不是"违规",而是"结果不可用",并且 veto 被置为 False。

为什么这比普通 bug 更危险
一票否决的设计前提是误放代价不可逆。而这个缺陷让红线在特定输入下自动失效—— 而且系统显示的不是"违规",是"结果不可用"。
人看到这个状态,不会去拦内容,只会以为"这稿子输入格式有问题"。
它不会报错,只会静默地放过违规内容。

修复:两处改动,各有明确理由。

// ① 加绝对样本量下限:样本不足时只提示、不降级
C3_MIN_SAMPLE_FOR_GUARD = 4
guard_triggered = (total_numbers >= C3_MIN_SAMPLE_FOR_GUARD and ratio > C3_GUARD_RATIO)

// ② 置信度与 veto 解耦:置信度只决定"分数能不能用",永远不能关闭违规结论
result["veto"] = bool(violations)

修复前 · 短稿 1 个无出处数字

veto = False ✗

修复后 · 同一输入

veto = True ✓
注意一个细节:修复后 降级依然发生(提示"这个分数不可直接采用"), 但违规结论与一票否决同时成立。
护栏的作用从"关闭结论"变成了"提示分数不可用"——这才是它本来该做的事。

发现 2 · 召回率从未被测量

C1 的判定是串联两级,我只测了第二级 方法固有缺陷 盲区已可见

人工和判分器判的是同一批被抽取出来的候选。没被抽出的,永远进不了混淆矩阵。

脚本全文→ ① 抽取层(会漏)→ 候选集→ ② 判定层→ 混淆矩阵

假设脚本里真实有 5 个自创类比:

违规 A(含"就像")    → 进入候选 → 判分器正确判违规  → ✅ TP
违规 B(含"就好比")  → 进入候选 → 判分器正确判违规  → ✅ TP
违规 C(措辞罕见)    → 未被抽出 → 从未被判定 → 直接"通过"流出 → ❌ 不进矩阵
违规 D(措辞罕见)    → 同上
违规 E(措辞罕见)    → 同上

表格报的: 0/2 = 0%     ← 分母只有候选集里的 2 个
真实情况: 3/5 = 60%    ← 分母应该是全文的 5 个
判分器一次都没判错,但系统漏掉了 60% 的自创类比。
因为 端到端 = 抽取召回率 × 判定准确率 = 40% × 100% = 40%,而我只测了右边那个 100%。

这不是参数没调好,是指标结构上看不见这类错误。 "一致率 ≥80%" 无法证明 C1 拦得住红线——因为未被抽出的假阴性不在任何指标的分母里。

修复:新增召回率诊断,只诊断、不改变判定结论(含标记的句子未必都是类比)。

输出字段含义
coverage含标记的句子中,实际参与判定的比例
unextracted_marked含标记但未参与判定的句子(需人工确认是不是类比)
possible_missed_nonmarker无标记词但形态像类比的句子 —— 直接暴露标记词表的盲区

这项的边界(如实记录)

  • 这只是诊断,不是召回率本身的测量。真正的召回率需要人工通读全文标出所有类比作为分母。
  • 标记词表是人工维护的,第三项字段非空即说明词表有盲区。
  • 召回率标定已列入下一步待办。

发现 3 · 规则分退化成二值,没法做版本对比

"规则分用于版本对比",但实测只有 3 个取值 覆盖范围问题 已修复

扣分逻辑只对 C2/C4/C5/C6 生效,但 C2/C5 压根没被调用,C6 当时没有计数字段 → 恒不扣分。

distinct rule_score values = [59, 90, 100]

C1=0 C3=0 C4miss=0 -> 100
C1=0 C3=0 C4miss=1 ->  90
C1=1 C3=0 C4miss=0 ->  59
C1=1 C3=1 C4miss=0 ->  59      ← 踩 1 条红线 = 踩 2 条红线
C1=1 C3=1 C4miss=1 ->  59

三个问题:① 取值只有 3 个,撑不起"版本对比";② 踩 1 条红线和踩 2 条同分,严重度无法区分;③ C2/C5 的扣分分支永远不会执行。

修复:C6 初筛产出计数字段纳入扣分;一票否决按命中项数体现严重度;新增 score_scope 显式声明哪些维度参与计分。

修复前取值

[59, 90, 100]

修复后取值

[54,59,80,85,90,95,100]
新增 score_scope 的价值:不会再出现"看起来是 6 项综合分,实际只有 2 项在扣分"的误读。

发现 4 · 换词不换义,字符比对区分不了

两种相似度方法的实测对比 方法固有缺陷 已换方法

原方法用字符二元组 Jaccard 算相似度。构造四档改写强度实测:

改写强度字符基线(原)LSA 语义匹配(新)
① 库内原句1.000 ✅ 命中1.000 ✅ 命中
② 轻度改写0.855 ✅ 命中0.998 ✅ 命中
③ 中度改写(换词不换义)0.059 ❌0.833 ✅
④ 重度改写0.020 ❌0.709 ✅
⑤ 完全无关类比0.0380.421
改写最低 − 无关 的余量−0.018+0.288
关键发现:字符基线的余量是负数。
改写最低 0.020 < 无关 0.038 → 余量 −0.018

这意味着存在一个"改写分数比无关类比还低"的区域。 任何阈值都无法同时正确区分这两类。
这不是"阈值没调好",是方法的固有缺陷——遇到负余量,要做的是换方法,不是继续调参。

修复:离线语义检索,纯 numpy 实现,不依赖任何外部 API。

层实现替代了什么
分词自写 tokenizer(中文字 + bigram + 英文/数字词)不需要 jieba
向量化自写 TF-IDF(sublinear tf + 平滑 idf + L2 归一化)替代 sklearn
语义空间自写幂迭代 SVD → LSA 潜在语义分析替代 sentence-transformers
相似度余弦相似度—
为什么选择离线而非 API:与判分器本身"能代码判的绝不用模型判"是同一个原则—— 能离线跑的,不要依赖 API。
离线实现让结果可复现(模型不会漂)、零成本、断网可用、面试现场双击即可演示。

三个设计决策,依据都来自业务需求(不是技术惯例):

决策选择理由
chunk 粒度一条类比 = 一个 chunk C1 的判定单位就是"一条类比"。若 chunk 是整段,向量是整段的平均语义 → 检索不准
双重向量原句 + 改写模板各一个向量,取较高分 库里既有原句也有抽象模板,两者都能命中
元数据每条携带来源篇号 / 分类 / 模板 C1 判定需指出"类比来自哪一篇",否则无法追溯

这项的边界(如实记录)

  • 模板覆盖率只有 22%(5/23 条)。只有"明确保留"的 5 条被人工改写为模板, 其余 18 条退化为原文向量单路检索——这是已知的召回上限,不是 bug, 是保守保留策略的代价。
  • 语义检索目前尚未接入判定链路,还是独立的验证脚本。已列入下一步。
  • LSA 是统计方法,不是真正的语义模型。它能抓住共现语义,但不理解世界知识—— 在 23 条的小库上够用,库大了需要更强的方法。

我从这轮迭代里得到的判断

评测体系必须先验证自己

判分器是测量内容合格与否的尺子。尺子本身不准,后面跑出的所有质量分、所有改善结论都不成立。 所以顺序是:先把判据定死并校准 → 再接入生产链路。这个顺序是有意的取舍,不是遗漏。

"算不准"不能成为放过违规的理由

C3 那个缺陷给我的教训是:"这个分数能不能用"和"内容合不合格"是两件事,必须解耦。 混在一起的后果是——系统用"我算不准"为理由,放过了"确实违规"。

指标方向要匹配红线性质

红线项的指标方向一律是"越低越好"(漏放率),不是"越高越好"(一致率)。 标定时必须按"漏放优先"选点:先保证漏放率≈0,再在满足条件的区间里选误拦最低的。

负余量意味着要换方法,不是调参

字符相似度的余量是 −0.018。任何阈值都救不了一个负余量。 遇到这种情况,要做的是换方法。

能离线跑的,不要依赖 API

和"能代码判的绝不用模型判"是同一个原则。离线实现让结果可复现、零成本、断网可用、现场可演示。

下一步

序动作目的
1把语义检索接入判定链路,加 --embedding 开关让修复真正作用到判定,支持 A/B 对比
2人工通读全文标出所有类比,标定抽取召回率补上红线验证的最后一环
3扩阈值标定样本到 ≥100 条,按"漏放优先"重选阈值把 0.25 从"实测值"变成"标定值"
4为保守保留的 18 条补写模板模板覆盖率 22% → 提升,直接提升召回
5跑 30 条用例 → 人工标真值 → 填一致率表拿到三个真实数字
6补 C2/C5 的判定(先人工标 10 条验证一致率)让合格率的分母完整

怎么复现

# 判分器自测 + 3 组回归测试
python run_eval.py --self-test

# 两种相似度方法的对比验证
python _verify_lsa.py
判分器自测输出:SELF TEST RESULT: PASS(含 3 组回归测试)
语义检索验证输出:字符基线余量 −0.018 / LSA 余量 +0.288

我主动说的边界

不用你追问,我先说

相关材料

判据是怎么定的

6 条约束、三档判定、30 条用例,以及 7 类判据设计错误的完整留档。

看评测方法 →

原始 PRD 与产品决策

目标与非目标、关键决策取舍表、v1 为何先做单链路。

看 PRD →

迭代记录(全文)

本页对应的 Markdown 全文,含完整代码片段与验证输出。

看全文 →