Stufe 2: Antworttyp-Routing — Survey Map-Reduce + Rueckfrage-Regel (D13, M6)

- Planer liefert jetzt type: survey|specific. Survey-Fragen (Uebersicht
  ueber viele Dokumente) laufen ueber Map-Reduce: Retrieval auf
  survey_blocks=16 erweitert, ein Map-Call destilliert JE Block als
  Stichpunkte mit seiner KB-ID (MAP_SYSTEM_PROMPT), ein Reduce-Call
  synthetisiert die Endantwort. Grounding unveraendert: Zitier-Validierung
  strikt ueber die Retrieved-Union; leerer Map-Output -> Fallback auf
  Einzelantwort.
- Systemprompt-Regel 9: haengt die Antwort wesentlich von nicht genanntem
  Kontext ab (Branche, Bundesland, Zeitraum), belegte allgemeine Aussage
  plus EINE Rueckfrage statt Verweigerung (API-first; Odoo-Chat kann die
  Rueckfrage als Follow-up nutzen).
- Ergebnis: q-029 (WIKU-Survey, bisher hartnaeckigste Fehlverweigerung)
  geheilt - Teilantwort mit 5 belegten Heften; q-015 antwortet mit
  expliziter KV-Abhaengigkeit + Rueckfrage statt Branchen-Noise.
- Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 % (Gate
  erfuellt), erwartete Quelle 90,2 %, Latenz mean 34,2 s (Map-Reduce nur
  bei Survey-Fragen, ~95 s). Report lokal data/eval-qwen38-stage2.json.
- Tests 57 -> 59 (Survey-Integration, Typ-Parsing).
This commit is contained in:
2026-09-15 11:16:53 +02:00
parent 062e010d7b
commit 4a9e06f66e
7 changed files with 163 additions and 32 deletions
+44 -4
View File
@@ -47,11 +47,22 @@ Verbindliche Regeln:
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].
laut Kontext nicht; stattdessen gilt …“ — mit Beleg [ID].
Verweigere in diesem Fall nicht.
9. Hängt die Antwort wesentlich von nicht genanntem Kontext ab (z. B.
Branche, Bundesland, Zeitraum), sage dies explizit: gib die belegte
allgemeine Aussage für die im Kontext vertretenen Fälle und frage am
Ende in EINEM kurzen Satz nach dem fehlenden Kontext. Verweigere in
diesem Fall nicht.
Verletze Regel 2 oder Regel 4 niemals — im Zweifel verweigere die Antwort."""
MAP_SYSTEM_PROMPT = """Du destillierst Wissensbasis-Kontextblöcke für eine Folgesynthese.
Erstelle für JEDEN Kontextblock 1-3 prägnante Stichpunkte. Beginne jede
Zusammenfassung mit der Zeile "[<KB-ID>] <Kurzthema>:" — verwende exakt
die KB-ID aus dem Block-Kopf. Behalte konkrete Werte mit ihrem Stand.
Lasse keinen Block aus; keine Einleitung, keine Schlussbemerkung."""
CITE_RE = re.compile(r"\b(?:lb|wk|kv|ris)-[a-z0-9]+-\d+\b")
_THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL)
@@ -157,15 +168,19 @@ def answer_question(
sub_queries = [SubQuery(text=question)]
planned = False
qtype = "specific"
if cfg.planner_enabled:
try:
sub_queries, planned = plan_queries(question, client, cfg)
sub_queries, planned, qtype = plan_queries(question, client, cfg)
sub_queries = sub_queries[: cfg.planner_max_queries] or sub_queries[:1]
except Exception:
sub_queries, planned = [SubQuery(text=question)], False
sub_queries, planned, qtype = [SubQuery(text=question)], False, "specific"
n_entries = top_k
if n_entries is None and qtype == "survey":
n_entries = cfg.survey_blocks
try:
results = retriever.search_multi(sub_queries, n_entries=top_k)
results = retriever.search_multi(sub_queries, n_entries=n_entries)
finally:
if own_retriever:
retriever.close()
@@ -202,6 +217,15 @@ def answer_question(
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": build_user_content(question, results)},
]
map_messages = None
if qtype == "survey":
# Map-Reduce (Stufe 2): alle Bloecke destillieren (Map), dann
# synthetisieren (Reduce). Zitiert werden duerfen weiterhin nur IDs
# aus der Retrieved-Menge — die Post-Validierung bleibt unveraendert.
map_messages = [
{"role": "system", "content": MAP_SYSTEM_PROMPT},
{"role": "user", "content": build_user_content(question, results)},
]
def chat(msgs):
return strip_think(
@@ -215,6 +239,22 @@ def answer_question(
)
)
if map_messages is not None:
summary = chat(map_messages)
if summary:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": (
"Kontextblöcke aus der Wissensbasis "
f"(Block-Zusammenfassungen):\n\n{summary}\n\n"
f"Frage: {question}"
),
},
]
# leerer Map-Output -> Fallback: messages bleibt die Einzelantwort
final = chat(messages)
violations = validate_answer(final, allowed)
regenerations = 0