Avez-vous vraiment besoin d'IA pour ça ?

Pour chaque tâche courante, toutes les options connues, de la règle au modèle généraliste. Avec le code qui tourne, les ordres de grandeur, et la condition précise qui justifie de monter d'un cran.

Voir le catalogue

Overkill n'est pas anti-IA. Certaines fiches recommandent franchement un modèle généraliste, et ce sont elles qui rendent crédibles celles qui recommandent l'inverse.

La même tâche, deux fois

Masquer un numéro de téléphone dans un message

N3 Ce qu’on vous propose 42 lignes

Un appel de modèle généraliste sur chaque message entrant.

PROMPT = (
    "Find every piece of personal contact information in the message below.\n"
    "Answer with JSON only: a list of objects with keys `text` and `kind`,\n"
    "where `kind` is one of email, phone, iban, address.\n"
    "If there is none, answer with an empty list.\n\n"
    "Message:\n{message}"
)

MAX_CHARACTERS = 8000


class MaskingUnavailable(Exception):
    """The provider could not be reached, or answered something unusable."""


def mask(message: str, client=None, *, attempts: int = 3) -> str:
    """
    Replace contact details with a label naming what was removed.

    `client` is injected so this function can be tested without a network call.
    In production it defaults to a real provider client.
    """
    if client is None:  # pragma: no cover - needs a key and a network
        from openai import OpenAI

        client = OpenAI()

    # A model charges by the token. Refusing oversized input is not an
    # optimisation, it is a cost control.
    if len(message) > MAX_CHARACTERS:
        raise ValueError(f"message longer than {MAX_CHARACTERS} characters")

    found = _ask(client, message, attempts)

    # Replace the longest matches first, so a substring never eats its parent.
    for item in sorted(found, key=lambda i: len(i.get("text", "")), reverse=True):
        text, kind = item.get("text"), item.get("kind")
        if text and kind:
            message = message.replace(text, f"[{kind}]")
    return message


def _ask(client, message: str, attempts: int) -> list[dict]:
    last_error: Exception | None = None
    for _ in range(attempts):
        try:
            answer = client.complete(prompt=PROMPT.format(message=message), temperature=0)
            parsed = json.loads(answer)
            if isinstance(parsed, list):
                return parsed
            last_error = ValueError("the model answered something that is not a list")
        except Exception as error:  # noqa: BLE001 - any provider failure is retried
            last_error = error
    raise MaskingUnavailable(str(last_error))

N0 Ce qui suffit 25 lignes

Une normalisation, puis trois expressions régulières.

UNUSUAL_SPACES = re.compile(r"[     ]")

EMAIL = re.compile(r"[\w.+-]+@[\w-]+(?:\.[\w-]+)+")

# French numbers: 0X XX XX XX XX, or +33 X XX XX XX XX. The separator between
# digits may be a space, a dot or a dash, or absent.
SEP = r"[ .-]?"
PHONE = re.compile(rf"(?<![\d+]){SEP}(?:\+{SEP}33{SEP}|0)[1-9](?:{SEP}\d){{8}}(?!\d)")

# IBAN: two letters, two check digits, then up to thirty alphanumerics,
# conventionally grouped in fours.
IBAN = re.compile(r"(?<![A-Z0-9])[A-Z]{2} ?\d{2}(?: ?[A-Z0-9]){10,28}(?![A-Z0-9])")

# Order matters: an email may contain digits that would otherwise be read as
# the start of a phone number.
PATTERNS = ((EMAIL, "[email]"), (IBAN, "[iban]"), (PHONE, "[phone]"))


def normalise(text: str) -> str:
    """Reduce the many spellings of a space to a plain one."""
    return UNUSUAL_SPACES.sub(" ", unicodedata.normalize("NFKC", text))


def mask(text: str) -> str:
    """
    Replace contact details with a label naming what was removed.

    A label beats a row of asterisks: whoever reads the thread later can see
    that a phone number was removed, not merely that something was.
    """
    text = normalise(text)
    for pattern, label in PATTERNS:
        text = pattern.sub(label, text)
    return text
Masquer un numéro de téléphone dans un message
Barreau N3 N0
Coût élevé nul
Latence ~1 s <1 ms
Données partent chez un tiers rien ne sort
Déterministe non oui
Testable difficilement unitairement

Le barreau le plus lourd n’est pas toujours le mauvais choix. Sur cette tâche-ci, il l’est.

Comment lire une fiche

Quatre barreaux, toujours dans le même ordre. L'échelle ne mesure pas la qualité d'une solution, elle mesure son poids : ce qu'il faut mettre en marche pour obtenir le résultat.

  1. Règle et algorithme classique N0

    Règle et algorithme classique

    Une expression régulière, une requête, un automate. Le code fait exactement ce qu’il dit, et vous pouvez le lire en entier.

  2. Modèle classique léger N1

    Modèle classique léger

    Un modèle entraîné sur vos données, assez petit pour être versionné à côté du code qui s’en sert, et qui tourne sur un processeur ordinaire.

  3. Petit modèle spécialisé auto-hébergé N2

    Petit modèle spécialisé auto-hébergé

    Un modèle spécialisé qui tourne chez vous. Il pèse lourd, il fait une chose, et rien ne sort de votre infrastructure.

  4. API de LLM généraliste N3

    API de LLM généraliste

    Un appel à un fournisseur externe. Le modèle sait tout faire à peu près, rien exactement, et il ne vous appartient pas.

Comment on évalue

Ce catalogue vous appartient

Deux cents besoins sont sur la feuille de route, vingt-cinq sont écrits. Si vous avez vu quelqu'un employer un modèle généraliste là où une règle aurait suffi, il manque une fiche.