Command:resume / goto / update — drei-in-eins Primitiv
Verantwortung
Command ist LangGraphs «graphübergreifendes Steuerprimitiv (control primitive)», definiert in libs/langgraph/langgraph/types.py(Command class:759). Eine Command-Instanz kann gleichzeitig drei Bedeutungen tragen — resume (Ausführung aus einer Unterbrechung (interrupt) wieder aufnehmen), goto (angeben, zu welchem Knoten als Nächstes gesprungen wird) und update (Werte in die Zustandskanäle (channel) des aktuellen Graphen schreiben). Knotenfunktionen geben Command(...) zurück, oder die aufrufende Seite verwendet es als Eingabe für graph.invoke(Command(...)); Pregel zerlegt es in pending writes und schreibt sie in die Kanäle.
Seine Position liegt zwischen Knoten und dem Pregel-Hauptloop: Knoten erzeugen Command, Pregel verwendet map_command(map_command:56), um seine Felder in (task_id, channel, value)-Tripel zu zerlegen und in pending writes zu schreiben. Die nächste Runde prepare_next_tasks sieht diese writes natürlich und调度t entsprechend — goto wird zu Send und in den TASKS-Kanal geschrieben, update schreibt direkt in den entsprechenden Kanal, resume geht über den RESUME-Kanal und wird von interrupt() behandelt.
Entwurfsmotivation
- Einheitliche Darstellung von «Seiteneffekten»: Neben dem Schreiben von Werten in den Zustand möchte ein Knoten eventuell auch «zu einem nicht-Standard-Nachfolgeknoten springen», «eine Nachricht an den Elterngraphen senden» oder «eine Unterbrechung (interrupt) wiederaufnehmen» — früher waren das verteilte APIs.
Commandpackt sie in einen einzigen Rückgabetyp; die Knotenfunktionssignatur-> Command[Literal["node_a", "node_b"]]erlaubt zudem Compile-Time-Prüfung der erlaubten Sprungziele. Command.PARENTfür graphübergreifende Hierarchie: Ein Knoten in einem Untergraph (subgraph) kannreturn Command(graph=Command.PARENT, update=...)ausführen und Updates in den Zustand des Elterngraphen schreiben —map_commandsieht beigraph == "__parent__"(PARENT guard:58)raise InvalidUpdateError("There is no parent graph"). Das tatsächliche Ankommen im Elterngraphen passiert im_firstdes Elterngraphen, wenn die vom Untergraphen gemeldeten writes verarbeitet werden.gotoakzeptiertSend:Command(goto=Send("node", arg))ist äquivalent dazu, eine PUSH-Aufgabe auszufächern;goto="node"ist äquivalent zugoto=Send("node", START)(löst den PULL dieses Knotens aus). So bettetCommanddie Fähigkeiten von conditional edges in den Rückgabewert des Knotens ein — ein separatesadd_conditional_edgesist nicht nötig.resumeals einziger Einstiegspunkt:Command(resume=...)ist der einzige Wiederaufnahmepfad fürinterrupt()— es unterstützt sowohl einen einzelnen WertCommand(resume="answer"), der automatisch an den einzigen ausstehenden Interrupt geroutet wird, als auch ein dictCommand(resume={interrupt_id: value, ...})zur gleichzeitigen Wiederaufnahme mehrerer Interrupts.updatezusammen mit Reducer:Command(update={"messages": [msg]})läuft weiterhin über den Reducer des Kanals — dermessages-Kanal verwendetoperator.add, dieses update ist also append und kein overwrite. DasOverwrite-Wrapper umgeht den Reducer(Overwrite:938)` und geht über den Direktschreibpfad des BinaryOperatorAggregate-Kanals.
Schlüsseldateien
Command class:759—@dataclass class Command(graph, update, resume, goto), generisch überN(Typ der goto-Knotennamen).Command fields:780— Definitionen und Docstrings der Feldergraph/update/resume/goto._update_as_tuples:791— zerlegtupdatein[(channel, value), ...]; unterstützt dict, list[tuple] und annotated dataclass.map_command:56— Kernfunktion, dieCommandin eine Sequenz von pending writes zerlegt.PARENT guard:58— wirft «kein Elterngraph»-Fehler, wenncmd.graph == Command.PARENT.goto as sends:62—cmd.gotowird in eine Liste vonSendzerlegt;Sendwird in denTASKS-Kanal geschrieben,strinbranch:to:<node>und löst START aus.resume write:72—cmd.resumewird in denRESUME-Kanal geschrieben und beim Wiedereintritt voninterrupt()gelesen.update writes:74—cmd.updatewird in(channel, value)-Paare zerlegt, in den entsprechenden Kanal geschrieben und über den Reducer verarbeitet.Command resume mapping:904— Wenn der ElterngraphCommand(resume=...)erhält, routet er resume an den Eingang des entsprechenden pending interrupt.Overwrite:938—Overwrite(value=...)-Wrapper; umgeht bei Verwendung mit einemBinaryOperatorAggregate-Kanal den Reducer und schreibt direkt.
Datenfluss
map_command ist der Kern der Command-Auflösung(map_command:56):
def map_command(cmd: Command) -> Iterator[tuple[str, str, Any]]:
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
if cmd.graph == Command.PARENT:
raise InvalidUpdateError("There is no parent graph")
if cmd.goto:
if isinstance(cmd.goto, (tuple, list)):
sends = cmd.goto
else:
sends = [cmd.goto]
for send in sends:
if isinstance(send, Send):
yield (NULL_TASK_ID, TASKS, send)
elif isinstance(send, str):
yield (NULL_TASK_ID, f"branch:to:{send}", START)
else:
raise TypeError(
f"In Command.goto, expected Send/str, got {type(send).__name__}"
)
if cmd.resume is not None:
yield (NULL_TASK_ID, RESUME, cmd.resume)
if cmd.update:
for k, v in cmd._update_as_tuples():
yield (NULL_TASK_ID, k, v)task_id ist immer NULL_TASK_ID — diese writes gehören keiner konkreten Aufgabe an, sondern sind graph-level «Outbound»: goto's Send geht in den TASKS-Kanal (die nächste Runde prepare_next_tasks sieht es und schedult es als PUSH-Aufgabe), goto's str geht in den branch:to:<node>-Kanal (ein spezialkanal für conditional edges; START schreiben bedeutet «den Eingang dieses Knotens auslösen»); resume geht in den RESUME-Kanal und wird beim Wiedereintritt des Knotens von interrupt() konsumiert; jedes (k, v) von update geht in den entsprechenden Zustandskanal und wird vom Reducer zusammengeführt.
Wenn Command als Eingabe für graph.stream(...) dient, wandelt PregelLoop._first(Command resume mapping:904)es zuerst in pending writes um, gruppiert diese dann nach task_id und schreibt sie incheckpoint_pending_writes; anschließend verarbeitet prepare_next_taskssie auf natürliche Weise. WennCommandals Rückgabewert eines Knotens dient, hängtattach_nodeden_get_updates-Mapper in die writersdes Knotens(<SrcLink path="libs/langgraph/langgraph/graph/state.py" lines="1431" label="attach_node"/>), extrahiert das update ausCommand._update_as_tuples()und schreibt es in die Zustandskanäle; goto und resume werden über den_control_branch-Mapper in den branch:to:-Kanal bzw. nach RESUME` geroutet.
Grenzen und Fehler
Command.PARENTfehlerhaft im Root-Graph:map_commandführt direktraise InvalidUpdateError("There is no parent graph")aus(PARENT guard:58)—Command.PARENT` ist nur in Untergraph-Knoten sinnvoll; ein Aufruf im Root-Graph fehler direkt.goto-Typeinschränkung:gotoakzeptiert nurSend/stroder eine Sequenz davon; andere Typen führen zuraise TypeError(goto type:69)` — direkte Übergabe von int, dict usw. ist nicht erlaubt.- resume ohne id erzwingt einzelnen pending:
Command(resume="single")mit mehreren pending interrupts führt zuraise RuntimeError(multi interrupt error:917); in diesem Fall mussCommand(resume={id: value, ...})verwendet werden. updateüber Reducer nicht direkt schreibbar:BinaryOperatorAggregate-Kanäle (wieAnnotated[list, operator.add]) gehen standardmäßig über den Reducer, mehrere updates werden zusammengeführt; um den Reducer zu umgehen,Overwrite(value=...)(Overwrite:938)verwenden — aber mehrfachesOverwriteauf denselben Kanal im selben Superstep (superstep) führt zuInvalidUpdateError`.Commandundentrypoint.finalsind gegenseitig exklusiv:@entrypoint-Funktionen gebenentrypoint.final(value=..., save=...)zurück und verwenden intern denEND- /PREVIOUS-Kanal;Commandgeht über den Steuerkanal — beides kann nicht gleichzeitig verwendet werden, eine entrypoint-Funktion sollte entwederCommandoderfinalzurückgeben, nicht mischen.- Leeres
Commandfehlerhaft:Wenn alle drei Felder vonCommand()None/()sind, liefertmap_commandnichts;_firsterkennt leere writes und führtraise EmptyInputError("Received empty Command input")aus(empty command:928).
Zusammenfassung
Command ist das Drei-in-eins-Primitiv für resume / goto / update; über map_command wird es in Kanal-writes zerlegt, die in der nächsten Runde von prepare_next_tasks verarbeitet werden. Es vereint conditional edges, interrupt resume und Zustandsaktualisierungen in einem einzigen Rückgabewert. Das zugehörige interrupt() siehe /interrupt/interrupt, die Send-Ausfächerungs-Semantik siehe /subgraph/send-command. Siehe offizielle Dokumentation: LangGraph 文档 · README.