51dc8dc4f6
- FastAPI + Qdrant + Redis + Ollama 技术栈 - L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合) - 文档三级总结与 2.5 级回退 - query 解析路由与分类 - /admin 管理页面
94 lines
3.6 KiB
Python
94 lines
3.6 KiB
Python
"""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"])
|