Prompt v2 + Kontext-Section-Prioritaet: Fehlverweigerungen behoben (D8)
Diagnose: q-008 bekam die 'Verweise'-Navigationssektion von lb-end-03 als Kontext (BM25-Langennormalisierung bevorzugt duenne Chunks) -> inhaltlose Bloecke -> korrekte Verweigerung nach Regel 4. q-031 enthaelt eine falsche Praemisse (kein 'Mindestlohngesetz' in Oesterreich; Wissensbasis sagt: kein gesetzlich betraglich festgelegtes Mindestentgelt). Fixes: (a) retrieve.py _representative_chunk - pro Eintrag beste Inhalts- sektion (Zusammenfassung > Kernwerte > Rechtsgrundlagen > Payroll > sonstige > Verweise), Section-Swap aus dem Index falls nur 'Verweise' rangiert; (b) generate.py Prompt v2 - Regel 4 erlaubt Teilantworten, Regel 8 verlangt Prämisse-Korrektur mit Muster-Beispiel. Bestaetigungslauf qwen3.8:27b (35 Fragen): Zitier-Praezision 100 % (8 Regenerierungen, alle geheilt), Verweigerung korrekt 94,3 %, erwartete Quelle 83,9 % (v1 80,6 %), mean 34 s. q-008 + q-031 behoben; verbleibender bekannter Fall q-029 (breite Survey-Frage, sicherer Fehlermodus). Retrieval-Metriken unveraendert (Recall@8 0,952). 41 Tests gruen.
This commit is contained in:
+53
-5
@@ -34,6 +34,29 @@ class ChunkResult:
|
||||
source: str = "fused" # bm25 | dense | fused | cross_ref
|
||||
|
||||
|
||||
def _section_priority(section: str) -> int:
|
||||
"""Kontext-Sektionen priorisieren: Inhalt vor Navigation.
|
||||
|
||||
„Verweise“-Sektionen sind Navigationslisten (KB-IDs) — sie tragen
|
||||
Retrieval-Signal (Stichworte), sind aber als Kontextblock wertlos und
|
||||
provozieren Fehlverweigerungen. BM25-Längennormalisierung rangiert sie
|
||||
bevorzugt, daher wird pro Eintrag bewusst die beste Inhaltssektion
|
||||
gewählt (Fix 2026-09-14, q-008).
|
||||
"""
|
||||
s = (section or "").casefold()
|
||||
if s.startswith("zusammenfassung"):
|
||||
return 0
|
||||
if s.startswith("kernwerte"):
|
||||
return 1
|
||||
if s.startswith("rechtsgrundlagen"):
|
||||
return 2
|
||||
if s.startswith("payroll"):
|
||||
return 3
|
||||
if s.startswith("verweise"):
|
||||
return 5
|
||||
return 4
|
||||
|
||||
|
||||
class Retriever:
|
||||
def __init__(self, cfg: Config, db_path: str | None = None, client=None):
|
||||
self.cfg = cfg
|
||||
@@ -182,6 +205,28 @@ class Retriever:
|
||||
source=source,
|
||||
)
|
||||
|
||||
def _representative_chunk(
|
||||
self, entry_id: str, ranked: list[ChunkResult]
|
||||
) -> ChunkResult:
|
||||
"""Beste Inhaltssektion des Eintrags als Kontextblock.
|
||||
|
||||
Bevorzugt die rangierte (gefundene) Sektion mit bester Priorität;
|
||||
traf der Eintrag nur über „Verweise“, wird seine beste Inhalts-
|
||||
sektion aus dem Index nachgeladen (source="section-swap").
|
||||
"""
|
||||
content = [c for c in ranked if _section_priority(c.section) < 5]
|
||||
if content:
|
||||
return min(content, key=lambda c: (_section_priority(c.section), -c.score))
|
||||
rows = self._con.execute(
|
||||
"SELECT * FROM chunks WHERE entry_id = ? AND section NOT LIKE 'Verweise%' "
|
||||
"ORDER BY CASE WHEN section LIKE 'Zusammenfassung%' THEN 0 "
|
||||
"WHEN section LIKE 'Kernwerte%' THEN 1 ELSE 2 END, chunk_id LIMIT 1",
|
||||
(entry_id,),
|
||||
).fetchall()
|
||||
if rows:
|
||||
return self._row_to_result(rows[0], 0.0, "section-swap")
|
||||
return ranked[0] # Eintrag hat nur Verweise-Sektionen
|
||||
|
||||
def _best_chunk_of_entry(self, entry_id: str) -> ChunkResult | None:
|
||||
rows = self._con.execute(
|
||||
"SELECT * FROM chunks WHERE entry_id = ? "
|
||||
@@ -224,16 +269,19 @@ class Retriever:
|
||||
results.append(self._row_to_result(rows[cid], score, source))
|
||||
results.sort(key=lambda r: -r.score)
|
||||
|
||||
# Bester Chunk je Eintrag -> Kontext (Entry-Level-Dedup)
|
||||
# Bester Chunk je Eintrag -> Kontext (Entry-Level-Dedup).
|
||||
# Der Vertreter-Chunk ist die beste Inhaltssektion des Eintrags,
|
||||
# nicht die Rangfolge-Beste (vgl. _section_priority).
|
||||
main: list[ChunkResult] = []
|
||||
seen: set[str] = set()
|
||||
by_entry: dict[str, list[ChunkResult]] = {}
|
||||
for r in results:
|
||||
if r.entry_id in seen:
|
||||
continue
|
||||
seen.add(r.entry_id)
|
||||
main.append(r)
|
||||
by_entry.setdefault(r.entry_id, []).append(r)
|
||||
for entry_id, chunks in by_entry.items():
|
||||
if len(main) >= n:
|
||||
break
|
||||
seen.add(entry_id)
|
||||
main.append(self._representative_chunk(entry_id, chunks))
|
||||
|
||||
# cross_ref-Erweiterung (kontrolliert, markiert, begrenzt)
|
||||
extra: list[ChunkResult] = []
|
||||
|
||||
Reference in New Issue
Block a user