LastValue:覆蓋式寫入的預設通道
職責
LastValue 是 BaseChannel 最簡單的子類:它存一個值,每次寫就把舊值覆蓋掉(LastValue class:20-21)。它就是「state 欄位不加任何 Annotated 標註時預設用的通道」,你在 StateGraph 裡寫 class State(TypedDict): count: int,編譯期看到的 count 欄位背後就是一個 LastValue(int)(fallback LastValue:1857-1859)。
它的關鍵約束是「每步只能寫一個值」。update 拿到長度大於 1 的序列時不合併也不覆蓋,而是直接拋 InvalidUpdateError(update:56-67),提示使用者該用 Annotated[int, reducer] 表達「這一步裡多個節點同時寫要怎麼 fold」。這個約束不是技術限制,而是語意選擇:純量狀態本來就沒有合理的合併方式,與其默默丟值,不如顯式報錯。
另外這個檔案還放了一個變體 LastValueAfterFinish(LastValueAfterFinish:81-152):行為像 LastValue,但寫進去的值要等整個 Pregel 執行 finish() 之後才對外可見,見一次就清空。它用於「中斷訊號」「結束訊號」這類只在執行結尾生效的內部通道。
設計動機
- 覆蓋是純量狀態唯一合理的預設:整數、字串、布林這些值沒有「合併」語意。
count: int這一類欄位,新值來了直接覆蓋舊值是最符合直覺的行為,所以StateGraph在fallback:1857裡就把沒標註 reducer 的欄位編譯成LastValue。 - 拒絕靜默丟值:如果同一超步裡兩個節點同時寫
count,LastValue不會隨便挑一個贏,而是拋INVALID_CONCURRENT_GRAPH_UPDATE(InvalidUpdateError:60-64)。錯誤訊息明確提示「Use an Annotated key to handle multiple values」,把使用者推向正確的解法——加一個 reducer,見 /channels/topic-binop。 MISSING而不是None:初始值用langgraph._internal._typing.MISSING(value = MISSING:29),這樣None是合法的存值。get()在MISSING時拋EmptyChannelError(get:69-72)。這套和BaseChannel預設實作保持一致,但is_available在這裡被覆寫成一行self.value is not MISSING(is_available:74-75),避免每次都走 try/except。copy比from_checkpoint便宜:雖然父類預設copy = from_checkpoint(checkpoint()),這裡覆寫了copy(copy:44-48),直接共享同一個value引用——因為純量本來也不需要深拷貝。from_checkpoint(from_checkpoint:50-54)則按MISSING分支決定要不要賦值。LastValueAfterFinish的延遲可見性:有些控制訊號(比如Command的 goto)在寫的時候不能立刻被prepare_next_tasks看到,否則會在本步內自我觸發形成環。LastValueAfterFinish用一個finished標誌位(finished flag:93-95),get()只在finished=True時回傳(get gated:145-148),finish()由 Pregel 在收尾時呼叫(finish:138-143),consume()在被消費一次後清空(consume:130-136)。
關鍵檔案
LastValue class:20-25— 類別定義,泛型BaseChannel[Value, Value, Value],即存值型別、寫值型別、checkpoint 型別都是同一個Value。__init__:27-29— 構造時value = MISSING,只有被寫過才不是MISSING。ValueType / UpdateType:34-42— 兩者都回傳self.typ,即宣告裡給的那個型別。copy:44-48— 直接復用value引用,純量不需要深拷貝。from_checkpoint:50-54— 從 checkpoint 值重建實例,MISSING表示從空開始。update:56-67— 核心約束:空序列回傳False,長度大於 1 拋InvalidUpdateError,否則存最後一個值並回傳True。get / is_available:69-75— 讀介面,MISSING拋EmptyChannelError;is_available覆寫成一行判斷。LastValueAfterFinish:81-95— 變體:加finished標誌,延遲到finish()後可見。LastValueAfterFinish.update:122-128— 寫時順手把finished復位,意味著新寫會撤掉上一次的可見性。consume / finish / get:130-148— 三個鉤子配合實作「見一次就清空」的語意。fallback LastValue:1857-1859—StateGraph編譯時遇到沒標註 reducer 的欄位就退回LastValue(annotation)。
資料流
LastValue.update 的全部邏輯就這幾行(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 True注意第三分支寫的是 values[-1] 而不是 values[0]——因為前面已經判了 len != 1,這一行只會走到 len == 1 的情況,[-1] 和 [0] 等價。空序列回傳 False 是關鍵:它配合 apply_writes 裡對未變更通道的 update(EMPTY_SEQ) 呼叫(EMPTY_SEQ update:329),讓 LastValue 在被空呼叫時正確回傳「未改動」。
StateGraph 編譯時怎麼把沒標註的欄位映射成 LastValue,這段在 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 fallback按順序嘗試:managed value(如 MessagesState 裡的特殊管理)→ 顯式 BaseChannel 實例 → Annotated[..., reducer](BinaryOperatorAggregate,見 /channels/topic-binop)→ 都不命中,退回 LastValue。
整個讀寫時序如下:
邊界與失敗
- 多值並行寫直接報錯:
multi-value error:59-64拋InvalidUpdateError,錯誤碼INVALID_CONCURRENT_GRAPH_UPDATE。這是初學者最常見的坑:兩個節點同時往同一個純量欄位寫。修法是改成Annotated[int, lambda a, b: a + b]這種帶 reducer 的欄位。 - 空寫回傳
False:empty seq:57-58回傳False,和BaseChannel契約一致,apply_writes據此跳過channel_versions更新。 __eq__不看值只看型別:__eq__:31-32回傳isinstance(value, LastValue),意味著兩個LastValue實例無論存什麼都被認為相等。這是給圖編譯期去重用的,不要拿它做值比較。MISSING與None:None是合法存值;只有MISSING才表示「從未寫過」(get / is_available:70-75)。如果你的業務欄位允許None,放心寫,通道不會把它當空。LastValueAfterFinish的新寫會撤掉可見性:LAF.update:122-128一開頭self.finished = False。也就是說如果finish()之後又來一個寫,通道會重新進入「未公開」狀態,要等下一次finish()才能讀。LastValueAfterFinish.consume在finished為假時是 no-op:見LAF.consume:130-136。這意味著「寫完但還沒finish」的值不會被清掉,只有在被消費過一次後才清空——這正是它「結束訊號」的語意所需。
小結
LastValue 是 LangGraph 狀態的預設形狀——存一個純量,覆蓋式更新,每步單值約束在寫多值時報錯而不是默默丟值。理解它就理解了所有沒標註 reducer 的欄位在執行時是怎麼變化的。要看並行寫怎麼正確合併,繼續讀 /channels/topic-binop;通道介面的定義回 /channels/base-channel;具體什麼時候被 update 見 /pregel/algo。
對照官方資料:LangGraph 文件 · README