Agentic AI dengan LangGraph
Perbedaan Chain, Agent, dan Graph, mendefinisikan tool dengan benar, membangun graph LangGraph, siklus ReAct, dan debugging agent yang tidak mau menggunakan tool.
Sistem RAG yang dibangun di Bab 13 sangat baik untuk satu tugas: menjawab pertanyaan berdasarkan dokumen. Tapi banyak use case nyata membutuhkan lebih dari itu, AI yang bisa memutuskan sendiri tindakan apa yang perlu diambil, memanggil tools eksternal, dan mengeksekusi alur kerja yang bercabang berdasarkan kondisi.
Inilah yang disebut Agentic AI, dan LangGraph adalah framework yang paling terstruktur untuk membangunnya.
14.1 Perbedaan Chain, Agent, dan Graph
Sebelum masuk ke kode, penting untuk memahami tiga paradigma orkestrasi yang berbeda karena pilihan di antara ketiganya menentukan fleksibilitas dan kompleksitas sistem.
Chain digunakan ketika alur sudah pasti dan tidak perlu bercabang. Setiap langkah selalu dieksekusi dalam urutan yang sama. Sederhana dan mudah diprediksi, tapi tidak bisa menangani kondisi yang berbeda.
Agent memberi LLM kebebasan penuh untuk memilih tool mana yang digunakan dan kapan. Sangat fleksibel tapi sulit diprediksi dan sulit di-debug karena alurnya ditentukan sepenuhnya oleh keputusan model.
Graph adalah titik tengah yang paling cocok untuk produksi: alur bisa bercabang berdasarkan kondisi, tapi setiap cabang didefinisikan secara eksplisit oleh developer. LLM hanya membuat keputusan di titik percabangan yang sudah ditentukan, bukan di setiap langkah.
14.2 Konsep Dasar LangGraph
LangGraph membangun sistem AI sebagai directed graph, sebuah jaringan node yang terhubung melalui edge. Tiga konsep yang perlu dipahami:
14.3 Mendefinisikan Tool dengan Benar
Ini adalah bagian yang paling sering menjadi penyebab agent tidak mau menggunakan tool yang sudah dibuat. LLM memutuskan apakah akan menggunakan sebuah tool berdasarkan docstring fungsi tersebut, bukan dari nama fungsinya, bukan dari nama parameternya.
Ada empat elemen yang harus ada dalam setiap definisi tool agar LLM bisa menggunakannya dengan benar:
Definisi Buruk
LLM tidak bisa memutuskan kapan dan bagaimana menggunakannya. Hasilnya: agent mengabaikan tool atau mengirim input dalam format yang salah.
from langchain.tools import tool
# BURUK: tidak ada docstring sama sekali
@tool
def cek_pesanan(nomor):
return database.get_order(nomor)
# BURUK: docstring terlalu pendek dan tidak informatif
@tool
def cari_produk(query):
"""Cari produk."""
return product_db.search(query)
# BURUK: tidak ada type hint, LLM tidak tahu format input
@tool
def cek_kebijakan(topik):
"""Ambil kebijakan toko."""
return kebijakan_db[topik]
# Akibat dari ketiga definisi di atas:
# - Agent tidak tahu kapan memilih tool mana
# - Agent mungkin mengirim "nomor pesanan ORD-2024-789"
# padahal fungsi hanya menerima string "ORD-2024-789"
# - Agent tidak tahu nilai valid untuk parameter 'topik'
Definisi Baik
LLM mendapat semua informasi yang dibutuhkan untuk memutuskan kapan menggunakannya dan dalam format apa input harus dikirim.
from langchain.tools import tool
@tool
def cek_pesanan(nomor_pesanan: str) -> dict:
"""
Gunakan tool ini ketika pengguna menanyakan status,
lokasi, atau estimasi tiba pesanan mereka.
JANGAN gunakan untuk pertanyaan tentang produk atau kebijakan.
Args:
nomor_pesanan: Nomor pesanan dalam format ORD-YYYY-NNNNN.
Ekstrak dari pesan pengguna jika disebutkan.
Contoh valid: "ORD-2024-00789"
Returns:
dict berisi: status, estimasi_tiba, lokasi_terakhir.
Jika tidak ditemukan, returns {"error": "pesan error"}.
"""
hasil = database.get_order(nomor_pesanan)
if not hasil:
return {"error": f"Pesanan {nomor_pesanan} tidak ditemukan"}
return hasil
@tool
def cari_produk(kata_kunci: str, batas: int = 5) -> list[dict]:
"""
Gunakan tool ini ketika pengguna ingin mencari produk,
menanyakan harga, ketersediaan, atau membandingkan produk.
JANGAN gunakan untuk pertanyaan tentang pesanan atau kebijakan.
Args:
kata_kunci: Nama atau deskripsi produk. Bisa berupa nama
merek, kategori, atau deskripsi fitur.
Contoh: "meja belajar kayu", "kursi ergonomis"
batas: Jumlah maksimum hasil (default 5, maksimum 10).
Returns:
list of dict, setiap item berisi: nama, harga, stok, url.
"""
return product_db.search(kata_kunci, limit=batas)
Kumpulan Tools Lengkap
Kumpulan tiga tools untuk customer service agent, masing-masing dengan docstring lengkap yang saling menjelaskan perbedaan use case-nya.
from langchain.tools import tool
@tool
def cek_pesanan(nomor_pesanan: str) -> dict:
"""
Cek status dan detail pesanan berdasarkan nomor pesanan.
Gunakan ketika pengguna menyebut nomor pesanan atau bertanya:
'di mana pesanan saya', 'kapan sampai', 'status pengiriman'.
Args:
nomor_pesanan: Format ORD-YYYY-NNNNN. Contoh: ORD-2024-00789.
Returns:
dict: {status, estimasi_tiba, kurir, nomor_resi}
"""
return database.get_order(nomor_pesanan) or {"error": "Tidak ditemukan"}
@tool
def cari_produk(kata_kunci: str) -> list[dict]:
"""
Cari produk berdasarkan nama atau deskripsi.
Gunakan untuk: mencari produk, cek harga, cek stok.
JANGAN gunakan untuk pertanyaan tentang pesanan atau kebijakan.
Args:
kata_kunci: Nama atau deskripsi produk yang dicari.
Returns:
list of dict: [{nama, harga, stok, deskripsi}]
"""
return product_db.search(kata_kunci, limit=5)
@tool
def cek_kebijakan(topik: str) -> str:
"""
Ambil informasi kebijakan toko berdasarkan topik.
Gunakan untuk pertanyaan tentang: pengembalian, garansi,
pembayaran, pengiriman, atau kebijakan umum toko.
Args:
topik: Pilih salah satu nilai berikut:
- "pengembalian" : kebijakan retur barang
- "garansi" : informasi garansi produk
- "pembayaran" : metode pembayaran yang diterima
- "pengiriman" : biaya dan estimasi pengiriman
- "umum" : kebijakan umum toko
Returns:
str: teks kebijakan yang relevan.
"""
kebijakan = {
"pengembalian": "Barang dapat dikembalikan dalam 30 hari...",
"garansi": "Semua produk bergaransi resmi 1 tahun...",
"pembayaran": "Menerima transfer, kartu kredit, e-wallet...",
"pengiriman": "Gratis ongkir untuk pembelian di atas Rp200.000...",
"umum": "Toko beroperasi Senin-Sabtu pukul 09.00-17.00...",
}
return kebijakan.get(topik, "Silakan hubungi customer service kami.")
# Kumpulkan semua tools — urutan tidak berpengaruh
TOOLS = [cek_pesanan, cari_produk, cek_kebijakan]
14.4 Membangun Graph dengan LangGraph
Sekarang semua komponen sudah siap: tools, state, dan model. Berikut cara merakitnya menjadi graph yang berfungsi.
State dan Model
State adalah struktur data yang menyimpan semua informasi yang dibutuhkan agent untuk memutuskan langkah selanjutnya. Model adalah LLM yang sudah terikat dengan tools.
Langkah pertama: definisikan State (struktur data yang mengalir antar node) dan bind tools ke model agar LLM mengetahui tools yang tersedia.
import os
from typing import Annotated
from typing_extensions import TypedDict
from langchain_google_genai import ChatGoogleGenerativeAI
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from dotenv import load_dotenv
load_dotenv()
# add_messages: fungsi reducer yang menggabungkan
# pesan baru dengan pesan yang sudah ada (bukan menimpa)
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
# Model dengan tools terikat
# Langkah kritis: bind_tools memberi tahu LLM
# tools apa yang tersedia dan kapan memanggilnya
llm = ChatGoogleGenerativeAI(
model="gemini-1.5-flash",
google_api_key=os.getenv("GEMINI_API_KEY"),
temperature=0.1
)
# TOOLS harus sudah didefinisikan sebelum baris ini
llm_with_tools = llm.bind_tools(TOOLS)
Definisi Node
Node adalah unit eksekusi dalam graph. Node menerima state, memprosesnya, dan mengembalikan state baru. Node bisa berupa fungsi Python biasa atau node bawaan LangGraph.
Definisikan dua node utama: node agent yang memanggil LLM, dan node tools yang mengeksekusi tool yang dipilih LLM. Tambahkan fungsi routing untuk menentukan apakah perlu memanggil tool atau sudah selesai.
# Node 1: Agent
# Node ini memanggil LLM dan mendapat keputusan:
# apakah perlu memanggil tool atau langsung jawab
def node_agent(state: AgentState) -> dict:
\"\"\"LLM memproses pesan dan memutuskan tindakan.\"\"\"
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
# Node 2: Tools
# ToolNode adalah node bawaan LangGraph yang
# mengeksekusi tool yang diminta LLM secara otomatis
node_tools = ToolNode(TOOLS)
# Routing function
# Fungsi ini menentukan edge mana yang diikuti
# setelah node_agent selesai berjalan
def routing(state: AgentState) -> str:
\"\"\"
Periksa apakah LLM meminta tool call atau sudah selesai.
Kembalikan nama node berikutnya sebagai string.
\"\"\"
pesan_terakhir = state["messages"][-1]
# Jika LLM meminta tool call -> eksekusi tools
if hasattr(pesan_terakhir, "tool_calls") and pesan_terakhir.tool_calls:
return "tools"
# Jika tidak ada tool call -> selesai, kirim jawaban
return END
Rakit Graph
Rakit semua komponen menjadi graph: tambahkan node, tentukan edge, dan set titik masuk. Pola ReAct (Reason -> Act -> Observe) diimplementasikan melalui loop antara node_agent dan node_tools.
graph_builder = StateGraph(AgentState)
# Tambahkan node ke graph
graph_builder.add_node("agent", node_agent)
graph_builder.add_node("tools", node_tools)
# Edge dari START ke agent (titik masuk selalu agent)
graph_builder.add_edge(START, "agent")
# Conditional edge dari agent:
# - Jika routing() return "tools" -> ke node tools
# - Jika routing() return END -> selesai
graph_builder.add_conditional_edges(
"agent",
routing,
{"tools": "tools", END: END}
)
# Setelah tools selesai -> kembali ke agent
# (agar LLM bisa memproses hasil tool dan memutuskan
# apakah perlu tool lagi atau sudah bisa menjawab)
graph_builder.add_edge("tools", "agent")
# Kompilasi graph menjadi objek yang bisa dijalankan
agent = graph_builder.compile()
# Alur: START -> agent -> (tools -> agent)* -> END
Jalankan
Dengan graph sudah siap, kita bisa membuat fungsi pembungkus untuk mengirim pertanyaan ke agent dan menerima jawaban akhir. Fungsi ini menyiapkan state awal, memanggil agent, dan mengembalikan jawaban terakhir.
Cara menjalankan agent dan membaca hasilnya. Perhatikan cara state mengalir dan bagaimana membaca jawaban akhir dari riwayat pesan.
from langchain_core.messages import HumanMessage, SystemMessage
SYSTEM_PROMPT = """
Kamu adalah asisten customer service ReWork Store.
Gunakan tools yang tersedia untuk menjawab pertanyaan pelanggan.
Jika informasi tidak tersedia melalui tools, katakan dengan jujur.
Gunakan Bahasa Indonesia yang sopan dan ringkas.
"""
def tanya_agent(pertanyaan: str) -> str:
"""
Kirim pertanyaan ke agent dan kembalikan jawaban akhir.
"""
pesan_awal = [
SystemMessage(content=SYSTEM_PROMPT),
HumanMessage(content=pertanyaan)
]
# Jalankan graph
hasil = agent.invoke({"messages": pesan_awal})
# Jawaban akhir ada di pesan terakhir
return hasil["messages"][-1].content
# ── Contoh penggunaan ────────────────────────────
# Kasus 1: Pertanyaan yang butuh tool cek_pesanan
print(tanya_agent("Pesanan saya ORD-2024-00789 sudah sampai mana?"))
# Agent: memanggil cek_pesanan("ORD-2024-00789") → menjawab
# Kasus 2: Pertanyaan yang butuh tool cari_produk
print(tanya_agent("Ada kursi ergonomis yang harganya di bawah 500 ribu?"))
# Agent: memanggil cari_produk("kursi ergonomis") → menjawab
# Kasus 3: Pertanyaan yang bisa dijawab langsung (tanpa tool)
print(tanya_agent("Selamat siang, apa yang bisa dibantu?"))
# Agent: menjawab langsung tanpa memanggil tool
# Kasus 4: Pertanyaan yang butuh dua tool sekaligus
print(tanya_agent("Bagaimana kebijakan pengembalian, dan ada produk meja apa saja?"))
# Agent: memanggil cek_kebijakan("pengembalian") DAN
# cari_produk("meja") → menggabungkan jawaban
14.5 Siklus Eksekusi Agent
Sebelum masuk ke debugging, penting memahami siklus yang terjadi di balik layar setiap kali agent dijalankan. Pola ini disebut ReAct (Reason -> Act -> Observe) dan merupakan inti dari cara LangGraph menangani tool calling.
Yang penting dipahami dari siklus ini: agent bisa melewati siklus Reason -> Act -> Observe lebih dari satu kali untuk satu pertanyaan. Jika pengguna bertanya sesuatu yang membutuhkan dua tool berbeda, agent akan memanggil tool pertama, mengamati hasilnya, memutuskan masih perlu informasi lagi, memanggil tool kedua, lalu baru menyusun jawaban akhir.
14.6 Debugging: Mengapa Agent Tidak Mau Menggunakan Tool
Ini adalah masalah paling umum yang akan kamu temui. Gunakan urutan diagnosis berikut secara berurutan, jangan melompat ke solusi sebelum memastikan penyebabnya.
1. Apakah bind_tools() Sudah Dipanggil?
Penyebab paling umum: lupa memanggil llm.bind_tools(TOOLS) dan langsung menggunakan llm biasa di node_agent. Tanpa ini, LLM bahkan tidak tahu tools apa yang tersedia.
# Salah: response = llm.invoke(messages)
# Benar: response = llm_with_tools.invoke(messages)
2. Apakah Docstring Tool Cukup Jelas dan Spesifik?
Jika docstring ambigu atau terlalu umum, LLM tidak yakin apakah harus menggunakan tool tersebut. Periksa: apakah ada kalimat "Gunakan ketika..." yang eksplisit? Apakah ada contoh format input?
# Test: tanyakan langsung ke LLM tanpa tools
# "Berdasarkan docstring ini, kapan saya harus
# memanggil tool cek_pesanan?"
# Jika jawabannya tidak jelas, docstring perlu diperbaiki
3. Apakah Tipe Data Input Sesuai Harapan?
LLM sering mengirim input sebagai string padahal fungsi mengharapkan integer, atau sebaliknya. Periksa log tool call untuk melihat tipe data yang dikirim LLM.
for msg in hasil["messages"]:
if hasattr(msg, "tool_calls") and msg.tool_calls:
print(msg.tool_calls)
# Lihat persis apa yang dikirim LLM
4. Apakah Conditional Edge dan Routing Function Benar?
Jika LLM sudah benar meminta tool call, tapi graph tidak mengeksekusinya, periksa fungsi routing, pastikan ia memeriksa tool_calls dengan benar dan add_conditional_edges mengarah ke node yang tepat.
# Tambahkan print untuk debug routing
def routing(state):
pesan = state["messages"][-1]
print(f"Routing check: {pesan.tool_calls}")
...
Kesalahan Umum
Nama fungsi seperti cek_pesanan tidak memberi informasi apapun ke LLM tentang kapan harus digunakan. Semua keputusan LLM didasarkan sepenuhnya pada docstring. Investasikan waktu untuk menulis docstring yang jelas, ini adalah satu-satunya cara LLM "memahami" tool yang kamu buat.
Ini kesalahan paling umum kedua. Jika kamu menggunakan llm biasa (bukan llm_with_tools) di dalam node_agent, LLM tidak akan pernah memanggil tool apapun, bukan karena tidak mau, tapi karena ia bahkan tidak tahu tools itu ada.
Jika dua tools memiliki use case yang mirip tanpa batasan yang jelas, LLM akan kesulitan memilih yang tepat. Selalu sertakan kalimat "JANGAN gunakan untuk..." di setiap docstring untuk membedakan tools yang fungsinya berdekatan.
Sebelum mengubah kode apapun, periksa dulu apa yang sebenarnya dikirim LLM melalui message.tool_calls. Ini menunjukkan tool mana yang dipilih dan argumen apa yang dikirim, informasi yang paling berguna untuk mendiagnosis masalah secara tepat, bukan menebak-nebak.
Jangan langsung membangun agent dengan sepuluh tools sekaligus. Mulai dengan satu tool, pastikan agent menggunakannya dengan benar dan konsisten, baru tambahkan tool berikutnya satu per satu. Ini memudahkan diagnosis ketika ada masalah, kamu tahu persis tool mana yang baru ditambahkan saat masalah muncul.
Ringkasan Bab 14
- Saya memahami perbedaan Chain (alur tetap), Agent (LLM bebas memutuskan), dan Graph (bercabang terkontrol), dan kapan masing-masing tepat digunakan.
- Saya bisa menjelaskan tiga konsep dasar LangGraph: Node (unit kerja), Edge (koneksi antar node), dan State (data yang mengalir).
- Saya bisa menulis docstring tool yang lengkap: kapan dipakai, format input dengan type hint, contoh nilai, dan kapan TIDAK boleh dipakai.
- Saya bisa membangun graph LangGraph lengkap: definisi State, bind_tools ke LLM, node_agent, ToolNode, conditional edge, dan kompilasi graph.
- Saya memahami siklus ReAct (Reason -> Act -> Observe) dan bagaimana agent bisa melewati siklus ini lebih dari sekali untuk satu pertanyaan.
- Saya bisa mendiagnosis masalah "agent tidak mau menggunakan tool" secara sistematis: cek bind_tools, cek docstring, cek tipe data, cek routing function.
- Saya memulai pengembangan agent dengan satu atau dua tools terlebih dahulu, dan menambahkan tools baru secara bertahap untuk memudahkan debugging.