Memahami cara kerja AI agent

Setelah menulis tentang AI Coding assitant dan Vibe Coding, saya ingin membahas bagaimana cara kerja tool coding ini dan AI Agent secara umum. Secara sederhana: agent ini cuma pemanggilan API chat completion, plus tool use dalam sebuah loop.

Supaya lebih jelas, saya akan menunjukkan langsung bagaimana membuat tool agent yang bisa membantu aneka hal (199 baris Python, kodenya bisa dilihat di https://gist.github.com/yohanes/2af7c4d9dac7ddf0854450da814ecbc5). Tentunya tool ini masih perlu dikembangkan jika ingin masuk production, tapi idenya tetap sama.

Sebagai catatan: saya menggunakan tool uv agar bisa menjalankan Python plus requirementnya dengan PEP 723 tanpa harus menginstall tiap package dengan pip install dulu.

Semuanya sudah saya coba

Saya juga akan membahas aneka fitur yang sekarang dimiliki oleh berbagai agent yang “serius”: AGENTS.md, subagen, MCP, ACP, dan Skill.

Mari membuat agen sendiri

Saya akan menjelaskan komponen-komponen kode untuk membuat sebuah agen, diawali dengan konsepnya dulu.

Tool use/Function call

Pada artikel saya tentang RAG dua tahun lalu, saya sudah menyebut-nyebut tentang Function Calling (disebut juga: tool use). Intinya adalah: kita memberikan daftar tool yang bisa dipakai oleh agent, dan LLM bisa memakai tool itu jika diperlukan. Flownya seperti Who Wants to Be a Millionaire?:

  • Peserta ditanya sesuatu
  • Peserta bisa langsung menjawab, atau memilih bantuan (lifelines): telpon teman, 50/50, atau tanya penonton
  • Jika memilih bantuan: pilih bantuan mana (misalnya tanya penonton)
  • Sekarang ditanya lagi, plus diberi info jawaban dari penonton.
  • Peserta bisa menjawab (jika setuju dengan penonton), atau minta bantuan lagi. Misalnya setelah tanya penonton, tetap masih belum tahu jawabannya apa, peserta bisa nelpon teman.
  • Setelah itu peserta bisa menjawab, atau meminta bantuan lain

Tool use juga sama (bantuan bagi agen), bedanya di Who Wants to Be a Millionaire?, kita cuma bisa memakai satu jenis bantuan sekali, sedangkan di tool use, kita boleh memakai satu tool berkali-kali.

  • Kita memberikan daftar tool, dan pertanyaan atau tugas
  • Agen bisa menjawab, atau memakai tool
  • Setelah mendapatkan jawaban tool, agen bisa menjawab, atau minta penggunaan tool lain, atau tool yang sama dengan parameter berbeda
  • Agen bisa berhenti setelah menemukan jawaban (atau menyelesaikan tugas), atau karena melewati batas waktu (atau melewati batas interaksi)

Agar sebuah agen bisa berguna, kita bisa memberikan banyak tool. Tool yang biasanya ada di sebuah coding agent: list file, read file, grep (search di banyak file), write/edit file. Lalu supaya lebih pintar, coding agent juga biasanya bisa mencari di web, membaca isi website, dsb.

Dalam contoh ini, supaya gampang saya hanya membuat tool palugada: tool menjalankan kode Python. Misalnya agent ditanya sekarang jam berapa, maka agent bisa membuat kode python yang mencetak waktu saat ini, jika agent butuh search web, maka agentnya akan membuat kode Python untuk search di duckduckgo dengan Python. Jika agent ingin menulis ke file, maka dia bisa menjalankan kode Python yang membuat file, dsb.

Dengan satu tool ini, agent bisa melakukan apa saja. Tapi perlu diingat bahwa ini sangat berbahaya, karena kode Python bisa saja menghapus semua file Anda, menerbitkan file rahasia ke Internet, dsb. Jadi jika ingin aman jalankan kode program saya dalam docker. Supaya lebih aman, seharusnya sebelum menjalankan kode program, minta konfirmasi user dulu (asumsi: user membaca kodenya).

Sebuah tool hanyalah sebuah kode Python dengan isi terserah. Dalam kasus ini:

  • diberikan filename dan content, tuliskan content ke tool/filename. Saya menggunakan library terpisah supaya tidak memenuhi direktori saat ini dengan source code python yang hanya dipakai sekali oleh agen
  • jalankan tool dengan parameter params. Catatan: saya membatasi 5 menit, jadi jika toolnya butuh lebih dari 5 menit, maka akan dianggap error
  • kembalikan outputnya
import os
import subprocess
from pathlib import Path
from typing import Any, Iterable

def run_python_tool(filename: str, content: str, params: list[str]) -> dict[str, Any]:
    """Write a Python file under `tool/` and run it via `uv run`."""
    tool_dir = Path.cwd() / "tool"
    created = not tool_dir.exists()
    tool_dir.mkdir(parents=True, exist_ok=True)
    if created:
        subprocess.run(["uv", "venv"], cwd=tool_dir, capture_output=True, text=True)

    (tool_dir / filename).write_text(content, encoding="utf-8")

    try:
        result = subprocess.run(
            ["uv", "run", filename, *params],
            cwd=tool_dir,
            capture_output=True,
            text=True,
            timeout=300,
        )
        return {
            "success": result.returncode == 0,
            "stdout": result.stdout,
            "stderr": result.stderr,
            "returncode": result.returncode,
        }
    except subprocess.TimeoutExpired:
        return {"success": False, "error": "Command timed out after 300 seconds"}
    except Exception as exc:
        return {"success": False, "error": str(exc)}

Masalah Ukuran Konteks

Sebuan agent biasanya perlu konteks ukuran besar, puluhan ribu hingga jutaan token (kebanyakan saat ini sekitar 200 ribu token). Jika isi percakapan sudah penuh, maka kita bisa menghapus pesan awal (sliding window), atau meringkas percakapan awal (seperti saya jelaskan dulu di artikel tahun 2023: Membuat Telegram Bot memakai API ChatGPT di AWS Lambda).

OpenAI memakai pendekatan khusus untuk ini, memakai fitur token compaction yang menggunakan representasi internal model. Jika tertarik dengan ini, silakan baca artikel ini dari OpenAI.

Response Streaming

Jika ukuran konteks kita set sangat kecil, maka kode program akan jauh lebih singkat: teks bisa langsung diterima dan diproses. Tapi karena kita butuh konteks yang besar, kita perlu berurusan dengan API Streaming.

Agar kode lebih jelas, saya membuat fungsi process_stream yang akan menggunakan loop untuk menunggu jawaban dari LLM. Inti dari loop ini adalah: mengumpulkan teks hasil dari API, lalu menggabung teksnya (sambil kita print, supaya agent tidak kelihatan bengong).

Event yang kita terima dari server:

  • content_block_start artinya awal teks, bisa teks response dari LLM (jenisnya text) atau permintaan penggunaan tool (jenisnya tool_use)
  • content_block_delta artinya tambahkan pada teks yang sudah diterima, bisa text_delta (tambahan isi teks), atau input_json_delta (tamhakan isi tool_use)
  • content_block_stop artinya akhir dari message
import json

def process_stream(stream: Iterable[Any]) -> list[dict[str, Any]]:
    """Process a streaming Anthropic response and return content blocks."""
    content_blocks: list[dict[str, Any]] = []
    text_parts: list[str] = []
    tool_use: dict[str, Any] | None = None
    tool_json_parts: list[str] = []
    need_newline = True

    print("Assistant: ", end="", flush=True)

    for event in stream:
        if event.type == "content_block_start":
            if event.content_block.type == "text":
                text_parts = []
            elif event.content_block.type == "tool_use":
                tool_use = {"id": event.content_block.id, "name": event.content_block.name}
                tool_json_parts = []
            continue

        if event.type == "content_block_delta":
            if event.delta.type == "text_delta":
                text_parts.append(event.delta.text)
                print(event.delta.text, end="", flush=True)
                need_newline = True
            elif event.delta.type == "input_json_delta" and tool_use is not None:
                tool_json_parts.append(event.delta.partial_json)
            continue

        if event.type != "content_block_stop":
            continue

        if text_parts:
            print()
            need_newline = False
            content_blocks.append({"type": "text", "text": "".join(text_parts)})
            text_parts = []
            continue

        if tool_use is not None:
            if need_newline:
                print()
                need_newline = False
            raw_json = "".join(tool_json_parts)
            try:
                tool_input = json.loads(raw_json) if raw_json else {}
            except json.JSONDecodeError:
                tool_input = {}

            content_blocks.append(
                {"type": "tool_use", "id": tool_use["id"], "name": tool_use["name"], "input": tool_input}
            )
            tool_use = None
    return content_blocks

Agentic Loop

Sekarang bagian utama: yang disebut agentic loop adalah membiarkan agen menggunakan tool sampai selesai. Selesai di sini bisa benar-benar menemukan jawaban, mentok di sesuatu, atau butuh input dari user.

Pertama kita perlu membuat object client untuk melakukan koneksi LLM. Saya memakai SDK Anthropic untuk ini. Alasan saya: Claude Code didukung oleh banyak endpoint provider LLM. Di sini saya menggunakan platform Deepseek (karena sangat murah untuk eksperimen), tapi silakan diganti dengan Anthropic, Kimi, Z.AI, atau yang lain. DEEPSEEK_API_KEY saya simpan di environment variable.

from anthropic import Anthropic

def run():
    api_key = os.environ.get("DEEPSEEK_API_KEY") 
    client = Anthropic(base_url="https://api.deepseek.com/anthropic", api_key=api_key)
    
    current_dir = Path.cwd().name

Agar agen tahu sedang berada di direktori mana, saya memberikan direktori saat ini. Setelah itu bagian terpenting adalah instruksi sistem. Kelemahan memakai deepseek adalah: tidak menyediakan tool web search secara internal (tidak seperti Anthropic). Jadi saya menyertakan tips agar agen menggunakan search engine duckduckgo. PERHATIKAN: saat ini duckduckgo diblokir di Indonesia, jadi gunakan DNS anti blokir atau ganti search engine lain yang bisa diakses dari Python (google tidak bisa, karena butuh javascript)

    system_instruction = f"""You are a helpful AI assistant. You have access to tools to help the user.

Current directory: {current_dir}

Guidelines:
- When creating Python files, use PEP-723 inline script metadata
- Use the current directory as the base for file operations
- Use code to do everything: get current date/time, search information from web (e.g. duckduckgo via requests), retrieve information from web (e.g. requests + trafilatura)
"""

Kita perlu mendeskripsikan tool yang kita punya:

    messages = []
    
    tools = [
       {
            "name": "run_python_tool",
            "description": "Create a Python file in the tool directory and run it",
            "input_schema": {
                "type": "object",
                "properties": {
                    "filename": {
                        "type": "string",
                        "description": "The name of the Python file"
                    },
                    "content": {
                        "type": "string",
                        "description": "The content of the Python file"
                    },
                    "params": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Command-line parameters to pass to the script"
                    }
                },
                "required": ["filename", "content", "params"]
            }
        }
    ]

