درس 5 — Analyser

Signaux cités et mémo vérifié

9 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Signaux cités et mémo vérifié

Lors d'un essai sur Vertex AI, le lecteur de risques a rendu six signaux pour Maison Ferlac. Quatre « citations » ont été rejetées ; trois d'entre elles n'existaient pas dans le rapport : c'étaient des résumés fidèles, mais des résumés. « L'emprunt syndiqué est assorti d'un covenant [...]. Ce ratio s'établit à 3,4x » colle deux phrases de la note 9 en supprimant le montant de l'emprunt et la mention « selon la définition contractuelle ». Dans un mémo de crédit, cette nuance est le sujet. Cette leçon impose deux règles : une citation est recopiée ou elle n'existe pas ; un nombre du mémo figure dans les faits calculés ou il est refusé.

Le mécanisme

sequenceDiagram
  participant L as "Lecteur de risques"
  participant P as "Python"
  participant R as "Rédacteur"
  L->>P: signaux (citation, page)
  P->>P: citation présente mot pour mot sur la page ?
  P-->>L: citations introuvables (une relecture)
  L->>P: signaux corrigés
  P->>R: faits identifiés R-, A-, Q-
  R->>P: mémo (paragraphes + sources)
  P->>P: sources existent ? nombres présents dans les faits ?
  P-->>R: défauts (une réécriture)

La citation vérifiée

files/analyste/pdf.py
def normaliser(texte: str) -> str:
    texte = unicodedata.normalize("NFKC", texte).replace("’", "'").replace("|", " ")
    return re.sub(r"\s+", " ", texte).strip().lower()


def citation_verifiee(pages: list[str], page: int, citation: str) -> bool:
    # Ponctuation de bord tolérée : un modèle ajoute souvent un point final ou des guillemets.
    c = normaliser(citation).strip(" .;:,«»\"'")
    if not 1 <= page <= len(pages) or len(c) < 12:
        return False
    return c in normaliser(pages[page - 1])

La normalisation absorbe ce qui n'a pas de sens (retours à la ligne du PDF, apostrophes typographiques, casse, point final ajouté) et rien d'autre : un mot changé fait échouer la vérification. Le minimum de 12 caractères évite qu'une « citation » de deux mots passe par hasard.

Une citation rejetée est renvoyée une fois au modèle (« recopie chaque extrait exactement ou retire le signal »). Ce qui reste introuvable est écarté, compté dans signaux_rejetes, et n'atteint jamais le tableau de bord.

Le lecteur de risques cite « le ratio s'établit à 3,4x » en page 7. La phrase exacte est « ce ratio s'établit à 3,4x selon la définition contractuelle ». Faut-il assouplir la vérification pour accepter cette citation ?

Le mémo vérifié

Le rédacteur ne voit ni les PDF ni les états : il reçoit des faits, chacun avec un identifiant.

[R-ferlac-levier] Maison Ferlac SAS - Levier 2025 : 5,5x (2024 : 3,6x) ; formule : dette nette / EBITDA
[A-solvea-2] Solvéa Énergies SA - Résultat net de 11 175 k€ mais free cash-flow de -47 225 k€ en 2025.
[Q-ferlac-3] Maison Ferlac SAS - « L'emprunt syndiqué de 85 000 k€ est assorti d'un covenant : ... » (...)

Sa sortie structurée (Memo) impose une liste de paragraphes, chacun avec ses sources. La vérification est déterministe :

files/analyste/memo.py
NOMBRE = re.compile(r"\d+(?:[  ]\d{3})*(?:,\d+)?")

def verifier_memo(memo: Memo, sources: dict[str, dict[str, Any]]) -> list[str]:
    autorises = set().union(*(_nombres(s["texte"]) for s in sources.values()), {"2024", "2025"})
    problemes = []
    for i, p in enumerate(memo.paragraphes, start=1):
        if not p.sources:
            problemes.append(f"paragraphe {i} : aucune source citée")
        for sid in p.sources:
            if sid not in sources:
                problemes.append(f"paragraphe {i} : source inconnue {sid}")
        for nb in sorted(_nombres(p.texte) - autorises):
            problemes.append(f"paragraphe {i} : le nombre {nb} ne figure dans aucun fait fourni")
    return problemes

Un « le levier a gagné 1,9 point » (5,5 - 3,6, calculé de tête) est refusé : 1,9 ne figure dans aucun fait. Le test test_memo_refuse_nombre_invente_et_source_inconnue le vérifie.

Résultat obtenu (2026-10-15)

Premier essai sur Vertex AI (gemini-2.5-flash), avant les deux corrections (ni relecture, ni tolérance de ponctuation) :

INFO analyste.agents ferlac : 2 signaux cités, 4 rejetés (citation introuvable)
Maison Ferlac SAS          68/68      3/3         OK        0        0.00%     2/6       2/6
Exactitude des champs : 204/204 (100.0%) | écart max des ratios : 0.00% | rappel des signaux : 69% | mémo vérifié : oui

Une des quatre citations n'était rejetée que pour un point final ajouté ; les trois autres étaient des résumés. Second essai, avec relecture et ponctuation de bord tolérée, même modèle :

INFO analyste.agents ferlac : signaux, tentative 1 : 6 cités, 0 rejetés (citation introuvable)
Exactitude des champs : 204/204 (100.0%) | écart max des ratios : 0.00% | rappel des signaux : 100% | mémo vérifié : oui

