Building Advanced Agents: Function-Calling
The Shape of a Tool Call, End to End
Before we look at the code, here's the minimum viable example, distilled. A tool in LangChain is a Python function decorated with @tool. The decorator inspects the function's signature, builds a JSON schema for it, and registers the function as something the model can invoke.
Let's run through a simple example to understand before we dive into more complex code (change model and base_url to match your setup). Here is the example we're going to run; it's in the companion toolkit repo:
# repl/test_agent.py
from langchain_core.tools import tool
from langchain.agents import create_agent
from langchain_ollama import ChatOllama
@tool
def add_numbers(a: int, b: int) -> int:
"""Add two integers and return the result."""
# Debugging
print("I'm being called with", a, b)
return a + b
llm = ChatOllama(
model="granite3.3:2b",
base_url="http://localhost:11434"
)
agent = create_agent(model=llm, tools=[add_numbers])
response = agent.invoke(
{
"messages":
[
{
"role": "user",
"content": "What is 17 plus 25?"
}
]
}
)
print(response["messages"][-1].content)
When you run this with uv run test_agent, the model will produce the correct answer, 42, even though it has no built-in math ability:
# Change directory
cd $HOME/companion/code/repl
# Run the agent
uv run test_agent.py
# Output:
# I'm being called with 17 25
# The sum of 17 and 25 is 42.
When you run this, what happens internally is more than a single model call. The agent runs a small loop: the model reads the user's question, decides to call add_numbers with a=17, b=25, the agent runs the function, the result (42) gets fed back to the model as a tool message, and the model produces a final reply like 17 plus 25 equals 42. The user sees one answer; the loop fired three messages internally to produce it.
Two things make this work. The first is that the model has to be tool-capable. Not every model supports function/tool calling; the architecture has to have been trained for it. You can check with ollama show $MODEL and look for tools in the Capabilities section. granite3.3:2b supports tools; smaller or older models often don't. The second is that the docstring of each tool function is what the model reads to decide whether to call it. A vague docstring leads to skipped tool calls or wrong tools fired. Short, specific, action-oriented docstrings give the best results.
To our existing code, we will add 2 weather tools backed by the free Open-Meteo HTTP API.
Step 1: Write a Private Helper for Geocoding
Weather APIs want latitude/longitude, not place names. We write one helper that turns "Paris" into (48.85, 2.35):
def _get_coordinates(location: str) -> tuple[float, float]:
response = httpx.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={
"name": location,
"count": 1,
...
},
timeout=10,
)
response.raise_for_status()
...
Two important details:
- The leading underscore (
_get_coordinates) is a Python convention for "private". This function is not exposed to the model. Only the two@tool-decorated functions below are. response.raise_for_status()turns HTTP errors (4xx/5xx) into Python exceptions. We want exceptions because the agent's middleware (later) knows how to handle them.
Step 2: Define Real Tools with @tool
A tool is just a normal function decorated with @tool. Here's how we define the air quality tool:
@tool
def get_air_quality(location: str) -> str:
"""Get current air quality (PM10 and PM2.5) for a named location."""
if DEBUG:
print(f"Tool called: get_air_quality({location})")
latitude, longitude = _get_coordinates(location)
response = httpx.get(
"https://air-quality-api.open-meteo.com/v1/air-quality",
params={
"latitude": latitude,
"longitude": longitude,
"hourly": "pm10,pm2_5",
"forecast_days": 1,
},
timeout=10,
)
response.raise_for_status()
data = response.json()
if (
"hourly" in data
and "pm10" in data["hourly"]
and "pm2_5" in data["hourly"]
):
pm10 = data["hourly"]["pm10"][
0
] # [0] = current hour
pm2_5 = data["hourly"]["pm2_5"][0]
result = f"PM10: {pm10} μg/m³, PM2.5: {pm2_5} μg/m³"
else:
result = "Air quality data not available"
return fLocal AI Engineering with Ollama
Run, understand, customize, fine-tune, and build agentic apps on your own hardwareEnroll now to unlock all content and receive all future updates for free.