Sekarang kita bisa mencetak pesan petunjuk awal:

    print(f"Welcome! Current directory: {current_dir}")
    print("Type 'exit' or 'quit' to end the conversation.\n")

Main loop diawali dengan: user ingin melakukan apa. Jika ingin exit/quit, maka exit.

    while True:
        user_input = input("You: ").strip()
        
        if user_input.lower() in ["exit", "quit"]:
            print("Goodbye!")
            break
        
        if not user_input:
            continue

Berikutnya pesan dari user dimasukkan ke array messages, dan kita kirimkan ke provider LLM. Balasannya kita terima dengan process_stream lalu kita tambahkan hasil LLM ke array messages untuk membentuk percakapan.

Karena agen bisa meminta banyak tool, kita membuat loop, selama agent masih butuh tool, kita teruskan loop ini.

    messages.append({"role": "user", "content": user_input})
        while True:
            with client.messages.stream(
                model="deepseek-chat",
                max_tokens=65536,
                system=system_instruction,
                messages=messages,
                tools=tools
            ) as stream:
                content_blocks = process_stream(stream)
            messages.append({"role": "assistant", "content": content_blocks})

Kita cek apakah blok response mengandung tool_use, jika tidak, maka loop ini bisa dihentikan.

        tool_use_blocks = [block for block in content_blocks if block["type"] == "tool_use"]

        if not tool_use_blocks:
            break

