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.
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| 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.
Dix familles, rangées par verbe
- Détecter et filtrer Comment repérer ce qui ne doit pas passer ?
- Extraire Comment sortir cette information du texte ?
- Classer et router Où doit aller ce message ?
- Chercher Comment retrouver la bonne chose dans tout ça ?
- Recommander Qu'est-ce que je propose ensuite ?
- Prédire Que va-t-il se passer, et qu'est-ce qui cloche ?
- Générer Comment produire ça sans l'écrire à la main ?
- Transformer Comment rendre ça sous une autre forme ?
- Reconnaître et transcrire Comment lire ce qui n'est pas déjà du texte ?
- Décider et valider Est-ce que j'accepte ceci, et sur quel fondement ?
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.
-
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.
-
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.
-
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.
-
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.
Trois fiches pour commencer
Choisies pour la diversité de leurs verdicts, y compris celui qui recommande d'appeler un modèle.
- Masquer les coordonnées dans un message RecommandéN0
- Router un ticket de support vers la bonne équipe RecommandéN1
- Rédiger des descriptions produit RecommandéN3
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.