feat: 完成 issue #6 LLM 网关 + RAG 模板化(混合网关主编排)

This commit is contained in:
2026-08-04 18:02:00 +08:00
parent fa5523a37d
commit 5d937e8efd
12 changed files with 1707 additions and 20 deletions
@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
"""幻觉/事实性校验(hallucination)单元测试:引用溯源 / 信度阈值 / 评测。"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import _bootstrap # noqa: F401
from llm_gateway.hallucination import HallucinationGuard # noqa: E402
class CitationCheckTest(unittest.TestCase):
def setUp(self):
self.guard = HallucinationGuard(default_threshold=0.8)
def test_declared_sources_are_verified(self):
verdict = self.guard.check(
answer="炉温偏高应降温。[来源: 沸腾氯化炉异常处置SOP]",
sources=["沸腾氯化炉异常处置SOP", "交接班规范"],
)
self.assertTrue(verdict.supported)
self.assertEqual(verdict.action, "pass")
def test_missing_source_is_unsupported(self):
verdict = self.guard.check(
answer="应停机。[来源: 不存在的文档]",
sources=["沸腾氯化炉异常处置SOP"],
)
self.assertFalse(verdict.supported)
self.assertEqual(verdict.action, "unsupported")
self.assertIn("不存在的文档", verdict.missing_sources)
def test_no_citation_is_supported(self):
# 无引用声明 = 不判幻觉(引用为强制项由 RAG 模板保证)
verdict = self.guard.check(answer="按操作规程执行。", sources=[])
self.assertTrue(verdict.supported)
self.assertEqual(verdict.action, "pass")
class ConfidenceThresholdTest(unittest.TestCase):
def setUp(self):
self.guard = HallucinationGuard(default_threshold=0.8)
def test_low_confidence_high_stakes_human_review(self):
verdict = self.guard.check(
answer="建议立即停机。[来源: SOP]",
sources=["SOP"], confidence=0.55, high_stakes=True,
)
self.assertEqual(verdict.action, "human_review")
def test_high_confidence_high_stakes_passes(self):
verdict = self.guard.check(
answer="建议观察并记录。[来源: SOP]",
sources=["SOP"], confidence=0.95, high_stakes=True,
)
self.assertEqual(verdict.action, "pass")
def test_low_confidence_non_stakes_passes(self):
# 非高利害场景不启用阈值
verdict = self.guard.check(
answer="一般说明。[来源: 手册]", sources=["手册"],
confidence=0.3, high_stakes=False,
)
self.assertEqual(verdict.action, "pass")
def test_custom_threshold(self):
verdict = self.guard.check(
answer="处置建议。[来源: 手册]", sources=["手册"],
confidence=0.7, high_stakes=True, threshold=0.6,
)
self.assertEqual(verdict.action, "pass")
class EvaluateTest(unittest.TestCase):
def setUp(self):
self.guard = HallucinationGuard()
def test_evaluate_rates(self):
samples = [
{"answer": "a[来源: X]", "sources": ["X"], "confidence": 0.9, "high_stakes": True},
{"answer": "b[来源: Y]", "sources": ["Z"], "confidence": 0.9},
{"answer": "c[来源: X]", "sources": ["X"], "confidence": 0.4, "high_stakes": True},
]
report = self.guard.evaluate(samples)
self.assertEqual(report["total"], 3)
# 支持 2 条(a/c),human_review 1 条(c)
self.assertEqual(report["supported_rate"], round(2 / 3, 4))
self.assertEqual(report["human_review_rate"], round(1 / 3, 4))
def test_empty_samples(self):
report = self.guard.evaluate([])
self.assertEqual(report["total"], 0)
def test_audit_records(self):
self.guard.check("x[来源: A]", sources=["A"])
records = self.guard.drain_audit()
self.assertEqual(len(records), 1)
self.assertEqual(records[0]["action"], "pass")
if __name__ == "__main__":
unittest.main()