Teorinya: LLM bisa langsung minta banyak tool sekaligus. Pada praktiknya biasanya hanya satu tool yang dipanggil. Meski demikian, ini ditangani dengan menggunakan loop terhadap semua tool:

            tool_results = []
            for block in tool_use_blocks:
                tool_name = block["name"]
                tool_input = block["input"]
                tool_id = block["id"]
                
                print(f"[Tool call: {tool_name}]")

Di contoh ini hanya ada satu tool yang digunakan, jika namanya sesuai, maka fungsi run_python_tool akan dipanggil. Silakan ditambahkan di sini jika ingin tool baru. Hasil tool kita masukkan ke array tool_results.

                if tool_name == "run_python_tool":
                    result = run_python_tool(
                        tool_input.get("filename", ""),
                        tool_input.get("content", ""),
                        tool_input.get("params", [])
                    )
                else:
                    result = {"success": False, "error": f"Unknown tool: {tool_name}"}
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": tool_id,
                    "content": json.dumps(result, ensure_ascii=False)
                })            

Semua hasil tool akan kita kirimkan dalam satu pesan:

            messages.append({"role": "user", "content": tool_results})

Dan cukup itu saja, tinggal kita tambahkan main:

if __name__ == "__main__":
    run()

Tentunya bisa juga saya masukkan saja semua fungsi run di luar fungsi, agar lebih singkat lagi. Totalnya sekarang: 199 baris.

