varib commited on
Commit
f81ee9b
·
verified ·
1 Parent(s): c3dc5dd

Update evaluate.py

Browse files
Files changed (1) hide show
  1. evaluate.py +270 -141
evaluate.py CHANGED
@@ -81,9 +81,6 @@ SUB_TYPE_TO_ABBR = {
81
  "Answer Refusal": "AR"
82
  }
83
 
84
- # Sub type 简称到全称的映射(用于调试)
85
- ABBR_TO_SUB_TYPE = {v: k for k, v in SUB_TYPE_TO_ABBR.items()}
86
-
87
  # 指标列表
88
  METRICS = ["LLM", "P", "R", "F1", "BLEU-1"]
89
 
@@ -175,7 +172,6 @@ class LexicalMetricsCalculator:
175
  return 2 * prec * rec / (prec + rec) if (prec + rec) > 0 else 0.0
176
 
177
  def _bleu1(self, prediction: str, reference: str, **kwargs) -> float:
178
- # Tokenize
179
  pred_tokens = self.text_processor.tokenize(prediction, remove_stopwords=False)
180
  ref_tokens = self.text_processor.tokenize(reference, remove_stopwords=False)
181
 
@@ -184,27 +180,25 @@ class LexicalMetricsCalculator:
184
  if pred_tokens == ref_tokens:
185
  return 1.0
186
 
187
- # Use sentence_bleu to calculate BLEU-1 (weights=(1, 0, 0, 0) means use only 1-gram)
188
  try:
189
- # Add smoothing function to avoid zero score issues
190
  smoothie = SmoothingFunction().method4
191
  bleu_score = sentence_bleu(
192
- [ref_tokens], # Reference needs to be a list of lists
193
  pred_tokens,
194
- weights=(1.0, 0, 0, 0), # BLEU-1
195
  smoothing_function=smoothie
196
  )
197
  return float(bleu_score)
198
- except Exception as e:
199
  return 0.0
200
 
201
 
202
  # =========================================================
203
- # 第二部分:LLM-as-judge 评估器
204
  # =========================================================
205
 
206
  class LLMJudgeEvaluator:
207
- """LLM-as-judge 评估器"""
208
 
209
  SCORE_RUBRIC = """
210
  **Score 0 (Incorrect / Miss):**
@@ -232,6 +226,7 @@ class LLMJudgeEvaluator:
232
  self.base_url = self.config.get('base_url') or os.getenv('OPENAI_BASE_URL', 'https://api.openai.com/v1')
233
  self.model_name = self.config.get('model_name', 'gpt-4o-mini')
234
  self.temperature = self.config.get('temperature', 0)
 
235
  self._client = None
236
 
237
  def _get_client(self):
@@ -241,60 +236,120 @@ class LLMJudgeEvaluator:
241
  self._client = OpenAI(api_key=self.api_key, base_url=self.base_url)
242
  return self._client
243
 
244
- def _build_prompt(self, question: str, ground_truth: str, assistant_answer: str) -> str:
245
- return f"""You are an impartial judge evaluating an AI assistant's answer.
 
 
246
 
247
  ### Scoring Rubric
248
  {self.SCORE_RUBRIC}
249
 
250
- ### Question
251
- {question}
252
-
253
- ### Ground Truth
254
- {ground_truth}
 
 
 
255
 
256
- ### Assistant Answer
257
- {assistant_answer if assistant_answer else "(No answer provided)"}
258
 
 
259
  ### Output Format
260
- Output strictly in JSON format:
261
- {{"score": 0.75, "reasoning": "<short explanation>"}}
262
-
263
- Score must be one of: 0, 0.25, 0.5, 0.75, or 1.
 
 
 
 
 
 
264
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
  @tenacity.retry(
267
  stop=tenacity.stop_after_attempt(3),
268
  wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
269
  retry=tenacity.retry_if_exception_type(Exception),
270
  )
271
- def evaluate_single(self, question: str, ground_truth: str, assistant_answer: str) -> Dict[str, Any]:
 
 
 
 
272
  try:
