18. Multi-Agent-Systeme — Das Supervisor-Muster
Denken Sie an den Kundenservice-Agent zurück, den wir in Kapitel 16 erstellt haben. Er hatte genau ein Tool — die Bestellabfrage — sodass er den Versandstatus meldete, wenn ein Kunde nach einer Bestellung fragte. Mit nur einem Tool gab es schlicht keine Möglichkeit, das falsche auszuwählen.
Nehmen wir nun an, wir bauen diesen Agent zu einem echten Produktionsdienst aus. Die Bestellabfrage allein reicht nicht. Wir bräuchten Bestellstornierung, Sendungsverfolgung, Adressänderungen, Umtauschanfragen, Rücksendeanfragen, Prüfungen der Rückerstattungsberechtigung, Rückerstattungsabwicklung, Bestandsprüfungen, Gutscheinausgabe, Punkteabfrage, Erstellung von Support-Tickets und mehr. Die Mechanik ist einfach: Immer weitere Tools hinzufügen und Geschäftsregeln an den System-Prompt anhängen.
Doch je mehr sich Tools und Regeln anhäufen, desto mehr treten drei Probleme zutage.
-
Die Tool-Auswahl wird weniger genau. In jedem Durchgang liest das Modell alle Tool-Beschreibungen und entscheidet, welches es aufruft. Wenn Sie Tools hinzufügen, die dieselben Eingaben erwarten und sich im Zweck überschneiden — wie Umtauschanfrage und Rücksendeanfrage — steigt die Wahrscheinlichkeit, das falsche auszuwählen.
-
Der Kontext füllt sich mit Informationen, die Sie gerade nicht benötigen. Selbst während der Abwicklung einer Rückerstattung reisen das Schema des Bestandsprüfungs-Tools, die Regeln zur Gutscheinausgabe, das Verfahren für Umtauschanfragen und alles andere bei jedem Aufruf mit. Sie verschicken jedes Mal den vollständigen Satz von Tool-Schemas und die Geschäftsregeln jeder Domäne. Wenn der Kontext mit für die aktuelle Aufgabe irrelevantem Material überfüllt ist, werden die wichtigen Teile begraben und die Genauigkeit leidet.
-
Änderungen werden schwierig. Eine einzelne Rückerstattungsregel anzupassen bedeutet, einen Prompt zu bearbeiten, in dem die Regeln jeder Domäne miteinander verwoben sind, und Sie können nicht sicher sein, dass sich die Änderung nicht auf Umtausch oder Versand auswirkt. Wenn verschiedene Domänen verschiedene Eigentümer haben, wächst das Problem nur.
Dieses Kapitel lehrt einen Weg, damit umzugehen. Anstatt einem einzelnen Agent immer mehr Tools und Regeln aufzubürden, teilen wir die Arbeit in einen Agent pro Domäne auf. Wir setzen das Kundenservice-Szenario aus Kapitel 16 fort und bauen ein Team, bestehend aus einem Agent, der nur Bestellabfragen bearbeitet, und einem Agent, der nur Rückerstattungen bearbeitet. Jeder hat seine eigenen Tools und seinen eigenen Prompt. Dann fügen wir noch einen Agent hinzu, um sie zu dirigieren — dieser Dirigent wird als Supervisor bezeichnet. Eine Konstellation mit mehreren Agents wie dieser ist ein Multi-Agent-System, und die Anordnung, bei der ein Supervisor die übrigen befehligt, ist das Supervisor-Muster.
Multi-Agent-Systeme sind allerdings nicht kostenlos. Da der Supervisor bei jedem Schritt entscheiden muss, an welchen Worker er delegiert, gibt es mehr LLM-Aufrufe, und das bedeutet mehr Latenz und mehr Kosten. Wenn Sie also nicht so viele Tools und Regeln haben, besteht überhaupt kein Grund, zu einem Multi-Agent-System zu greifen.
Hier ist der Plan. In 18.1 betrachten wir, was ein Multi-Agent-System ist und wie es funktioniert. In 18.2 bauen wir die Worker-Agents. In 18.3 bauen wir einen Supervisor von Hand mit dem Command-Objekt von LangGraph. In 18.4 verpacken wir die Worker als Tools und bauen dasselbe Team mit weitaus weniger Code neu auf.
18.1) Multi-Agent-Systeme verstehen
18.1.1) Was ein Multi-Agent-System ist
Entwerfen wir einen Agent, der die Anfrage bearbeiten kann: „Meine Bestellung ist nie angekommen — wenn etwas nicht stimmt, erstatte sie zurück.“ Wir könnten ihn mit allem bauen, was wir bisher gelernt haben: Hängen Sie ein Bestellabfrage-Tool und ein Rückerstattungs-Tool an einen einzigen Agent, und lassen Sie den Agent in einer Schleife laufen, bis die Arbeit erledigt ist. Ein einzelner Agent fragt die Bestellung ab, bestätigt, dass die Zustellung fehlgeschlagen ist, sieht dieses Ergebnis, fordert die Rückerstattung an und verfasst die endgültige Antwort.
Ein Multi-Agent-System ist eine Struktur, in der mehrere Agents diese Arbeit untereinander aufteilen. Sie teilen die Agents nach Domäne auf und stellen einen Supervisor über sie, und jeder Agent hält nur die Tools, die er benötigt. Der Agent, den wir gerade skizziert haben, teilt sich zum Beispiel natürlich in einen Bestellabfrage-Agent und einen Rückerstattungs-Agent auf. Der Supervisor ruft zunächst den Bestellabfrage-Agent auf, um den Versandstatus zu prüfen; wenn er zurückgemeldet bekommt, dass die Zustellung fehlgeschlagen ist, ruft er den Rückerstattungs-Agent auf, um die Rückerstattung abzuwickeln; dann sammelt er beide Ergebnisse, um dem Kunden zu antworten. Was früher ein Agent war, der Tools nacheinander aufrief, ist zu einem Supervisor geworden, der Agents nacheinander aufruft.
Wenn es nur zwei Tools gäbe, gäbe es keinen Grund, so aufzuteilen. Ein Agent würde genügen, und einen Supervisor hinzuzufügen würde nur LLM-Aufrufe hinzufügen. Doch wie wir in der Einleitung gesehen haben, kann ein Agent, sobald es viele Tools gibt, das falsche auswählen, sein Kontext füllt sich mit für die vorliegende Aufgabe irrelevanten Informationen, und sein Prompt wird schwer änderbar.
Die Aufteilung löst diese Probleme. Der Bestellabfrage-Agent sieht nur ein paar bestellungsbezogene Tools, sodass die Auswahl aus einer Liste von Dutzenden auf die Auswahl aus einer Handvoll schrumpft. Sein Prompt hält nur Bestellabfrageregeln, sodass die Rückerstattungsrichtlinie, Gutscheinbedingungen und andere mit der Bestellabfrage nicht zusammenhängende Angelegenheiten seinen Kontext nicht füllen. Und wenn Sie eine Rückerstattungsregel ändern müssen, berühren Sie nur den Rückerstattungs-Agent, sodass die Änderung die anderen nicht erreicht.
Der Bestellabfrage-Agent und der Rückerstattungs-Agent hier sind nichts Besonderes. Sie sind dieselbe Art von Agent, die Sie in Kapitel 16 gebaut haben. Sie erstellen sie, indem Sie ein Modell, Tools und einen Prompt an create_agent übergeben, und Sie rufen sie mit invoke auf. Sie decken nur weniger Fläche ab.
Was macht also der Supervisor? Der Bestellabfrage-Agent und der Rückerstattungs-Agent wissen nicht, dass der andere existiert. Jeder erledigt nur seine eigene Aufgabe; keiner weiß, wer zuerst gehen sollte. Im obigen Beispiel war es das Urteil des Supervisors, zuerst die Bestellabfrage aufzurufen und — erst nach Sichtung ihres Ergebnisses — die Rückerstattung aufzurufen.
Wer ruft wen, und wann. Das ist es, was wir Orchestrierung nennen.
18.1.2) Das Supervisor-Muster
Das Supervisor-Muster ist eine Struktur, in der ein einziger zentraler Supervisor mehrere Worker-Agents orchestriert. Es folgt diesen Regeln:
- Der Supervisor erledigt die Arbeit niemals selbst. Er fragt keine Bestellungen ab und wickelt keine Rückerstattungen ab. Er entscheidet nur, an wen er übergibt, und sammelt dann die zurückgegebenen Ergebnisse zu einer endgültigen Antwort.
- Worker rufen einander niemals auf. Der Bestellabfrage-Agent ruft niemals direkt den Rückerstattungs-Agent auf. Jeder Pfad läuft durch den Supervisor.
- Nur der Supervisor spricht mit dem Kunden. Worker berichten dem Supervisor, nicht dem Kunden.
Wie entscheidet der Supervisor also, welchen Agent er aufruft? Das LLM entscheidet. Der Supervisor liest die gesamte bisherige Konversation und urteilt. Wenn er den Bestellstatus noch nicht kennt, ruft er den Bestellabfrage-Agent auf; sobald bestätigt ist, dass die Zustellung fehlgeschlagen ist und eine Rückerstattung nötig ist, ruft er den Rückerstattungs-Agent auf.
Der Supervisor wiederholt dieses Urteil, bis die Anfrage des Benutzers abgeschlossen ist. Er ruft einen Agent auf, empfängt einen Bericht, liest die Konversation erneut, nun da der Bericht hinzugefügt wurde, und entscheidet, welchen Agent er als Nächstes aufruft.
Diese Schleife hat dieselbe Struktur wie diejenige, die Sie in Kapitel 14 gebaut haben.
- Think — die bisherige Konversation lesen und entscheiden, welcher Agent aufgerufen wird.
- Act — den gewählten Agent ausführen.
- Observe — den Bericht des Agents nehmen und ihn der Konversation hinzufügen.
In Kapitel 14 haben Sie Tools aufgerufen; hier rufen Sie Agents auf. Das ist das Einzige, was sich ändert.
Beachten Sie, wie die Pfeile zum Supervisor zurückkehren. Wenn ein Worker fertig ist, berichtet er dem Supervisor, und der Supervisor liest diesen Bericht und entscheidet über den nächsten Zug.
Der Supervisor ist es, der die Schleife beendet. Sobald er urteilt, dass die Anfrage des Kunden vollständig bearbeitet ist, erstellt er die endgültige Antwort und stoppt.
18.2) Die Worker-Agents erstellen
Bauen wir die beiden Worker-Agents, die wir in 18.1 entworfen haben: einen Bestellabfrage-Worker, der den Bestellstatus prüft, und einen Rückerstattungs-Worker, der Rückerstattungen abwickelt. Den Supervisor bauen wir in 18.3.
Ein Worker-Agent ist einfach der gewöhnliche Agent, den Sie in Kapitel 16 gebaut haben. Wir bauen sie schnell mit create_agent.
Zunächst richten wir die Bestelldaten ein, die die beiden Worker gemeinsam nutzen.
ORDERS = {
"12345": {"item": "Kabellose Ohrhörer", "amount": 89,
"status": "in_transit", "status_text": "Unterwegs (Ankunft morgen)"},
"67890": {"item": "Mechanische Tastatur", "amount": 129,
"status": "delivered", "status_text": "Zugestellt"},
"24680": {"item": "Noise-Cancelling-Kopfhörer", "amount": 249,
"status": "delivery_failed", "status_text": "Zustellung fehlgeschlagen (zurückgesendet — Empfänger nicht gefunden)"},
}Der Bestellabfrage-Worker hat nur ein Tool.
from langchain.tools import tool
from langchain.agents import create_agent
@tool
def get_order_status(order_id: str) -> str:
"""Ruft den Artikel, den gezahlten Betrag und den Versandstatus für eine Bestellnummer ab."""
order = ORDERS.get(order_id)
if order is None:
return f"Bestellung {order_id} nicht gefunden."
return (f"Bestellung {order_id}: {order['item']}, "
f"${order['amount']:,}, Status: {order['status_text']}")
order_agent = create_agent(
name="order_expert",
model="openai:gpt-5.4-mini",
tools=[get_order_status],
system_prompt=(
"Sie sind ein Spezialist für Bestellabfragen. Fragen Sie den Bestellstatus ab und antworten Sie.\n"
"Geben Sie die Bestellnummer, den Artikel, den gezahlten Betrag und den Versandstatus in Ihrer endgültigen Antwort an.\n"
"Beurteilen Sie nicht, ob eine Rückerstattung gerechtfertigt ist, und erwähnen Sie Rückerstattungen in keiner Weise. Ihre Rolle ist ausschließlich die Bestellabfrage und Statusberichterstattung."
),
)Der Rückerstattungs-Worker hat zwei Tools: eines, das entscheidet, ob eine Bestellung rückerstattungsberechtigt ist, und eines, das die Rückerstattung tatsächlich abwickelt.
@tool
def check_refund_eligibility(order_id: str) -> str:
"""Ermittelt, ob eine Bestellung für eine Rückerstattung berechtigt ist. Nur Bestellungen mit fehlgeschlagener Zustellung qualifizieren sich."""
order = ORDERS.get(order_id)
if order is None:
return f"Bestellung {order_id} nicht gefunden."
if order["status"] == "delivery_failed":
return f"Bestellung {order_id} ist für eine Rückerstattung berechtigt (Grund: fehlgeschlagene Zustellung)."
return f"Bestellung {order_id} ist nicht für eine Rückerstattung berechtigt (aktueller Status: {order['status_text']})."
@tool
def issue_refund(order_id: str) -> str:
"""Wickelt eine Rückerstattung ab. Bestätigen Sie die Berechtigung immer mit check_refund_eligibility, bevor Sie dies aufrufen."""
order = ORDERS.get(order_id)
if order is None:
return f"Bestellung {order_id} nicht gefunden."
return (f"Rückerstattung abgeschlossen: ${order['amount']:,} für Bestellung {order_id} "
f"werden innerhalb von 3–5 Werktagen zurückerstattet. (Genehmigungsnummer: RF-{order_id})")
refund_agent = create_agent(
name="refund_expert",
model="openai:gpt-5.4-mini",
tools=[check_refund_eligibility, issue_refund],
system_prompt=(
"Sie sind ein Spezialist für die Rückerstattungsabwicklung.\n"
"Bestätigen Sie die Berechtigung immer zuerst mit check_refund_eligibility und rufen Sie erst "
"dann issue_refund auf.\n"
"Wenn Sie eine Rückerstattung abgewickelt haben, geben Sie den Betrag und die Genehmigungsnummer in Ihrer endgültigen Antwort an.\n"
"Wenn die Bestellung nicht berechtigt ist, wickeln Sie sie nicht ab — melden Sie stattdessen den Grund."
),
)Wir haben beiden Workern einen name gegeben. Dieser Name ist das, was create_supervisor in 18.5 als Node-Namen und als Namen des Handoff-Tools verwendet.
Vier Dinge, die der System-Prompt eines Workers benötigt
Der System-Prompt eines Workers wird aus vier Elementen aufgebaut — Prinzipien, die Anthropic beim Aufbau seines eigenen Multi-Agent-Forschungssystems destilliert hat. Wenn diese schwach sind, verdoppeln Worker Arbeit, lassen Aufgaben unerledigt oder finden die benötigten Informationen nicht.
| Element | order_agent | refund_agent |
|---|---|---|
| Rolle | Sie sind ein Spezialist für Bestellabfragen | Sie sind ein Spezialist für die Rückerstattungsabwicklung |
| Tool-Anleitung | Bestätigen Sie immer zuerst mit check_refund_eligibility und rufen Sie erst dann issue_refund auf | |
| Ausgabeformat | Geben Sie die Bestellnummer, den Artikel, den gezahlten Betrag und den Versandstatus in Ihrer endgültigen Antwort an | Wenn Sie eine Rückerstattung abgewickelt haben, geben Sie den Betrag und die Genehmigungsnummer in Ihrer endgültigen Antwort an |
| Aufgabengrenze | Beurteilen Sie nicht, ob eine Rückerstattung gerechtfertigt ist, und erwähnen Sie Rückerstattungen in keiner Weise. Ihre Rolle ist ausschließlich die Bestellabfrage und Statusberichterstattung. | Wenn die Bestellung nicht berechtigt ist, wickeln Sie sie nicht ab — melden Sie den Grund |
Rolle legt in einem Satz fest, wer dieser Worker ist. Seine Identität mit „Sie sind ein Spezialist für Bestellabfragen“ festzuzurren hält das Modell auf seine eigene Aufgabe fokussiert und lässt es weniger wahrscheinlich in fremdes Gebiet abschweifen.
Tool-Anleitung. Schreiben Sie diese, wenn es etwas gibt, das das Tool-Schema allein nicht vermitteln kann — zum Beispiel die Reihenfolge und Bedingungen, unter denen Tools verwendet werden sollten. Wenn es über das Schema hinaus nichts hinzuzufügen gibt, können Sie sie weglassen.
Ausgabeformat und Aufgabengrenze sind in einem Multi-Agent-Umfeld von großer Bedeutung.
Ausgabeformat. Die endgültige Antwort eines Workers ist keine Antwort an den Kunden — sie ist ein Bericht, der dem Supervisor vorgelegt wird. Alles, was dort nicht geschrieben steht, erreicht den Supervisor nie. Wenn ein Worker einen Betrag mit einem Tool abfragt, ihn aber in seiner endgültigen Antwort weglässt, hat der Supervisor keine Möglichkeit, davon zu erfahren.
Aufgabengrenze. Dies ist der Umfang, der definiert, wie weit ein Worker gehen darf und was er nicht tun darf. Der Bestellabfrage-Worker sollte nur abfragen — niemals zurückerstatten. Deshalb haben wir ihm kein Rückerstattungs-Tool gegeben. Doch das Tool vorzuenthalten reicht für sich allein nicht aus, denn das Modell kann immer noch ganz ohne Tool sagen: „Die Zustellung ist fehlgeschlagen, also veranlasse ich für Sie eine Rückerstattung.“ Wenn dieser Satz den Supervisor erreicht, könnte der Supervisor annehmen, dass eine Rückerstattung bereits im Gange ist, und den Rückerstattungs-Worker nie aufrufen. Deshalb sagt der Prompt auch: „Beurteilen Sie nicht, ob eine Rückerstattung gerechtfertigt ist, und erwähnen Sie Rückerstattungen in keiner Weise“, was ihn davon abhält, Rückerstattungen auch nur in Worten zur Sprache zu bringen.
Modelle auswählen
Wir verwenden gpt-5.4-mini für die Worker und gpt-5.4 für den Supervisor. Ein Worker erledigt die einfache Aufgabe, ein paar Tools in einer festgelegten Reihenfolge aufzurufen, sodass ein kleines Modell völlig ausreicht. Der Supervisor muss die gesamte Konversation lesen und urteilen, wen er als Nächstes aufruft, sodass er ein größeres Modell benötigt. Ein Modell pro Agent auswählen zu können, abgestimmt auf die Schwierigkeit seiner Aufgabe, ist ein weiterer Vorteil der Aufteilung.
Nun können wir den Supervisor hinzufügen.
18.3) Den Supervisor von Hand bauen
Bauen wir einen Supervisor von Hand. In der Praxis werden Sie meist den Ansatz verwenden, bei dem das Framework dies für Sie erledigt (behandelt im nächsten Abschnitt), aber um zu verstehen, was unter der Haube geschieht, müssen Sie ihn einmal selbst bauen.
Wie wir in 18.1 gesehen haben, ist das, was der Supervisor tut, eine einzelne Schleife: einen Worker aufrufen, den Bericht lesen und entscheiden, wen er als Nächstes aufruft — oder ob er stoppt — immer wieder.
18.3.1) Handoffs und Command
Damit sich diese Schleife dreht, muss die Kontrolle zwischen dem Supervisor und den Workern hin und her wechseln. Der Supervisor übergibt die Kontrolle — „dieser Worker geht als Nächstes“ — und wenn der Worker fertig ist, übergibt er die Kontrolle zurück an den Supervisor. Diese Weitergabe der Kontrolle von einem Node an einen anderen wird als Handoff bezeichnet.
Ein Handoff benötigt zwei Informationsteile: wohin gegangen wird (das Ziel) und was mitgegeben wird (die Nutzlast). Das Ziel ist immer erforderlich; die Nutzlast wird nur einbezogen, wenn es etwas zu übergeben gibt. In LangGraph gibt ein Node beides an, indem er ein Command zurückgibt.
from typing import Literal
from langgraph.graph import MessagesState
from langgraph.types import Command
def some_node(state: MessagesState) -> Command[Literal["refund_expert_proxy"]]:
return Command(
goto="refund_expert_proxy", # wohin: der als Nächstes auszuführende Node
update={"messages": [...]}, # was: der Bericht des Workers (die dem State hinzugefügte Nutzlast)
)goto ist das Ziel; update ist die Nutzlast. Der Rückgabetyp-Hinweis Command[Literal["refund_expert_proxy"]] listet im Voraus die Ziele auf, zu denen dieser Node gehen kann. Wie dieses Command tatsächlich verwendet wird, sehen wir im nächsten Abschnitt, wo wir den Supervisor-Node und die Worker-Proxies bauen.
18.3.2) Die Supervisor-Schleife bauen
Die Struktur selbst ist einfach. Wir erstellen einen Supervisor-Node und so viele Worker-Proxies, wie wir benötigen. Ein Worker-Proxy ist ein Node, der seinen zugewiesenen Worker-Agent im Auftrag des Supervisors aufruft. Der Einstiegspunkt ist der Supervisor, und jeder Worker-Proxy geht, sobald er fertig ist, zum Supervisor zurück — und bildet so die Schleife. Das Command, das jeder Node zurückgibt, ist das, was entscheidet, wohin die Kontrolle als Nächstes geht.
Durchgezogene Linien sind Handoffs zwischen Nodes (goto); gepunktete Linien sind ein Worker-Proxy, der seinen Worker-Agent aufruft (invoke). Der Supervisor übergibt an einen Worker-Proxy, und der Worker-Proxy kehrt, sobald er fertig ist, zum Supervisor zurück. Wenn der Supervisor FINISH entscheidet, verlässt er die Schleife zu END.
Verwandeln wir dieses Bild nun in Code. Zunächst die Route-Klasse. Route ist das Schema, um die Antwort des Supervisors als strukturierte Antwort zu empfangen, wenn wir das LLM fragen, welchen Worker-Proxy es als Nächstes aufrufen soll. Wenn das LLM in freiformatiger natürlicher Sprache antwortete, wäre es schwer zu erkennen, welcher Worker-Proxy ausgeführt werden soll. Route hält, welcher Worker-Proxy als Nächstes aufgerufen wird (next) und den Grund, warum es so entschieden hat (reason).
from typing import Literal
from pydantic import BaseModel, Field
class Route(BaseModel):
reason: str = Field(description="Der Grund für diese Entscheidung.")
next: Literal["order_expert_proxy", "refund_expert_proxy", "FINISH"] = Field(
description="Der als Nächstes auszuführende Worker-Node. FINISH, wenn die Anfrage vollständig bearbeitet ist."
)Es gibt einen Grund, warum reason vor next deklariert wird. Strukturierte Ausgabe wird in der Reihenfolge erzeugt, in der die Felder im Schema erscheinen, sodass das LLM mit reason zuerst seine Begründung schreibt, bevor es einen Worker auswählt. Zuerst begründen und dann entscheiden ergibt eine bessere Wahl. Kehren Sie die Reihenfolge um — setzen Sie next an die erste Stelle — und das LLM wählt einen Worker aus, bevor es überhaupt begründet hat, und liefert dann eine Rationalisierung nach, die zu einer Wahl passt, die es möglicherweise bereits falsch getroffen hat.
Als Nächstes der Supervisor-Node.
from typing import Literal
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.types import Command
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
supervisor_llm = ChatOpenAI(model="gpt-5.4")
SUPERVISOR_PROMPT = (
"Sie sind der Supervisor eines Kundenservice-Teams. Sie verwalten zwei Worker:\n"
"- order_expert_proxy: fragt den Bestellstatus ab.\n"
"- refund_expert_proxy: prüft die Rückerstattungsberechtigung und wickelt Rückerstattungen ab.\n"
"Um zu entscheiden, ob eine Rückerstattung nötig ist, müssen Sie zuerst den Bestellstatus prüfen.\n"
"Weisen Sie immer nur einem Worker zu und antworten Sie mit FINISH, sobald die Anfrage vollständig bearbeitet ist."
)
def supervisor(
state: MessagesState,
) -> Command[Literal["order_expert_proxy", "refund_expert_proxy", "__end__"]]:
messages = [{"role": "system", "content": SUPERVISOR_PROMPT}, *state["messages"]]
decision = supervisor_llm.with_structured_output(Route).invoke(messages)
print(f"[supervisor] → {decision.next} ({decision.reason})")
if decision.next == "FINISH":
final = supervisor_llm.invoke(
[{"role": "system", "content": "Verfassen Sie basierend auf der bisherigen Konversation eine Antwort an den Kunden."},
*state["messages"]]
)
return Command(goto=END, update={"messages": [final]}) # an END übergeben
return Command(goto=decision.next) # an den Worker-Proxy übergebenBisher gaben Nodes nur den geänderten State zurück. Doch der Supervisor-Node gibt ein Command zurück. Wenn ein Node ein Command zurückgibt, tut LangGraph zwei Dinge: Es wendet den Inhalt von update auf den State an und führt als Nächstes den in goto benannten Node aus. Im obigen Code setzen wir den aus decision.next gezogenen Worker-Proxy-Namen in goto, sodass der vom LLM gewählte Node — decision.next — ausgeführt wird.
Als Nächstes die Worker-Proxies. Ein Worker-Proxy erledigt seine Aufgabe und gibt die Kontrolle an den Supervisor zurück — weshalb er mit goto="supervisor" übergibt.
Die Aufgabe eines Worker-Proxys ist einfach: seinen Worker-Agent mit invoke aufrufen.
def order_expert_proxy(state: MessagesState) -> Command[Literal["supervisor"]]:
result = order_agent.invoke(state)
last = result["messages"][-1]
return Command(
goto="supervisor",
update={"messages": [HumanMessage(content=last.content, name="order_expert")]},
)
def refund_expert_proxy(state: MessagesState) -> Command[Literal["supervisor"]]:
result = refund_agent.invoke(state)
last = result["messages"][-1]
return Command(
goto="supervisor",
update={"messages": [HumanMessage(content=last.content, name="refund_expert")]},
)Beachten Sie, dass wir nur die endgültige Nachricht des Workers herausziehen und diese an den Supervisor übergeben. Der Supervisor benötigt nur die Schlussfolgerung; er muss nicht wissen, wie oft der Worker intern Tools aufgerufen hat.
18.3.3) Den Graph verdrahten und ausführen
Wir registrieren die drei Nodes und verbinden nur den Einstiegspunkt mit dem Supervisor. Jeder andere Zug wird durch das Command jedes Nodes entschieden, sodass keine weiteren Kanten nötig sind.
builder = StateGraph(MessagesState)
builder.add_node("supervisor", supervisor)
builder.add_node("order_expert_proxy", order_expert_proxy)
builder.add_node("refund_expert_proxy", refund_expert_proxy)
builder.add_edge(START, "supervisor")
team = builder.compile()
result = team.invoke(
{"messages": [HumanMessage(
content="Bestellung 24680 ist immer noch nicht angekommen. Wenn etwas nicht stimmt, erstatten Sie sie bitte zurück."
)]},
config={"recursion_limit": 15},
)Ausgabe:
[supervisor] → order_expert_proxy (Muss den Bestellstatus prüfen, bevor über eine Rückerstattung entschieden wird.)
[supervisor] → refund_expert_proxy (Fehlgeschlagene Zustellung ist bestätigt, also die Rückerstattungsberechtigung prüfen und abwickeln.)
[supervisor] → FINISH (Bestellprüfung und Rückerstattungsabwicklung sind beide abgeschlossen.)Der Supervisor routete zuerst zur Bestellabfrage. Als der Bericht zurückkam, dass die Zustellung fehlgeschlagen war, routete er zur Rückerstattung, und als der Rückerstattung-abgeschlossen-Bericht eintraf, beendete er. Er wählte jedes nächste Ziel, indem er den Bericht des vorherigen Agents las.
Der Worker-Proxy übergibt den gesamten gemeinsam genutzten State über invoke(state) an seinen Worker, sodass jeder Worker die gesamte bisherige Konversation sieht. Mit nur zwei Workern ist das in Ordnung, aber während Worker und Konversation wachsen, endet jeder Worker damit, Nachrichten zu lesen, die nichts mit seiner eigenen Aufgabe zu tun haben. Wir lösen das im nächsten Abschnitt anders.
18.4) Über Tools an Worker delegieren
Nachdem wir in 18.3 die Interna des Supervisors von Hand gebaut haben, bauen wir nun dasselbe Team auf die für echte Projekte empfohlene Weise neu auf. Dieser Ansatz benötigt keine neue API. Sie verwandeln jeden Worker mit @tool in ein Tool und geben diese Tools einem Supervisor-Agent. Wir bauen den Supervisor-Agent einfach mit create_agent.
Die Kernidee passt in einen Satz: der Supervisor ist selbst nur ein Agent, und jeder Worker wird zu einem Tool, das der Supervisor aufruft.
So betrachtet hat der Supervisor dieselbe Struktur wie der Tool-aufrufende Agent, den Sie in Kapitel 16 gebaut haben. Er hält nur High-Level-Tools, die Agents aufrufen, anstelle von Low-Level-Tools wie get_order_status.
18.4.1) Worker als Tools verpacken
Wir verwenden den order_agent und den refund_agent aus 18.2 unverändert. Alles, was wir tun, ist, jeden in eine @tool-Funktion zu verpacken.
from langchain.tools import tool
# order_agent und refund_agent sind die Worker aus 18.2
@tool
def lookup_order(request: str) -> str:
"""Fragt Artikel, gezahlten Betrag und Versandstatus einer Bestellung ab. Verwenden Sie dies, wenn Sie den Status einer Bestellung wissen müssen.
Eingabe: eine natürlichsprachige Abfrageanfrage (z. B. „Nenne mir den Versandstatus von Bestellung 24680“).
"""
print("[tool call] lookup_order")
print(f" request: {request}")
result = order_agent.invoke({"messages": [{"role": "user", "content": request}]})
return result["messages"][-1].content
@tool
def handle_refund(request: str) -> str:
"""Prüft die Rückerstattungsberechtigung und wickelt eine Rückerstattung ab. Verwenden Sie dies, wenn der Kunde eine Rückerstattung möchte und Sie den Bestellstatus bereits bestätigt haben.
Eingabe: eine natürlichsprachige Rückerstattungsanfrage. Geben Sie die Bestellnummer und den durch die Abfrage bestätigten Versandstatus an.
(z. B. „Bestellung 24680 ist im Status fehlgeschlagene Zustellung. Wickle eine Rückerstattung ab, falls berechtigt.“)
"""
print("[tool call] handle_refund")
print(f" request: {request}")
result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})
return result["messages"][-1].contentDrei Dinge haben sich geändert.
-
Die Tool-Beschreibungen ersetzen die Routing-Logik. In 18.3 haben wir
SUPERVISOR_PROMPTund dasRoute-Schema von Hand geschrieben, um dem Supervisor seine Liste von Workern und seine Wahlmöglichkeiten mitzuteilen. Hier erledigen die Tool-Docstrings diese Aufgabe. Das LLM des Supervisors liest die Tool-Beschreibungen und entscheidet, wann was aufgerufen wird. -
Jeder Worker startet aus einem sauberen Kontext. Nebeneinander mit 18.3 gestellt, ist der Unterschied klar.
python# 18.3 (manueller Graph): übergibt den gesamten gemeinsam genutzten State result = refund_agent.invoke(state) # 18.4 (Tool-Delegation): übergibt nur die vom Supervisor verfasste Aufgabenbeschreibung result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})In 18.4 erhält der Rückerstattungs-Worker nur einen Satz, der seine Aufgabe beschreibt. Er sieht nie die ursprüngliche Formulierung des Kunden, die Begründung des Supervisors oder die Tool-Aufruf-Historie eines anderen Workers. Selbst mit zehn Workern und hundert Gesprächsrunden ist der Kontext jedes Workers immer noch nur diese eine Aufgabenbeschreibung.
-
Im Gegenzug übernimmt der Supervisor die Aufgabe, Informationen weiterzugeben. Da ein Worker die Gesprächshistorie nicht sehen kann, muss alles, was er benötigt, vom Supervisor in den
request-String gepackt werden. Deshalb legt der Docstring vonhandle_refundfest: „Geben Sie die Bestellnummer und den durch die Abfrage bestätigten Versandstatus an.“ Ohne diese Anweisung könnte der Supervisor nur"Wickle eine Rückerstattung ab"übergeben, sodass der Rückerstattungs-Worker im Unklaren bleibt, um welche Bestellung es überhaupt geht.
18.4.2) Den Supervisor zusammensetzen und ausführen
Bei diesem Ansatz ist auch der Supervisor nur ein Agent. Kein Command, kein Route-Schema nötig.
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
TOOL_SUPERVISOR_PROMPT = (
"Sie sind der Supervisor eines Kundenservice-Teams.\n"
"Um zu entscheiden, ob eine Rückerstattung nötig ist, müssen Sie zuerst den Bestellstatus prüfen.\n"
"Erledigen Sie die Arbeit nicht selbst — delegieren Sie an die Worker.\n"
"Die Worker können diese Konversation nicht sehen. Wenn Sie delegieren, packen Sie alles, was sie benötigen, in die Anfrage.\n"
"Wenn die gesamte Arbeit erledigt ist, fassen Sie die Ergebnisse der Worker zu einer Antwort an den Kunden zusammen."
)
supervisor_agent = create_agent(
model="openai:gpt-5.4",
tools=[lookup_order, handle_refund],
system_prompt=TOOL_SUPERVISOR_PROMPT,
)
result = supervisor_agent.invoke(
{"messages": [HumanMessage(
content="Bestellung 24680 ist immer noch nicht angekommen. Wenn etwas nicht stimmt, erstatten Sie sie bitte zurück."
)]}
)
print("\n\n[final response]")
print(result["messages"][-1].content)Das Endergebnis ist dasselbe wie in 18.3.
[tool call] lookup_order
request: Der Kunde sagt, dass Bestellung 24680 noch nicht angekommen ist. Um zu entscheiden, ob eine
Rückerstattung nötig ist, gib mir bitte den Artikel, den gezahlten Betrag und den aktuellen Versandstatus für Bestellung 24680.
[tool call] handle_refund
request: Bestellung 24680 sind Noise-Cancelling-Kopfhörer, $249, und ihr Versandstatus ist
bestätigt als „Zustellung fehlgeschlagen (zurückgesendet — Empfänger nicht gefunden)“. Der Kunde
fordert eine Rückerstattung, also prüfe die Berechtigung und wickle sie ab, falls berechtigt.
[final response]
Ich habe nachgeprüft, und Bestellung 24680 war im Status fehlgeschlagene Zustellung (zurückgesendet — Empfänger nicht gefunden).
Sie war für eine Rückerstattung berechtigt, und ich habe die Rückerstattung abgeschlossen.
- Artikel: Noise-Cancelling-Kopfhörer
- Rückerstattungsbetrag: $249
- Rückerstattungs-Genehmigungsnummer: RF-24680
Je nach Zahlungsmethode dauert es üblicherweise ein paar Werktage, bis die Rückerstattung gutgeschrieben wird.Aber schauen Sie sich die Tool-Aufrufe an, die der Supervisor unterwegs getätigt hat — dort zeigt sich der Unterschied zu 18.3. Schauen Sie sich den request von handle_refund an. Der Supervisor hat das frühere Abfrageergebnis zusammengefasst und die Aufgabenbeschreibung selbst geschrieben. Der Rückerstattungs-Worker erhält nur diesen einen Satz. Wo 18.3 dem Worker die gesamte Konversation übergab und ihn darin herumsuchen ließ, pickt hier der Supervisor genau das heraus, was benötigt wird, und gibt das weiter.
18.4.3) Dem Supervisor mit einem Checkpointer Gedächtnis hinzufügen
Zu sagen, dass der Supervisor ein gewöhnlicher Agent ist, bedeutet, dass das Checkpointing, das Sie in Kapitel 17 gelernt haben, unverändert an ihm funktioniert.
from langgraph.checkpoint.memory import InMemorySaver
supervisor_agent = create_agent(
model="openai:gpt-5.4",
tools=[lookup_order, handle_refund],
system_prompt=TOOL_SUPERVISOR_PROMPT,
checkpointer=InMemorySaver(), # der Checkpointer kommt nur auf den Top-Level-Agent
)
config = {"configurable": {"thread_id": "cs-1"}}
supervisor_agent.invoke(
{"messages": [HumanMessage(content="Wo ist meine Bestellung 12345?")]},
config,
)
follow_up = supervisor_agent.invoke(
{"messages": [HumanMessage(content="Wie viel hat die gekostet?")]},
config,
)
print(follow_up["messages"][-1].content)Ausgabe:
Ihre Bestellung 12345, die Kabellosen Ohrhörer, kostete $89.Der Supervisor liest die in der Folgefrage korrekt als Bestellung 12345 aus der vorherigen Runde. Der Checkpointer funktioniert am Supervisor genauso.
Hängen Sie keinen Checkpointer an die Worker-Agents (
order_agent,refund_agent) an. Wenn Sie es tun, trägt ein Worker die Ergebnisse seines vorherigen Aufrufs in den aktuellen hinein, was die vorliegende Aufgabe stören kann. Ohne einen läuft ein Worker auf nichts als der Anfrage des Supervisors. Für Subagenten ist dies der empfohlene Standard.
18.5) create_supervisor: Die Form, der Sie in Legacy-Code begegnen
In bestehenden Codebasen und älteren Tutorials werden Sie auf den create_supervisor-Helfer aus dem Paket langgraph-supervisor stoßen. Geben Sie ihm eine Liste von Agents und einen Prompt, und er baut den gesamten Supervisor-Graph für Sie.
from langgraph_supervisor import create_supervisor
from langchain_openai import ChatOpenAI
workflow = create_supervisor(
agents=[order_agent, refund_agent], # jeder Agent muss einen Namen gesetzt haben
model=ChatOpenAI(model="gpt-5.4"),
prompt="Weise Bestellprüfungen order_expert und Rückerstattungen refund_expert zu.",
)
app = workflow.compile()create_supervisor ist ein Helfer, der ein Supervisor-Team in einem einzigen Funktionsaufruf zusammensetzt (der Handoff-Ansatz aus 18.3). Verwenden Sie ihn jedoch nicht in neuen Projekten. Es ist Legacy, das LangChain nicht mehr empfiehlt, und intern hängt es von create_react_agent ab, das in v1 als veraltet markiert wurde (Entfernung ist für v2 geplant). LangChain empfiehlt den tool-basierten Supervisor, den Sie in 18.4 gelernt haben.
In diesem Kapitel haben wir einen Agent, der innerhalb eines einzelnen Graphs gelebt hatte, zu einem Team erweitert — aufgeteilt nach Domäne, orchestriert von einem Supervisor. Wir haben dasselbe Team auf drei Weisen gebaut: den manuellen Command-Graph in 18.3, den Tool-Delegations-Ansatz in 18.4 und den Legacy-create_supervisor-Helfer in 18.5. Für echte Arbeit machen Sie den Tool-Delegations-Ansatz zu Ihrem Standard.