Python & AI Tutorials Logo
LangChain & LangGraph

14. Membangun Agent Loop

Di Bab 13 kita belajar cara mengeksekusi permintaan tool-call dari LLM dan mengembalikan hasilnya. Namun, pekerjaan itu mengasumsikan setiap tugas bisa ditangani dalam satu tool-call. Pada praktiknya, tool-call sering perlu berlanjut selama beberapa putaran hingga LLM mengumpulkan semua informasi yang dibutuhkan untuk jawaban akhir.

Perhatikan permintaan "cari populasi Prancis, lalu kalikan dengan dua." Tugas ini membutuhkan setidaknya dua tool-call. Kamu harus mencari populasinya dulu — baru setelah itu kamu bisa menjalankan perhitungan. Panggilan kedua bergantung pada hasil pertama, jadi tidak mungkin menanganinya dalam satu tool-call.

Di bab ini, kita mengubah siklus tunggal dari Bab 13 menjadi sebuah loop. Selama LLM meminta tool-call, kita terus mengeksekusinya — mengulang hingga LLM berhenti meminta alat dengan sendirinya. Kemudian kita menambahkan batas keamanan(safety limits) untuk mencegah loop berjalan selamanya, serta membahas penanganan error(error handling) agar agen tidak crash saat sebuah alat gagal.

14.1) Dari Siklus Tunggal ke Loop

14.1.1) Bagaimana Cara Kerja Agent Loop?

Seperti yang kita lihat di pendahuluan, tugas di mana aksi berikutnya bergantung pada hasil langkah sebelumnya sering tidak bisa ditangani dengan satu tool-call. LLM perlu memanggil alat, memeriksa hasilnya, lalu memutuskan lagi. Inilah yang dilakukan agent loop, dan cara kerjanya melalui tiga tahap:

  • Think — LLM membaca percakapan sejauh ini dan memutuskan apa yang harus dilakukan berikutnya. Jika membutuhkan alat, ia meminta tool-call melalui tool_calls. Jika tidak, ia mengembalikan jawaban akhir.
  • Act — Eksekusi alat yang ditentukan di tool_calls.
  • Observe — Periksa hasil eksekusi alat dan tambahkan ke percakapan dalam sebuah ToolMessage.

Agent loop mengulang ketiga tahap ini hingga LLM tidak lagi meminta alat apa pun. Pola ini juga dikenal sebagai ReAct (Reason + Act), dan ide utamanya adalah bergantian antara penalaran dan aksi.

Ya

Tidak

Input Pengguna

THINK
Panggil LLM

tool_calls
ada?

ACT
Eksekusi alat

OBSERVE
Periksa hasil

Jawaban Akhir

Jika tool_calls ada, eksekusi alatnya, tambahkan hasilnya ke percakapan, dan panggil LLM lagi. Jika tool_calls kosong, LLM telah mengembalikan jawaban akhir dan loop berhenti. Sekarang mari kita terapkan ini ke dalam kode.

14.1.2) Mengimplementasikan Loop Think-Act-Observe

Mari kita ubah loop Think-Act-Observe dari bagian sebelumnya menjadi kode. Pertama, kita siapkan alat dan model.

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain.tools import tool
 
# Definisikan alat
@tool
def get_weather(city: str) -> str:
    """Dapatkan cuaca terkini untuk sebuah kota."""
    fake_data = {"Tokyo": "18°C, berawan", "Kairo": "31°C, cerah"}
    return fake_data.get(city, f"Tidak ada data cuaca untuk {city}.")
 
@tool
def calculate(expression: str) -> str:
    """Evaluasi ekspresi aritmetika sederhana, misalnya '3 * 21'."""
    return str(eval(expression))  # Peringatan: eval() adalah risiko keamanan. Jangan gunakan di produksi.
 
# Bind alat
tools = [get_weather, calculate]
llm = ChatOpenAI(model="gpt-5-mini")
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}

Di Bab 13, kita mengeksekusi sebuah alat sekali lalu berhenti. Sekarang kita mengulang hingga LLM berhenti meminta tool-call. Di dalam loop while True, kita memanggil LLM, dan jika responsnya berisi tool_calls, kita mengeksekusi alatnya dan memanggil LLM lagi. Jika tidak ada tool_calls, LLM telah mengembalikan jawaban akhir, jadi kita keluar dari loop.

