LangGraph 1.0: Stateful AI Agents sebagai Graph di Python

Linear prompt chains tidak cukup untuk agent kompleks. LangGraph 1.0: StateGraph, conditional edges, checkpointing, dan human-in-the-loop dengan interrupt().

Kalau kamu sudah baca post DSPy dan post Pydantic AI di seri AI agents ini, pipeline kamu sekarang typed, teroptimasi, dan punya validasi di setiap langkah. Tapi kalau jujur sama diri sendiri: pipeline-mu masih garis lurus. Input masuk dari kiri, output keluar dari kanan, satu langkah demi satu langkah, tanpa percabangan.

Garis lurus itu cap. Ada workflow yang tidak bisa kamu ekspresikan dengan rantai prompt biasa, dan di sinilah LangGraph masuk: framework yang memperlakukan agent sebagai directed graph — dengan state yang typed, percabangan yang eksplisit, dan memori yang bertahan antar panggilan.

Kenapa Linear Chain Cepat Ambruk

Coba pikirkan requirement yang satu ini saja: agent yang bisa pakai tools. Alurnya bukan satu langkah — agent harus memutuskan tool mana yang dipanggil, menunggu hasil tool, lalu memutuskan lagi apakah hasilnya cukup atau perlu memanggil tool berikutnya. Itu loop yang berjalan sampai kondisi tertentu terpenuhi, bukan pipeline linear:

start → agent (butuh tool?) → ya → tools → agent lagi...
                          → tidak → selesai

Dan loop hanyalah permulaan. Agent production butuh:

  • Branching — rute yang berbeda tergantung isi state (misal: tiket validasi gagal masuk ke jalur revisi, bukan jalur sukses).
  • Durable state — state harus bertahan antar langkah dan antar percakapan, bukan variabel lokal di satu function call.
  • Recovery — kalau process crash di tengah eksekusi, kamu tidak mau mulai dari nol; kamu mau lanjut dari checkpoint terakhir.
  • Human approval — ada keputusan (deploy ke prod, kirim email, transfer uang) yang harus pause dan menunggu manusia menyetujui.

Coba ekspresikan semua itu dengan chain | chain | chain. Kamu akan berakhir dengan spaghetti code: flag-flag boolean di prompt, retry logic manual, dan state yang diselundupkan lewat variabel global. Tidak ada runtime yang menegakkan struktur — semuanya kehendak baik.

LangGraph: Agent sebagai Graph

LangGraph (dari LangChain, sekarang stable di v1.0 sejak Oktober 2025 — public API sudah frozen, dan sekarang di-maintain di jalur 1.1.x) membalik pendekatannya. Alih-alih rantai, kamu mendefinisikan: State, nodes, dan edges.

  • State adalah kontrak data — TypedDict, Pydantic BaseModel, atau dataclass — yang dibagikan antar semua node. Ini schema dari workflow-mu.
  • Node adalah fungsi Python biasa. Node menerima state dan mengembalikan partial update — bukan state penuh.
  • Edge menghubungkan node: edge statis untuk urutan tetap, conditional edge untuk percabangan dan loop.
  • Checkpointer menyimpan state ke storage (memory, Postgres, SQLite) per thread_id, sehingga eksekusi bisa di-replay dan dilanjutkan.

Kalau kamu developer Django, posisikan State schema sebagai ORM models-nya agent orchestration. Sama seperti model menentukan bentuk tabel, State schema menentukan bentuk data yang lewat antar node. Dan sama seperti migrations menyimpan riwayat perubahan DB, checkpointer menyimpan riwayat state per thread — yang kamu bisa pause, resume, dan replay. Insting yang sudah kamu punya sebagai Django dev mentransfer langsung ke sini.

Catatan freshness: hati-hati dengan tutorial lama. Mayoritas post yang ranking atas di Google masih mengajarkan API v0.1 yang sudah usang — set_entry_point(), ToolExecutor, dan sejenisnya. v1.0 mempertahankan core graph APIs dan runtime model dari 0.2.x — jadi upgrade-nya murah — tapi yang berubah adalah type safety, ergonomics, dan banyak deprecation. Salah satu yang penting: create_react_agent prebuilt deprecated di v1; kode baru pakai langchain.agents.create_agent, yang menambahkan middleware system. Post ini pakai API v1.

Solution Walkthrough

Semua contoh di bawah butuh Python 3.10+. Instal dengan pip install langgraph (dan langchain untuk contoh agent).

1. StateGraph Minimal dengan Reducer

Titik awal: sebuah graph dengan satu node chatbot. Perhatikan dua hal: state didefinisikan sebagai TypedDict, dan field messages memakai reducer via Annotated:

from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

class State(TypedDict):
    # reducer: append instead of overwrite
    messages: Annotated[list, add_messages]

def chatbot(state: State) -> dict:
    return {"messages": [{"role": "assistant", "content": "how can I help?"}]}

builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)
graph = builder.compile()

result = graph.invoke({"messages": [{"role": "user", "content": "hi"}]})

