Langchainrust:中LLM-as-a-Judge,用 Rust 实现一套 LLM 评估系统

LLM-as-a-Judge:用 Rust 实现一套 LLM 评估系统

代码仓库:langchainrust · 全部可跑,文末有真实测试数据

一、为什么需要 LLM 评估

LLM 应用落地后,绕不开一个问题:输出到底好不好?

改了一版 prompt,换了模型,接了 RAG,加了个 Agent--每次改动都需要量化"是变好了还是变差了"。靠人眼看几百条不现实,得自动评估。

评估这件事,业界把它拆成两类:

  • 有参考评估(reference-based):有标准答案,拿模型输出跟它比。简单、客观,但很多任务没有标准答案(翻译、摘要、开放式问答)。
  • 无参考评估(reference-free):没有标准答案,按有用性、安全性、连贯性等维度直接评。灵活,但更难,通常需要"懂语义"的判官。

传统自动指标(BLEU、ROUGE、精确匹配)在有参考场景用了几十年,但它们都是字面比对:模型答"法国的首都巴黎",参考是"巴黎",意思对,字面不等,直接判错。这对 LLM 这种"话多但意思对"的输出是灾难。

于是有了 LLM-as-a-Judge:用一个 LLM 当阅卷老师,让它读懂语义后打分。这篇讲怎么在 Rust 里实现一套完整的 LLM 评估系统,从最笨的字面匹配,一直到带偏差缓解的 LLM 裁判。


二、评估的核心抽象

评估就一个 trait--给 (输入, 模型预测, 参考答案),吐一个 0.0--1.0 的分数:

rust 复制代码
#[async_trait]
pub trait Evaluator: Send + Sync {
    async fn eval(
        &self,
        input: &str,        // 原始输入
        prediction: &str,   // 模型预测
        reference: &str,    // 参考答案
    ) -> Result<Score, EvalError>;

    fn name(&self) -> &str;
}

被测对象(LLMChain / Agent / 任何东西)抽象成 Predictor:

rust 复制代码
#[async_trait]
pub trait Predictor: Send + Sync {
    async fn predict(&self, input: &str) -> Result<String, EvalError>;
}

EvalRunner 把它们串起来:遍历数据集 -> 每条样例让 predictor 预测 -> 用所有评测器各打一次分 -> 汇总成 Report(逐条得分 + 各评测器平均分)。这个设计让"被测对象"和"打分标准"完全解耦,同一个数据集可以换不同评测器跑。

下面看四种评测器,从笨到聪明。每个评测器后面都附一个真实跑出来的例子(GLM-5.2 裁判 + LocalEmbeddings),直观看它的局限。


三、十种评测器:从字面到语义到规则到 RAG

按打分依据分五类:

类别 评测器 依据 成本
字面 ExactMatch 字符串相等 0
字面 StringDistance Levenshtein 编辑距离 0
语义 EmbeddingSimilarity 向量余弦相似度 0
语义 LLMAsJudge LLM 读懂语义打分 每条调一次 LLM
规则 ContainsKeyword 是否包含关键词 0
规则 RegexMatch 正则匹配 0
规则 LengthCheck 长度范围 0
经典NLP Bleu n-gram 精度 + 短句惩罚 0
语义 PairwiseJudge LLM 成对比较(二选一,带位置消偏差) 每次调 2 次 LLM
RAG Faithfulness 拆陈述逐条验证能否从上下文推导(幻觉检测) 每条陈述调 1 次 LLM

下面逐个看十种(前四种带真实例子,后六种带构造例子)。

1. ExactMatch -- 精确匹配

最笨:两个字符串去空格后相不相等。相等 1.0,不等 0.0。

rust 复制代码
pub struct ExactMatch;

#[async_trait]
impl Evaluator for ExactMatch {
    async fn eval(&self, _input: &str, prediction: &str, reference: &str) -> Result<Score, EvalError> {
        let matched = prediction.trim() == reference.trim();
        Ok(Score::new(if matched { 1.0 } else { 0.0 })
            .with_label(if matched { "match" } else { "mismatch" }))
    }
    fn name(&self) -> &str { "exact_match" }
}

适用 :事实性问答、分类任务(答案空间小且固定)。致命伤:答"法国的首都巴黎"对"巴黎" -> 0 分,冤枉。

