How Function Calling Actually Works Under the Hood
Function calling (also called tool use) is a structured contract between your host process and a model.
Busque em todas as páginas da documentação
Function calling (also called tool use) is a structured contract between your host process and a model.
The model never runs your code. It proposes a named operation with JSON arguments. Your host validates those arguments, executes the real function, and feeds the result back as the next observation.
tool_calls, you run them, append tool results, and call the model again until it answers in plain text or you stop the loop.Without tools, a chat model only returns text from its parameters and the current context.
With tools, you also send a list of tool definitions (name, description, JSON Schema for parameters). On each reason step the model chooses one of three outcomes:
The important ownership split:
| Party | Owns |
|---|---|
| Model | Selecting tools and filling argument fields from context |
| Host | Validating args, authz, execution, timeouts, redaction, logging |
| Tool implementation | Side effects and returning a compact, truthful observation |
If you let the model emit free-form "run this shell" text and you execute it, that is not safe function calling. Native tool use keeps the action space closed: only tools you registered can be requested by name.
request = messages + tools + tool_choice + model params
response = assistant message (content and/or tool_calls)
if tool_calls:
for each call:
result = host.execute(call)
messages += tool result bound to call id
request again with updated messages
else:
return assistant content (or stop for other reasons)That cycle is the same whether you use raw OpenAI-compatible APIs, OpenRouter, LangGraph tool nodes, Pydantic AI, CrewAI tools, or MCP-backed tool lists. Frameworks hide the plumbing; they do not remove the host's duty to execute safely.
A minimal successful path looks like this:
system - policy and role.user - goal.assistant with tool_calls - proposed action(s).tool (one per call id) - observation string or JSON.assistant - final natural-language answer (or more tool_calls).Providers differ on field names (tool_calls vs function_call legacy, role: "tool" vs vendor variants).
The logical roles stay stable: proposal, then observation, then next decision.
A tool call typically includes:
Your host should never trust the name blindly. Map name → implementation through an allowlist. Reject unknown names with a tool-result error the model can read.
Schema validity is the first gate, not the last.
import json
from typing import Any, Callable
REGISTRY: dict[str, Callable[..., Any]] = {}
def handle_tool_call(name: str, arguments: str | dict) -> dict:
if name not in REGISTRY:
return {"ok": False, "error": f"unknown_tool:{name}"}
args = json.loads(arguments) if isinstance(arguments, str) else arguments
# schema validate args here (jsonschema / pydantic)
try:
value = REGISTRY[name](**args)
return {"ok": True, "data": value}
except Exception as exc: # narrow in production
return {"ok": False, "error": type(exc).__name__, "message": str(exc)[:300]}Business rules (positive ids, allowed tenants, idempotency keys) belong in this gate. Crashing the process on bad args aborts the agent loop; returning a structured error usually lets the model recover.
The model only "sees" what you put back.
Prefer short, structured payloads:
{"ok": True, "order_id": 1042, "status": "shipped", "eta": "2026-07-18"}Avoid dumping full HTTP transcripts, HTML pages, or secrets. Those burn tokens, hide the goal, and can inject untrusted instructions into the next reason step.
APIs expose controls such as:
Forced tool use is helpful for extraction steps. It is harmful if you force a write tool when the user only asked a question.
Always read the provider finish reason (tool_calls, stop, length, and so on).
A length finish mid-arguments is a broken call; do not execute partial JSON.
Modern models may return several tool_calls in one assistant message. That is batching independent reads, not true multi-threaded reasoning inside the model.
Dependent steps still need sequential turns: call A, observe A, then call B with A's id. See Parallel Tool Calls.
MCP standardizes how hosts discover and invoke remote tools. Frameworks (LangGraph, CrewAI, OpenAI Agents SDK, Pydantic AI, Vercel AI SDK) own graph/state helpers and often auto-loop tool calls.
Under every abstraction you still have: schema in, structured call out, host execute, observation back.
Tool schemas sit in context on every reason call (unless the provider caches a stable prefix). Large libraries raise cost and worsen selection. Patterns like deferred tool loading / tool search address that; see Tool Search.
Streaming APIs may deliver argument fragments token-by-token. Buffer until the call is complete and JSON-parseable before executing. Never run side effects on partial arguments.
Log call id, name, args (redacted), result hash, latency, and model identity. Replay with stubbed tools is the foundation of agent tests. Without call ids, parallel results are hard to re-associate.
Treat tool outputs as untrusted text in the next prompt. They can contain prompt-injection payloads from web pages, tickets, or emails. Keep hard deny rules in host code, not only in the system prompt.
| Design | Strength | Weakness | Best fit |
|---|---|---|---|
| Native function calling + host loop | Clear contract, portable mental model | You implement validation and stops | Most production agents |
| Free-form "Action:" text parsing | Works on older models | Brittle parsing, easier injection | Legacy demos only |
| Forced single tool | High reliability for extraction | No multi-tool flexibility | Form-fill / structured ETL |
| Huge always-on tool list | Maximum capability surface | Cost, confusion, risky writes | Avoid; prefer phased tools |
ok / error fields.Mostly branding. Both mean the model emits structured calls against declared tools instead of only free text.
No. It sees the name, description, and parameter schema you send (plus any docs you put in prompts).
It may believe parametric knowledge is enough, the description was unclear, tools were not passed, or tool_choice forbade tools. Improve descriptions, require tools when ground truth is mandatory, or instruct "use tools for live data."
Only when the host can safely parallelize independent side-effect-free calls. Preserve result-to-call-id mapping. See the parallel-calls page in this section.
Use the provider's tool call id on the tool message. Never rely on order alone when multiple calls exist.
Do not execute. Return a tool error describing the parse failure, or repair once under a strict policy, then re-ask the model.
Prefer references (urls, file ids) or short base64 with hard size caps. Huge blobs destroy the context budget.
Structured outputs constrain the final answer shape. Function calling selects and parameterizes actions mid-loop. Many products use both.
No. Capability, parallel-call quality, and schema strictness vary. Verify at build for each model slug you route to.
System policy still governs when tools may be used, what to refuse, and how to present answers. Tools expand actions; they do not replace policy.
Default to structured tool errors so the model can adapt. Crash only on host-fatal conditions (auth misconfig, out-of-memory). See Handling Tool Errors.
No. MCP is a transport/discovery standard for tools. Local in-process tools work with plain function calling.
Task-dependent. Simple lookups may need one tool turn. Multi-step workflows need several. Always enforce max turns and timeouts in the host.
Most production bugs are wrong args, wrong tool choice, or bad observations. Final prose alone is not enough to debug.
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026