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:
@@ -256,9 +256,29 @@ def _find_diagnostics_associes(
|
||||
|
||||
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
|
||||
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"))
|
||||
existing_codes.add(code)
|
||||
|
||||
|
||||
@@ -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 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
|
||||
- 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}"
|
||||
TYPE : {type_diag}
|
||||
@@ -120,6 +122,13 @@ RÈGLES IMPÉRATIVES :
|
||||
- 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.
|
||||
|
||||
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 :
|
||||
- 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
|
||||
|
||||
@@ -40,12 +40,25 @@ _NEGATION_CUES = (
|
||||
"pas de",
|
||||
"pas d",
|
||||
"absence de",
|
||||
"absence d",
|
||||
"non retenu",
|
||||
"exclu",
|
||||
"a eliminer",
|
||||
"a éliminer",
|
||||
"ecarte",
|
||||
"é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",
|
||||
"négatif",
|
||||
"infirme",
|
||||
)
|
||||
|
||||
_CONDITIONAL_CUES = (
|
||||
@@ -57,6 +70,15 @@ _CONDITIONAL_CUES = (
|
||||
"probable",
|
||||
"hypothese",
|
||||
"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]
|
||||
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:
|
||||
negated = True
|
||||
|
||||
Reference in New Issue
Block a user