Tools and function calling
A model cannot run code. It can only ask for a function to be run, by name and with arguments — and your program decides whether to obey.
What the model actually does
Function calling is often described as the model using a tool. It does not. The model receives a list of tool descriptions and may reply with a structured request: this function, these arguments. Your code reads that request, runs the function, and sends the result back as another message.
That separation is the whole safety model. The model proposes; the program disposes. A tool call is untrusted input in exactly the same way a user message is, and it has to be validated before anything executes.
The round trip has three messages: the human question, the assistant message carrying the tool call, and the tool message carrying the result with the call id. The model never sees a function body or a database; it sees the declaration you gave it and the result you chose to return. Everything else — credentials, validation, rate limits — stays on your side of the boundary.
The description is the interface
A model chooses a tool from its name, its description and its parameter schema. A vague description produces wrong choices, and no amount of prompt engineering downstream repairs a tool called process that “does stuff”. Name tools after the action, describe when to use them, and describe when not to.
Parameter schemas matter for the same reason. If a date is expected as YYYY-MM-DD, say so in the field description; the model reads it. Types alone are weaker guidance than an example of the expected value.
Declaring a tool
from langchain_core.tools import tool
@tool
def search_orders(customer_id: str, status: str = "open") -> list[dict]:
"""Find a customer's orders. Use when the question mentions an order,
a delivery or a refund. Status is one of: open, shipped, cancelled."""
return db.orders(customer_id=customer_id, status=status)
model_with_tools = model.bind_tools([search_orders])
reply = model_with_tools.invoke("Where is order A-1024?")
print(reply.tool_calls)
# [{'name': 'search_orders', 'args': {'customer_id': 'A-1024'}, 'id': '...'}]A registry, not an if-chain
The dispatcher looks tools up by name. Ask for a tool that does not exist and for one with a missing argument, then add a `refund` tool of your own.
Describe the failure, not just the function
A tool description should say what the tool returns and how it fails. “Returns status, total and delivery date; fails if the id is unknown” tells the model what to expect and what a failure means. Without that, the model treats an error string as a result and builds an answer on top of it.
A common mistake is a description written for a human reviewer — accurate about the implementation, silent about when to call it. The model does not need the SQL; it needs the boundary. The symptom is a tool called with plausible but wrong arguments, or skipped on a question it was built for.
Validate before you execute
The model can invent a tool name, omit a required argument, or pass a string where a number belongs. Every one of those is a normal event, not an exceptional one, and each needs a reply the model can act on: “no such tool”, “customer_id is required”.
Validation also protects you from the model itself. A tool that takes a file path and is described loosely will eventually be asked to read a path nobody intended. If the operation is sensitive, the permission belongs in the tool, not in the prompt.
Never build a tool that evaluates its argument as code. A model that can ask for eval("...") is a remote code execution endpoint with extra steps.
The loop closes with a result
After running the tool, the result goes back as a tool message tied to the call’s id. The model then answers using it. That round trip — request, execute, return — is one step of an agent, and a loop around it is what people mean by an agentic workflow.
Every loop needs a stop condition: a maximum number of tool calls, or a budget. Without one, a model that keeps asking for the same failing tool will keep being answered.
The tool message must carry the call id, not just the result. The model matches results to requests by id; without it, two calls in one turn become ambiguous and the model may use the wrong output for the wrong argument. That ambiguity is silent — the answer still looks fluent — which is why the id belongs in the message rather than in a comment.
Read the model’s request
The fake model replies with a `CALL name {...}` line. Parse it, validate it, execute it, and return an error message the model could recover from.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Who executes a tool call?