真实例子(翻译场景):输入 "It's raining cats and dogs.",参考答案"倾盆大雨"。

预测 内容 ExactMatch StringDistance EmbeddingSim LLMAsJudge
A(意译,对) 大雨滂沱 0.0000 0.0000 0.7500 1.0000
B(直译,错) 正在下猫和狗 0.0000 0.0000 0.5000 0.1000

ExactMatch 给 A、B 都是 0--A "大雨滂沱"意思明明对,但字面 ≠ "倾盆大雨"就 0 分,冤枉。翻译这种"正确答案有无数种说法"的场景,字面相等几乎永远不成立,ExactMatch 全面失效。

2. StringDistance -- Levenshtein 编辑距离

编辑距离(Levenshtein):把一个字符串改成另一个,最少要动几下手。三种动作,每下算 1 分,取所有改法里步数最少的:

  • 替换一个字(巴->东)
  • 删除一个字
  • 插入一个字

举例:

预测 -> 参考 怎么改 距离
巴黎 -> 巴黎 不用改 0
巴黎 -> 巴厘 换 1 个字(黎->厘) 1
巴黎 -> 东京 换 2 个字 2
巴黎 -> 巴黎人 插 1 个字(人) 1

距离是绝对数(0 到"较长串长度"之间),当不了分数,所以要归一:除以 max(两串长度) 压成 0~1 的"差异度",再用 1 减翻成"相似度"--改得越少越像,不改就是 1.0,推倒重写就是 0。

复制代码
相似度 = 1 - 编辑距离 / max(两串长度)
预测 -> 参考 距离 max长度 相似度
巴黎 -> 巴黎 0 2 1.0
巴黎 -> 巴厘 1 2 0.5
巴黎 -> 东京 2 2 0.0
rust 复制代码
fn levenshtein(a: &str, b: &str) -> usize {
    // 经典 DP,两个滚动数组省内存
    let (a, b): (Vec<char>, Vec<char>) = (a.chars().collect(), b.chars().collect());
    let (m, n) = (a.len(), b.len());
    let mut prev: Vec<usize> = (0..=n).collect();
    let mut curr: Vec<usize> = vec![0; n + 1];
    for i in 1..=m {
        curr[0] = i;
        for j in 1..=n {
            let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
            curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
        }
        std::mem::swap(&mut prev, &mut curr);
    }
    prev[n]
}
// score = 1.0 - levenshtein(pred, ref) as f64 / max_len

比 ExactMatch 柔和(有连续值),但本质还是比字面。拼写纠错场景好用,语义判断不行。

真实例子(代码场景) :输入"写反转字符串的 Python 函数",参考答案 s[::-1]

预测 内容 ExactMatch StringDistance EmbeddingSim LLMAsJudge
B(循环,对) for i in range(...): result += s[i] 0.0000 0.0870 0.8769 0.5000
C(错) s.reverse() 0.0000 0.0909 0.7500 0.0000

B 是对的、C 是错的,但 StringDistance 给两者几乎一样的低分(0.087 / 0.091)--字面距离根本分不出对错。它只知道"跟参考差几个字",不知道"这代码能不能跑"。

3. EmbeddingSimilarity -- 嵌入余弦相似度

把预测和参考答案都 embed 成向量,算余弦,从 [-1, 1] 映射到 [0, 1]:

rust 复制代码
let p = self.embeddings.embed_query(prediction).await?;
let r = self.embeddings.embed_query(reference).await?;
let sim = cosine_similarity(&p, &r);
let v = ((sim + 1.0) / 2.0).clamp(0.0, 1.0);

终于不看字面了,看语义像不像。但它有两个问题:

  • 是"像不像",不是"对不对":模型答了句和参考很像但事实错误的话,照样高分。
  • 无判别力的中间值:不相关的两句话余弦≈0,归一后是 0.5。这个 0.5 不是"半对",是"我不知道"--对决策没帮助。

嵌入质量决定一切。本文测试用的是纯 Rust 的 LocalEmbeddings(词袋 hash,粗粒度);生产环境应上 BGE/E5 这类神经网络嵌入。

真实例子(RAG 幻觉场景):输入"公司年假多少天?",参考答案"年假 15 天"。

