1use async_trait::async_trait;
8
9use lc_core::BaseChatModel;
10use lc_schema::Message;
11
12use super::{EvalError, Evaluator, Score};
13
14pub struct Faithfulness<M: BaseChatModel> {
19 judge: M,
20 llm_split: bool,
22 empty_score: f64,
24}
25
26fn split_claims(prediction: &str) -> Vec<String> {
28 prediction
29 .split(['。', '.', '!', '?', ';', ';', '\n'])
30 .map(|s| s.trim().to_string())
31 .filter(|s| !s.is_empty())
32 .collect()
33}
34
35impl<M: BaseChatModel> Faithfulness<M> {
36 pub fn new(judge: M) -> Self {
37 Self {
38 judge,
39 llm_split: false,
40 empty_score: 1.0,
41 }
42 }
43
44 pub fn with_llm_split(mut self, v: bool) -> Self {
46 self.llm_split = v;
47 self
48 }
49
50 pub fn with_empty_score(mut self, score: f64) -> Self {
52 self.empty_score = score;
53 self
54 }
55
56 async fn verify_claim(&self, context: &str, claim: &str) -> Result<bool, EvalError> {
58 let system =
59 "你是事实核查员。判断给定的陈述能否从参考上下文中推导出来。只输出\"是\"或\"否\"。"
60 .to_string();
61 let user =
62 format!("参考上下文:\n{context}\n\n陈述:\n{claim}\n\n这条陈述能从上下文推导出来吗?");
63 let result = self
64 .judge
65 .chat_with_system(system, vec![Message::human(user)])
66 .await
67 .map_err(|e| EvalError::PredictorError(e.to_string()))?;
68 Ok(parse_yes_no(&result.content))
69 }
70
71 async fn split_claims_llm(&self, prediction: &str) -> Result<Vec<String>, EvalError> {
73 let system =
74 "你是文本分析助手。把回答拆成原子陈述,每条一行,只输出陈述本身,不要编号不要解释。"
75 .to_string();
76 let user = format!("回答:\n{prediction}\n\n把它拆成原子陈述,每行一条:");
77 let result = self
78 .judge
79 .chat_with_system(system, vec![Message::human(user)])
80 .await
81 .map_err(|e| EvalError::PredictorError(e.to_string()))?;
82 Ok(result
83 .content
84 .lines()
85 .map(|s| s.trim().to_string())
86 .filter(|s| !s.is_empty())
87 .collect())
88 }
89}
90
91#[async_trait]
92impl<M: BaseChatModel> Evaluator for Faithfulness<M> {
93 async fn eval(
94 &self,
95 _input: &str,
96 prediction: &str,
97 reference: &str,
98 ) -> Result<Score, EvalError> {
99 let claims = if self.llm_split {
100 self.split_claims_llm(prediction).await?
101 } else {
102 split_claims(prediction)
103 };
104 if claims.is_empty() {
105 return Ok(Score::new(self.empty_score).with_label("no_claims"));
106 }
107 let futs = claims
109 .iter()
110 .map(|claim| self.verify_claim(reference, claim));
111 let results = futures_util::future::join_all(futs).await;
112 let mut supported = 0usize;
113 for r in results {
114 if r? {
115 supported += 1;
116 }
117 }
118 let value = supported as f64 / claims.len() as f64;
119 Ok(Score::new(value).with_label("faithfulness"))
120 }
121
122 fn name(&self) -> &str {
123 "faithfulness"
124 }
125}
126
127fn parse_yes_no(raw: &str) -> bool {
129 let lower = raw.to_lowercase();
130 if lower.contains("否")
132 || lower.contains("no")
133 || lower.contains("不能")
134 || lower.contains("不是")
135 || lower.contains("false")
136 {
137 return false;
138 }
139 lower.contains("是") || lower.contains("yes") || lower.contains("能") || lower.contains("true")
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use futures_util::Stream;
146 use lc_core::language_models::LLMResult;
147 use lc_core::{BaseLanguageModel, Runnable, RunnableConfig};
148 use std::pin::Pin;
149 use std::sync::atomic::{AtomicUsize, Ordering};
150 use std::sync::Arc;
151
152 #[derive(Debug)]
153 struct JudgeError(String);
154 impl std::fmt::Display for JudgeError {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 write!(f, "{}", self.0)
157 }
158 }
159 impl std::error::Error for JudgeError {}
160
161 struct SeqMockJudge {
162 replies: Vec<String>,
163 call: Arc<AtomicUsize>,
164 }
165 impl SeqMockJudge {
166 fn new(replies: Vec<String>) -> Self {
167 Self {
168 replies,
169 call: Arc::new(AtomicUsize::new(0)),
170 }
171 }
172 }
173
174 #[async_trait]
175 impl Runnable<Vec<Message>, LLMResult> for SeqMockJudge {
176 type Error = JudgeError;
177 async fn invoke(
178 &self,
179 _input: Vec<Message>,
180 _config: Option<RunnableConfig>,
181 ) -> Result<LLMResult, Self::Error> {
182 Err(JudgeError("use chat".into()))
183 }
184 }
185 #[async_trait]
186 impl BaseLanguageModel<Vec<Message>, LLMResult> for SeqMockJudge {
187 fn model_name(&self) -> &str {
188 "seq-mock"
189 }
190 fn get_num_tokens(&self, t: &str) -> usize {
191 t.len()
192 }
193 fn with_temperature(self, _: f32) -> Self {
194 self
195 }
196 fn with_max_tokens(self, _: usize) -> Self {
197 self
198 }
199 }
200 #[async_trait]
201 impl BaseChatModel for SeqMockJudge {
202 async fn chat(
203 &self,
204 _messages: Vec<Message>,
205 _config: Option<RunnableConfig>,
206 ) -> Result<LLMResult, Self::Error> {
207 let idx = self.call.fetch_add(1, Ordering::SeqCst);
208 let reply = self.replies.get(idx).cloned().unwrap_or_default();
209 Ok(LLMResult {
210 content: reply,
211 model: "seq-mock".to_string(),
212 token_usage: None,
213 tool_calls: None,
214 thinking_content: None,
215 })
216 }
217 async fn stream_chat(
218 &self,
219 _messages: Vec<Message>,
220 _config: Option<RunnableConfig>,
221 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error>
222 {
223 Err(JudgeError("not supported".into()))
224 }
225 }
226
227 #[test]
228 fn test_split_claims() {
229 let claims = split_claims("巴黎是法国首都。伦敦是英国首都。");
230 assert_eq!(claims.len(), 2);
231 assert_eq!(claims[0], "巴黎是法国首都");
232 assert_eq!(claims[1], "伦敦是英国首都");
233 }
234
235 #[test]
236 fn test_split_claims_empty() {
237 assert!(split_claims("").is_empty());
238 assert!(split_claims("。。。").is_empty());
239 }
240
241 #[tokio::test]
242 async fn test_faithfulness_all_supported() {
243 let judge = Faithfulness::new(SeqMockJudge::new(vec!["是".into(), "是".into()]));
244 let s = judge
245 .eval("", "巴黎是法国首都。伦敦是英国首都。", "ctx")
246 .await
247 .unwrap();
248 assert!((s.value - 1.0).abs() < 1e-9);
249 }
250
251 #[tokio::test]
252 async fn test_faithfulness_half_supported() {
253 let judge = Faithfulness::new(SeqMockJudge::new(vec!["是".into(), "否".into()]));
254 let s = judge
255 .eval("", "巴黎是法国首都。伦敦是英国首都。", "ctx")
256 .await
257 .unwrap();
258 assert!((s.value - 0.5).abs() < 1e-9);
259 }
260
261 #[tokio::test]
262 async fn test_faithfulness_none_supported() {
263 let judge = Faithfulness::new(SeqMockJudge::new(vec!["否".into(), "否".into()]));
264 let s = judge
265 .eval("", "巴黎是法国首都。伦敦是英国首都。", "ctx")
266 .await
267 .unwrap();
268 assert!((s.value - 0.0).abs() < 1e-9);
269 }
270
271 #[tokio::test]
272 async fn test_faithfulness_empty_prediction() {
273 let judge = Faithfulness::new(SeqMockJudge::new(vec![]));
274 let s = judge.eval("", "", "ctx").await.unwrap();
275 assert!((s.value - 1.0).abs() < 1e-9); }
277
278 #[tokio::test]
279 async fn test_faithfulness_empty_score_configurable() {
280 let judge = Faithfulness::new(SeqMockJudge::new(vec![])).with_empty_score(0.0);
282 let s = judge.eval("", "", "ctx").await.unwrap();
283 assert!((s.value - 0.0).abs() < 1e-9);
284 }
285
286 #[tokio::test]
287 async fn test_faithfulness_llm_split() {
288 let judge = Faithfulness::new(SeqMockJudge::new(vec![
290 "巴黎是法国首都\n伦敦是英国首都".into(),
291 "是".into(),
292 "是".into(),
293 ]))
294 .with_llm_split(true);
295 let s = judge
296 .eval("", "巴黎是法国首都,伦敦是英国首都。", "ctx")
297 .await
298 .unwrap();
299 assert!((s.value - 1.0).abs() < 1e-9);
300 }
301
302 #[test]
303 fn test_parse_yes_no() {
304 assert!(parse_yes_no("是"));
305 assert!(parse_yes_no("yes"));
306 assert!(!parse_yes_no("否"));
307 assert!(!parse_yes_no("no"));
308 assert!(!parse_yes_no("不是"));
309 assert!(!parse_yes_no("不能"));
310 }
311}