一條訊息先被覆蓋了:LangGraph 的狀態設計,是一套看不見的合併規則
從一段被覆蓋的訊息列表看起,觀察 LangGraph 的狀態與節點如何把資料合併規則設計成一種看不見的介面。
先從一個輸出結果看起。同樣兩個節點,同樣各回傳一句打招呼的字串,跑完之後列表裡只剩一條訊息。第一個節點寫進去的「你好,我是小黃」不見了,只剩第二個節點的「AI:你好,小黃」。這不是 bug,是框架被設計出來的行為,而且它不會提前告訴你。
2026 年 9 月 23 日,掘金上一篇教學文章把 LangGraph 的狀態與節點機制講得相當清楚。這篇文章本身是技術教學,但如果換一個角度,把它當成一份介面設計的案例來讀,會看到一些有趣的東西:一個開發框架如何決定資料的去留,本質上和一個 App 如何決定通知的優先順序,是同一種設計工作。
流程圖會跑,但資料放在哪
LangGraph 可以被理解成一張會運行的流程圖。State 儲存資料,Node 處理資料,Edge 決定下一步執行誰。這個三段式分工在視覺上很好想像:一張有向圖,箭頭指著方塊,資料像小球一樣在裡面滾動。
但真正值得看的,是那條不會被畫出來的規則:節點只返回自己修改的部分,框架負責把更新合併回狀態。這句話翻譯成介面設計的語言,就是「每個元件只提交自己的差異,系統層負責合併」。聽起來體貼,實際上卻藏著一個預設值問題:合併的方式,預設是覆蓋。
狀態規約用 TypedDict 定義,total=False 表示欄位可以逐步產生。普通欄位沒有特殊合併規則,同一欄位被再次更新時,新值直接覆蓋舊值。呼叫圖時只需提供入口欄位,節點返回局部更新,沒被更新的舊欄位會繼續保留。
注意這裡的細節層次:沒被碰的欄位會保留,被碰的欄位會覆蓋。保留與覆蓋之間的分界線,畫在一個開發者最容易誤判的位置。兩個節點都更新 messages,common_b 回傳的新列表就把 common_a 的列表整個替換掉。被覆蓋的只有 messages,username 仍會保留,因為普通列表不會自動判斷應該追加還是替換。
同樣的輸入,兩種表情
這裡出現了一個很乾淨的對照組。把同樣的例子原樣換成 MessagesState,兩個節點依舊回傳字串列表,依舊用同樣的順序連接,跑完之後輸出變成了兩條訊息,先後都被保留了。
同樣的更新內容,普通狀態只剩最後一條,MessagesState 卻保留了兩條。差別在於 MessagesState 自帶的 messages 欄位背後有一套附加的合併邏輯,新訊息會被接在舊訊息後面,而不是替換。
這個對照放在產品設計的脈絡裡,很像兩種訊息通知的設計哲學。一種是每次更新都刷新畫面,只顯示最新狀態;另一種是累積歷史,讓每次更新都成為時間軸上的一格。兩者沒有對錯,但設計者必須知道自己選了哪一種。問題在於,LangGraph 把這個選擇藏在類型的名稱裡,而不是寫在行為發生的當下。
換句話說,這是一個把語意寫進預設值的設計。TypedDict 定義的是資料的形狀,MessagesState 定義的卻是資料的命運。使用者在宣告階段做的選擇,決定了執行階段資料的生死,而這條因果線在程式碼裡幾乎不可見。你看到的只是兩個幾乎一樣的類別名稱,和兩個完全相反的結果。
預設值是框架的價值觀
框架設計裡最有分量的決定,從來都不是能做什麼,而是預設做什麼。LangGraph 讓普通欄位預設覆蓋,讓訊息欄位預設累積,這組預設值本身就表達了一種判斷:對話是被特別對待的資料。
這個判斷有其脈絡。LangGraph 的主要使用場景是建構以語言模型為核心的代理流程,對話歷史幾乎是所有此類應用的剛需。把訊息的累積做成開箱即得的行為,等於把最常見的需求放進預設值,讓大多數人不用寫額外的合併程式碼就能得到正確結果。這是好的預設值設計:照顧多數,暴露例外。
但預設值的光環之下總有陰影。當一個初學者先用 TypedDict 寫出了自己的狀態,再依樣畫葫蘆地更新訊息欄位,覆蓋行為就成了一個安靜的陷阱。程式不會報錯,資料不會丟失例外訊息,只是安安靜靜地少了一條。這種錯誤最難排查,因為系統的每一步都按照規則運行,出錯的是人對規則的想像。
介面設計裡有一條經驗:當一個操作不可逆,介面必須讓後果可見。覆蓋就是一種不可逆。LangGraph 用類型系統承載這個後果,好處是宣告集中、行為一致,代價是後果與操作之間隔了整個框架的厚度。這是所有宣告式設計共同的交換,用當下的可讀換執行的透明。
讀規格的方式,決定讀框架的方式
回到那份教學本身。它之所以值得從設計角度重讀,是因為它展示了一種讀框架的方式:不是背 API,而是找到框架做選擇的位置。狀態怎麼合併、訊息怎麼累積、欄位怎麼保留,這些都是選擇,而每個選擇背後都站著一個對使用場景的假設。
LangGraph 的狀態設計,把「資料的去留」做成了一套看不見的介面。它運作得很好,前提是你讀過規格。而所有把複雜度藏進預設值的設計,都在做同一個賭注:賭使用者會花時間理解預設值代表了誰的利益。賭贏了,框架顯得優雅;賭輸了,就只剩一條被安靜覆蓋掉的訊息,和一個盯著輸出結果發愣的開發者。
理解一個框架,最省力的起點從來不是它的功能清單,而是它的預設值。預設值說出了框架相信什麼、照顧誰、願意讓誰踩坑。就這點而言,LangGraph 的那張流程圖雖然畫的是節點與邊,真正值得細讀的,是圖外面那套關於覆蓋與保留的靜默規則。
主題 介面與互動設計
標籤 #langgraph設計、#狀態管理介面、#開發框架美學