273
  client = self._get_client()
274
- prompt = self._build_prompt(question, ground_truth, assistant_answer)
275
 
276
  response = client.chat.completions.create(
277
  model=self.model_name,
278
  messages=[{"role": "user", "content": prompt}],
279
  temperature=self.temperature,
280
- timeout=30
281
  )
282
 
283
  response_text = response.choices[0].message.content.strip()
284
- json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
285
 
286
- if json_match:
287
- result = json.loads(json_match.group())
288
- score = float(result.get('score', 0))
289
- valid_scores = [0, 0.25, 0.5, 0.75, 1]
290
- if score not in valid_scores:
291
- score = min(valid_scores, key=lambda x: abs(x - score))
292
- return {'score': score, 'reasoning': result.get('reasoning', ''), 'success': True}
293
- else:
294
- return {'score': 0, 'reasoning': 'Parse failed', 'success': False}
295
  except Exception as e:
296
- logger.error(f"LLM评估失败: {e}")
297
- return {'score': 0, 'reasoning': str(e), 'success': False}
 
 
 
 
 
 
 
 
 
 
298
 
299
 
300
  # =========================================================
@@ -305,21 +360,18 @@ class ScoreCollector:
305
  """分数收集器,用于按类别统计加权平均"""
306
 
307
  def __init__(self):
308
- self.scores = defaultdict(list) # {category: [scores]}
309
- self.weights = defaultdict(int) # {category: count}
310
 
311
  def add(self, category: str, score: float):
312
- """添加一个分数"""
313
  self.scores[category].append(score)
314
  self.weights[category] += 1
315
 
316
  def get_weighted_average(self, category: str = None) -> float:
317
- """获取加权平均分数"""
318
  if category:
319
  scores = self.scores.get(category, [])
320
  return np.mean(scores) if scores else 0.0
321
 
322
- # 计算所有分数的加权平均
323
  total_score = 0
324
  total_weight = 0
325
  for cat, scores in self.scores.items():
@@ -328,7 +380,6 @@ class ScoreCollector:
328
  return total_score / total_weight if total_weight > 0 else 0.0
329
 
330
  def get_all_averages(self) -> Dict[str, float]:
331
- """获取所有类别的平均值"""
332
  return {cat: np.mean(scores) if scores else 0.0
333
  for cat, scores in self.scores.items()}
334
 
@@ -348,7 +399,7 @@ class SubmissionEvaluator:
348
  if self.enable_llm_judge and llm_config:
349
  try:
350
  self.llm_judge = LLMJudgeEvaluator(llm_config)
351
- logger.info("LLM-as-judge 已启用")
352
  except Exception as e:
353
  logger.warning(f"LLM-as-judge 初始化失败: {e}")
354
  self.enable_llm_judge = False
@@ -363,129 +414,206 @@ class SubmissionEvaluator:
363
  raise ValueError(f"文件格式错误: 缺少 'predictions' 字段")
364
  return data['predictions']
365
 
366
- def evaluate_single_prediction(self, pred: Dict, enable_llm: bool = True) -> Dict[str, Any]:
367
- """评估单个预测,返回各指标的分数"""
368
  system_answer = pred.get('system_answer', '').strip()
369
  original_answer = pred.get('original_answer', '').strip()
370
 
371
- # 计算词法指标
372
  lexical = self.lexical_calculator.calculate(system_answer, original_answer)
373
 
374
- result = {
375
  'P': lexical['precision'],
376
  'R': lexical['recall'],
377
  'F1': lexical['f1'],
378
- 'BLEU-1': lexical['bleu1'],
379
- 'LLM': 0.0
380
  }
381
-
382
- # LLM 评估
383
- if self.enable_llm_judge and enable_llm and self.llm_judge:
384
- llm_result = self.llm_judge.evaluate_single(
385
- question=pred.get('question_text', ''),
386
- ground_truth=original_answer,
387
- assistant_answer=system_answer
388
- )
389
- if llm_result['success']:
390
- result['LLM'] = llm_result['score']
391
-
392
- return result
393
 
