1use async_trait::async_trait;
6use futures_util::future;
7use serde::Deserialize;
8
9use lc_core::judge::{structured_call, truncate, StructuredJudgeError};
10use lc_core::tools::ToolDefinition;
11use lc_core::BaseChatModel;
12use lc_schema::Message;
13
14use super::{EvalError, PairwiseEvaluator, Score};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Verdict {
19 AWins,
21 BWins,
23 Tie,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29enum Pick {
30 First,
31 Second,
32 Tie,
33}
34
35pub struct PairwiseJudge<M: BaseChatModel> {
40 judge: M,
41 rubric: String,
42}
43
44const DEFAULT_PAIRWISE_RUBRIC: &str = "\
45正确性:回答是否事实准确、是否切题。
46完整性:是否完整回答了问题。
47清晰性:表达是否清晰、简洁。";
48
49impl<M: BaseChatModel> PairwiseJudge<M> {
50 pub fn new(judge: M) -> Self {
52 Self {
53 judge,
54 rubric: DEFAULT_PAIRWISE_RUBRIC.to_string(),
55 }
56 }
57
58 pub fn with_rubric(mut self, rubric: impl Into<String>) -> Self {
60 self.rubric = rubric.into();
61 self
62 }
63
64 pub async fn compare(&self, input: &str, a: &str, b: &str) -> Result<Verdict, EvalError> {
70 let (v1, v2) = future::join(self.ask(input, a, b), self.ask(input, b, a)).await;
71 let v1 = v1?; let v2 = v2?; Ok(match (v1, v2) {
75 (Pick::Tie, _) | (_, Pick::Tie) => Verdict::Tie,
76 (Pick::First, Pick::Second) => Verdict::AWins, (Pick::Second, Pick::First) => Verdict::BWins, _ => Verdict::Tie, })
80 }
81
82 async fn ask(&self, input: &str, first: &str, second: &str) -> Result<Pick, EvalError> {
83 let system = format!(
84 "你是裁判。根据评分标准,判断两个回答哪个更好。调用 pick_better 工具提交判定。\n\n\
85 评分标准:\n{rubric}\n\n\
86 判定的 verdict 取三者之一:\"a\"(第一个更好) / \"b\"(第二个更好) / \"tie\"(平局)",
87 rubric = self.rubric
88 );
89 let user =
90 format!("题目:\n{input}\n\n第一个回答:\n{first}\n\n第二个回答:\n{second}\n\n哪个更好?");
91 let messages = vec![Message::system(system), Message::human(user)];
92
93 let args: PickArgs = structured_call(&self.judge, pick_tool(), messages, |raw| {
95 let pick = parse_pick(raw).ok_or_else(|| {
96 StructuredJudgeError::Parse(format!(
97 "failed to parse winner from judge reply: {}",
98 truncate(raw, 200)
99 ))
100 })?;
101 Ok(PickArgs {
102 verdict: pick_to_str(pick).to_string(),
103 reason: String::new(),
104 })
105 })
106 .await?;
107 str_to_pick(&args.verdict)
108 }
109}
110
111#[async_trait]
115impl<M: BaseChatModel> PairwiseEvaluator for PairwiseJudge<M> {
116 async fn eval_pair(&self, input: &str, a: &str, b: &str) -> Result<Score, EvalError> {
117 let (value, label) = match self.compare(input, a, b).await? {
118 Verdict::AWins => (1.0, "a_wins"),
119 Verdict::Tie => (0.5, "tie"),
120 Verdict::BWins => (0.0, "b_wins"),
121 };
122 Ok(Score::new(value).with_label(label))
123 }
124
125 fn name(&self) -> &str {
126 "pairwise"
127 }
128}
129
130#[derive(Debug, Deserialize)]
132struct PickArgs {
133 verdict: String, #[serde(default)]
136 #[allow(dead_code)]
137 reason: String,
138}
139
140fn pick_tool() -> ToolDefinition {
142 ToolDefinition::new(
143 "pick_better",
144 "判断两个回答哪个更好。verdict 取 \"a\"(第一个更好)、\"b\"(第二个更好)、\"tie\"(平局)。",
145 )
146 .with_parameters(serde_json::json!({
147 "type": "object",
148 "properties": {
149 "verdict": {
150 "type": "string",
151 "enum": ["a", "b", "tie"],
152 "description": "a=第一个更好, b=第二个更好, tie=平局"
153 },
154 "reason": { "type": "string", "description": "简短理由" }
155 },
156 "required": ["verdict", "reason"]
157 }))
158}
159
160fn pick_to_str(pick: Pick) -> &'static str {
161 match pick {
162 Pick::First => "a",
163 Pick::Second => "b",
164 Pick::Tie => "tie",
165 }
166}
167
168fn str_to_pick(verdict: &str) -> Result<Pick, EvalError> {
170 match verdict {
171 "a" => Ok(Pick::First),
172 "b" => Ok(Pick::Second),
173 "tie" => Ok(Pick::Tie),
174 other => Err(EvalError::ParseError(format!(
175 "judge returned invalid verdict: {}",
176 other
177 ))),
178 }
179}
180
181fn parse_pick(raw: &str) -> Option<Pick> {
184 let lower = raw.to_lowercase();
185 if lower.contains("平局") || lower.contains("tie") || lower.contains("一样") {
186 return Some(Pick::Tie);
187 }
188 let first_pos = ["第一个", "first", "前者", "former"]
190 .into_iter()
191 .filter_map(|kw| lower.find(kw))
192 .min();
193 let second_pos = ["第二个", "second", "后者", "latter"]
195 .into_iter()
196 .filter_map(|kw| lower.find(kw))
197 .min();
198 match (first_pos, second_pos) {
199 (Some(f), Some(s)) if f < s => Some(Pick::First),
200 (Some(_), Some(_)) => Some(Pick::Second),
201 (Some(_), None) => Some(Pick::First),
202 (None, Some(_)) => Some(Pick::Second),
203 (None, None) => None,
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use async_trait::async_trait;
211 use futures_util::Stream;
212 use lc_core::language_models::{LLMResult, StreamChunk};
213 use lc_core::{BaseLanguageModel, Runnable, RunnableConfig};
214 use std::pin::Pin;
215 use std::sync::atomic::{AtomicUsize, Ordering};
216 use std::sync::Arc;
217
218 #[derive(Debug)]
219 struct JudgeError(String);
220 impl std::fmt::Display for JudgeError {
221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222 write!(f, "{}", self.0)
223 }
224 }
225 impl std::error::Error for JudgeError {}
226
227 struct SeqMockJudge {
229 replies: Vec<String>,
230 call: Arc<AtomicUsize>,
231 }
232 impl SeqMockJudge {
233 fn new(replies: Vec<String>) -> Self {
234 Self {
235 replies,
236 call: Arc::new(AtomicUsize::new(0)),
237 }
238 }
239 }
240
241 #[async_trait]
242 impl Runnable<Vec<Message>, LLMResult> for SeqMockJudge {
243 type Error = JudgeError;
244 async fn invoke(
245 &self,
246 _input: Vec<Message>,
247 _config: Option<RunnableConfig>,
248 ) -> Result<LLMResult, Self::Error> {
249 Err(JudgeError("use chat".into()))
250 }
251 }
252
253 #[async_trait]
254 impl BaseLanguageModel<Vec<Message>, LLMResult> for SeqMockJudge {
255 fn model_name(&self) -> &str {
256 "seq-mock"
257 }
258 fn get_num_tokens(&self, t: &str) -> usize {
259 t.len()
260 }
261 fn with_temperature(self, _: f32) -> Self {
262 self
263 }
264 fn with_max_tokens(self, _: usize) -> Self {
265 self
266 }
267 }
268
269 #[async_trait]
270 impl BaseChatModel for SeqMockJudge {
271 async fn chat(
272 &self,
273 _messages: Vec<Message>,
274 _config: Option<RunnableConfig>,
275 ) -> Result<LLMResult, Self::Error> {
276 let idx = self.call.fetch_add(1, Ordering::SeqCst);
277 let reply = self.replies.get(idx).cloned().unwrap_or_default();
278 Ok(LLMResult {
279 content: reply,
280 model: "seq-mock".to_string(),
281 token_usage: None,
282 tool_calls: None,
283 thinking_content: None,
284 })
285 }
286 async fn stream_chat(
287 &self,
288 _messages: Vec<Message>,
289 _config: Option<RunnableConfig>,
290 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk, Self::Error>> + Send>>, Self::Error>
291 {
292 Err(JudgeError("not supported".into()))
293 }
294 }
295
296 #[tokio::test]
297 async fn test_pairwise_a_wins() {
298 let judge = PairwiseJudge::new(SeqMockJudge::new(vec![
300 "第一个更好".into(),
301 "第二个更好".into(),
302 ]));
303 assert_eq!(judge.compare("q", "A", "B").await.unwrap(), Verdict::AWins);
304 }
305
306 #[tokio::test]
307 async fn test_pairwise_b_wins() {
308 let judge = PairwiseJudge::new(SeqMockJudge::new(vec![
310 "第二个更好".into(),
311 "第一个更好".into(),
312 ]));
313 assert_eq!(judge.compare("q", "A", "B").await.unwrap(), Verdict::BWins);
314 }
315
316 #[tokio::test]
317 async fn test_pairwise_position_bias_tie() {
318 let judge = PairwiseJudge::new(SeqMockJudge::new(vec![
320 "第一个更好".into(),
321 "第一个更好".into(),
322 ]));
323 assert_eq!(judge.compare("q", "A", "B").await.unwrap(), Verdict::Tie);
324 }
325
326 #[tokio::test]
327 async fn test_pairwise_explicit_tie() {
328 let judge = PairwiseJudge::new(SeqMockJudge::new(vec!["平局".into(), "平局".into()]));
329 assert_eq!(judge.compare("q", "A", "B").await.unwrap(), Verdict::Tie);
330 }
331
332 #[test]
333 fn test_parse_pick() {
334 assert_eq!(parse_pick("第一个更好"), Some(Pick::First));
335 assert_eq!(parse_pick("第二个更好"), Some(Pick::Second));
336 assert_eq!(parse_pick("平局"), Some(Pick::Tie));
337 assert_eq!(parse_pick("两个一样好"), Some(Pick::Tie));
338 assert_eq!(parse_pick("第二个比第一个好"), Some(Pick::Second));
339 assert_eq!(parse_pick("前者更好"), Some(Pick::First));
341 assert_eq!(parse_pick("后者更准确"), Some(Pick::Second));
342 assert_eq!(parse_pick("the former is better"), Some(Pick::First));
343 assert_eq!(parse_pick("the latter wins"), Some(Pick::Second));
344 assert_eq!(parse_pick("我无法判断"), None);
346 }
347
348 #[tokio::test]
350 async fn test_pairwise_structured_verdict() {
351 use crate::test_support::ToolJudge;
352 let judge = PairwiseJudge::new(ToolJudge::sequence(vec![
354 r#"{"verdict": "a", "reason": "第一个更完整"}"#.into(),
355 r#"{"verdict": "b", "reason": "第二个更完整"}"#.into(),
356 ]));
357 assert_eq!(judge.compare("q", "A", "B").await.unwrap(), Verdict::AWins);
358 }
359
360 #[tokio::test]
361 async fn test_pairwise_structured_verdict_b() {
362 use crate::test_support::ToolJudge;
363 let judge = PairwiseJudge::new(ToolJudge::sequence(vec![
365 r#"{"verdict": "b", "reason": "第二个更准确"}"#.into(),
366 r#"{"verdict": "a", "reason": "第一个更准确"}"#.into(),
367 ]));
368 assert_eq!(judge.compare("q", "A", "B").await.unwrap(), Verdict::BWins);
369 }
370
371 #[tokio::test]
372 async fn test_pairwise_structured_verdict_tie() {
373 use crate::test_support::ToolJudge;
374 let judge = PairwiseJudge::new(ToolJudge::new(
375 r#"{"verdict": "tie", "reason": "难分高下"}"#,
376 ));
377 assert_eq!(judge.compare("q", "A", "B").await.unwrap(), Verdict::Tie);
378 }
379}