Agentischer ERP-Copilot für die Lieferkette
Eine Planerin stellt eine Frage zur Beschaffung in normaler Sprache. Das System rechnet exakt, statt zu schätzen, und hält bei allem über 10.000 $ für eine Freigabe an.
Das Problem
Planerinnen und Planer in der Lieferkette stellen ihre Fragen in Sätzen. Optimierungssolver lesen Restriktionsmatrizen. Setzen Sie ein Sprachmodell dazwischen und lassen es rechnen, liefert es plausible Antworten, die Geld kosten, wenn sie falsch sind. Ein Transportplan, der optimal aussieht und es nicht ist, taucht später als echte Rechnung auf.
Architektur
Ein LangGraph-StateGraph führt jede Anfrage durch 6 Knoten. Der Intent-Klassifikator bildet natürliche Sprache auf 1 von 10 abgegrenzten Intents ab (mcnf_solve, jsp_schedule, vrp_route, robust_allocate, meio_optimize, bullwhip_analyze, disruption_resource, kg_query, contract_query, multi_step). Ein mit DSPy kompilierter Klassifikator (MIPROv2) übernimmt zur Laufzeit von der Zero-Shot-Basis, sobald sein JSON-Artefakt vorliegt, und der Zero-Shot-Weg bleibt ohne Konfigurationsänderung verfügbar.
Der Think-on-Graph-Agent zieht die Entitäten heraus, wählt Relationen aus einer Whitelist und durchläuft Neo4j mit parametrisiertem Cypher. Der CRAG-Stack (dichte Suche mit BGE-large-en-v1.5, dünnbesetzte BM25-Suche, RRF bei k=60 und ein CrossEncoder-Reranker) holt Vertragsabschnitte und kennzeichnet jeden als Correct, Ambiguous oder Incorrect, bevor der Synthesizer ihn sieht. Ein zweistufiger semantischer Cache auf Redis überspringt bei einer Wiederholungsanfrage den ganzen LangGraph-Durchlauf: zuerst exakter SHA-256-Treffer, dann Kosinusähnlichkeit bei 0,95 mit einer Sperre auf unterscheidende Tokens für Zahlen und Entitätscodes.
Übersteigt ein Solver-Ergebnis 10.000 $, hält der Graph an einem Human-Gate-Knoten an, über interrupt() von LangGraph. Die Entscheidungs-UUID liegt in Redis mit einer Lebensdauer von 24 Stunden. Der Freigabe-Endpunkt setzt den Graphen mit der Entscheidung fort und sperrt eine doppelte Einreichung (HTTP 409 beim erneuten Senden). Im Browser sieht die Planerin ein Warnband mit den Schaltflächen Freigeben und Ablehnen, die sich zu einem grünen oder roten Ergebnis auflösen.
Der Stack
| Schicht | Werkzeug | Warum |
|---|---|---|
| Orchestrierung | LangGraph StateGraph | Graph aus 6 Knoten: classify → route → {kg_agent | contract_agent | solver_dispatch} → human_gate (bedingt) → synthesize. Ein MemorySaver-Checkpoint hält den pausierten Lauf über die Unterbrechung für den Menschen hinweg. |
| Intent-Klassifikation | Zero-Shot, DSPy optional | Der Hauptweg ist ein Aufruf mit strukturierter Ausgabe, 10 abgegrenzten Intents und einer Konfidenzschwelle. Die optionale Stufe ist eine mit DSPy MIPROv2 kompilierte ChainOfThought aus einem JSON-Artefakt, und der Zero-Shot-Weg bleibt bestehen, wenn DSPy fehlt oder das Artefakt nicht da ist. |
| Retrieval (CRAG) | BGE-large-en-v1.5, BM25, RRF | Dichte Suche mit 1024 Dimensionen über pgvector ivfflat und dünnbesetzte BM25Okapi-Suche, per RRF (k=60) zusammengeführt, dann ein CrossEncoder-Reranker und ein Relevanz-Gate des Sprachmodells, das jede Textstelle als Correct, Ambiguous oder Incorrect kennzeichnet. |
| Wissensgraph | Neo4j 5, Cypher aus Whitelist | Think-on-Graph: Entitäten extrahieren, Relationen aus einer Whitelist wählen, mit 1 Wiederholung durchlaufen, die sich bei leerem Teilgraphen selbst korrigiert. Kein roher Modelltext erreicht eine Datenbankabfrage. |
| Solver | OR-Tools, CVXPY, SciPy SLSQP | 7 exakte Solver: MCNF (lineare Programmierung), VRP (CVRP), JSP, robustes Min-Max (CVXPY), MEIO GSM (SLSQP, nichtkonvex), Bullwhip-Analyse und Ressourceneinsatz bei Störungen. Ein Solver pro Intent, und das Sprachmodell rechnet nichts davon. |
| MCP-Verträge | 6 FastMCP-Server | server_erp, server_kg, server_crag, server_ortools, server_cvxpy, server_scipy. Jeder Solver- und Datenbankaufruf überquert einen typisierten Pydantic-Vertrag, also wählt eine fehlerhafte Ausgabe schlimmstenfalls das falsche Werkzeug. |
| Semantischer Cache | Redis, BGE-Kosinus (0,95) | Zweistufige Suche: exakter SHA-256-Treffer, dann Kosinusähnlichkeit. Eine Sperre auf unterscheidende Tokens (Multimenge der Zahlen und Menge der Entitätscodes) blockiert falsche Treffer, wenn 2 Anfragen dasselbe Embedding teilen, aber in den Parametern abweichen. |
| Sicherheit | JWT, RBAC, Bereinigung personenbezogener Daten | 3 Rollen: Lesezugriff (kg_query, contract_query), Analyse (alle Solver), Administration (voller Zugriff). Ein Filter über reguläre Ausdrücke entfernt E-Mail-Adressen, Telefonnummern und Ausweisnummern, bevor Text in den Kontext des Modells gelangt. JWT HS256, Ablauf nach 60 Minuten. |
| CI/CD | GitHub-Actions-Pipeline aus 6 Jobs | ruff, black, mypy, bandit und pytest-Unittests → tsc im Frontend → Integrationstests → Red Team (promptfoo) → Build und Push nach ACR mit OIDC → repository_dispatch an das Deployment-Repository. |
| Fine-Tuning | DPO und QLoRA auf Llama 3.1-8B | Produktionstraces aus LangSmith, in Präferenzpaare überführt. LoRA r=16 auf allen Projektionsschichten. Eingereiht für einen Lauf auf einer L4-GPU bei Lightning AI. |
Was ich gelernt habe
Die Grenze, die am schwersten zu halten ist, liegt dort, wo die Ausgabe des Modells zu ausführbarem Code wird, weit vor dem Agentengraphen. Sobald jeder Solver-Aufruf einen typisierten MCP-Vertrag in Pydantic überquerte und jede Cypher-Abfrage aus einer Vorlage der Whitelist kam, verschwanden ganze Fehlerklassen. Das Modell kann eine Anfrage weiterhin falsch lesen und das falsche Werkzeug wählen, und ein Mensch fängt das auf. Eine fehlerhafte Abfrage kann es nicht bilden.
Die Sperre auf unterscheidende Tokens im semantischen Cache war die Korrektur, die ich nicht erwartet hatte. BGE-Embeddings verwischen den Unterschied zwischen „400 Einheiten zuteilen“ und „1000 Einheiten zuteilen“: die Kosinusähnlichkeit übersteigt 0,95, und die richtigen Antworten haben nichts gemeinsam. Die Sperre vergleicht die Zahlen und Entitätscodes beider Anfragen und verweigert die zwischengespeicherte Antwort, sobald sie abweichen.