Saya lalu mencoba bertanya apakah chiang mai planetarium buka hari ini (“is the chiang mai planetarium open today?”). Jawabannya benar. Cara yang dilakukan agent adalah: membuat kode python untuk melakukan search plus mencetak waktu saat ini, lalu dari hasil searchnya, dia membuat kode untuk mendownload halaman, dan memparse halamannya dengan BeautifulSoup4.

Aksi agen

Di direktori tool, saya lihat bahwa agen ini membuat search_planetarium.py dan planetarium_details.py. Tool pertama akan mencari dengan duckduckgo. Perhatikan bahwa ketika dijalankan ulang, tool yang dibuat tidak sama persis.

Bagian awal tool pertama

Tool kedua mendownload halaman, lalu memparse halamannya. Bisa dilihat penggunaan library BeautifulSoup.

Bagian awal tool kedua

Tentunya agentnya bisa diminta apa saja, misalnya membuat contoh program quicksort (agen akan membuat tool python yang menuliskan ke file), melakukan analisis data, dsb.

Fitur ekstra

Untuk membuat agen yang lebih baik, ada beberapa fitur ekstra yang biasanya perlu diimplementasikan.

AGENTS.md/CLAUDE.md

Ketika kita menggunakan coding agent seperti Codex, kita akan menggunakan AGENTS.md, dan ketika menggunakan Claude Code, kita akan menggunakan CLAUDE.md. Ini merupakan instruksi sistem tambahan spesifik untuk proyek saat ini.

