15. Membangun Graph Pertama Anda dengan LangGraph
Di Bagian IV, kita mendefinisikan alat, menghubungkannya ke LLM, dan menyelesaikan sebuah loop agen yang mengulang siklus putuskan-dan-eksekusi. Menggerakkan loop, menjalankan alat ketika LLM memintanya, mengetahui kapan harus berhenti — kita menulis kode setiap bagian dari alur tersebut secara manual.
Di bab ini, kita akan membangun agen yang sama dengan cara yang sepenuhnya berbeda. Alih-alih menulis alurnya secara langsung, kita akan mendaftarkan langkah (node) dan aturan koneksi (edge) ke framework LangGraph dan membiarkannya menangani eksekusi. Perilakunya identik dengan Bab 14, tetapi cara kita membangunnya berubah.
Bab ini membahas empat konsep inti — StateGraph, node, edge, dan State — lalu me-refactor loop agen dari Bab 14 menjadi graph LangGraph. Di bab-bab berikutnya, Bab 16 membahas conditional routing dan komponen prebuilt, dan Bab 17 membahas persistensi state yang memungkinkan agen melanjutkan dari titik saat ia terhenti.
15.1) Mengapa Graph?
15.1.1) Keterbatasan Loop Agen yang Sudah Ada
Mari kita tinjau kembali loop agen dari Bab 14. Dengan menghilangkan penanganan error dan detail lainnya, struktur intinya tampak seperti ini:
# Loop agen Bab 14 — struktur inti (disederhanakan)
messages = [
SystemMessage(content="You are a helpful assistant."),
HumanMessage(content=user_input),
]
for step in range(max_steps):
# Minta LLM untuk memutuskan aksi berikutnya
ai_message = llm_with_tools.invoke(messages)
messages.append(ai_message)
# Jika tidak ada tool call yang diminta, kembalikan jawaban akhir
if not ai_message.tool_calls:
return ai_message.content
# Jalankan alat yang diminta
for tool_call in ai_message.tool_calls:
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)Kode ini hanya mencakup dasar-dasarnya dan tidak lebih. Namun, di lingkungan produksi nyata, jauh lebih banyak yang dibutuhkan. Berikut beberapa contohnya.
- Pemulihan dari crash — Jika sebuah agen crash pada langkah ke-7 dari tugas riset 10 langkah, ia seharusnya bisa melanjutkan dari langkah ke-7 alih-alih memulai dari awal.
- Permintaan persetujuan — Sebelum agen melakukan operasi kritis, ia seharusnya bisa berhenti sejenak dan bertanya kepada manusia "Apakah boleh dilanjutkan?"
- Pemantauan real-time — Pengguna seharusnya bisa melihat apa yang sedang dilakukan agen dan alat mana yang sedang dipanggilnya.
- Visualisasi dan debugging — Diagram yang menunjukkan cara kerja agen seharusnya tersedia sehingga ketika masalah muncul, Anda bisa menelusuri langkah mana yang salah.
Mengimplementasikan fitur-fitur ini sendiri bukan hal yang mustahil, tetapi juga tidak mudah. Pemulihan dari crash saja membutuhkan penulisan kode untuk men-serialize state di setiap langkah, menyimpannya ke disk, memulihkannya, dan melanjutkan tepat di posisi yang sama. Anda bisa berakhir dengan lebih banyak kode infrastruktur daripada logika bisnis.
LangGraph dibangun untuk menyediakan fitur-fitur ini di tingkat framework. Pemulihan dari crash, permintaan persetujuan, pemantauan, visualisasi — framework-nya menangani semua ini. Tetapi ada satu syarat: Anda harus membangun agen Anda dalam struktur yang bisa dipahami oleh framework.
Loop agen Bab 14 menangani semua logika secara langsung, jadi tidak ada tempat bagi framework untuk terhubung. Untuk memanfaatkan apa yang ditawarkan LangGraph, kita perlu membangun kembali agen dalam struktur yang dipahami LangGraph — sebuah graph. Itulah yang menjadi inti bab ini.
15.1.2) Apa Itu LangGraph?
LangGraph adalah framework orkestrasi yang mendefinisikan dan menjalankan alur kerja agen sebagai graph. Graph di sini berarti sebuah struktur di mana setiap node (langkah) yang dilakukan agen dihubungkan oleh edge (aturan koneksi).
Di LangGraph, Anda memecah alur kerja menjadi node-node independen dan menghubungkannya dengan edge. LangGraph kemudian menelusuri graph, menjalankan setiap node di sepanjang jalan. Berikut adalah tampilan loop agen Bab 14 yang diekspresikan sebagai graph:
Kotak persegi panjang adalah node, dan panah adalah edge. Belah ketupat merepresentasikan sebuah conditional edge yang bercabang ke jalur berbeda berdasarkan suatu kondisi.
Di Bab 14, seluruh alur kerja berada di dalam loop for, pemeriksaan if, dan kode tulisan tangan lainnya. Dengan LangGraph, Anda mendefinisikan apa yang dilakukan setiap node dan menghubungkan node-node tersebut dengan edge. Singkatnya, Anda beralih dari menuliskan alur kerja menjadi mendeklarasikannya sebagai struktur.
LangGraph tidak menggantikan apa pun yang Anda pelajari di Bab 12–14. Definisi alat, bind_tools(), tool_calls, ToolMessage — semua ini masih digunakan di dalam node, persis seperti sebelumnya.
Bagian berikutnya membahas komponen inti LangGraph — StateGraph, State, node, dan edge — satu per satu.
15.2) Komponen LangGraph: StateGraph, State, Node, Edge
Bagian ini membahas empat komponen inti LangGraph satu per satu. Kita akan mulai dengan StateGraph — kelas yang membungkus State, node, dan edge menjadi sebuah graph — lalu membahas masing-masing bagian (State, node, edge) yang ada di dalamnya.
15.2.1) StateGraph
StateGraph adalah kelas yang digunakan untuk membangun graph di LangGraph. Anda menentukan State yang akan dikelola graph, menambahkan node, menghubungkannya dengan edge, lalu compile untuk menghasilkan graph yang dapat dieksekusi.
Mari lihat cara kerjanya.
Membuat Instance StateGraph
Panggil konstruktor StateGraph untuk membuat sebuah instance. Anda perlu meneruskan skema State (kelasnya sendiri) sebagai parameter. Di sini kita menggunakan MessagesState, sebuah State bawaan yang disediakan LangGraph untuk mengelola daftar pesan. Kita akan membahas detailnya di 15.2.2.
from langgraph.graph import StateGraph, MessagesState
builder = StateGraph(MessagesState)Menambahkan Node
Gunakan add_node() untuk mendaftarkan sebuah node. Node adalah fungsi Python yang menerima State saat ini dan mengembalikan bagian yang ingin diubahnya. Kita akan membahas fungsi node secara detail di 15.2.3.
def say_hello(state: MessagesState):
return {"messages": [{"role": "ai", "content": "hello world"}]}
builder.add_node(say_hello) # nama node menjadi "say_hello"Menghubungkan Edge
Gunakan add_edge(source, target) untuk menghubungkan node. source adalah tempat edge dimulai; target adalah tujuannya. START dan END adalah penanda khusus untuk titik masuk dan keluar graph. Kita akan membahas edge di 15.2.4.
from langgraph.graph import START, END
builder.add_edge(START, "say_hello") # graph dimulai → jalankan say_hello
builder.add_edge("say_hello", END) # say_hello selesai → akhiri graphCompile dan Menjalankan
Memanggil compile() memvalidasi struktur graph dan menghasilkan objek yang dapat dieksekusi. Anda menjalankan graph yang telah di-compile dengan invoke(), meneruskan nilai State awal.
graph = builder.compile()
initial_state = {"messages": [{"role": "user", "content": "hi!"}]}
result = graph.invoke(initial_state)Sekarang mari kita satukan semuanya dan bangun sebuah graph sederhana:
from langgraph.graph import StateGraph, MessagesState, START, END
def say_hello(state: MessagesState):
return {"messages": [{"role": "ai", "content": "hello world"}]}
builder = StateGraph(MessagesState)
builder.add_node(say_hello)
builder.add_edge(START, "say_hello")
builder.add_edge("say_hello", END)
graph = builder.compile()
initial_state = {"messages": [{"role": "user", "content": "hi!"}]}
result = graph.invoke(initial_state)
print(result["messages"][-1].content)Output:
hello worldKetika Anda memanggil invoke(), graph berjalan dengan urutan START → say_hello → END. say_hello mengembalikan sebuah dictionary dengan messages sebagai kunci, dan nilai tersebut ditambahkan ke daftar messages di MessagesState. Kita akan menggali cara kerjanya di 15.2.2. Intinya, mengambil konten pesan terakhir memberi kita "hello world".
15.2.2) State: Data yang Mengalir Melalui Graph
State adalah data yang dibagikan oleh setiap node dalam graph. Ketika sebuah node berjalan, ia menerima State saat ini, melakukan tugasnya, dan mengembalikan hanya bagian yang ingin diubahnya. LangGraph menyatukan kembali perubahan tersebut ke dalam State dan menyerahkan versi yang telah diperbarui ke node berikutnya.
Mendefinisikan State
Anda mendefinisikan State dengan membuat subclass dari TypedDict. Pilih field dan tipe yang sesuai dengan apa yang perlu dilacak agen Anda. Berikut contoh sederhananya:
from typing_extensions import TypedDict
class AgentState(TypedDict):
messages: list # daftar pesan
llm_calls: int # jumlah panggilan LLMDari sini, Anda meneruskan AgentState saat membuat StateGraph dan menggunakannya sebagai type hint untuk fungsi node Anda.
Reducer
Ketika sebuah node mengembalikan sebuah nilai, field State yang bersesuaian akan diperbarui. Perilaku default-nya adalah overwrite — jika sebuah node mengembalikan {"llm_calls": 3}, llm_calls cukup menjadi 3, apa pun nilainya sebelumnya.
Tetapi beberapa field perlu ditambahkan (append), bukan ditimpa (overwrite). Apa yang terjadi jika messages ditimpa? Setiap kali sebuah node mengembalikan pesan baru, seluruh riwayat percakapan lenyap. Untuk messages, append adalah perilaku yang tepat.
LangGraph memungkinkan Anda mengatur strategi pembaruan yang berbeda per field melalui fungsi reducer. Anda menentukan reducer sebagai argumen kedua dalam Annotated:
from typing_extensions import TypedDict, Annotated
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages] # reducer: append
llm_calls: int # tanpa reducer: overwriteadd_messages adalah reducer yang disediakan LangGraph. Alih-alih mengganti daftar, ia menambahkan pesan baru ke apa yang sudah ada. Karena reducer ini diatur pada field messages, nilai apa pun yang dikembalikan sebuah node untuk messages akan ditambahkan. llm_calls tidak memiliki reducer, jadi nilai yang dikembalikan cukup menimpa apa pun yang ada sebelumnya.
Inilah alasan pesan yang dikembalikan say_hello di 15.2.1 ditambahkan ke messages alih-alih menggantikannya — reducer yang menanganinya.
MessagesState
LangGraph hadir dengan State bawaan bernama MessagesState. Berikut tampilannya di balik layar:
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]Struktur yang sama yang baru saja kita bahas — sebuah field messages dengan reducer add_messages yang sudah terpasang.
Jika Anda membutuhkan field tambahan, cukup buat subclass-nya:
from langgraph.graph import MessagesState
class AgentState(MessagesState):
llm_calls: int # perilaku default overwrite15.2.3) Node: Fungsi yang Memperbarui State
Sebuah node adalah fungsi Python yang melakukan satu tugas spesifik di dalam graph.
def say_hello(state: MessagesState):
return {"messages": [{"role": "ai", "content": "hello world"}]}Dua hal yang perlu diketahui saat menulis fungsi node:
Aturan 1: Node menerima State saat ini sebagai argumennya. LangGraph meneruskan objek State saat ini ketika menjalankan node tersebut.
Aturan 2: Node hanya mengembalikan bagian yang ingin diubah, bukan seluruh State. Node tidak memodifikasi State secara langsung. Cukup kembalikan field yang ingin Anda perbarui, dan LangGraph menggabungkannya ke dalam State yang ada sesuai aturan reducer masing-masing field.
Gunakan add_node() untuk menambahkan node ke StateGraph:
builder.add_node(say_hello) # nama fungsi "say_hello" menjadi nama node
builder.add_node("my_node", my_func) # Anda juga bisa menentukan namanya secara eksplisit15.2.4) Edge: Aturan yang Menghubungkan Node
Sebuah edge menentukan "setelah node ini selesai, apa yang berjalan berikutnya?" Ada dua jenis.
Edge Normal
Edge normal menghubungkan "go-to" yang tetap antara dua node. Gunakan add_edge(source, target) — source adalah node awal, target adalah tujuan.
builder.add_edge(START, "say_hello") # ketika graph dimulai, jalankan say_hello
builder.add_edge("say_hello", "llm_call") # setelah say_hello, jalankan llm_call
builder.add_edge("llm_call", END) # setelah llm_call, akhiri graphConditional Edge
Sebuah conditional edge memilih node berikutnya saat runtime berdasarkan State saat ini. Gunakan add_conditional_edges(source, routing_function) — source adalah node awal, dan routing_function adalah fungsi yang menerima State saat ini dan mengembalikan nama node berikutnya:
from langgraph.graph import END
def should_continue(state: AgentState):
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tool_node" # tool call diminta → pergi ke tool_node
return END # tidak ada tool call → akhiri
builder.add_conditional_edges("llm_call", should_continue)add_conditional_edges("llm_call", should_continue) memberi tahu LangGraph: "ketika llm_call selesai, panggil should_continue untuk memutuskan apa yang berjalan berikutnya." should_continue mengarahkan ke "tool_node" jika pesan terakhir memiliki tool_calls, atau ke END jika tidak. Dalam praktiknya, itu berarti graph melanjutkan ke node eksekusi alat ketika LLM meminta tool call, dan berhenti ketika tidak.
Sekarang setelah kita membahas keempat komponen, bagian berikutnya menggunakannya untuk me-refactor loop agen Bab 14 menjadi graph LangGraph.
15.3) Me-refactor Loop Agen Menjadi Graph
Mari kita bangun kembali loop agen Bab 14 menggunakan LangGraph. Perilakunya identik dengan Bab 14 — LLM memutuskan, alat dieksekusi berdasarkan permintaan LLM, dan siklusnya berulang hingga selesai. Satu-satunya yang berubah adalah bagaimana kita menyusun alur ini.
Berikut tampilan graph yang sudah jadi nanti:
Graph berputar antara llm_call dan tool_node hingga LLM berhenti meminta tool call, pada saat itu ia keluar ke END. Mari kita bangun langkah demi langkah.
15.3.1) Mendefinisikan State
Kita membuat subclass dari MessagesState dari 15.2.2 untuk mendefinisikan State agen. Field messages diwarisi dari MessagesState, dan kita menambahkan field llm_calls untuk melacak jumlah panggilan LLM.
from langgraph.graph import MessagesState
class AgentState(MessagesState):
llm_calls: int # jumlah panggilan LLM (overwrite)Daftar messages akan mengumpulkan input pengguna (HumanMessage), respons LLM (AIMessage), dan hasil eksekusi alat (ToolMessage) secara berurutan.
15.3.2) Membangun Node
Pertama, mari kita siapkan alat dan model dari Bab 14:
from langchain_openai import ChatOpenAI
from langchain_core.messages import ToolMessage
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Mendapatkan cuaca saat ini untuk sebuah kota."""
fake_data = {"Tokyo": "18°C, cloudy", "Cairo": "31°C, sunny"}
return fake_data.get(city, f"No weather data for {city}.")
@tool
def calculate(expression: str) -> str:
"""Menghitung ekspresi aritmetika sederhana. Contoh: '3 * 21'."""
return str(eval(expression)) # Peringatan: eval() adalah risiko keamanan. Jangan gunakan di produksi.
tools = [get_weather, calculate]
tool_map = {t.name: t for t in tools}
llm = ChatOpenAI(model="gpt-5-mini")
model_with_tools = llm.bind_tools(tools)Sekarang mari kita tulis dua fungsi node.
Node llm_call — Memanggil LLM dan mengembalikan responsnya:
def llm_call(state: AgentState):
"""Memanggil LLM dan mengembalikan responsnya."""
response = model_with_tools.invoke(state["messages"])
return {
"messages": [response],
"llm_calls": state.get("llm_calls", 0) + 1,
}model_with_tools.invoke() memanggil LLM, dan responsnya dibungkus di bawah kunci messages dalam dictionary yang dikembalikan. Reducer menambahkannya ke messages yang ada di AgentState. llm_calls mengembalikan jumlah saat ini ditambah 1, menimpa nilai sebelumnya.
Node tool_node — Menjalankan alat yang diminta LLM dan mengembalikan hasilnya:
def tool_node(state: AgentState):
"""Menjalankan alat yang diminta oleh LLM."""
last_message = state["messages"][-1]
results = []
for tool_call in last_message.tool_calls:
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
results.append(tool_message)
return {"messages": results}Karena tool_node selalu berjalan tepat setelah llm_call, pesan terakhir di messages dijamin adalah AIMessage yang baru saja dihasilkan LLM. Field tool_calls pada pesan tersebut berisi tool call yang diminta LLM. Node menjalankan setiap alat, mengumpulkan hasilnya di results, dan mengembalikannya di bawah kunci messages — reducer yang mengurus penambahannya ke daftar yang ada.
15.3.3) Conditional Edge
Setelah llm_call selesai, kita membutuhkan conditional edge untuk memutuskan apakah akan menjalankan tool_node atau mengakhiri graph. Ini mengikuti pola yang sama dari 15.2.4:
from typing import Literal
from langgraph.graph import END
def should_continue(state: AgentState) -> Literal["tool_node", "__end__"]:
"""Memutuskan apakah akan menjalankan alat atau mengakhiri graph."""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tool_node"
return ENDJika last_message.tool_calls ada, LLM sedang meminta tool call, jadi kita mengarahkan ke tool_node. Jika tidak, kita mengarahkan ke END dan graph berhenti.
Type hint return
Literal["tool_node", "__end__"]mendeklarasikan tujuan yang mungkin dikembalikan fungsi ini. LangGraph membutuhkan hint ini untuk menggambar jalur conditional edge dengan benar dalam visualisasi graph. Ini tidak berpengaruh pada perilaku runtime.
"__end__"adalah nilai string dasar dariEND. KarenaLiteralhanya menerima literal string, kita menulis"__end__"alih-alihEND.
15.3.4) Merakit dan Menjalankan Graph
Saatnya menghubungkan semuanya. Mari kita rakit State, node, dan conditional edge menjadi sebuah StateGraph dan compile:
from langgraph.graph import StateGraph, START, END
builder = StateGraph(AgentState)
builder.add_node("llm_call", llm_call)
builder.add_node("tool_node", tool_node)
builder.add_edge(START, "llm_call") # start → llm_call
builder.add_conditional_edges("llm_call", should_continue) # llm_call → tool_node atau END
builder.add_edge("tool_node", "llm_call") # tool_node → llm_call (loop)
agent = builder.compile()Edge dari tool_node kembali ke llm_call membuat sebuah loop. Eksekusi terus berputar hingga LLM merespons dengan jawaban akhir alih-alih meminta tool call lain, pada saat itu loop keluar.
Mari kita jalankan:
from langchain_core.messages import HumanMessage
result = agent.invoke({
"messages": [HumanMessage(content="Get the temperature in Cairo, then multiply the number by 3.")],
"llm_calls": 0,
})
print(result["messages"][-1].content)
print(f"\nTotal LLM calls: {result['llm_calls']}")Output:
Current temperature in Cairo: 31°C. Multiplied by 3 = 93.
Total LLM calls: 3Agen memanggil get_weather("Cairo"), melihat hasilnya, memanggil calculate("31 * 3"), dan menghasilkan jawaban akhir — hasil yang sama yang kita dapatkan di Bab 14.
Memeriksa riwayat pesan lengkap menunjukkan setiap langkah terekam di messages, secara berurutan:
for message in result["messages"]:
message.pretty_print()Output:
================================ Human Message =================================
Get the temperature in Cairo, then multiply the number by 3.
================================== Ai Message ==================================
Tool Calls:
get_weather (call_DiL9WF)
Args:
city: Cairo
================================= Tool Message =================================
Name: get_weather
31°C, sunny
================================== Ai Message ==================================
Tool Calls:
calculate (call_wa6RqWST)
Args:
expression: 31 * 3
================================= Tool Message =================================
Name: calculate
93
================================== Ai Message ==================================
Current temperature in Cairo: 31°C. Multiplied by 3 = 93.15.3.5) Visualisasi Graph
Di dalam notebook Jupyter, agent.get_graph().draw_mermaid_png() merender struktur graph sebagai gambar langsung di output cell.
from IPython.display import Image, display
display(Image(agent.get_graph().draw_mermaid_png()))Di lingkungan terminal, simpan sebagai file PNG saja.
agent.get_graph().draw_mermaid_png(output_file_path="agent_graph.png")Gambar yang dihasilkan:
Garis solid adalah edge normal dan garis putus-putus adalah conditional edge. Diagram ini dibuat secara otomatis dari kode.
15.3.6) Recursion Limit
Sama seperti kita menggunakan max_steps untuk menjaga dari loop tak terbatas di Bab 14, LangGraph memiliki jaring pengaman bawaan. Setiap kali sebuah node berjalan selama eksekusi graph, sebuah counter internal bertambah satu. Ketika counter tersebut melampaui batas yang dikonfigurasi, LangGraph memunculkan GraphRecursionError.
Untuk melihat cara kerja penghitungannya, lihat run sebelumnya. Memanggil get_weather dan calculate mengunjungi node dengan urutan ini:
llm_call(1) → tool_node(2) → llm_call(3) → tool_node(4) → llm_call(5) → END
Itu total 5 kunjungan node. Jika Anda mengatur recursion_limit ke 3, batas mulai berlaku pada kunjungan ke-3 dan run terpotong lebih awal:
from langgraph.errors import GraphRecursionError
try:
result = agent.invoke(
{"messages": [HumanMessage(content="Get the temperature in Cairo, then multiply the number by 3.")],
"llm_calls": 0},
config={"recursion_limit": 3},
)
except GraphRecursionError:
print("Agent hit the recursion limit — stopping execution.")Output:
Agent hit the recursion limit — stopping execution.Atur batasnya dengan meneruskan config={"recursion_limit": number} ke invoke(). Nilai yang tepat bergantung pada kasus penggunaan Anda dan kompleksitas graph Anda. Mulailah dengan angka yang murah hati dan sesuaikan melalui pengujian.