Module 01
Tool Use and Function Calling
On this page
In plain words
A model by itself can only write text. It cannot check your order, read a database, or run code. So you give it a small list of tools with clear descriptions. The model writes a request saying which tool and with what arguments, your program checks that request and actually runs it, then hands the result back. That is all function calling is.
How it flows
- 1User asks
- 2Model picks tool
- 3Validate arguments
- 4Your code runs it
- 5Result goes back
- 6Model answers
A tiny example
call = model_picks_tool(user_question)
error = validate(call.name, call.args)
if error:
result = error # send words back, never crash
else:
result = run_tool(call.name, call.args)
send_back(call.id, result) # id keeps result glued to its callNotice that a bad call becomes a readable message the model can retry from, and the call id travels with the result.
What you will learn
- What "tool use" and "function calling" actually mean, in plain words.
- How to describe a tool so the model picks the right one.
- How to check the arguments the model sends you before running anything.
- Why one tool call is easy now, but twenty in a row is still hard.
The problem, simply
See, imagine you go to the college office to get a bonafide certificate. The clerk sitting there does not know your attendance, your fee status, or your roll number by heart. He knows the process. He fills a small slip, sends it to the accounts window, gets the reply, and then hands you the certificate.
A language model is exactly that clerk. On its own it cannot check today's IRCTC train status, it cannot read your database, it cannot run your code. It only produces text. But if you tell it "here are three windows you can send a slip to, and here is what each window does", it becomes very useful.
That slip is a tool call. The model writes a small structured request, your program actually runs it, and you hand the answer back. Basically the model gets hands.
The idea
The loop
Nothing magical here. It is a loop.
- 1You ask
- 2Model picks a tool
- 3Your code runs it
- 4Result goes back
- 5Model answers
The model never runs anything itself. It only says "call get_marks with roll_number 21B0142". Your program is the one that actually touches the database. That separation is the whole safety story.
What a tool looks like
Every provider has slightly different wording, but the shape is always the same three things:
- name — like
get_train_status. - description — what it does and when to use it.
- input schema — the argument names, their types, which ones are compulsory.
The description matters more than students expect. That is the only thing the model reads to decide between your tools. If you have search_orders and search_products and both descriptions just say "searches", the model will keep picking the wrong one. Most wrong-tool bugs are actually bad-description bugs.
TipTip: Write the description like you are writing it for a new intern on day one. "Use this to get the delivery status of one order. Do not use it to search for products."
A worked example
Suppose Priya builds a small assistant for a Swiggy-like app. She gives it two tools:
get_order_status(order_id: string)cancel_order(order_id: string, reason: one of "late", "wrong_item", "changed_mind")
Priya types: "My order 4471 is very late, cancel it."
The model sends two calls in one go — get_order_status("4471") and cancel_order("4471", "late"). Both come with their own id, something like call_1 and call_2. Your code runs them, and sends back two results, each tagged with the id it belongs to.
Now here is the trick. If you send back the cancel result labelled call_1 and the status result labelled call_2, everything still looks fine. No crash. The model just reads the wrong answer for the wrong question and confidently tells Priya something false. Those ids are not decoration.
- 1Model sends call_1 and call_2
- 2Run both
- 3Tag each result
- 4Model reads both
- 5One clean reply
Never trust the arguments
The model is guessing your schema from a description. It will make mistakes. So validate before running:
- Types. Schema says integer, model sent
"5". Fixing that one is safe. Model sent"five"? Reject it. - Allowed values. Reason must be
late,wrong_itemorchanged_mind. Model sentin_progress. Reject. - Missing fields.
order_idnot given? Do not crash your server. Send back an error message. - Formats. Dates, emails, phone numbers — parse them properly, do not trust a quick regex.
Remember: when validation fails, send the model a clear error sentence, not a stack trace. The model reads that sentence and retries correctly. A crash gives it nothing to learn from.
Where the field stands
There is a 2023 research paper called Toolformer that first showed models can teach themselves when to call a tool. The trick was simple: insert a candidate tool call into some text, actually run it, and keep it only if the result made the next words easier to predict. No human labelling at all. They also found this works well only in bigger models.
The standard scoreboard today is BFCL, short for Berkeley Function Calling Leaderboard — a public benchmark that scores how well models call functions. Its latest version splits marks roughly as 40% full agent runs, 30% multi-turn conversations, 10% real user prompts, 10% synthetic cases, and 10% "should not have called anything at all".
That last 10% is interesting. Knowing when not to call a tool is a graded skill.
Scoring also changed along the way. Earlier it compared the shape of your call against an expected one. Now it checks the actual end state — was the file really created? A call can look perfect on paper and still leave the system wrong.
IMPNote: Single tool calls are basically a solved problem in 2026. The hard parts left are memory across turns, picking the next tool based on what the last one returned, and long chains that drift after twenty-odd steps.
Build it
"""A tiny tool registry: describe tools, validate arguments, run them."""
# --- 1. The actual tools (your code, not the model's) ---
ORDERS = {"4471": {"status": "out for delivery", "amount": 349}}
def get_order_status(order_id):
order = ORDERS.get(order_id)
return order["status"] if order else "no such order"
def cancel_order(order_id, reason):
if order_id not in ORDERS:
return "no such order"
return "cancelled order " + order_id + " because: " + reason
# --- 2. Descriptions + schema, exactly what the model would read ---
TOOLS = {
"get_order_status": {
"description": "Get the delivery status of ONE order by its id.",
"args": {"order_id": {"type": "string", "required": True}},
"run": get_order_status,
},
"cancel_order": {
"description": "Cancel ONE order. Needs a reason from the allowed list.",
"args": {
"order_id": {"type": "string", "required": True},
"reason": {"type": "string", "required": True,
"allowed": ["late", "wrong_item", "changed_mind"]},
},
"run": cancel_order,
},
}
def check_and_run(name, args):
"""Validate first. Return a readable error instead of crashing."""
if name not in TOOLS:
return "ERROR: no tool named " + name
spec = TOOLS[name]["args"]
clean = {}
for key, rule in spec.items():
if key not in args:
if rule.get("required"):
return "ERROR: missing argument '" + key + "'"
continue
value = args[key]
if rule["type"] == "string" and not isinstance(value, str):
value = str(value) # safe, unambiguous coercion
if "allowed" in rule and value not in rule["allowed"]:
return ("ERROR: '" + key + "' must be one of "
+ ", ".join(rule["allowed"]))
clean[key] = value
fn = TOOLS[name]["run"]
return fn(**clean)
# --- 3. A fake "model" that emits calls, each with its own id ---
def fake_model():
return [
("call_1", "get_order_status", {"order_id": 4471}), # int, coerced
("call_2", "cancel_order", {"order_id": "4471", "reason": "late"}),
("call_3", "cancel_order", {"order_id": "4471", "reason": "bored"}),
("call_4", "get_order_status", {}), # missing arg
]
results = {}
for call_id, name, args in fake_model():
results[call_id] = check_and_run(name, args)
for call_id in sorted(results):
print(call_id, "->", results[call_id])Look at the output carefully. call_1 works even though the id came as a number, because that coercion was unambiguous. call_3 and call_4 fail, but they fail with a sentence the model can read and fix — not with a traceback. And every result stays glued to its own call id.
Where you will see this
- Coding assistants like Claude Code, Cursor and GitHub Copilot — reading files, running tests, editing code are all tools.
- ChatGPT browsing the web or running Python for you is the same pattern underneath.
- Customer-support bots on Flipkart or a bank app: check order, raise ticket, issue refund.
- Swiggy and Zomato style assistants: search restaurants, track the delivery partner, apply a coupon.
- Internal company bots: "how many leaves do I have left" calls an HR system, not the model's memory.
Common mistakes
- Lazy descriptions. "Searches stuff" tells the model nothing, so it picks whichever tool is listed first. Spend two extra minutes here and half your wrong-tool bugs vanish.
- Running arguments without checking. One bad argument straight into your database call and you have either a crash or, worse, real damage.
- Crashing instead of replying. An exception ends the conversation. A clear error string lets the model retry and succeed.
- Giving one giant
run_shell(command)tool. It feels convenient, but you have just handed the model your whole machine. Narrow tools likegit_status()keep the damage boundary small. - Mixing up result ids in parallel calls. No error appears anywhere; the model simply answers wrongly with full confidence.
If they ask in an interview
Q: What actually happens when a model "calls a function"?
A: The model does not run anything. It outputs a structured request naming a tool and its arguments. Your program validates it, runs the real function, and feeds the result back so the model can continue.
Q: Your agent keeps picking the wrong tool. How do you debug it?
A: I would look at the descriptions first, since that is the only thing guiding the choice. Vague or overlapping ones are the usual cause. I would state clearly when to use each tool and when not to, and merge tools that genuinely overlap.
Q: Single function calls work well now. So what is still hard?
A: Long chains. Carrying memory across many turns, choosing the next tool based on what the previous one returned, and staying on track after twenty or more steps. Also knowing when to call no tool at all — benchmarks specifically test that refusal.
Try these
- Add a third tool,
refund_order(order_id, amount), with a rule that the amount cannot exceed the order value. Make the model's bad attempt fail with a helpful message. - Add a
do_nothing()tool the model can pick when no real tool fits. Then feed it a question like "who won the match yesterday" and see if refusing is even representable. - Add a per-tool retry counter. After three failures of the same tool, block it for the rest of the run and return "tool disabled". Watch how that changes recovery.
- Try coercing
"five"to5. Think about which real bugs that silently hides, and write down where you would draw the line.
Words, simply
| Word | Meaning in simple words |
|---|---|
| Tool / function calling | The model asks your code to do something it cannot do itself |
| Tool schema | The tool's name, its description, and the list of arguments it takes |
| Description | The sentence the model reads to decide if this is the right tool |
| Call id | A tag that keeps each result matched to the call it came from |
| Coercion | Quietly fixing an obvious type mismatch, like "5" into 5 |
| Hallucinated tool | The model invents a tool that does not exist in your list |
| Sandbox | The fence around a tool: what it can read, write, and how long it may run |
| BFCL | A public scoreboard for how well models call functions |
Quick recap
- The model only writes the request; your code runs it, so your code decides what is possible.
- Descriptions and argument checks are where almost all real tool bugs live.
- One call is easy today; long chains, memory, and knowing when to call nothing are still open problems.