Skip to content

LastValue: Der Standard-Kanal mit überschreibendem Schreiben

源码版本1.2.9

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: int ist es intuitiv, dass ein neuer Wert den alten überschreibt. Daher kompiliert StateGraph in fallback:1857 Felder ohne reducer-Markierung zu LastValue.
  • Stilles Verwerfen vermeiden: Wenn zwei Knoten im selben Superstep (superstep) gleichzeitig count schreiben, wählt LastValue nicht willkürlich einen Gewinner, sondern wirft INVALID_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.
  • MISSING statt None: Der Anfangswert ist langgraph._internal._typing.MISSING (value = MISSING:29), sodass None ein legitimer gespeicherter Wert bleiben kann. get() wirft bei MISSING ein EmptyChannelError (get:69-72). Das ist konsistent mit der BaseChannel-Standardimplementierung, aber is_available wird hier auf eine einzige Zeile self.value is not MISSING überschrieben (is_available:74-75), um nicht jedes Mal try/except durchlaufen zu müssen.
  • copy ist billiger als from_checkpoint: Obwohl die Basisklasse copy = from_checkpoint(checkpoint()) als Standard vorgibt, ist copy hier überschrieben (copy:44-48) und teilt einfach dieselbe value-Referenz — ein Skalar braucht ohnehin keine tiefe Kopie. from_checkpoint (from_checkpoint:50-54) entscheidet anhand des MISSING-Falls, ob zugewiesen wird.
  • Verzögerte Sichtbarkeit bei LastValueAfterFinish: Bestimmte Steuersignale (z. B. goto bei Command) dürfen nicht sofort von prepare_next_tasks gesehen werden, sonst würden sie noch im selben Schritt selbst auslösen und einen Zyklus bilden. LastValueAfterFinish nutzt ein finished-Flag (finished flag:93-95), get() liefert nur bei finished=True den Wert zurück (get gated:145-148), finish() wird von Pregel beim Abschließen aufgerufen (finish:138-143), und consume() leert den Wert, nachdem er einmal konsumiert wurde (consume:130-136).

Schlüsseldateien

  • LastValue class:20-25 — Klassendefinition, generisch BaseChannel[Value, Value, Value], d. h. Speicher-, Schreib- und Checkpoint-Typ sind alle dasselbe Value.
  • __init__:27-29 — Beim Konstruieren gilt value = MISSING; erst nach einem Schreiben ist der Wert nicht mehr MISSING.
  • ValueType / UpdateType:34-42 — Beide geben self.typ zurück, also den bei der Deklaration angegebenen Typ.
  • copy:44-48 — Verwendet die value-Referenz direkt; Skalare brauchen keine tiefe Kopie.
  • from_checkpoint:50-54 — Rekonstruiert eine Instanz aus einem Checkpoint-Wert; MISSING bedeutet «von leer beginnen».
  • update:56-67 — Kern-Einschränkung: leere Sequenz gibt False zurück, Länge größer als 1 wirft InvalidUpdateError, sonst wird der letzte Wert gespeichert und True zurückgegeben.
  • get / is_available:69-75 — Lese-Schnittstelle; bei MISSING wirft get ein EmptyChannelError; is_available ist als einzeilige Prüfung überschrieben.
  • LastValueAfterFinish:81-95 — Variante: finished-Flag verschiebt die Sichtbarkeit bis nach finish().
  • LastValueAfterFinish.update:122-128 — Setzt beim Schreiben finished zurü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 — Wenn StateGraph bei der Kompilierung ein Feld ohne reducer-Markierung trifft, fällt es auf LastValue(annotation) zurück.

Datenfluss

Die gesamte Logik von LastValue.update besteht aus diesen Zeilen (update:56-67):

python
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 True

Beachte, 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:

python
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 fallback

Reihenfolge: 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-64 wirft InvalidUpdateError mit Fehlercode INVALID_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 wie Annotated[int, lambda a, b: a + b] mit reducer umwandeln.
  • Leeres Schreiben gibt False zurück: empty seq:57-58 verhält sich konsistent mit dem BaseChannel-Vertrag; apply_writes überspringt daraufhin das Aktualisieren von channel_versions.
  • __eq__ vergleicht nur den Typ, nicht den Wert: __eq__:31-32 gibt isinstance(value, LastValue) zurück, d. h. zwei LastValue-Instanzen gelten unabhängig vom Inhalt als gleich. Das dient der Deduplizierung während der Graphkompilierung und sollte nicht für Wertvergleiche verwendet werden.
  • MISSING vs. None: None ist ein legitimer gespeicherter Wert; nur MISSING bedeutet «nie geschrieben» (get / is_available:70-75). Wenn dein Geschäftsfield None zulässt, kannst du es unbesorgt schreiben — der Kanal interpretiert es nicht als leer.
  • Ein neuer Schreibvorgang bei LastValueAfterFinish entzieht die Sichtbarkeit: LAF.update:122-128 setzt ganz am Anfang self.finished = False. Wenn also nach finish() erneut geschrieben wird, kehrt der Kanal in den «nicht veröffentlicht»-Zustand zurück und wird erst nach einem weiteren finish() wieder lesbar.
  • LastValueAfterFinish.consume ist no-op, solange finished False ist: Siehe LAF.consume:130-136. Das bedeutet: Ein Wert, der geschrieben, aber noch nicht per finish freigegeben 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