394
- def evaluate_dataset(self, predictions: List[Dict], dataset_name: str) -> Dict[str, Any]:
395
  """
396
  评估单个数据集,按 main_type 和 sub_type 分类统计
397
-
398
- Returns:
399
- 包含按指标、按 main_type、按 sub_type 分类的分数
400
  """
401
- # 初始化收集器
402
- # 按指标和 main_type 收集
403
- metric_main_collectors = {metric: {main_type: ScoreCollector() for main_type in MAIN_TYPE_MAPPING.values()}
404
- for metric in METRICS}
405
 
406
- # 按指标和 sub_type 收集
407
- metric_sub_collectors = {metric: defaultdict(ScoreCollector) for metric in METRICS}
 
408
 
409
- # 总问题数统计
410
- total_count = len(predictions)
411
- main_type_counts = defaultdict(int)
412
- sub_type_counts = defaultdict(int)
413
 
414
- for pred in predictions:
 
 
 
 
 
415
  # 获取类型信息
416
  main_type_full = pred.get('question_type', {}).get('main_type', '')
417
  sub_type_full = pred.get('question_type', {}).get('sub_type', '')
418
 
419
- # 映射到内部标识
420
  main_type = MAIN_TYPE_MAPPING.get(main_type_full, '')
421
- sub_type_abbr = SUB_TYPE_TO_ABBR.get(sub_type_full, sub_type_full)
422
-
423
- # 统计数量
424
- if main_type:
425
- main_type_counts[main_type] += 1
426
- if sub_type_abbr:
427
- sub_type_counts[sub_type_abbr] += 1
428
-
429
- # 评估当前预测
430
- scores = self.evaluate_single_prediction(pred)
431
-
432
- # 按 main_type 收集分数
433
- if main_type:
434
- for metric, score in scores.items():
435
- metric_main_collectors[metric][main_type].add(main_type, score)
436
-
437
- # 按 sub_type 收集分数
438
- if sub_type_abbr:
439
- for metric, score in scores.items():
440
- metric_sub_collectors[metric][sub_type_abbr].add(sub_type_abbr, score)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
  # 构建结果
443
  result = {
444
- 'total': total_count,
445
- 'main_type_counts': dict(main_type_counts),
446
- 'sub_type_counts': dict(sub_type_counts)
447
  }
448
 
449
- # 按指标组织结果
450
  for metric in METRICS:
451
- result[metric] = {}
452
-
453
- # 计算 overall(所有问题的平均)
454
- all_scores = []
455
- for pred in predictions:
456
- scores = self.evaluate_single_prediction(pred)
457
- all_scores.append(scores[metric])
458
- result[metric]['overall'] = np.mean(all_scores) if all_scores else 0.0
459
-
460
- # 按 main_type 计算
461
- result[metric]['recall'] = {
462
- 'score': metric_main_collectors[metric].get('recall', ScoreCollector()).get_weighted_average(),
463
- 'sub_types': {}
464
- }
465
- result[metric]['reasoning'] = {
466
- 'score': metric_main_collectors[metric].get('reasoning', ScoreCollector()).get_weighted_average(),
467
- 'sub_types': {}
468
- }
469
- result[metric]['application'] = {
470
- 'score': metric_main_collectors[metric].get('application', ScoreCollector()).get_weighted_average(),
471
- 'sub_types': {}
472
  }
473
 
474
- # 按 sub_type 填充子类型分数
475
- # Recall 子类型
476
- for sub in ['UPR', 'CRR', 'KR']:
477
- collector = metric_sub_collectors[metric].get(sub, ScoreCollector())
478
- result[metric]['recall']['sub_types'][sub] = collector.get_weighted_average()
479
-
480
- # Reasoning 子类型
481
- for sub in ['TR', 'MCR', 'RET']:
482
- collector = metric_sub_collectors[metric].get(sub, ScoreCollector())
483
- result[metric]['reasoning']['sub_types'][sub] = collector.get_weighted_average()
484
-
485
- # Application 子类型
486
- for sub in ['TTL', 'CD', 'AR']:
487
- collector = metric_sub_collectors[metric].get(sub, ScoreCollector())
488
- result[metric]['application']['sub_types'][sub] = collector.get_weighted_average()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
  return result
