AI
LangGraph vs CrewAI 2026: Mana untuk Agent Produksi?
Oktober 202612 menit baca

LangGraph adalah runtime orkestrasi low-level di mana Anda mendefinisikan graph eksplisit berisi node dan edge, dan checkpointer menyimpan state setelah setiap langkah. CrewAI dimulai dari agent berbasis peran yang dikelompokkan menjadi crew, lalu menambahkan Flows dengan decorator start, listen dan router saat Anda butuh urutan yang tetap. Perbedaan praktisnya adalah seberapa banyak alur kontrol yang Anda tulis sendiri.
LangGraph adalah default yang lebih kuat ketika run harus berhenti menunggu persetujuan manusia, bertahan saat restart, dan melanjutkan dari checkpoint, karena itu semua API intinya. CrewAI cocok untuk pekerjaan agent yang kolaboratif dan terbuka, dan lebih cepat menghasilkan versi pertama. CrewAI Flows juga bisa menangani workflow produksi, tetapi Anda harus memilih backend persistence dan provider feedback non-blocking sendiri.
Sebuah node memanggil interrupt() dengan payload yang bisa diserialisasi ke JSON, lalu graph menyimpan checkpoint dan mengembalikan payload itu ke pemanggil di bawah __interrupt__. Anda melanjutkannya nanti dengan meng-invoke graph memakai Command(resume=value) dan thread_id yang sama. Node yang terhenti dijalankan ulang dari baris pertamanya, jadi kode sebelum interrupt() harus idempotent.
Ya. Decorator human_feedback pada method Flow meminta feedback dan bisa meng-emit outcome seperti approved atau rejected, opsional dengan LLM untuk mengklasifikasikan balasan teks bebas. Secara default ia memblokir input console, jadi untuk aplikasi web Anda memberikan provider yang melempar HumanFeedbackPending, yang menyimpan state, lalu memanggil resume() atau resume_async() nanti.
Keduanya bisa, asalkan persistence dikonfigurasi. LangGraph memakai checkpointer seperti PostgresSaver dengan key thread_id, dan durability mode exit, async serta sync mengatur kapan checkpoint ditulis. CrewAI memakai decorator persist, dengan SQLite sebagai default, dan melanjutkan run saat Anda kickoff dengan state id yang tersimpan.