python
def run_agent(user_input: str) -> str:
    """Jalankan loop Think-Act-Observe sampai LLM mengembalikan jawaban akhir."""
    messages = [
        SystemMessage(content="Kamu adalah asisten yang membantu."),
        HumanMessage(content=user_input),
    ]
 
    while True:
        # THINK: Minta LLM memutuskan aksi berikutnya
        print("THINK: Meminta LLM memutuskan")
        ai_message = llm_with_tools.invoke(messages)
        messages.append(ai_message)
 
        # Kondisi keluar: tidak ada tool_calls berarti ini jawaban akhir
        if not ai_message.tool_calls:
            return ai_message.content
 
        # ACT + OBSERVE: Eksekusi alat yang diminta dan tambahkan hasilnya ke percakapan
        for tool_call in ai_message.tool_calls:
            selected_tool = tool_map[tool_call["name"]]
            print(f"ACT: memanggil '{tool_call['name']}', args={tool_call['args']}")
 
            tool_message = selected_tool.invoke(tool_call)
            print(f"OBSERVE: {tool_message.content}")
 
            messages.append(tool_message)

Mari kita bandingkan ini dengan kode Bab 13. Di Bab 13, setelah mengeksekusi alat, kita memanggil LLM satu kali terakhir untuk mendapatkan jawaban. Di Bab 14, kita menempatkan proses yang sama itu di dalam while True dan memeriksa tool_calls di setiap iterasi untuk memutuskan apakah harus lanjut. Blok penyusunnya sama seperti Bab 13 — kita hanya membungkusnya dalam sebuah loop.

Mari kita jalankan.

python
answer = run_agent("Bagaimana cuaca di Tokyo, dan apakah cukup hangat untuk berjalan-jalan?")
print(f'Jawaban akhir: {answer}')

Output:

THINK: Meminta LLM memutuskan
ACT: memanggil 'get_weather', args={'city': 'Tokyo'}
OBSERVE: 18°C, berawan
THINK: Meminta LLM memutuskan
Jawaban akhir: Saat ini di Tokyo suhunya 18°C (sekitar 64°F) dan berawan.
Suhu itu umumnya sejuk dan nyaman untuk berjalan-jalan bagi kebanyakan orang.

Output menunjukkan alur THINK → ACT → OBSERVE → THINK. Pada iterasi pertama LLM meminta panggilan get_weather, dan pada iterasi kedua ia melihat hasil cuaca dan menghasilkan jawaban akhir. Karena tool_calls kosong, jawaban akhir dikembalikan dan loop berhenti.

Sekarang mari kita uji skenario langkah-bergantung dari pendahuluan — di mana panggilan berikutnya baru bisa terjadi setelah melihat hasil dari yang sebelumnya.

python
answer = run_agent("Dapatkan suhu di Kairo, lalu kalikan angkanya dengan 3.")
print(f'Jawaban akhir: {answer}')

Output:

THINK: Meminta LLM memutuskan
ACT: memanggil 'get_weather', args={'city': 'Kairo'}
OBSERVE: 31°C, cerah
THINK: Meminta LLM memutuskan
ACT: memanggil 'calculate', args={'expression': '31 * 3'}
OBSERVE: 93
THINK: Meminta LLM memutuskan
Jawaban akhir: Suhu terkini di Kairo: 31°C. Dikalikan 3 = 93.

Kali ini loop berjalan tiga iterasi.

  1. Iterasi pertama — LLM meminta get_weather("Cairo").
  2. Iterasi kedua — Setelah melihat hasil "31°C, sunny", LLM meminta calculate("31 * 3"). Ia baru bisa membentuk ekspresi tersebut setelah melihat suhunya.
  3. Iterasi ketiga — Setelah melihat kedua hasil "31°C, sunny" dan "93", LLM mengembalikan jawaban akhir.

14.2) Menambahkan Batas Keamanan

Loop yang kita bangun di atas hanya memiliki satu kondisi keluar: ketika LLM merespons tanpa tool_calls, kita keluar dari loop. Dalam kondisi normal ini sudah cukup, tetapi apa yang terjadi jika LLM tidak pernah berhenti meminta tool-call?

Contohnya, jika sebuah alat selalu mengembalikan hasil yang ambigu, LLM mungkin terus memanggilnya dengan harapan mendapatkan sesuatu yang lebih baik. Karena loop-nya adalah while True, jika LLM tidak berhenti, program juga tidak berhenti. Biaya panggilan API terus bertambah seiring program berjalan tanpa batas.

Perbaikan paling sederhana adalah memberi batas atas pada berapa kali loop boleh berjalan. Ganti while True dengan for step in range(max_steps), dan loop dijamin berhenti setelah max_steps iterasi terlepas dari apa yang dilakukan LLM.