491
 
@@ -676,7 +804,8 @@ def evaluate_submission(dyadic_path: str, multiparty_path: str,
676
  'api_key': os.getenv('OPENAI_API_KEY'),
677
  'base_url': os.getenv('OPENAI_BASE_URL', 'https://api.openai.com/v1'),
678
  'model_name': os.getenv('LLM_JUDGE_MODEL', 'gpt-4o-mini'),
679
- 'temperature': 0
 
680
  }
681
  _evaluator = SubmissionEvaluator(llm_config=llm_config, enable_llm_judge=True)
682
 
 
81
  "Answer Refusal": "AR"
82
  }
83
 
 
 
 
84
  # 指标列表
85
  METRICS = ["LLM", "P", "R", "F1", "BLEU-1"]
86
 
 
172
  return 2 * prec * rec / (prec + rec) if (prec + rec) > 0 else 0.0
173
 
174
  def _bleu1(self, prediction: str, reference: str, **kwargs) -> float:
 
175
  pred_tokens = self.text_processor.tokenize(prediction, remove_stopwords=False)
176
  ref_tokens = self.text_processor.tokenize(reference, remove_stopwords=False)
177
 
 
180
  if pred_tokens == ref_tokens:
181
  return 1.0
182
 
 
183
  try:
 
184
  smoothie = SmoothingFunction().method4
185
  bleu_score = sentence_bleu(
186
+ [ref_tokens],
187
  pred_tokens,
188
+ weights=(1.0, 0, 0, 0),
189
  smoothing_function=smoothie
190
  )
191
  return float(bleu_score)
192
+ except Exception:
193
  return 0.0
194
 
195
 
196
  # =========================================================
197
+ # 第二部分:LLM-as-judge 评估器(支持批量评估)
198
  # =========================================================
199
 
200
  class LLMJudgeEvaluator:
201
+ """LLM-as-judge 评估器 - 支持批量评估"""
202
 
203
  SCORE_RUBRIC = """
204
  **Score 0 (Incorrect / Miss):**
 
226
  self.base_url = self.config.get('base_url') or os.getenv('OPENAI_BASE_URL', 'https://api.openai.com/v1')
227
  self.model_name = self.config.get('model_name', 'gpt-4o-mini')
228
  self.temperature = self.config.get('temperature', 0)
229
+ self.batch_size = self.config.get('batch_size', 5)
230
  self._client = None
231
 
232
  def _get_client(self):
 
236
  self._client = OpenAI(api_key=self.api_key, base_url=self.base_url)
237
  return self._client
238
 
239
+ def _build_batch_prompt(self, qa_pairs: List[Dict[str, str]]) -> str:
240
+ """构建批量评估的提示词,包含临时 ID"""
241
+ prompt = f"""You are an impartial judge evaluating an AI assistant's answers.
242
+ You will evaluate {len(qa_pairs)} questions and provide scores for each.
243
 
244
  ### Scoring Rubric
245
  {self.SCORE_RUBRIC}
246
 
247
+ ### Questions and Answers
248
+ """
249
+ for item in qa_pairs:
250
+ prompt += f"""
251
+ --- Question ID: {item['temp_id']} ---
252
+ Question: {item['question']}
253
+ Ground Truth: {item['ground_truth']}
254
+ Assistant Answer: {item['assistant_answer'] if item['assistant_answer'] else "(No answer provided)"}
255
 
256
+ """
 
257
 
258
+ prompt += """
259
  ### Output Format