预测 内容 ExactMatch StringDistance EmbeddingSim LLMAsJudge
A(忠实) 员工年假为 15 天 0.0000 0.7000 0.8780 1.0000
B(幻觉) 员工年假为 20 天,可累积 0.0000 0.3571 0.7372 0.0000

B 是幻觉(编了"20 天"),但 EmbeddingSim 给它 0.7372 ,跟忠实的 0.878 差不多--因为两者都含"年假""天"。它只看像不像,分不出幻觉 。这是 RAG 场景的致命伤:幻觉回答放出去会误导用户,而嵌入相似度完全检测不出来。这也是 Ragas 框架专门做 faithfulness 指标的原因。

4. LLMAsJudge -- LLM 当裁判

前三个都是尺子:量字面、量距离、量向量。都不懂"意思"。

LLM-as-a-Judge 直接请一个 LLM 当阅卷老师:把题目、参考答案、学生回答打包给它,让它打分。它能读懂语义,知道"答对没答对",还能评"有没有跑题、有没有胡说、是否安全"。

这个概念来自两篇 2023 年的论文(注:以下论文信息凭记忆,具体 arXiv 编号建议自行核实):

  • LMSYS 的 Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena (Zheng et al., 2023):用 GPT-4 当裁判评估聊天机器人,提出 MT-Bench(多轮基准)和 Chatbot Arena(竞技场)。关键发现:GPT-4 当裁判与人类判断一致率约 80%(单点)/ 90%(成对),成对比较比单点更接近人类。
  • G-Eval(Liu et al., 2023,微软):提出用**思维链(CoT)**让裁判先写分析再给分,显著提升一致性。

真实例子(摘要场景,无参考):给一段关于光合作用的长文,模型生成摘要,把原文当 reference。B 摘要把"植物"写成了"动物"、说"发生在根部"--事实错误。

预测 内容 ExactMatch StringDistance EmbeddingSim LLMAsJudge
A(好摘要) 光合作用是植物利用阳光将二氧化碳和水转化为葡萄糖和氧气... 0.0000 0.3688 0.9567 0.7000
B(差摘要) 光合作用是动物利用阳光制造食物的过程,只发生在根部。 0.0000 0.1125 0.7521 0.0000

EmbeddingSim 对好坏摘要都给高分(都从原文衍生、共享大量词),分不出好坏。LLMAsJudge 看出 B 有事实错误,直接给 0;A 给 0.7。前三个评测器在无参考场景给的全是无意义分数,只有 LLM 裁判能真正评语义质量。

5. ContainsKeyword -- 关键词包含

检查预测是否包含指定关键词。all_required=true 全部命中才 1.0,false 任一命中即 1.0,可配大小写敏感。

rust 复制代码
pub struct ContainsKeyword {
    keywords: Vec<String>,
    all_required: bool,
    case_sensitive: bool,
}
// 全部命中(all_required)或任一命中 -> 1.0,否则 0.0

适用 :检查回答是否提到必备要素(如"年假""天")、合规词检测。局限:只看在不在,不看对不对--"年假 20 天"含"年假""天"照样 1.0,测不出数字错。

例子 :关键词 ["年假", "天"],all_required=true:

预测 命中 分数
员工年假为15天 全部 1.0
不清楚 0.0

6. RegexMatch -- 正则匹配

预测是否符合给定正则。命中 1.0,不命中 0.0。

rust 复制代码
pub struct RegexMatch(Regex);
// self.0.is_match(prediction) -> 1.0 / 0.0

适用 :格式校验(手机号、邮箱、日期)、结构化输出检查。局限:只验格式不验语义--"12345678901"符合 11 位但未必是真号。

例子 :正则 \d{4}-\d{2}-\d{2}(日期):

预测 匹配 分数
2026-07-14 1.0
2026/7/14 0.0

7. LengthCheck -- 长度范围

预测字符数是否落在 [min, max]。在范围内 1.0,否则 0。min/max 可选(只卡上限或下限)。

rust 复制代码
pub struct LengthCheck { min: Option<usize>, max: Option<usize> }
// let n = prediction.chars().count(); 满足 min/max 约束 -> 1.0

