Cloud LLM API memang praktis, tapi berbayar, nge-log prompt kamu, dan butuh koneksi internet. Buat beberapa kasus — internal tool, kerja offline, atau apa pun yang nyentuh data sensitif — jalanin model di lokal lebih masuk akal. Masalahnya: bikin agent yang beneran manggil function dengan bener di model lokal itu lebih rewel dari yang keliatan di dokumentasi.
Artikel ini nunjukin setup yang jalan: Ollama dengan model yang support tool, script Python kecil, dan satu tool nyata (baca file lokal). Semua muat di bawah 100 baris.
Prerequisites
- macOS, Linux, atau Windows dengan WSL2
- Minimal RAM 16GB (disarankan 32GB buat model 13B+)
- Python 3.10+
- Sekitar 10GB disk buat bobot model
Step 1: Install Ollama
# macOS / Linux
curl -fsSL https://ollama.com/install.sh | sh
# Cek
ollama --version
Itu aja. Ollama jalan sebagai service lokal di http://127.0.0.1:11434 dan nge-ekspos API yang kompatibel sama OpenAI. Tanpa akun, tanpa login.
Step 2: Pilih Model yang Support Tool
Nggak semua model bagus buat function calling. Yang bagus, per awal 2026:
| Model | Ukuran | Tool Support | Kualitas |
|---|---|---|---|
llama3.1:8b |
4.7GB | Bagus | General purpose yang kuat |
qwen2.5:14b |
9.0GB | Sangat bagus | Lebih jago di structured output |
mistral-nemo:12b |
7.1GB | Bagus | Reasoning kuat |
llama3.3:70b |
43GB | Excellent | Butuh RAM 64GB+ |
Pull salah satu:
ollama pull llama3.1:8b
Model 8B itu sweet spot buat kebanyakan laptop. Model 70B jauh lebih pinter tapi butuh hardware serius.
Step 3: Cek Modelnya Respon
ollama run llama3.1:8b "What is 17 * 23?"
Kalau kamu dapet 391, modelnya jalan.
Step 4: Loop Python-nya
Tool-calling agent itu loop. Model liat prompt sama daftar tool yang tersedia. Kalau dia mutusin manggil tool, loop jalanin tool-nya, kasih hasilnya balik, dan tanya model lagi. Ini berulang sampai model kasih jawaban akhir (atau mentok di turn limit).
import json
import requests
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
MODEL = "llama3.1:8b"
# Tool yang bisa dipanggil agent. Di project beneran, taruh tiap tool di
# function sendiri dan dispatch by name. Satu tool cukup buat nunjukin polanya.
TOOLS = [
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read the contents of a text file at the given path. Use this when the user asks about a file.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute or relative path to the file",
}
},
"required": ["path"],
},
},
}
]
def read_file(path: str) -> str:
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
return f"Error: file not found at {path}"
except Exception as e:
return f"Error: {e}"
TOOL_FUNCTIONS = {"read_file": read_file}
def chat(messages: list, max_turns: int = 5) -> str:
for turn in range(max_turns):
response = requests.post(
OLLAMA_URL,
json={
"model": MODEL,
"messages": messages,
"tools": TOOLS,
"stream": False,
},
)
response.raise_for_status()
data = response.json()
msg = data["message"]
# Kalau model nggak manggil tool, dia kasih jawaban akhir.
if not msg.get("tool_calls"):
return msg["content"]
# Kalau iya, jalanin tiap tool call dan tambahin hasilnya.
messages.append(msg)
for call in msg["tool_calls"]:
name = call["function"]["name"]
args = call["function"]["arguments"]
result = TOOL_FUNCTIONS[name](**args)
messages.append(
{
"role": "tool",
"content": str(result),
}
)
return "Agent stopped after max turns without a final answer."
if __name__ == "__main__":
messages = [
{
"role": "system",
"content": "You are a helpful assistant. Use the read_file tool when the user asks about a file.",
},
{
"role": "user",
"content": "Read ./README.md and tell me what this project does in one sentence.",
},
]
print(chat(messages))
Jalanin dengan README apapun di working directory:
python agent.py
Model nerima request user, mutusin manggil read_file, dapet isi file-nya, dan jawab pakai bahasa biasa. Tanpa API key, tanpa biaya per token.
Step 5: Tambah Tool Kedua
Loop-nya udah nge-handle banyak tool call per turn. Tambah tool lain dengan cara yang sama:
def list_directory(path: str) -> str:
import os
try:
entries = os.listdir(path)
return json.dumps(entries)
except Exception as e:
return f"Error: {e}"
TOOL_FUNCTIONS["list_directory"] = list_directory
TOOLS.append({
"type": "function",
"function": {
"name": "list_directory",
"description": "List files and folders at the given path. Use this when the user wants to know what is in a directory.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Directory path"}
},
"required": ["path"],
},
},
})
Model sekarang bisa ngegabungin tool: list_directory dulu, terus read_file di sesuatu yang ditemuin. Ini bentuk dasar dari semua framework agent — LangChain, LlamaIndex, AutoGen. Mereka semua nge-wrap loop yang sama dengan prompt, retry, dan tracing yang lebih bagus.
Yang Sering Salah dan Cara Benerinnya
Model ngehalusinasi tool call. Model yang lebih kecil kadang ngarang tool yang nggak ada di daftar. Kencengin system prompt: "Cuma pake tool yang ada di daftar. Kalau nggak ada yang cocok, bilang aja."
Argumen tool tipenya salah. Yang sering: model ngasih {"path": null} atau lupa field yang required. Validasi di dalam function tool dan balikin string error yang jelas. Model baca error dan retry.
Latency. Di MacBook Air 2020, llama3.1:8b makan 1-3 detik per token. Percakapan 5 turn bisa kerasa lambat. Turun ke llama3.2:3b (2GB) buat prototyping — jauh lebih cepet, reasoning lebih lemah.
Model nge-ignore tool. Beberapa prompt jalan lebih baik dari yang lain. "Read the README and summarize" jalan. "Tell me about the project" nggak — modelnya nggak ngerti yang kamu mau. Jelasin kapan harus pake tiap tool.
Kapan Ini Udah Cukup
Buat satu user, satu mesin, beberapa tool: loop ini adalah seluruh agent-nya. Tanpa framework. Nambah error handling, retry, dan logging itu straightforward. Kamu bisa ship.
Kalau kamu butuh banyak agent, shared state, async, observability, atau reliability production, pindah ke LangChain, LangGraph, atau Claude Agent SDK. Mereka bukan sulap — mereka nge-wrap loop yang sama. Ngerti cara kerja loop bikin kode framework-nya masuk akal.
Lanjut ke Mana
- Tambahin structured output dengan parameter
formatdi Ollama (constrained generation pakai JSON schema) - Coba
qwen2.5:14bbuat akurasi tool calling yang lebih bagus - Wrap tool di MCP server biar tool yang sama bisa dipake dari Claude Desktop, Cursor, atau host MCP apapun
- Tambah vector store (Chroma atau Qdrant) buat RAG — baca file, chunk, embed, retrieve