Initial commit: QMDSearch 分层信息检索服务

- FastAPI + Qdrant + Redis + Ollama 技术栈
- L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合)
- 文档三级总结与 2.5 级回退
- query 解析路由与分类
- /admin 管理页面
This commit is contained in:
2026-07-29 21:24:40 +08:00
commit 51dc8dc4f6
83 changed files with 10794 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
"""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"])