2026
SourceCheck
Recall 1.000 sur 40 dossiers de screening, zéro faux négatif, tenu par un gate CI
Agent d'aide à l'onboarding client pour banque privée, construit sur Microsoft Foundry. Il screene un dossier contre les sanctions SECO et les PEP OpenSanctions, et rend un verdict tracé adossé à des identifiants d'enregistrement. Le vrai sujet n'est pas l'agent mais l'appareil qui le mesure : 40 dossiers de vérité terrain, un évaluateur custom, un gate CI qui refuse le build sous 100 % de recall.
SourceCheck est un agent d'aide à l'onboarding client pour une banque privée, construit sur Microsoft Foundry entre le 23 et le 26 juillet 2026. La question de départ n'était pas de savoir si un LLM sait lire une liste de sanctions, mais comment prouver qu'un agent de screening a raison, et pas seulement qu'il répond bien. La distinction n'est pas rhétorique : à un moment du projet, l'agent produisait des réponses fluides, sourcées et plausibles, avec une precision mesurée à 0.00. Le projet a donc deux moitiés inégales. La chaîne technique (un matcher de noms déterministe, un serveur MCP sur Azure Functions, deux sources externes) est la moitié visible. La moitié qui compte est l'appareil de mesure : un jeu de 40 dossiers de vérité terrain dérivé des listes officielles, un évaluateur custom écrit pour ce métier, et un gate de non-régression qui casse le build si le recall descend sous 100 %. Cadre de travail assumé et posé dès le PRD : le code applicatif a été écrit par Claude Code. Ce que j'ai tenu, ce sont les décisions, l'architecture, la configuration Azure et l'arbitrage sur ce qui compte comme une bonne réponse.
Architecture
dossier client (personne ou société)
|
v
agent Foundry (gpt-5-mini)
|
| serveur MCP, Azure Functions (mcpToolTrigger)
|--> screen_name -> matcher.py -> index SECO, 8 604 lignes
|--> check_pep -> index OpenSanctions, 748 000 PEP
|--> lookup_entity -> API GLEIF (structure, bénéficiaire ultime)
|
v
verdict : hit | a_verifier | clean
+ identifiants (SECO:5142, OpenSanctions:Q59321885)
|
v
trace -> SanctionsRecallEvaluator -> gate CI (recall < 100 % = rouge)Le serveur MCP ne contient aucune logique de rapprochement : il délègue à matcher.py. Il est hébergé sur Azure Functions via le binding natif mcpToolTrigger (Flex Consumption, Sweden Central), protégé par une system key générée par Azure qui ne vit que dans la configuration Foundry, jamais dans le repo. En amont, tools/seco.py convertit la liste consolidée SECO, un XML de 38 Mo illisible en l'état, en un index CSV cherchable de 8 604 enregistrements ; tools/pep.py construit l'index PEP de 748 000 personnes depuis les données bulk d'OpenSanctions.
Contraintes de conception
- Aucun fallback silencieux. Si une source externe tombe, l'outil renvoie une erreur explicite (gleif_unavailable, pep_index_unavailable), jamais une réponse vide. Un outil qui répond « aucune correspondance » alors que sa source est tombée fabrique un faux négatif indétectable, le pire mode de défaillance en compliance.
- La comparaison se fait sur des identifiants d'enregistrement (SECO:5142, OpenSanctions:Q59321885), jamais sur des chaînes de noms. C'est ce qui rend un score vérifiable par un tiers plutôt que crédible sur parole.
- Priorité au recall. Un faux négatif, c'est un sanctionné qui passe : échec binaire. Un faux positif coûte du temps d'analyste. Les seuils du gate reflètent cette asymétrie : recall 100 % obligatoire, precision au-dessus de 70 %.
- Score décomposable. Chaque correspondance expose la contribution exacte de ses cinq composantes (token 0.45, phonétique 0.20, couverture 0.20, ordre 0.10, exact 0.05), dont la somme est le score. Pas de boîte noire : en environnement régulé, un score qu'on ne peut pas expliquer ne vaut rien.
- Aucun LLM et aucun embedding dans le matcher, bibliothèque standard Python uniquement, donc déterministe et rejouable. Pipeline : normalisation Unicode, translittération (cyrillique BGN/PCGN, arabe, persan, ourdou, sinogrammes, hangul calculé depuis le point de code, grec, hébreu), tokenisation, deux clés phonétiques dont un squelette consonantique tolérant, alignement de tokens, score. Environ 200 ms pour comparer un nom à 5 000 noms de référence.
Trois moments d'ingénierie
- Le bug trouvé en lisant une trace. Sur les cinq premiers dossiers instrumentés, la precision tombait à 0.00 sur trois d'entre eux. La trace disait pourquoi : l'agent recopiait dans son champ hits les dix candidats renvoyés par screen_name, si bien qu'un Suédois sans le moindre lien avec une liste ressortait avec dix correspondances de sanctions. La réponse restait fluide et citait ses sources : un évaluateur de qualité l'aurait passée sans broncher. C'est l'écart entre « l'agent répond bien » et « l'agent a raison », en chiffres.
- Le fix dans l'outil plutôt que dans le prompt. Corriger cela par le prompt revenait à espérer qu'un petit modèle filtre correctement le bruit à chaque appel. Le plancher de score est donc allé dans screen_name (0.80 par défaut, réglable par SCREEN_MIN_SCORE), avec un champ suppressed_below_floor pour garder trace de ce qui a été coupé : l'agent ne voit plus jamais les candidats sous le seuil. « Thomas Johansson » est passé de dix candidats à zéro, un vrai match à 1.0 passe toujours. Défense en profondeur : l'outil coupe le bruit, le prompt gère les cas limites.
- Le désaccord métier que l'éval a fait sortir. Un PEP est-il un hit ou une escalade ? La vérité terrain disait hit, l'agent disait a_verifier, et son argument tenait debout : un PEP relève de la vigilance renforcée, pas d'un blocage. Le désaccord est resté documenté plutôt qu'aplati par une correction de prompt. Un jeu d'éval ne sert pas qu'à noter l'agent, il sert aussi à découvrir que sa propre définition était discutable.
Les chiffres
- Sur les 40 dossiers : recall 1.000, precision 0.838, zéro faux négatif, 8 faux positifs. Cibles du PRD atteintes (recall 100 %, precision au-dessus de 70 %).
- La precision est passée de 0.912 à 0.838 en listant davantage d'identifiants : tradeoff assumé au profit du recall. Les 8 faux positifs sont surtout des dossiers a_verifier qui remontent un near_miss légitime, que l'évaluateur compte comme faute.
- 77 tests automatisés verts : matcher, outils MCP, évaluateur.
- CI verte de bout en bout, environ 17 minutes : login OIDC sans aucun secret stocké, rejeu des 40 dossiers contre l'agent déployé, gate de non-régression, scorecard dans le résumé GitHub.
- 0,2513 CHF de coût total, dont 80 % en tokens, relevé via l'API Cost Management. Hébergement (Functions Flex Consumption) et tracing (Application Insights) à 0,00 grâce aux paliers gratuits. Environ 0,0015 CHF par dossier screené, soit 0,15 centime.
- 15 commits, 4 sprints livrés en 3 jours pour une fenêtre planifiée sur 4 week-ends.
La plateforme, verdict
Foundry est une plateforme d'AgentOps. Son intérêt n'est pas d'aider à écrire un agent, n'importe quel SDK le fait : c'est l'usine autour, traces, évaluation, gates de non-régression, gouvernance de l'identité.
- Ce que ça évite d'écrire : le harnais d'observabilité et de non-régression. Le tracing est le service le plus utile du lot, c'est là que les deux vrais bugs sont devenus visibles plutôt que devinés.
- Ce que ça coûte : une courbe RBAC et identité raide. Le « qui » d'une attribution de rôle n'est pas l'utilisateur mais l'identité managée du service, ce qui est contre-intuitif. Un 401 annonçant « invalid subscription key » alors que l'endpoint attend un jeton Entra envoie sur une fausse piste. Et deux ressources Foundry homonymes peuvent coexister, dont une auto-générée, la configuration finissant posée sur la mauvaise.
- Le piège OIDC, celui qui vaut le détour : le subject présenté par GitHub inclut désormais des identifiants numériques immuables (repo:owner@<id>/repo@<id>:ref:refs/heads/main) et non le format repo:owner/repo que montrent encore les documentations Microsoft et GitHub. D'où un AADSTS700213 au premier essai, réglé en recréant le federated credential avec le subject exact renvoyé dans l'erreur.
- Les Toolboxes ont fini contournées au profit d'un serveur MCP branché directement sur la Function. Le numéro de version est figé dans l'URL par défaut, donc mettre à jour une toolbox oblige à reconfigurer l'agent, sauf à passer par l'endpoint sans version qui sert la default_version. Interface polie, UX pas encore.
- La contrainte réelle n'a jamais été la facture (0,25 CHF) mais le quota de débit : des 429 sur gpt-5-mini ont cassé un run complet, d'où un backoff exponentiel et un throttle dans le runner d'évaluation.
Ce que ce n'est pas
Ce n'est pas un produit de compliance utilisable par un analyste, et le PRD le posait explicitement comme un non-objectif. 40 dossiers, c'est un jeu de test d'apprentissage, pas une validation industrielle. Le reste des réserves, telles quelles :
- Le code applicatif a été écrit par une IA (Claude Code), délibérément : c'était la règle de délégation du PRD. Je suis l'auteur des décisions, de l'architecture, de la configuration Azure et des arbitrages, pas des lignes de Python.
- La vérité terrain n'est pas écrite à la main. C'est un pipeline qui la dérive des listes officielles réelles (SECO, OpenSanctions) avec un garde-fou anti-hallucination : chaque identifiant est vérifié présent dans la liste et recoupé avec le nom du dossier, chaque nom inventé vérifié absent.
- La régression provoquée a été démontrée sur le gate unitaire : neutralisation volontaire de la vérification de date de naissance, deux tests rouges, exit code 1, la fonction fautive nommée dans le rapport. Elle ne l'a pas été sur le gate d'éval : aucun run réel contre un agent volontairement dégradé n'a été exécuté.
- check_pep fait un lookup exact sur le nom et les alias, pas du matching flou : il est moins tolérant aux translittérations que screen_name. Limite assumée, question d'échelle sur 748 000 entrées.
- GLEIF ne couvre que les entités disposant d'un LEI, donc reste aveugle aux PME. Les données PEP d'OpenSanctions sont sous licence CC-BY-NC : utilisables pour un projet d'apprentissage, pas pour un produit commercial.
- L'adhérence de l'agent au format de sortie a demandé quatre itérations de prompt. gpt-5-mini est un facteur limitant assumé.
- Hors périmètre, assumé : fine-tuning, réseau privé (BYO VNet), publication Teams ou M365 Copilot, orchestration multi-agents, routage de modèles par coût.
Technologies
Microsoft Foundry · MCP · Azure Functions · Python · Entra ID / RBAC · OIDC · GitHub Actions · OpenSanctions · GLEIF · SECO · pytest