← Kembali ke Blog

Bikin AI Agent dengan Tool Use dari Nol di Python

Tutorial ini ajarin kamu bikin AI agent dengan tool use dari nol di Python, cuma pake Anthropic SDK. Nggak ada LangChain, nggak ada LangGraph, nggak ada abstraksi. Kamu nulis agent loop sendiri, define tool sendiri, dan handle conversation sendiri.

Ngerti yang terjadi di dalam agent.run() itu bedanya antara debug agent dan nebak-nebak. Kalau ada yang error dan itu pasti terjadi, kamu perlu tau masalahnya di layer mana: definisi tool, eksekusi tool, message history, atau reasoning model.

Tutorial ini bikin agent yang jalan dari nol step by step. Selesai nanti kamu punya CLI tool yang bisa search web, hitung matematika, dan inget apa yang kamu bilang tiga pesan lalu.

Prerequisites

  • Python 3.10 atau lebih baru (cek dengan python3 --version)
  • API key Anthropic (dapat di console.anthropic.com)
  • Familiar basic sama function dan dictionary di Python

Yang Kita Bangun

CLI agent dengan tiga tool:

  1. Web search pakai Tavily API (free tier: 1.000 pencarian/bulan)
  2. Kalkulator buat ekspresi matematika
  3. File reader buat ngambil informasi dari file lokal

Agent mutusin tool mana yang dipanggil berdasarkan pertanyaan kamu, jalanin tool-nya, dan pake hasilnya buat bikin jawaban. Kalau satu tool call belum cukup, dia bisa panggil tool lain. Ini loop inti di balik semua agent, dari Claude Code sampai ChatGPT plugins.

Step 1: Install Dependencies

mkdir agent-from-scratch && cd agent-from-scratch
python3 -m venv .venv
source .venv/bin/activate

pip install anthropic tavily-python

Set API key kamu:

export ANTHROPIC_API_KEY="sk-ant-..."
export TAVILY_API_KEY="tvly-..."

Key Tavily gratis di basic tier. Daftar di tavily.com kalau belum punya.

Step 2: Define Tool Kamu

API tool use Anthropic expecting JSON schema buat tiap tool. Model baca schema ini buat mutusin tool mana yang cocok sama pertanyaan user.

# tools.py
"""Definisi tool buat agent."""

import json
import os
from tavily import TavilyClient

tavily = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])

TOOLS = [
    {
        "name": "web_search",
        "description": "Search web buat info terkini. Pake ini kalau kamu perlu cari fakta, berita terbaru, atau riset topik.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Query pencarian"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "calculate",
        "description": "Hitung ekspresi matematika. Pake buat kalkulasi.",
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "Ekspresi matematika, contoh: '2 + 3 * 4'"
                }
            },
            "required": ["expression"]
        }
    },
    {
        "name": "read_file",
        "description": "Baca isi file teks lokal. Pake buat ngambil informasi yang user simpen di file.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Path file yang mau dibaca"
                }
            },
            "required": ["path"]
        }
    }
]


def execute_tool(name: str, input_data: dict) -> str:
    """Jalanin tool berdasarkan nama, return hasil sebagai string."""
    try:
        if name == "web_search":
            result = tavily.search(query=input_data["query"], max_results=3)
            snippets = []
            for r in result.get("results", []):
                snippets.append(f"{r['title']}
{r['url']}
{r['content']}")
            return "

".join(snippets) if snippets else "Nggak ada hasil ditemukan."

        elif name == "calculate":
            allowed = {"__builtins__": {}}
            result = eval(input_data["expression"], allowed)
            return str(result)

        elif name == "read_file":
            path = input_data["path"]
            if not os.path.exists(path):
                return f"Error: file nggak ditemukan di {path}"
            with open(path) as f:
                content = f.read()
            if len(content) > 5000:
                content = content[:5000] + "
... (dipotong)"
            return content

        else:
            return f"Tool nggak dikenal: {name}"

    except Exception as e:
        return f"Error jalanin {name}: {str(e)}"

Beberapa keputusan desain:

  • List TOOLS pake format schema tool Anthropic langsung. Nggak ada wrapper, nggak ada translation layer framework. Ini yang diliat model.
  • execute_tool itu function biasa dengan switch statement. Kalau debug tool error, kamu langsung nyampe ke masalahnya.
  • Kalkulator pake eval() dengan builtins kosong. Buat production, pake math parser yang proper kayak numexpr atau asteval. Ini cuma biar contohnya pendek.
  • File reader dipotong di 5.000 karakter. Ngirim file 100MB ke model bikin token sia-sia dan cepet kena context limit.

Step 3: Bangun Agent Loop

Ini intinya. Loop ini ngelakuin tiga hal: kirim pesan ke model, cek apakah model mau panggil tool, jalanin tool, dan ulang sampai model kasih jawaban teks.

# agent.py
"""Agent loop. Nggak ada framework, cuma API calls."""

import anthropic
from tools import TOOLS, execute_tool

client = anthropic.Anthropic()

SYSTEM_PROMPT = """Kamu adalah asisten yang helpful dengan akses ke tools.
Pake tools kalau itu bantu jawab pertanyaan user.
Kalau tool call gagal, jelasin apa yang salah dan coba pendekatan lain.
Selalu cantumkan sumber kalau pake hasil web search."""


def run_agent(user_message: str, history: list[dict] | None = None) -> tuple[str, list[dict]]:
    """Jalanin agent loop, return (jawaban_akhir, history_updated)."""
    if history is None:
        history = []

    history.append({"role": "user", "content": user_message})

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=4096,
            system=SYSTEM_PROMPT,
            tools=TOOLS,
            messages=history
        )

        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    result = execute_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })

            history.append({"role": "assistant", "content": response.content})
            history.append({"role": "user", "content": tool_results})

        elif response.stop_reason == "end_turn":
            text = ""
            for block in response.content:
                if block.type == "text":
                    text += block.text
            history.append({"role": "assistant", "content": response.content})
            return text, history

        else:
            text = ""
            for block in response.content:
                if block.type == "text":
                    text += block.text
            return text or f"Stop reason nggak dikenal: {response.stop_reason}", history

