1use async_trait::async_trait;
8use futures_util::stream::{self, StreamExt};
9use serde::Deserialize;
10
11use lc_core::judge::{structured_call, truncate, StructuredJudgeError};
12use lc_core::tools::ToolDefinition;
13use lc_core::BaseChatModel;
14use lc_schema::Message;
15
16use super::{EvalError, Evaluator, Score};
17
18const MAX_CONCURRENT_VERIFY: usize = 4;
20
21const DEFAULT_MAX_CONTEXT_CHARS: usize = 2000;
24
25pub struct Faithfulness<M: BaseChatModel> {
30 judge: M,
31 llm_split: bool,
33 empty_score: f64,
35 max_context_chars: usize,
37}
38
39pub(crate) fn split_claims(prediction: &str) -> Vec<String> {
44 prediction
45 .split(['。', '.', '!', '?', ';', ';', '\n'])
46 .map(|s| s.trim().to_string())
47 .filter(|s| !s.is_empty())
48 .collect()
49}
50
51impl<M: BaseChatModel> Faithfulness<M> {
52 pub fn new(judge: M) -> Self {
54 Self {
55 judge,
56 llm_split: false,
57 empty_score: 0.0, max_context_chars: DEFAULT_MAX_CONTEXT_CHARS,
59 }
60 }
61
62 pub fn with_llm_split(mut self, v: bool) -> Self {
64 self.llm_split = v;
65 self
66 }
67
68 pub fn with_empty_score(mut self, score: f64) -> Self {
70 self.empty_score = score;
71 self
72 }
73
74 pub fn with_max_context_chars(mut self, max: usize) -> Self {
76 self.max_context_chars = max;
77 self
78 }
79
80 async fn verify_claim(&self, context: &str, claim: &str) -> Result<bool, EvalError> {
82 let system =
83 "你是事实核查员。判断给定的陈述能否从参考上下文中推导出来。调用 check_claim 工具提交判定。"
84 .to_string();
85 let user =
86 format!("参考上下文:\n{context}\n\n陈述:\n{claim}\n\n这条陈述能从上下文推导出来吗?");
87 let messages = vec![Message::system(system), Message::human(user)];
88
89 let args: VerdictArgs = structured_call(&self.judge, verdict_tool(), messages, |raw| {
91 let verdict = parse_yes_no(raw).ok_or_else(|| {
92 StructuredJudgeError::Parse(format!(
93 "failed to parse yes/no from judge reply: {}",
94 truncate(raw, 200)
95 ))
96 })?;
97 Ok(VerdictArgs {
98 verdict,
99 reason: String::new(),
100 })
101 })
102 .await?;
103 Ok(args.verdict)
104 }
105
106 async fn split_claims_llm(&self, prediction: &str) -> Result<Vec<String>, EvalError> {
108 let system =
109 "你是文本分析助手。把回答拆成原子陈述,每条一行,只输出陈述本身,不要编号不要解释。"
110 .to_string();
111 let user = format!("回答:\n{prediction}\n\n把它拆成原子陈述,每行一条:");
112 let result = self
113 .judge
114 .chat_with_system(system, vec![Message::human(user)])
115 .await
116 .map_err(|e| EvalError::PredictorError(e.to_string()))?;
117 Ok(result
118 .content
119 .lines()
120 .map(|s| s.trim().to_string())
121 .filter(|s| !s.is_empty())
122 .collect())
123 }
124}
125
126#[async_trait]
127impl<M: BaseChatModel> Evaluator for Faithfulness<M> {
128 async fn eval(
129 &self,
130 _input: &str,
131 prediction: &str,
132 reference: &str,
133 ) -> Result<Score, EvalError> {
134 let claims = if self.llm_split {
135 self.split_claims_llm(prediction).await?
136 } else {
137 split_claims(prediction)
138 };
139 if claims.is_empty() {
140 return Ok(Score::new(self.empty_score).with_label("no_claims"));
141 }
142 let context = truncate(reference, self.max_context_chars);
144 let ctx = &context;
149 let total = claims.len();
150 let results: Vec<Result<bool, EvalError>> = stream::iter(claims)
151 .map(|claim| async move { self.verify_claim(ctx, &claim).await })
152 .buffer_unordered(MAX_CONCURRENT_VERIFY)
153 .collect()
154 .await;
155 let mut supported = 0usize;
156 for r in results {
157 if r? {
158 supported += 1;
159 }
160 }
161 let value = supported as f64 / total as f64;
162 Ok(Score::new(value).with_label("faithfulness"))
163 }
164
165 fn name(&self) -> &str {
166 "faithfulness"
167 }
168}
169
170#[derive(Debug, Deserialize)]
172struct VerdictArgs {
173 verdict: bool,
174 #[serde(default)]
176 #[allow(dead_code)]
177 reason: String,
178}
179
180fn verdict_tool() -> ToolDefinition {
182 ToolDefinition::new(
183 "check_claim",
184 "判断陈述能否从参考上下文推导出来,提交布尔判定。",
185 )
186 .with_parameters(serde_json::json!({
187 "type": "object",
188 "properties": {
189 "verdict": { "type": "boolean", "description": "能否从上下文推导" },
190 "reason": { "type": "string", "description": "简短依据" }
191 },
192 "required": ["verdict", "reason"]
193 }))
194}
195
196pub(crate) fn parse_yes_no(raw: &str) -> Option<bool> {
201 let lower = raw.to_lowercase();
202 if lower.contains("否")
204 || lower.contains("no")
205 || lower.contains("不能")
206 || lower.contains("不是")
207 || lower.contains("false")
208 {
209 return Some(false);
210 }
211 if lower.contains("是")
212 || lower.contains("yes")
213 || lower.contains("能")
214 || lower.contains("true")
215 {
216 return Some(true);
217 }
218 None
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use futures_util::Stream;
225 use lc_core::language_models::{LLMResult, StreamChunk};
226 use lc_core::{BaseLanguageModel, Runnable, RunnableConfig};
227 use lc_schema::MessageType;
228 use std::pin::Pin;
229 use std::sync::atomic::{AtomicUsize, Ordering};
230 use std::sync::{Arc, Mutex};
231
232 #[derive(Debug)]
233 struct JudgeError(String);
234 impl std::fmt::Display for JudgeError {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 write!(f, "{}", self.0)
237 }
238 }
239 impl std::error::Error for JudgeError {}
240
241 struct SeqMockJudge {
242 replies: Vec<String>,
243 call: Arc<AtomicUsize>,
244 last_user: Arc<Mutex<Option<String>>>,
245 }
246 impl SeqMockJudge {
247 fn new(replies: Vec<String>) -> Self {
248 Self {
249 replies,
250 call: Arc::new(AtomicUsize::new(0)),
251 last_user: Arc::new(Mutex::new(None)),
252 }
253 }
254 fn last_user_content(&self) -> String {
255 self.last_user
256 .lock()
257 .unwrap_or_else(|e| e.into_inner())
258 .clone()
259 .unwrap_or_default()
260 }
261 }
262
263 #[async_trait]
264 impl Runnable<Vec<Message>, LLMResult> for SeqMockJudge {
265 type Error = JudgeError;
266 async fn invoke(
267 &self,
268 _input: Vec<Message>,
269 _config: Option<RunnableConfig>,
270 ) -> Result<LLMResult, Self::Error> {
271 Err(JudgeError("use chat".into()))
272 }
273 }
274 #[async_trait]
275 impl BaseLanguageModel<Vec<Message>, LLMResult> for SeqMockJudge {
276 fn model_name(&self) -> &str {
277 "seq-mock"
278 }
279 fn get_num_tokens(&self, t: &str) -> usize {
280 t.len()
281 }
282 fn with_temperature(self, _: f32) -> Self {
283 self
284 }
285 fn with_max_tokens(self, _: usize) -> Self {
286 self
287 }
288 }
289 #[async_trait]
290 impl BaseChatModel for SeqMockJudge {
291 async fn chat(
292 &self,
293 messages: Vec<Message>,
294 _config: Option<RunnableConfig>,
295 ) -> Result<LLMResult, Self::Error> {
296 let idx = self.call.fetch_add(1, Ordering::SeqCst);
297 let reply = self.replies.get(idx).cloned().unwrap_or_default();
298 if let Some(human) = messages
299 .iter()
300 .find(|m| m.message_type == MessageType::Human)
301 {
302 *self.last_user.lock().unwrap_or_else(|e| e.into_inner()) =
303 Some(human.content.clone());
304 }
305 Ok(LLMResult {
306 content: reply,
307 model: "seq-mock".to_string(),
308 token_usage: None,
309 tool_calls: None,
310 thinking_content: None,
311 })
312 }
313 async fn stream_chat(
314 &self,
315 _messages: Vec<Message>,
316 _config: Option<RunnableConfig>,
317 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk, Self::Error>> + Send>>, Self::Error>
318 {
319 Err(JudgeError("not supported".into()))
320 }
321 }
322
323 #[test]
324 fn test_split_claims() {
325 let claims = split_claims("巴黎是法国首都。伦敦是英国首都。");
326 assert_eq!(claims.len(), 2);
327 assert_eq!(claims[0], "巴黎是法国首都");
328 assert_eq!(claims[1], "伦敦是英国首都");
329 }
330
331 #[test]
332 fn test_split_claims_empty() {
333 assert!(split_claims("").is_empty());
334 assert!(split_claims("。。。").is_empty());
335 }
336
337 #[tokio::test]
338 async fn test_faithfulness_all_supported() {
339 let judge = Faithfulness::new(SeqMockJudge::new(vec!["是".into(), "是".into()]));
340 let s = judge
341 .eval("", "巴黎是法国首都。伦敦是英国首都。", "ctx")
342 .await
343 .unwrap();
344 assert!((s.value - 1.0).abs() < 1e-9);
345 }
346
347 #[tokio::test]
348 async fn test_faithfulness_half_supported() {
349 let judge = Faithfulness::new(SeqMockJudge::new(vec!["是".into(), "否".into()]));
350 let s = judge
351 .eval("", "巴黎是法国首都。伦敦是英国首都。", "ctx")
352 .await
353 .unwrap();
354 assert!((s.value - 0.5).abs() < 1e-9);
355 }
356
357 #[tokio::test]
358 async fn test_faithfulness_none_supported() {
359 let judge = Faithfulness::new(SeqMockJudge::new(vec!["否".into(), "否".into()]));
360 let s = judge
361 .eval("", "巴黎是法国首都。伦敦是英国首都。", "ctx")
362 .await
363 .unwrap();
364 assert!((s.value - 0.0).abs() < 1e-9);
365 }
366
367 #[tokio::test]
368 async fn test_faithfulness_empty_prediction() {
369 let judge = Faithfulness::new(SeqMockJudge::new(vec![]));
371 let s = judge.eval("", "", "ctx").await.unwrap();
372 assert!((s.value - 0.0).abs() < 1e-9);
373 assert_eq!(s.label.as_deref(), Some("no_claims"));
374 }
375
376 #[tokio::test]
377 async fn test_faithfulness_empty_score_configurable() {
378 let judge = Faithfulness::new(SeqMockJudge::new(vec![])).with_empty_score(1.0);
380 let s = judge.eval("", "", "ctx").await.unwrap();
381 assert!((s.value - 1.0).abs() < 1e-9);
382 }
383
384 #[tokio::test]
385 async fn test_faithfulness_llm_split() {
386 let judge = Faithfulness::new(SeqMockJudge::new(vec![
388 "巴黎是法国首都\n伦敦是英国首都".into(),
389 "是".into(),
390 "是".into(),
391 ]))
392 .with_llm_split(true);
393 let s = judge
394 .eval("", "巴黎是法国首都,伦敦是英国首都。", "ctx")
395 .await
396 .unwrap();
397 assert!((s.value - 1.0).abs() < 1e-9);
398 }
399
400 #[test]
401 fn test_parse_yes_no() {
402 assert_eq!(parse_yes_no("是"), Some(true));
403 assert_eq!(parse_yes_no("yes"), Some(true));
404 assert_eq!(parse_yes_no("否"), Some(false));
405 assert_eq!(parse_yes_no("no"), Some(false));
406 assert_eq!(parse_yes_no("不是"), Some(false));
407 assert_eq!(parse_yes_no("不能"), Some(false));
408 assert_eq!(parse_yes_no("我不会告诉你"), None);
410 }
411
412 #[tokio::test]
414 async fn test_faithfulness_structured_verdict() {
415 use crate::test_support::ToolJudge;
416 let judge = Faithfulness::new(ToolJudge::sequence(vec![
418 r#"{"verdict": true, "reason": "能从上下文推导"}"#.into(),
419 r#"{"verdict": false, "reason": "无法推导"}"#.into(),
420 ]));
421 let s = judge
422 .eval("", "巴黎是法国首都。伦敦是英国首都。", "巴黎是法国首都")
423 .await
424 .unwrap();
425 assert!((s.value - 0.5).abs() < 1e-9);
426 }
427
428 #[tokio::test]
430 async fn test_faithfulness_structured_all_false() {
431 use crate::test_support::ToolJudge;
432 let judge = Faithfulness::new(ToolJudge::new(
433 r#"{"verdict": false, "reason": "均无法推导"}"#,
434 ));
435 let s = judge
436 .eval("", "巴黎是法国首都。伦敦是英国首都。", "巴黎是法国首都")
437 .await
438 .unwrap();
439 assert!((s.value - 0.0).abs() < 1e-9);
440 }
441
442 #[tokio::test]
444 async fn test_faithfulness_reference_truncated_once() {
445 let judge = SeqMockJudge::new(vec!["是".into(), "是".into()]);
446 let f = Faithfulness::new(judge).with_max_context_chars(10);
447 let long_ref =
448 "这是一段非常长的参考上下文,远超默认的单条传输上限,里面藏了一个不该被完整发送的尾巴"
449 .to_string();
450 let s = f
451 .eval("", "巴黎是首都。伦敦是首都。", &long_ref)
452 .await
453 .unwrap();
454 assert!((s.value - 1.0).abs() < 1e-9);
455 let sent = f.judge.last_user_content();
456 assert!(sent.contains("这是一段非常长"), "actual sent: {sent}");
458 assert!(
459 !sent.contains("不该被完整发送"),
460 "full long reference was sent repeatedly"
461 );
462 }
463}