Tutorial 7: Function Calling and Tool Use
- Contributor
- 4 days ago
- 3 min read
Function calling lets the LLM invoke your code with structured arguments. The model decides when to call; you decide what's available. This tutorial walks through it.
What You'll Build
A working tool-using LLM that can call functions to get info, perform actions, and return results to the user.
Step 1: Pick the Tools (15 min)
What capabilities does the model need?
For a customer service agent:
search_knowledge_base(query) — find docs
look_up_order(order_id) — get order status
create_support_ticket(summary, severity) — escalate
Keep the tool set small initially. 3-5 tools is plenty to start.
Step 2: Define the Schema (15 min)
TOOLS = [
{
"name": "search_knowledge_base",
"description": "Search internal knowledge base for relevant articles",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
},
"required": ["query"],
}
},
{
"name": "look_up_order",
"description": "Look up order details by order ID",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order ID (format: ORD-XXXXX)"
},
},
"required": ["order_id"],
}
},
]
Descriptions matter — they're how the model decides when to use each tool.
Step 3: Implement the Tools (30 min)
def search_knowledge_base(query: str) -> str:
results = kb_search(query, top_k=3)
return "\n\n".join([r["text"] for r in results])
def look_up_order(order_id: str) -> dict:
order = db.query("SELECT * FROM orders WHERE id = %s", [order_id])
if not order:
return {"error": "Order not found"}
return {
"id": order["id"],
"status": order["status"],
"created_at": order["created_at"].isoformat(),
# ... whatever info is appropriate
}
# Dispatch table
TOOL_FUNCTIONS = {
"search_knowledge_base": search_knowledge_base,
"look_up_order": look_up_order,
}
Functions return data the model can use.
Step 4: The Tool-Use Loop (30 min)
def chat_with_tools(messages):
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=TOOLS,
messages=messages,
)
# Check if model wants to use a tool
if response.stop_reason == "tool_use":
tool_use = next(c for c in response.content if c.type == "tool_use")
# Execute the tool
tool_name = tool_use.name
tool_input = tool_use.input
result = TOOL_FUNCTIONS[tool_name](**tool_input)
# Continue conversation with tool result
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": str(result),
}]
})
else:
# Final response
return response.content[0].text, messages
Loop: call → tool use → execute → continue. Until model gives final answer.
Step 5: Test a Simple Flow (15 min)
messages = [{"role": "user", "content": "What's the status of order ORD-12345?"}]
response, _ = chat_with_tools(messages)
print(response)
Trace through:
Model calls look_up_order("ORD-12345")
Your code executes; returns order data
Model uses that data to respond
Step 6: Handle Tool Failures (15 min)
Tools can fail:
def execute_tool(name, input):
try:
return TOOL_FUNCTIONS[name](**input)
except Exception as e:
# Return error to model; let it decide
return {"error": str(e)}
Model receives the error; can apologize, try different approach, etc.
Step 7: Limit Iterations (10 min)
Prevent infinite loops:
def chat_with_tools(messages, max_iterations=10):
for _ in range(max_iterations):
# ... tool use loop
if response.stop_reason != "tool_use":
return response.content[0].text
raise Exception("Max iterations reached")
Without limits, a confused model can loop forever.
Step 8: Log Tool Calls (15 min)
def execute_tool(name, input):
start = time.time()
try:
result = TOOL_FUNCTIONS[name](**input)
logger.info("tool_call", {
"tool": name,
"input": input,
"result_size": len(str(result)),
"latency_ms": (time.time() - start) * 1000,
"success": True,
})
return result
except Exception as e:
logger.error("tool_call_failed", {
"tool": name,
"input": input,
"error": str(e),
})
return {"error": str(e)}
Critical for debugging and cost analysis.
Step 9: Restrict Tools by Context (15 min)
Different users get different tools:
def get_tools_for_user(user):
base_tools = [SEARCH_TOOL, LOOK_UP_ORDER_TOOL]
if user.has_permission("escalate"):
base_tools.append(CREATE_TICKET_TOOL)
if user.is_admin:
base_tools.extend(ADMIN_TOOLS)
return base_tools
Authorization at the tool level. Don't give every user every capability.
Step 10: Test the Failure Modes (varies)
What if:
Model calls a tool with bad input?
Model loops on the same tool?
Tool returns nothing useful?
Tool returns sensitive data?
Test each. Handle each.
What You Just Did
You enabled the LLM to take action through your code. Capabilities are gated by what tools you provide; data flow is controlled; logging gives visibility.
Common Failure Modes
Too many tools. Model gets confused; uses wrong ones.
Vague descriptions. Model can't tell when to use each.
No error handling. Tool failure crashes the conversation.
Unlimited iterations. Model loops; cost explodes.
Tool returns too much. Token budget consumed; model loses focus.


