Copilote agentique pour la chaîne d'approvisionnement
Un planificateur pose une question d'approvisionnement en langage courant. Le système fait le calcul exact au lieu de l'estimer, et s'arrête pour l'accord d'un responsable au-delà de 10 000 $.
Le problème
Les planificateurs de la chaîne d'approvisionnement posent leurs questions en phrases. Les solveurs d'optimisation lisent des matrices de contraintes. Mettez un modèle de langage entre les deux et laissez-le faire le calcul, il renvoie des réponses plausibles qui coûtent de l'argent quand elles sont fausses. Un plan d'acheminement qui a l'air optimal sans l'être ressort plus tard en facture réelle.
Architecture
Un StateGraph LangGraph fait passer chaque requête par 6 nœuds. Le classifieur d'intentions associe le langage naturel à 1 des 10 intentions bornées (mcnf_solve, jsp_schedule, vrp_route, robust_allocate, meio_optimize, bullwhip_analyze, disruption_resource, kg_query, contract_query, multi_step). Un classifieur compilé avec DSPy (MIPROv2) prend le relais de la référence zéro-exemple à l'exécution quand son artefact JSON est présent, et la voie zéro-exemple reste disponible sans changer la configuration.
L'agent Think-on-Graph extrait les entités, sélectionne les relations dans une liste blanche et parcourt Neo4j avec du Cypher paramétré. La pile CRAG (recherche dense BGE-large-en-v1.5, recherche creuse BM25, fusion RRF à k=60 et reclasseur CrossEncoder) retrouve les fragments de contrats et étiquette chacun Correct, Ambigu ou Incorrect avant que le synthétiseur le voie. Un cache sémantique Redis à deux niveaux évite entièrement le parcours LangGraph sur une requête répétée : correspondance exacte SHA-256 d'abord, puis similarité cosinus à 0,95 avec une garde sur les tokens discriminants pour les nombres et les codes d'entité.
Quand un résultat de solveur dépasse 10 000 $, le graphe s'arrête à un nœud de porte humaine, via interrupt() de LangGraph. L'UUID de décision reste dans Redis avec une durée de vie de 24 heures. Le point de terminaison d'approbation reprend le graphe avec la décision du responsable et se garde d'une double soumission (HTTP 409 en cas de renvoi). Dans le navigateur, le planificateur voit une bannière d'avertissement avec les boutons Approuver et Refuser, qui se résolvent en badge vert ou rouge.
La pile
| Couche | Outil | Pourquoi |
|---|---|---|
| Orchestration | LangGraph StateGraph | Graphe à 6 nœuds : classify → route → {kg_agent | contract_agent | solver_dispatch} → human_gate (conditionnel) → synthesize. Un point de reprise MemorySaver conserve l'exécution suspendue pendant l'interruption pour intervention humaine. |
| Classification d'intentions | Zéro-exemple, option DSPy | La voie principale est un appel à sortie structurée avec 10 intentions bornées et un seuil de confiance. La montée facultative est une ChainOfThought compilée par DSPy MIPROv2, chargée depuis un artefact JSON, et la voie zéro-exemple reste en place quand DSPy manque ou que l'artefact est absent. |
| Recherche d'information (CRAG) | BGE-large-en-v1.5, BM25, RRF | Recherche dense en 1024 dimensions via pgvector ivfflat et recherche creuse BM25Okapi, fusionnées par RRF (k=60), puis un reclasseur CrossEncoder et une porte de pertinence tenue par le modèle de langage, qui étiquette chaque passage Correct, Ambigu ou Incorrect. |
| Graphe de connaissances | Neo4j 5, Cypher en liste blanche | Think-on-Graph : extraire les entités, sélectionner les relations dans une liste blanche, parcourir avec 1 nouvelle tentative qui se corrige sur un sous-graphe vide. Aucun texte brut du modèle n'atteint une requête de base de données. |
| Solveurs | OR-Tools, CVXPY, SciPy SLSQP | 7 solveurs exacts : MCNF (programmation linéaire), VRP (CVRP), JSP, min-max robuste (CVXPY), MEIO GSM (SLSQP, non convexe), analyse de l'effet coup de fouet, et affectation de ressources en cas de perturbation. Un solveur par intention, et le modèle de langage ne fait aucun calcul. |
| Contrats MCP | 6 serveurs FastMCP | server_erp, server_kg, server_crag, server_ortools, server_cvxpy, server_scipy. Chaque appel de solveur et de base de données franchit un contrat Pydantic typé, donc une sortie malformée choisit au pire le mauvais outil. |
| Cache sémantique | Redis, cosinus BGE (0,95) | Recherche à deux niveaux : correspondance exacte SHA-256, puis similarité cosinus. Une garde sur les tokens discriminants (multi-ensemble des nombres et ensemble des codes d'entité) bloque les fausses correspondances quand 2 requêtes partagent un plongement mais diffèrent par leurs paramètres. |
| Sécurité | JWT, RBAC, nettoyage des données personnelles | 3 rôles : lecteur (kg_query, contract_query), analyste (tous les solveurs), administrateur (accès complet). Un nettoyeur par expressions régulières retire les adresses de courriel, les numéros de téléphone et les identifiants nationaux avant qu'un texte entre dans le contexte du modèle. JWT HS256, expiration à 60 minutes. |
| Intégration et déploiement continus | Chaîne GitHub Actions à 6 tâches | ruff, black, mypy, bandit et tests unitaires pytest → tsc côté interface → tests d'intégration → équipe rouge (promptfoo) → construction et envoi vers ACR avec OIDC → repository_dispatch vers le dépôt de déploiement. |
| Affinage | DPO et QLoRA sur Llama 3.1-8B | Traces de production LangSmith converties en paires de préférence. LoRA r=16 sur toutes les couches de projection. En file pour une exécution sur GPU L4 chez Lightning AI. |
Ce que j'ai appris
La frontière la plus difficile à faire tenir se trouve là où la sortie du modèle devient du code exécutable, bien avant le graphe d'agents. Une fois que chaque appel de solveur a franchi un contrat MCP typé en Pydantic et que chaque requête Cypher est venue d'un gabarit en liste blanche, des classes entières de défaillances ont cessé d'apparaître. Le modèle peut encore mal lire une demande et choisir le mauvais outil, ce qu'une personne rattrape. Il reste incapable de former une requête malformée.
La garde sur les tokens discriminants du cache sémantique a été le correctif dont je n'avais pas prévu le besoin. Les plongements BGE effacent la différence entre « allouer 400 unités » et « allouer 1000 unités » : la similarité cosinus dépasse 0,95, et les bonnes réponses n'ont rien en commun. La garde compare les nombres et les codes d'entité des deux requêtes, et refuse la réponse en cache quand ils diffèrent.