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:
+9
-3
@@ -79,9 +79,15 @@ MRR 0,690 — M1-Ziel >0,9 erreicht (BM25-only war 0,855; die vier
|
||||
BM25-Fehltreffer behebt die Dense-Suche alle).
|
||||
|
||||
**Antworten** (Bake-off-Sieger qwen3.8:27b, Thinking aus, Temperatur 0,1):
|
||||
**Zitier-Präzision 100 %** (4 Zitierverletzungen wurden von der
|
||||
Post-Validierung abgefangen und regeneriert) · Verweigerung korrekt
|
||||
94,3 % · erwartete Quelle zitiert 80,6 % · Latenz mean 32 s / p95 53 s.
|
||||
**Zitier-Präzision 100 %** · Verweigerung korrekt 94,3 % · erwartete Quelle
|
||||
zitiert **83,9 %** · Latenz mean 34 s / p95 54 s.
|
||||
|
||||
**Prompt v2 + Kontext-Section-Priorität (2026-09-14):** Teilantworten bei
|
||||
unvollständiger Deckung erlaubt (Regel 4), Prämisse-Korrektur statt
|
||||
Verweigerung (Regel 8), pro Eintrag beste Inhaltssektion als Kontextblock
|
||||
(Zusammenfassung > Kernwerte > … > Verweise zuletzt — Navigations-Chunks
|
||||
lösen keine Fehlverweigerungen mehr aus). v1→v2: q-008 + q-031 behoben,
|
||||
erwartete Quelle 80,6 % → 83,9 %, Zitier-Präzision unverändert 100 %.
|
||||
|
||||
**Modell-Bake-off (M3, 2026-09-14)** — Entscheidung: **qwen3.8:27b**
|
||||
(Protokoll: Zitier-Präzision → Verweigerungskorrektheit → Latenz):
|
||||
|
||||
@@ -99,6 +99,10 @@ questions:
|
||||
- id: q-031
|
||||
question: "Was regelt das Mindestlohngesetz und für wen gilt es?"
|
||||
expected_ids: [lb-ent-09]
|
||||
note: "Falsche Prämisse — ideale Antwort korrigiert: kein gesetzlich
|
||||
betraglich festgelegtes Mindestentgelt; Untergrenze via KV/Satzung/
|
||||
Mindestlohntarif (BEA) belegt aus lb-ent-09."
|
||||
tags: [premise-correction]
|
||||
|
||||
# --- Verweigerungsfälle (Antwortmodus) ---
|
||||
- id: r-001
|
||||
|
||||
+10
-1
@@ -33,12 +33,21 @@ Verbindliche Regeln:
|
||||
IDs, die nur im Fließtext als Verweis genannt werden, sind Querverweise
|
||||
und KEINE Belege.
|
||||
3. Gib jeden Wert mit seinem Stand an, z. B. „28,5 % (Stand 2026-01)“.
|
||||
4. Beantworten die Kontextblöcke die Frage nicht, antworte exakt:
|
||||
4. Beantworte die Frage mit den fachlichen Aussagen der thematisch
|
||||
relevanten Blöcke — auch wenn sie die Frage nur teilweise decken;
|
||||
mache klar, welcher Aspekt belegt ist. Verweigere nur, wenn KEIN
|
||||
Block thematisch zur Frage passt, mit exakt:
|
||||
„Dazu enthält die Wissensbasis keine Aussage.“ — und schlage nichts vor.
|
||||
5. Widersprechen sich Kontextblöcke, nenne beide Werte mit ihren IDs und
|
||||
kennzeichne den Widerspruch mit ⚠. Löse Widersprüche niemals stillschweigend auf.
|
||||
6. Nenne Paragraphen und Gesetze nur, wenn ein Kontextblock sie nennt.
|
||||
7. Antworte auf Deutsch und prägnant (Stichpunkte, wo sinnvoll).
|
||||
8. Baut die Frage auf einer falschen Annahme auf (z. B. ein nicht
|
||||
existierendes Gesetz), korrigiere die Annahme anhand der Blöcke und
|
||||
gib die zutreffende, belegte Aussage. Muster: Auf „Was regelt das
|
||||
Mindestlohngesetz?“ antworte sinngemäß „Ein Mindestlohngesetz existiert
|
||||
laut Kontext nicht; stattdätzlich gilt …“ — mit Beleg [ID].
|
||||
Verweigere in diesem Fall nicht.
|
||||
|
||||
Verletze Regel 2 oder Regel 4 niemals — im Zweifel verweigere die Antwort."""
|
||||
|
||||
|
||||
+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