Article

AI Harness 实战 03:评分器与裁判体系(规则判定 + LLM-as-Judge 的混合策略)

前两篇我们分别解决了:

  • 第 01 篇:评测基线可重复、可对比、可回归
  • 第 02 篇:样本工程可分层、可演进、可闭环

接下来会遇到第三个工程瓶颈:

  • 回归能跑,但结论不可信
  • 同一输出,不同评审人结论不同
  • 指标在涨,但业务方仍认为“质量没提升”

根因通常不在模型本身,而在评分器系统:判定标准不稳定、不可解释、不可校准。

这篇目标很明确:搭建一个规则判定 + LLM-as-Judge 的混合裁判体系,让评测结果既稳定又贴近业务语义。

一、为什么“只有规则”或“只有 LLM Judge”都不够

单一路径都会失真:

  1. 只有规则判定
  • 优点:稳定、可重复、可追责。
  • 缺点:覆盖语义能力弱,容易出现“形式正确、实质错误”。
  1. 只有 LLM-as-Judge
  • 优点:语义理解强,覆盖复杂场景能力高。
  • 缺点:波动性高,提示词和上下文稍变就可能影响分数。

工程上最稳的做法不是二选一,而是分层混合:

  • 规则负责“硬约束”
  • LLM Judge 负责“软质量”
  • 最终由聚合器输出可解释总分与阻断结论

二、裁判体系三层结构

建议固定为三层流水线:

Output
  -> Gate Rules(硬门禁)
  -> Quality Judges(语义裁判)
  -> Score Aggregator(加权聚合与阻断决策)
  -> Final Verdict

1)Gate Rules(硬门禁)

用于不可妥协约束:

  • 安全策略违规
  • 输出格式不合法
  • 关键字段缺失
  • 明确越权建议

规则层一票否决,直接 Fail,不进入语义加权。

2)Quality Judges(语义裁判)

用于质量维度打分(0-5 或 0-100):

  • 准确性(Correctness)
  • 可执行性(Executability)
  • 完整性(Completeness)
  • 可维护性(Maintainability)
  • 风险意识(Risk Awareness)

每个维度应独立提示词与独立输出字段,避免“一个总分掩盖问题”。

3)Score Aggregator(聚合器)

把多维结果转换为可发布决策:

  • 维度加权总分
  • 分层阈值(L1/L2/L3/L4)
  • 失败原因 TopN
  • 与 baseline 的差异(Delta)

三、评分协议:先定义“裁判输入合同”

很多团队评分不稳定,是因为每次给 Judge 的上下文不一致。先固定协议:

{
  "case_id": "task_code_review_203",
  "layer": "L2-Task",
  "input": "请审查这段并发逻辑并给出修复建议",
  "model_output": "...",
  "expected_constraints": {
    "must_include": ["竞态条件", "最小修改方案"],
    "must_not_include": ["跳过测试", "直接上线"]
  },
  "rubric": {
    "correctness": "结论是否与事实一致",
    "executability": "建议是否可直接落地",
    "maintainability": "是否引入额外技术债"
  }
}

合同固定后,你才有资格谈“分数可比较”。

四、LLM Judge 抗漂移:三种稳态机制

要把 LLM Judge 当工程组件,而不是灵感组件:

  1. 模板固定化
  • 使用版本化 judge prompt(judge-v1, judge-v2),禁止临时改词上线。
  1. 多裁判一致性
  • 同一 case 运行 2-3 次,计算方差;方差超阈值则标记 unstable
  1. 基准锚点校准
  • 每次评测混入固定锚点样本(高分/低分),若锚点偏移超阈值则本次结果降级为仅参考。

五、C# 混合评分最小实现

public sealed class RuleCheckResult
{
    public bool Passed { get; set; }
    public List<string> Violations { get; set; } = new();
}

public sealed class JudgeScore
{
    public double Correctness { get; set; }
    public double Executability { get; set; }
    public double Completeness { get; set; }
    public double Maintainability { get; set; }
    public double RiskAwareness { get; set; }
    public bool Unstable { get; set; }
}

public sealed class Verdict
{
    public string CaseId { get; set; } = string.Empty;
    public bool Passed { get; set; }
    public double TotalScore { get; set; }
    public List<string> Reasons { get; set; } = new();
}

public static class HybridJudge
{
    public static Verdict Evaluate(HarnessCase testCase, string output, JudgeScore judgeScore)
    {
        var rule = CheckHardRules(testCase, output);
        if (!rule.Passed)
        {
            return new Verdict
            {
                CaseId = testCase.Id,
                Passed = false,
                TotalScore = 0,
                Reasons = rule.Violations
            };
        }

        var total = WeightedScore(judgeScore);
        var passed = total >= 3.8 && !judgeScore.Unstable;

        var reasons = new List<string>();
        if (!passed)
        {
            if (judgeScore.Unstable) reasons.Add("judge_unstable");
            if (judgeScore.Correctness < 3.5) reasons.Add("low_correctness");
            if (judgeScore.Executability < 3.5) reasons.Add("low_executability");
        }

        return new Verdict
        {
            CaseId = testCase.Id,
            Passed = passed,
            TotalScore = total,
            Reasons = reasons
        };
    }

    private static RuleCheckResult CheckHardRules(HarnessCase c, string output)
    {
        var result = new RuleCheckResult { Passed = true };

        foreach (var token in c.Expected.MustInclude)
        {
            if (!output.Contains(token, StringComparison.OrdinalIgnoreCase))
            {
                result.Passed = false;
                result.Violations.Add($"missing:{token}");
            }
        }

        foreach (var token in c.Expected.MustNotInclude)
        {
            if (output.Contains(token, StringComparison.OrdinalIgnoreCase))
            {
                result.Passed = false;
                result.Violations.Add($"forbidden:{token}");
            }
        }

        return result;
    }

    private static double WeightedScore(JudgeScore s)
    {
        return s.Correctness * 0.35
             + s.Executability * 0.25
             + s.Completeness * 0.15
             + s.Maintainability * 0.15
             + s.RiskAwareness * 0.10;
    }
}

这个实现重点不在复杂算法,而在可解释性:

  • 先硬规则,再语义加权
  • 每个失败有明确原因码
  • 阈值与权重可版本化管理

六、报告设计:必须可审计,而非只看一个分数

每次运行至少输出以下字段:

  • rule_pass_rate
  • judge_avg_score
  • judge_variance
  • unstable_case_count
  • blocked_by_rule_count
  • blocked_by_score_count
  • top_failure_reasons

并固定附带:

  • judge_prompt_version
  • rubric_version
  • baseline_version
  • run_id

这样才能回答关键问题:是模型变差、评分器漂移,还是样本结构变化。

七、门禁建议:先“并行裁判”,再“单轨切换”

避免一次性替换评分体系,建议两阶段:

  1. 并行期(1-2 周)
  • 旧评分器与新混合裁判并行运行,只告警不阻断。
  1. 切换期
  • 当新裁判与人工复核一致率达到阈值(如 >= 90%)后,切主门禁。
  1. 稳定期
  • 每周做一次锚点样本校准,防止 Judge 漂移。

八、第 03 篇结论

评测体系的可信度,取决于裁判体系,而不是模型口碑。

把“硬规则底线 + 语义评分上限 + 稳态校准机制”三件事建立起来,评测结果才会真正可用于发布决策。

下一篇进入《Harness 实战 04:评测运营与发布门禁(把评测结果变成真实交付控制面)》,解决“有指标但管不住发布”的最后一公里问题。