157 lines
5.8 KiB
Python
157 lines
5.8 KiB
Python
# -*- coding: utf-8 -*-
|
||||
|
|
"""iAOP-Core · LLM 网关 —— 幻觉/事实性校验评测报告脚本(Issue #47)。
|
|||
|
|
|
|||
|
|
对应 PRD 5.4「定期用评测集检验事实一致性」:把评测集 JSON + Prompt 版本库
|
|||
|
|
配置跑一遍 `HallucinationGuard.evaluate()`,产出 Markdown 评测报告
|
|||
|
|
(按 `name@version` 分解,Prompt 变更后可用于对比各版本事实一致性)。
|
|||
|
|
|
|||
|
|
用法示例:
|
|||
|
|
# 使用内置演示评测集,输出报告到文件(utf-8)
|
|||
|
|
python evaluate_hallucination.py --demo --output reports/hallucination.md
|
|||
|
|
|
|||
|
|
# 使用自定义评测集 JSON(见下方 SAMPLE 字段说明)
|
|||
|
|
python evaluate_hallucination.py --samples eval_set.json --output report.md
|
|||
|
|
|
|||
|
|
# 不指定 --output:报告打印到 stdout
|
|||
|
|
python evaluate_hallucination.py --demo
|
|||
|
|
|
|||
|
|
评测集 JSON 格式(顶层为数组):
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
"answer": "建议降温。[来源: 沸腾氯化炉异常处置SOP]",
|
|||
|
|
"sources": ["沸腾氯化炉异常处置SOP"],
|
|||
|
|
"confidence": 0.9,
|
|||
|
|
"high_stakes": true,
|
|||
|
|
"prompt_name": "alarm_explain", // 可选,与版本库联动
|
|||
|
|
"prompt_version": "1.0.0" // 可选,缺省取当前默认版本
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
测试:`python -m unittest discover -s tests -v`(在 core/llm-gateway 目录下执行)。
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import argparse
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import types
|
|||
|
|
from typing import List
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# 包挂载(目录名含连字符,无法直接以包名 import;与 tests/_bootstrap.py 同款)
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
_LLM_GW_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||
|
|
sys.path.insert(0, _LLM_GW_DIR)
|
|||
|
|
if "llm_gateway" not in sys.modules:
|
|||
|
|
_pkg = types.ModuleType("llm_gateway")
|
|||
|
|
_pkg.__path__ = [_LLM_GW_DIR]
|
|||
|
|
sys.modules["llm_gateway"] = _pkg
|
|||
|
|
|
|||
|
|
from llm_gateway.hallucination import HallucinationGuard # noqa: E402
|
|||
|
|
from llm_gateway.prompts import PromptRegistry # noqa: E402
|
|||
|
|
|
|||
|
|
# 内置演示评测集(无 --samples 时使用,覆盖 pass / unsupported / human_review)
|
|||
|
|
DEMO_SAMPLES: List[dict] = [
|
|||
|
|
{
|
|||
|
|
"answer": "建议降低氯化炉温度并观察。[来源: 沸腾氯化炉异常处置SOP]",
|
|||
|
|
"sources": ["沸腾氯化炉异常处置SOP"],
|
|||
|
|
"confidence": 0.95,
|
|||
|
|
"high_stakes": True,
|
|||
|
|
"prompt_name": "alarm_explain",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"answer": "应立即停机检修。[来源: 不存在的工艺文档]",
|
|||
|
|
"sources": ["沸腾氯化炉异常处置SOP"],
|
|||
|
|
"confidence": 0.9,
|
|||
|
|
"high_stakes": True,
|
|||
|
|
"prompt_name": "alarm_explain",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"answer": "班次生产概况:炉温稳定。[来源: 交接班规范]",
|
|||
|
|
"sources": ["交接班规范"],
|
|||
|
|
"confidence": 0.6,
|
|||
|
|
"high_stakes": False,
|
|||
|
|
"prompt_name": "shift_handover",
|
|||
|
|
"prompt_version": "1.0.1",
|
|||
|
|
},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_samples(path: str) -> List[dict]:
|
|||
|
|
"""从 JSON 文件加载评测集(顶层为样本数组)。"""
|
|||
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|||
|
|
data = json.load(fh)
|
|||
|
|
if not isinstance(data, list):
|
|||
|
|
raise ValueError(f"评测集文件 {path} 顶层必须是样本数组")
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main(argv: List[str]) -> int:
|
|||
|
|
parser = argparse.ArgumentParser(
|
|||
|
|
prog="evaluate_hallucination",
|
|||
|
|
description="幻觉/事实性校验评测报告(PRD 5.4 定期评测)",
|
|||
|
|
)
|
|||
|
|
parser.add_argument(
|
|||
|
|
"--prompts-config", default="config/prompts.template.yaml",
|
|||
|
|
help="Prompt 版本库模板资产路径(默认 config/prompts.template.yaml)",
|
|||
|
|
)
|
|||
|
|
parser.add_argument(
|
|||
|
|
"--samples", default=None,
|
|||
|
|
help="评测集 JSON 文件路径(与 --demo 二选一)",
|
|||
|
|
)
|
|||
|
|
parser.add_argument(
|
|||
|
|
"--demo", action="store_true",
|
|||
|
|
help="使用内置演示评测集(未指定 --samples 时默认开启)",
|
|||
|
|
)
|
|||
|
|
parser.add_argument(
|
|||
|
|
"--threshold", type=float, default=0.8,
|
|||
|
|
help="高利害信度阈值(默认 0.8)",
|
|||
|
|
)
|
|||
|
|
parser.add_argument(
|
|||
|
|
"--output", default=None,
|
|||
|
|
help="报告输出文件路径(utf-8);缺省打印到 stdout",
|
|||
|
|
)
|
|||
|
|
args = parser.parse_args(argv)
|
|||
|
|
|
|||
|
|
# 评测集:--samples 优先,否则内置演示集
|
|||
|
|
if args.samples:
|
|||
|
|
samples = load_samples(args.samples)
|
|||
|
|
else:
|
|||
|
|
samples = DEMO_SAMPLES
|
|||
|
|
print("[evaluate_hallucination] 未指定 --samples,使用内置演示评测集",
|
|||
|
|
file=sys.stderr)
|
|||
|
|
|
|||
|
|
# Prompt 版本库(与评测联动:按 name@version 分解)
|
|||
|
|
registry = PromptRegistry.from_template_config(args.prompts_config)
|
|||
|
|
|
|||
|
|
guard = HallucinationGuard(default_threshold=args.threshold)
|
|||
|
|
report = guard.evaluate(samples, registry=registry)
|
|||
|
|
text = guard.render_evaluation_report(report)
|
|||
|
|
|
|||
|
|
if args.output:
|
|||
|
|
os.makedirs(os.path.dirname(os.path.abspath(args.output)) or ".", exist_ok=True)
|
|||
|
|
with open(args.output, "w", encoding="utf-8") as fh:
|
|||
|
|
fh.write(text)
|
|||
|
|
print(f"[evaluate_hallucination] 评测报告已写入:{args.output}", file=sys.stderr)
|
|||
|
|
else:
|
|||
|
|
# Windows 控制台可能为 GBK:显式用 utf-8 输出避免编码错误
|
|||
|
|
try:
|
|||
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|||
|
|
except (AttributeError, ValueError): # pragma: no cover - 旧版解释器
|
|||
|
|
pass
|
|||
|
|
print(text)
|
|||
|
|
|
|||
|
|
# 简报(stderr,便于定时任务抓取结论)
|
|||
|
|
print(
|
|||
|
|
"[evaluate_hallucination] 样本 {total}|支持率 {sr:.1%}|人工复核率 {hr:.1%}"
|
|||
|
|
.format(total=report["total"], sr=float(report["supported_rate"]),
|
|||
|
|
hr=float(report["human_review_rate"])),
|
|||
|
|
file=sys.stderr,
|
|||
|
|
)
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
sys.exit(main(sys.argv[1:]))
|