python
def run_agent(user_input: str, max_steps: int = 10) -> str:
    """Jalankan loop Think-Act-Observe dalam batas max_steps iterasi."""
    messages = [
        SystemMessage(content="Kamu adalah asisten yang membantu."),
        HumanMessage(content=user_input),
    ]
 
    for step in range(max_steps):
        # THINK
        ai_message = llm_with_tools.invoke(messages)
        messages.append(ai_message)
 
        # Kondisi keluar: tidak ada tool_calls berarti ini jawaban akhir
        if not ai_message.tool_calls:
            return ai_message.content
 
        # ACT + OBSERVE
        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)
 
    # max_steps tercapai: loop berakhir tanpa jawaban akhir
    return f"[Berhenti setelah mencapai iterasi maksimum ({max_steps})]"

Dibandingkan dengan kode sebelumnya, dua hal berubah. while True menjadi for step in range(max_steps), dan sebuah nilai return ditambahkan untuk saat loop mencapai max_steps. Loop sekarang berhenti dengan salah satu dari dua cara: LLM mengembalikan jawaban akhir dengan sendirinya (terminasi alami), atau max_steps tercapai (terminasi keamanan).

Mari kita verifikasi bahwa batas keamanan benar-benar berfungsi. Kita akan membuat sebuah alat yang tidak pernah mengembalikan hasil yang berguna, memaksa LLM ke situasi di mana ia tidak pernah berhenti meminta tool-call.

python
@tool
def unhelpful_search(query: str) -> str:
    """Cari informasi."""
    return "Tidak ada hasil ditemukan. Coba ubah kueri Anda."
 
llm_with_bad_tool = llm.bind_tools([unhelpful_search])
tool_map_bad = {unhelpful_search.name: unhelpful_search}
 
def run_agent_bad(user_input: str, max_steps: int = 5) -> str:
    messages = [
        SystemMessage(content=(
            "Kamu HARUS SELALU menggunakan alat unhelpful_search untuk mencari informasi. "
            "Kamu TIDAK diizinkan menjawab dari pengetahuanmu sendiri. "
            "Jika alat tidak mengembalikan hasil, kamu HARUS mengubah kata dan mencari lagi. "
            "Terus mencari sampai kamu menemukan jawabannya."
        )),
        HumanMessage(content=user_input),
    ]
 
    for step in range(max_steps):
        print(f"--- Langkah {step + 1} ---")
        ai_message = llm_with_bad_tool.invoke(messages)
        messages.append(ai_message)
 
        if not ai_message.tool_calls:
            return ai_message.content
 
        for tool_call in ai_message.tool_calls:
            selected_tool = tool_map_bad[tool_call["name"]]
            tool_message = selected_tool.invoke(tool_call)
            print(f"ACT: '{tool_call['name']}' → {tool_message.content}")
            messages.append(tool_message)
 
    return f"[Berhenti setelah mencapai iterasi maksimum ({max_steps})]"
 
answer = run_agent_bad("Berapa populasi Prancis?")
print(f"\nJawaban akhir: {answer}")

Output:

--- Langkah 1 ---
ACT: 'unhelpful_search' → Tidak ada hasil ditemukan. Coba ubah kueri Anda.
--- Langkah 2 ---
ACT: 'unhelpful_search' → Tidak ada hasil ditemukan. Coba ubah kueri Anda.
--- Langkah 3 ---
ACT: 'unhelpful_search' → Tidak ada hasil ditemukan. Coba ubah kueri Anda.
--- Langkah 4 ---
ACT: 'unhelpful_search' → Tidak ada hasil ditemukan. Coba ubah kueri Anda.
--- Langkah 5 ---
ACT: 'unhelpful_search' → Tidak ada hasil ditemukan. Coba ubah kueri Anda.
 
Jawaban akhir: [Berhenti setelah mencapai iterasi maksimum (5)]

Tanpa max_steps, loop ini akan berjalan selamanya. Berkat max_steps=5, ia dipaksa berhenti setelah lima iterasi.

Nilai yang tepat untuk max_steps bergantung pada kompleksitas agenmu. Terlalu rendah, dan tugas kompleks terpotong sebelum selesai. Terlalu tinggi, dan agen yang berperilaku buruk menumpuk biaya sebelum dihentikan. 15–25 adalah titik awal yang umum; sesuaikan berdasarkan beban kerja aktualmu.

14.3) Menangani Error Alat di Dalam Loop

