在這篇文章中,您將學習如何使用 LangGraph 在 Python 中建置完整 agentic 工作流程,從單一模型呼叫到具工具使用能力且具持久對話記憶的代理人。
我們將涵蓋的主題包括:
- 狀態、節點與邊如何結合以定義 LangGraph 代理人的執行流程。
- 如何註冊工具並透過圖的推理迴圈路由模型的工具呼叫。
- 檢查點器如何在不同的圖呼叫之間保留對話歷史。
讓我們不要再浪費時間。

簡介
大多數 AI 代理人 設定都能很好地處理單輪情境:接收問題、呼叫模型,然後回傳答案。較困難的問題很快就會出現。代理人可能需要查詢您的資料庫、記住先前訊息的脈絡,或讓您清楚看見模型究竟做了什麼決定以及原因。若要解決這些挑戰,卻又不想為每個使用情境打造自訂管線,許多實作便會在此階段開始崩壞。
LangGraph 為處理上述每個問題提供了乾淨的結構。代理人以圖形呈現,其中節點是工作單元,邊定義下一步要執行什麼,而共享狀態物件則將完整訊息歷史帶過每一步。模型在節點內執行,因此每一次推理步驟、工具呼叫與回應都成為圖狀態的一部分。這使得整個執行流程可見、可檢視,並可供後續執行的任何節點使用。
在這篇文章中,您將學習如何理解每個 LangGraph 圖的基礎狀態、節點與邊原語;使用 MessagesState 自動管理對話歷史;在節點內呼叫語言模型並將其連接到圖;註冊工具並將工具呼叫路由回模型;追蹤完整訊息序列以清楚看見模型在每個步驟的行為;以及使用檢查點器在不同呼叫之間保留對話。我們將從頭建置此圖,從安裝步驟開始。
設定環境
安裝所需套件:
pip install langgraph langchain-openai python-dotenv |
接著在專案根目錄建立 .env 檔案,並放入您的 OpenAI API 金鑰:
OPENAI_API_KEY="your_key_here" |
在任何 LangChain 或 LangGraph 匯入之前,於腳本最上方載入它:
from dotenv import load_dotenv load_dotenv() |
python-dotenv 會讀取 .env 檔案,並將金鑰設定為環境變數。
理解狀態、節點與邊
每個 LangGraph 圖都由以下三個元件建置而成。一開始就掌握這些元件,可避免圖形變得複雜時產生混淆。
狀態是一個 TypedDict,作為整個圖的共享記憶體。每個節點都會從中讀取資料,並將更新寫回。除此之外,節點之間沒有其他傳遞資料的方式。您未在節點中更新的欄位會維持不變;您只需回傳想要修改的內容。
節點是普通的 Python 函式。節點會將目前狀態作為參數,並回傳想要更新的欄位字典。使用 add_node 註冊函式,即可將其納入圖中,而不需要特殊裝飾器或基底類別。若您只傳遞函式而未提供名稱字串,LangGraph 會自動使用函式名稱。
邊定義執行順序。add_edge(A, B) 的意思是:節點 A 完成後,執行節點 B。add_conditional_edges 的意思是:節點 A 完成後,呼叫路由函式,並前往其指向的位置。每個圖都需要 START 作為進入點,並至少有一條路徑到達 END。
預設情況下,當節點回傳狀態欄位的值時,該值會取代原有內容。對於需要跨節點累積的欄位(例如日誌、訊息歷史),您可以使用 reducer 函式 為欄位加上註解。在以下範例中,串列欄位上的 operator.add 表示附加而非取代:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
from typing import Annotated import operator from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END class TicketState(TypedDict): customer_message: str log: Annotated[list, operator.add] def log_received(state: TicketState) -> dict: return {"log": [f"Received: {state['customer_message']}"]} def log_assigned(state: TicketState) -> dict: return {"log": ["Assigned to support queue"]} builder = StateGraph(TicketState) builder.add_node("log_received", log_received) builder.add_node("log_assigned", log_assigned) builder.add_edge(START, "log_received") builder.add_edge("log_received", "log_assigned") builder.add_edge("log_assigned", END) graph = builder.compile() result = graph.invoke({"customer_message": "My invoice looks wrong", "log": []}) print(result) |
輸出結果如下:
{'customer_message': 'My invoice looks wrong', 'log': ['Received: My invoice looks wrong', 'Assigned to support queue']} |
兩個節點都寫入了 log,且兩筆記錄都存在。customer_message 維持不變,因為兩個節點都沒有回傳它。這正是 MessagesState 處理其 messages 欄位的方式,使用稍為專門的 reducer add_messages,它同時處理訊息物件的去重與排序。
使用 MessagesState 管理對話歷史
LangGraph 圖中的每個節點都會讀取目前狀態,並將更新寫回狀態。對於對話式代理人而言,狀態需要攜帶完整的訊息歷史——使用者輸入、模型回應、工具輸出——讓模型在決定下一步時永遠擁有必要脈絡。
LangGraph 內建了專門用於此目的的狀態類型:MessagesState。它是一個 TypedDict,包含單一 messages 欄位,並使用 add_messages reducer 而非直接覆寫。每次節點回傳新訊息時,都會附加到現有清單,而非取代它。您無需手動拼接對話歷史。
from langgraph.graph import MessagesState |
這是大多數單一代理人圖所需的狀態定義。您可以擴充額外欄位,例如 customer_id、priority 旗標,或任何您的節點需要的內容。但 messages 欄位已存在且已設定為累積模式。

