Skip to main content

Module 3 — Function calling and tool descriptions

The loop from module 2 will only work if the model can pick the right tool with the right arguments. That decision is made almost entirely from what you write in the tool's description — the name, the fields, and one sentence per field. Get those wrong and no amount of prompt engineering upstream will save the agent.

The JSON schema of a tool

Every provider that supports function calling — OpenAI, Anthropic, Google, Mistral, open-source models via a local server — accepts the same shape. A tool has a name, a natural-language description, and a schema for its parameters written in JSON Schema.

web_search = {
"type": "function",
"function": {
"name": "web_search",
"description": (
"Search the public web for a short query. "
"Returns up to five results, each with title, URL and snippet. "
"Use when the answer is likely on a public page and you do not "
"already know the URL. Do not use for internal documents."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Three to eight words. Prefer nouns over verbs.",
"minLength": 3,
"maxLength": 120,
},
"top_k": {
"type": "integer",
"description": "Number of results to return, from 1 to 5.",
"minimum": 1,
"maximum": 5,
"default": 3,
},
},
"required": ["query"],
"additionalProperties": False,
},
},
}

Five decisions are already visible. The name is a verb phrase, not a noun — web_search, not search_engine. The description says when to use it and when not to, because the model reads it as a spec. Each field carries its own one-line description. Bounds on the string length and the integer are enforced by the schema, so a hallucinated top_k=42 is rejected before the tool is called. And additionalProperties: False refuses extra fields, protecting downstream code from surprises.

Description quality is 80% of the work

Two descriptions for the same tool, on the running example. The first was our starting point.

read_page — reads a page and returns text.

The second, after a week of watching failure traces.

read_page — Fetches the given HTTPS URL and returns the main text of the page, stripped of navigation and ads, up to 8 000 characters. Use after web_search to actually read a promising result. Do not pass a search-engine results page — use web_search for that. Returns "not found" if the URL 404s.

The second description reduced wrong-tool calls on our evaluation set from 34% to 6% of iterations without changing the model, the loop, or the system prompt. This is not a rhetorical flourish: the description is a specification, and the model treats it as one. Undocumented edge cases become hallucinated edge cases. The classic culprits are the return type on failure, the difference between two tools that sound similar, and the maximum size of a field.

How many tools before confusion?

Empirically, on GPT-4-class models with careful descriptions, an agent handles about seven distinct tools before selection accuracy degrades noticeably. Beyond twelve, the wrong-tool rate roughly doubles. Two remedies exist.

Grouping. Turn three sibling read operations into one tool with a source field. read({"source": "web", "url": ...}) and read({"source": "internal", "id": ...}) replace read_web_page and read_internal_doc. The model picks the field value more reliably than the tool name.

Tiering. Expose only the tools relevant to the current phase. A planner-phase agent sees search and plan; an executor-phase agent sees read and finish. Sub-agents in module 5 formalize this.

Validate before executing

Function calling from the model returns arguments as strings inside a JSON object. Two failure modes recur.

The model omits a required field, because it decided the default was obvious. The schema catches this. Reject the call and feed the error back as an observation: the model usually retries correctly.

The model stuffs a payload into a string field. Asked for a URL, it returns a paragraph. Length bounds catch the worst cases, but validation still belongs in Python, using pydantic or the schema library of your choice.

from pydantic import BaseModel, Field, HttpUrl, ValidationError

class ReadArgs(BaseModel):
url: HttpUrl
max_chars: int = Field(default=8000, ge=100, le=20000)

def run_read(raw_args: dict) -> str:
try:
args = ReadArgs(**raw_args)
except ValidationError as exc:
return f"invalid arguments: {exc.errors()[0]['msg']}"
return fetch(args.url, args.max_chars)

The ValidationError becomes a tool observation, not a Python exception. The model reads it and adjusts on the next iteration. Turning validation errors into observations is what makes an agent recover from its own mistakes instead of crashing on them.

The tools of the running example

The watch agent uses four tools, deliberately kept small.

ToolWhen the model should pick it
web_search(query, top_k)The answer is likely on a public web page and the URL is unknown.
read_page(url)A promising URL is in hand and its full content is needed.
internal_kb(query, top_k)The question mentions a product, team or internal decision.
finish(answer)The transcript already contains enough sourced evidence to answer.

Notice what is not there. No parse_html, because read_page returns stripped text. No compare_sources, because that reasoning belongs in a thought, not a tool. Each tool must do a job the model cannot do inside a thought — every extra tool that duplicates a thought is dead weight that slows selection.

Read the trace before rewriting the tools

When the agent picks the wrong tool or the wrong arguments, the reflex is to change the code. Read the last three thoughts first. Nine times out of ten, one sentence added to the description — a "do not use for…" or a "returns X on Y" — fixes the behaviour without touching a single line of Python.

Summary

  • A tool is a name, a description, and a JSON Schema for its parameters; every provider accepts this shape.
  • The description is a specification: state when to use the tool, when not to, and what it returns on failure.
  • Beyond about seven tools, selection degrades; group with a source field or tier by phase.
  • Validate arguments with a schema library and turn validation errors into tool observations, not Python exceptions.

Next module: memory — the working memory of the loop and the long-term memory that survives across sessions.