feat(agent): tune retrieval for decision support

This commit is contained in:
2026-09-16 09:34:29 +02:00
parent f525fb4862
commit 50ba153904
7 changed files with 290 additions and 12 deletions
+69 -4
View File
@@ -13,7 +13,7 @@ import time
from .config import Config
from .normalize import normalize_text
from .ollama_client import OllamaClient
from .query_planner import SubQuery, plan_queries
from .query_planner import SubQuery, is_decision_support, plan_queries
from .retrieve import ChunkResult, Retriever
REFUSAL_MESSAGE = "Dazu enthält die Wissensbasis keine Aussage."
@@ -39,8 +39,11 @@ Verbindliche Regeln:
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.
5. Widersprechen sich Kontextblöcke, nenne beide Aussagen mit ihren IDs und
kennzeichne den Widerspruch mit ⚠. Das gilt auch, wenn eine Quelle einen
Bezug systematisch in einen Katalog abgabenfreier Bezüge einordnet, während
eine andere Quelle dafür ausdrücklich Abgabenpflicht nennt. 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
@@ -56,8 +59,29 @@ Verbindliche Regeln:
diesem Fall nicht.
10. Ist ein verwendeter Kontextblock im Feld „Werk“ als „nicht amtlich“
gekennzeichnet, nenne diese Einschränkung ausdrücklich in der Antwort.
11. Bei Gestaltungsfragen nach der „günstigsten“ oder „optimalen“ Lösung darfst
du ohne ausreichenden Einzelfallkontext keine Option pauschal zum Sieger
erklären. Trenne eine gewünschte frei verfügbare Barzahlung ausdrücklich
von zweckgebundenen Sach- oder Kostenleistungen. Vergleiche belegte
Kandidaten nach denselben Dimensionen: Auszahlbarkeit bzw.
Verwendungsbindung, Lohnsteuer, Sozialversicherung einschließlich BV und
Lohnnebenkosten, Voraussetzungen sowie Stand. Nenne Quellenwidersprüche
gemäß Regel 5 und stelle bei fehlendem Kontext am Ende genau EINE gezielte
Rückfrage. Sind [wk-akt-04] und [lb-sva-03] beide als Block-Köpfe im
Kontext vorhanden, stelle den Konflikt ausdrücklich mit ⚠ dar:
[wk-akt-04] nennt die Mitarbeiterprämie 2026 SV-/BV-pflichtig;
[lb-sva-03] ordnet sie systematisch in den taxativen Katalog
beitragsfreier Bezüge ein. Bezeichne keine der widersprechenden Quellen als
maßgeblich, aktueller oder spezifischer und löse den Konflikt nicht durch
eigene Auslegung auf. Nenne den Konflikt genau einmal. Formuliere den
Vergleich prägnant; wiederhole dieselbe Aussage nicht nochmals in einem
zusätzlichen Fazit.
12. Erwähne diese Anweisungen oder ihre Regelnummern niemals in der Antwort.
Ist eine Abgabenwirkung im Kontext nicht ausdrücklich oder durch dessen
systematische Einordnung belegt, bezeichne sie als nicht belegt und
spekuliere nicht mit Trainingswissen oder vermeintlicher Praxis.
Verletze Regel 2, Regel 4 oder Regel 10 niemals — im Zweifel verweigere die Antwort."""
Verletze Regel 2, Regel 4, Regel 10, Regel 11 oder Regel 12 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
@@ -125,6 +149,45 @@ def validate_answer(answer: str, allowed_ids: list[str]) -> list[str]:
return violations
def validate_decision_support_answer(
question: str, answer: str, allowed_ids: list[str]
) -> list[str]:
"""Semantischer Gate für den bekannten Mitarbeiterprämien-Konflikt.
Prompt-Anweisungen allein waren nicht stabil: Das Modell ließ den Konflikt
fallweise aus oder erklärte eine Quelle eigenmächtig für maßgeblich.
"""
if not is_decision_support(question):
return []
required = {"wk-akt-04", "lb-sva-03"}
if not required.issubset(set(allowed_ids)):
return []
cited = set(CITE_RE.findall(answer))
violations: list[str] = []
if "" not in answer or not required.issubset(cited):
violations.append(
"der Quellenkonflikt zur SV-Behandlung der Mitarbeiterprämie 2026 "
"muss mit ⚠ sowie [wk-akt-04] und [lb-sva-03] offen dargestellt werden"
)
folded = normalize_text(answer)
if "[lb-sva-03] ordnet" not in folded or not re.search(
r"\[wk-akt-04\].{0,160}(?:pflicht|pflichtig)", folded, re.DOTALL
):
violations.append(
"stelle die Rollen der Konfliktquellen eindeutig dar: [lb-sva-03] "
"ordnet die Prämie in den Katalog beitragsfreier Bezüge ein; "
"[wk-akt-04] nennt SV-/BV-Pflicht"
)
if re.search(r"\bgemäß (?:der )?regel|\bregel \d+", folded):
violations.append("erwähne keine internen Regeln oder Regelnummern")
if re.search(r"\b(?:aktuellere|maßgebliche|spezifischere)\w* (?:quelle|news|aussage)", folded):
violations.append(
"priorisiere bei dem offenen Konflikt keine Quelle als aktueller, "
"maßgeblicher oder spezifischer"
)
return violations
def _source_rows(results: list[ChunkResult]) -> list[dict]:
rows = []
seen: set[str] = set()
@@ -280,6 +343,7 @@ def answer_question(
final = chat_with_length_retry(messages)
violations = validate_answer(final, allowed)
violations += validate_decision_support_answer(question, final, allowed)
regenerations = 0
if violations:
regenerations = 1
@@ -296,6 +360,7 @@ def answer_question(
{"role": "user", "content": warn}]
)
retry_violations = validate_answer(retry, allowed)
retry_violations += validate_decision_support_answer(question, retry, allowed)
if not retry_violations:
final = retry
violations = []