fix(r4-28v4): chat 候选腐烂根治——Qdrant 条目 paper_id 派生 DOI/PMCID 回填引用

根因(TL 实测 + 只读探针确认):
175 归一化库 Neo4j ASSERTS 关系 13040841 条全无 evidence 属性
(_ONE_HOP Cypher 也不返回 evidence);而 175 无 paper metadata registry,
Qdrant payload 无 pmid/pmcid/doi 键 -> chat 模式 public_citation 过滤
把有正文的 qdrant 条目(120 条)全滤掉,只留无 evidence 的 neo4j 图断言
(136 条)-> BGE 复刻 _document 模板输入 evidence 空 -> logit=-3.07,
sigmoid=0.044 与审计 bge 0.020-0.047 完全吻合 -> 0.25 final 门全拒
sources=[]。176 正常是因为配了 registry 回填 pmid。
数字链:dedup 244 -> public_citation kept 136(全图断言)-> pre_rerank 160
-> cluster 136 代表 -> rerank 全垃圾分。

修复:item_from_payload 引用字段回填优先级 payload 显式 > registry >
paper_id 派生(doi_from_paper_id/pmcid_from_paper_id + doi: 前缀形态)。
r4 v1/v2/v3 的阈值语义不变;search 免 rerank 路径不受影响。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 17:17:25 +08:00
co-authored by Claude Fable 5
parent 0252d52425
commit d54419f040
2 changed files with 54 additions and 2 deletions
@@ -3,6 +3,7 @@ from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
import json
import os
import re
from pathlib import Path
from typing import Any, Dict, Iterable, List, Tuple
@@ -16,6 +17,7 @@ from src.rag.normalized_release import (
)
from .schemas import EvidenceItem, RetrievalHypothesis
from .soft_gate_policy import doi_from_paper_id, pmcid_from_paper_id
ALLOWED_QDRANT_COLLECTIONS = {
"plant_srt_direct_v1_bge_m3_20260702",
@@ -36,6 +38,12 @@ def _norm(value: Any) -> str:
return " ".join(text.split())
# doi: 前缀形态 paper_id 的 DOI 全文校验(回改 v4,2026-09-11)
_DOI_LITERAL = re.compile(
r"10\.\d{4,9}/[A-Za-z0-9][A-Za-z0-9._;()/:+\-]*"
)
def load_paper_metadata_registry(path: Path) -> Dict[str, Dict[str, Any]]:
records: Dict[str, Dict[str, Any]] = {}
if not path.is_file():
@@ -342,6 +350,20 @@ class DirectFlowRetriever:
metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
paper_id = _text(payload.get("paper_id") or payload.get("best_paper_id"))
registry_metadata = getattr(self, "paper_metadata", {}).get(paper_id, {})
# 缺陷 #28 回改 v4(2026-09-11):175 无 paper metadata registry,
# Qdrant payload 又无 pmid/pmcid/doi 键——chat 的 public_citation
# 过滤会把有正文的 qdrant 条目全滤掉,只剩无 evidence 的 neo4j
# 归一化图断言(bge 全 0.02-0.047),final 门 0.25 全拒 sources=[]。
# 从 paper_id 派生 DOI/PMCID 回填引用字段(payload 显式字段 >
# registry > paper_id 派生)。
_derived_doi = doi_from_paper_id(paper_id)
_derived_pmcid = pmcid_from_paper_id(paper_id)
# doi: 前缀形态(如 doi:10.1002/ps.6526)直接剥前缀(DOI 已带斜杠,
# doi_from_paper_id 不覆盖此形态)。
if not _derived_doi and paper_id.lower().startswith("doi:"):
_candidate = paper_id[4:].strip()
if _DOI_LITERAL.fullmatch(_candidate):
_derived_doi = _candidate
return EvidenceItem(
evidence_id="",
kb="V16/V15",
@@ -352,8 +374,8 @@ class DirectFlowRetriever:
paper_id=paper_id,
title=_text(payload.get("title") or payload.get("paper_title") or metadata.get("title") or registry_metadata.get("title")),
pmid=_text(payload.get("pmid") or payload.get("pubmed_id") or metadata.get("pmid") or registry_metadata.get("pmid")),
pmcid=_text(payload.get("pmcid") or metadata.get("pmcid") or registry_metadata.get("pmcid")),
doi=_text(payload.get("doi") or metadata.get("doi") or registry_metadata.get("doi")),
pmcid=_text(payload.get("pmcid") or metadata.get("pmcid") or registry_metadata.get("pmcid") or _derived_pmcid),
doi=_text(payload.get("doi") or metadata.get("doi") or registry_metadata.get("doi") or _derived_doi),
species=_text(payload.get("species") or metadata.get("species")),
section=_text(payload.get("section") or metadata.get("section")),
output_layer=_text(payload.get("output_layer") or "main_kg"),
@@ -294,6 +294,36 @@ def test_direct_flow_retriever_threshold_passthrough():
assert r.qdrant.calls[-1]["score_threshold"] is None
def test_item_from_payload_derives_citation_from_paper_id():
"""回改 v4 根因单测:Qdrant payload 无 pmid/pmcid/doi 时从 paper_id
派生 DOI——chat 模式 public_citation 过滤不得把有正文的 qdrant 条目
全滤掉(此前 rerank 输入全是无 evidence 的 neo4j 图断言,bge 全
0.02-0.047,final 门 0.25 全拒 sources=[])。"""
import rag.qa_v1_1_73.direct_flow_retriever as dfr_mod
r = dfr_mod.DirectFlowRetriever.__new__(dfr_mod.DirectFlowRetriever)
r.paper_metadata = {}
# 内部 DOI 形态
it = r.item_from_payload(
{"paper_id": "10.3390_f12050650", "source": "s", "relation": "r",
"target": "t", "evidence": "e"},
"qdrant_vector", "q",
)
assert it.doi == "10.3390/f12050650"
assert it.retrieval_methods == ["qdrant_vector"]
# doi: 前缀形态
it2 = r.item_from_payload(
{"paper_id": "doi:10.1002/ps.6526", "source": "s", "relation": "r",
"target": "t", "evidence": "e"},
"qdrant_vector", "q",
)
assert it2.doi == "10.1002/ps.6526"
# 派生出的 DOI 能通过 public_citation 校验(过滤不再误杀)
from rag.qa_v1_1_73.citation_contract import public_citation
cite = public_citation(evidence_id="", pmid="", pmcid="", doi=it.doi)
assert cite == "[DOI: 10.3390/f12050650]"
def test_env_overrides(monkeypatch):
monkeypatch.setenv("QA_MIN_VECTOR_SCORE", "0.5")
assert score_gate._env_value("QA_MIN_VECTOR_SCORE", 0.45) == 0.5