AI 自动评分功能实现方法详解(AI 大模型评测平台的多评委交叉验证)

多评委交叉验证解决的核心问题,是单个模型打分既不稳还偏心。最初用单个大模型当裁判,上线一周就发现评分明显偏高,同一批答案换个模型重评,分数能差出一分多,用户投诉接二连三。后来我引入了 LLM-as-Judge 加多评委交叉验证:多个不同家族的模型独立打分,取中位数,再算方差,方差大就转人工复核。这是平台最有技术含量的部分,下面把实现细节完整拆开,包括评分 prompt 怎么写、并行怎么调、阈值怎么定、成本怎么压。

一、为什么需要多评委交叉

单评委方案的问题不是某一个模型的锅,而是结构性缺陷。模型在给自己家族的输出打分时会不自觉地放水,这叫自我偏好;不同模型对不同题型(创意、技术、数学)的把握也不一样,评分标准天然不齐;再加上推理过程的随机性,同样输入两次打分都可能不一样。这三条叠加在一起,单评委的分数就很难让人信服。当时我们收集到的现象主要有三类:

  1. 自我偏好:GPT-4 评 GPT-4 输出普遍偏高 0.5-1 分;
  2. 领域偏见:不同模型对不同类型题目(创意/技术/数学)评分标准不一;
  3. 随机性:温度 > 0 时同一答案两次评分可能差 0.5-1 分。

针对这三个问题,多评委交叉验证的逻辑是这样设计的:

  • 多个独立模型打分 → 取中位数 → 算方差;
  • 方差小(< 阈值)= 评分一致 → 取中位数作最终分;
  • 方差大(> 阈值)= 评分分歧 → 触发人工复核。

说白了,多评委的本质是把”一个人的判断”升级成”一个小组的投票”,用统计量而不是单个模型的输出来兜底。中位数抗极端值,方差识别分歧,两条一起用,比任何单一指标都稳。

二、整体架构

架构上我们刻意保持简单:一个评分服务入口,三个评委并行,中位数定分,方差分流,全部串成一条直通链路。

评分请求
   ↓
评分服务
   ├─ 选 3 个评委(不同家族)
   ├─ 并行调用 3 个 LLM
   │   ├─ GPT-4o 打分 → score1
   │   ├─ Claude 打分 → score2
   │   └─ DeepSeek 打分 → score3
   ├─ 中位数算法 → finalScore
   ├─ 方差检测
   │   ├─ 方差小 → 入库
   │   └─ 方差大 → 人工复核队列
   └─ 写库 + 通知

这个流程跑通后,线上评分一致性从单评委时代的七成出头提升到九成以上,人工复核率稳定压在 10% 以内。用户对分数质疑的工单数量明显下降,这算是整套方案最直接的验收标准。

三、评分 Prompt 设计

评委模型各不相同,但评分标准必须统一,否则三个模型的分数没有可比性。我们把角色设定、评分标准、输出格式、待评估内容全部写进同一个模板,对三个评委一视同仁,差异只体现在模型本身的风格上。

# 角色
你是一名资深的 AI 答案质量评审员。

# 评分标准
- 9-10:与参考答案一致,覆盖所有关键点
- 7-8:整体正确,遗漏次要细节
- 5-6:方向正确但有明显错误
- 3-4:偏离主题
- 0-2:与问题无关

# 输出格式(严格 JSON)
{
  "score": 8,
  "dimensions": {
    "accuracy": 9,
    "completeness": 7,
    "usability": 8
  },
  "issues": ["未提及 X 的边界条件"],
  "summary": "答案整体可用,但 X 场景不完整"
}

# 待评估内容
问题:{question}
参考答案:{reference}
待评答案:{candidate}

模板里最关键的约束是”严格 JSON 输出”,它直接决定了后面解析层的成功率。我们试过不给格式约束,模型会输出一大段解释文字再把分数藏在里面,解析起来非常痛苦。另外把参考答案也放进模板,是为了让”对错”有据可依,而不是模型凭感觉给分。

四、并行调用实现

三个评委如果串行调用,评分耗时就是三次调用之和,一个批次用户要等一分钟以上,完全不可接受。我们基于 WebFlux 的 Flux 并行发起三个请求,全部返回后统一汇总。

@Service
public class JudgeService {
    @Autowired private List<JudgeModel> judges;  // 注入 3 个评委
    
    public JudgeResult score(ScoringRequest req) {
        // 1) 并行调用 3 个评委
        List<JudgeResult> results = Flux.fromIterable(judges)
            .flatMap(j -> j.scoreAsync(req)
                .subscribeOn(Schedulers.boundedElastic()))
            .collectList()
            .block();
        
        // 2) 解析分数
        List<Integer> scores = results.stream()
            .map(JudgeResult::getScore)
            .sorted()
            .toList();
        
        // 3) 中位数
        int median = scores.size() % 2 == 1
            ? scores.get(scores.size() / 2)
            : (scores.get(scores.size()/2 - 1) 
               + scores.get(scores.size()/2)) / 2;
        
        // 4) 方差
        double variance = variance(scores);
        if (variance > VARIANCE_THRESHOLD) {
            reviewQueueService.push(req, results);
        }
        
        // 5) 汇总多维分
        Map<String, Double> dimAvg = averageDimensions(results);
        return new JudgeResult(median, dimAvg, results, variance);
    }
}

选中位数而不是平均数,是为了抗极端值——万一某个评委抽风给出 2 分,平均数会被明显拉低,中位数几乎不受影响。方差检测放在中位数之后,目的就是识别这种分歧并把请求送进人工复核,而不是悄悄吞掉异常。

五、评委选型策略

评委不是随便凑三个模型就行的。我们的硬约束是跨家族,GPT 家族、Claude 家族、国产模型各自出评委,避免同源的模型相互印证、集体放水。不同评委在不同题型上各有强项,下面这张表是我们的选型依据。

评委 优势 适用
GPT-4o 综合能力强 默认评委
Claude-3.5 逻辑严谨 代码/数学题
DeepSeek 中文理解强 中文场景
国产大模型 性价比高 大批量评分

默认组合是 GPT-4o 加 Claude 加 DeepSeek,覆盖了综合、逻辑、中文三个方向,实际跑下来评分分歧率在三个候选组合里是偏低的。工程上我们用 Spring 的 @Qualifier 注入不同实现,加新评委只写一个类就行,不用动评分逻辑。

public interface JudgeModel {
    String name();
    Mono<JudgeResult> scoreAsync(ScoringRequest req);
}

@Service("gpt4o") public class GPT4oJudge implements JudgeModel { ... }
@Service("claude") public class ClaudeJudge implements JudgeModel { ... }
@Service("deepseek") public class DeepSeekJudge implements JudgeModel { ... }

@Configuration
public class JudgeConfig {
    @Bean
    public List<JudgeModel> defaultJudges(
        @Qualifier("gpt4o") JudgeModel gpt4o,
        @Qualifier("claude") JudgeModel claude,
        @Qualifier("deepseek") JudgeModel deepseek
    ) {
        return List.of(gpt4o, claude, deepseek);
    }
}

评委列表通过配置类装配成 Bean,后续如果要按用户选择评委组合,只要动态注入不同的 List 实现,评分核心逻辑一行都不用改。

六、方差阈值设定

方差阈值是整套方案的灵魂参数。设大了,分歧全漏进自动评分;设小了,人工复核队列瞬间爆满。我们不用拍脑袋的经验值,而是拿历史评分数据来定。

// 收集过去 30 天评分数据
// 相同答案不同评委分数的方差分布
// 经验值:90% 情况 variance < 1.5
private static final double VARIANCE_THRESHOLD = 1.5;

// 动态调整:每月统计
public void updateThreshold() {
    double p90 = scoreVarianceLog.percentile(0.9);
    VARIANCE_THRESHOLD = p90;
}

p90 的思路很直观:过去 30 天里 90% 的正常评分方差都低于这个值,超过的才算异常,值得人工介入。阈值每个月滚动更新,模型升级、评分 prompt 调整后,阈值会自动跟着漂移,不用人工干预。

七、JSON 解析与降级

即使 prompt 里写了严格 JSON,模型偶尔还是会在外面包一层解释文字,甚至输出一段完全没有 JSON 的文本。解析层必须做三级降级,逐级兜底。

public JudgeResult parseResponse(String response) {
    try {
        return JsonUtil.parse(response, JudgeResult.class);
    } catch (JsonParseException e) {
        // 降级 1:提取 JSON 块
        Pattern p = Pattern.compile("\\{[\\s\\S]*\\}");
        Matcher m = p.matcher(response);
        if (m.find()) {
            try {
                return JsonUtil.parse(m.group(), JudgeResult.class);
            } catch (Exception ignore) {}
        }
        
        // 降级 2:提取第一个数字作为 score
        Pattern num = Pattern.compile("\\b(\\d{1,2})\\b");
        Matcher m2 = num.matcher(response);
        if (m2.find()) {
            int score = Integer.parseInt(m2.group(1));
            return JudgeResult.simple(score, "JSON 解析失败降级");
        }
        
        // 降级 3:标记失败
        throw new JudgeParseException("无法解析评分: " + response);
    }
}

三级降级把解析失败率从最初的 15% 压到 1% 以内。降级 2 只提取第一个数字,风险是可能取到别的数字,所以我们会给结果打上”降级”标记,让后续的统计和质量监控能识别这类样本,单独分析。

八、成本控制

3 个评委意味着每次评分的成本是单评委的 3 倍,评测规模上来之后账单很吓人。我们做了四档降本策略,每一档都是基于”按需置信度”的思路。

策略 实现 节省
短答案只 1 评委 长度 < 200 字符 60%
高置信度 1 评委 短答案 + 高 temperature 50%
小模型当评委 优先级低的任务用国产小模型 70%
结果缓存 相同答案+模型 24h 缓存 30%

组合拳打下来,整体成本大约降到全量三评委方案的 55% 左右,评分质量没有明显下降。关键判断是:短答案和低优先级任务不需要那么高的置信度,用单评委足够。代码上的落地很直接,按候选答案长度分流。

public JudgeResult score(ScoringRequest req) {
    if (req.getCandidate().length() < 200) {
        // 短答案:单评委
        return judges.get(0).scoreAsync(req).block();
    }
    // 长答案:3 评委交叉
    return scoreWithMultipleJudges(req);
}

200 字符这条线是统计出来的:长度低于它的答案信息量有限,多评委和单评委的分歧率本来就低,砍掉两个评委对质量影响很小。这一刀是性价比很高的一档。

九、人工复核机制

方差超阈值的请求会进”待复核”队列,需要真人来看。队列表设计上把三个评委的原始输出全部存下来,复核人一眼就能看到分歧点在哪,而不是对着一个孤零零的最终分发呆。

CREATE TABLE judge_review_queue (
    id          BIGINT PRIMARY KEY,
    request_id  BIGINT NOT NULL,
    candidate   TEXT,
    reference   TEXT,
    judge_results JSON,    -- 3 个评委的原始输出
    variance    DECIMAL(4,2),
    status      VARCHAR(16) DEFAULT 'pending',  -- pending/in_review/resolved
    final_score INT,
    reviewer_id BIGINT,
    created_at  DATETIME,
    INDEX idx_status_created (status, created_at)
);

status 字段支持 pending、inreview、resolved 三态流转,索引建在 (status, createdat) 上,后台按时间倒序拉待办,翻页很顺。管理后台提供一个列表接口,默认按状态筛选。

