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 exige le mot de passe du responsable, reprend le graphe avec la décision 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 | 6 nœuds : classify → {kg_agent | contract_agent | solver_dispatch} → human_gate (conditionnel) → synthesize, et une arête conditionnelle (route_by_intent) choisit la branche. 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. Ils exposent les solveurs et les bases de données à tout client MCP sous forme d'outils typés. Dans le graphe, solver_dispatch valide les paramètres extraits par le modèle contre un modèle d'entrée Pydantic propre à chaque solveur (SolveMcnfInput et 6 autres) avant toute exécution. |
| 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é | Mot de passe du responsable | Le point de terminaison d'approbation répond 403 à un mauvais mot de passe et 503 tant qu'aucun n'est défini. L'authentification JWT (HS256, expiration à 60 minutes), une table RBAC à 3 rôles et un nettoyeur de données personnelles par expressions régulières (adresses de courriel, numéros de téléphone, numéros de sécurité sociale américains) sont écrits et testés, mais pas encore branchés sur les routes de l'API. |
| 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 et pytest) → construction et envoi vers ACR → repository_dispatch vers le dépôt de déploiement. |
| Affinage | DPO et QLoRA sur Llama 3.1-8B | Des scripts convertissent les traces LangSmith en paires de préférence et entraînent avec LoRA r=16 sur toutes les couches de projection. Écrits, pas encore exécutés : l'entraînement attend 5 000 paires de préférence et un 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 entrée de solveur a dû passer un modèle Pydantic typé et que chaque requête Cypher a utilisé des relations en liste blanche avec des paramètres liés, 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.