BetterAgentRouting: Drei Router-Architekturen für Multi-Agent-Workflows im Vergleich

AgentensystemBetterAgentRouting · 2025
C#.NET 8Microsoft Semantic Kernel 1.30OpenAI gpt-4o-miniMicrosoft.Extensions.ConfigurationMicrosoft.Extensions.LoggingDotNetEnv

Projekt-Übersicht

BetterAgentRouting ist eine Konsolenanwendung in C# auf .NET 8, die ich 2025 gebaut und im Repository LargeLanguageModelsPublic veröffentlicht habe. Sie beantwortet eine einzige Frage: Wie entscheidet ein Router in einem Multi-Agent-System, welcher Agent die nächste Nachricht bekommt, ohne einen laufenden Workflow bei jeder Nebenfrage abzureißen?

Statt eine Antwort zu behaupten, stellt das Projekt drei Router-Architekturen nebeneinander, die gegen dieselben drei Agenten laufen: einen Buchungsagenten, einen Support-Agenten und einen Wissensagenten. Alle drei nutzen Microsoft Semantic Kernel 1.30 und standardmäßig gpt-4o-mini als Modell. Die Ansätze lassen sich im selben Programm per Menü umschalten, sodass sich identische Gesprächsverläufe direkt vergleichen lassen.

Die Grundidee und die drei Ansätze habe ich im Blogartikel Multi-Agent-Routing: Wer kommt wann dran? beschrieben. Diese Fallstudie geht einen Schritt weiter: Sie zeigt die Architekturentscheidungen im Code, ihre Kosten in Modellaufrufen und die Prinzipien, die ich daraus für robuste Router mitnehme.

Ausgangslage

Das Problem tritt in fast jedem Agentensystem auf, das mehr als einen Spezialisten hat. Ein klassischer Router bewertet jede Nachricht für sich: Er fragt, welcher Agent am besten zur aktuellen Eingabe passt, und gibt sie dorthin weiter. Für einzelne Fragen funktioniert das. Bei mehrstufigen Abläufen bricht es.

Ein typischer Verlauf: Der Nutzer beginnt eine Flugbuchung, der Buchungsagent fragt nach Reisedatum und Budget. Dann kommt eine Zwischenfrage zum Wetter am Zielort. Ein zustandsloser Router sieht nur diese eine Nachricht, schickt sie an den Wissensagenten, und die Buchung ist aus Sicht des Systems verwaist. Die nächste Antwort des Nutzers („Dann nehme ich den Flug am Dienstag“) landet womöglich beim falschen Agenten.

Die Gegenrichtung ist genauso problematisch. Ein Router, der grundsätzlich beim aktuellen Agenten bleibt, ignoriert echte Themenwechsel. Wer mitten in der Buchung schreibt „Vergiss das, ich komme nicht mehr in mein Konto“, will den Support und keinen weiteren Buchungsschritt.

Die Anforderung lautete deshalb: Der Router braucht Gedächtnis über den Workflow-Zustand, eine Schwelle, die einen Wechsel erschwert, aber nicht verhindert, und eine nachvollziehbare Begründung für jede Entscheidung.

Architektur

Alle drei Ansätze teilen sich eine gemeinsame Schicht. Das Interface IRouter definiert eine einzige Methode RouteAsync, die Eingabe, Konversationszustand und Agentenliste erhält und ein RoutingResult mit gewähltem Agenten, Konfidenz, Begründung und den Scores aller Alternativen zurückgibt. Jeder Ansatz hat einen eigenen Orchestrator, der Sitzungen verwaltet, den Router aufruft, die Antwort des Agenten einholt und den Zustand fortschreibt.

flowchart TD
    U["Nutzereingabe"] --> P["Program.cs: Menü und Chat-Schleife"]
    P --> O1["Orchestrator Ansatz 1"]
    P --> O2["Orchestrator Ansatz 2"]
    P --> O3["Orchestrator Ansatz 3"]
    O1 --> R1["StatefulRouter + ConversationStatePlugin"]
    O2 --> R2["AgentOwnershipRouter"]
    O3 --> R3["StickySessionRouter + ExitDetectionPlugin"]
    R1 --> A["Agenten: Booking, Support, Knowledge"]
    R2 --> SM["Selbstverwaltete Agenten mit Ownership-Entscheidung"]
    R3 --> A
    SM --> A
    A --> K["Semantic Kernel: IChatCompletionService"]
    K --> M["OpenAI gpt-4o-mini"]
    O1 --> S["ConversationState: Agent, WorkflowStage, Turns, Kontext, Historie"]
    O2 --> S
    O3 --> S