在節點內呼叫模型
狀態就緒後,任何 LangGraph 代理人的核心節點就是將目前訊息清單傳給模型,並附加其回應的函式。模型會回傳 AIMessage;只要將它放在以「messages」為鍵的字典中回傳,即可將它加入狀態。
from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage llm = ChatOpenAI(model="gpt-4o-mini") def run_model(state: MessagesState) -> dict: system = SystemMessage("You are a support agent for a SaaS product. " "Be concise and helpful.") response = llm.invoke([system] + state["messages"]) return {"messages": [response]} |
ChatOpenAI 透過 LangChain 的標準聊天模型介面封裝 OpenAI API。若要切換至其他供應商(Anthropic、Google、透過 Ollama 的本地模型),只需更改匯入與模型字串;節點的其餘部分保持不變。SystemMessage 會在每次呼叫時設定模型的角色,而不會儲存在狀態中,以保持持久歷史的乾淨。
將它接入圖並執行:
from langgraph.graph import StateGraph, START, END from langchain_core.messages import HumanMessage builder = StateGraph(MessagesState) builder.add_node("run_model", run_model) builder.add_edge(START, "run_model") builder.add_edge("run_model", END) graph = builder.compile() result = graph.invoke({"messages": [HumanMessage("My dashboard isn't loading. What should I try?")]}) print(result["messages"][-1].content) |
result["messages"] 是完整清單:原始 HumanMessage 加上模型產生的 AIMessage。[-1] 取得最新的那一筆。
註冊工具並路由工具呼叫
模型可以從訓練資料回答一般性問題,但任何與您的資料相關的特定內容——帳戶細節、訂閱等級、票證歷史——都需要 工具呼叫。模型決定何時需要工具;您的程式碼定義工具的行為。
使用 @tool 裝飾器定義工具:
from langchain_core.tools import tool @tool def get_customer_tier(customer_id: str) -> str: """Look up the subscription tier for a customer by their ID. Returns 'free', 'pro', or 'enterprise'.""" tiers = { "cust_1001": "enterprise", "cust_2002": "pro", "cust_3003": "free", } return tiers.get(customer_id, "not found") |
文件字串是模型在決定是否呼叫此工具以及傳遞什麼參數時會讀取的內容。請保持精確,因為模糊的文件字串會導致遺漏呼叫或格式錯誤的參數。
將工具綁定到模型,讓它知道工具的存在,並更新節點:
tools = [get_customer_tier] llm_with_tools = llm.bind_tools(tools) def run_model(state: MessagesState) -> dict: system = SystemMessage("You are a support agent for a SaaS product. " "Use available tools when you need account-specific information.") response = llm_with_tools.invoke([system] + state["messages"]) return {"messages": [response]} |
bind_tools 會將工具的結構描述與每個請求一起傳送給模型。當模型決定使用工具時,回應會以帶有 tool_calls 欄位的 AIMessage 形式回傳,而非純文字內容。

