Language models are impressive reasoners, but without tools they can only generate text. They can’t check real-time data, perform precise calculations, or interact with external systems. In this post, I’ll explore how to extend agents with tools through function calling, and ensure reliable outputs using Pydantic for structured data validation.
From Passive to Active AI
Ask a basic LLM “What’s the weather in Tokyo right now?” and you’ll get one of two responses: a hallucinated answer or an admission that it doesn’t have current data. Neither is useful.
Tools transform agents from passive responders into active problem-solvers:
flowchart LR
U[User Query] --> A[Agent]
A --> D{Decision}
D -->|Need Data| T[Tool Call]
T --> R[Result]
R --> A
D -->|Can Answer| O[Response]
style T fill:#e3f2fd
style A fill:#fff3e0
With tools, the agent can:
Fetch real-time weather data
Calculate precise results
Query databases
Send notifications
Execute code
Function Calling: The Bridge
Early approaches to tool use relied on fragile prompt engineering - asking the model to output specific strings that could be parsed. Modern function calling is far more robust.
How It Works
Define tools with clear schemas (name, description, parameters)
Model decides when a tool is needed based on the query
API returns a structured tool call (not free text)
Backend executes the tool and returns results
Model generates final response using tool output
sequenceDiagram
participant U as User
participant A as Agent
participant T as Tool
U->>A: "What's the weather in Tokyo?"
A->>A: Decides to use weather tool
A->>T: get_weather(city="Tokyo")
T->>A: {"temp": "22°C", "condition": "Sunny"}
A->>U: "It's currently 22°C and sunny in Tokyo"
Defining Tools
Tools need clear documentation so the model knows when and how to use them:
@tool defget_weather(city: str) -> dict: """ Get the current weather for a city. Args: city: Name of the city to get weather for Returns: Dictionary with temperature and conditions """ # In production, call actual weather API weather_data = { "Tokyo": {"temp": "22°C", "condition": "Sunny"}, "London": {"temp": "15°C", "condition": "Cloudy"}, "New York": {"temp": "18°C", "condition": "Clear"} } return weather_data.get(city, {"temp": "Unknown", "condition": "Unknown"})
@tool defcalculate(expression: str) -> float: """ Evaluate a mathematical expression. Args: expression: Math expression to evaluate (e.g., "23 * 45") Returns: Numerical result """ returneval(expression) # Use safer evaluation in production
Key requirements for effective tool definitions:
Clear docstring: Explains what the tool does
Typed parameters: Model knows what arguments to provide
# Usage notes = """ Q4 Planning Meeting - January 10th Attendees: Alice, Bob, Carol Discussion: - Reviewed Q3 results, exceeded targets by 15% - Discussed new product launch timeline - Budget allocation for marketing campaigns Action Items: - Alice to prepare launch slides by Friday - Bob to finalize budget proposal by EOW - Carol to schedule customer interviews """
try: # If model returns invalid data, Pydantic catches it summary = MeetingSummary(**raw_data) except ValidationError as e: print(f"Validation failed: {e}") # Retry with clarified prompt or use fallback
Design tools carefully: Good documentation helps models choose correctly
With tools and structured outputs, agents can interact with the real world and integrate with other systems reliably. In the next post, I’ll explore how to manage agent state and memory across conversations.
This is Part 8 of my series on building intelligent AI systems. Next: agent state and memory management.
Comments