Der zentrale Datentyp ist ConversationState. Er hält den aktuellen Agenten, eine WorkflowStage (Idle, BookingInProgress, SupportInProgress, KnowledgeQuery, Completed), die Zahl der Turns insgesamt und mit dem aktuellen Agenten, ein Kontext-Dictionary für Workflow-Daten wie den Buchungsstatus und die Gesprächshistorie. Alle drei Router lesen diesen Zustand; sie unterscheiden sich darin, wer am Ende über einen Wechsel entscheidet.

Technische Umsetzung

Gemeinsame Agentenbasis

Die Agenten erben von einer abstrakten Klasse BaseAgent, die direkt auf dem IChatCompletionService von Semantic Kernel aufsetzt. Jeder Agent liefert einen eigenen System-Prompt, eine Beschreibung seiner Fähigkeiten und eigene Regeln dafür, wann er die Kontrolle behalten will und wann sein Workflow abgeschlossen ist. In die Antwort fließen die letzten fünf Turns der Historie ein, damit der Agent den Faden behält, auch wenn zwischendurch ein anderer Agent geantwortet hat.

Die Eignung eines Agenten für eine Nachricht bestimmt ein eigener, kurzer Modellaufruf: Das Modell bekommt Agentenbeschreibung, Workflow-Status und Eingabe und soll eine Zahl zwischen 0 und 1 zurückgeben. Fällt die Antwort aus dem Rahmen, greift ein Fallback von 0,5.

Der Buchungsagent führt zusätzlich eine einfache Zustandsmaschine im Kontext: initial, flight_search, selection, confirmation, completed. Die Übergänge werden über Schlüsselwörter in Eingabe und Antwort erkannt. Eine echte Buchungsschnittstelle gibt es nicht; das Projekt ist eine Testumgebung für das Routing, nicht für Reisebuchungen.

Ansatz 1: Stateful Routing mit dynamischer Schwelle

Der erste Router behält die Entscheidung zentral, macht sie aber zustandsabhängig. Er bewertet alle drei Agenten, gibt dem aktuellen Agenten einen Bonus und verlangt von einem Herausforderer, diesen Vorsprung plus eine dynamische Schwelle zu überbieten. Die Schwelle berechnet das ConversationStatePlugin:

double baseThreshold = 0.6;
if (state.WorkflowStage == WorkflowStage.BookingInProgress)
    baseThreshold += 0.2;   // Höhere Hürde während einer Buchung
else if (state.WorkflowStage == WorkflowStage.SupportInProgress)
    baseThreshold += 0.15;
if (state.CurrentAgentTurnCount > 2)
    baseThreshold += Math.Min(0.2, state.CurrentAgentTurnCount * 0.05);
return Math.Min(0.95, baseThreshold);  // Wechsel bleibt möglich

Die Obergrenze von 0,95 ist eine bewusste Entscheidung: Die Schwelle darf hoch werden, aber nie unüberwindbar. Zusätzlich gibt es eine harte Sperre. Steht der Buchungsstatus auf confirmation, erlaubt IsWorkflowSwitchAllowed keinen Wechsel mehr; eine angefangene Bestätigung wird zu Ende geführt.

Der Bonus für den aktuellen Agenten setzt sich aus drei Teilen zusammen: einem Anteil je nach Workflow (0,3 bei Buchung, 0,2 bei Support, 0,1 bei Wissensfragen), einem Anteil für die Gesprächslänge mit diesem Agenten (bis 0,2) und einem Stabilitätsbonus von 0,1 in den ersten beiden Turns nach einem Wechsel. Letzterer verhindert, dass der Router direkt nach einer Übergabe wieder zurückspringt.

Die Stärke dieses Ansatzes ist die Transparenz. Jede Entscheidung enthält den angepassten Score, die Schwelle und die Scores aller Alternativen, und die Konsole gibt sie nach jeder Nachricht aus.

Ansatz 2: Agent-Ownership

Der zweite Ansatz verlagert die Entscheidung zu den Agenten. Agenten, die das Interface ISelfManagedAgent implementieren, beantworten bei jeder neuen Nachricht selbst die Frage, ob sie die Kontrolle behalten wollen, und schlagen gegebenenfalls einen Nachfolger vor. Der Router folgt diesem Votum:

if (ownershipDecision.KeepControl && ownershipDecision.Confidence > 0.5)
{
    result.SelectedAgent = currentAgent;   // Agent behält die Kontrolle
    return result;
}
if (!string.IsNullOrEmpty(ownershipDecision.SuggestedNextAgent))
{
    var suggested = availableAgents.FirstOrDefault(
        a => a.Name == ownershipDecision.SuggestedNextAgent);
    if (suggested != null) { /* Übergabe an den vorgeschlagenen Agenten */ }
}

Gibt der aktuelle Agent ab, ohne einen Nachfolger zu nennen, fragt der Router alle Agenten nach Eignung und Anspruch und sortiert in fester Reihenfolge: zuerst Agenten, die die Kontrolle wollen, dann nach Priorität, dann nach Eignungsscore. Die Priorität ist der Hebel für kritische Phasen. Der Buchungsagent setzt sie in der Bestätigungsphase auf 10 und erzwingt KeepControl, während einer laufenden Buchung auf 5.

Die Ownership-Entscheidung liefert das Modell in einem Pipe-getrennten Format (DECISION|CONFIDENCE|REASON|NEXT_AGENT). Die Architekturentscheidung dahinter: Der Agent kennt seinen eigenen Workflow am besten, also soll er über das Ende entscheiden. Der Preis ist, dass jede Ownership-Analyse ein eigener Modellaufruf ist.

Ansatz 3: Sticky Sessions mit Exit-Erkennung

Der dritte Ansatz kehrt die Beweislast um. Der Router bleibt beim aktuellen Agenten, bis ein Ausstiegssignal erkannt wird. Das ExitDetectionPlugin prüft in vier Stufen, von billig nach teuer:

  1. Explizite Ausstiegsphrasen wie „andere Frage“, „abbrechen“ oder „vergiss es“ per Textsuche (Konfidenz 0,95).
  2. Abschlusssignale wie „vielen Dank“ oder „problem gelöst“ (Konfidenz 0,8).
  3. Eine Modellanalyse, die zusätzlich den Typ des Ausstiegs liefert: explizit, Abschluss, Themenwechsel oder Frustration. Sie zählt erst ab einer Konfidenz von 0,6.
  4. Eine Kontextanalyse: Mehr als sieben Turns mit demselben Agenten ohne Abschluss werten als mögliche Frustration.

Dazu kommt eine Stagnationserkennung: Sind unter den letzten vier Eingaben höchstens zwei verschiedene, wird die Sitzung ebenfalls aufgelöst. Erst wenn eine dieser Bedingungen greift, bewertet der Router alle Agenten neu.

if (exitDetection.ExitDetected && exitDetection.Confidence > _exitThreshold)
    return true;                       // Ausstiegssignal stark genug
if (currentAgentScore < 0.1)
    return true;                       // Agent passt überhaupt nicht mehr
var shouldBreak = await _exitDetector.ShouldBreakStickinessAsync(
    state.History.LastOrDefault()?.UserInput ?? "", state, currentAgentScore);
return shouldBreak;                    // sonst: Sitzung bleibt kleben

Dieser Ansatz ist am einfachsten zu erklären und am stabilsten in langen Abläufen. Die Schlüsselwortlisten sind deutschsprachig und müssen für andere Sprachen oder Domänen angepasst werden.

Qualitätssicherung

Automatisierte Tests enthält das Repository nicht. Die Prüfung ist explorativ und auf Nachvollziehbarkeit ausgelegt: Nach jeder Nachricht zeigt die Konsole Routing-Begründung, Konfidenz, Routing-Zeit, die Scores aller Alternativen und bei Ansatz 3 die erkannten Ausstiegssignale. Mit stats lassen sich Sitzungsmetriken abrufen, etwa die Zahl der Übergaben, die durchschnittliche Dauer einer Sticky Session oder die erkannten Themenwechsel. Weil alle drei Ansätze dieselben Agenten und dasselbe Modell verwenden, lassen sich identische Gesprächsverläufe nacheinander durch alle drei Router schicken.

Gemessene Latenzen oder Trefferquoten habe ich nicht im Repository festgehalten. Was sich aus dem Code exakt ablesen lässt, ist die Zahl der Modellaufrufe pro Nachricht, und die unterscheidet sich deutlich:

Ansatz Modellaufrufe pro Nachricht (laut Code)
Stateful Routing 4: drei Eignungsbewertungen und die Antwort
Agent-Ownership 3, wenn der aktuelle Agent behält; bis 9 bei einer Neuverteilung
Sticky Sessions etwa 6 bei bestehender Sitzung; bis 10, wenn sie aufgelöst wird

