"""LLM-based scoring evaluation rule.""" import json import httpx from agenteval.evaluation.rules.base import EvalRule, RuleResult, register_rule from agenteval.models import Case, Turn from agenteval.utils.llm import extract_content_from_llm_response, extract_reply_text, parse_json_from_llm_text @register_rule class LlmScoreRule(EvalRule): """Use an external LLM to score reply quality against criteria.""" name = "llm_score" async def evaluate(self, case: Case, dialog: list[Turn]) -> RuleResult: if not dialog: return RuleResult(passed=False, reason="无回复记录") last_turn = dialog[-1] reply_text = extract_reply_text(last_turn.reply) # 用户问题取自当前轮发送的消息(sent_message),而非上一轮的智能体回复。 # 旧逻辑用 dialog[-2].reply 会把「上一轮 AI 回复」误当成「用户问题」, # 导致多轮/动态用例里评分 LLM 收到牛头不对马嘴的问答对,普遍打 0 分。 question_text = "" if last_turn.sent_message: body = last_turn.sent_message.get("msgBody", "") if isinstance(body, dict): question_text = body.get("content", "") else: try: question_text = json.loads(body).get("content", "") except Exception: question_text = str(body) criteria = self.params.get("criteria", "") min_score = float(self.params.get("min_score", 7)) api_url = self.params.get("api_url") api_key = self.params.get("api_key") model = self.params.get("model", "gpt-4o-mini") if not api_url: return RuleResult(passed=False, reason="LLM 评分规则未配置 api_url") score, reason = await self._call_llm(api_url, api_key, model, question_text, reply_text, criteria) if score is None: return RuleResult(passed=False, reason=f"LLM 评分失败: {reason}") passed = score >= min_score verdict = "通过" if passed else "未通过" detail = f";{reason}" if reason else "" return RuleResult( passed=passed, score=score / 10.0, reason=f"LLM 评分 {score}/10,{verdict} (阈值 {min_score}){detail}", ) async def _call_llm( self, api_url: str, api_key: str | None, model: str, question: str, reply: str, criteria: str, ) -> tuple[float | None, str]: """Call the configured LLM API and parse a numeric score between 0 and 10.""" system_prompt = ( "你是一位严格的智能客服质量评估专家。请根据用户问题和智能体回复," f"按照以下标准打分(0-10分,10分最高):{criteria}\n" '只输出一个 JSON 对象:{"score": number, "reason": "简短说明"}' ) user_prompt = f"用户问题:{question}\n智能体回复:{reply}" headers = {"Content-Type": "application/json"} if api_key: headers["Authorization"] = f"Bearer {api_key}" payload = { "model": model, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], "temperature": 0.2, } try: async with httpx.AsyncClient(timeout=60) as client: resp = await client.post(api_url, headers=headers, json=payload) resp.raise_for_status() content = extract_content_from_llm_response(resp.json()) if not content: return None, "LLM 返回内容为空" parsed = parse_json_from_llm_text(content) score = float(parsed["score"]) reason = parsed.get("reason", "") return max(0.0, min(10.0, score)), reason except Exception as exc: return None, str(exc)