适用 :摘要字数控制、防超长/过短、简单护栏。局限:长度对≠内容对--废话凑字数也 1.0。

例子 :min=10, max=50:

预测 字数 分数
光合作用是植物...(30字) 30 1.0
1 0.0(太短)

8. Bleu -- n-gram 精度

经典机器翻译指标。统计预测与参考的 n-gram(默认到 4)重叠比例取几何平均,再乘短句惩罚(BP):预测比参考短就扣分。

rust 复制代码
// 每阶:matches = 预测 n-gram 在参考中的出现次数(clip 截断)
// p_n = matches / total;  BLEU = BP * exp(mean(ln p_n))
// BP = 1 if plen>rlen else exp(1 - rlen/plen)

适用 :翻译/生成与参考的重合度(精度侧)。局限 :只看 n-gram 不懂语义;中文须开 with_char_level(true)(按字切,否则整句一个 token);短句默认因高阶 n-gram 缺失归零,可开 with_smoothing(true)

例子 :Bleu::new().with_char_level(true).with_max_n(2):

预测 参考 分数
猫坐在垫子上 猫坐在垫子上 1.0
猫坐在垫子上 狗跑到院子里 0.0

9. PairwiseJudge -- 成对比较

不给绝对分,拿两个回答 A/B 让裁判二选一。关键:交换 A/B 顺序跑两次,两次都选同一个才算真赢,否则判平局--消除"裁判偏爱排在前面的"位置偏差。

rust 复制代码
// v1 = ask(A在前, B在后); v2 = ask(B在前, A在后)
// (First, Second) => AWins  // 两次都选 A
// (Second, First) => BWins  // 两次都选 B
// 其他 => Tie                 // 位置偏差,不敢判

适用 :模型对比(A vs B 谁好)、排序;成对比较与人类更接近(MT-Bench:成对 ~90% 一致 > 单点 ~80%)。局限:每次调 2 次 LLM;保守策略使 Tie 偏多。详见第五节。

真实例子 :忠实回答 vs 幻觉回答 -> 判 Tie(交换后裁判两次选择不一致,位置消偏差正确判平局,而非瞎选)。

10. Faithfulness -- 忠实度(幻觉检测)

RAG 专用。把回答拆成原子陈述,逐条问裁判"能否从上下文推导",通过率即忠实度。

rust 复制代码
// claims = split(prediction)       // 按标点拆,可 with_llm_split 用 LLM 拆复合句
// 并发 verify 每条 claim -> 能否推导; score = 支持数 / 总数

适用 :RAG 幻觉检测--回答有没有超出检索内容编造。局限 :每条陈述调 1 次 LLM(已 join_all 并发优化);只验"忠于上下文",上下文本身错则查不到(需 Factuality);空预测默认 1.0(没编=忠实),可 with_empty_score 改。

真实例子 (详见附录 A):幻觉回答(编"20 天")忠实度 0 ,忠实回答 1.0,完美区分。

以上十种:1-3 是字面尺子,4 是语义裁判,5-7 是规则护栏,8 是经典 NLP 指标,9-10 是 LLM 驱动的进阶评测。下面先看 LLMAsJudge 的 Rust 实现,以及如何缓解它固有的偏差。


四、LLMAsJudge 实现:单点打分

数据结构

rust 复制代码
pub struct LLMAsJudge<M: BaseChatModel> {
    judge: M,        // 当裁判的 chat 模型
    rubric: String,  // 评分标准
    max_score: u8,   // 满分值,默认 10
}

泛型 M: BaseChatModel--任何实现框架 BaseChatModel 的模型都能当裁判(OpenAI、Ollama、Anthropic、GLM 等)。with_rubric / with_max_score 可覆盖默认。

构造裁判 prompt(轻量 CoT)

rust 复制代码
fn build_prompt(&self, input: &str, prediction: &str, reference: &str) -> (String, String) {
    let system = format!(
        "你是一个严格、公正的评估员。请根据以下评分标准对待评估的回答打分。\n\n\
         评分标准:\n{rubric}\n\n\
         打分范围:0 到 {max}(0 = 完全错误或无关,{max} = 完全正确)。\n\n\
         要求:先在 reason 字段写出简短分析,再在 score 字段给出分数。\n\
         只输出一行 JSON:{{\"reason\":\"...\",\"score\":N}}",
        rubric = self.rubric,
        max = self.max_score
    );
    let user = format!(
        "输入:\n{input}\n\n参考答案:\n{reference}\n\n待评估的回答:\n{prediction}\n\n请评估。",
    );
    (system, user)
}

