iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
AI Engineering

30天用 Claude Code + LangGraph 實作個人化 AI 學習教練系列 第 10

Day 10:LangGraph 最小範例 - 組出第一個 Agent

  • 分享至 

  • xImage
  •  

前兩天我們讓 ollama_client.py 能穩定拿到結構化回應。但那只是「呼叫一次、拿到結果、結束」,還不是 Agent。Day 8 講過 Agent 的核心是一個會重複跑的循環:Observe → Think → Action → Repeat。今天要用 LangGraph 把這個循環真正組出來。

這是這兩週最難的一天,觀念比代碼多。不要求快,先搞懂三個名詞在做什麼,代碼就看得懂。

LangGraph 是什麼

前面九天寫的都是「一條直線」的代碼:呼叫函式、拿到結果、往下走。LangGraph 讓你把流程畫成一張圖,資料在圖上的節點之間流動,每個節點做一件事,節點之間用邊連起來決定下一步去哪裡。

想像成捷運路線圖:State 是坐在車廂裡的乘客(裝著目前的所有資訊),Node 是每一站(每一站做一件事,比如「上車」「驗票」「下車」),Edge 是連接站與站之間的軌道(決定坐完這站要去哪一站)。列車從起點站開出,經過每一站,最後到終點站,這整趟路線就是一張圖。

起點
  ↓(Edge)
Node:處理一件事
  ↓(Edge)
Node:處理下一件事
  ↓(Edge)
終點

Day 12 開始,我們會在圖上加更多站(判斷意圖、查資料庫、生成計畫),今天先組最簡單的一條路線:只有一站,輸入進來,呼叫 Ollama,輸出結果。

核心概念

State:車廂裡裝的東西

State 是一個資料結構,定義「整趟旅程中,車廂裡要裝哪些資訊」。乘客(資料)從起點上車,每經過一站,車廂裡的東西可能被更新,到終點時車廂裡裝的就是最終結果。

今天的 State 很單純,只裝兩樣東西:使用者輸入的文字,還有 Ollama 回應的文字。

Node:每一站要做的事

Node 就是一個 Python 函式,收到目前的 State,做一件事,回傳更新後的 State。今天只有一個 Node:把使用者輸入丟給 Ollama,把回應寫回 State。

Edge:站與站之間的軌道

Edge 決定「做完這一站,接下來去哪一站」。LangGraph 有兩個特殊的站:START(起點)和 END(終點)。今天的路線很短:START → 呼叫模型 → END


實作步驟

步驟1:安裝 LangGraph

cd backend
python -m pip install langgraph

驗證:

python -c "import langgraph; print('安裝成功')"

步驟2:定義 State 結構

State 用 TypedDict 定義,就是告訴 LangGraph「車廂裡有哪些欄位、各是什麼型別」。

檔案位置: backend/graph.py
狀態: 新增檔案
用途: 定義並組建第一個 LangGraph StateGraph
依賴: langgraph, langchain-ollama

from typing import TypedDict


class GraphState(TypedDict):
    """圖上流動的資料:使用者輸入什麼、模型回應了什麼"""
    user_input: str
    ai_response: str

user_input 是進站時就有的資料,ai_response 一開始是空的,經過 Node 處理後才會被填進去。

步驟3:寫 Node 函數

Node 的規則很簡單:收一個 State,回傳一個 State(或是 State 裡要更新的那一部分)。

檔案位置: backend/graph.py
狀態: 修改檔案(接續步驟2,繼續往下加)
用途: 定義呼叫Ollama的Node函數
依賴: langchain-ollama

from langchain_ollama import ChatOllama

_llm = ChatOllama(model="llama3.1:8b", temperature=0.3)


def call_model(state: GraphState) -> dict:
    """Node:把使用者輸入丟給 Ollama,回傳更新後的欄位"""
    response = _llm.invoke(state["user_input"])
    return {"ai_response": response.content}

這裡刻意只回傳 {"ai_response": ...},不是整個 State。LangGraph 會自動把這個回傳值「合併」進現有的 State 裡,user_input 不用重複回傳,LangGraph 會幫你保留。

步驟4:組建 StateGraph

檔案位置: backend/graph.py
狀態: 修改檔案(接續步驟3,繼續往下加)
用途: 組建並編譯出最終的 graph
依賴: langgraph

from langgraph.graph import StateGraph, START, END

graph_builder = StateGraph(GraphState)

# 把 Node 加進圖裡,取名叫 "chat"
graph_builder.add_node("chat", call_model)

# 畫軌道:從起點直接進 "chat",做完 "chat" 直接到終點
graph_builder.add_edge(START, "chat")
graph_builder.add_edge("chat", END)

# 編譯成可以真正執行的圖
graph = graph_builder.compile()

add_node("chat", call_model) 把剛剛寫的函式取名叫 "chat",放進圖裡當一站。add_edge 畫軌道,compile() 把整張圖組裝成可以呼叫的物件。

完整的 graph.py 到這裡長這樣:

from typing import TypedDict
from langchain_ollama import ChatOllama
from langgraph.graph import StateGraph, START, END


class GraphState(TypedDict):
    """圖上流動的資料:使用者輸入什麼、模型回應了什麼"""
    user_input: str
    ai_response: str


_llm = ChatOllama(model="llama3.1:8b", temperature=0.3)


def call_model(state: GraphState) -> dict:
    """Node:把使用者輸入丟給 Ollama,回傳更新後的欄位"""
    response = _llm.invoke(state["user_input"])
    return {"ai_response": response.content}


graph_builder = StateGraph(GraphState)
graph_builder.add_node("chat", call_model)
graph_builder.add_edge(START, "chat")
graph_builder.add_edge("chat", END)
graph = graph_builder.compile()

步驟5:寫測試腳本,讓圖真的跑起來

檔案位置: backend/test_graph.py
狀態: 新增檔案
用途: 驗證 StateGraph 能正常運行,走完整條路線
依賴: graph

from graph import graph


def main() -> None:
    """給圖一個起始 State,看它跑完整條路線後回傳什麼"""
    result = graph.invoke({"user_input": "用一句話說明什麼是學習教練", "ai_response": ""})
    print("使用者輸入:", result["user_input"])
    print("模型回應:", result["ai_response"])


if __name__ == "__main__":
    main()

執行:

python test_graph.py

應該看到類似這樣的輸出:

使用者輸入: 用一句話說明什麼是學習教練
模型回應: 學習教練是根據你的目標和進度,持續陪伴並調整學習方向的夥伴。

graph.invoke(...) 就是把乘客送上車,圖會自動照著 START → chat → END 這條軌道跑完,最後回傳跑完之後的完整 State。


常見問題

ModuleNotFoundError: No module named 'langgraph'

python -m pip install langgraph 確認裝在跟執行腳本相同的 Python 環境,前幾天遇過的環境不一致問題在這裡也可能發生。

KeyError: 'ai_response'

graph.invoke(...) 給的起始 State 少了某個欄位。今天的範例故意連 ai_response 都先給空字串 "",比較保險;如果只給 user_input,有些版本的 LangGraph 一樣能跑,但養成把 State 所有欄位都先給預設值的習慣,之後 State 變複雜時比較不會出錯。

為什麼不直接呼叫 _llm.invoke(...) 就好,繞這一大圈?

今天這個範例本身確實繞了路,因為只有一個 Node。但 Day 12 開始要加「判斷意圖」「查資料庫」「生成計畫」好幾個 Node,還要依照條件決定走哪條路,那時候「一條直線的代碼」會變得很難維護,LangGraph 的圖狀結構才會顯出價值。今天先把最小骨架搭熟,後面加站會快很多。

add_edgeadd_node 的順序重要嗎?

不重要,compile() 之前,先加 Node 或先加 Edge 都可以,LangGraph 會在編譯的時候把整張圖組裝起來檢查有沒有問題。

graph.invoke() 跑很慢

第一次呼叫要等 Ollama 把模型載進記憶體,跟 Day 8、Day 9 遇到的暖機情況一樣,不是卡住。


進度回顧

今天把 Agent 的骨架真正組出來了。搞懂了 State、Node、Edge 這三個概念,寫出第一張能運行的 StateGraph,輸入文字進去,經過一個呼叫 Ollama 的 Node,拿到回應輸出。雖然今天的圖只有一站,但這就是接下來所有 Agent 功能的地基。

系統現在是這樣的:

Day 1 ✓ 產品定義完成
Day 2 ✓ 開發環境準備
Day 3 ✓ 專案架構設計
Day 4 ✓ 資料庫設計
Day 5 ✓ SQLite 資料庫建置
Day 6 ✓ FastAPI 基礎
Day 7 ✓ 使用者檔案 API
Day 8 ✓ 理解 LLM Agent 的本質
Day 9 ✓ 連接 Ollama 本機模型
Day 10 ✓ LangGraph 最小範例(今天)
Day 11 ⬜ Thread 與 State 管理

今天的圖跑完一次就結束了,沒有記憶。明天要幫這張圖加上記憶功能,讓使用者能進行多輪對話,關掉程式重開,對話歷史還能接得回來。


上一篇
Day 9:連接 Ollama 本機模型 - 拿到結構化回應
系列文
30天用 Claude Code + LangGraph 實作個人化 AI 學習教練10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言