π Tool/Function Calling di Agent: Desain Schema & Error Recovery
Tanggal sesi: 2026-11-04. Verifikasi via dokumentasi resmi OpenAI, Anthropic, dan spesifikasi MCP (sumber di bawah). Angka yang tidak bisa diverifikasi saya tandai eksplisit.
π― Tujuan Belajar
Setelah sesi ini kamu bisa:
1. Menulis tool schema yang lolos strict mode OpenAI + tidak bikin model salah pilih parameter
2. Membedakan 4 kelas error tool dan memilih strategi recovery yang benar per kelas (retry di kode vs kembali ke model vs stop)
3. Memahami arah industri 2025β2026: tool search / defer loading (skala token), stateless tool protocol (MCP 2026-07-28), dan cara benchmark tool calling (BFCL V4)
ποΈ Arsitektur & Alur Teknis
Tool calling = loop lima langkah:
[app] --tools + messages--> [LLM]
[LLM] --tool_calls[]-------> [app]
[app] --validate args-------> eksekusi (dengan validation + error taxonomy)
[app] --tool results--------> [LLM] (semua hasil dipulangkan, error pun)
[LLM] --final answer / more tool_calls--> ulang sampai selesai / budget habis
Tiga lapis pertahanan yang harus ada di layer eksekusi (dari luar ke dalam):
- Schema layer β schema ketat supaya model sulit generate argumen salah (strict mode,
pattern,enum,minimum) - Validation layer β pydantic/Zod tetap validasi hasil model sebelum eksekusi. Strict mode mengurangi kesalahan, bukan menjamin (laporan komunitas JanβMei 2025: strict mode kadang tidak dipaksa pada UI dashboard; anggap "best effort + constraint decoding", bukan jaminan absolut)
- Error taxonomy layer β klasifikasi kegagalan menentukan siapa yang memperbaiki: kode (retry sama-input), model (input berbeda), atau manusia (stop)
π» Kode Nyata
Stack: Python 3.11, pydantic 2.13.4, stdlib. File /tmp/toolcall/toolkit.py sudah saya jalankan, semua assertion lulus. Tiga komponen:
1. Compiler pydantic β OpenAI strict-mode schema
OpenAI Structured Outputs/strict mode mensyaratkan (dok resmi): semua field required, setiap object additionalProperties: false, subset JSON Schema, $defs/$ref tidak boleh tersisa. Pydantic default menghasilkan optional fields dan $defs β jadi perlu compiler:
def to_openai_strict(model: type[BaseModel], name: str, desc: str) -> dict:
schema = model.model_json_schema()
defs = schema.pop("$defs", {})
def resolve(node):
if isinstance(node, list):
return [resolve(n) for n in node]
if not isinstance(node, dict):
return node
if "$ref" in node: # inline $ref; strict menolak $defs
ref = node["$ref"].rsplit("/", 1)[-1]
return resolve(defs[ref])
out = {k: resolve(v) for k, v in node.items() if k not in ("title", "default")}
if out.get("type") == "object" or "properties" in out:
out["required"] = list(out.get("properties", {})) # SEMUA field required
out["additionalProperties"] = False
return out
return {"type": "function", "function": {
"name": name, "description": desc,
"parameters": resolve(schema), "strict": True}}
Output aktual dari kode demo (model GetOrder):
{
"properties": {
"order_id": {"description": "Order id like ORD-123456",
"pattern": "^ORD-\\d{6}$", "type": "string"},
"include_items":{"description": "Include line items", "type": "boolean"}
},
"required": ["order_id", "include_items"],
"type": "object",
"additionalProperties": false
}
π‘ Catatan: required berisi semua properti bukan berarti semua wajib diisi secara semantik β teknik standar: jadikan field opsional sebagai type: ["string", "null"] (nullable), model diberi tahu via description kapan boleh null.
2. Error taxonomy β 4 kelas, 4 strategi
class ErrKind(str, Enum):
INVALID_ARGS = "invalid_args" # salah model -> balikin ke model, bisa dia perbaiki
NOT_FOUND = "not_found" # salah model, retry input sama percuma
TRANSIENT = "transient" # salah infra -> retry DI KODE, model tak perlu tahu
PERMANENT = "permanent" # unrecoverable -> stop, jangan biarkan model retry
def render(e: ToolError) -> str:
"""Teks error DIBACA MODEL. 1-2 kalimat, actionable, tanpa stack trace."""
hint = {
ErrKind.INVALID_ARGS: f"Fix the arguments and call again. Problem: {e.msg}",
ErrKind.NOT_FOUND: f"Do not retry with the same input. {e.msg} Choose a different id or ask the user.",
ErrKind.TRANSIENT: "Temporary. Retry once; already retried in code.",
ErrKind.PERMANENT: "Do not retry this tool. Report the failure to the user.",
}[e.kind]
return f"[{e.kind.value}] {hint}"
3. Executor dengan validation gate + retry budget
def call_tool(call_id, tool, raw_args: str, model, budget: RetryBudget,
code_retries: int = 2) -> ToolResult:
try:
args = model.model_validate_json(raw_args) # gate 1: shape validation
except ValidationError as ve:
budget.record(False)
return ToolResult(call_id, render(ToolError(ErrKind.INVALID_ARGS,
ve.errors()[0]["msg"])), is_error=True)
for attempt in range(code_retries + 1): # gate 2: retry same-input DI KODE
try:
out = tool(**args.model_dump())
budget.record(True)
return ToolResult(call_id, json.dumps(out, default=str)[:4000])
except ToolError as te:
if te.kind is ErrKind.TRANSIENT and attempt < code_retries:
time.sleep(0.2 * 2 ** attempt); continue # exponential backoff
budget.record(False)
return ToolResult(call_id, render(te), is_error=True)
except Exception as exc: # gate 3: jangan bocorkan stack trace
budget.record(False)
return ToolResult(call_id, render(ToolError(ErrKind.PERMANENT,
type(exc).__name__)), is_error=True)
RetryBudget menahan loop gila: max_model_calls=8, dan circuit breaker β 3 error konsekutif β sisipkan pesan "This tool has failed repeatedly. Do not retry it." lalu hentikan loop dan eskalasi ke user. Hasil eksekusi nyata:
[invalid_args] Fix the arguments and call again. Problem: String should match pattern '^ORD-\d{6}$'
[not_found] Do not retry with the same input. No order ORD-000000. ...
{"order_id": "ORD-999999", "items": null} is_error=False <- retry kode menyerap 503
{"order_id": "ORD-123456", "items": null}
Aturan kunci retry (DIINFERENSI dari konsensus praktik produksi, bukan angka hard): kalau retry dengan input sama bisa berhasil β tangani di kode (backoff). Kalau butuh input berbeda β pulangkan ke model dengan pesan yang menjelaskan apa yang salah. Dan: kalau satu turn punya multiple tool calls, pulangkan semua hasil (sukses + error) masing-masing dengan tool_use_id β hasil yang hilang merusak turn berikutnya. Di Anthropic API pakai is_error: true di tool_result; MCP pakai isError: true pada tool execution result; OpenAI tidak punya flag β pesan role: "tool" yang berisi error text saja.
π Angka & Benchmark
| Data point | Angka | Sumber |
|---|---|---|
| BFCL V4 overall acc, peringkat 1: Claude-Opus-4-5-20251101 (FC) | 77.47 (leaderboard update 2026-04-12) | gorilla.cs.berkeley.edu |
| BFCL V4: GLM-4.6 (FC thinking), model open-terbaik, rank 4 | 72.38, biaya $4.64 | sama |
| Tool Search Tool: penghematan token tool definitions | ~85% (klaim Anthropic, Nov 2025); cross-platform benchmark Spring AI: 34β64% | anthropic.com/news/advanced-tool-use |
| Contoh kasus Anthropic: 58 tools β 55K token sebelum optimasi; dengan tool search β 8.7K | ~95% context tersisa | sama |
| Akurasi MCP eval Opus 4: tanpaβdengan tool search | 49% β 74%; Opus 4.5: 79.5% β 88.1% | sama (internal testing, bukan benchmark netral) |
| MCP adoption | ~500 juta download/bulan lintas Tier-1 SDK; TS & Python >1 miliar total | blog.modelcontextprotocol.io, 2026-07-28 |
| Contoh tool definition heboh: satu server Jira β 17K token; kasus ekstrem Anthropic 134K token sebelum optimasi | β | sama |
β οΈ Angka 85%/49β74% berasal dari vendor sendiri (Anthropic internal testing). Benchmark independen paling dekat untuk akurasi tool calling: BFCL (Berkeley, paper di ICML 2025). Perlakukan angka vendor sebagai DIKETAHUI-atas-klaim, bukan fakta netral.
βοΈ Trade-off & Alternatif
| Keputusan | Pro | Kontra |
|---|---|---|
| Strict mode ON | Argumen hampir selalu valid; hemat round-trip error | Subset JSON Schema saja; semua field jadi required (perlu trik nullable); schema diproses+di-cache di request pertama (latency jika schema berubah-ubah); tidak eligible untuk zero data retention |
| Strict mode OFF | Full JSON Schema, field opsional natural | Validation gagal lebih sering β wajib pydantic/Zod gate + error recovery yang bagus |
| Semua tool preload | Zero friction, model lihat semua | 10Kβ134K token overhead; akurasi turun di library besar (tool mirip nama) |
Tool search / defer loading (Anthropic defer_loading: true, OpenAI tool_search) |
Context hemat 34β85%; akurasi naik di library besar; tidak merusak prompt caching | +1 search step latency; kurang berguna <10 tools atau semua tool sering dipakai |
| Programmatic tool calling / code mode (Anthropic code_execution, Cloudflare Code Mode) | Orkestrasi (loop, filter, gabung 5 tools) dalam 1 inference pass; data besar tidak masuk context | Perlu sandbox eksekusi; model menulis kode orkestrasi bisa salah; debugging lebih susah |
| MCP sebagai lapisan tool transport | Standar lintas-vendor, registry, OAuth | Lapisan ekstra; spec bergerak cepat (4 revisi dalam 20 bulan) |
| Retry di kode vs biarkan model retry | Kode: murah, cepat, deterministic | Kode salah retensi kalau error sebenarnya butuh input baru (waste retries); model retry mahal & bisa loop |
π Use Case Produksi & Failure Mode Umum
Use case nyata: Claude for Excel pakai Programmatic Tool Calling untuk baca/modifikasi spreadsheet ribuan baris tanpa membanjiri context (Anthropic, Nov 2025). Supabase MCP pakai MRTR (Multi Round-Trip Requests) untuk konfirmasi elicitation ("delete data? / biaya proyek?") di server stateless.
Failure mode umum + mitigasi:
- Loop tak berhenti β model retry tool yang gagal terus. Mitigasi: retry budget + circuit breaker di kode (lihat executor).
- Tool mirip nama β
notification-send-uservsnotification-send-channelsalah pilih. Mitigasi: prefix namespacing (github.createPullRequest), description eksplisit, defer loading biar hanya tool relevan di context. - Stack trace / secret bocor ke model atau log β error mentah dari upstream diinterpolasi begitu saja. Mitigasi: error message dari template sendiri, whitelist tipe exception, tanpa body response mentah.
- Hasil tool yang valid-tapi-kosong diberi flag error β
not foundvsempty listbeda makna;is_errorhanya untuk kegagalan aktual. Kalau semua balikan di-flag error, model kehilangan sinyal. - Side-effect tool dieksekusi dobel β retry setelah timeout pada tool non-idempotent (payment, kirim email). Mitigasi: idempotency key sebagai parameter schema, atau tandai tool itu
allowed_callers/sekali-jalan. DIINFERENSI: ini alasan Anthropic menyarankan PTC hanya untuk tool idempotent/paralel. - Schema drift β schema berubah tiap request merusak cache schema strict mode dan prompt cache. Mitigasi: versioning tool name, schema statis.
Perubahan ekosistem yang perlu kamu tahu (DIKETAHUI, terverifikasi):
- MCP 2026-07-28 (final 28 Juli 2026, menggantikan 2025-11-25): protokol jadi stateless β initialize handshake dan Mcp-Session-Id DIHAPUS. Info versi/klien/kapabilitas kini di _meta tiap request; header Mcp-Method + Mcp-Name wajib untuk routing di load balancer; hasil list (tools/list dll.) dapat ttlMs + cacheScope untuk caching; serverβclient mid-call input via InputRequiredResult + requestState (MRTR). Breaking changes; HTTP+SSE transport dan Roots/Sampling/Logging deprecated (deprecation window β₯12 bulan). Konsekuensi praktis: server MCP tak lagi butuh sticky session/shared session store β jalan di balik round-robin LB biasa.
- Tool schema di MCP: input tetap type: object root, tapi kini full JSON Schema 2020-12 (oneOf/$ref boleh). Error input validation sebaiknya dikembalikan sebagai Tool Execution Error (bukan protocol error) supaya model bisa self-correct β keputusan desain yang sama persis dengan taxonomy di atas.
- OpenAI: tool_search hanya untuk model gpt-5.4+; GPT-6 Astra/6.1 Sol mewajibkan Responses API untuk tool calling.
π οΈ Latihan Praktis Kecil
File lengkap sudah di /tmp/toolcall/toolkit.py (sudah lulus semua assertion di laptop saya). Coba di laptop:
python3 -c "import pydantic" || pip install pydantic
# salin toolkit.py, lalu modifikasi:
# 1. Tambah field enum: status: Literal['pending','shipped'] β cek output schema-nya
# 2. Buat tool yang raise PERMANENT β verifikasi pesan "Do not retry" sampai ke model
# 3. Naikkan RetryBudget.max_consecutive_errors ke 5, trip 4 error β verifikasi tidak exhausted
# 4. (Bonus) jalankan loop LLM sungguhan: kirim schema ke API, sengaja kirim argumen salah
# dari sisi model, pulangkan error, hitung berapa kali model berhasil self-correct
β Cek Pemahaman
- Tool database-mu gagal dengan connection timeout 2x berturut-turut. Ini kelas error apa, dan siapa yang harus me-retry β kode atau model? Kenapa beda perlakuannya dengan error "no order ORD-000000"?
- Kenapa
requireddi schema strict mode OpenAI harus berisi SEMUA properti, dan bagaimana cara tetap memberi model kemampuan mengosongkan field opsional? - Server MCP-mu saat ini bergantung pada
Mcp-Session-Iduntuk state antar tool call. Menurut spesifikasi2026-07-28, apa pengganti yang direkomendasikan dan apa keuntungan operasionalnya di balik load balancer?
π Poin Kunci
- Tool calling bukan fitur API β ini loop dengan validation gate, error taxonomy, dan retry budget. Tanpa ketiganya, agent produksi rapuh.
- Error punya 4 kelas; keputusan "kode retry atau model retry" adalah keputusan arsitektur paling penting di loop ini. Pesan error ditulis untuk model: 1β2 kalimat, actionable, tanpa stack trace.
- Strict mode mengurangi argumen salah, bukan menghapusnya β pydantic/Zod tetap wajib.
- Skala token tool definitions adalah masalah nyata (55Kβ134K token terdokumentasi); tool search/defer loading hemat 34β85% dan justru menaikkan akurasi di library besar.
- MCP
2026-07-28membuat tool layer stateless: handle eksplisit lewat argumen tool menggantikan session. PelajariMcp-Method/ttlMs/InputRequiredResultkalau kamu build MCP server tahun ini.
π Sumber
- OpenAI β Function calling & Structured Outputs docs (akses 2026-11-04): developers.openai.com/api/docs/guides/function-calling, /guides/structured-outputs
- Anthropic β "Introducing advanced tool use" (Nov 2025): anthropic.com/news/advanced-tool-use
- Anthropic β Handle tool calls (
is_error): platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls - MCP Specification
2025-11-25changelog: modelcontextprotocol.io/specification/2025-11-25/changelog - MCP Blog β
2026-07-28spec final (28 Jul 2026) + RC announcement: blog.modelcontextprotocol.io/posts/2026-07-28/, /posts/2026-07-28-release-candidate/ - Stacktree β MCP 2026-07-28 breaking changes breakdown (updated 30 Sep 2026): stacktr.ee/blog/mcp-2026-spec-changes
- Berkeley Function Calling Leaderboard V4 (update 2026-04-12): gorilla.cs.berkeley.edu/leaderboard.html
β‘οΈ Eksplorasi Lanjutan
- Prompt caching interplay: kenapa defer_loading tidak merusak cache β dan kapan schema dinamis merusaknya
- Programmatic tool calling / code mode: sandbox design (Anthropic code_execution, Cloudflare Code Mode)
- Evaluasi tool calling berkelanjutan: bikin regression suite mini dari failure log produksi, bandingkan model via BFCL-style AST matching
- Topik berikutnya di kurikulum: vector DB (RAG pipeline)