Chapter 12
12 / 50

Function Calling & Tool Use

⏱️ 35 min

Function calling does not let a model run a function directly. The model emits a structured request; the application validates arguments, checks policy, executes the tool, and returns the result. That boundary determines whether the system is controllable.


The complete data path

user request
  → model decides whether a tool is needed
  → tool name + arguments + call ID
  → application validates schema, policy, and business rules
  → tool executes inside a boundary
  → application returns output for that call ID
  → model answers or requests another tool
ResponsibilityModelApplication code
Interpret natural-language intentYesSupplies business context
Select a candidate toolYesControls the tools exposed for this run
Generate argumentsYesEnforces schema and business validation
Execute API, SQL, or refundNoYes
Decide permissions and approvalMay proposeMust decide deterministically
Verify the final side effectMay explainMust read real system state

Treat model output as an untrusted execution proposal, not an authorised command.


When tool calling is useful

RequirementTool callText response
Current orders, weather, or inventoryAppropriateModel knowledge may be stale
Create ticket, update CRM, or refundAppropriate with policy and idempotencyText cannot create the real action
Return a machine-consumed business objectStructured output or toolFree text is harder to parse reliably
Explain a stable conceptUsually unnecessaryDirect response is faster
Required user information is missingAsk firstDo not invent arguments

For a task with two or three deterministic steps, ordinary application code may be enough. Do not add an autonomous loop only to make the system look agentic.


A schema should express business boundaries

{
	"type": "function",
	"name": "create_support_ticket",
	"description": "Create a ticket after the customer and issue are identified. Do not use for status lookup.",
	"parameters": {
		"type": "object",
		"properties": {
			"customer_id": {
				"type": "string",
				"description": "Verified internal customer ID"
			},
			"category": {
				"type": "string",
				"enum": ["billing", "account", "technical"]
			},
			"summary": {
				"type": "string",
				"minLength": 10,
				"maxLength": 500
			},
			"evidence_ids": {
				"type": "array",
				"items": { "type": "string" },
				"minItems": 1
			}
		},
		"required": ["customer_id", "category", "summary", "evidence_ids"],
		"additionalProperties": false
	}
}

A useful description says when to use the tool and when not to use it. Put enums, length, and format constraints in the schema. Keep permissions, balances, and state-machine rules in server-side validation.


A controlled executor

async function executeToolCall(call: ToolCall, actor: Actor) {
	const tool = registry.get(call.name);
	if (!tool) return toolError(call.id, 'TOOL_NOT_AVAILABLE');

	const parsed = tool.schema.safeParse(call.arguments);
	if (!parsed.success) {
		return toolError(call.id, 'INVALID_ARGUMENTS', parsed.error.issues);
	}

	const policy = await authorize(actor, call.name, parsed.data);
	if (!policy.allowed) return toolError(call.id, 'FORBIDDEN');

	if (policy.requiresApproval) {
		return toolPendingApproval(call.id, await createApproval(call, actor));
	}

	try {
		const result = await withTimeout(
			tool.execute(parsed.data, {
				idempotencyKey: `${actor.taskId}:${call.id}`
			}),
			tool.timeoutMs
		);
		return toolSuccess(call.id, sanitizeForModel(result));
	} catch (error) {
		return mapToolError(call.id, error);
	}
}

Return concise field errors to the model for one repair attempt. Do not place stack traces, secrets, or internal network details in model-visible context.


Errors should drive a safe next step

ErrorRetry?Model-visible resultSystem action
INVALID_ARGUMENTSOnce after repairInvalid fieldsDo not execute
NOT_FOUNDUsually noResource does not existAsk or change query
FORBIDDENNo automatic retryInsufficient accessStop or request approval
RATE_LIMITEDLimited backoffSuggested waitExponential backoff plus jitter
DEPENDENCY_DOWNLimited retryService unavailableCircuit breaker
ALREADY_APPLIEDDo not repeatExisting evidence IDRead back state and accept idempotent success

“Tool failed” is not diagnostic. Use stable error codes, brief messages, and internal-only raw exceptions.


Separate reads from writes

Do not create one manage_customer tool for lookup, update, and deletion. Split it into:

  • get_customer_profile
  • update_customer_contact
  • request_customer_deletion

This allows dynamic tool exposure by user state. Before identity verification, expose only reads. Load updates after verification. Always require approval for deletion.

Mutation tools also need an idempotency key, dry-run diff, provider request ID, rollback semantics, and a final state read-back.


Parallel calls are not always faster

Inventory queries across three independent warehouses can run in parallel. Looking up an order and then using its payment ID must be sequential. Before parallelising, confirm:

  • Calls have no data dependency.
  • The tool and downstream service support the concurrency.
  • Result order does not change business meaning.
  • Partial failure has a defined merge policy.

Do not ask the model to generate dozens of parallel mutations. Let code control batch size, rate, idempotency, and recovery.


Security and data boundaries

BoundaryMinimum control
Tool exposureNarrow by task and actor permissions
ArgumentsSchema validation followed by business validation
CredentialsShort-lived credentials inside the tool, never model context
FilesValidate MIME, size, source, and isolated workspace
NetworkAllow required endpoints; restrict arbitrary egress
LogsRedact arguments; retain call ID and business evidence
High-risk actionApproval, idempotency, and provider read-back

Web pages and documents returned by tools are data, not higher-priority instructions. An injected sentence cannot grant itself refund permission.


Evaluate more than schema compliance

Your golden set should cover correct tool use, direct answers without tools, missing information, similar tool names, business-rule rejection, timeouts, rate limits, prompt injection, and replayed mutations.

MetricMeaning
Tool selection accuracyCorrect tool when needed and restraint when not needed
Argument validityCalls passing schema on the first attempt
Business-rule rejectionValid JSON rejected by domain rules
Side-effect precisionNecessary and unique mutations among all writes
Recovery successCorrect final state after timeout or retry

Practice task

Implement order lookup, ticket creation, and refund request tools:

  1. State inclusion and exclusion conditions for each tool.
  2. Add amount, reason, approval ID, and evidence IDs to the refund schema.
  3. Simulate missing fields, forbidden access, 429, and provider timeout.
  4. Replay one refund call and confirm only one side effect occurs.
  5. Record call ID, redacted arguments, latency, error code, and read-back evidence.

Definition of done

  • The model proposes calls; the application retains execution authority.
  • Schema and business rules are validated separately.
  • Reads and writes are separated; risky writes require approval.
  • Every tool has a timeout, stable error codes, and retry limit.
  • Mutations are idempotent and verified against real state.

Official references

📚 Related resources

Common questions

Open a question to review the practical answer.

When should I use function calling instead of letting the model generate free text?

Consider function calling when an answer depends on current data, an external system, an executable action, or machine-readable output. If the task only requires explaining known context, a direct answer is usually simpler. If required information is missing, ask for it instead of guessing arguments. The boundary is: the model proposes a call; the application validates, authorizes, executes, and records it.

How should I design a tool schema?

Give each tool a single, explicit action. Its description should say when to use it and when not to. Define parameter types, required fields, enums, and format constraints. Do not rely on JSON Schema for permissions, pricing, inventory, or other business rules: validate and authorize those server-side. Keep sensitive defaults under application control so the model cannot silently invent high-risk arguments.

What does a healthy tool execution loop look like?

Send the available tools and conversation context to the model. When it returns a call, validate the arguments, permission, and call ID before execution, then return a structured result. Cap total steps, time, and cost. Match isolation to risk: ordinary APIs need authorization and rate limits; code, shell commands, and untrusted file processing need an isolated execution environment.

How do I handle tool errors — should I retry?

Return stable error codes such as `VALIDATION_ERROR`, `PERMISSION_DENIED`, `NOT_FOUND`, `RATE_LIMITED`, and `UPSTREAM_TIMEOUT`, plus a short actionable message. Use capped retries only for transient failures such as timeouts or rate limits; do not blindly retry invalid arguments or denied permissions. Give writes an idempotency key, confirm high-risk actions before execution, and read state back afterwards when confirmation matters.

How do I test a tool-using system?

Use replayable cases to test both calling and not calling a tool, asking for missing information, choosing between similar tools, and server-side rejection of unauthorized, replayed, or invalid calls. Inject timeouts, rate limits, empty results, and prompt injection inside tool output. Track tool-selection accuracy, valid-argument rate, blocked unauthorized actions, latency, and duplicate writes.