Skip to main content

lc_evaluation/
criteria.rs

1//! 评测核心类型与 trait:EvalError、Score、Example、Dataset,
2//! 以及 Evaluator / Predictor trait。
3
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6
7/// 评测错误
8#[derive(Debug, thiserror::Error)]
9#[non_exhaustive]
10pub enum EvalError {
11    /// 底层 IO 错误(如文件读取失败)。
12    #[error("IO error: {0}")]
13    IoError(String),
14    /// 数据解析错误(如 JSON/JSONL 解析失败)。
15    #[error("parse error: {0}")]
16    ParseError(String),
17    /// 嵌入(embedding)计算错误。
18    #[error("embedding error: {0}")]
19    EmbeddingError(String),
20    /// 预测器(predictor)执行错误。
21    #[error("prediction error: {0}")]
22    PredictorError(String),
23}
24
25/// P2-6: 共享裁判内核(lc-core::judge)的错误映射进评测错误域,
26/// 让 `structured_call(...).await?` 在 `Result<_, EvalError>` 上下文里直接可用。
27impl From<lc_core::judge::StructuredJudgeError> for EvalError {
28    fn from(e: lc_core::judge::StructuredJudgeError) -> Self {
29        match e {
30            lc_core::judge::StructuredJudgeError::Call(s) => EvalError::PredictorError(s),
31            lc_core::judge::StructuredJudgeError::Parse(s) => EvalError::ParseError(s),
32            // `StructuredJudgeError` is `#[non_exhaustive]`; forward any future
33            // variants to the generic predictor-error slot.
34            _ => EvalError::PredictorError(e.to_string()),
35        }
36    }
37}
38
39/// 评测分数(0.0–1.0,1.0 为最佳)
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Score {
42    /// 分数值(0.0–1.0)
43    pub value: f64,
44    /// 可选的分数标签
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub label: Option<String>,
47}
48
49impl Score {
50    /// 构造 0.0–1.0 之间的分数。
51    ///
52    /// P2-8: Rust 的 `f64::clamp(0.0, 1.0)` 对 NaN 返回 NaN,会污染
53    /// summary 均值/标准差。这里先做 NaN 前置检查,按 0.0 处理(负无穷、
54    /// 正无穷交给 `clamp` 收敛到边界)。
55    pub fn new(value: f64) -> Self {
56        let value = if value.is_nan() {
57            log::warn!("Score::new received NaN, treating as 0.0");
58            0.0
59        } else {
60            value
61        };
62        Self {
63            value: value.clamp(0.0, 1.0),
64            label: None,
65        }
66    }
67
68    /// 附加分数标签(builder 风格)。
69    pub fn with_label(mut self, label: impl Into<String>) -> Self {
70        self.label = Some(label.into());
71        self
72    }
73}
74
75/// 评测样例
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct Example {
78    /// 评测输入
79    pub input: String,
80    /// 参考答案
81    pub reference: String,
82}
83
84impl Example {
85    /// 构造评测样例。
86    pub fn new(input: impl Into<String>, reference: impl Into<String>) -> Self {
87        Self {
88            input: input.into(),
89            reference: reference.into(),
90        }
91    }
92}
93
94/// 数据集
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct Dataset {
97    /// 数据集中的评测样例列表
98    pub examples: Vec<Example>,
99}
100
101impl Dataset {
102    /// 构造数据集。
103    pub fn new(examples: Vec<Example>) -> Self {
104        Self { examples }
105    }
106
107    /// 从 JSONL 文件加载(每行一个 ``{input, reference}``)。
108    ///
109    /// P2-2: 异步 I/O(`tokio::fs`),避免同步阻塞落在 async 评测链路里。
110    pub async fn from_jsonl(path: &str) -> Result<Self, EvalError> {
111        let content = tokio::fs::read_to_string(path)
112            .await
113            .map_err(|e| EvalError::IoError(e.to_string()))?;
114        let mut examples = Vec::new();
115        for (i, line) in content.lines().enumerate() {
116            let line = line.trim();
117            if line.is_empty() {
118                continue;
119            }
120            let ex: Example = serde_json::from_str(line)
121                .map_err(|e| EvalError::ParseError(format!("line {}: {}", i + 1, e)))?;
122            examples.push(ex);
123        }
124        Ok(Self { examples })
125    }
126
127    /// 返回样例数量。
128    pub fn len(&self) -> usize {
129        self.examples.len()
130    }
131
132    /// 数据集是否为空。
133    pub fn is_empty(&self) -> bool {
134        self.examples.is_empty()
135    }
136}
137
138/// 评测器 trait
139#[async_trait]
140pub trait Evaluator: Send + Sync {
141    /// 对单条预测打分
142    async fn eval(
143        &self,
144        input: &str,
145        prediction: &str,
146        reference: &str,
147    ) -> Result<Score, EvalError>;
148
149    /// 评测器名称(用于报告汇总)
150    fn name(&self) -> &str;
151}
152
153/// 成对比较评测器 trait(竞技场模式):对同一输入的 A/B 两个回答判优劣。
154///
155/// P1-1: 与单点 `Evaluator` 并列的一等公民,`EvalRunner` 同时收纳两种,
156/// 竞技场评测因此也能进统一报告。得分约定:1.0 = A 优、0.5 = 平局、0.0 = B 优。
157#[async_trait]
158pub trait PairwiseEvaluator: Send + Sync {
159    /// 比较 A、B 两个回答,返回 0-1 得分
160    /// (1.0 = A 优,0.5 = 平局,0.0 = B 优)。
161    async fn eval_pair(&self, input: &str, a: &str, b: &str) -> Result<Score, EvalError>;
162
163    /// 评测器名称(用于报告汇总)
164    fn name(&self) -> &str;
165}
166
167/// 预测器 trait(待评测的对象:LLMChain / Agent 等)
168#[async_trait]
169pub trait Predictor: Send + Sync {
170    /// 对单条输入进行预测,返回文本结果。
171    async fn predict(&self, input: &str) -> Result<String, EvalError>;
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_score_new_normal() {
180        assert!((Score::new(0.5).value - 0.5).abs() < 1e-9);
181    }
182
183    #[test]
184    fn test_score_new_clamps_overflow() {
185        assert_eq!(Score::new(2.0).value, 1.0);
186        assert_eq!(Score::new(-1.0).value, 0.0);
187        assert_eq!(Score::new(f64::INFINITY).value, 1.0);
188        assert_eq!(Score::new(f64::NEG_INFINITY).value, 0.0);
189    }
190
191    /// P2-8: NaN 不再穿透 `.clamp(0.0, 1.0)` 污染汇总统计。
192    #[test]
193    fn test_score_new_nan_guarded() {
194        assert_eq!(Score::new(f64::NAN).value, 0.0);
195        // 保证 NaN 被清掉,而不是残留在统计里
196        assert!(Score::new(f64::NAN).value.is_finite());
197    }
198
199    /// P2-2: from_jsonl 异步读文件,单行解析失败带行号。
200    #[tokio::test]
201    async fn test_from_jsonl_async() {
202        let dir = tempfile::tempdir().unwrap();
203        let path = dir.path().join("data.jsonl");
204        std::fs::write(
205            &path,
206            "{\"input\":\"q1\",\"reference\":\"a1\"}\n\n{\"input\":\"q2\",\"reference\":\"a2\"}\n",
207        )
208        .unwrap();
209        let dataset = Dataset::from_jsonl(path.to_str().unwrap()).await.unwrap();
210        assert_eq!(dataset.len(), 2);
211        assert_eq!(dataset.examples[1].input, "q2");
212        assert_eq!(dataset.examples[1].reference, "a2");
213    }
214
215    #[tokio::test]
216    async fn test_from_jsonl_missing_file() {
217        let err = Dataset::from_jsonl("不存在-的文件.jsonl")
218            .await
219            .unwrap_err();
220        assert!(matches!(err, EvalError::IoError(_)));
221    }
222
223    #[tokio::test]
224    async fn test_from_jsonl_bad_line() {
225        let dir = tempfile::tempdir().unwrap();
226        let path = dir.path().join("bad.jsonl");
227        std::fs::write(&path, "{\"input\":\"q\"}\n").unwrap();
228        let err = Dataset::from_jsonl(path.to_str().unwrap())
229            .await
230            .unwrap_err();
231        assert!(matches!(err, EvalError::ParseError(_)));
232    }
233}