Soyons précis sur ce que prouve ce second essai : aucune citation n'a été rejetée dès la première tentative, la relecture n'a donc pas servi. L'écart entre les deux essais tient surtout à la variance du modèle d'un appel à l'autre. La relecture est une assurance, sa valeur se mesure sur plusieurs passages.

Extrait du mémo produit par make e2e (gemini-3.5-flash), vérifié du premier coup :

Maison Ferlac SAS cumule une forte vulnérabilité commerciale et financière. Son principal client représente 38 % du chiffre d'affaires. L'emprunt syndiqué de 85 000 k€ impose un ratio dette nette sur EBITDA ajusté de maximum 3,5x, alors que les lignes non tirées sont limitées à 6 000 k€. Sources : Q-ferlac-1, Q-ferlac-3, Q-ferlac-6, Q-ferlac-5.

Signaux d'alerte avec leur gravité et leurs pages, mémo vérifié avec ses sources cliquables

Ce qui casse en production

  • L'affirmation sans nombre. Le mémo du 15 octobre écrit que le free cash-flow d'Helvane est « tiré par une gestion maîtrisée du besoin en fonds de roulement ». Aucun nombre, donc aucun refus, et la source citée (le free cash-flow) ne dit rien du BFR. La vérification des nombres ne couvre pas les liens de cause. Parade partielle : une relecture humaine obligatoire des paragraphes « comparaison », ou un juge LLM qui vérifie chaque phrase contre ses seules sources, avec son propre taux d'erreur à mesurer.
  • Le signe perdu. L'expression régulière ignore le signe : « une marge nette de 0,5 % » passerait alors que le fait dit -0,5 %. C'est une limite connue de ce contrôle (exercice ci-dessous).
  • La citation hors contexte. Une citation exacte peut être trompeuse (« aucune provision n'a été constituée » n'a de sens qu'avec le litige qui précède). La visionneuse de la leçon 6 affiche la page entière, surlignée : le contexte reste à un clic.
  • La variance d'un essai à l'autre. Même modèle, même document : quatre citations rejetées, puis aucune. Une mesure sur un seul passage ne dit rien du taux réel. Parade : répéter l'évaluation trois à cinq fois et suivre la moyenne et le pire cas. Le lecteur de risques reçoit le texte extrait, jamais le PDF : c'est ce texte qui sert de référence à la vérification.

Règle de décision

Vérifiez mécaniquement tout ce qui peut l'être (citations, nombres, identifiants de sources) et limitez la liberté du rédacteur à la formulation. Ne demandez pas au modèle de « vérifier lui-même ses citations » : il confirme ce qu'il vient d'écrire. Surveillez deux taux : citations rejetées par rapport, et mémos refusés après réécriture ; une hausse signale un changement de modèle ou de format de document.

Pratique

Un rapport réel cite en page 12 une phrase qui apparaît aussi, identique, en page 3 (résumé). Le lecteur de risques indique la page 3. La vérification l'accepte. Est-ce un problème ?

Le rédacteur écrit « Solvéa a levé 42 000 k€ de financements externes » (15 000 + 27 000). Le contrôle refuse le mémo. Le comité trouve pourtant cette phrase utile. Que faites-vous ?

Exercice. Corrigez la limite du signe : un nombre écrit sans signe dans le mémo ne doit être accepté que s'il existe aussi sans signe dans les faits, ou si la phrase l'accompagne d'un mot qui porte le signe (« négatif », « perte », « déficit »). Écrivez le test qui accepte « un free cash-flow négatif de 14 930 k€ » et refuse « une marge nette de 0,5 % » pour Ferlac.

Voir la solution

On garde le signe dans les faits et dans le mémo, avec une exception lexicale :

files/analyste/memo.py
NOMBRE_SIGNE = re.compile(r"(?<![\d,])(-?)(\d+(?:[  ]\d{3})*(?:,\d+)?)")
MOTS_NEGATIFS = ("négatif", "négative", "perte", "déficit")

def _nombres_signes(texte: str) -> set[str]:
    return {s + re.sub(r"[  ]", "", n) for s, n in NOMBRE_SIGNE.findall(texte)}

def nombres_non_autorises(texte: str, autorises: set[str]) -> set[str]:
    negatif_explicite = any(m in texte.lower() for m in MOTS_NEGATIFS)
    return {n for n in _nombres_signes(texte)
            if n not in autorises and not (negatif_explicite and "-" + n in autorises)}
files/tests/test_calculs.py
def test_signe_du_memo():
    from analyste.memo import _nombres_signes, nombres_non_autorises
    autorises = _nombres_signes("Free cash-flow 2025 : -14 930 k€ ; marge nette : -0,5 %")
    assert not nombres_non_autorises("un free cash-flow négatif de 14 930 k€", autorises)
    assert nombres_non_autorises("une marge nette de 0,5 %", autorises) == {"0,5"}

La règle reste lexicale : « une perte limitée, et une marge nette de 0,5 % » passerait. C'est le plafond de ce type de contrôle ; au-delà, il faut un rédacteur qui insère des références ({R-ferlac-marge_nette}) remplacées par le code, au lieu d'écrire les nombres.

  • Je sais vérifier qu'une citation existe mot pour mot à la page indiquée
  • Je sais construire des faits identifiés pour un rédacteur
  • Je sais refuser un mémo qui contient un nombre calculé de tête
  • Je connais les limites de cette vérification (causes, signes, contexte)
التالي