reason 字段排在 score 前面--逼裁判先写分析再下结论。这是 G-Eval 的核心思想:先推理再打分,比直接甩个数字一致性高得多。

G-Eval 的进阶做法 :原论文不是直接取裁判输出的数字,而是让模型输出 1--5 的评分 token,取每个候选 token 的对数概率(logprob)加权求和 :score = Σ(score_i × P(score_i))。这比硬取一个数字更平滑、更稳定,但实现要拿到模型 logprob,比较复杂。本文用"先 reason 后 score 的单步 JSON"做轻量近似,工程上够用。

调裁判 + 解析分数

rust 复制代码
#[async_trait]
impl<M: BaseChatModel> Evaluator for LLMAsJudge<M> {
    async fn eval(&self, input: &str, prediction: &str, reference: &str) -> Result<Score, EvalError> {
        let (system, user) = self.build_prompt(input, prediction, reference);
        let result = self.judge
            .chat_with_system(system, vec![Message::human(user)])
            .await
            .map_err(|e| EvalError::PredictorError(e.to_string()))?;

        let raw = result.content;
        let value = parse_score(&raw, self.max_score)
            .ok_or_else(|| EvalError::ParseError(format!("无法解析分数: {}", truncate(&raw, 200))))?;
        Ok(Score::new(value).with_label("llm_judge"))
    }
    fn name(&self) -> &str { "llm_as_judge" }
}

分数解析:三级回退(工程重点)

你 prompt 里写了"只输出 JSON",但真实 LLM 经常前面带句"好的,我的评估如下:"再给 JSON,或干脆不按格式回。别信任 LLM 的输出格式,层层兜底:

rust 复制代码
fn parse_score(raw: &str, max_score: u8) -> Option<f64> {
    let max = max_score as f64;
    let n = extract_json_score(raw)                          // 1. 先试 JSON {"score":8}
        .or_else(|| find_number_after_keyword(raw, "score")) // 2. 再试 "score:8"
        .or_else(|| find_number_after_keyword(raw, "分数"))  //    或 "分数:8"
        .or_else(|| first_number(raw))?;                     // 3. 最后兜底:第一个数字
    Some((n / max).clamp(0.0, 1.0))                          // 归一到 0~1,超了 clamp
}
  • JSON 解析 :找 {},serde_json 解析取 score 字段(as_f64() 同时认整数和浮点)
  • 关键词回退 :定位 score/分数,取其后第一个数字
  • 纯数字兜底:扫描取第一段连续数字

三级都失败就报 ParseError。生产里你还可以加一层降级:裁判解析失败时回退到 EmbeddingSimilarity,别让整条评测挂掉。


五、成对比较与位置偏差缓解

单点打分有个问题:让裁判给一个回答打 7 分还是 8 分,它自己都不稳定。但如果你给它看两个回答问"哪个更好",它判得更准--这是 MT-Bench 论文的核心发现(成对比较一致率 90% > 单点 80%)。

竞技场(Chatbot Arena)就是成对比较:两个模型答同一题,裁判二选一。

成对比较的坑:位置偏差

裁判有位置偏差 :A、B 两个回答摆出来,它倾向于选排在前面的那个。直接问一次不可信。

缓解:交换顺序跑两次

把 A、B 顺序换一下再问一次。两次都选同一个,才算真赢;不一致,判平局。这一招直接把位置偏差的影响压下去:

rust 复制代码
pub enum Verdict { AWins, BWins, Tie }

pub struct PairwiseJudge<M: BaseChatModel> {
    judge: M,
    rubric: String,
}

