"""LLM-as-judge 评测指标:基于 Ollama 小模型的幻觉率与类目一致性判定 所有判定均要求模型以 JSON 输出(json_mode),解析失败时保守处理并记录日志: - hallucination_rate 解析失败返回 0.0(不低报问题,避免阻塞评测主流程) - taxonomy_consistency 解析失败返回 True(不冤枉摘要) """ import json import re import structlog from app.services.ollama import OllamaClient logger = structlog.get_logger() # 每次判定的断言抽取上限,避免长摘要导致判定过慢 _MAX_CLAIMS = 5 _HALLUCINATION_PROMPT = """你是一个事实核查员。请从下面的【摘要】中抽取最多 {max_claims} 条事实性断言, 并逐条判断【原文】是否支持该断言(断言中的数字、名称、条款等关键信息必须与原文一致才算支持)。 【原文】 {source} 【摘要】 {summary} 只输出 JSON,格式:{{"assertions": [{{"claim": "断言内容", "supported": true 或 false}}]}}""" _TAXONOMY_PROMPT = """你是一个文档分类审核员。请判断下面的【摘要】所描述的文档是否适合归入类目【{category}】。 【摘要】 {summary} 只输出 JSON,格式:{{"consistent": true 或 false}}""" def _extract_json(text: str) -> dict | None: """从模型输出提取首个 JSON 对象(容忍前后噪声),失败返回 None""" match = re.search(r"\{.*\}", text, re.DOTALL) if not match: return None try: data = json.loads(match.group()) except json.JSONDecodeError: return None return data if isinstance(data, dict) else None async def hallucination_rate(summary: str, source_text: str, ollama: OllamaClient) -> float: """幻觉率:摘要中不被原文支持的断言占比 流程:模型抽取断言(最多 _MAX_CLAIMS 条)→ 逐条判原文是否支持 → 返回不支持比例。 调用或解析失败保守返回 0.0 并记录日志。 """ prompt = _HALLUCINATION_PROMPT.format(max_claims=_MAX_CLAIMS, source=source_text, summary=summary) try: raw = await ollama.generate(prompt, json_mode=True) except Exception as exc: logger.warning("幻觉率判定调用失败,保守返回 0.0", error=str(exc)) return 0.0 data = _extract_json(raw) if data is None: logger.warning("幻觉率判定输出解析失败,保守返回 0.0", raw=raw[:200]) return 0.0 claims = data.get("assertions") if not isinstance(claims, list): logger.warning("幻觉率判定输出缺少 assertions 字段,保守返回 0.0", raw=raw[:200]) return 0.0 supported_flags = [bool(c["supported"]) for c in claims[:_MAX_CLAIMS] if isinstance(c, dict) and "supported" in c] if not supported_flags: return 0.0 unsupported = sum(1 for supported in supported_flags if not supported) return unsupported / len(supported_flags) async def taxonomy_consistency(summary: str, category: str, ollama: OllamaClient) -> bool: """类目一致性:摘要是否支持归入指定类目;调用或解析失败保守返回 True""" prompt = _TAXONOMY_PROMPT.format(category=category, summary=summary) try: raw = await ollama.generate(prompt, json_mode=True) except Exception as exc: logger.warning("类目一致性判定调用失败,保守返回 True", error=str(exc)) return True data = _extract_json(raw) if data is None or "consistent" not in data: logger.warning("类目一致性判定输出解析失败,保守返回 True", raw=raw[:200]) return True return bool(data["consistent"])