mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 16:56:42 +00:00
fix(agent): stabilize decision support grounding
This commit is contained in:
@@ -5,11 +5,10 @@ Rollender Übergabe-Log für agent-Threads. Workflow: `.agents/SKILL.md`
|
|||||||
|
|
||||||
## Current focus
|
## Current focus
|
||||||
|
|
||||||
Retrieval-/Antwort-Tuning für komplexe Gestaltungsfragen läuft (D16).
|
Decision-Support-Tuning (D16) ist stabilisiert und im 53-Fragen-
|
||||||
Goldset q-124–126 und deterministischer Decision-Support-Pfad sind umgesetzt;
|
Bestätigungslauf zitierseitig validiert. Verbleibend ist die bekannte
|
||||||
offen ist der vollständige 53-Fragen-Antwortmodus-Eval. Danach folgen die
|
stochastische Survey-Fehlverweigerung q-029; danach folgen Rechtsprechungs-
|
||||||
bewusste Publikations-/Lizenzfreigabe der quellentreuen Rechtsprechungs-
|
Publikations-/Lizenzfreigabe und M4 (API-first Odoo-Integration inkl.
|
||||||
Volltexte und M4 (API-first Odoo-Integration inkl. separat zu planender
|
|
||||||
Privacy-Grenze für Lohndaten).
|
Privacy-Grenze für Lohndaten).
|
||||||
|
|
||||||
## Completed (2026-09-14)
|
## Completed (2026-09-14)
|
||||||
@@ -145,8 +144,36 @@ Privacy-Grenze für Lohndaten).
|
|||||||
erzwingt ein semantischer Post-Validation-Gate ⚠ + beide IDs + korrekte
|
erzwingt ein semantischer Post-Validation-Gate ⚠ + beide IDs + korrekte
|
||||||
Rollen und verhindert interne Regelverweise bzw. Quellenpriorisierung.
|
Rollen und verhindert interne Regelverweise bzw. Quellenpriorisierung.
|
||||||
Gezielte Real-Läufe: q-124/q-125 nicht verweigert, zitiergültig; q-126 war
|
Gezielte Real-Läufe: q-124/q-125 nicht verweigert, zitiergültig; q-126 war
|
||||||
bereits sauber. Tests **72 grün**; `git diff --check` sauber. Vollständiger
|
bereits sauber. Tests **72 grün**; `git diff --check` sauber.
|
||||||
Antwortmodus-Eval steht noch aus.
|
- **D16 Voll-Eval (2026-09-16):** 53 Fragen mit `qwen3.8:27b`; Report
|
||||||
|
`data/eval-qwen38-decision-support.json` (lokal/gitignored). Retrieval:
|
||||||
|
Hit-Rate 0,9583, Recall@8 **0,9271**, MRR 0,6266. Antworten:
|
||||||
|
Zitier-Präzision **98,11 %**, Verweigerung korrekt **98,11 %**, erwartete
|
||||||
|
Quelle 91,67 %, mean 40,3 s / p95 98,4 s, 4 Regenerierungen. Einziger
|
||||||
|
Gate-Fehler: q-125 eskalierte nach zwei semantisch unzureichenden Antworten
|
||||||
|
zu `UNCERTAIN` (`verified=false`). Direkte Wiederholung von q-125 war mit
|
||||||
|
einer Regenerierung vollständig, zitiergültig und stellte den Konflikt
|
||||||
|
korrekt dar — verbleibend ist stochastische Gate-/Regenerierungs-Flakiness,
|
||||||
|
kein Retrieval-Fehler. q-124 und q-126 bestanden im Voll-Eval.
|
||||||
|
- **D16 Stabilisierung/Bestätigung (2026-09-16):** Bei vollständig
|
||||||
|
ausgelassenem, aber retrieved Mitarbeiterprämien-Konflikt ergänzt die
|
||||||
|
Pipeline deterministisch eine feste, ausschließlich aus `lb-sva-03` und
|
||||||
|
`wk-akt-04` formulierte ⚠-Notiz. Eigene Konfliktversuche bleiben ungeändert
|
||||||
|
und werden weiter auf Rollenfehler/Priorisierung geprüft. Der semantische
|
||||||
|
Gate akzeptiert gleichwertige Verben (`ordnet`, `listet`, `führt`) statt
|
||||||
|
fragiler exakter Wortwahl. Sonden: q-124 3/3 ohne Regenerierung, q-125 3/3
|
||||||
|
verifiziert, q-126 verifiziert. Voller Bestätigungslauf (53 Fragen), Report
|
||||||
|
`data/eval-qwen38-decision-support-confirm.json`: Zitier-Präzision **100 %**,
|
||||||
|
erwartete Quelle 93,75 %, mean 40,3 s / p95 69,6 s, 3 Regenerierungen;
|
||||||
|
q-124–126 alle bestanden. Verweigerungskorrektheit 98,11 % nur wegen der
|
||||||
|
bekannten stochastischen Survey-Fehlverweigerung q-029; direkte q-029-
|
||||||
|
Wiederholung antwortete verifiziert (aber sehr langsam, 280 s).
|
||||||
|
- **q-015 fachlich rekalibriert:** Eine allgemeine Tagesgeldfrage mischt zwei
|
||||||
|
Ebenen: Der KV regelt vorrangig den arbeitsrechtlichen Anspruch, das EStG den
|
||||||
|
steuerfreien Satz. Branchen-KV-Treffer sind deshalb korrekt; q-015 hat keine
|
||||||
|
künstliche Einzel-Pflicht-ID mehr. Neue q-127 prüft ausdrücklich nur die
|
||||||
|
steuerliche Höhe und findet `lb-rei-09`. Goldset 54 Fragen; Offline-Retrieval
|
||||||
|
nun Hit-Rate 0,9792, Recall@8 **0,9479**, MRR 0,6653. Tests **75 grün**.
|
||||||
|
|
||||||
## Open issues / blockers
|
## Open issues / blockers
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,14 @@ questions:
|
|||||||
expected_ids: [lb-pen-01]
|
expected_ids: [lb-pen-01]
|
||||||
- id: q-015
|
- id: q-015
|
||||||
question: "Wie werden Tagesgelder bei Dienstreisen abgerechnet?"
|
question: "Wie werden Tagesgelder bei Dienstreisen abgerechnet?"
|
||||||
|
expected_ids: []
|
||||||
|
note: "Bewusst offene Mischfrage: Der anzuwendende Kollektivvertrag regelt den arbeitsrechtlichen Anspruch und hat dafür Vorrang; das EStG regelt davon getrennt den steuerfreien Satz. Branchen-KV-Treffer sind daher korrekt. Ohne Branche soll die Antwort beide Ebenen unterscheiden und gezielt nach dem anzuwendenden KV fragen; keine einzelne Pflicht-ID für Recall."
|
||||||
|
tags: [clarification, kv-priority, tax-distinction]
|
||||||
|
- id: q-127
|
||||||
|
question: "Bis zu welcher Höhe können Tagesgelder bei Dienstreisen steuerfrei abgerechnet werden?"
|
||||||
expected_ids: [lb-rei-09]
|
expected_ids: [lb-rei-09]
|
||||||
|
note: "Reine Steuerfrage: Anders als q-015 wird hier nicht nach dem arbeitsrechtlichen KV-Anspruch, sondern ausdrücklich nach dem steuerfreien Satz gefragt."
|
||||||
|
tags: [tax, travel-expenses]
|
||||||
- id: q-016
|
- id: q-016
|
||||||
question: "Wie läuft eine GPLB ab?"
|
question: "Wie läuft eine GPLB ab?"
|
||||||
expected_ids: [lb-gpl-01]
|
expected_ids: [lb-gpl-01]
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ UNCERTAIN_MESSAGE = (
|
|||||||
"⚠ Zu dieser Frage kann ich keine verlässlich belegte Antwort "
|
"⚠ Zu dieser Frage kann ich keine verlässlich belegte Antwort "
|
||||||
"aus der Wissensbasis geben."
|
"aus der Wissensbasis geben."
|
||||||
)
|
)
|
||||||
|
DECISION_CONFLICT_NOTE = (
|
||||||
|
"⚠ Quellenkonflikt zur Mitarbeiterprämie 2026: [lb-sva-03] ordnet die "
|
||||||
|
"Prämie systematisch in den taxativen Katalog beitragsfreier Bezüge ein; "
|
||||||
|
"[wk-akt-04] nennt sie ausdrücklich SV- und BV-pflichtig. Der Konflikt "
|
||||||
|
"bleibt offen."
|
||||||
|
)
|
||||||
|
|
||||||
SYSTEM_PROMPT = """Du bist ein präziser Assistent für österreichische Personalverrechnung.
|
SYSTEM_PROMPT = """Du bist ein präziser Assistent für österreichische Personalverrechnung.
|
||||||
Du beantwortest Fragen AUSSCHLIESSLICH auf Basis der nummerierten Kontextblöcke
|
Du beantwortest Fragen AUSSCHLIESSLICH auf Basis der nummerierten Kontextblöcke
|
||||||
@@ -149,6 +155,24 @@ def validate_answer(answer: str, allowed_ids: list[str]) -> list[str]:
|
|||||||
return violations
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_decision_support_conflict(
|
||||||
|
question: str, answer: str, allowed_ids: list[str]
|
||||||
|
) -> str:
|
||||||
|
"""Ergänzt nur einen vollständig ausgelassenen, retrieved Konflikt.
|
||||||
|
|
||||||
|
Sobald die Antwort selbst einen Konflikt/Widerspruch zu formulieren versucht,
|
||||||
|
bleibt sie unverändert und durchläuft die semantische Validierung. Dadurch
|
||||||
|
werden falsche Rollenzuordnungen oder Quellenpriorisierungen nicht kaschiert.
|
||||||
|
"""
|
||||||
|
required = {"wk-akt-04", "lb-sva-03"}
|
||||||
|
if not is_decision_support(question) or not required.issubset(set(allowed_ids)):
|
||||||
|
return answer
|
||||||
|
folded = normalize_text(answer)
|
||||||
|
if "konflikt" in folded or "widerspr" in folded:
|
||||||
|
return answer
|
||||||
|
return f"{answer.rstrip()}\n\n{DECISION_CONFLICT_NOTE}"
|
||||||
|
|
||||||
|
|
||||||
def validate_decision_support_answer(
|
def validate_decision_support_answer(
|
||||||
question: str, answer: str, allowed_ids: list[str]
|
question: str, answer: str, allowed_ids: list[str]
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
@@ -170,9 +194,16 @@ def validate_decision_support_answer(
|
|||||||
"muss mit ⚠ sowie [wk-akt-04] und [lb-sva-03] offen dargestellt werden"
|
"muss mit ⚠ sowie [wk-akt-04] und [lb-sva-03] offen dargestellt werden"
|
||||||
)
|
)
|
||||||
folded = normalize_text(answer)
|
folded = normalize_text(answer)
|
||||||
if "[lb-sva-03] ordnet" not in folded or not re.search(
|
lb_role = re.search(
|
||||||
r"\[wk-akt-04\].{0,160}(?:pflicht|pflichtig)", folded, re.DOTALL
|
r"\[lb-sva-03\].{0,220}(?:ordnet|listet|fuhrt|fuehrt)"
|
||||||
):
|
r".{0,220}(?:beitragsfrei|katalog)",
|
||||||
|
folded,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
wk_role = re.search(
|
||||||
|
r"\[wk-akt-04\].{0,220}(?:pflicht|pflichtig)", folded, re.DOTALL
|
||||||
|
)
|
||||||
|
if not lb_role or not wk_role:
|
||||||
violations.append(
|
violations.append(
|
||||||
"stelle die Rollen der Konfliktquellen eindeutig dar: [lb-sva-03] "
|
"stelle die Rollen der Konfliktquellen eindeutig dar: [lb-sva-03] "
|
||||||
"ordnet die Prämie in den Katalog beitragsfreier Bezüge ein; "
|
"ordnet die Prämie in den Katalog beitragsfreier Bezüge ein; "
|
||||||
@@ -342,6 +373,7 @@ def answer_question(
|
|||||||
# leerer Map-Output -> Fallback: messages bleibt die Einzelantwort
|
# leerer Map-Output -> Fallback: messages bleibt die Einzelantwort
|
||||||
|
|
||||||
final = chat_with_length_retry(messages)
|
final = chat_with_length_retry(messages)
|
||||||
|
final = ensure_decision_support_conflict(question, final, allowed)
|
||||||
violations = validate_answer(final, allowed)
|
violations = validate_answer(final, allowed)
|
||||||
violations += validate_decision_support_answer(question, final, allowed)
|
violations += validate_decision_support_answer(question, final, allowed)
|
||||||
regenerations = 0
|
regenerations = 0
|
||||||
@@ -359,6 +391,7 @@ def answer_question(
|
|||||||
+ [{"role": "assistant", "content": final},
|
+ [{"role": "assistant", "content": final},
|
||||||
{"role": "user", "content": warn}]
|
{"role": "user", "content": warn}]
|
||||||
)
|
)
|
||||||
|
retry = ensure_decision_support_conflict(question, retry, allowed)
|
||||||
retry_violations = validate_answer(retry, allowed)
|
retry_violations = validate_answer(retry, allowed)
|
||||||
retry_violations += validate_decision_support_answer(question, retry, allowed)
|
retry_violations += validate_decision_support_answer(question, retry, allowed)
|
||||||
if not retry_violations:
|
if not retry_violations:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from agent.generate import (
|
|||||||
UNCERTAIN_MESSAGE,
|
UNCERTAIN_MESSAGE,
|
||||||
answer_question,
|
answer_question,
|
||||||
build_user_content,
|
build_user_content,
|
||||||
|
ensure_decision_support_conflict,
|
||||||
looks_like_refusal,
|
looks_like_refusal,
|
||||||
strip_think,
|
strip_think,
|
||||||
trim_results,
|
trim_results,
|
||||||
@@ -61,6 +62,20 @@ class TestDecisionSupportValidation:
|
|||||||
)
|
)
|
||||||
allowed = ["wk-akt-04", "lb-sva-03"]
|
allowed = ["wk-akt-04", "lb-sva-03"]
|
||||||
|
|
||||||
|
def test_appends_fully_omitted_retrieved_conflict(self):
|
||||||
|
answer = ensure_decision_support_conflict(
|
||||||
|
self.question, "Die Prämie ist lohnsteuerfrei [wk-akt-04].", self.allowed
|
||||||
|
)
|
||||||
|
assert "⚠ Quellenkonflikt" in answer
|
||||||
|
assert "[lb-sva-03] ordnet" in answer
|
||||||
|
assert validate_decision_support_answer(self.question, answer, self.allowed) == []
|
||||||
|
|
||||||
|
def test_does_not_mask_existing_conflict_attempt(self):
|
||||||
|
original = "⚠ Widerspruch: Beide Quellen [wk-akt-04] [lb-sva-03]."
|
||||||
|
assert ensure_decision_support_conflict(
|
||||||
|
self.question, original, self.allowed
|
||||||
|
) == original
|
||||||
|
|
||||||
def test_requires_conflict_when_both_sources_are_in_context(self):
|
def test_requires_conflict_when_both_sources_are_in_context(self):
|
||||||
violations = validate_decision_support_answer(
|
violations = validate_decision_support_answer(
|
||||||
self.question, "Nur lohnsteuerfrei [wk-akt-04].", self.allowed
|
self.question, "Nur lohnsteuerfrei [wk-akt-04].", self.allowed
|
||||||
@@ -74,6 +89,13 @@ class TestDecisionSupportValidation:
|
|||||||
)
|
)
|
||||||
assert validate_decision_support_answer(self.question, answer, self.allowed) == []
|
assert validate_decision_support_answer(self.question, answer, self.allowed) == []
|
||||||
|
|
||||||
|
def test_accepts_equivalent_listet_wording(self):
|
||||||
|
answer = (
|
||||||
|
"⚠ [wk-akt-04] nennt die Prämie SV- und BV-pflichtig. "
|
||||||
|
"[lb-sva-03] listet sie im Katalog als beitragsfreien Bezug."
|
||||||
|
)
|
||||||
|
assert validate_decision_support_answer(self.question, answer, self.allowed) == []
|
||||||
|
|
||||||
def test_rejects_reversed_source_roles_and_prompt_leakage(self):
|
def test_rejects_reversed_source_roles_and_prompt_leakage(self):
|
||||||
answer = (
|
answer = (
|
||||||
"⚠ [wk-akt-04] und [lb-sva-03] ordnen die Prämie als beitragsfrei "
|
"⚠ [wk-akt-04] und [lb-sva-03] ordnen die Prämie als beitragsfrei "
|
||||||
|
|||||||
Reference in New Issue
Block a user