先講結論
LangGraph 做的事,是把 Agent 的流程從「一條直線」改寫成「一張有岔路的地鐵圖」。
打個比方。傳統的 Chain 像一條單向輸送帶:原料從這頭進去,成品從那頭出來,中間不會回頭,也不會突然換線。這在流程簡單時非常好用。
但真實世界的流程很少這麼乖。你很快就會遇到這三種情況:
- 有些問題要繞道去查資料,有些不用
- 查不到要回頭重試
- 跑到一半要停下來等人審批
輸送帶做不到這些。你需要的是一張地鐵圖:有轉乘站,有迴圈線,也可以在某一站先停車等人。
LangGraph 就是幫你把「輸送帶」畫成「地鐵圖」的那套工具。你定義三樣東西:狀態(列車上載什麼)、節點(每一站做什麼)、邊(什麼條件下開往下一站)。
一、先看一個真實的痛點
假設你在做一個客服問答機器人。它上線之後,日常會遇到四類輸入:
| 使用者輸入 | 理想處理方式 |
|---|---|
| 「退貨政策是什麼」 | 查知識庫,然後直接回答 |
| 「我要改收貨地址」 | 查訂單系統,但要先驗證身分 |
| 「你們服務太差了」 | 轉人工,不要硬答 |
| 「上一個問題再解釋一次」 | 沿用上文,不要從頭來 |
把這四句話攤開看,會發現一件事:它們共用同一批工具(知識庫、訂單系統、人工客服),卻不是同一個流程。
真正決定走向的,是中間那個判斷:這句話該走哪條路?
Chain 的麻煩就在這裡。它假設你事先就知道順序,順著寫下去就好。可是「該走哪條路」這件事,往往要讀完內容才知道。
這就像餐廳的點餐流程。如果每位客人都點同一份套餐,一條輸送帶就夠了。但現實是有人要加辣、有人不吃牛、有人只想喝湯。你需要一個在旁邊看著菜單、決定往哪個廚房送的人。
那個「決定往哪送」的動作,就是 LangGraph 幫你顯式化的東西。
二、Chain 撐不住的三件事
先講清楚:不是說 Chain 不好。而是當需求長成下面三個樣子時,Chain 會開始吃力。
1. 分支
「如果問題屬於 A 類,就做 X;否則做 Y。」
你當然可以在 Chain 裡硬寫 if/else。一兩個條件還行,但當條件變成八個、又互相嵌套的時候,整條鏈就會變成一坨沒人敢改的判斷式。更麻煩的是:這些判斷藏在程式碼深處,看不到全貌。
2. 循環
「檢索回來的資料不夠,換個關鍵詞再查一次。」
這是 Agent 最常見的行為。但 Chain 沒有「回到上一步」的原生概念。你得自己包一個 while 迴圈,然後自己處理「重試幾次要放棄」的計數。
3. 中斷恢復
「這筆退款金額比較大,先送人工審批,明天再繼續。」
這一條最現實,也最致命。原型階段完全看不出來,但上線後第一個「需要人工介入」的需求就會把它打穿。因為 Chain 一旦中斷,那個跑到一半的狀態就沒有地方放,只能整條從頭再跑一次。
打個比方:Chain 像一張寫滿步驟的紙。你走到第三步被叫去開會,回來時發現那張紙已經被風吹走了,只能從第一步重做。
LangGraph 的做法是:每一步都往一塊共用的白板上記一筆。中斷了?白板還在,回頭接著走。
三、三個概念,用廚房來理解
LangGraph 的文件充滿 State、Node、Edge 這些詞。但其實它們對應的都是很日常的東西。
| 概念 | 廚房裡的對應 | 你要回答的問題 |
|---|---|---|
| State | 共用白板 | 每一步之間要傳什麼資料? |
| Node | 工作站 | 這一步具體做什麼? |
| Edge | 路口的指示 | 什麼條件下走哪條路? |
有三件事值得特別說明,因為它們是新手最容易誤解的地方。
第一,節點之間不直接傳資料。 節點不呼叫節點,它們都只跟白板打交道。這樣做的好處是:任何一步都可以被單獨替換或測試,不會牽一髮動全身。
第二,節點回傳的是「增量」,不是整個狀態。 你不需要把白板上所有內容複製一份再改,只要交回你改動的那一格。合併的工作由 LangGraph 負責。
第三,判斷結果本身也該記在白板上。 例如把「決定走教學路線」記進 route 欄位。這樣事後你就能查到「當初為什麼走這條路」,對除錯和可觀測性幫助極大。
四、把那段程式碼拆開看
底下的程式碼是可直接複製運行的完整版本,在 Python 3.9.6 + langgraph 0.6.11(langchain-core 0.3.86) 實測通過。
它刻意不呼叫任何 LLM。原因很簡單:如果一開頭就拉模型進來,你會分不清哪些是「圖的機制」、哪些是「模型的功勞」。用純函式節點,機制可以被單獨隔離出來看。
先用一張圖說明這段程式碼的結構:
看懂這張圖,程式碼就只剩語法問題了。
from typing import Literal, TypedDict
from langgraph.graph import END, START, StateGraph
class State(TypedDict):
question: str
route: str
answer: str
def classify(state: State) -> dict:
"""分流:示範用簡易規則,實務上這裡換成 LLM 分類即可"""
return {"route": "tutorial" if "怎麼" in state["question"] else "direct"}
def pick(state: State) -> Literal["tutorial", "direct"]:
"""條件邊的判斷函式:回傳下一個節點的名字"""
return state["route"]
def tutorial_node(state: State) -> dict:
return {"answer": f"[教學路徑] 為「{state['question']}」產出分步說明"}
def direct_node(state: State) -> dict:
return {"answer": f"[直接路徑] 為「{state['question']}」產出簡答"}
g = StateGraph(State)
g.add_node("classify", classify)
g.add_node("tutorial", tutorial_node)
g.add_node("direct", direct_node)
g.add_edge(START, "classify")
g.add_conditional_edges("classify", pick, {"tutorial": "tutorial", "direct": "direct"})
g.add_edge("tutorial", END)
g.add_edge("direct", END)
app = g.compile()
for q in ["LangGraph 怎麼用?", "LangGraph 是什麼?"]:
out = app.invoke({"question": q})
print(f"{q} → 路由 {out['route']}|{out['answer']}")
整段程式碼可以分成四塊,逐塊看就不難:
第一塊:定義白板上要放什麼。
class State(TypedDict):
question: str
route: str
answer: str
只有三個格子。question 是進來時就有的,route 是分流時寫的,answer 是最後產出的。
第二塊:定義三個工作站。
classify 讀問題內容,決定路線;tutorial_node 和 direct_node 各自產出不同形式的回答。注意每個函式都只回傳一個 dict,也就是「我要在白板上改哪一格」。
第三塊:把線接起來。
g.add_edge(START, "classify")
g.add_conditional_edges("classify", pick, {"tutorial": "tutorial", "direct": "direct"})
g.add_edge("tutorial", END)
g.add_edge("direct", END)
這裡有兩種邊,值得分清楚:
add_edge是普通邊:A 做完必定去 B,沒有懸念。add_conditional_edges是條件邊:它綁定一個判斷函式(此例是pick),由那個函式讀白板、回傳下一個節點的名字。
換句話說,條件邊就是路口那位指揮。而指揮的依據不是憑空猜的,是白板上那個 route 欄位。
第四塊:跑起來。
app = g.compile()
out = app.invoke({"question": "LangGraph 怎麼用?"})
compile() 把圖編譯成可執行的物件,invoke() 丟進初始狀態、跑完、回傳最終狀態。
實測輸出如下(同一段程式、同一個 app,差別只在輸入句子裡有沒有「怎麼」兩個字):
LangGraph 怎麼用? → 路由 tutorial|[教學路徑] 為「LangGraph 怎麼用?」產出分步說明
LangGraph 是什麼? → 路由 direct|[直接路徑] 為「LangGraph 是什麼?」產出簡答
到這裡,你已經看完了 LangGraph 的核心。剩下的是工程細節。
五、檢查點:像遊戲存檔,而且每人一個存檔槽
前面說過,Chain 最怕中斷。LangGraph 的解法叫做檢查點(checkpoint)。
用遊戲來比喻最貼切:檢查點就是存檔點。你不需要每次重開都從第一關打起,回到上一個存檔點繼續就好。
加上檢查點只需要改兩行:
from langgraph.checkpoint.memory import MemorySaver
app = g.compile(checkpointer=MemorySaver())
cfg = {"configurable": {"thread_id": "demo-1"}}
app.invoke({"question": "LangGraph 怎麼用?"}, cfg)
snap = app.get_state(cfg)
print(snap.values) # 已保存的完整狀態
print(snap.next) # 下一個待執行節點
實測輸出:
{'question': 'LangGraph 怎麼用?', 'route': 'tutorial', 'answer': '[教學路徑] 為「LangGraph 怎麼用?」產出分步說明'}
()
這裡有兩個新手常問的問題。
問題一:snap.next 為什麼是空的?
因為流程已經跑完了。next 代表「下一個還沒執行的節點」,跑完自然是空的。如果圖停在中間(例如正在等待人工審批),這裡就會列出待執行的節點,之後你可以把它推下去繼續跑。這就是「可恢復」的具體長相。
問題二:thread_id 是什麼?
可以理解成存檔槽。同一個 app 可以同時服務多條對話,只要給不同的 thread_id,狀態就各自獨立,不會互相污染。實測中以 demo-1 和 demo-2 分別跑不同問題,得到的 route 一個是 tutorial、一個是 direct,互不干擾。
順帶一提,MemorySaver 把狀態存在記憶體裡,適合開發測試。上生產要換成資料庫型的檢查點,否則重啟服務就全忘了。
⚠️ 一個實測踩到的小坑:
get_state()回傳的是 NamedTuple,取值要用snap.values,不是snap['values']。後者會直接拋TypeError。我當初就卡在這一下。
六、什麼時候不該用 LangGraph
這是很多教學文章略過的部分,但其實很重要:加了圖就會變好,是錯的。
以下情況,用 Chain 或單次呼叫更划算:
- 流程是一條直線,沒有分支也沒有重試
- 只有一兩個步驟,多加一層圖只是多餘的抽象
- 團隊還沒熟悉狀態管理,硬上圖會讓除錯變得更難
- 你要的是單純的「問答」,不是「流程」
判斷準則只有一句:問自己「這個流程需要回頭,或需要中途停下來嗎?」
需要,就值得用圖。不需要,就別加。
七、給第一次接觸的人:三個常見誤解
誤解一:以為 LangGraph 是「更強的 Chain」。
不是。它是控制流的表達方式。Chain 描述的是「順序」,Graph 描述的是「關係」。這兩件事不一樣。
誤解二:以為一定要搭配 LLM。
不用。這篇的範例就完全沒有呼叫模型。圖只是流程的骨架,模型是放在節點裡的其中一種零件。
誤解三:以為節點之間可以直接傳值。
不行,也不該。所有資料都走白板。這個約定看起來囉嗦,但它正是「每一步可以獨立測試」的前提。
八、帶走三句話
- LangGraph 的價值不在「更強的 Chain」,而在把控制流顯式化:分支、循環、中斷都變成圖上看得見的東西。
- 節點只回傳增量,狀態由框架合併。這個約定讓複雜流程變得可推理。
- 檢查點是可恢復性的基礎,也是原型與生產之間最容易被低估的一道檻。
下一步
想繼續往下走,可以先補齊框架層的基礎(LangChain 完整教學 2026),再看工具如何接進 Agent(MCP 生態詳解),最後用架構視角收束(三層 Agent 協同框架)。
範例程式碼實測環境:Python 3.9.6、langgraph 0.6.11、langchain-core 0.3.86。