LastValue: Der Standard-Kanal mit überschreibendem Schreiben
Verantwortung
LastValue ist die einfachste Unterklasse von BaseChannel: Sie speichert einen Wert und überschreibt ihn bei jedem Schreiben (LastValue class:20-21). Es ist genau der Kanal (channel), der verwendet wird, wenn ein state-Feld ohne jegliche Annotated-Markierung deklariert wird. Wenn du in einem StateGraph class State(TypedDict): count: int schreibst, steckt hinter dem count-Feld zur Kompilierzeit ein LastValue(int) (fallback LastValue:1857-1859).
Die zentrale Einschränkung ist: pro Schritt darf nur ein Wert geschrieben werden. Wenn update eine Sequenz mit Länge größer als 1 erhält, wird nicht zusammengeführt und nicht überschrieben, sondern direkt ein InvalidUpdateError geworfen (update:56-67). Die Fehlermeldung rät, Annotated[int, reducer] zu verwenden, um auszudrücken, wie mehrere gleichzeitige Schreibungen innerhalb eines Schritts gefaltet (fold) werden sollen. Diese Einschränkung ist keine technische, sondern eine semantische Entscheidung: Für skalare Zustände gibt es keine sinnvolle Merge-Semantik, und anstatt Werte stillschweigend zu verwerfen, wird explizit ein Fehler geworfen.
Außerdem enthält die Datei eine Variante LastValueAfterFinish (LastValueAfterFinish:81-152): Sie verhält sich wie LastValue, aber ein geschriebener Wert wird erst sichtbar, nachdem die gesamte Pregel-Ausführung finish() aufgerufen hat, und wird nach einmaligem Lesen geleert. Sie dient internen Kanälen für Signale wie «Unterbrechung» oder «Ende», die erst am Ende eines Laufs wirksam werden.
Entwurfsmotivation
- Überschreiben ist der einzig sinnvolle Default für skalare Zustände: Integer, Strings, Booleans haben keine Merge-Semantik. Bei einem Feld wie
count: intist es intuitiv, dass ein neuer Wert den alten überschreibt. Daher kompiliertStateGraphinfallback:1857Felder ohne reducer-Markierung zuLastValue. - Stilles Verwerfen vermeiden: Wenn zwei Knoten im selben Superstep (superstep) gleichzeitig
countschreiben, wähltLastValuenicht willkürlich einen Gewinner, sondern wirftINVALID_CONCURRENT_GRAPH_UPDATE(InvalidUpdateError:60-64). Die Fehlermeldung weist explizit auf «Use an Annotated key to handle multiple values» hin und führt den Nutzer zur korrekten Lösung — einen reducer hinzuzufügen, siehe /channels/topic-binop. MISSINGstattNone: Der Anfangswert istlanggraph._internal._typing.MISSING(value = MISSING:29), sodassNoneein legitimer gespeicherter Wert bleiben kann.get()wirft beiMISSINGeinEmptyChannelError(get:69-72). Das ist konsistent mit derBaseChannel-Standardimplementierung, aberis_availablewird hier auf eine einzige Zeileself.value is not MISSINGüberschrieben (is_available:74-75), um nicht jedes Mal try/except durchlaufen zu müssen.copyist billiger alsfrom_checkpoint: Obwohl die Basisklassecopy = from_checkpoint(checkpoint())als Standard vorgibt, istcopyhier überschrieben (copy:44-48) und teilt einfach dieselbevalue-Referenz — ein Skalar braucht ohnehin keine tiefe Kopie.from_checkpoint(from_checkpoint:50-54) entscheidet anhand desMISSING-Falls, ob zugewiesen wird.- Verzögerte Sichtbarkeit bei
LastValueAfterFinish: Bestimmte Steuersignale (z. B. goto beiCommand) dürfen nicht sofort vonprepare_next_tasksgesehen werden, sonst würden sie noch im selben Schritt selbst auslösen und einen Zyklus bilden.LastValueAfterFinishnutzt einfinished-Flag (finished flag:93-95),get()liefert nur beifinished=Trueden Wert zurück (get gated:145-148),finish()wird von Pregel beim Abschließen aufgerufen (finish:138-143), undconsume()leert den Wert, nachdem er einmal konsumiert wurde (consume:130-136).
Schlüsseldateien
LastValue class:20-25— Klassendefinition, generischBaseChannel[Value, Value, Value], d. h. Speicher-, Schreib- und Checkpoint-Typ sind alle dasselbeValue.__init__:27-29— Beim Konstruieren giltvalue = MISSING; erst nach einem Schreiben ist der Wert nicht mehrMISSING.ValueType / UpdateType:34-42— Beide gebenself.typzurück, also den bei der Deklaration angegebenen Typ.copy:44-48— Verwendet dievalue-Referenz direkt; Skalare brauchen keine tiefe Kopie.from_checkpoint:50-54— Rekonstruiert eine Instanz aus einem Checkpoint-Wert;MISSINGbedeutet «von leer beginnen».update:56-67— Kern-Einschränkung: leere Sequenz gibtFalsezurück, Länge größer als 1 wirftInvalidUpdateError, sonst wird der letzte Wert gespeichert undTruezurückgegeben.get / is_available:69-75— Lese-Schnittstelle; beiMISSINGwirftgeteinEmptyChannelError;is_availableist als einzeilige Prüfung überschrieben.LastValueAfterFinish:81-95— Variante:finished-Flag verschiebt die Sichtbarkeit bis nachfinish().LastValueAfterFinish.update:122-128— Setzt beim Schreibenfinishedzurück; ein neuer Schreibvorgang entzieht also die Sichtbarkeit des vorherigen Laufs.consume / finish / get:130-148— Die drei Hooks zusammen erzeugen die Semantik «nach einmaligem Lesen leeren».fallback LastValue:1857-1859— WennStateGraphbei der Kompilierung ein Feld ohne reducer-Markierung trifft, fällt es aufLastValue(annotation)zurück.
Datenfluss
Die gesamte Logik von LastValue.update besteht aus diesen Zeilen (update:56-67):
def update(self, values: Sequence[Value]) -> bool:
if len(values) == 0:
return False
if len(values) != 1:
msg = create_error_message(
message=f"At key '{self.key}': Can receive only one value per step. Use an Annotated key to handle multiple values.",
error_code=ErrorCode.INVALID_CONCURRENT_GRAPH_UPDATE,
)
raise InvalidUpdateError(msg)
self.value = values[-1]
return TrueBeachte, dass der dritte Zweig values[-1] statt values[0] schreibt — weil vorher bereits len != 1 geprüft wurde, läuft diese Zeile nur im Fall len == 1, sodass [-1] und [0] äquivalent sind. Dass eine leere Sequenz False zurückgibt, ist entscheidend: Das spielt mit dem Aufruf update(EMPTY_SEQ) für unveränderte Kanäle in apply_writes zusammen (EMPTY_SEQ update:329), sodass LastValue bei einem leeren Aufruf korrekt «unverändert» signalisiert.
Wie StateGraph bei der Kompilierung nicht markierte Felder auf LastValue abbildet, steht in field_to_channel:1840-1859:
if manager := _is_field_managed_value(name, annotation):
if allow_managed:
return manager
else:
raise ValueError(f"This {annotation} not allowed in this position")
elif channel := _is_field_channel(annotation):
channel.key = name
return channel
elif channel := _is_field_binop(annotation):
channel.key = name
return channel
fallback: LastValue = LastValue(annotation)
fallback.key = name
return fallbackReihenfolge: managed value (z. B. die spezielle Verwaltung in MessagesState) → explizite BaseChannel-Instanz → Annotated[..., reducer] (BinaryOperatorAggregate, siehe /channels/topic-binop) → falls nichts zutrifft, Rückfall auf LastValue.
Die gesamte Lese-/Schreibreihenfolge sieht so aus:
Grenzen und Fehler
- Gleichzeitige Mehrfachschreibungen werfen direkt einen Fehler:
multi-value error:59-64wirftInvalidUpdateErrormit FehlercodeINVALID_CONCURRENT_GRAPH_UPDATE. Das ist die häufigste Falle für Einsteiger: Zwei Knoten schreiben gleichzeitig in dasselbe Skalar-Feld. Abhilfe: das Feld in etwas wieAnnotated[int, lambda a, b: a + b]mit reducer umwandeln. - Leeres Schreiben gibt
Falsezurück:empty seq:57-58verhält sich konsistent mit demBaseChannel-Vertrag;apply_writesüberspringt daraufhin das Aktualisieren vonchannel_versions. __eq__vergleicht nur den Typ, nicht den Wert:__eq__:31-32gibtisinstance(value, LastValue)zurück, d. h. zweiLastValue-Instanzen gelten unabhängig vom Inhalt als gleich. Das dient der Deduplizierung während der Graphkompilierung und sollte nicht für Wertvergleiche verwendet werden.MISSINGvs.None:Noneist ein legitimer gespeicherter Wert; nurMISSINGbedeutet «nie geschrieben» (get / is_available:70-75). Wenn dein GeschäftsfieldNonezulässt, kannst du es unbesorgt schreiben — der Kanal interpretiert es nicht als leer.- Ein neuer Schreibvorgang bei
LastValueAfterFinishentzieht die Sichtbarkeit:LAF.update:122-128setzt ganz am Anfangself.finished = False. Wenn also nachfinish()erneut geschrieben wird, kehrt der Kanal in den «nicht veröffentlicht»-Zustand zurück und wird erst nach einem weiterenfinish()wieder lesbar. LastValueAfterFinish.consumeist no-op, solangefinishedFalse ist: SieheLAF.consume:130-136. Das bedeutet: Ein Wert, der geschrieben, aber noch nicht perfinishfreigegeben wurde, wird nicht geleert; erst nach einmaligem Konsumieren wird er geleert — genau die Semantik, die ein «Ende-Signal» braucht.
Zusammenfassung
LastValue ist die Standardform von LangGraph-Zuständen — ein Skalar, überschreibend aktualisiert, mit der Einschränkung «ein Wert pro Schritt», die bei Mehrfachschreibungen einen Fehler wirft, statt Werte stillschweigend zu verlieren. Wer das versteht, versteht, wie sich alle Felder ohne reducer-Markierung zur Laufzeit verhalten. Wie gleichzeitige Schreibungen korrekt zusammengeführt werden, steht in /channels/topic-binop; die Definition der Kanal-Schnittstelle in /channels/base-channel; wann genau update aufgerufen wird, in /pregel/algo.
Siehe offizielle Dokumentation: LangGraph 文档 · README。