fix: qualité codage — anti-hallucination LLM + négation regex + veto calibration

- Prompt DAS_EXTRACTION : ajout consignes anti-hallucination (zero invention,
  pas d'inférence de comorbidités, exiger citation exacte du texte)
- Prompt CODING_CIM10 : ajout consignes conditionnel et négation
- diagnostic_extraction.py : détection de négation avant les patterns regex DAS
  (bloque "pas d'embolie", "absence de sepsis", "sans signe d'IRC", etc.)
- veto_engine.py : VETO-03 conditionnel cherche maintenant PRÈS du concept
  (40 chars), "si" isolé ne déclenche plus de faux positif, ajout cues
  (possible, risque de, aspect de, à confirmer, à rechercher)
- veto_engine.py : négation enrichie (ne retrouve pas, sans signe/argument,
  écarté, infirmé, pas mis en évidence)

Batch analysis: VETO-02 63% from LLM hallucinations, VETO-03 63% false positives

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-03-07 23:59:02 +01:00
parent a371626f40
commit 214a5d1914
3 changed files with 69 additions and 4 deletions

View File

@@ -256,9 +256,29 @@ def _find_diagnostics_associes(
text_norm = normalize_text(text_lower) text_norm = normalize_text(text_lower)
# Patterns de négation pour filtrer les faux positifs regex
_NEG_PATTERNS = (
r"pas\s+d[e']?\s*",
r"(?:absence|aucun[e]?)\s+d[e']?\s*",
r"(?:sans|ni)\s+",
r"(?:elimine[er]?|exclut?|ecarte[er]?)\s+",
r"ne\s+retrouv\w+\s+pas\s+d[e']?\s*",
r"ne\s+montr\w+\s+pas\s+d[e']?\s*",
r"pas\s+(?:mis\s+en\s+evidence|objective?|retrouve)",
r"infirm\w+\s+",
r"sans\s+(?:signe|argument|evidence)\s+d[e']?\s*",
)
_neg_rx = re.compile("|".join(_NEG_PATTERNS), re.IGNORECASE)
# Patterns DAS # Patterns DAS
for pat, label, code in _DAS_PATTERNS: for pat, label, code in _DAS_PATTERNS:
if re.search(pat, text_norm) and code not in existing_codes: m = re.search(pat, text_norm)
if m and code not in existing_codes:
# Vérifier que le match n'est pas précédé d'une négation (40 chars avant)
start = max(0, m.start() - 40)
context_before = text_norm[start:m.start()]
if _neg_rx.search(context_before):
continue
das.append(Diagnostic(texte=label, cim10_suggestion=code, source="regex")) das.append(Diagnostic(texte=label, cim10_suggestion=code, source="regex"))
existing_codes.add(code) existing_codes.add(code)

View File

@@ -40,6 +40,8 @@ RÈGLES IMPÉRATIVES :
- Si le diagnostic est un DP, il doit refléter le motif principal de prise en charge du séjour - Si le diagnostic est un DP, il doit refléter le motif principal de prise en charge du séjour
- Si c'est un DAS, il doit avoir mobilisé des ressources supplémentaires pendant le séjour - Si c'est un DAS, il doit avoir mobilisé des ressources supplémentaires pendant le séjour
- EXCLUSION SYMPTÔME : Si le diagnostic est un symptôme (R00-R99) et qu'un diagnostic précis (Chapitres I-XIV, A00-N99) expliquant ce symptôme est présent, le symptôme ne doit PAS être codé comme DAS - EXCLUSION SYMPTÔME : Si le diagnostic est un symptôme (R00-R99) et qu'un diagnostic précis (Chapitres I-XIV, A00-N99) expliquant ce symptôme est présent, le symptôme ne doit PAS être codé comme DAS
- Un diagnostic "suspecté", "probable", "à éliminer" ne doit PAS être codé comme confirmé sauf s'il a été traité
- Si le texte dit "pas de X", "absence de X" → ne PAS coder X
DIAGNOSTIC À CODER : "{texte}" DIAGNOSTIC À CODER : "{texte}"
TYPE : {type_diag} TYPE : {type_diag}
@@ -120,6 +122,13 @@ RÈGLES IMPÉRATIVES :
- Ne propose que des diagnostics CLAIREMENT mentionnés dans le texte - Ne propose que des diagnostics CLAIREMENT mentionnés dans le texte
- ATTENTION aux valeurs biologiques : ne code PAS un diagnostic si les valeurs sont dans les normes indiquées entre crochets [N: min-max]. Exemple : Créatinine 76 [N: 50-120] = NORMAL, pas d'insuffisance rénale. - ATTENTION aux valeurs biologiques : ne code PAS un diagnostic si les valeurs sont dans les normes indiquées entre crochets [N: min-max]. Exemple : Créatinine 76 [N: 50-120] = NORMAL, pas d'insuffisance rénale.
ANTI-HALLUCINATION — CRITIQUE :
- ZERO INVENTION : ne code JAMAIS un diagnostic qui n'est pas explicitement écrit dans le texte
- Ne PAS inférer de comorbidités (ex: patient obèse ≠ forcément diabétique ou hypertendu)
- Si le texte dit "pas de X", "absence de X", "élimine X" → ne PAS coder X
- Un diagnostic "suspecté", "probable", "à éliminer" ne doit PAS être codé sauf s'il a été traité
- Chaque DAS proposé DOIT avoir une justification citant un passage EXACT du texte
DÉNUTRITION — CRITÈRES HAS/FFN 2021 : DÉNUTRITION — CRITÈRES HAS/FFN 2021 :
- Diagnostic = 1 critère phénotypique + 1 critère étiologique - Diagnostic = 1 critère phénotypique + 1 critère étiologique
- Seuils IMC : adulte <18.5 modéré / ≤17 sévère ; ≥70 ans <22 modéré / <20 sévère - Seuils IMC : adulte <18.5 modéré / ≤17 sévère ; ≥70 ans <22 modéré / <20 sévère

View File

@@ -40,12 +40,25 @@ _NEGATION_CUES = (
"pas de", "pas de",
"pas d", "pas d",
"absence de", "absence de",
"absence d",
"non retenu", "non retenu",
"exclu", "exclu",
"a eliminer", "ecarte",
"a éliminer", "écarté",
"ne retrouv",
"ne montr",
"ne met pas en evidence",
"pas objective",
"pas retrouve",
"pas mis en evidence",
"sans signe",
"sans argument",
"sans evidence",
"aucun signe",
"aucun argument",
"negatif", "negatif",
"négatif", "négatif",
"infirme",
) )
_CONDITIONAL_CUES = ( _CONDITIONAL_CUES = (
@@ -57,6 +70,15 @@ _CONDITIONAL_CUES = (
"probable", "probable",
"hypothese", "hypothese",
"hypothèse", "hypothèse",
"a eliminer",
"a éliminer",
"a rechercher",
"a confirmer",
"possible",
"potentiel",
"risque de",
"aspect de",
"aspect d",
"?", "?",
) )
@@ -142,7 +164,21 @@ def _analyze_neg_cond(excerpts: Iterable[str], label: str) -> tuple[bool, bool,
pre = ns[max(0, hit_pos - 40):hit_pos] pre = ns[max(0, hit_pos - 40):hit_pos]
has_neg = any(cue in pre for cue in _NEGATION_CUES) has_neg = any(cue in pre for cue in _NEGATION_CUES)
has_cond = any(cue in ns for cue in _CONDITIONAL_CUES)
# Conditionnel : chercher PRÈS du concept (40 chars avant + 20 après)
# et exclure "si" isolé qui est trop ambigu (ex: "si besoin", "si allergie")
context_cond = ns[max(0, hit_pos - 40):min(len(ns), hit_pos + len(kws[0]) + 20)]
has_cond = False
for cue in _CONDITIONAL_CUES:
if cue == "si":
# "si" seulement s'il est directement suivi du concept
# (ex: "si embolie" mais pas "si allergie connue ... embolie")
if f"si {kws[0]}" in context_cond or f"s il {kws[0]}" in context_cond:
has_cond = True
break
elif cue in context_cond:
has_cond = True
break
if has_neg: if has_neg:
negated = True negated = True