Struktur loop-nya sama dengan pattern yang dipake semua agent framework di bawah tenda:

  1. Kirim pesan ke model
  2. Kalau model bilang tool_use, jalanin tools dan tambahin hasilnya sebagai user message
  3. Kalau model bilang end_turn, ambil teksnya dan return

Detail kuncinya ada di langkah 2: tool results masuk ke percakapan sebagai user message dengan tool_result blocks. Ini cara Anthropic API kerja. Model liat output tool di giliran berikutnya dan mutusin apa yang harus dilakukan.

Perhatikan loop while True. Model bisa panggil beberapa tool berurutan sebelum kasih jawaban akhir. Misalnya, user tanya "Berapa populasi Tokyo dan berapa 15% dari angka itu?" Model panggil web_search buat populasi, terus panggil calculate dengan hasilnya, terus kasih jawaban akhir. Semua dalam satu agent loop.

Step 4: Tambah CLI Interface

# main.py
"""CLI interface buat agent."""

from agent import run_agent

def main():
    print("Agent siap. Ketik 'quit' buat keluar, 'clear' buat reset history.
")
    history = []

    while True:
        try:
            user_input = input("You: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("
Bye.")
            break

        if not user_input:
            continue
        if user_input.lower() == "quit":
            break
        if user_input.lower() == "clear":
            history = []
            print("History direset.
")
            continue

        answer, history = run_agent(user_input, history)
        print(f"
Agent: {answer}
")

if __name__ == "__main__":
    main()

Step 5: Test

python main.py

Coba percakapan ini:

You: Berapa harga Bitcoin sekarang?
Agent: [search web, kasih harga terkini dengan sumber]

You: Berapa 15% dari angka itu?
Agent: [pake konteks sebelumnya, hitung 15%, kasih hasilnya]

You: Simpen hasil kalkulasi itu di file notes.txt
Agent: [pake read_file buat cek file ada nggak, terus respon]

Pertanyaan kedua jalan karena conversation history include harga Bitcoin dari jawaban pertama. Model punya konteks yang cukup buat hitung 15% tanpa search ulang.

Step 6: Handle Failure Mode yang Umum

Tool call error

Kalau tool gagal, model dapet error message dan bisa coba cara lain. Function execute_tool return error string tanpa raise exception, jadi model selalu dapet respon.

Context window overflow

Percakapan panjang kena context limit model. Ini simple guard:

def trim_history(history: list[dict], max_messages: int = 40) -> list[dict]:
    """Keep context awal dengan trim pesan lama."""
    if len(history) <= max_messages:
        return history
    return history[:2] + history[-(max_messages - 2):]

Ini preserve user message pertama (yang sering punya konteks penting) dan pesan-pesan terbaru. Buat production, pake prompt caching Anthropic buat ngurangi cost di bagian conversation yang nggak berubah.

Hallucinated tool calls

Kadang model coba pake tool yang nggak ada. API handle ini: kalau model panggil tool yang nggak ada di list TOOLS kamu, dia raise error. Catch dan tambahin pesan yang bilang ke model tool mana yang tersedia.

try:
    response = client.messages.create(...)
except anthropic.BadRequestError as e:
    if "tool" in str(e).lower():
        history.append({"role": "assistant", "content": [
            {"type": "text", "text": f"Tool call gagal: {e}. Tool yang tersedia: web_search, calculate, read_file"}
        ]})
        continue
    raise

Kapan Pake Framework Saja

Tutorial ini nunjukin fundamentals. Framework ada karena fundamentals makin complicated di skala besar:

  • LangGraph nambahin state machines, persistence, human-in-the-loop checkpoints, dan parallel tool execution. Pake kalau agent kamu punya branching logic atau perlu survive crash.
  • Claude Agent SDK ngasih pre-built patterns buat tool use, multi-turn conversations, dan integrasi MCP. Pake kalau mau ship cepet dan pattern-nya cocok sama use case kamu.
  • OpenAI Agents SDK nambahin routing, guardrails, dan handoffs antar agent. Pake kalau butuh multi-agent orchestration dengan safety checks.

Pilihan framework tergantung bentuk masalah kamu, bukan mana yang "terbaik." Kalau agent kamu bisa diekspresi dalam 100 baris Python kayak yang ini, framework itu overhead. Kalau butuh persistence, retries, parallel execution, atau human review, baru pake framework.

Langkah Selanjutnya

  • Tambah prompt caching buat ngurangi cost di system prompt yang diulang. Cache hit Anthropic cost 90% lebih murah dari prompt fresh.
  • Tambah streaming pakai client.messages.stream() biar user liat token satu per satu, nggak nunggu full response.
  • Tambah MCP biar agent kamu bisa akses database, API, dan tools lain tanpa hardcoded.
  • Baca dokumentasi tool use lengkap di docs.anthropic.com/en/docs/build-with-claude/tool-use.

Referensi

Butuh Bantuan Implementasi?

Saya membantu tim mendesain dan membangun infrastruktur cloud scalable, pipeline DevOps, dan sistem production-grade.

Konsultasi Gratis