Membangun AI Agent dari Nol dengan OpenAI
Jika kamu belum pernah membangun AI agent sebelumnya, konsep ini mungkin terasa cukup mengintimidasi. Dengan begitu banyaknya abstraksi framework yang rumit, istilah baru, dan library berlapis-lapis, sangat wajar jika kita mengira bahwa membangun AI agent membutuhkan ribuan baris infrastruktur software yang rumit.
Kenyataannya jauh lebih sederhana: membangun AI agent yang berfungsi penuh sebenarnya sangat mudah dipahami begitu kamu menangkap konsep intinya. Di balik semua hype yang beredar, AI agent hanyalah sebuah model bahasa (LLM) yang dibungkus dalam loop yang memiliki state (kondisi) dan dibekali kemampuan untuk memanggil fungsi (tool/function calling).
Dalam tutorial ini, kita akan membangun AI agent lengkap dari nol menggunakan Python murni dan OpenAI SDK. Langkah demi langkah, kita akan memberi LLM memori, mengajarinya menggunakan custom tools, dan menyusun loop eksekusi yang mampu menyelesaikan instruksi multi-step.
Empat Primitives Utama AI Agent
Kita akan membangun sebuah class Agent di sekitar empat primitives inti:
- System Instructions: Prompt sistem yang menentukan persona, peran, tujuan, dan batasan kerja model.
- Memory: List pesan yang menyimpan riwayat percakapan agar konteks tetap terjaga lintas giliran (turns).
- Tools: Fungsi Python biasa yang dipasangkan dengan skema JSON agar model tahu kapan dan bagaimana meminta eksekusi fungsi eksternal.
- Execution Loop: Loop kontrol yang menerima instruksi pengguna, menjalankan tool yang diminta oleh LLM, lalu mengirimkan kembali hasilnya ke model hingga tugas selesai.
graph TD
A[1. System Instructions & User Goal] --> B[2. Memori]
B --> C[3. Penalaran LLM]
C --> D{Apakah Perlu Panggil Tool?}
D -- Ya --> E[4. Eksekusi Tool]
E -->|Kirim Hasil Balik| B
D -- Tidak --> F[Output Akhir ke Pengguna]
Gambar 1: Empat primitives inti dari siklus eksekusi agent.
Sebelum memulai, kamu membutuhkan OpenAI API Key (berbayar) atau API key lain yang kompatibel dengan endpoint OpenAI. Untuk mencoba secara gratis, kamu bisa mendapatkannya melalui OpenRouter. Kamu bisa memakai model gratis apa pun yang ada di OpenRouter (pilih model dengan akhiran/tag :free di katalog model OpenRouter, contohnya nvidia/nemotron-3-nano-30b-a3b:free, meta-llama/llama-3.3-70b-instruct:free, dan lainnya) atau model OpenAI jika kamu memakai API OpenAI langsung. Simpan konfigurasi ini ke dalam file .env:
OPENAI_API_KEY=<your-api-key>
OPENAI_BASE_URL=https://openrouter.ai/api/v1
OPENAI_MODEL=nvidia/nemotron-3-nano-30b-a3b:free
1. Pemanggilan API LLM Bersifat Stateless Secara Default
Ketika kamu memanggil API model bahasa, model tidak memiliki ingatan terhadap pesan-pesan sebelumnya. Setiap request HTTP ke LLM selalu dimulai dalam keadaan bersih tanpa konteks riwayat percakapan.
Mari siapkan lingkungan proyek menggunakan uv:
uv add openai python-dotenv
Berikut adalah implementasi dasar class Agent yang mengirim setiap prompt secara terisolasi:
import os
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
class Agent:
"""AI Agent sederhana yang dapat menjawab pertanyaan."""
def __init__(self):
self.client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL")
)
self.model = os.getenv("OPENAI_MODEL", "nvidia/nemotron-3-nano-30b-a3b:free")
self.system_message = (
"You are a helpful assistant"
)
def chat(self, message: str) -> str:
"""Menerima pesan user dan mengembalikan jawaban dari LLM."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self.system_message},
{"role": "user", "content": message}
],
temperature=0.1,
max_tokens=1024
)
return response.choices[0].message.content
Jika kita mengajukan dua pertanyaan berurutan yang saling berkaitan, masalah stateless ini langsung terlihat:
agent = Agent()
print(agent.chat("Saya punya 4 pisang. Kamu punya berapa?"))
print('---')
print(agent.chat("Saya makan 1 pisang. Sekarang sisa berapa?"))
Saya tidak memiliki pisang. Saya adalah AI, jadi saya tidak memiliki benda fisik atau buah-buahan. Terima kasih sudah bertanya!
---
Itu tergantung berapa banyak pisang yang kamu miliki sejak awal! Jika awalnya hanya punya 1, maka sekarang habis (0). Jika sebelumnya punya lebih banyak, berapa jumlah awalnya?
Karena request API kedua tidak membawa catatan dari percakapan pertama, model sama sekali tidak tahu bahwa awalnya kita memiliki 4 pisang.
2. Memberikan Memori pada Agent
Untuk mengatasi hilangnya konteks, agent membutuhkan memori. Untuk memori jangka pendek (short-term memory), solusinya sangat sederhana: sebuah list Python yang menyimpan setiap pesan dari user dan balasan dari asisten. Di setiap giliran baru, kita menyertakan seluruh riwayat pesan tersebut ke dalam panggilan API.
graph LR
subgraph Isolated Prompts
U1[Prompt Pengguna 1] --> M1[LLM]
U2[Prompt Pengguna 2] --> M2[LLM]
end
subgraph Stateful Memory
U3[Prompt Pengguna 1] --> S[Array Memori]
A1[Balasan Asisten 1] --> S
U4[Prompt Pengguna 2] --> S
S --> M3[LLM dengan Riwayat Lengkap]
end
Gambar 2: Perbandingan panggilan API terisolasi vs. riwayat percakapan berbasis stateful memory.
Mari kita perbarui class Agent untuk mempertahankan riwayat percakapan:
class Agent:
"""AI agent dengan memori jangka pendek untuk percakapan multi-turn."""
def __init__(self):
self.client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL")
)
self.model = os.getenv("OPENAI_MODEL", "nvidia/nemotron-3-nano-30b-a3b:free")
self.system_message = (
"You are a helpful assistant"
)
self.messages = [] # Memori jangka pendek
def chat(self, message: str) -> str:
"""Memproses pesan user sembari menjaga riwayat percakapan."""
self.messages.append({"role": "user", "content": message})
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self.system_message},
*self.messages
],
temperature=0.1,
max_tokens=1024
)
assistant_message = response.choices[0].message.content
self.messages.append({"role": "assistant", "content": assistant_message})
return assistant_message
Menjalankan kembali pengujian multi-turn sekarang memberikan hasil yang kita harapkan:
agent = Agent()
print(agent.chat("Saya punya 4 pisang. Kamu punya berapa?"))
print('---')
print(agent.chat("Saya makan 1 pisang. Sekarang sisa berapa?"))
Saya tidak memiliki pisang. Saya adalah AI, jadi tidak punya kepemilikan fisik. Tapi kamu punya 4!
---
Awalnya kamu punya **4 pisang**, dan kamu makan **1**.
Jadi: **4 - 1 = 3**.
Sekarang kamu punya **3 pisang tersisa**!
Lalu, bagaimana jika kita meminta data terkini (realtime)?
print(agent.chat("Bagaimana cuaca hari ini di Jakarta?"))
Saya tidak dapat mengakses data cuaca secara realtime, sehingga saya tidak bisa memberikan kondisi cuaca di Jakarta saat ini secara akurat.
Untuk melihat prakiraan terkini, kamu dapat memeriksa layanan cuaca seperti BMKG, Weather.com, atau Google Weather.
Memori menyelesaikan masalah kesinambungan obrolan, namun pengetahuan model tetap terbatas pada data yang dipelajarinya saat masa pelatihan. Jika pengguna menanyakan data cuaca terkini, harga saham, atau isi database internal, model membutuhkan tools.
3. Mendefinisikan Tools dengan Function Schemas
LLM tidak dapat menjalankan kode Python secara langsung di komputer kamu. Sebagai gantinya, fitur function calling bekerja melalui sebuah kontrak kesepakatan:
- Skema JSON (JSON Schema): Kita mengirimkan spesifikasi JSON ke LLM yang mendeskripsikan nama fungsi, tujuan penggunaannya, dan parameter apa saja yang dibutuhkan.
- Keputusan LLM: Jika LLM memutuskan bahwa ia memerlukan tool tersebut untuk menjawab pertanyaan, ia akan mengembalikan respons berupa payload JSON berisi nama fungsi dan nilai argumen yang dibutuhkan.
- Eksekusi Python: Program Python kita membaca payload tersebut, mengeksekusi fungsi Python yang bersangkutan secara lokal di server kita, lalu mengirimkan hasil keluarannya kembali ke model.
Berikut adalah contoh tool mock cuaca sederhana yang mengimplementasikan pola ini:
class WeatherTool:
"""Tool untuk mengambil metrik cuaca berdasarkan nama kota."""
def get_schema(self) -> dict:
"""Mengembalikan skema JSON yang mendeskripsikan fungsi ini ke LLM."""
return {
"type": "function",
"function": {
"name": "check_weather",
"description": (
"Mengambil rincian cuaca terkini untuk suatu kota. "
"Gunakan tool ini setiap kali pengguna menanyakan tentang cuaca, suhu, "
"atau kondisi udara di kota tertentu."
),
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "Nama kota tujuan, contoh: 'Jakarta', 'Bandung', 'Surabaya'."
}
},
"required": ["city"]
}
}
}
def execute(self, city: str) -> dict:
"""Simulasi eksekusi backend yang mengembalikan data cuaca terstruktur."""
weather_db = {
"jakarta": {"city": "Jakarta", "temperature": 32, "condition": "Cerah berawan", "humidity": 70},
"bandung": {"city": "Bandung", "temperature": 24, "condition": "Berawan", "humidity": 85},
"surabaya": {"city": "Surabaya", "temperature": 34, "condition": "Cerah", "humidity": 65},
"yogyakarta": {"city": "Yogyakarta", "temperature": 28, "condition": "Hujan ringan", "humidity": 80}
}
city_key = city.lower().strip()
if city_key in weather_db:
return weather_db[city_key]
return {
"error": f"Data cuaca untuk '{city}' tidak tersedia. Kota yang tersedia: Jakarta, Bandung, Surabaya, Yogyakarta."
}
4. Membangun Loop Eksekusi Utama Agent
Sekarang kita gabungkan memori, tools, dan loop kontrol menjadi satu kesatuan agent yang bekerja mandiri.
Agent perlu menangani dua kemungkinan skenario saat menerima balasan dari OpenAI:
- Skenario A: LLM mengembalikan teks jawaban biasa -> Kita hentikan loop dan tampilkan jawaban ke pengguna.
- Skenario B: LLM meminta pemanggilan fungsi (
tool_calls) -> Kita jalankan fungsi Python tersebut, format hasilnya ke dalam pesan ber-role: "tool", tambahkan ke memori riwayat pesan, lalu panggil LLM kembali.
Berikut implementasi lengkap class Agent:
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
class Agent:
"""AI agent yang mampu menangani function calling dan stateful memory."""
def __init__(self, tools=None):
self.client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
self.model = os.getenv("OPENAI_MODEL", "nvidia/nemotron-3-nano-30b-a3b:free")
self.system_message = (
"You are a helpful and friendly assistant. "
"If the user asks about the weather in a city, ALWAYS use the 'check_weather' tool. "
"Never guess or make up weather data."
)
self.messages = []
self.tools = tools or []
self.tool_map = {
tool.get_schema()["function"]["name"]: tool
for tool in self.tools
}
def _get_tool_schemas(self):
return [tool.get_schema() for tool in self.tools]
def step(self, new_input):
"""Memproses input baru (string prompt user atau list pesan hasil tool) dan memanggil LLM."""
if isinstance(new_input, str):
self.messages.append({"role": "user", "content": new_input})
elif isinstance(new_input, list):
# Masukkan pesan hasil eksekusi tool langsung ke riwayat memori
self.messages.extend(new_input)
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self.system_message},
*self.messages
],
tools=self._get_tool_schemas() if self.tools else None,
tool_choice="auto" if self.tools else None,
temperature=0.1,
max_tokens=1024
)
assistant_message = response.choices[0].message
# Simpan respons asisten ke dalam riwayat percakapan
assistant_dict = {
"role": "assistant",
"content": assistant_message.content or ""
}
if assistant_message.tool_calls:
assistant_dict["tool_calls"] = [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
}
for tc in assistant_message.tool_calls
]
self.messages.append(assistant_dict)
return assistant_message
Selanjutnya, kita buat fungsi run_agent yang memandu siklus loop hingga tugas terselesaikan (dibatasi oleh max_turns untuk mencegah loop tanpa batas):
def run_agent(user_input: str, tools: list, max_turns: int = 10) -> str:
"""Menjalankan siklus eksekusi agent hingga jawaban akhir berbentuk teks dikembalikan."""
agent = Agent(tools=tools)
current_input = user_input
for i in range(max_turns):
print(f"\n{'='*50}\nIterasi {i + 1}\n{'='*50}")
print(f"Input: {current_input}")
response = agent.step(current_input)
# Cek apakah LLM meminta pemanggilan tool
if response.tool_calls:
print(f"Keputusan Agent: {response.content or '(meminta eksekusi tool)'}")
tool_results = []
for tool_call in response.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
print(f"Memanggil Tool: {tool_name}")
print(f" Argumen: {tool_args}")
# Jalankan fungsi Python lokal dengan penanganan aman
tool = agent.tool_map.get(tool_name)
if not tool:
result = {"error": f"Tool '{tool_name}' tidak ditemukan."}
else:
result = tool.execute(**tool_args)
print(f"Output Tool: {result}")
# Format output tool sesuai spesifikasi API OpenAI
tool_results.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_name,
"content": json.dumps(result)
})
current_input = tool_results
else:
print(f"Output Akhir: {response.content}")
return response.content
return "Telah mencapai batas iterasi maksimum."
5. Menguji Agent: Menangani Alur Kerja Multi-Step
Sekarang mari kita uji agent kita dengan pertanyaan yang membutuhkan dua pemanggilan tool terpisah untuk bisa dijawab:
weather_tool = WeatherTool()
run_agent(
"Saya berencana pergi ke Bandung besok. Bagaimana cuacanya? Apakah lebih sejuk dibandingkan Surabaya?",
tools=[weather_tool]
)
Berikut alur eksekusi langkah demi langkah yang terjadi di konsol:
==================================================
Iterasi 1
==================================================
Input: Saya berencana pergi ke Bandung besok. Bagaimana cuacanya? Apakah lebih sejuk dibandingkan Surabaya?
Keputusan Agent: (meminta eksekusi tool)
Memanggil Tool: check_weather
Argumen: {'city': 'Bandung'}
Output Tool: {'city': 'Bandung', 'temperature': 24, 'condition': 'Berawan', 'humidity': 85}
==================================================
Iterasi 2
==================================================
Input: [{'role': 'tool', 'tool_call_id': 'call_ghi789', 'name': 'check_weather', 'content': '{"city": "Bandung", "temperature": 24, "condition": "Berawan", "humidity": 85}'}]
Keputusan Agent: (meminta eksekusi tool)
Memanggil Tool: check_weather
Argumen: {'city': 'Surabaya'}
Output Tool: {'city': 'Surabaya', 'temperature': 34, 'condition': 'Cerah', 'humidity': 65}
==================================================
Iterasi 3
==================================================
Input: [{'role': 'tool', 'tool_call_id': 'call_jkl012', 'name': 'check_weather', 'content': '{"city": "Surabaya", "temperature": 34, "condition": "Cerah", "humidity": 65}'}]
Output Akhir:
Berikut adalah perbandingan cuacanya:
- Bandung: Berawan, suhu 24°C, kelembapan 85%
- Surabaya: Cerah, suhu 34°C, kelembapan 65%
Bandung jauh lebih sejuk dibandingkan Surabaya dengan selisih suhu sekitar 10°C.
sequenceDiagram
participant User as Pengguna
participant Agent as Agent Loop
participant LLM as LLM API
participant Tool as WeatherTool
User->>Agent: Bandingkan cuaca di Bandung & Surabaya
Agent->>LLM: Kirim prompt + skema tool
LLM-->>Agent: Minta tool check_weather(city='Bandung')
Agent->>Tool: execute(city='Bandung')
Tool-->>Agent: Mengembalikan 24°C, Berawan
Agent->>LLM: Kirim hasil tool + riwayat percakapan
LLM-->>Agent: Minta tool check_weather(city='Surabaya')
Agent->>Tool: execute(city='Surabaya')
Tool-->>Agent: Mengembalikan 34°C, Cerah
Agent->>LLM: Kirim hasil tool + riwayat percakapan
LLM-->>Agent: Mengembalikan jawaban teks akhir
Agent-->>User: Bandung (24°C) lebih sejuk 10°C dari Surabaya (34°C)
Gambar 3: Diagram urutan yang menggambarkan bagaimana memori dan eksekusi tool memungkinkan penalaran multi-step.
Perhatikan bagaimana agent secara otonom memecah instruksi pengguna:
- Agent menyadari bahwa ia membutuhkan informasi cuaca Bandung terlebih dahulu, sehingga ia memanggil
check_weather(city='Bandung'). - Setelah menerima hasil cuaca Bandung, ia menyadari bahwa ia masih membutuhkan cuaca Surabaya, sehingga ia memanggil
check_weather(city='Surabaya'). - Setelah kedua fakta tersimpan rapi di dalam memori, agent mensintesis respons perbandingan akhir secara lengkap.
Catatan tentang Parallel Tool Calling: Bergantung pada model dan instruksi prompt yang diberikan, LLM modern sering kali dapat memicu beberapa pemanggilan tool sekaligus dalam satu turn balasan (parallel tool calling) jika model menyadari bahwa kedua kota bisa diperiksa secara bersamaan. Struktur loop eksekusi kita di atas (
for tool_call in response.tool_calls:) sudah siap menangani pemanggilan tunggal maupun paralel tanpa masalah.
Apa Saja yang Ditangani oleh Framework Agent (Dan Mengapa Fundamental Tetap Penting)
Sekarang setelah kamu berhasil membangun agent dari nol, kamu telah memahami model mental fundamental yang menggerakkan hampir seluruh sistem AI agent modern saat ini.
Ketika kamu melihat framework populer seperti LangChain, LlamaIndex, CrewAI, atau AutoGen, kamu akan menyadari bahwa mereka tidak melakukan hal magis di balik layar. Mereka pada dasarnya membangun lapisan abstraksi level produksi di atas primitives yang baru saja kamu tulis sendiri.
Kemudahan yang Disediakan Framework
- Unified Model Interfaces: Abstraksi terstandarisasi untuk berbagai penyedia LLM (OpenAI, Anthropic, Gemini, Ollama, dll.). Karena setiap provider memiliki sintaks SDK yang berbeda, framework memungkinkan kamu menukar model tanpa menulis ulang skema tool atau loop pesan.
- Integrasi Siap Pakai: Konektor bawaan ke search engine, database, parser file (PDF, CSV), dan ribuan API pihak ketiga.
- Manajemen Memori Tingkat Lanjut: Fitur sliding window otomatis, penyimpanan vektor (RAG), dan integrasi basis data persisten (bukan hanya list Python di memori RAM).
- Orkestrasi Multi-Agent: Pola terstandarisasi untuk membagi pekerjaan dan mengalihkan tugas (hand-off) antar beberapa agent spesialis.
- Observabilitas & Telemetri: Pelacakan otomatis bawaan untuk latensi, penggunaan token, dan jejak eksekusi tool di tiap langkah (tracing).
Mengapa Memahami Fundamental Tetap yang Utama
Meskipun framework mempercepat proses pembuatan prototipe, memahami primitives dasar sangat krusial:
- Debugging dengan Percaya Diri: Saat agent yang dibuat dengan framework tiba-tiba macet, berhalusinasi saat mengisi argumen tool, atau mengalami error tersembunyi, kamu tidak akan terjebak dalam rasa bingung di balik kotak hitam abstraksi. Kamu tahu persis cara memeriksa susunan array pesan, membetulkan skema JSON, atau menyesuaikan alur loop kontrol.
- Menghindari Over-Engineering: Untuk banyak kebutuhan praktis (seperti script otomatisasi CLI sederhana atau alat integrasi API internal), menulis loop agent custom sekitar ~100 baris kode jauh lebih cepat, bersih, dan jauh lebih mudah dirawat dibandingkan membawa ketergantungan library framework yang berat.
Kesimpulan
Membangun AI agent ternyata tidak serumit yang dibayangkan begitu kamu memahami konsep dasarnya:
- Simpan riwayat interaksi dalam list pesan yang memiliki state.
- Sediakan fungsi Python menggunakan skema JSON terstruktur.
- Kendalikan siklus interaksi menggunakan loop eksekusi yang mengirimkan kembali hasil tool ke LLM hingga instruksi selesai dijawab.
Dengan membangun fondasi ini sendiri, kamu kini dapat mengevaluasi framework-framework tingkat tinggi dengan pemahaman yang utuh, atau bahkan menulis loop agent ringan kamu sendiri kapan pun kamu membutuhkan kendali penuh.
Sumber Daya & Bacaan Lanjutan
- OpenAI Function Calling Guide: Dokumentasi resmi OpenAI mengenai skema tool dan structured outputs.
- Anthropic: Building Effective Agents: Panduan komprehensif mengenai alur kerja, primitives agentik, dan siklus evaluasi.