260
+ Output strictly in JSON format as a list of objects, each containing the question_id and score:
261
+ [
262
+ {"question_id": 0, "score": 0.75, "reasoning": "<explanation>"},
263
+ {"question_id": 1, "score": 0.5, "reasoning": "<explanation>"},
264
+ ...
265
+ ]
266
+
267
+ Each score must be one of: 0, 0.25, 0.5, 0.75, or 1.
268
+ The number of objects must equal the number of questions.
269
+ The question_id must match the ID provided above.
270
  """
271
+ return prompt
272
+
273
+ def _parse_batch_response(self, response_text: str, qa_pairs: List[Dict[str, str]]) -> List[Dict[str, Any]]:
274
+ """解析批量评估的响应,按临时 ID 匹配"""
275
+ expected_count = len(qa_pairs)
276
+
277
+ # 创建 ID 到结果的映射
278
+ result_map = {item['temp_id']: None for item in qa_pairs}
279
+
280
+ try:
281
+ # 尝试解析 JSON 数组
282
+ json_match = re.search(r'\[.*\]', response_text, re.DOTALL)
283
+ if json_match:
284
+ parsed_results = json.loads(json_match.group())
285
+
286
+ if isinstance(parsed_results, list):
287
+ for r in parsed_results:
288
+ if isinstance(r, dict) and 'question_id' in r:
289
+ qid = r['question_id']
290
+ score = float(r.get('score', 0))
291
+ valid_scores = [0, 0.25, 0.5, 0.75, 1]
292
+ if score not in valid_scores:
293
+ score = min(valid_scores, key=lambda x: abs(x - score))
294
+ result_map[qid] = {
295
+ 'score': score,
296
+ 'reasoning': r.get('reasoning', ''),
297
+ 'success': True
298
+ }
299
+
300
+ # 为每个问题返回结果(未匹配的使用默认值)
301
+ results = []
302
+ for item in qa_pairs:
303
+ qid = item['temp_id']
304
+ if result_map[qid] is not None:
305
+ results.append(result_map[qid])
306
+ else:
307
+ logger.warning(f"问题 ID {qid} 未在响应中找到,使用默认值")
308
+ results.append({'score': 0, 'reasoning': 'Not found in response', 'success': False})
309
+
310
+ return results
311
+
312
+ except Exception as e:
313
+ logger.error(f"解析批量响应失败: {e}")
314
+ return [{'score': 0, 'reasoning': str(e), 'success': False} for _ in range(expected_count)]
315
 
316
  @tenacity.retry(
317
  stop=tenacity.stop_after_attempt(3),
318
  wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
319
  retry=tenacity.retry_if_exception_type(Exception),
320
  )
321
+ def evaluate_batch(self, qa_pairs: List[Dict[str, str]]) -> List[Dict[str, Any]]:
322
+ """批量评估多个问答对"""
323
+ if not qa_pairs:
324
+ return []
325
+
326
  try:
327
  client = self._get_client()
328
+ prompt = self._build_batch_prompt(qa_pairs)
329
 
330
  response = client.chat.completions.create(
331
  model=self.model_name,
332
  messages=[{"role": "user", "content": prompt}],
333
  temperature=self.temperature,
334
+ timeout=120
335
  )
336
 
337
  response_text = response.choices[0].message.content.strip()
338
+ return self._parse_batch_response(response_text, qa_pairs)
339
 
 
 
 
 
 
 
 
 
 
340
  except Exception as e:
341
+ logger.error(f"批量LLM评估失败: {e}")
342
+ return [{'score': 0, 'reasoning': str(e), 'success': False} for _ in range(len(qa_pairs))]
343
+
344
+ def evaluate_single(self, question: str, ground_truth: str, assistant_answer: str) -> Dict[str, Any]:
345
+ """单个评估(向后兼容)"""
346
+ result = self.evaluate_batch([{
347
+ 'temp_id': 0,
348
+ 'question': question,
349
+ 'ground_truth': ground_truth,
350
+ 'assistant_answer': assistant_answer
351
+ }])
352
+ return result[0] if result else {'score': 0, 'reasoning': 'Failed', 'success': False}
353
 
354
 
355
  # =========================================================
 
360
  """分数收集器,用于按类别统计加权平均"""
361
 
362
  def __init__(self):
363
+ self.scores = defaultdict(list)
364
+ self.weights = defaultdict(int)
365
 
366
  def add(self, category: str, score: float):
 
367
  self.scores[category].append(score)
368
  self.weights[category] += 1
369
 
370
  def get_weighted_average(self, category: str = None) -> float:
 
371
  if category:
372
  scores = self.scores.get(category, [])
373
  return np.mean(scores) if scores else 0.0
374
 
 
375
  total_score = 0
376
  total_weight = 0
377
  for cat, scores in self.scores.items():
 
380
  return total_score / total_weight if total_weight > 0 else 0.0
381
 
382
  def get_all_averages(self) -> Dict[str, float]:
 
383
  return {cat: np.mean(scores) if scores else 0.0
384
  for cat, scores in self.scores.items()}
385
 
 
399
  if self.enable_llm_judge and llm_config:
400
  try:
401
  self.llm_judge = LLMJudgeEvaluator(llm_config)
402
+ logger.info(f"LLM-as-judge 已启用,批量大小: {self.llm_judge.batch_size}")
403
  except Exception as e:
404
  logger.warning(f"LLM-as-judge 初始化失败: {e}")
405
  self.enable_llm_judge = False
 
414
  raise ValueError(f"文件格式错误: 缺少 'predictions' 字段")
415
  return data['predictions']
416
 
417
+ def evaluate_single_prediction(self, pred: Dict) -> Dict[str, float]:
418
+ """评估单个预测的词法指标(不包含 LLM)"""
419
  system_answer = pred.get('system_answer', '').strip()
420
  original_answer = pred.get('original_answer', '').strip()
421
 
 
422
  lexical = self.lexical_calculator.calculate(system_answer, original_answer)
423
 
424
+ return {
425
  'P': lexical['precision'],
426
  'R': lexical['recall'],
427
  'F1': lexical['f1'],
428
+ 'BLEU-1': lexical['bleu1']
 
429
  }
 
 
 
 
 
 
 
 
 
 
 
 
430
 
431
+ def evaluate_dataset(self, predictions: List[Dict], dataset_name: str, enable_llm: bool = True) -> Dict[str, Any]:
432
  """
