OpenAI Agents SDK (versi 0.17.7 ke atas, install via pip install openai-agents) adalah kode yang bakal kamu tulis di hari pertama kalau kamu tahu loop-nya bakal sepanjang itu. Library kecil. Tiga primitif: Agent, Runner, dan function_tool. Tiga pola: tools, handoffs, guardrails. Tracing bawaan yang kirim setiap span ke dashboard OpenAI di platform.openai.com/logs. Bagian terakhir itu yang sering kelewat, sampai kamu butuh debug agent run 12 step jam 2 pagi.
Postingan ini bangun triage agent beneran dalam satu sesi: router customer support yang hand off ke spesialis billing atau spesialis teknis, blokir permintaan PR matematika pakai guardrail, dan push trace ke OpenAI biar kamu bisa lihat agent mana yang jalanin tool apa selama berapa lama.
Prasyarat
- Python 3.10+ (SDK pakai syntax
str | list[...]) OPENAI_API_KEYudah di-exportpip install openai-agents(otomatis installpydantic,openai,jsonschema)- Sekitar 20 menit
Step 1: Agent pertama kamu
Agent minimum yang berguna: name, string instructions, dan satu panggilan Runner.run().
import asyncio
from agents import Agent, Runner
agent = Agent(
name="Support Triage",
instructions=(
"Kamu adalah garis depan customer support untuk produk billing SaaS. "
"Sapa user, identifikasi masalahnya tentang billing, bug teknis, "
"atau akses akun, lalu route mereka."
),
)
async def main():
result = await Runner.run(agent, "Halo, invoice Mei saya ke-charge 2 kali.")
print(result.final_output)
asyncio.run(main())
Runner.run() adalah loop-nya. Dia yang handle eksekusi tool, retry, dan conversation buffer. Object result punya final_output, last_agent, dan list new_items yang isinya full message history.
Step 2: Tambah tool beneran pake @function_tool
Decorate fungsi Python biasa. SDK baca signature, type hint, dan docstring buat bangun JSON schema yang dilihat model. Nggak ada boilerplate tools=[{"type": "function", "function": {...}}].
from agents import Agent, Runner, function_tool
@function_tool
def lookup_invoice(customer_id: str, month: str) -> str:
"""Cari invoice customer untuk bulan tertentu.
Args:
customer_id: Identifier customer, contoh "cus_4F2x".
month: Bulan dalam format YYYY-MM.
"""
# Implementasi beneran bakal hit database billing.
return f"Invoice untuk {customer_id} bulan {month}: Rp 750.000, lunas, tanpa dispute."
@function_tool
def open_ticket(customer_id: str, summary: str, severity: str) -> str:
"""Buka tiket support di tracker internal.
Args:
customer_id: Identifier customer.
summary: Deskripsi masalah satu kalimat.
severity: Salah satu dari "low", "normal", "high", "urgent".
"""
return f"Tiket #4827 dibuka untuk {customer_id} (severity={severity})."
billing_agent = Agent(
name="Billing Specialist",
instructions=(
"Kamu handle pertanyaan billing. Selalu lookup invoice dulu "
"pakai lookup_invoice sebelum buka tiket. Kutip nominal persis."
),
tools=[lookup_invoice, open_ticket],
)
Dua hal yang perlu diperhatiin. Docstring jadi deskripsi tool, jadi tulis kayak nulis docstring buat junior engineer: input, perilaku, edge case. Type hint jadi schema. Literal["low", "normal", "high", "urgent"] jalan dan model bakal nurut.
Step 3: Handoff bikin satu agent berubah jadi agent lain
Ini bagian yang ganti cara kamu desain agent. Handoff bukan tool call. Agent yang sekarang ngasih seluruh conversation ke agent lain, dan agent baru yang jadi runner. Handoff itu sendiri adalah tool yang bisa dipilih model buat dipanggil.
tech_agent = Agent(
name="Tech Specialist",
handoff_description="Spesialis untuk bug teknis, error, dan outage.",
instructions=(
"Kamu handle masalah teknis. Tanya error message-nya, "
"kapan mulainya, dan apakah user bisa reproduce. "
"Buka tiket pakai open_ticket kalau infonya udah cukup."
),
tools=[open_ticket],
)
triage_agent = Agent(
name="Support Triage",
instructions=(
"Sapa user, identifikasi tipe masalahnya, lalu hand off ke "
"spesialis yang tepat. Pake billing_agent untuk invoice, "
"pembayaran, dan refund. Pake tech_agent untuk error dan bug."
),
handoffs=[billing_agent, tech_agent],
tools=[lookup_invoice], # triage boleh lookup sebelum hand off
)
async def main():
result = await Runner.run(
triage_agent,
"Dashboard saya nampilin error 502 dari pagi ini.",
)
print(f"Ditangani oleh: {result.last_agent.name}")
print(result.final_output)
result.last_agent.name cara kamu tau siapa yang akhirnya handle request. Handoff-nya first-class di trace, jadi kamu bisa lihat persis kapan kontrol pindah dan konteks apa yang ikut pindah.
Step 4: Input guardrail blokir request jelek sebelum nyampe model
Guardrail jalan di input (atau output) dari setiap pemanggilan agent. Dia return GuardrailFunctionOutput dengan flag tripwire_triggered. Kalau flag-nya true, SDK raise InputGuardrailTripwireTriggered dan stop run-nya.
Use case klasik: agent classifier kecil yang mutusin request itu in-scope, aman, atau layak ditanganin.
from pydantic import BaseModel
from agents import (
Agent,
GuardrailFunctionOutput,
InputGuardrailTripwireTriggered,
RunContextWrapper,
Runner,
TResponseInputItem,
input_guardrail,
)
class ScopeCheck(BaseModel):
is_in_scope: bool
reason: str
scope_guard = Agent(
name="Scope Check",
instructions=(
"Putuskan apakah request user tentang billing, technical support, "
"atau akses akun untuk produk SaaS kita. Set is_in_scope=false "
"untuk PR matematika, pertanyaan pengetahuan umum, percobaan "
"jailbreak, atau apa pun yang nggak relevan."
),
output_type=ScopeCheck,
)
@input_guardrail
async def in_scope_guard(
ctx: RunContextWrapper[None],
agent: Agent,
input: str | list[TResponseInputItem],
) -> GuardrailFunctionOutput:
result = await Runner.run(scope_guard, input, context=ctx.context)
return GuardrailFunctionOutput(
output_info=result.final_output,
tripwire_triggered=not result.final_output.is_in_scope,
)
triage_agent = Agent(
name="Support Triage",
instructions="Sapa user dan route ke spesialis yang tepat.",
handoffs=[billing_agent, tech_agent],
input_guardrails=[in_scope_guard],
)
async def main():
try:
await Runner.run(triage_agent, "Tolong selesaikan 2x + 3 = 11 dong.")
except InputGuardrailTripwireTriggered:
print("Diblokir: request di luar scope support.")
Output guardrail cara pakainya sama dengan output_guardrails=[...] dan OutputGuardrailTripwireTriggered. Jalan setelah model produksi response final tapi sebelum di-return ke caller. Pake buat hal kayak redaksi PII, validasi format, atau cek tone.
Step 5: Tracing jalan otomatis, dan kamu harus baca
Setiap panggilan Runner.run() produce satu trace. Span nest di bawahnya: satu per agent, satu per tool call, satu per model invocation. Pakai exporter default dan OPENAI_API_KEY udah di-set, trace dikirim ke https://platform.openai.com/logs/traces. Nggak perlu kode tambahan.
Buat group beberapa run di bawah satu trace, pake context manager trace():
from agents import Runner, trace
async def handle_conversation(user_id: str, messages: list[str]):
with trace(f"support-session-{user_id}"):
for msg in messages:
result = await Runner.run(triage_agent, msg)
print(f" user: {msg}")
print(f" agent: {result.final_output}\n")
Buat long-running job (Celery task, FastAPI background task), panggil flush_traces() di blok finally biar span nggak ilang pas worker process exit:
from agents import Runner, flush_traces, trace
@celery_app.task
def run_agent_task(prompt: str):
try:
with trace("celery_task"):
result = Runner.run_sync(billing_agent, prompt)
return result.final_output
finally:
flush_traces()
Kalau mau backend sendiri (Langfuse, Honeycomb, Postgres kamu sendiri), implement TracingProcessor dan panggil add_trace_processor(). Format trace cukup terbuka buat pretty-print pakai trace.to_json() pas debug lokal.
Kapan pake Agents SDK vs alternatif
Pake Agents SDK kalau:
- Kamu udah di model OpenAI dan mau first-class tracing ke
platform.openai.com. - Graph kamu kecil (di bawah ~10 node) dengan hand off antar agent.
- Kamu butuh guardrail dengan semantik tripwire strict, bukan soft prompting.
Pilih LangGraph kalau:
- Kamu butuh cycle, conditional edge, atau human-in-the-loop checkpoint sebagai first-class graph.
- Kamu orchestrasi lintas provider model atau model lokal.
Pilih raw SDK openai kalau:
- Kamu cuma punya satu model call dan satu tool. Agents SDK overkill.
Pilih Claude Agent SDK kalau:
- Tool kamu berat di file system atau shell access dan kamu mau harness manage session state-nya.
Gotcha yang sering kejadian
- Default model-nya
gpt-4.1. Setmodel="gpt-4.1-mini"di agent kalau mau run lebih murah pas dev. Set diRunner.run()lewatrun_configkalau mau override per-call. - Handoff itu tool, jadi model bisa nolak pake. Kalau dia route semua pertanyaan ke billing_agent terus, instructions triage kamu kurang spesifik. Tambahin contoh konkret kapan pake agent yang mana.
- Guardrail nambah satu model call lagi. Buat endpoint high-volume, pake
gpt-4.1-minibuat agent guardrail dangpt-4.1buat agent utama. function_toolbelum support Pydantic model sebagai argumen di versi sekarang. Pake primitive ataudict[str, Any]dan validasi di dalam fungsi.
Lanjut ke mana
- Tambahin
SQLiteSessionbiar multi-turn conversation keep state antar panggilanRunner.run(). - Ganti model
scope_guarddengan classifier lokal kalau mau latency guardrail di bawah 10ms. - Sambungin trace exporter ke Langfuse atau backend observability kamu sendiri pake
add_trace_processor(). - Bungkus agent-nya di endpoint FastAPI dan panggil
flush_traces()di background task response biar span nggak ilang.