加入 ToolNode 來處理執行,並設定路由:
from langgraph.prebuilt import ToolNode, tools_condition tool_node = ToolNode(tools) builder = StateGraph(MessagesState) builder.add_node("run_model", run_model) builder.add_node("tools", tool_node) builder.add_edge(START, "run_model") builder.add_conditional_edges("run_model", tools_condition) builder.add_edge("tools", "run_model") graph = builder.compile() |
ToolNode 會讀取最後一個 AIMessage 中的 tool_calls,以模型指定的參數執行對應函式,並將結果包裝成 ToolMessage 附加到狀態。tools_condition 會在每次模型呼叫後檢查最後一個 AIMessage。若 tool_calls 非空,則路由至「tools」,否則路由至「__end__」。從「tools」回到「run_model」的邊會關閉迴圈:它將工具結果送回模型,讓模型產生最終答案。
追蹤推理迴圈
在繼續之前,請思考當模型使用工具時,圖內部實際發生了什麼,因為實際情況比最終輸出所顯示的更多。
result = graph.invoke({"messages": [ HumanMessage("Can you check what plan customer cust_1001 is on?") ]}) for msg in result["messages"]: print(type(msg).__name__, ":", msg.content or msg.tool_calls) |
範例輸出:
HumanMessage : Can you check what plan customer cust_1001 is on? AIMessage : [{'name': 'get_customer_tier', 'args': {'customer_id': 'cust_1001'}, 'id': 'call_Rx7kLmNpQ2wJtA3s', 'type': 'tool_call'}] ToolMessage : enterprise AIMessage : Customer cust_1001 is on the enterprise plan. |
這裡我們有四則訊息與兩次模型呼叫。第一次模型呼叫產生帶有 tool_calls 且 content 為空的 AIMessage。模型正在表示它想要做什麼,而不是立即回答。tools_condition 看到這一點,路由至 ToolNode,ToolNode 執行 get_customer_tier("cust_1001") 並附加帶有結果的 ToolMessage。
回到 run_model 的邊再次觸發。此時模型已擁有先前三則訊息的脈絡,了解查詢成功,並寫入帶有答案的最終 AIMessage。tools_condition 再次執行,發現沒有工具呼叫,結束圖形。
這個迴圈——模型呼叫、工具執行、再次模型呼叫——是標準的 ReAct 模式。每次使用工具都需要兩次模型呼叫:一次決定要查詢什麼,一次解讀結果。當您新增更多工具時,這是思考延遲與成本時需要了解的重要事項。
跨呼叫保留對話
上述每個 graph.invoke() 都從全新的圖狀態開始。若無 persistence,模型不會記住先前的對話。
若要在呼叫之間保留狀態,請在編譯圖時附加檢查點器:
from langgraph.checkpoint.memory import InMemorySaver checkpointer = InMemorySaver() graph = builder.compile(checkpointer=checkpointer) |
接著在每次呼叫時傳入相同的 thread_id:
config = {"configurable": {"thread_id": "ticket-7741"}} graph.invoke( {"messages": [HumanMessage("Hi, I can't access my account.")]}, config, ) result = graph.invoke( {"messages": [HumanMessage("My ID is cust_2002, can you check my plan?")]}, config, ) print(result["messages"][-1].content) |
範例輸出:
You're on the pro plan, cust_2002. Since you're having trouble accessing your account, I'd recommend resetting your password first. Pro accounts also have priority support available if the issue continues. |
第二次呼叫可以看到第一次的對話,因為檢查點器在執行前還原了該執行緒的狀態,並在執行後儲存了更新後的狀態。使用不同的 thread_id 會從一個獨立的空狀態開始。
InMemorySaver 將檢查點儲存在處理程序記憶體中,適合開發與測試使用。在正式環境中,您通常會將它替換為由資料庫或其他持久儲存支援的檢查點器。您的圖形程式碼其餘部分保持不變。

檢查點器 會為執行緒保留 圖狀態。若您的應用程式還需要獨立於對話之外保留資料,例如使用者設定檔、偏好設定,或跨多個執行緒共享的長期記憶,請使用 Store。Stores 透過提供持久的應用程式層級儲存來補充檢查點器,圖形可在執行期間存取這些儲存。
結語
在這篇文章中,您從頭建置了一個完整的 LangGraph 代理人。過程中,您學習了狀態如何流經圖形、節點如何執行工作、工具如何融入執行迴圈,以及檢查點器如何在不同呼叫之間保留對話。這些相同的建置區塊可從簡單聊天機器人擴展到更複雜的代理人工作流程。
LangGraph 的優勢之一是每個元件都是獨立的。您可以更換語言模型、註冊新工具,或更改對話保留方式,而無需重新設計圖形的其餘部分。所有內容都透過共享狀態溝通,讓圖形可預測且易於擴展。
這些概念也適用於 多代理人系統。協調器將請求路由至專門代理人,仍然是一個具有狀態、節點與條件邊的圖。架構會變得更大,但底層原語保持不變。
若您想進一步探索,以下資源是不錯的起點:
- LangGraph persistence docs
- Tool calling with LangChain
- MessagesState and add_messages
- LangGraph prebuilt components
Happy building!
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.