Bei Ansatz 3 kommen zur Antwort die Modellstufen der Ausstiegs- und Themenwechsel-Erkennung hinzu. Für eine Vergleichsumgebung, in der jede Prüfung einzeln sichtbar sein soll, ist das gewollt; in einem produktiven System lohnt es sich, solche Prüfungen in möglichst wenigen Aufrufen zu bündeln.

Ergebnis

Das Projekt liefert keine Siegerarchitektur, sondern eine Entscheidungshilfe, die sich am Code belegen lässt:

  • Stateful Routing ist der beste Ausgangspunkt, wenn Workflows klare Phasen haben und Entscheidungen erklärbar sein müssen. Die Logik liegt an einer Stelle, die Parameter sind einzeln justierbar, und der Ansatz kommt mit vier Modellaufrufen pro Nachricht aus.
  • Agent-Ownership passt, wenn Agenten komplexe eigene Abläufe haben und selbst am besten wissen, wann sie fertig sind. Die Prioritätslogik schützt kritische Phasen zuverlässig, dafür ist das Verhalten schwerer vorherzusagen, weil die Entscheidung im Modell liegt.
  • Sticky Sessions sind die einfachste Lösung für lange, zusammenhängende Aufgaben. Sie sind stabil, reagieren aber nur auf Signale, die vorher als Phrase oder Muster bekannt sind oder die das Modell als Ausstieg erkennt.

Das Repository ist öffentlich, sodass sich die drei Ansätze mit einem eigenen API-Schlüssel nachvollziehen und an eigene Agenten anpassen lassen.

Was ich dabei gelernt habe

  • Zahlen aus Modellantworten kulturunabhängig parsen. Scores kommen als Text zurück, und in .NET hängt das Parsen ohne Kulturangabe von der Systemeinstellung ab: Unter einer deutschen Kultur wird aus „0.8“ schnell eine 8. Bei Werten zwischen 0 und 1 verschwimmen so die Unterschiede, ohne dass eine Fehlermeldung erscheint. CultureInfo.InvariantCulture oder gleich strukturierte Ausgaben mit JSON-Schema schließen das aus.
  • Strukturierte Ausgaben statt Freitextformaten. Pipe-getrennte Antworten sind schnell gebaut und gut lesbar, setzen aber voraus, dass das Modell das Format exakt einhält. Für den produktiven Einsatz sind strukturierte Ausgaben mit Schema die robustere Wahl, weil sie fehlende oder falsch platzierte Felder von vornherein ausschließen.
  • Feste Kennungen statt Namensvergleichen. Wenn Orchestrator und Agenten sich über Namen als Strings abstimmen, wird jede Umbenennung oder Vererbung zur stillen Fehlerquelle. Typen oder feste Kennungen machen diese Kopplung explizit und vom Compiler prüfbar.
  • Schlüsselwortsignale brauchen Wortgrenzen. Eine günstige Schlüsselwortstufe vor der Modellanalyse spart Aufrufe. Sie funktioniert am besten mit Wortgrenzen und einer kurzen Liste eindeutiger Signale, weil Wörter wie „egal“ auch in Sätzen vorkommen, die keinen Ausstieg meinen.
  • Die Kosten eines Routers stecken in den Aufrufen, nicht im Modell. Mit gpt-4o-mini ist jeder einzelne Aufruf günstig. Ob ein Router vier oder zehn Aufrufe pro Nachricht auslöst, bestimmt aber Latenz und Kosten stärker als die Wahl des Modells.

Technologien

  • Sprache und Laufzeit: C# mit Nullable Reference Types, .NET 8, Konsolenanwendung
  • Orchestrierung: Microsoft Semantic Kernel 1.30 (IChatCompletionService, ChatHistory, Plugins mit KernelFunction-Attributen, die die Router direkt aus C# aufrufen); das Paket Agents.Core ist referenziert, die Agenten sind aber eigene Klassen auf dem Chat-Service
  • Modell: OpenAI gpt-4o-mini (per Konfiguration austauschbar)
  • Konfiguration: Microsoft.Extensions.Configuration mit JSON-Datei und Umgebungsvariablen, DotNetEnv für .env-Dateien
  • Logging: Microsoft.Extensions.Logging mit Konsolenausgabe
  • Quellcode: öffentlich auf GitHub im Repository LargeLanguageModelsPublic