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