Ada tiga hal yang penting dipahami di contoh ini:

  • Node mengembalikan partial state. chatbot tidak mengembalikan seluruh state — hanya {"messages": [...]}. LangGraph yang merge hasilnya ke state yang ada. Ini kontrak penting: node tidak boleh me-return state penuh, cukup delta-nya.
  • Reducer menentukan cara merge. Tanpa Annotated[list, add_messages], nilai messages baru akan menimpa diam-diam (overwrite) list lama setiap kali node jalan. Dengan reducer, pesan baru di-append ke list yang ada. Kalau field list-mu terlihat “hilang” antar node, cek dulu apakah reducer-nya ada — ini pitfall nomor satu.
  • Graph di-compile dulu sebelum di-invoke. compile() yang menghasilkan executable graph dari blueprint.

2. Conditional Edges: Routing dan Loop

Sekarang tambahkan percabangan. Conditional edge adalah fungsi yang membaca state dan mengembalikan nama node tujuan. Contoh klasik: loop agent↔tools — agent memanggil tools, hasilnya kembali ke agent, sampai tidak ada tool_calls tersisa:

from langgraph.graph import StateGraph, START, END

def should_continue(state: State) -> str:
    last = state["messages"][-1]
    if getattr(last, "tool_calls", None):
        return "tools"   # loop back through tool execution
    return END

builder.add_conditional_edges("agent", should_continue, ["tools", END])
builder.add_edge("tools", "agent")

should_continue membaca state terbaru dan memutuskan arah: kembali ke "tools" kalau masih ada tool call yang harus dieksekusi (loop), atau END kalau sudah selesai (keluar). Daftar ["tools", END] adalah mapping dari return value ke node tujuan. Inilah cara LangGraph mengekspresikan loop dan branch — bukan dengan while di kode kamu, tapi sebagai bagian dari struktur graph.

3. Agent dengan Tools: create_agent (Bukan create_react_agent)

Di v1, cara cepat membuat ReAct agent adalah langchain.agents.create_agent. Ini menggantikan prebuilt create_react_agent yang deprecated — jangan ikuti tutorial lama yang masih memakainya:

from langchain_core.tools import tool
from langchain.agents import create_agent  # v1 path

@tool
def multiply(a: int, b: int) -> int:
    """Multiply two integers."""
    return a * b

agent = create_agent(model="openai:gpt-4o-mini", tools=[multiply])
result = agent.invoke(
    {"messages": [{"role": "user", "content": "What is 47 * 83?"}]}
)
print(result["messages"][-1].content)

@tool mengubah fungsi Python biasa menjadi tool yang bisa dipanggil model — docstring-nya menjadi deskripsi tool. create_agent membungkus loop reasoning-and-tool-use yang kita bangun manual di contoh sebelumnya, plus middleware system yang baru di v1. Kalau agent standar seperti ini sudah cukup, pakai create_agent. Kalau kamu butuh kontrol penuh atas graph — interrupt, multi-agent handoff, retry policy khusus — turun ke StateGraph langsung, seperti contoh 1 dan 2. Keduanya sah; keduanya punya tempatnya.

4. Checkpointing: Memori per thread_id

Graph polos tidak ingat apa pun antar invoke. Untuk membuat agent punya memori multi-turn — atau untuk durable execution yang bisa di-resume — tambahkan checkpointer dan beri thread_id pada tiap percakapan:

from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
agent = builder.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "user-42-thread-1"}}
agent.invoke({"messages": [{"role": "user", "content": "My name is Alex"}]}, config)
agent.invoke({"messages": [{"role": "user", "content": "What's my name?"}]}, config)
# -> remembers "Alex" because state is replayed from the same thread_id

Invoke kedua “ingat” Alex karena state di-replay dari checkpoint dengan thread_id yang sama. Ini analog dengan session di web app — bedanya, state-nya di-serialize oleh checkpointer, bukan disimpan di cookie.

Pitfall yang sering menjegal: kalau kamu compile graph dengan checkpointer tapi lupa memberikan thread_id, setiap invoke dianggap session baru — percakapan dimulai dari nol. Gejalanya: agent “lupa” apa yang kamu bilang dua pesan lalu, dan kamu bingung kenapa. MemorySaver cukup untuk development; untuk production, pakai saver berbasis database (Postgres/SQLite) supaya state bertahan antar proses dan bisa di-share antar instance.

5. Human-in-the-Loop dengan interrupt()

Fitur yang paling membedakan LangGraph dari rantai prompt biasa: graph bisa pause di tengah eksekusi dan menunggu input manusia. Kasus klasik: approval gate sebelum deploy.

from langgraph.types import interrupt, Command

def approve_deploy(state: State) -> dict:
    decision = interrupt({"question": "Deploy to prod?", "diff": state["diff"]})
    # graph pauses here until you resume with Command(resume=...)
    return {"approved": decision["approved"]}

# resume later:
# graph.invoke(Command(resume={"approved": True}), config)

Saat interrupt() dipanggil, eksekusi berhenti dan checkpointer menyimpan posisi persis eksekusi beserta payload-nya ({"question": ..., "diff": ...} — bisa kamu tampilkan di UI approval). Nanti — menit, jam, atau hari kemudian — kamu resume dari titik yang sama dengan Command(resume=...). Seluruh state dan posisi node di-replay dari checkpoint, jadi tidak ada yang perlu disimpan manual di sisi aplikasi. Ini pola yang sangat relevan untuk workflow Django-mu: approval flow yang biasanya kamu implement dengan status field di DB, sekarang jadi bagian dari graph runtime itu sendiri.

Trade-offs: Kapan Pakai LangGraph (Dan Kapan Tidak)

LangGraph berlebihan saat:

  • Workflow-mu memang garis lurus — satu prompt, satu panggilan. Rantai kecil atau create_agent tanpa custom graph sudah selesai. Menambah StateGraph di sini hanya menambah boilerplate.
  • Kamu cuma butuh satu agent sederhana tanpa state lintas-turn dan tanpa approval. create_agent ada supaya kamu tidak perlu menyentuh graph API sama sekali.

LangGraph memberikan value paling besar saat:

  • Ada loop atau branching yang tidak bisa diekspresikan linear: tool calling dengan retry, routing berdasar kondisi, multi-step pipelines yang bercabang.
  • Kamu butuh durable state dan recovery: proses crash di langkah 4 dari 7 — dengan checkpointer, resume dari langkah 4, bukan mulai ulang.
  • Ada human-in-the-loop: keputusan yang butuh approval manusia di tengah eksekusi.
  • Kamu butuh observability: setiap langkah, state transition, dan durasi bisa di-stream dan di-inspect karena semuanya eksplisit dalam graph.

Soal lock-in: LangGraph adalah low-level runtime — di situlah kekuatan dan biayanya. Kamu ikut ekosistem LangChain: API-nya stabil sekarang (v1.0 frozen), ekosistemnya besar — ada langgraph-supervisor untuk hierarchical multi-agent, langgraph-swarm untuk agent handoffs, dan langchain-mcp-adapters untuk memanggil MCP tools dari dalam node (lihat post MCP kita untuk sisi server-nya). Sekitar 34,5 juta download PyPI per bulan. Alternatifnya — rolling your own state machine dengan asyncio dan database — selalu menggoda, tapi kamu akan re-invent checkpointing, replay, koncurrency handling, dan retry logic yang sudah bertahun-tahun di-battle-test di sini. Kalau workflow-mu sederhana, DIY itu masuk akal. Kalau sudah menyentuh loop + state + approval, biaya DIY meledak.

Common Pitfalls

  • Tutorial v0.1. set_entry_point() dan ToolExecutor tidak ada lagi di v1 — kalau contoh kode yang kamu temukan memakainya, cari versi yang lebih baru atau baca dokumentasi resmi v1.
  • create_react_agent deprecated. Di v1, gunakan langchain.agents.create_agent. Yang lama masih jalan, tapi bukan untuk kode baru.
  • Node harus return partial state, bukan full state. Mengembalikan state penuh (atau state yang tidak lengkap sebagai “full”) adalah sumber bug yang umum — return hanya field yang berubah.
  • Lupa reducer = list ke-overwrite diam-diam. Field list tanpa Annotated[..., add_messages] (atau reducer custom) akan ditimpa setiap node jalan.
  • Lupa thread_id. Dengan checkpointer terpasang tapi tanpa thread_id di config, setiap invoke dihitung sebagai session baru.
  • Python 3.10+. LangGraph v1 butuh Python 3.10 ke atas; kalau base image atau environment kamu masih 3.9, upgrade dulu.

Kesimpulan

Linear chain itu bagus — sampai workflow-mu butuh loop, percabangan, dan state yang bertahan. Saat itu terjadi, kamu tidak butuh prompt yang lebih pintar; kamu butuh runtime yang mengekspresikan struktur eksekusinya: LangGraph. State schema-nya adalah ORM models-nya orchestration, conditional edges menggantikan spaghetti branching, checkpointer menggantikan session management manual, dan interrupt() menggantikan approval flow yang di-letakkan di luar logika agent.

Mulai dari StateGraph minimal di atas, tambah conditional edges untuk tool loop, pasang checkpointer, dan sisipkan interrupt() di titik keputusan. Kamu akan menemukan bahwa sebagian besar kompleksitas yang biasanya kamu tulis manual — retry, resume, state handling — sudah disediakan runtime-nya.

Lanjutan Seri AI Agents

Post ini bagian dari seri AI agents di blog DKNET. Kalau belum baca, ini urutan yang kami rekomendasikan:

Dari keempat post itu sampai post ini, satu tema yang konsisten: kualitas agent production ditentukan oleh struktur — typed contracts, explicit control flow, dan durable state — bukan oleh panjang prompt. LangGraph adalah lapisan terakhir yang menyatukan semuanya.