@GetMapping("/admin/review/list")
public Page<ReviewVO> listPending(
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "20") int size
) {
    return reviewService.listPending(page, size);
}

复核人改完最终分后状态置为 resolved,系统会回调更新评测结果表。这套闭环跑通后,人工复核不再是个黑盒,谁复核的、改了什么分,全程留痕。

十、为什么不用 reward model

方案评审时有人提议用 reward model 做评分——让模型学会打分,而不是每次推理打分。我们认真评估过,结论是现阶段不划算,成本收益差太多。

维度 LLM-as-Judge Reward Model
训练成本 零 数十万 + 标注数据
跨模型泛化 强 弱(只评训过的领域)
维护成本 改 prompt 即可 重训
可解释性 强(输出 issues) 弱(一个分数)
适用 中小规模平台 大厂核心业务

reward model 强在速度快、成本低,但它的泛化边界是训练时见过的领域,换个题型、换个模型家族就要重训,而且只输出一个分数,没有理由可看。我们的用户会追问”为什么给这个分”,LLM-as-Judge 的 issues 字段就是现成的解释。平台规模 1000-10000 答案/天,LLM-as-Judge 完全够用。Reward Model 是大厂游戏。

十一、踩过的坑

这套方案从原型到上线,踩的坑比预想的多。列出来的这几条,每一条都改写过我们的实现,写下来给后来的人。

  • JSON 解析失败率高:模型输出多了换行/解释文字。prompt 加”严格 JSON 格式” + 解析层降级。
  • 评分漂移:同一答案隔天评分差 1-2 分。温度设 0 + 多次评分取众数。
  • 评委超时拖累整批:3 评委任一超时整个任务失败。评委独立超时(30s)+ 失败重试 1 次。
  • 评分员泄题:GPT-4 评 GPT-4 答案触发先验。评委必须跨家族。
  • 维度分数不稳定:dimensions 字段经常少 1-2 维。prompt 强调”必须输出 3 个维度”。

这些坑的共性是:模型行为不可控,所以代码和 prompt 都要按”模型会出错”来设计,而不是按”模型很听话”来设计。解析降级、超时隔离、跨家族选评委,本质都是在给模型的不可控性兜底。

十二、与 SxS 评测的集成

SxS(Side-by-Side)评测是平台的另一条产品线:多个模型答同一道题,横向比谁答得好。多评委机制天然能嵌进去,每个答案独立评分,再按分数排序。

@PostMapping("/sxs/submit")
public BaseResponse<Long> submit(@RequestBody SxSRequest req) {
    // 1) 调 N 个模型生成答案
    List<Answer> answers = parallelCall(req.getModels(), req.getQuestion());
    
    // 2) 每个答案独立评分
    List<ScoredAnswer> scored = answers.stream()
        .map(a -> new ScoredAnswer(a, judgeService.score(toScoringRequest(a))))
        .sorted(Comparator.comparingInt(ScoredAnswer::getScore).reversed())
        .toList();
    
    // 3) 排序后返回
    return ResultUtils.success(sessionService.create(req, scored));
}

集成之后,SxS 的结果直接带分数排序,用户看到的不是孤立的答案,而是有评分依据的横向对比。到这里,多评委方案在平台的落地链路就完整了——从单答案评分到 SxS 对比,用的都是同一套评分内核。

常见问题(FAQ)

Q1:评委能选吗?

能。高级用户可自定义评委组合(如”只用国产模型”),平台按选择调用。

Q2:人工复核要多久?

3 个工作日内。紧急工单标记 priority=high,2 小时内响应。

Q3:评分公平性怎么验证?

平台每月抽 100 个”评委一致”的答案做人工抽检,准确率 < 90% 触发 prompt 调整。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
赵其鑫的头像赵其鑫管理团队

相关推荐

返回顶部