Streaming-Ausgabe: astream und stream_mode
Verantwortung
Pregel.astream / Pregel.stream sind die vom kompilierten Graphen nach außen gelegten Einstiegspunkte für die Ausführung. Man wirft eine Eingabe hinein, die PregelLoop startet und läuft Runde für Runde; dabei werden die in jedem Superstep (superstep) erzeugten Daten gemäß dem vom Aufrufer angegebenen stream_mode zu Stream-Events projiziert, in eine Queue gesteckt und vom Aufrufer iterativ konsumiert. ainvoke / invoke sind im Wesentlichen astream / stream, die zu einem Einzelwert konvergieren – der Rumpf von Pregel.invoke ist for chunk in self.stream(...) und nimmt das letzte Stück (Pregel.invoke body:3891).
Die Verantwortung dieser Schicht ist es, «Ausführung» und «Ausgabeform» zu entkoppeln: Die Ausführung folgt weiterhin der BSP-Schleife von Pregel, die Ausgabeform wird durch stream_mode bestimmt – derselbe Graph, einmal ausgeführt, kann verschiedene Projektionen liefern: values (voller Zustand pro Schritt), updates (inkrementell pro Schritt), messages (LLM-Token-Strom), custom (selbstdefinierte Ausgaben innerhalb eines Knotens), checkpoints (Checkpoint-Events), tasks (Task-Beginn/-Ende), debug (alles); es kann sogar eine Liste übergeben werden, um mehrere Modi gleichzeitig zu beziehen.
Entwurfsmotivation
- Ausführung nicht blockieren: Die Ausgabe läuft über eine Queue;
runner.tick/atickschreibt ein Event, sobald ein Task fertig ist, und die äußerefor-Schleife nimmt es bei der Iteration ab, statt erst den gesamten Graphen fertig zu rechnen und dann alles auf einmal zurückzugeben. Für LLM-Token-Streams (messages) ist das zwingend – der Nutzer möchte Token für Token sehen, während sie generiert werden. - Mehrere Projektionen aus derselben Ausführung: Innerhalb eines
runner.atick(...)entstehen mehrere Events; die Funktion_output(_output:4184) führt nur Filterung/Verteilung durch und rechnet den Graphen nicht erneut.stream_mode=["values","updates"]führt nicht zu zwei Durchläufen. - Einheitlicher Subgraph-Namensraum: Mit
subgraphs=Trueerhalten Events einennamespace-Präfix ((ns, mode, payload)); der Eltern-Graph sieht die Events aus dem Inneren des Subgraphen. Das Tagging passiert beim Verlassen der Queue durch_output, der Subgraph öffnet keinen eigenen Stream. - v2 typisiert: In der neuen Version formt
version="v2"Events zu einem Dict{"type": mode, "ns": ..., "data": ..., "interrupts": ...}um, was die programmgesteuerte Verarbeitung erleichtert. Imvalues-Modus wird__interrupt__zusätzlich in ein separatesinterrupts-Feld ausgekoppelt.
Schlüsseldateien
Pregel class:450— DiePregel-Klassendefinition; alle Streaming-Einstiegspunkte liegen hier.Pregel.stream:2655— Synchroner Streaming-Einstieg; die Signatur listet allestream_mode-Optionen auf.Pregel.astream:3063— Asynchroner Streaming-Einstieg; die eigentliche asynchrone Hauptschleife steht in seinem Rumpf.Pregel.invoke:3836— Synchrone Konvergenz; internfor chunk in self.stream(...), nimmt den letztenvalues-Chunk.sync main loop:2964— Synchrone Hauptschleife:while loop.tick()→runner.tick→loop.after_tick().async main loop:3437— Asynchrone Hauptschleife: dreiteilig wie oben,runner.atickersetzt die synchrone Variante._output:4184— Dequeue-Filter-Funktion; entscheidet anhand vonstream_mode/print_mode/subgraphs, wie jedes Event yielded wird.GraphRunStream:31— Synchrone, aufrufergetriebene Stream-Verpackung; diefor-Schleife ist die Pump, ohne Hintergrund-Thread.AsyncGraphRunStream:304— Asynchrones Gegenstück; mehrere Projektionen (run.values/run.messages) teilen sich eine Pump.stream_mode attr:709—Pregelhat den Defaultstream_mode="values", kann durchstream(stream_mode=...)überschrieben werden.
Datenfluss
Das Skelett der asynchronen Hauptschleife steht im Rumpf von astream (async main loop:3437); der dreiteilige Aufbau entspricht dem auf der Pregel-Engine-Seite Beschriebenen, mit dem Unterschied, dass zwischen den Teilen _output die Inhalte der Queue an den Aufrufer weitergibt:
while loop.tick():
for task in await loop.amatch_cached_writes():
loop.output_writes(task.id, task.writes, cached=True)
async for _ in runner.atick(
[t for t in loop.tasks.values() if not t.writes],
timeout=self.step_timeout,
get_waiter=get_waiter,
schedule_task=loop.aaccept_push,
):
# emit output
for o in _output(
stream_mode,
print_mode,
subgraphs,
stream.get_nowait,
asyncio.QueueEmpty,
version,
_output_mapper,
_state_mapper,
):
yield o
loop.after_tick()
await aemit_graph_lifecycle_events(loop)
# wait for checkpoint
if durability_ == "sync":
await cast(asyncio.Future, loop._put_checkpoint_fut)runner.atick schreibt intern die von jedem Task erzeugten Writes zurück ins loop und legt gleichzeitig die Tripel (ns, mode, payload) in den stream (eine asyncio.Queue); wenn die äußere async for-Schleife die Kontrolle erhält, ruft _output stream.get_nowait() auf, um alle bereiten Events aus der Queue zu entleeren, filtert sie nach stream_mode und yieldet sie an den Nutzer. Die Kernlogik von _output (_output:4184): Holt (ns, mode, payload), gibt bei mode in print_mode zunächst eine print-Kopie aus und yieldet nach außen nur, wenn mode in stream_mode – print_mode ist also ein Debug-Schalter «nur anzeigen, nicht senden» und beeinflusst den tatsächlich geyieldeten Inhalt nicht.
Das folgende Diagramm durchläuft den stream-Aufruf vom Einstieg bis zum Verlassen der Queue:
Grenzen und Fehler
- Default stream_mode: Bei
stream_mode=Noneist der Defaultvalues, wenn der Aufruf als Subgraph erfolgt (CONFIG_KEY_TASK_IDim Config), andernfalls giltself.stream_mode(stream_mode default:2740), damit der Subgraph die Eltern-Ausgabe nicht mit seinem Defaultupdatesverschmutzt. - Schrittzahl überschritten: Nach dem Verlassen der Hauptschleife wird
loop.statusgeprüft; ist esout_of_steps, wirft der CodeGraphRecursionErrorund empfiehlt,recursion_limitzu erhöhen (out_of_steps:3002); der StatusdrainingwirftGraphDrainedund gibt die Kontrolle an das externeRunControlzurück. - Mehrere Modi gleichzeitig: Wenn
stream_modeeine Liste ist, wird jedes Event zum Tupel(mode, payload); zusammen mitsubgraphs=Trueergibt sich(ns, mode, payload)(tuple mode:4240), und der Aufrufer muss das Tupel entsprechend entpacken. - Vererbungsbehandlung im messages-Modus: Bei
stream_mode="messages"undversion="v1"wird der vom übergeordneten Layer vererbte v2-messages-Handler entfernt, damit der v1-Stream nicht in das Content-Block-Event-Protokoll geroutet wird (strip v2 handler:2773); der v1-Handler bleibt aber erhalten, damit beisubgraphs=Trueinneremessages-Events nach außen sichtbar sind. - durability und Checkpoint-Synchronisation: Bei
durability="sync"wartet die Hauptschleife nach jedem Schritt aufawait loop._put_checkpoint_fut, also darauf, dass der Checkpoint persistiert ist, bevor der nächste Schritt beginnt;"async"ist der Default – Persistenz und nächster Schritt laufen parallel;"exit"persistiert nur beim Beenden. - Experimentelles v3 stream_events:
stream_events(version="v3")akzeptiert keine Parameterstream_modeundsubgraphs, diese werden intern vom Mux übernommen; explizites Übergeben wird von_reject_v3_invariant_kwargsabgelehnt (_reject_v3_invariant_kwargs:387).
Zusammenfassung
astream / stream verpacken die BSP-Schleife von Pregel als iterierbare Schnittstelle; stream_mode bestimmt die Ausgabeprojektion, und _output ist der einzige Engpass für die Dequeue-Filterung. Für Ausführungsdetails siehe /pregel/pregel und /pregel/loop; für Multi-Modus-Mux siehe libs/langgraph/langgraph/stream/_mux.py. Siehe offizielle Dokumentation: LangGraph 文档 · README.