Ringkasan Utama
LangGraph cocok untuk agent produksi yang harus berhenti menunggu persetujuan, bertahan saat restart, dan melanjutkan dari checkpoint, karena state, edge dan persistence ditulis eksplisit. CrewAI cocok untuk tim agent berbasis peran dan lebih cepat menghasilkan versi pertama; Flows menambah persistence dan human feedback, tetapi feedback bawaannya memblokir input console sampai Anda memasang provider.
Bayangkan agent accounts payable untuk sebuah distributor di Indonesia. Agent ini membaca PDF invoice supplier, membandingkan totalnya dengan purchase order di ERP, lalu membuat draft invoice. Sebagian besar invoice cocok. Yang selisihnya lebih dari dua persen perlu disetujui seseorang, dan orang itu bisa saja baru menjawab empat jam kemudian dari ponselnya, setelah server di-redeploy dua kali.
Kalimat terakhir itulah tempat pilihan LangGraph vs CrewAI sebenarnya ditentukan. Artikel ini membangun workflow tiga langkah yang sama di kedua framework, hanya dengan API dari dokumentasi terbaru mereka: LangGraph 1.x, yang rilis 1.0-nya pada Oktober 2025 menjadi versi major stabil pertamanya, dan CrewAI, yang dokumentasinya saat ini mengarah ke versi 1.15.23. Perbandingannya mencakup model kontrol, durable execution, persetujuan manusia, observability, platform managed, dan kurva belajar.
Keduanya framework Python open-source dan keduanya sanggup menjalankan workflow ini. Bedanya ada pada apa yang dibuat eksplisit. LangGraph meminta Anda menggambar graph dan memberi checkpoint setelah setiap langkah. CrewAI meminta Anda mendeskripsikan agent lewat peran dan tujuan, lalu menyediakan Flows saat langkah-langkahnya perlu dikunci.
| Dimensi | LangGraph | CrewAI |
|---|---|---|
| Model kontrol | StateGraph berisi node dan edge. Setiap transisi adalah kode yang Anda tulis, termasuk routing kondisional lewat Command(goto=...). | Crew berisi agent berbasis peran yang berjalan berurutan atau di bawah manager agent, diorkestrasi oleh Flows berbasis event dengan decorator start, listen dan router. |
| State | Schema bertipe, biasanya TypedDict, digabung dari return value setiap node dan ditulis oleh checkpointer. | Model Pydantic atau dict pada Flow, dengan field id yang ditambahkan otomatis. |
| Durable execution | Checkpointer untuk Postgres, SQLite, MongoDB dan Cosmos DB, ditambah durability mode exit, async dan sync. | Decorator persist, dengan SQLiteFlowPersistence sebagai default, di level class atau method. |
| Resume | Invoke ulang dengan thread_id yang sama. Node yang terhenti dijalankan lagi dari baris pertamanya. | Kickoff dengan state id tersimpan untuk melanjutkan, atau restore_from_state_id untuk fork run baru. |
| Human-in-the-loop | interrupt() di dalam node mengembalikan payload; Command(resume=...) memberikan jawabannya. | Decorator human_feedback; default lewat input console, provider untuk penggunaan non-blocking, lalu resume(). |
| Riwayat dan replay | get_state_history menampilkan setiap checkpoint; resume dari checkpoint_id memutar ulang mulai titik itu. | Fork run baru dari state id tersimpan; dokumentasi Flows tidak menjelaskan riwayat checkpoint per langkah. |
| Observability dan platform | Tracing ke LangSmith; LangSmith Deployment menjalankan graph sebagai layanan managed. | Tracing bawaan ke CrewAI AMP, flow.plot() untuk diagram HTML; AMP meng-host crew dan flow. |
| Kurva belajar | Lebih curam. Anda memikirkan bentuk state, edge dan replay sebelum run pertama yang berguna. | Lebih landai. Role, goal dan backstory terbaca seperti deskripsi pekerjaan, jadi crew pertama cukup singkat. |
Kalau bentuk workflow-nya tetap, ada uang yang dipertaruhkan, dan ada manusia di tengahnya, sifat eksplisit LangGraph sepadan dengan usahanya. Kalau pekerjaannya riset atau drafting yang terbuka, di mana agent sebaiknya memutuskan di antara mereka sendiri, crew CrewAI lebih alami, dan Flows memungkinkan Anda menambah struktur di sekelilingnya tanpa pindah framework.
LangGraph menyebut dirinya runtime orkestrasi low-level, dan istilah low-level itu tepat. Node adalah fungsi yang menerima state dan mengembalikan update. Edge adalah garis yang Anda tambahkan. Saat model yang harus memilih, Anda tetap yang menentukan pilihannya, dengan mengembalikan Command yang menyebut node berikutnya. Tidak ada yang bergerak tanpa Anda sambungkan, dan itulah yang membuat graph bisa diaudit.
CrewAI berangkat dari arah sebaliknya. Crew adalah sekumpulan agent dengan role, goal dan backstory, yang mengerjakan task secara berurutan atau di bawah manager agent yang mendelegasikan dan memeriksa hasil. Flows ditambahkan untuk kasus di mana otonomi adalah default yang salah: isinya method Python biasa yang dirangkai dengan start, listen dan router, dan satu method Flow bisa menjalankan seluruh crew. Untuk pipeline invoice, Anda akhirnya menulis sebagian besar kode Flow dengan satu agent di dalamnya, tanda bahwa masalahnya memang berbentuk graph.
Tiga langkah: ekstrak field dengan panggilan model ber-structured output, cocokkan total dengan purchase order, lalu buat draft atau berhenti untuk persetujuan. Toleransi dan fungsi helper ERP hanya ilustrasi; pemanggilan LangGraph-nya sesuai dokumentasi.
from typing import Literal, TypedDict
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt
from pydantic import BaseModel
class InvoiceFields(BaseModel):
vendor_code: str
po_number: str
total_idr: int
class InvoiceState(TypedDict, total=False):
raw_text: str
fields: InvoiceFields
variance_pct: float
erp_doc_id: str
llm = init_chat_model("openai:gpt-4.1-mini").with_structured_output(InvoiceFields)
TOLERANCE_PCT = 2.0 # example policy: above this, a person signs off
def extract(state: InvoiceState) -> InvoiceState:
return {"fields": llm.invoke(state["raw_text"])}
def match_po(state: InvoiceState) -> Command[Literal["approve", "post"]]:
po_total = erp.get_po_total(state["fields"].po_number) # read-only
variance = abs(state["fields"].total_idr - po_total) / po_total * 100
nxt = "approve" if variance > TOLERANCE_PCT else "post"
return Command(update={"variance_pct": variance}, goto=nxt)
def approve(state: InvoiceState) -> Command[Literal["post", "__end__"]]:
# Nothing with a side effect above this line: on resume the whole
# node runs again from the top, not from the interrupt() call.
decision = interrupt({
"po": state["fields"].po_number,
"variance_pct": round(state["variance_pct"], 2),
})
return Command(goto="post" if decision == "approve" else END)
def post(state: InvoiceState) -> InvoiceState:
# Idempotency key = PO number: a replayed node finds the existing draft.
doc_id = erp.create_draft_invoice(state["fields"], key=state["fields"].po_number)
return {"erp_doc_id": doc_id}
builder = StateGraph(InvoiceState)
builder.add_node(extract)
builder.add_node(match_po)
builder.add_node(approve)
builder.add_node(post)
builder.add_edge(START, "extract")
builder.add_edge("extract", "match_po")
builder.add_edge("post", END)
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
checkpointer.setup() # creates the checkpoint tables once
graph = builder.compile(checkpointer=checkpointer)
# thread_id is the checkpoint primary key: make it the business key.
config = {"configurable": {"thread_id": "inv-2026-10-0042"}}
result = graph.invoke({"raw_text": pdf_text}, config, durability="sync")
if "__interrupt__" in result:
notify_approver(result["__interrupt__"]) # hours may pass here
# Later, from the approval webhook, in any process:
graph.invoke(Command(resume="approve"), config)Dua detail memikul sebagian besar beban produksi. thread_id adalah primary key checkpointer, jadi memakai nomor invoice membuat setiap run mudah ditemukan oleh orang yang menanyakannya. Selain itu, pemanggilan resume tidak butuh proses aslinya: worker mana pun dengan checkpointer dan thread_id yang sama bisa melanjutkan dari titik graph berhenti, persis yang dibutuhkan persetujuan yang datang empat jam kemudian.
Versi Flow-nya setara langkah demi langkah. Extractor adalah satu Agent dengan response format Pydantic, pencocokan adalah router yang mengembalikan label, dan persetujuan memakai decorator human_feedback yang meng-emit salah satu outcome yang didaftarkan. Argumen llm membuat reviewer bisa mengetik teks bebas, lalu model mengklasifikasikannya menjadi approved atau rejected.
from crewai.agent import Agent
from crewai.flow.flow import Flow, listen, router, start
from crewai.flow.human_feedback import HumanFeedbackResult, human_feedback
from crewai.flow.persistence import persist
from pydantic import BaseModel
class InvoiceFields(BaseModel):
vendor_code: str
po_number: str
total_idr: int
class InvoiceState(BaseModel):
raw_text: str = ""
fields: InvoiceFields | None = None
variance_pct: float = 0.0
erp_doc_id: str = ""
TOLERANCE_PCT = 2.0
@persist # SQLiteFlowPersistence by default: state saved per method
class InvoiceFlow(Flow[InvoiceState]):
@start()
async def extract(self):
clerk = Agent(
role="AP clerk",
goal="Read supplier invoices into structured fields",
backstory="Indonesian supplier invoices, amounts in rupiah.",
)
result = await clerk.kickoff_async(self.state.raw_text, response_format=InvoiceFields)
self.state.fields = result.pydantic
@router(extract)
def match_po(self):
po_total = erp.get_po_total(self.state.fields.po_number)
self.state.variance_pct = abs(self.state.fields.total_idr - po_total) / po_total * 100
return "needs_review" if self.state.variance_pct > TOLERANCE_PCT else "auto_post"
@listen("needs_review")
@human_feedback(
message="Variance above tolerance. Approve posting this invoice?",
emit=["approved", "rejected"],
llm="gpt-4.1-mini", # maps free-text feedback onto an outcome
default_outcome="rejected", # silence never posts an invoice
)
def request_review(self):
return f"PO {self.state.fields.po_number}: {self.state.variance_pct:.2f}% over"
@listen("auto_post")
def post_clean(self):
self._post()
@listen("approved")
def post_approved(self, result: HumanFeedbackResult):
self._post()
@listen("rejected")
def park(self, result: HumanFeedbackResult):
erp.flag_for_ap_team(self.state.fields.po_number, result.feedback)
def _post(self):
self.state.erp_doc_id = erp.create_draft_invoice(
self.state.fields, key=self.state.fields.po_number
)
flow = InvoiceFlow()
flow.kickoff(inputs={"raw_text": pdf_text})
# Resume the same run later: InvoiceFlow().kickoff(inputs={"id": saved_state_id})
# Fork a new run from it: InvoiceFlow().kickoff(restore_from_state_id=saved_state_id)Kodenya lebih mirip Python biasa dibanding graph, dan bagi tim yang baru mengenal agent hal itu penting. Konsekuensinya, dua hal yang di LangGraph sudah tersedia menjadi tanggung jawab Anda: memilih backend persistence selain file SQLite default, dan memastikan langkah persetujuan tidak memblokir worker, yang dibahas di bagian berikutnya.
Kedua framework menyimpan state, tetapi granularitas restart-nya berbeda, dan perbedaan itu menentukan di mana side effect harus diletakkan.
Untuk workflow back-office, aturan praktisnya sama di keduanya: apa pun yang menulis ke ERP harus idempotent, dengan key berupa identitas bisnis seperti nomor PO atau invoice, supaya langkah yang diputar ulang menemukan draft yang sudah dibuat alih-alih membuat draft kedua. Pilih durability sync di LangGraph kalau kehilangan satu langkah lebih mahal daripada langkah yang sedikit lebih lambat.
Double-post klasik: penulisan ke ERP diletakkan sebelum interrupt() di node yang sama. Run pertama membuat draft lalu berhenti; persetujuan me-resume node dari atas dan membuat draft lagi. Letakkan penulisan di node terpisah setelah persetujuan, atau buat idempotent, dan jangan pernah membungkus interrupt() dengan try/except kosong, karena mekanismenya bekerja dengan melempar exception secara internal.
Langkah persetujuan adalah titik di mana default kedua dokumentasi paling berbeda. human_feedback milik CrewAI memblokir input console kecuali Anda memberikan provider. interrupt milik LangGraph tidak pernah memblokir: ia menyimpan checkpoint dan mengembalikan payload ke pemanggil, sehingga jeda dan jawaban secara alami menjadi dua request terpisah.
# CrewAI: the default @human_feedback calls input() and blocks a worker.
# Wrong for a web app: the request thread waits on a terminal nobody sees.
# Right: pass provider=... to @human_feedback. The provider raises
# HumanFeedbackPending, kickoff() returns it, and the flow state is
# persisted automatically. Your approval endpoint later calls:
await flow.resume_async(feedback_text) # resume() from inside a running
# event loop raises RuntimeError
# LangGraph: the interrupt payload must be JSON-serialisable, because it
# is stored by the checkpointer and handed to whoever resumes the thread.
graph.invoke(Command(resume="approve"), {"configurable": {"thread_id": tid}})Pada praktiknya keduanya berakhir di arsitektur yang sama: run berhenti, record persetujuan masuk ke antrean atau pesan chat, lalu webhook me-resume run dengan keputusannya. LangGraph sampai ke sana tanpa kode tambahan. CrewAI sampai ke sana setelah Anda mengimplementasikan provider, dan sebagai gantinya balasan teks bebas dari reviewer bisa diklasifikasikan menjadi outcome oleh model.
Simpan payload jeda di tabel approval milik Anda sendiri, selain di state framework. Approver, auditor dan tim finance akan bertanya invoice mana yang sedang menunggu dan kenapa, dan query itu sebaiknya mengenai tabel biasa, bukan blob checkpoint.
Setiap framework punya platform komersial dari perusahaan yang sama. LangGraph mengirim tracing ke LangSmith, dan LangSmith Deployment menjalankan graph sebagai layanan managed dengan persistence dan task queue. CrewAI punya tracing bawaan yang melapor ke CrewAI AMP, platform manajemen agent-nya, yang juga bisa meng-host crew dan flow. Tidak satu pun framework mewajibkan platformnya, tetapi tooling visual masing-masing mengasumsikannya.
Bagi tim yang sudah menjalankan infrastrukturnya sendiri, pertanyaan yang lebih penting adalah apa yang bisa dilihat tanpa platform. get_state dan get_state_history di LangGraph memungkinkan Anda memeriksa setiap checkpoint sebuah thread dari database Anda sendiri. CrewAI menyediakan flow.plot() untuk strukturnya dan flow.usage_metrics untuk jumlah token di semua panggilan LLM dalam satu run, berguna untuk menghitung biaya per invoice.
Periksa poin berikut secara berurutan dan berhenti di poin pertama yang menjawab.
Tidak ada pilihan yang menjebak. LangGraph 1.0 berkomitmen tidak ada breaking change sampai 2.0, dan CrewAI menerbitkan dokumentasi berversi untuk setiap rilis, jadi apa pun yang Anda pilih, pin versinya dan baca changelog sebelum upgrade.
Pertanyaan penentunya bukan framework mana yang punya lebih banyak agent atau lebih banyak star. Pertanyaannya adalah apa yang terjadi saat sebuah run berhenti di tengah jalan lalu dilanjutkan nanti di proses lain. LangGraph menjawabnya dengan checkpoint eksplisit dan node yang diputar ulang dari atas; CrewAI menjawabnya dengan state Flow yang dipersist dan provider feedback yang Anda sediakan. Rancang side effect untuk jawaban itu dulu, baru prompt-nya.