TL;DR:LangGraph 的核心只有三個概念——State(一份跨步驟共用的狀態)、Node(一個收 state、回傳「我要改哪些欄位」的函式)、Edge(把 node 串起來的線)。這篇用一個血脂決策 pipeline 的真實場景,帶你寫出第一個會跑的 graph:StateGraph定義 →add_node→add_edge→compile→invoke。看完你就抓得住 LangGraph 的心智模型,後面分流跟重試都是在這之上加東西。這是「用真實專案學 LangGraph」系列 EP2。
上一篇(EP1)講完選型,結論是:複雜流程下沉到 LangGraph,而學 LangGraph 先學三件事——state、node、edge。這篇就把這三件事一次教完,而且直接給你一個跑得起來的最小範例。
📌 目錄
- 先建立心智模型:graph 就是一條流水線
- State:跨步驟共用的那份狀態
- Node:收 state、回傳要改的欄位
- Edge:把 node 串成流程
- compile 跟 invoke:讓 graph 跑起來
- 完整可跑範例
- 我踩過的坑
- 常見問題 FAQ
- 延伸資源
先建立心智模型:graph 就是一條流水線
別被「graph」這個字嚇到。對 9 成的應用來說,LangGraph 的圖就是一條流水線:資料從頭進來,一站一站處理,最後出去。
舉個真實場景:一個血脂用藥決策的 pipeline。一筆病人資料進來,要經過這幾站:
每一站就是一個 node。站跟站之間的箭頭就是 edge。而那筆「一路被加工的病人資料」,就是 state——它從頭流到尾,每一站往上面寫一點東西。
抓住這個畫面,剩下的都是把它翻成 code。
🧠 State:跨步驟共用的那份狀態
State 是整張圖的 single source of truth。所有 node 讀它、寫它。在 LangGraph 裡,最常見的定義方式是 Python 的 TypedDict:
from typing import TypedDict, Any
class AnalyzeState(TypedDict, total=False):
"""跨 node 傳遞的狀態。total=False = 每個欄位都可選填。"""
patient: dict[str, Any] # 病人資料(會被第一站 enrich)
computed: dict[str, Any] # 公式算出來的事實
active_rule: dict[str, Any] # 載入的決策規則
advice: str # 最終建議
兩個關鍵設計:
total=False:代表每個 key 都是可選的。第一站跑完只有patient,第二站才填computed,不會因為「現在還沒有 advice」就報錯。- 欄位就是合約:哪個 node 負責寫哪個欄位,要講清楚。這是團隊協作時最重要的紀律——
patient由第一站寫,computed由第二站寫,其他站只讀不改。
進階:state 的欄位可以用更嚴格的型別(像 Pydantic model 配extra="forbid"),讓「寫錯欄位」當場炸出來,而不是靜悄悄帶著髒資料往下走。新手先用TypedDict就好。
⚡ Node:收 state、回傳要改的欄位
這是 LangGraph 最容易誤解的地方,先把規則記死:
一個 node 就是一個函式,收整份 state,回傳一個 dict——dict 裡只放「我這站要改的欄位」。
def patient_loader(state: AnalyzeState) -> dict:
"""第一站:補齊衍生指標。"""
patient = state["patient"]
# 假裝這裡算了 BMI / eGFR / LDL
enriched = {**patient, "bmi": 24.5, "ldl_c": 71}
return {"patient": enriched} # 只回傳我改的那個 key
注意 return {"patient": enriched}——它不是回傳整份 state,只回傳要更新的部分。LangGraph 會自動把這個 dict 合併進 state。你不用自己去 mutate state,也不該去 mutate(不可變原則,避免隱性 side effect)。
再寫第二、第三站,長得一模一樣:
def precompute(state: AnalyzeState) -> dict:
"""第二站:跑公式判斷風險。"""
ldl = state["patient"]["ldl_c"]
computed = {"ldl_on_target": ldl <= 55} # 達標門檻
return {"computed": computed}
def load_rule(state: AnalyzeState) -> dict:
"""第三站:根據風險載入規則 + 出建議。"""
on_target = state["computed"]["ldl_on_target"]
advice = "維持現用藥" if on_target else "建議加上 ezetimibe 強化降脂"
return {"advice": advice}
每個 node 都是純函式:輸入 state、輸出「要改什麼」,不碰外面、不改傳進來的東西。這個紀律一守住,你的 graph 就好 debug、好測試、好擴充。
🔗 Edge:把 node 串成流程
有了 node,用 edge 把它們串起來。線性流程超簡單:
from langgraph.graph import StateGraph, START, END
g = StateGraph(AnalyzeState)
# 1. 註冊 node(給名字 + 對應函式)
g.add_node("patient_loader", patient_loader)
g.add_node("precompute", precompute)
g.add_node("load_rule", load_rule)
# 2. 用 edge 串起來:START → 第一站 → 第二站 → 第三站 → END
g.add_edge(START, "patient_loader")
g.add_edge("patient_loader", "precompute")
g.add_edge("precompute", "load_rule")
g.add_edge("load_rule", END)
START 跟 END 是 LangGraph 內建的兩個特殊節點,標記圖的入口跟出口。add_edge(A, B) 的意思是「A 跑完就跑 B」。
就這樣,一條線性流水線就接好了。下一篇 EP3 會講,當你需要「跑完第三站,視結果決定去哪」的時候,把 add_edge 換成 add_conditional_edges 就能分流。
🎯 compile 跟 invoke:讓 graph 跑起來
圖畫完要 compile() 才能跑:
app = g.compile()
compile() 回傳一個可執行的物件。然後 invoke() 丟初始 state 進去,它會跑完整條流水線,回傳最終 state:
initial = {"patient": {"name": "test", "ldl_c": 71}}
final = app.invoke(initial)
print(final["advice"])
# → 建議加上 ezetimibe 強化降脂
如果你的 node 裡有 async 操作(查 DB、打 API),用 await app.ainvoke(initial) 的 async 版本。production 服務幾乎都用 ainvoke。
一個實務小技巧:compile() 只需要做一次。在 web 服務裡,把它放在 module 載入時跑一次、存成全域變數,每個 request 重複用同一個 compiled graph,不要每次 request 都重 compile:
# module 載入時 compile 一次
_app = g.compile()
async def handle_request(patient: dict):
return await _app.ainvoke({"patient": patient})
📝 完整可跑範例
把上面全部拼起來,這是一個你複製貼上就能跑的最小 LangGraph:
from typing import TypedDict, Any
from langgraph.graph import StateGraph, START, END
class AnalyzeState(TypedDict, total=False):
patient: dict[str, Any]
computed: dict[str, Any]
advice: str
def patient_loader(state: AnalyzeState) -> dict:
enriched = {**state["patient"], "bmi": 24.5}
return {"patient": enriched}
def precompute(state: AnalyzeState) -> dict:
ldl = state["patient"]["ldl_c"]
return {"computed": {"ldl_on_target": ldl <= 55}}
def load_rule(state: AnalyzeState) -> dict:
on_target = state["computed"]["ldl_on_target"]
advice = "維持現用藥" if on_target else "建議加上 ezetimibe"
return {"advice": advice}
g = StateGraph(AnalyzeState)
g.add_node("patient_loader", patient_loader)
g.add_node("precompute", precompute)
g.add_node("load_rule", load_rule)
g.add_edge(START, "patient_loader")
g.add_edge("patient_loader", "precompute")
g.add_edge("precompute", "load_rule")
g.add_edge("load_rule", END)
app = g.compile()
result = app.invoke({"patient": {"ldl_c": 71}})
print(result["advice"]) # 建議加上 ezetimibe
裝好 LangGraph 就能跑:
pip install langgraph
python first_graph.py
驗證成功的標準:終端機印出 建議加上 ezetimibe。把 ldl_c 改成 50 再跑一次,應該印出 維持現用藥——這證明你的 state 真的一路流過三站、每站都正確讀寫。
🔥 我踩過的坑
坑一:node 回傳整份 state,結果欄位被洗掉
新手最常見錯誤——以為要回傳完整 state:
# ❌ 錯:手動組整份 state,很容易漏欄位
def precompute(state):
state["computed"] = {...}
return state # 看起來沒事,但養成壞習慣
正確做法是只回傳你要改的 key,讓 LangGraph 自己合併:
# ✓ 對:只回傳這站負責的欄位
def precompute(state):
return {"computed": {...}}
回傳整份 state 在簡單情況下不會出錯,但一旦多個 node 並行、或用到 reducer(下一篇講),「回傳整份」就會把別人寫的東西洗掉。從第一天就養成「只回傳 diff」的習慣。
坑二:忘記接 START 或 END
add_node 只是「註冊」這個 node 存在,不代表它會被跑。你一定要用 edge 把它接進流程,而且整條流程的頭要接 START、尾要接 END。漏掉 START 那條 edge,compile 不會報錯,但 invoke 時 graph 不知道從哪開始,直接給你怪結果。
坑三:在 node 裡 mutate 傳進來的 state
def bad_node(state):
state["patient"]["bmi"] = 24.5 # ❌ 直接改傳進來的物件
return {}
這違反不可變原則。改傳進來的 dict 會造成難追的 side effect,尤其多 node 共讀同一份資料時。永遠建新物件:{**state["patient"], "bmi": 24.5},回傳新的,不動原本的。
❓ 常見問題 FAQ
Q:State 一定要用 TypedDict 嗎?
A:不一定,但建議。LangGraph 支援 TypedDict、Pydantic model、dataclass。新手用 TypedDict 最直觀;要嚴格驗證欄位(防打錯字)就上 Pydantic。重點是 state 的欄位要當成合約寫清楚,用哪種型別是次要的。
Q:node 一定要是函式嗎?可以是 class 嗎?
A:node 本質是「可呼叫物件」(callable),函式最常見,但任何 __call__ 接收 state、回傳 dict 的物件都行。複雜 node 要帶設定或依賴時,用 class 包起來會比較乾淨。
Q:一個 node 可以同時改多個欄位嗎?
A:可以。回傳的 dict 想放幾個 key 都行,例如 return {"computed": ..., "advice": ...}。只要那些 key 是這個 node 的職責就好。
Q:graph 跑到一半出錯會怎樣?
A:預設 node raise exception 就整條 graph 中斷、把 exception 往外丟。你可以在 node 內自己 try/except 處理,或用 LangGraph 的 durable state(1.0 強化的功能)讓流程能從中斷點接回。新手先讓它 raise,把錯誤暴露出來比較好 debug。
Q:怎麼看 graph 實際跑了哪些 node?
A:用 app.stream(initial) 取代 invoke,它會 yield 每一步的中間結果,你能看到 state 一站一站怎麼變。debug 流程超好用。
🔗 延伸資源
- LangGraph 官方 repo(langchain-ai/langgraph) — State / Node / Edge 的 API 文件
- LangGraph 1.0 GA 公告 — 為什麼這套 runtime 值得學
- 上一篇:EP1 — LangChain vs LangGraph 是什麼?2026 該學哪個
- 下一篇:EP3 — Conditional Edge + Reducer,讓你的 graph 會分流、會重試