Agent kamu jawab salah dan kamu nggak tahu langkah mana yang rusak. Salah prompt, tool yang dipanggil dengan argumen keliru, atau model yang milih fungsi yang nggak tepat? Kalau cuma satu LLM call, debugging artinya memeriksa satu request. Kalau agent, loop-nya jalan beberapa kali, manggil tool di sela-sela, dan failure-nya sering baru kelihatan di akhir. Print statement cuma bantu setengah jalan, sisanya kamu habiskan buat nyambungin timestamp secara manual.
Tutorial ini nyelesaiin masalah itu. Kamu bakal self-host Langfuse, platform observability LLM open source, pakai Docker Compose, lalu instrument agent tool-calling minimal dengan Python SDK biar setiap model call dan eksekusi tool masuk ke satu trace tree lengkap dengan latency, pemakaian token, dan biaya per langkah.
Prerequisites
- Docker dan Docker Compose (Docker Desktop bisa dipakai di macOS dan Windows)
- Python 3.10 atau lebih baru
- OpenAI API key, atau endpoint apa pun yang OpenAI-compatible (Ollama juga bisa)
- Sekitar 15 menit
Langkah 1: Jalankan Langfuse dengan Docker Compose
Langfuse itu platform lengkap, dan untuk development lokal repo-nya sudah nyediain docker-compose.yml yang menjalankan semuanya: web app, background worker, Postgres, ClickHouse, Redis, dan MinIO buat blob storage.
git clone https://github.com/langfuse/langfuse.git
cd langfuse
docker compose up
Tunggu sampai container langfuse-web-1 log "Ready". Saat pertama jalan biasanya 2-3 menit. Lalu buka http://localhost:3000.
Buat akun, buat project, dan buka project settings untuk generate API keys. Kamu dapat dua: public key (pk-lf-...) dan secret key (sk-lf-...). Secret key itu password. Jangan sampai masuk git.
Satu hal sebelum instance ini kebuka dari luar laptop: file compose menandai semua secret dengan # CHANGEME. Rotate kalau instance-nya bakal bisa diakses dari jaringan.
Langkah 2: Bikin Agent Tool-Calling Minimal
Agent itu sebuah loop. Panggil model dengan percakapan dan daftar tools, cek apakah responsnya berisi tool calls, eksekusi, tempel hasilnya, ulangi sampai model jawab tanpa manggil tool.
import json
from openai import OpenAI
client = OpenAI()
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "get_exchange_rate",
"description": "Get the USD exchange rate for a currency",
"parameters": {
"type": "object",
"properties": {
"currency": {"type": "string"}
},
"required": ["currency"],
},
},
},
]
def get_weather(city: str) -> str:
# Replace with a real weather API call in your app
return json.dumps({"city": city, "temperature_c": 31, "condition": "partly cloudy"})
def get_exchange_rate(currency: str) -> str:
# Replace with a real rates API call in your app
return json.dumps({"currency": currency.upper(), "usd": 16250})
TOOL_IMPL = {
"get_weather": get_weather,
"get_exchange_rate": get_exchange_rate,
}
def run_agent(user_input: str) -> str:
messages = [
{"role": "system", "content": "You answer questions using the provided tools."},
{"role": "user", "content": user_input},
]
for _ in range(5): # safety cap against infinite loops
response = client.chat.completions.create(
model="gpt-4o-mini", # any current chat model works
messages=messages,
tools=TOOLS,
)
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
return message.content or ""
for tool_call in message.tool_calls:
result = TOOL_IMPL[tool_call.function.name](
**json.loads(tool_call.function.arguments)
)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
return "Reached max steps without a final answer"
if __name__ == "__main__":
print(run_agent("How warm is Jakarta today, and what is one dollar in rupiah?"))
Jalankan sekali buat mastiin jalan. Lalu rusak satu tool dengan sengaja (lempar exception di dalam get_weather) dan minta agent ngerjain sesuatu yang butuh tool itu. Error yang kamu dapat nggak berguna: traceback-nya keliatan, tapi kamu nggak tahu model-nya lagi nyoba apa atau argumen apa yang dikirim. Itu masalah yang observability selesaikan.
Langkah 3: Instrument dengan Python SDK
Install SDK dan arahkan ke instance lokal kamu:
pip install langfuse
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
export LANGFUSE_BASE_URL=http://localhost:3000
Python SDK v4 dibangun di atas OpenTelemetry. Ada tiga cara bikin observation: decorator @observe() buat fungsi utuh, start_as_current_observation() sebagai context manager buat blok kode, dan start_observation() manual. Ketiganya bisa di-nesting, jadi bebas dicampur.
Ini agent yang sudah di-instrument. Decorator membungkus seluruh run, tiap LLM call jadi observation tipe generation, dan tiap eksekusi tool jadi span:
from langfuse import get_client, observe, propagate_attributes
langfuse = get_client()
@observe(name="agent-run")
def run_agent(user_input: str) -> str:
propagate_attributes(user_id="demo-user", session_id="demo-session")
messages = [
{"role": "system", "content": "You answer questions using the provided tools."},
{"role": "user", "content": user_input},
]
for _ in range(5):
with langfuse.start_as_current_observation(
as_type="generation",
name="llm-call",
model="gpt-4o-mini",
input=messages,
) as generation:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS,
)
generation.update(output=response.choices[0].message.content)
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
return message.content or ""
for tool_call in message.tool_calls:
with langfuse.start_as_current_observation(
as_type="span",
name=f"tool-{tool_call.function.name}",
) as tool_span:
result = TOOL_IMPL[tool_call.function.name](
**json.loads(tool_call.function.arguments)
)
tool_span.update(input=tool_call.function.arguments, output=result)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
return "Reached max steps without a final answer"
if __name__ == "__main__":
print(run_agent("How warm is Jakarta today, and what is one dollar in rupiah?"))
langfuse.flush()
Perubahannya kecil. Decorator otomatis nyatet input, output, durasi, dan exception dari fungsi. Tiap generation nyatet pesan yang dikirim persis dan jawaban model. Tiap tool span nyatet argumen hasil parsing dan hasilnya.
langfuse.flush() penting di script ini. SDK ngumpulin event secara batch dan ngirimnya di background, jadi script yang langsung keluar bisa kehilangan trace terakhir. Service yang jalan terus nggak perlu ini.
Jalankan script, lalu buka halaman Traces. Kamu bakal lihat satu trace per run dengan pohon seperti ini:
agent-run
├── llm-call (generation)
├── tool-get_weather (span)
├── llm-call (generation)
├── tool-get_exchange_rate (span)
└── llm-call (generation)
Klik node mana pun buat lihat prompt, output, pemakaian token, dan biaya. Langfuse sudah punya tabel harga untuk model umum (OpenAI, Anthropic, Google), jadi biaya muncul tanpa konfigurasi tambahan. User id dan session id yang kamu set lewat propagate_attributes jadi filter. Saat trace kamu udah ribuan, itu cara nemuin yang penting.
Langkah 4: Jalur OpenTelemetry untuk Stack Non-Python
Python SDK itu praktis, tapi protokol di belakangnya adalah OpenTelemetry, dan Langfuse menerima OTLP langsung di /api/public/otel. Exporter apa pun, bahasa apa pun, bisa kirim trace:
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:3000/api/public/otel"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(echo -n 'pk-lf-...:sk-lf-...' | base64 -w 0),x-langfuse-ingestion-version=4"
Dua detail yang perlu diketahui. Pertama, auth pakai HTTP Basic dengan public key sebagai username dan secret key sebagai password. Kedua, header x-langfuse-ingestion-version: 4 bikin span yang masuk langsung kelihatan real time; tanpa itu bisa delay sampai 10 menit.
Kalau span kamu ngikutin OpenTelemetry GenAI semantic conventions (gen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens, dan seterusnya), Langfuse menampilkannya sebagai LLM generation dengan tracking token dan biaya, sama kayak jalur SDK. Ini rute buat telemetry vendor-neutral yang bisa juga dikirim ke Jaeger atau Tempo, atau buat agent yang ditulis di Go, Rust, atau TypeScript di mana kamu nggak mau ada Python SDK di jalur kritis.
Langfuse vs Alternatif
| Tool | Kelebihan | Perhatikan |
|---|---|---|
| Langfuse | Open source (core MIT), bisa self-host, OTel-native, prompt management dan evals sudah termasuk | Setup Compose cuma single-node; scaling artinya pindah ke Kubernetes |
| LangSmith | Integrasi LangChain dan LangGraph paling dalam, tooling dataset dan eval kuat | SaaS-first; self-hosting cuma untuk enterprise |
| Arize Phoenix | Local-first, bagus buat debugging di notebook, gratis | Bukan platform lengkap: nggak ada prompt management, fitur production lebih tipis |
| Helicone | Berbasis proxy, tanpa perubahan kode buat pengguna OpenAI SDK | Observability dibatasi apa yang proxy bisa lihat; custom span lebih ribet |
Buat project solo atau tim yang mau datanya di infrastruktur sendiri, Langfuse opsi open source paling lengkap. Kalau kamu hidup di dalam LangChain dan mau integrasi paling rapat, LangSmith pilihan yang nyaman. Kalau kebutuhannya cuma debug trace di notebook sore ini, Phoenix paling cepat.
Masalah yang Sering Muncul
Trace hilang dari script pendek. Lupa flush() bikin bagian akhir batch ilang. Panggil sebelum exit.
Capture payload gede-gedean. Trace dengan input ber-megabyte bikin render lambat dan makan storage. Matikan IO capture di tempat yang nggak membantu: @observe(capture_input=False) atau env var LANGFUSE_OBSERVE_DECORATOR_IO_CAPTURE_ENABLED.
Secret default kebuka. Nilai # CHANGEME di file compose itu pengetahuan umum. Rotate sebelum instance bisa diakses dari selain localhost.
Nggak ada sampling di volume tinggi. Di traffic besar, nyimpen 100% trace jadi mahal. Langfuse mendukung sampling, jadi simpan persentase tertentu dan buang sisanya.
Ringkasan dan Langkah Selanjutnya
Sekarang kamu punya stack observability lokal yang nunjukin setiap LLM call dan eksekusi tool dalam trace tree, lengkap dengan token dan biaya per langkah. "Agent-nya rusak di suatu tempat" berubah jadi "llm-call kedua ngirim argumen yang salah ke get_weather". Lanjut dari sini:
- Kelompokkan percakapan multi-turn dengan
session_idlewatpropagate_attributes - Tambah tombol user feedback dan kirim rating-nya ke Langfuse
- Bangun eval LLM-as-a-judge di atas trace yang sudah logged, pakai dataset dari run beneran
- Arahkan pipeline OTLP yang sama lewat collector biar log, metrics, dan trace lewat satu jalur