Loop yang kita bangun di 14.1 dan 14.2 mengasumsikan alat selalu berhasil dieksekusi. Tetapi apa yang terjadi jika sebuah alat memunculkan exception? Kode saat ini tidak memiliki penanganan exception, jadi jika exception terjadi selama eksekusi alat, seluruh agen akan crash.

Di Bab 12 kita belajar cara menangkap exception di dalam alat itu sendiri dengan try/except dan mengembalikan pesan error sebagai string. Jika sebuah alat dibangun dengan cara itu, tidak ada masalah. Tetapi tidak setiap alat menangani error secara internal. Alat yang memanggil library atau API eksternal bisa memunculkan exception yang tak terduga.

Untuk berjaga-jaga terhadap ini, ada baiknya menangani error di level loop juga. Pendekatannya sederhana: bungkus eksekusi alat dalam try/except, dan jika exception terjadi, masukkan pesan error ke dalam ToolMessage dan teruskan ke LLM. LLM bisa membaca pesan error ini dan mencoba lagi dengan argumen yang diperbaiki atau memilih pendekatan berbeda. Ini disebut koreksi-diri(self-correction).

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
from langchain.tools import tool
 
# Definisikan alat
@tool
def calculate(expression: str) -> str:
    """Evaluasi ekspresi aritmetika sederhana, misalnya '3 * 21'."""
    return str(eval(expression))  # Peringatan: eval() adalah risiko keamanan. Jangan gunakan di produksi.
 
# Bind alat
tools = [calculate]
llm = ChatOpenAI(model="gpt-5-mini")
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}
 
def run_agent(user_input: str, max_steps: int = 10) -> str:
    """Agent loop yang meneruskan error alat ke LLM, memungkinkan koreksi-diri."""
    messages = [
        SystemMessage(content="Kamu adalah asisten yang membantu."),
        HumanMessage(content=user_input),
    ]
 
    for step in range(max_steps):
        # THINK
        print("THINK: Meminta LLM memutuskan")
        ai_message = llm_with_tools.invoke(messages)
        messages.append(ai_message)
 
        if not ai_message.tool_calls:
            return ai_message.content
 
        # ACT + OBSERVE
        for tool_call in ai_message.tool_calls:
            selected_tool = tool_map[tool_call["name"]]
            try:
                print(f"ACT: memanggil '{tool_call['name']}', args={tool_call['args']}")
                tool_message = selected_tool.invoke(tool_call)
                print(f"OBSERVE: {tool_message.content}")
            except Exception as e:
                print(f"OBSERVE: Error - {e}")
                tool_message = ToolMessage(
                    content=f"Error: {e}",
                    tool_call_id=tool_call["id"],
                )
            messages.append(tool_message)
 
    return f"[Berhenti setelah mencapai iterasi maksimum ({max_steps})]"

Dibandingkan dengan kode 14.2, satu-satunya perubahan adalah try/except. Jika sebuah alat memunculkan exception, pesan error ditempatkan dalam sebuah ToolMessage dan ditambahkan ke percakapan. Perhatikan bahwa bahkan panggilan yang gagal pun harus memiliki ToolMessage dengan tool_call_id yang cocok. LLM akan melihat error ini di iterasi berikutnya dan memutuskan apa yang harus dilakukan selanjutnya.

Mari kita verifikasi bahwa koreksi-diri berfungsi. Kita akan memicu exception dengan meminta alat calculate membagi dengan nol.

python
answer = run_agent("Gunakan alat kalkulator untuk menghitung 10 / 0")
print(f"Jawaban akhir: {answer}")

Output:

THINK: Meminta LLM memutuskan
ACT: memanggil 'calculate', args={'expression': '10 / 0'}
OBSERVE: Error - division by zero
THINK: Meminta LLM memutuskan
Jawaban akhir: Saya menggunakan alat kalkulator dan ia mengembalikan error: "division by zero."
 
Penjelasan: 10 / 0 tidak terdefinisi dalam aritmetika biasa, jadi tidak bisa menghasilkan angka yang berhingga.
 
Apakah Anda ingin saya:
- menghitung limit satu sisi,
- menampilkan hasil floating-point IEEE-754,
- atau mengevaluasi ekspresi yang berbeda?

Pada iterasi pertama, LLM meminta calculate("10 / 0") dan sebuah ZeroDivisionError dimunculkan. try/except menangkap exception itu dan meneruskan pesan error ke LLM. Pada iterasi kedua, LLM melihat pesan error dan mengembalikan jawaban akhir yang menjelaskan bahwa pembagian dengan nol tidak mungkin. Tanpa try/except, program akan crash pada ZeroDivisionError pertama.