Jadi jika kita ingin menambahkan fitur ini ke agen kita: cukup cek apakah ada file AGENTS.md/CLAUDE.md, baca teksnya, dan tambahkan ke instruksi sistem sebelum loop.

Skills

Karena masalah besar konteks, kita tidak ingin menyertakan semua hal di CLAUDE.md/AGENTS.md, tapi kita tetap ingin memberitahu agent cara melakukan sesuatu.

Contoh: ada banyak sekali tool untuk mengolah PDF, baik melalui program command line, atau library tertentu. Sebagian library bisa jalan di mesin kita, sebagian tidak bisa (library tidak ada, tool tidak terinstall, dsb), atau bisa tapi lambat.

Dalam kasus ini kita tahu mana cara terbaik untuk melakukan sesuatu. Kita bisa membuat skill baru misalnya bernama “manipulasi pdf” di sebuah file markdown, kita bisa sebutkan skillnya meliputi apa saja. Di situ kita bisa menjelaskan cara-cara memproses PDF (misalnya ekstrak teks, atau rotasi PDF). Kita juga bisa menyediakan skrip siap pakai yang bisa dijalankan agen.

Intinya adalah: meski agen kadang bisa mencari tahu sendiri cara melakuan sesuatu, lebih bagus kalau kita beritahu caranya. Seperti tool use, daftar skill diberikan ke agen di depan, tapi hanya deskripsi singkatnya. Jika agen ingin memakai skill tertentu, baru akan dibaca dokumen lengkapnya.

Repository contoh skill publik bisa dilihat di: anthropics/skills: Public repository for Agent Skills

MCP (Model Context Protocol)

Jika ingin definisi sederhana MCP: tool yang disediakan oleh pihak eksternal. Di contoh di atas, kita yang menyediakan tool, tapi seringkali kita ingin mengakses resource di Internet yang bukan buatan kita.

Salah satu masalah agen adalah: bagaimana mengakses program lain atau layanan lain. Misalnya ingin mengakses Gmail, teorinya ada dua yang bisa dilakukan:

  • Memakai API Gmail. Artinya agen perlu menulis banyak kode untuk mengakses layanan ini
  • Mengakses Gmail melalui web browser. Artinya agen perlu diberi akses ke browser. Kadang untuk melakukan sesuatu, butuh banyak langkah

Keduanya tidak ideal, jadi Anthropic membuat protokol baru bernama Model Context Protocol (MCP). MCP terdiri dari client/host (bisa berupa agen, atau aplikasi seperti Claude Desktop, intinya siapapun yang mengakses MCP), dan server (yang menyediakan layanan MCP). Keduanya berkomunikasi dengan protokol JSON-RPC.

Protokolnya kira-kira seperti ini (protokolnya ini masih berkembang, jadi bisa berubah). Pertama client mengirimkan “initialize”:

{
  "method": "initialize"
}

Lalu server membalas dengan versi dan kapabilitas

{
  "capabilities": {
    "logging": {},
    "tools": {
      "listChanged": true
    }
  },
  "serverInfo": {
    "name": "Minimax",
    "version": "1.0.0"
  }
}

Berikutnya client bisa bertanya: kamu bisa ngapain? (meminta daftar tool)

{
  "method": "tools/list",
  "params": {}
}

Contoh balasan server (ini cuma 1 tool yang tersedia, biasanya bisa banyak):