433
  评估单个数据集,按 main_type 和 sub_type 分类统计
 
 
 
434
  """
435
+ # 存储每个问题的词法指标
436
+ all_lexical_metrics = []
437
+ category_lexical_metrics = {cat: [] for cat in SUB_TYPE_TO_ABBR.values()}
 
438
 
439
+ # 按 main_type 和 sub_type 分类的收集器(词法指标)
440
+ main_type_lexical = {mt: [] for mt in ['recall', 'reasoning', 'application']}
441
+ sub_type_lexical = {st: [] for st in SUB_TYPE_TO_ABBR.values()}
442
 
443
+ # 存储每个问题的完整信息(用于后续分类)
444
+ questions_info = []
 
 
445
 
446
+ # 第一遍:计算词法指标,收集问题信息
447
+ for idx, pred in enumerate(predictions):
448
+ system_answer = pred.get('system_answer', '').strip()
449
+ original_answer = pred.get('original_answer', '').strip()
450
+ question_text = pred.get('question_text', '')
451
+
452
  # 获取类型信息
453
  main_type_full = pred.get('question_type', {}).get('main_type', '')
454
  sub_type_full = pred.get('question_type', {}).get('sub_type', '')
455
 
 
456
  main_type = MAIN_TYPE_MAPPING.get(main_type_full, '')
457
+ sub_type_abbr = SUB_TYPE_TO_ABBR.get(sub_type_full, '')
458
+
459
+ # 计算词法指标
460
+ lexical_scores = self.evaluate_single_prediction(pred)
461
+
462
+ # 存储信息
463
+ questions_info.append({
464
+ 'index': idx,
465
+ 'main_type': main_type,
466
+ 'sub_type': sub_type_abbr,
467
+ 'lexical_scores': lexical_scores,
468
+ 'question_text': question_text,
469
+ 'original_answer': original_answer,
470
+ 'system_answer': system_answer
471
+ })
472
+
473
+ # 收集词法指标
474
+ all_lexical_metrics.append(lexical_scores)
475
+
476
+ # 按类别收集词法指标
477
+ if sub_type_abbr and sub_type_abbr in category_lexical_metrics:
478
+ category_lexical_metrics[sub_type_abbr].append(lexical_scores)
479
+
480
+ # 按 main_type 收集
481
+ if main_type and main_type in main_type_lexical:
482
+ main_type_lexical[main_type].append(lexical_scores)
483
+
484
+ # 按 sub_type 收集
485
+ if sub_type_abbr and sub_type_abbr in sub_type_lexical:
486
+ sub_type_lexical[sub_type_abbr].append(lexical_scores)
487
+
488
+ # =====================================================
489
+ # LLM-as-judge 批量评估
490
+ # =====================================================
491
+ llm_scores_by_index = {} # 索引 -> LLM分数
492
+
493
+ if self.enable_llm_judge and enable_llm and self.llm_judge:
494
+ # 准备批量评估数据
495
+ batch_size = self.llm_judge.batch_size
496
+ total_questions = len(questions_info)
497
+ total_batches = (total_questions + batch_size - 1) // batch_size
498
+
499
+ logger.info(f"开始批量LLM评估,共 {total_questions} 个问题,分成 {total_batches} 批(每批最多 {batch_size} 个)")
500
+
501
+ for batch_start in range(0, total_questions, batch_size):
502
+ batch_end = min(batch_start + batch_size, total_questions)
503
+ batch_questions = questions_info[batch_start:batch_end]
504
+ current_batch_num = batch_start // batch_size + 1
505
+
506
+ logger.info(f"评估第 {current_batch_num}/{total_batches} 批,包含 {len(batch_questions)} 个问题...")
507
+
508
+ # 准备批量数据,添加临时 ID
509
+ batch_data = []
510
+ for i, q in enumerate(batch_questions):
511
+ batch_data.append({
512
+ 'temp_id': batch_start + i, # 使用原始索引作为临时 ID
513
+ 'question': q['question_text'],
514
+ 'ground_truth': q['original_answer'],
515
+ 'assistant_answer': q['system_answer']
516
+ })
517
+
518
+ # 批量调用 LLM
519
+ batch_results = self.llm_judge.evaluate_batch(batch_data)
520
+
521
+ # 存储结果
522
+ for i, result in enumerate(batch_results):
523
+ original_idx = batch_start + i
524
+ if result['success']:
525
+ llm_scores_by_index[original_idx] = result['score']
526
+ else:
527
+ llm_scores_by_index[original_idx] = 0.0
528
+
529
+ success_count = len([r for r in batch_results if r['success']])
530
+ logger.info(f"第 {current_batch_num} 批完成,成功 {success_count}/{len(batch_questions)} 个")
531
+
532
+ logger.info(f"LLM批量评估完成,总成功 {len(llm_scores_by_index)}/{total_questions} 个")
533
+
534
+ # =====================================================
535
+ # 收集完整的分数(词法 + LLM)
536
+ # =====================================================
537
+
538
+ # 用于收集按类别的 LLM 分数
539
+ main_type_llm = {mt: [] for mt in ['recall', 'reasoning', 'application']}
540
+ sub_type_llm = {st: [] for st in SUB_TYPE_TO_ABBR.values()}
541
+
542
+ for q in questions_info:
543
+ idx = q['index']
544
+ llm_score = llm_scores_by_index.get(idx, 0.0)
545
+
546
+ # 按 main_type 收集 LLM 分数
547
+ if q['main_type'] and q['main_type'] in main_type_llm:
548
+ main_type_llm[q['main_type']].append(llm_score)
549
+
550
+ # 按 sub_type 收集 LLM 分数
551
+ if q['sub_type'] and q['sub_type'] in sub_type_llm:
552
+ sub_type_llm[q['sub_type']].append(llm_score)
553
+
554
+ # =====================================================
555
+ # 计算各类别的平均分
556
+ # =====================================================
557
+
558
+ def calc_avg(scores_list):
559
+ return np.mean(scores_list) if scores_list else 0.0
560
+
561
+ # 计算整体词法平均
562
+ overall_lexical = {
563
+ 'P': calc_avg([m['P'] for m in all_lexical_metrics]),
564
+ 'R': calc_avg([m['R'] for m in all_lexical_metrics]),
565
+ 'F1': calc_avg([m['F1'] for m in all_lexical_metrics]),
566
+ 'BLEU-1': calc_avg([m['BLEU-1'] for m in all_lexical_metrics])
567
+ }
568
+
569
+ # 计算整体 LLM 平均
570
+ overall_llm = calc_avg(list(llm_scores_by_index.values()))
571
 
572
  # 构建结果
573
  result = {
574
+ 'total': len(predictions),
575
+ 'main_type_counts': {mt: len(main_type_lexical[mt]) for mt in main_type_lexical},
576
+ 'sub_type_counts': {st: len(sub_type_lexical[st]) for st in sub_type_lexical}
577
  }
578
 
579
+ # 为每个指标构建结果
580
  for metric in METRICS:
581
+ result[metric] = {
582
+ 'overall': 0.0,
583
+ 'recall': {'score': 0.0, 'sub_types': {}},
584
+ 'reasoning': {'score': 0.0, 'sub_types': {}},
585
+ 'application': {'score': 0.0, 'sub_types': {}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  }
587
 
588
+ if metric == 'LLM':
589
+ # LLM 指标
590
+ result[metric]['overall'] = overall_llm
591
+ for mt in ['recall', 'reasoning', 'application']:
592
+ result[metric][mt]['score'] = calc_avg(main_type_llm.get(mt, []))
593
+ for st, abbr in SUB_TYPE_TO_ABBR.items():
594
+ if abbr in ['UPR', 'CRR', 'KR']:
595
+ result[metric]['recall']['sub_types'][abbr] = calc_avg(sub_type_llm.get(abbr, []))
596
+ elif abbr in ['TR', 'MCR', 'RET']:
597
+ result[metric]['reasoning']['sub_types'][abbr] = calc_avg(sub_type_llm.get(abbr, []))
598
+ elif abbr in ['TTL', 'CD', 'AR']:
599
+ result[metric]['application']['sub_types'][abbr] = calc_avg(sub_type_llm.get(abbr, []))
600
+ else:
601
+ # 词法指标
602
+ metric_key = metric if metric != 'BLEU-1' else 'BLEU-1'
603
+ result[metric]['overall'] = overall_lexical.get(metric_key, 0.0)
604
+
605
+ for mt in ['recall', 'reasoning', 'application']:
606
+ scores = [m[metric_key] for m in main_type_lexical.get(mt, [])]
607
+ result[metric][mt]['score'] = calc_avg(scores)
608
+
609
+ for st, abbr in SUB_TYPE_TO_ABBR.items():
610
+ scores = [m[metric_key] for m in sub_type_lexical.get(abbr, [])]
611
+ if abbr in ['UPR', 'CRR', 'KR']:
612
+ result[metric]['recall']['sub_types'][abbr] = calc_avg(scores)
613
+ elif abbr in ['TR', 'MCR', 'RET']:
614
+ result[metric]['reasoning']['sub_types'][abbr] = calc_avg(scores)
615
+ elif abbr in ['TTL', 'CD', 'AR']:
616
+ result[metric]['application']['sub_types'][abbr] = calc_avg(scores)
617
 
618
  return result
619
 
 
804
  'api_key': os.getenv('OPENAI_API_KEY'),
805
  'base_url': os.getenv('OPENAI_BASE_URL', 'https://api.openai.com/v1'),
806
  'model_name': os.getenv('LLM_JUDGE_MODEL', 'gpt-4o-mini'),
807
+ 'temperature': 0,
808
+ 'batch_size': 5 # 每批最多5个问题
809
  }
810
  _evaluator = SubmissionEvaluator(llm_config=llm_config, enable_llm_judge=True)
811