Skip to main content

lc_evaluation/
faithfulness.rs

1//! 忠实度评测器:检测回答是否忠于参考上下文(幻觉检测)。
2//!
3//! 思路来自 Ragas 的 faithfulness:把回答拆成原子陈述,
4//! 逐条判断能否从参考上下文推导出来,通过率即为忠实度。
5//! 这里 reference 充当"上下文/检索内容",prediction 是待检测的回答。
6
7use async_trait::async_trait;
8
9use lc_core::BaseChatModel;
10use lc_schema::Message;
11
12use super::{EvalError, Evaluator, Score};
13
14/// 忠实度评测器(幻觉检测):回答有多忠于参考上下文。
15///
16/// 把 prediction 拆成原子陈述,逐条问裁判能否从 reference 推导,
17/// 通过率 = 能推导的陈述数 / 总陈述数。
18pub struct Faithfulness<M: BaseChatModel> {
19    judge: M,
20    /// 用 LLM 拆原子陈述(默认 false,用规则按标点拆)
21    llm_split: bool,
22    /// 空预测(无可验证陈述)的得分,默认 1.0(没编造=100% 忠实)
23    empty_score: f64,
24}
25
26/// 把回答拆成原子陈述(按句号、问号、感叹号、分号、换行切分)。
27fn 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    /// 用 LLM 拆原子陈述(默认按标点规则拆;LLM 拆能处理逗号复合句)
45    pub fn with_llm_split(mut self, v: bool) -> Self {
46        self.llm_split = v;
47        self
48    }
49
50    /// 空预测的得分:默认 1.0(没编造即忠实),可设 0.0 表示"没回答=0"
51    pub fn with_empty_score(mut self, score: f64) -> Self {
52        self.empty_score = score;
53        self
54    }
55
56    /// 问裁判:单条陈述能否从上下文推导。
57    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    /// 用 LLM 把回答拆成原子陈述(每行一条),处理规则拆不动的复合句。
72    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        // 并发验证各陈述:每条一次 LLM 调用,串行会变成 N 次顺序往返
108        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
127/// 解析"是/否"。
128fn parse_yes_no(raw: &str) -> bool {
129    let lower = raw.to_lowercase();
130    // 先判否定(避免"不是""不能"被"是""能"误判)
131    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); // 无陈述视为不撒谎
276    }
277
278    #[tokio::test]
279    async fn test_faithfulness_empty_score_configurable() {
280        // 默认空预测 1.0(没编造);可配置成 0.0(没回答)
281        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        // 逗号复合句规则拆只算 1 条;LLM 拆能拆成 2 条分别验证
289        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}