Function Calling LLM Best Practices: Production Guide for 2026
Function calling transforms LLMs from text generators into action-taking agents. Production function calling LLM best practices require robust schemas, error handling, and security guardrails.

Function calling is what turns an LLM from a text generator into a system that can query your database, send your emails, and update your records. The gap between a demo and a production deployment is not the model. It is the engineering around the tools: schema design, validation, security, and the loop that executes calls and feeds results back. This guide covers the function calling LLM best practices that matter in production, with working code for Anthropic's Claude, OpenAI's API, and LangChain.
What function calling actually is
Function calling (Anthropic calls it tool use) is a contract between your application and the model. You declare what operations exist and what shape their inputs take. The model decides when to call them and generates the arguments. Your code executes the call and returns the result. The model never runs anything itself.
User: "What's the weather in Lagos?"
Model: [emits tool_use: get_weather(city="Lagos")]
Your code: runs the lookup, returns {"temp": 28, "condition": "Partly cloudy"}
Model: "It's 28°C and partly cloudy in Lagos right now."
Every client-side tool call is a round trip. Anthropic's documentation describes the canonical pattern as a loop keyed on stop_reason: send the request with your tools, and while the response comes back with stop_reason: "tool_use", execute each call, append the results as tool_result blocks, and send the conversation back. The loop exits when the model produces a final answer (How tool use works). OpenAI's Responses API works the same way: you receive function_call items, execute them, and send function_call_output items back with the matching call_id (OpenAI function calling guide).
A useful rule of thumb from Anthropic: if you are writing a regex to extract a decision from model output, that decision should have been a tool call. Parsing free-form text to recover structured intent means the structure belongs in a schema.
Enforce the schema, do not hope for it
The single biggest production improvement of the last two years is guaranteed schema conformance. Both major providers now offer it, and you should turn it on.
OpenAI strict mode. Set strict: true on each function definition. OpenAI recommends always enabling it. Two requirements follow: additionalProperties must be false on every object, and every field in properties must be listed in required (make optional fields nullable instead). The Responses API will try to normalize your schema into strict mode automatically; Chat Completions stays non-strict unless you opt in (OpenAI function calling guide).
Anthropic strict tool use. Add strict: true to your custom tool definitions so Claude's tool calls always match your input_schema exactly (Tool use with Claude).
Strict mode removes a whole class of failures: missing keys, hallucinated enum values, wrong types. It does not remove semantic errors. The model can still call the wrong tool or pass a plausible-but-wrong value, which is why the next section exists.
Validate every argument the model generates
Schema conformance is not business-logic validation. The model can produce a perfectly well-formed request to transfer a negative amount of money. Validate again on your side, in code you control.
from pydantic import BaseModel, field_validator
class TransferParams(BaseModel):
from_account: str
to_account: str
amount: float
@field_validator("amount")
@classmethod
def amount_must_be_positive(cls, v: float) -> float:
if v <= 0:
raise ValueError("Amount must be positive")
if v > 10_000:
raise ValueError("Amount exceeds transfer limit")
return v
@field_validator("from_account", "to_account")
@classmethod
def account_must_be_valid(cls, v: str) -> str:
if not v.startswith("ACC-"):
raise ValueError("Invalid account format")
return v
Note the @field_validator decorator: the old @validator is deprecated in Pydantic v2 and will emit warnings on current versions. Treat the LLM exactly like an untrusted external client of your API, because that is what it is.
Write descriptions the model can route on
The model uses your tool description to decide when to call the tool, not just how. A vague description causes both under-triggering (the tool never fires) and over-triggering (it fires when it should not).
Bad:
def search(query):
"""Searches stuff"""
Good:
def search_knowledge_base(
query: str,
max_results: int = 5,
filters: dict | None = None,
) -> list[dict]:
"""
Search the company knowledge base for relevant documents.
Use this when the user asks about company policies, procedures,
or internal documentation. Do NOT use for general knowledge
questions or real-time data.
Args:
query: Natural language search query
max_results: Maximum results to return (default 5)
filters: Optional filters like {"department": "engineering"}
Returns:
List of documents with title, content, and relevance score
Examples:
"What's our vacation policy?" -> search_knowledge_base("vacation policy")
"Engineering docs on API design" -> search_knowledge_base(
"API design", filters={"department": "engineering"})
"""
Three things to notice. The description says when to use the tool and when not to. It includes worked examples of user phrasing mapped to calls. And it documents the return shape, which helps the model reason about the result it will get back.
One steering note that changed with current models: Anthropic's prompting guidance for Claude Sonnet 4.5 and later warns that these models follow instructions literally and respond strongly to the system prompt. If your prompts were written to fix under-triggering on older models with aggressive language ("CRITICAL: You MUST use this tool when..."), newer models may over-trigger. Dial it back to plain instructions like "Use this tool when..." and add a short directive such as "Use the tools to investigate before responding" only if you see under-triggering (Claude prompting best practices).
Budget for the token overhead
Tools are not free. Every definition you ship is injected into the context on every request, and the API adds a tool-use system prompt on top. Anthropic publishes the exact overhead: for Claude Sonnet 4.5, Opus 4.5, and Haiku 4.5, the tool-use system prompt costs 496 tokens with tool_choice set to auto or none, and 588 tokens when you force a call with any or tool (Tool use with Claude). Your schemas and descriptions come in addition to that.
Practical consequences:
- Keep descriptions tight. "Search company knowledge base for policies and documentation. Use when: user asks about company info" beats a five-sentence essay.
- Prune the tool list. Twenty tools with overlapping purposes confuse routing and inflate every request. Merge or split by agent role instead.
- Return lean results. Strip fields the model does not need before sending a
tool_resultback. A 5,000-token database dump when the model needed three fields is pure waste on the next turn.
Design tools for the model, not for your codebase
Make functions composable. One monolithic send_report_email_to_team() is hard for the model to route and hard for you to debug. Split it into generate_report, format_email, and send_email, and let the model orchestrate the composition. That is what it is good at.
Make invalid states unrepresentable. OpenAI's guide recommends using enums and object structure so the model cannot express a bad call: set_light(state: "on" | "off") beats toggle_light(on: bool, off: bool), which permits the meaningless on=true, off=true (OpenAI function calling guide).
Return structured data, not prose. "It's sunny and 25 degrees" forces the model to re-parse text. Return typed JSON:
def get_weather(city: str) -> dict:
"""Get current weather for a city."""
return {
"city": city,
"temperature_celsius": 25.0,
"condition": "sunny",
"humidity_percent": 65,
"timestamp": "2026-09-26T08:00:00Z",
}
If you need the model's final answer in a fixed shape rather than a tool call, that is a different feature: OpenAI's Structured Outputs with a json_schema response format guarantees the reply itself adheres to a schema, which plain JSON mode never did (Structured outputs guide).
Name things explicitly. get(), process(), and handle() give the router nothing. get_user_profile, process_payment, handle_webhook_event do.
Expose side effects. If a function logs a message and emails the admins, say so in the description and the name. Hidden side effects produce surprised users and, worse, surprised compliance teams.
Security: treat every tool as an attack surface
An agent with tools is a confused-deputy problem waiting to happen. A prompt injection in a web page or email the model reads can attempt to call your tools with the model's authority.
- Authorize inside the function, not in the prompt. Check permissions against the requesting user's actual role before executing:
def delete_user(user_id: str, requesting_user: str) -> dict:
"""Delete a user account (admin only)."""
if not is_admin(requesting_user):
raise PermissionError("Only admins can delete users")
if not user_exists(user_id):
raise ValueError(f"User {user_id} not found")
return perform_deletion(user_id)
- Rate-limit anything that costs money or sends messages. Five SMS per minute per session is a reasonable default; unlimited is not.
- Require confirmation for destructive or irreversible actions. Have the tool return a
requires_confirmationpayload so the agent pauses and asks the user beforedelete_all_recordsorsend_paymentexecutes. Build a dry-run mode (dry_run: bool = False) into expensive tools so you can test flows without side effects. - Scope the tool list per context. A support-chat agent does not need the refund tool loaded "just in case." Fewer tools means a smaller blast radius and better routing.
Keep your failure-mode handling (backoff, retries, circuit breakers) in the execution wrapper, not in the prompt. Deterministic retries in code beat asking the model to "try again" every time.
Handle errors so the model can recover
A tool that throws an unhandled exception ends the loop. A tool that returns a structured error lets the model apologize, retry with corrected arguments, or ask the user for the missing piece.
from typing import Optional
from pydantic import BaseModel
class FunctionResult(BaseModel):
success: bool
data: Optional[dict] = None
error: Optional[str] = None
def safe_function_call(func, *args, **kwargs) -> FunctionResult:
try:
return FunctionResult(success=True, data=func(*args, **kwargs))
except PermissionError as e:
return FunctionResult(success=False, error=f"Permission denied: {e}")
except ValueError as e:
return FunctionResult(success=False, error=f"Invalid input: {e}")
except Exception:
logger.exception("Tool %s failed", func.__name__)
return FunctionResult(success=False, error="An unexpected error occurred")
Keep error messages specific enough for the model to act on ("Invalid input: amount exceeds transfer limit of 10000") but never leak stack traces, internal hostnames, or secrets into a tool_result. Everything you return goes into the model's context and potentially into the conversation the user sees.
Platform specifics, current as of this writing
OpenAI (Responses API):
response = client.responses.create(
model="gpt-5",
tools=[{
"type": "function",
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. Nairobi"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city", "unit"],
"additionalProperties": False,
},
"strict": True,
}],
input=[{"role": "user", "content": "What's the weather in Nairobi?"}],
)
Anthropic:
message = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=[{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
},
}],
messages=[{"role": "user", "content": "Weather in Nairobi?"}],
)
LangChain still auto-generates the schema from a decorator plus docstring, which is convenient but makes the quality of your docstring even more important:
from langchain.tools import tool
@tool
def search_knowledge_base(query: str, max_results: int = 5) -> list:
"""Search company knowledge base.
Args:
query: Search query
max_results: Number of results to return
"""
return perform_search(query, max_results)
If you want a full build walkthrough, our LangChain agent tutorial goes from zero to a working tool-using agent, and our complete function calling guide for 2026 is a broader platform-by-platform reference.
Current models also call independent tools in parallel by default. Anthropic notes you can push parallel-calling reliability toward 100% with an explicit instruction, and warns the flip side: never let the model parallelize calls where a later call depends on an earlier result, and never let it guess missing parameters with placeholders (Claude prompting best practices).
Test with malformed input, not just the happy path
Unit-test the functions themselves, integration-test that the model picks the right tool with the right arguments, and then chaos-test the boundary where the model hands you data:
def test_function_with_invalid_llm_args():
invalid_inputs = [
{"city": 12345}, # wrong type
{"city": ""}, # empty string
{}, # missing required field
{"city": "Lagos", "extra": "field"}, # unexpected fields
]
for bad in invalid_inputs:
result = safe_function_call(get_weather, **bad)
assert result.success is False
assert result.error is not None
Strict mode reduces how often these reach you, but your validation layer is the last line of defense regardless of provider guarantees. Also test the routing boundary: prompts that should not trigger a tool, because false positives cost real money when the tool sends email or touches a payment API.
When function calling is the wrong choice
Not every interaction needs tools. Anthropic's own guidance lists the cases to avoid (How tool use works):
- The model can answer from training data. Summarization, translation, and general knowledge questions gain nothing from a round trip.
- One-shot Q&A with no side effects. If there is nothing to execute, there is nothing for a tool to do.
- Latency dominates a trivial task. Every tool call is at least one extra round trip. On a lightweight interaction, the overhead can exceed the work.
- A deterministic pipeline would do. If the sequence of calls is fixed and known in advance, write a normal script or an n8n workflow and skip the model entirely. Use function calling when the path through the tools is what needs to be intelligent.
Frequently asked questions
What is the difference between function calling and tool use? They are two names for the same mechanism. OpenAI popularized "function calling"; Anthropic calls it "tool use." Both mean the model emits a structured request that your code executes, with the result fed back into the conversation.
How many tools can I give one agent? There is no hard universal limit, but routing accuracy degrades as the list grows and every definition costs tokens on every request. In practice, keep a single agent under roughly a dozen well-differentiated tools. Beyond that, split the work across specialized agents or use a tool-search pattern.
Does strict mode mean I can skip validation? No. Strict mode guarantees the arguments match your JSON schema's shape. It says nothing about whether the values make sense in your domain, so business-rule validation, authorization, and rate limits are still your job.
Should I use function calling or MCP? They solve different layers. Function calling is the model-to-code mechanism inside one application. The Model Context Protocol is an open standard, often described as a USB-C port for AI applications, for exposing tools from external servers to any compatible client. Most production systems use function calling under the hood and MCP at the integration boundary.
Which model is best for function calling? Current frontier models from both major providers handle tool use well, and both offer strict schema enforcement now. The differentiator is rarely the model; it is the quality of your schemas, descriptions, and error handling. Benchmark on your own tool set before switching providers over a synthetic leaderboard score.
Where to go from here
Start with three to five tools that map to real workflows, put strict mode and validation on from day one, and add confirmation gates around anything irreversible. If you would rather have a team that builds these loops every week set it up for you, our Claude implementation service covers tool design, MCP servers, and production guardrails, and our custom builds team handles the surrounding application when the agent needs a product around it.
About AI Agents Plus Editorial
AI automation expert and thought leader in business transformation through artificial intelligence.



