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