top of page

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:

  1. Model calls look_up_order("ORD-12345")

  2. Your code executes; returns order data

  3. 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.

bottom of page