{
  "tools": [
    {
      "name": "web_search",
      "description": "Search the web, like Google",
      "inputSchema": {
      //skema input
      },
      "outputSchema": {
      //skema output
    }
  ]
}

Tool yang ada bisa dipanggil dengan tool/call. Misalnya seperti ini:

{
  "method": "tools/call",
  "params": {
    "name": "web_search",
    "arguments": {
      "query": "yohanes nugroho"
    }
  }
}

Lalu hasilnya kira-kira seperti ini:

CLI (lokal)

Server MCP bisa berupa program yang berjalan lokal di mesin yang sama dengan tempat Client MCP. Programnya terserah ditulis dalam bahasa apapun yang bisa mengakses stdin/stdout.

Untuk server CLI, kita tidak perlu autentikasi untuk mengakses MCP-nya, karena memang hanya menjalankan program lokal.

Server (lokal/remote)

Server MCP juga bisa berada di mesin lain melalui HTTP. Untuk ini kita butuh autentikasi. Biasanya ini dilakukan dengan header tertentu

ACP (Agent Communication Protocol)

MCP digunakan untuk mengakses resource/tool. ACP adalah protokol komunikasi antar agent. Di contoh yang saya berikan, saya membuat agent serbaguna, tapi kita bisa membuat agen yang spesial untuk tujuan tertentu dan memakai model khusus.

Contoh: di dalam satu perusahaan, mungkin ada agen yang memonitor security server. Jika ada masalah security baru, agen ini bisa melaporkan ke agen lain yang mungkin bisa melakukan aksi tertentu (memasang rule WAF baru, melaporkan ke tim security, dsb).

Protokol ACP ini cukup kompleks, jadi tidak akan saya bahas di sini. Standardnya bisa dibaca di: https://agentcommunicationprotocol.dev/

Subagen

Sebuah agen bisa dirancang untuk melakukan satu task tertentu dengan baik (memiliki tool, skill, MCP yang sesuai) dengan biaya optimal (memakai model yang murah tapi cocok untuk task itu). Contoh: agen untuk translasi dokumen (agen ini tidak perlu tahu tentang coding, hanya translasi teks).

Ketika agen utama butuh melakukan translasi teks, maka agen ini tidak perlu memenuhi context menerjemahkan sendiri, cukup minta subagen melakukan translasinya dan terima beres. Ini merupakan salah satu cara memanage token supaya tidak cepat penuh.

LLM Lokal?

Apakah kita bisa memakai LLM lokal untuk agent ini? jawabannya: jika kita punya hardware yang sangat bagus maka bisa (ada banyak model besar seperti Kimi, DeepSeek, GLM yang bagus tapi butuh RAM ratusan GB). Jika hardware kita terbatas, maka kita hanya bisa menjalankan model kecil dengan kemampuan yang juga terbatas. Beberapa model kecil sudah cukup OK untuk task coding sederhana, tapi tidak bisa menjawab pertanyaan umum.

Saya sudah mencoba model kecil untuk mencari jam buka planetarium Chiang Mai dan hasilnya gagal. GPT-OSS-120B (120 milyar paramer) kadang berhasil. Tapi saya perlu bermain-main dengan system prompt-nya. GPT-OSS:120B tidak paham apa itu PEP 723, dan selalu membuat skrip Python yang gagal jalan. Di akhir system prompt, saya tambahkan:

Example of using PEP-723 (put this on top of the file):
# /// script
# dependencies = ["requests", "beautifulsoup4", "lxml"]
# ///

Setelah itu baru modelnya bisa menjalankan skrip Python dengan benar.

Penutup

Aneka LLM sekarang sudah sangat canggih, dan bisa digunakan untuk AI Agent. Secara konsep AI Agent ini sangat sederhana, yang perlu diperhatikan adalah:

  • Aneka tool yang perlu disediakan untuk agen
  • Penanganan error

Selamat membuat sistem AI agent sendiri. Untuk yang tidak mengimplementasikan dan hanya ingin memakai: semoga artikel ini membantu memakai agent dengan baik. Saya banyak melihat aneka guide penggunaan tool agentic coding yang menyesatkan (membuang-buang token) karena sepertinya penulisnya tidak terlalu paham bagaimana agen bekerja.

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *