Function calling patterns.
Schema with Pydantic
from pydantic import BaseModel, Field
class SearchArgs(BaseModel):
query: str = Field(description="Search query")
limit: int = Field(default=10, ge=1, le=100)
tool = {
"name": "search",
"description": "Search the web",
"input_schema": SearchArgs.model_json_schema(),
}
Validation
def run_tool(name: str, args: dict):
if name == "search":
try:
validated = SearchArgs(**args)
except ValidationError as e:
return {"error": str(e)}
return do_search(validated.query, validated.limit)
return {"error": "unknown tool"}
Tool registry
TOOLS = {}
def tool(name=None, description=None):
def dec(fn):
TOOLS[name or fn.__name__] = {"fn": fn, "description": description or fn.__doc__}
return fn
return dec
@tool(description="Get weather")
def get_weather(city: str, unit: str = "c") -> dict:
return {"temp": 20, "unit": unit}
Auto-generate schemas with pydantic or instructor.
Multi-tool parallel
LLM can call multiple in one response:
for block in response.content:
if block.type == "tool_use":
results.append(asyncio.create_task(run_tool(block.name, block.input)))
results = await asyncio.gather(*results)
Structured output via tool
Force structured JSON:
class Result(BaseModel):
summary: str
tags: list[str]
tools = [{"name": "submit", "description": "Submit result", "input_schema": Result.model_json_schema()}]
tool_choice = {"type": "tool", "name": "submit"}
Tools as instances
class Tools:
def __init__(self, db): self.db = db
def fetch_user(self, id: int):
return self.db.user.find(id)
tools_obj = Tools(db)
def run(name, args):
return getattr(tools_obj, name)(**args)
Error → recovery
try:
result = tool(**args)
except SomeError as e:
result = {"error": str(e), "hint": "Try with different args"}
# Pass back; LLM may retry
instructor library (Pydantic-first)
import instructor
from openai import OpenAI
client = instructor.from_openai(OpenAI())
result = client.chat.completions.create(
model="gpt-5",
response_model=Result,
messages=[...],
)
# result is a validated Result instance
Retry built-in on validation failure.
Common mistakes
- Tools accepting any JSON without schema → garbage in.
- Long-running tool → request timeout.
- Returning huge dict → fills context.
- Tools that have side effects in test runs.
- Missing return value handler → LLM stuck.
Read this next
If you want my function-calling helpers, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .