12: Membangun Tools untuk Agen Anda
Di Bagian IV, kita akan membangun sebuah agen(agent) — AI yang memikirkan apa yang harus dilakukan dengan permintaan pengguna lalu melakukannya, alih-alih hanya merespons seperti chatbot.
Inilah perbedaannya dalam praktik. Misalkan seorang pengguna bertanya, "Tolong batalkan pesanan #12345." Chatbot akan menjawab seperti ini: "Buka My Page > Order History dan klik tombol 'Cancel' untuk pesanan tersebut." Dari situ, pengguna sendiri yang harus mengikuti langkah-langkah itu. Sebaliknya, sebuah agen yang melakukan pekerjaan tersebut — ia mencari pesanan, memeriksa apakah pesanan tersebut memenuhi syarat untuk dibatalkan, lalu membatalkannya. Ia mengambil tindakan(action).
Yang membuat hal itu mungkin adalah tools: tool yang mencari pesanan, tool yang membatalkannya, tool yang mengirim email. Dengan tools, sebuah LLM tidak lagi terbatas pada menghasilkan teks dan mulai benar-benar menyelesaikan berbagai hal.
Kita akan membangunnya bagian demi bagian sepanjang Bagian IV: pertama tools yang akan digunakan agen (bab ini), kemudian menghubungkan tools tersebut ke sebuah LLM (Bab 13), dan terakhir agent loop yang berputar melalui putuskan → bertindak → amati (Bab 14).
Bab ini membahas bagian pertama — mendefinisikan tools, menghubungkannya ke data nyata, dan menangani error dengan aman.
12.1) Mendefinisikan Tools dengan Decorator @tool
12.1.1) Bagaimana Cara Kerja Tool?
Kita baru saja melihat sebuah agen mencari dan membatalkan pesanan menggunakan tools. Sebelum melangkah lebih jauh, ada satu hal yang perlu diluruskan: ketika orang berkata "LLM menggunakan tool," kedengarannya seolah LLM-lah yang memanggilnya secara langsung. Bukan begitu. LLM tidak pernah mengeksekusi apa pun sendiri — yang dilakukannya hanyalah meminta agar sebuah tool dipanggil dengan argumen tertentu. Eksekusi sesungguhnya terjadi di dalam kode kita.
Agar itu bisa berjalan, LLM harus tahu tool mana saja yang ada dan kapan masing-masing tool berlaku. Jadi setiap tool dilengkapi dengan tiga bagian metadata:
name— sebuah pengenal singkat sepertiget_orderyang digunakan LLM untuk menentukan tool mana yang diinginkannya.description— sebuah kalimat yang menjelaskan apa yang dilakukan tool dan kapan menggunakannya. Inilah yang dibaca LLM untuk memilih tool yang tepat untuk pekerjaan tersebut.- Input schema — apa saja parameter dari tool: nama, tipe, dan maknanya. LLM membutuhkan ini untuk mengisi argumen dengan benar.
Tidak satu pun dari ini memerlukan kerja tambahan dari Anda. name berasal langsung dari nama fungsi, description berasal dari docstring-nya, dan input schema berasal dari type hint pada parameter. Yang perlu Anda lakukan hanyalah menyematkan decorator @tool dari LangChain.
12.1.2) Membangun Tool Pertama Anda
Mari kita praktikkan. Tulis sebuah fungsi dengan type hint dan docstring, lalu hias dengan @tool.
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Mendapatkan cuaca saat ini untuk kota tertentu."""
return f"It's always sunny in {city}!"Mari kita periksa apa yang dihasilkan @tool untuk kita.
print(get_weather.name)
# Output: get_weather
print(get_weather.description)
# Output: Mendapatkan cuaca saat ini untuk kota tertentu.
print(get_weather.args)
# Output: {'city': {'title': 'City', 'type': 'string'}}Nama fungsi get_weather menjadi name-nya, docstring menjadi description-nya, dan type hint city: str menjadi input schema-nya. Ketiga bagian metadata dari bagian sebelumnya dihasilkan secara otomatis. Inilah yang digunakan LLM untuk memilih sebuah tool dan mengisi argumennya.
Setelah @tool disematkan, fungsi tersebut menjadi objek tool LangChain, yang berarti Anda tidak bisa lagi memanggilnya seperti fungsi biasa — get_weather("Paris") tidak akan berfungsi. Sebagai gantinya, Anda memanggilnya dengan .invoke(), metode eksekusi standar yang sama yang kita gunakan untuk chain di Bab 6. Argumen dimasukkan sebagai dictionary:
result = get_weather.invoke({"city": "Paris"})
print(result)
# Output: It's always sunny in Paris!12.1.3) Menyesuaikan name dan description
Secara default, name berasal dari nama fungsi dan description dari docstring. Anda bisa menimpa keduanya.
Berikan sebuah nama sebagai argumen pertama untuk @tool:
@tool("web_search")
def search(query: str) -> str:
"""Mencari informasi di web."""
return f"Results for: {query}"
print(search.name)
# Output: web_searchAnda juga bisa menimpa description, menggunakan parameter description. Ini berguna ketika Anda ingin menyimpan docstring sebagai catatan untuk developer lain sambil memberikan sesuatu yang lebih disesuaikan untuk LLM:
@tool("calculator", description="Melakukan operasi aritmetika. Gunakan ini untuk soal matematika apa pun.")
def calc(expression: str) -> str:
"""Mengevaluasi string ekspresi matematika."""
return str(eval(expression)) # PERINGATAN: eval() tidak aman. Jangan pernah menggunakannya di production.Gunakan snake_case untuk nama tool — beberapa penyedia LLM menolak nama yang mengandung spasi atau karakter khusus.
12.1.4) Mendefinisikan Input Schema dengan Pydantic
Ketika sebuah tool memiliki beberapa parameter, atau Anda ingin mendeskripsikan masing-masing secara individual, definisikan input schema dengan model Pydantic sebagai gantinya. Ini adalah BaseModel dan Field yang sama yang kita gunakan untuk structured output di Bab 7.
from pydantic import BaseModel, Field
from langchain.tools import tool
class WeatherInput(BaseModel):
"""Input untuk kueri cuaca."""
location: str = Field(description="Nama kota (mis. Seoul, Tokyo)")
units: str = Field(default="celsius", description="Satuan suhu (celsius atau fahrenheit)")
@tool(args_schema=WeatherInput)
def get_weather_detailed(location: str, units: str = "celsius") -> str:
"""Mendapatkan cuaca saat ini dengan satuan suhu yang dipilih."""
temp = 22 if units == "celsius" else 72
return f"Current weather in {location}: {temp} degrees {units[0].upper()}"Apa pun yang Anda tulis di Field(description=...) menjadi bagian dari input schema yang dibaca LLM, sehingga ia tahu persis apa arti setiap parameter. Sebagian besar waktu, type hint dan docstring yang jelas adalah semua yang Anda butuhkan — gunakan args_schema hanya ketika Anda membutuhkan tingkat detail ekstra per parameter.
12.2) Menangani Error pada Tool
Di dunia nyata, tool bisa gagal — koneksi database terputus, atau muncul input yang tidak Anda rencanakan. Di bagian ini, kita akan menangani error tersebut di dalam tool itu sendiri, sehingga agen bisa merespons secara masuk akal alih-alih berhenti total. Pertama, mari kita siapkan fungsi-fungsi yang akan diandalkan tool kita.
12.2.1) Persiapan: Fungsi Pencarian Produk
# product_service.py
PRODUCTS = {
1: {"name": "Wireless Mouse", "price": 29.99, "stock": 120},
2: {"name": "Mechanical Keyboard", "price": 89.99, "stock": 0},
3: {"name": "USB-C Hub", "price": 45.50, "stock": 35},
4: {"name": "Laptop Stand", "price": 39.00, "stock": 8},
}
def fetch_product(product_id: int) -> dict:
"""Mencari informasi produk berdasarkan ID."""
product = PRODUCTS.get(product_id)
if product is None:
raise ValueError(f"Product with ID {product_id} not found.")
return product
def fetch_stock(product_id: int) -> int:
"""Mengembalikan jumlah stok untuk sebuah produk."""
product = PRODUCTS.get(product_id)
if product is None:
raise ValueError(f"Product with ID {product_id} not found.")
return product["stock"]Kedua fungsi memunculkan ValueError ketika diberi ID produk yang tidak ada.
12.2.2) Menangani Error di dalam Tool
Mari kita bungkus fetch_product ke dalam sebuah tool get_product.
from langchain.tools import tool
from product_service import fetch_product
@tool
def get_product(product_id: int) -> str:
"""Mencari produk berdasarkan ID-nya. Mengembalikan nama, harga, dan tingkat stoknya."""
product = fetch_product(product_id)
return f"Product {product_id}: {product['name']} — ${product['price']:.2f}, {product['stock']} in stock."Dengan ID yang valid, ia berfungsi sesuai harapan.
print(get_product.invoke({"product_id": 1}))
# Output: Product 1: Wireless Mouse — $29.99, 120 in stock.Namun, berikan ID yang tidak ada, dan fetch_product memunculkan ValueError yang tidak ditangani apa pun — eksekusi agen berhenti tepat di situ.
print(get_product.invoke({"product_id": 99}))
# ValueError: Product with ID 99 not found.Perbaikannya sederhana: tangkap exception di dalam tool dan kembalikan sebuah string yang bisa dipahami LLM, alih-alih membiarkannya menyebar. Berhasil atau gagal, tool selalu mengembalikan sebuah string, dan LLM menggunakan string tersebut untuk memutuskan apa yang harus dilakukan selanjutnya.
from langchain.tools import tool
from product_service import fetch_product
@tool
def get_product(product_id: int) -> str:
"""Mencari produk berdasarkan ID-nya. Mengembalikan nama, harga, dan tingkat stoknya."""
try:
product = fetch_product(product_id)
return f"Product {product_id}: {product['name']} — ${product['price']:.2f}, {product['stock']} in stock."
except ValueError as e:
return f"Error: {e}"
except Exception as e:
return f"Unexpected error looking up product {product_id}: {e}"print(get_product.invoke({"product_id": 1}))
# Output: Product 1: Wireless Mouse — $29.99, 120 in stock.
print(get_product.invoke({"product_id": 99}))
# Output: Error: Product with ID 99 not found.ID yang tidak ada tidak lagi memunculkan exception — sebagai gantinya, ia mengembalikan pesan error yang bisa dipahami LLM.
Mari kita terapkan pola yang sama pada check_stock:
from product_service import fetch_stock
@tool
def check_stock(product_id: int) -> str:
"""Memeriksa apakah sebuah produk saat ini tersedia dalam stok."""
try:
stock = fetch_stock(product_id)
if stock > 0:
return f"{stock} units available."
return "Out of stock."
except ValueError as e:
return f"Error: {e}"
except Exception as e:
return f"Unexpected error checking stock for product {product_id}: {e}"Tool yang dibangun dengan cara ini bisa diuji langsung dengan .invoke(). Pastikan sebuah tool bekerja dengan benar secara mandiri sebelum menghubungkannya ke LLM. Jika tidak, ketika ada yang salah di dalam agen nanti, Anda tidak akan bisa membedakan apakah tool yang rusak atau model yang sekadar membuat keputusan yang buruk.