lc_evaluation/
criteria.rs1use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6
7#[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
20impl 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#[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 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#[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#[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 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#[async_trait]
122pub trait Evaluator: Send + Sync {
123 async fn eval(
125 &self,
126 input: &str,
127 prediction: &str,
128 reference: &str,
129 ) -> Result<Score, EvalError>;
130
131 fn name(&self) -> &str;
133}
134
135#[async_trait]
140pub trait PairwiseEvaluator: Send + Sync {
141 async fn eval_pair(&self, input: &str, a: &str, b: &str) -> Result<Score, EvalError>;
144
145 fn name(&self) -> &str;
147}
148
149#[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 #[test]
174 fn test_score_new_nan_guarded() {
175 assert_eq!(Score::new(f64::NAN).value, 0.0);
176 assert!(Score::new(f64::NAN).value.is_finite());
178 }
179
180 #[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}