Ask a language model what time it is in Tokyo and you will get an answer. It will be well formatted, it will name the right timezone, and it will be wrong, because the model has no clock. Everything it knows was fixed when its training data was collected, and nothing in the way it produces text distinguishes "I remember this" from "this sounds like the kind of thing that goes here". A wrong exchange rate reads exactly like a right one.
That is the whole case for tools. Not that models are bad at facts, but that a certain kind of fact cannot be in the model at all, and the model will not reliably tell you when you have asked for one. Your job as the person building the agent is to recognise those questions in advance and route them to something that actually knows.
What a model actually knows
A model's knowledge has three properties that matter here.
It has a cutoff. Training data stops at some date, and the model is then used for months or years after it. Anything that changed in between is invisible: a new holiday, a currency revaluation, a domain that started or stopped accepting mail.
It has no "now". Even for things that were true at the cutoff, the model does not know how much time has passed. It cannot compute "today" or "in three business days" without being told what today is.
It is compressed. The model did not memorise tables; it learned patterns. It will reproduce common facts (the capital of Japan) reliably and long-tail facts (which day of November 2026 is a US federal holiday, the MX record of a particular domain) with a fluency that has nothing to do with accuracy.
None of these is a bug to be fixed with a bigger model. A model trained yesterday still does not know the exchange rate an hour from now.
Hallucinated facts versus tool results
The difference between a hallucinated answer and a grounded one is not tone or confidence. It is provenance. A tool result carries evidence of where it came from: a timestamp, a source date, a field name you can check. Compare what an agent can say in each case:
| Question | Model alone | With a tool result |
|---|---|---|
| Time in Tokyo right now | A plausible time, no way to verify | time24 and date from the call, stamped at call time |
| USD to EUR rate | A rate from somewhere before the cutoff | exchangeRate plus date and lastUpdated |
Can [email protected] receive mail? | "It looks valid" (syntax only) | isMxValid, canReceiveMail, isDisposable from live DNS |
| Working days in the US, November 2026 | A guess around 20 | workingDaysCount with each holiday named |
The right-hand column is something you can log, show to a user and audit later. The left-hand column is something you can only hope about.
A model answers "The current USD to EUR rate is 0.92" with no tool call. What is the most accurate description of that number?
Answer the check above to continue.
Wire up a grounded answer
Here is the smallest useful agent: one tool, backed by World Time, behind a model with tool calling. The pattern is the same across the major model APIs. You describe the tool with a name, a description and a JSON Schema for its input. The model replies either with text or with a request to call the tool. You run the call on your server, send the result back, and the model writes its answer from it.
The examples use the Anthropic Messages API. Install @anthropic-ai/sdk or anthropic, set ANTHROPIC_API_KEY and APIVERVE_API_KEY, and pick a current model in ANTHROPIC_MODEL.
// Node 18+, ESM. npm install @anthropic-ai/sdk
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const MODEL = process.env.ANTHROPIC_MODEL ?? 'claude-opus-5-5';
async function apiverve(id, params) {
const url = `https://api.apiverve.com/v1/${id}?${new URLSearchParams(params)}`;
const res = await fetch(url, { headers: { 'x-api-key': process.env.APIVERVE_API_KEY } });
const body = await res.json();
if (body.status !== 'ok') throw new Error(`${id} ${res.status}: ${body.error}`);
return body.data;
}
const tools = [{
name: 'get_local_time',
description: 'Current local time and date in a city, from a live clock. ' +
'Use for any question about the time or date somewhere now. Returns time24 (HH:MM:SS), date (YYYY-MM-DD) and IANA timezone.',
input_schema: {
type: 'object',
properties: { city: { type: 'string', description: 'City name, e.g. "Tokyo"' } },
required: ['city'],
},
}];
async function runTool(name, input) {
if (name !== 'get_local_time') throw new Error(`unknown tool ${name}`);
const data = await apiverve('worldtime', { city: input.city });
const c = data.foundCities[0];
return { city: c.city, country: c.country, timezone: c.timezone, time24: c.time24, date: c.date };
}
const messages = [{ role: 'user', content: process.argv[2] ?? 'What time is it in Tokyo, and is it already tomorrow there?' }];
for (let turn = 0; turn < 4; turn++) {
const res = await client.messages.create({ model: MODEL, max_tokens: 4096, tools, messages });
messages.push({ role: 'assistant', content: res.content });
if (res.stop_reason !== 'tool_use') {
for (const b of res.content) if (b.type === 'text') console.log(b.text);
break;
}
const results = [];
for (const block of res.content) {
if (block.type !== 'tool_use') continue;
const out = await runTool(block.name, block.input);
results.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(out) });
}
messages.push({ role: 'user', content: results });
}# Python 3.9+. pip install anthropic requests
import json
import os
import sys
import anthropic
import requests
client = anthropic.Anthropic()
MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-opus-5-5")
def apiverve(api_id, params):
r = requests.get(
f"https://api.apiverve.com/v1/{api_id}",
params=params,
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]},
timeout=15,
)
body = r.json()
if body.get("status") != "ok":
raise RuntimeError(f"{api_id} {r.status_code}: {body.get('error')}")
return body["data"]
TOOLS = [{
"name": "get_local_time",
"description": (
"Current local time and date in a city, from a live clock. "
"Use for any question about the time or date somewhere now. "
"Returns time24 (HH:MM:SS), date (YYYY-MM-DD) and IANA timezone."
),
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string", "description": 'City name, e.g. "Tokyo"'}},
"required": ["city"],
},
}]
def run_tool(name, tool_input):
if name != "get_local_time":
raise ValueError(f"unknown tool {name}")
c = apiverve("worldtime", {"city": tool_input["city"]})["foundCities"][0]
return {k: c[k] for k in ("city", "country", "timezone", "time24", "date")}
question = sys.argv[1] if len(sys.argv) > 1 else "What time is it in Tokyo, and is it already tomorrow there?"
messages = [{"role": "user", "content": question}]
for _ in range(4):
res = client.messages.create(model=MODEL, max_tokens=4096, tools=TOOLS, messages=messages)
messages.append({"role": "assistant", "content": res.content})
if res.stop_reason != "tool_use":
print("".join(b.text for b in res.content if b.type == "text"))
break
results = []
for block in res.content:
if block.type == "tool_use":
out = run_tool(block.name, block.input)
results.append({"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(out)})
messages.append({"role": "user", "content": results})A few choices in that code are deliberate and will come back in later lessons.
- The loop has a hard limit. Four turns is plenty for one tool. An agent loop without a ceiling is a billing incident waiting for an ambiguous prompt.
- The tool returns five fields, not the whole response. The raw World Time response includes ASCII names, ISO3 codes and premium fields. Every byte you return is input tokens on the next request and one more thing the model might misread.
- The API call happens on your server. The model never sees your APIVerve key. It sees a tool name and a result.
- Every
tool_usegets atool_resultwith the same id. The model may ask for several tools in one turn, and the results must all come back together in the next user message.
Make the model use the result, not its prior
Getting the tool called is half the job. The other half is making sure the final answer comes from the result. Two habits help.
Put the provenance in the tool output and ask for it in the answer. If the result includes date and timezone, a system prompt line such as "When you state a time, rate or count from a tool, say where and when it applies" turns those fields into part of the user-visible answer.
Tell the model what to do when the tool and its memory disagree. Models sometimes "correct" a tool result that looks surprising to them, such as a holiday on an unexpected day. A line like "Tool results are authoritative. If one surprises you, report it as returned and say it is unexpected" stops that.
Both lines belong in the system prompt, which you pass on every request:
const system = [
'You answer questions about times, dates, rates and lookups using the tools provided.',
'Never state a current time, date, rate or count from memory; call a tool.',
'When you state a value from a tool, say where and when it applies (city, date, last updated).',
'Tool results are authoritative. If one surprises you, report it as returned and say it is unexpected.',
].join(' ');
const res = await client.messages.create({ model: MODEL, max_tokens: 4096, system, tools, messages });When not to call a tool
Tools cost time, tokens and credits, so the opposite mistake is real too. Stable conceptual knowledge ("what is a working day"), formatting and rewriting text the user already gave you, and arithmetic over numbers already in the conversation do not need a call. If an agent re-fetches the same rate three times in one conversation, the fix is a cache in your tool layer, not a smarter prompt; lesson four covers that.
A useful test before adding a tool: would a careful human expert need to look this up, or check a clock, before answering? If yes, the agent needs a tool. If a human would answer from memory and be right, the model probably can too.
Next: Choose and scope tools, where you decide which tools an agent gets and how to describe them so the model picks the right one.