impl<M: BaseChatModel> PairwiseJudge<M> {
    /// 交换 A/B 顺序跑两次,消除位置偏差。
    async fn compare(&self, input: &str, a: &str, b: &str) -> Result<Verdict, EvalError> {
        // 第一次:A 在前
        let v1 = self.ask(input, a, b).await?;
        // 第二次:交换,B 在前
        let v2 = self.ask(input, b, a).await?;

        Ok(match (v1, v2) {
            (Verdict::AWins, Verdict::AWins) => Verdict::AWins, // 两次都选前者=>
            (Verdict::BWins, Verdict::BWins) => Verdict::BWins, // 两次都选后者=> 但第二次后者是 A
            _ => Verdict::Tie,                                   // 不一致 => 位置偏差,判平局
        })
        // 注:第二次交换后,返回值要映射回原始 A/B,这里简化展示思路
    }

    async fn ask(&self, input: &str, first: &str, second: &str) -> Result<Verdict, EvalError> {
        // 构造 prompt:输入 + first + second,让裁判选"第一个更好"/"第二个更好"/"平局"
        // ... 调 self.judge.chat_with_system,解析返回
        todo!()
    }
}

(上面是设计示意,展示交换顺序消偏差的思路;ask 的 prompt 构造和返回解析跟 LLMAsJudge 类似。)


六、LLM 裁判的偏差全家桶

位置偏差只是其一。LLM 当裁判有一堆已知偏差,做评估系统必须知道:

偏差 表现 缓解
位置偏差 成对比较偏好靠前的 交换顺序跑两次,不一致判平局
冗长偏差(verbosity bias) 偏好更长的回答,即使没信息 rubric 里加"简洁性"维度;或对长度做归一
自我偏好(self-enhancement) GPT 当裁判偏好 GPT 的输出 跨模型族当裁判(GPT 判 Claude,或集成多个裁判)
权威偏差(authority bias) 被输出里"自信的语气"带偏 rubric 强调"只看内容,不看语气"
打分不稳 同输入跑两次分数不同 温度设 0;self-consistency(多次采样取均值/众数)

Self-consistency(自洽性采样)

针对打分不稳:同一输入让裁判跑 N 次(温度 > 0),取 N 个分数的均值或众数。代价是 N 倍调用,但分数稳定得多。关键场景值得。

多裁判集成

用一个裁判怕它有偏见?用多个不同模型族的裁判(GPT + Claude + GLM)各打一次,取均值或投票。能抵消单个模型的自我偏好。代价是 N 倍成本。


七、综合真实测试:四种评测器放一起比

把四种评测器放一起,跑三道"法国首都"题(题目"法国首都?",标准答案"巴黎"):

样例 模型预测 ExactMatch StringDistance EmbeddingSim LLMAsJudge
正确 巴黎 1.0000 1.0000 1.0000 1.0000
错误 伦敦 0.0000 0.0000 0.5000 0.0000
改写 法国的首都巴黎,位于法国北部 0.0000 0.1250 0.6667 1.0000

真实数据。裁判模型用 GLM-5.2(阿里云),EmbeddingSim 用 LocalEmbeddings(词袋 hash)。17 项测试全过。

逐行看:

  • 正确:四个全给 1.0,没分歧。
  • 错误(伦敦 vs 巴黎):字面类给 0(对);EmbeddingSim 给 0.5(不是判错,是"无信息"--词袋里两词没共享词,余弦 0 归一到 0.5);LLM 裁判给 0(懂语义,明确判错)。
  • 改写--分水岭 :ExactMatch 0.0(冤枉)、StringDistance 0.125(冤枉)、EmbeddingSim 0.667(勉强)、LLMAsJudge 1.0(读懂意思,满分)

三个尺子全把"意思对但话多"判成错,只有裁判看懂了。这就是 LLM-as-a-Judge 的价值。第三节里四个场景(翻译/代码/RAG/摘要)已经分别展示了各评测器的典型局限,这里是一组干净的综合对比。


八、业界框架对比

这套东西不是只有我们在做。Python 生态已经有一堆成熟框架,设计思路值得借鉴:

框架 语言 特点 评估方式
LangChain Evaluators Python LangChain 自带,CriteriaEvaluatorPairwiseStringEvaluator LLM-as-Judge 为主,集成在链里
Ragas Python RAG 专用,faithfulness(忠实度)/ answer_relevancy / context_precision LLM-as-Judge,针对 RAG 痛点设计指标
DeepEval Python 类 pytest 风格,assert 式断言,有指标打分 LLM-as-Judge + 规则,CI 友好
Promptfoo JS/Python YAML 配置驱动,对比多个 prompt/model 的输出 支持多种评估,包括 LLM-as-Judge
OpenAI Evals Python OpenAI 官方,标准化评估任务 模板化,支持多种打分
Inspect AI Python UK AISI 出品,偏安全/能力评估 主题化评估,评分器可组合

Ragas 的 faithfulness 指标值得单独说 :它不是简单打分,而是先把模型回答拆成原子陈述,再逐条判断每条陈述能不能从上下文/检索内容里推导出来,最后算"可推导的陈述占比"。这种把模糊的"好不好"拆成可验证的子问题的思路,比直接问裁判"这个回答忠不忠实"靠谱得多。

对比下来,Rust 生态在这块基本是空白--这也是本文实现的意义:把这套评估能力搬进 Rust 的 LangChain 实现。


九、rubric(评分标准)设计

LLM 裁判好不好用,一半取决于 rubric 写得好不好。差的 rubric("评估一下回答质量")让裁判瞎打;好的 rubric 具体、可操作、分维度。

差的 rubric:

复制代码
评估回答的质量,打 0-10 分。

好的 rubric(分维度,每条可判定):

复制代码
正确性(权重 50%):回答是否事实准确、是否与参考答案核心意思一致。
完整性(权重 30%):是否完整回答了输入的问题或指令,有无遗漏。
简洁性(权重 20%):有无冗余、跑题、无关内容。

判分锚点:
- 9-10:三个维度都好,可直接采用
- 6-8:基本正确但有瑕疵(啰嗦/小遗漏)
- 3-5:部分正确,有事实错误或严重跑题
- 0-2:完全错误或无关

几个原则:

  • 分维度:正确性、完整性、安全性分开打,别混成一个分。最后可加权合成。
  • 给锚点:每个分数段配个例子("什么样的回答给 9 分,什么给 5 分"),裁判才有参照。
  • 明确否定:"不要因回答长度加分""不要被自信语气影响"。
  • Likert vs 0-10:1-5 的 Likert 心理学上更稳(人区分 7 分和 8 分很难),但 0-10 粒度细。看场景。

十、裁判模型选择与对齐验证

选谁当裁判

MT-Bench 论文的核心结论:只有强模型能当好裁判。GPT-3.5 当裁判的一致率明显低于 GPT-4。弱模型自己都答不对,让它判别人就是瞎判。

实践上:

  • 关键评估:用你能拿到的最强模型(GPT-4 / Claude Opus 级别)。
  • 日常迭代:可以降一档省成本,但心里有数它会漏判一些。
  • 跨模型族:要防自我偏好,换不同厂商的模型当裁判。

对齐验证:裁判可不可信?

新接一个裁判模型,先验证它跟人类判断的一致率,别盲目信任:

  1. 抽 100--300 条样例,人工标注一遍(最好多人标,算标注者间一致率,确认标准本身清晰)。
  2. 让裁判跑同批,算一致率 (agreement rate)或 Cohen's Kappa(扣掉随机一致的部分)。
  3. 一致率 > 0.8 算可用;< 0.7 要么换模型,要么改 rubric。

这一步很多人跳过,直接信裁判--然后被偏差坑了都不知道。

相关推荐
风华圆舞1 小时前
鸿蒙HarmonyOS 多模态输入实战 —— 图片与文档输入智谱/DeepSeek
开发语言·人工智能·华为·php·harmonyos·arkts·arkui
芒鸽1 小时前
HarmonyOS 状态管理:@State、@Prop、@Link、@Watch
后端
一起努力啊~1 小时前
DataWhale组队学习笔记--llm-algo-leetcode(三)
笔记·学习·llm
@灯神1 小时前
SpringAI系列|第1篇:SpringAI概述与快速上手
java·人工智能·spring·ai·ai编程
Ljwuhe1 小时前
C++——模板进阶
开发语言·c++
金金金__1 小时前
一篇文章带你掌握SpecKit
后端
Hyyy2 小时前
流式渲染的基础——SSE
前端·后端·面试
中国搜索直付通2 小时前
避开直付通选型暗礁:二级商户的合规生存与背景甄别
java·大数据·开发语言·人工智能·游戏
Gopher_HBo2 小时前
docker网络学习
后端