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)
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user