1use lc_core::BaseChatModel;
6use lc_schema::Message;
7
8use super::EvalError;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Verdict {
13 AWins,
14 BWins,
15 Tie,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20enum Pick {
21 First,
22 Second,
23 Tie,
24}
25
26pub struct PairwiseJudge<M: BaseChatModel> {
31 judge: M,
32 rubric: String,
33}
34
35const DEFAULT_PAIRWISE_RUBRIC: &str = "\
36正确性:回答是否事实准确、是否切题。
37完整性:是否完整回答了问题。
38清晰性:表达是否清晰、简洁。";
39
40impl<M: BaseChatModel> PairwiseJudge<M> {
41 pub fn new(judge: M) -> Self {
42 Self {
43 judge,
44 rubric: DEFAULT_PAIRWISE_RUBRIC.to_string(),
45 }
46 }
47
48 pub fn with_rubric(mut self, rubric: impl Into<String>) -> Self {
49 self.rubric = rubric.into();
50 self
51 }
52
53 pub async fn compare(&self, input: &str, a: &str, b: &str) -> Result<Verdict, EvalError> {
57 let v1 = self.ask(input, a, b).await?; let v2 = self.ask(input, b, a).await?; Ok(match (v1, v2) {
61 (Pick::Tie, _) | (_, Pick::Tie) => Verdict::Tie,
62 (Pick::First, Pick::Second) => Verdict::AWins, (Pick::Second, Pick::First) => Verdict::BWins, _ => Verdict::Tie, })
66 }
67
68 async fn ask(&self, input: &str, first: &str, second: &str) -> Result<Pick, EvalError> {
69 let system = format!(
70 "你是裁判。根据评分标准,判断两个回答哪个更好。\n\n\
71 评分标准:\n{rubric}\n\n\
72 只输出三者之一:\"第一个更好\" / \"第二个更好\" / \"平局\"",
73 rubric = self.rubric
74 );
75 let user =
76 format!("题目:\n{input}\n\n第一个回答:\n{first}\n\n第二个回答:\n{second}\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(parse_pick(&result.content))
83 }
84}
85
86fn parse_pick(raw: &str) -> Pick {
88 let lower = raw.to_lowercase();
89 if lower.contains("平局") || lower.contains("tie") || lower.contains("一样") {
90 return Pick::Tie;
91 }
92 let first_pos = ["第一个", "first", "前者", "former"]
94 .into_iter()
95 .filter_map(|kw| lower.find(kw))
96 .min();
97 let second_pos = ["第二个", "second", "后者", "latter"]
99 .into_iter()
100 .filter_map(|kw| lower.find(kw))
101 .min();
102 match (first_pos, second_pos) {
103 (Some(f), Some(s)) if f < s => Pick::First,
104 (Some(_), Some(_)) => Pick::Second,
105 (Some(_), None) => Pick::First,
106 (None, Some(_)) => Pick::Second,
107 (None, None) => Pick::Tie,
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use async_trait::async_trait;
115 use futures_util::Stream;
116 use lc_core::language_models::LLMResult;
117 use lc_core::{BaseLanguageModel, Runnable, RunnableConfig};
118 use std::pin::Pin;
119 use std::sync::atomic::{AtomicUsize, Ordering};
120 use std::sync::Arc;
121
122 #[derive(Debug)]
123 struct JudgeError(String);
124 impl std::fmt::Display for JudgeError {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "{}", self.0)
127 }
128 }
129 impl std::error::Error for JudgeError {}
130
131 struct SeqMockJudge {
133 replies: Vec<String>,
134 call: Arc<AtomicUsize>,
135 }
136 impl SeqMockJudge {
137 fn new(replies: Vec<String>) -> Self {
138 Self {
139 replies,
140 call: Arc::new(AtomicUsize::new(0)),
141 }
142 }
143 }
144
145 #[async_trait]
146 impl Runnable<Vec<Message>, LLMResult> for SeqMockJudge {
147 type Error = JudgeError;
148 async fn invoke(
149 &self,
150 _input: Vec<Message>,
151 _config: Option<RunnableConfig>,
152 ) -> Result<LLMResult, Self::Error> {
153 Err(JudgeError("use chat".into()))
154 }
155 }
156
157 #[async_trait]
158 impl BaseLanguageModel<Vec<Message>, LLMResult> for SeqMockJudge {
159 fn model_name(&self) -> &str {
160 "seq-mock"
161 }
162 fn get_num_tokens(&self, t: &str) -> usize {
163 t.len()
164 }
165 fn with_temperature(self, _: f32) -> Self {
166 self
167 }
168 fn with_max_tokens(self, _: usize) -> Self {
169 self
170 }
171 }
172
173 #[async_trait]
174 impl BaseChatModel for SeqMockJudge {
175 async fn chat(
176 &self,
177 _messages: Vec<Message>,
178 _config: Option<RunnableConfig>,
179 ) -> Result<LLMResult, Self::Error> {
180 let idx = self.call.fetch_add(1, Ordering::SeqCst);
181 let reply = self.replies.get(idx).cloned().unwrap_or_default();
182 Ok(LLMResult {
183 content: reply,
184 model: "seq-mock".to_string(),
185 token_usage: None,
186 tool_calls: None,
187 thinking_content: None,
188 })
189 }
190 async fn stream_chat(
191 &self,
192 _messages: Vec<Message>,
193 _config: Option<RunnableConfig>,
194 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error>
195 {
196 Err(JudgeError("not supported".into()))
197 }
198 }
199
200 #[tokio::test]
201 async fn test_pairwise_a_wins() {
202 let judge = PairwiseJudge::new(SeqMockJudge::new(vec![
204 "第一个更好".into(),
205 "第二个更好".into(),
206 ]));
207 assert_eq!(judge.compare("q", "A", "B").await.unwrap(), Verdict::AWins);
208 }
209
210 #[tokio::test]
211 async fn test_pairwise_b_wins() {
212 let judge = PairwiseJudge::new(SeqMockJudge::new(vec![
214 "第二个更好".into(),
215 "第一个更好".into(),
216 ]));
217 assert_eq!(judge.compare("q", "A", "B").await.unwrap(), Verdict::BWins);
218 }
219
220 #[tokio::test]
221 async fn test_pairwise_position_bias_tie() {
222 let judge = PairwiseJudge::new(SeqMockJudge::new(vec![
224 "第一个更好".into(),
225 "第一个更好".into(),
226 ]));
227 assert_eq!(judge.compare("q", "A", "B").await.unwrap(), Verdict::Tie);
228 }
229
230 #[tokio::test]
231 async fn test_pairwise_explicit_tie() {
232 let judge = PairwiseJudge::new(SeqMockJudge::new(vec!["平局".into(), "平局".into()]));
233 assert_eq!(judge.compare("q", "A", "B").await.unwrap(), Verdict::Tie);
234 }
235
236 #[test]
237 fn test_parse_pick() {
238 assert_eq!(parse_pick("第一个更好"), Pick::First);
239 assert_eq!(parse_pick("第二个更好"), Pick::Second);
240 assert_eq!(parse_pick("平局"), Pick::Tie);
241 assert_eq!(parse_pick("两个一样好"), Pick::Tie);
242 assert_eq!(parse_pick("第二个比第一个好"), Pick::Second);
243 assert_eq!(parse_pick("前者更好"), Pick::First);
245 assert_eq!(parse_pick("后者更准确"), Pick::Second);
246 assert_eq!(parse_pick("the former is better"), Pick::First);
247 assert_eq!(parse_pick("the latter wins"), Pick::Second);
248 }
249}