What’s one thing you learned? What’s still confusing?
Welcome to AI
What is AI? What is machine learning? Start your journey here — no experience needed.
Your First Python Program
Install Python, write your first print() statement, and use the REPL to experiment with code interactively.
What is SQL? Your First Query
Understand what databases are, why SQL matters, and write your first SELECT query. See results instantly in the built-in SQL playground.
Interactive Labs for This Track
Tokenizer
How does AI read? First it breaks text into tokens — type a sentence and see it split into pieces
Embeddings Explorer
Words live in a space where similar meanings are close together — explore king - man + woman = queen
Transformer Attention
Watch data flow through a transformer step by step — the architecture behind ChatGPT
Ask questions, share insights
A pretrained language model only ever does one thing: given a prefix of tokens, predict the next token. Everything else — chat formatting, system prompts, RAG, agents — is a convention layered on top of that single capability. Tool calling is one such convention. It works like this:
{"name": "get_weather", "arguments": {"city": "Paris"}} wrapped in special tokens or a designated field.get_weather(city="Paris") in your runtime.The crucial point: the model never executes anything. It only emits a string the orchestrator agrees to interpret as a command. The "tool" is whatever real code your orchestrator decides to run when it sees the structured payload.
bash in Claude Code, the same one ChatGPT uses to run a plugin, the same one a customer-support bot uses to look up your order. Different surface, identical guts.Three things shifted in the year before this lesson was written:
If you want to build anything that reaches outside the chat window, tool calling is the only bridge. Everything else is plumbing.
What does a tool call actually look like to the model? It depends on which format the model was trained on. Here are the three you will meet in practice.
<tool_call>...</tool_call> wrapper inside the assistant turn.<|im_start|>system
You are a helpful assistant with access to functions. Call them
in JSON inside <tool_call> tags.
<tools>
[{"name": "get_weather", "description": "Current weather for a city",
"parameters": {"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]}}]
</tools>
<|im_end|>
<|im_start|>user
What's the weather in Paris?
<|im_end|>
<|im_start|>assistant
<tool_call>{"name": "get_weather", "arguments": {"city": "Paris"}}</tool_call>
<|im_end|>
<tool_call> is just two adjacent tokens it has seen thousands of times in its supervised-fine-tuning corpus, always followed by a JSON object, always terminated by </tool_call>. The orchestrator scans the generated text for that wrapper, parses the inner JSON, and dispatches.<|python_tag|> token followed by a Python-style call:<|start_header_id|>assistant<|end_header_id|>
<|python_tag|>get_weather.call(city="Paris")<|eom_id|>
<|eom_id|> ("end of message") tells the orchestrator the model is yielding control. The orchestrator parses the call (function.method(kwargs) syntax, not JSON), runs it, and appends a tool-response message.python_tag token simply tells the orchestrator "what follows is a structured call, not prose."The frontier API providers hide the special tokens from you entirely. You send a request like:
{
"model": "claude-sonnet-4-7",
"messages": [{"role": "user", "content": "Weather in Paris?"}],
"tools": [{
"name": "get_weather",
"description": "Current weather for a city",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}]
}
And get back a structured response:
{
"content": [
{"type": "tool_use", "id": "toolu_01abc",
"name": "get_weather", "input": {"city": "Paris"}}
],
"stop_reason": "tool_use"
}
tools list into an in-context prompt (often using a Hermes-style tag set), runs the model, parses the special tokens out of the raw output, and hands you a clean JSON object. The convenience is real, but the underlying mechanism is identical to the open formats. There is no separate "tool-call subsystem" inside the model.A model emits {name: 'get_weather', argments: {'city': 'NYC'}} — note the typo in 'argments'. The orchestrator validates this against the JSON Schema {required: ['arguments']}. Pass or fail?
A base model does not know what a tool call is. The behavior is taught during supervised fine-tuning (SFT) and reinforcement learning from human feedback (RLHF) or direct preference optimization (DPO). The recipe, simplified:
The Toolformer paper (Schick et al. 2023) showed that you can do most of this in a self-supervised way: insert tool calls into a corpus, keep the ones that improve next-token loss on the surrounding text, train on the kept examples. The Gorilla paper (Patil et al. 2023) extended this to a massive catalog of real APIs, training a Llama variant to invoke 1,600+ ML model APIs by name and signature.
The format you use to describe a tool dramatically affects whether the model picks the right one. The de facto standard is JSON Schema:
{
"name": "get_weather",
"description": "Get the current weather conditions for a city. Returns temperature in Celsius, humidity, wind speed, and a short condition string. Use this when the user asks about weather, temperature, rain, or conditions in a specific location.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'Paris' or 'San Francisco'. Country optional but recommended for disambiguation."
},
"units": {
"type": "string",
"enum": ["metric", "imperial"],
"description": "Units for the temperature. Default 'metric'."
}
},
"required": ["city"]
}
}
Four pieces matter, and three of them are prose:
name: a stable identifier the model emits verbatim. Snake_case or camelCase are both fine; the model will copy whatever you give it.description at the top level: the most important field. This is the prose the model uses to decide whether to call this tool at all. A description that is vague ("gets weather data") will lose to a description that gives the model concrete trigger phrases ("Use this when the user asks about weather, temperature, rain, or conditions"). Treat this like prompt engineering — because it is.parameters schema: structural constraints. Used by both the model (to format arguments correctly) and the validator (to reject malformed output).description on each parameter: tells the model how to fill the slot. Concrete examples beat type annotations. "e.g. 'Paris' or 'San Francisco'" is worth ten words of theory.<tools>...</tools> tag the model has been trained on. The model has no notion of "API parameters" — it just sees text describing functions and learns that the right response is structured JSON matching the schema.You have two tools: 'search_internal_docs' (description: 'searches the company wiki') and 'search_web' (description: 'general web search'). User asks 'what is our PTO policy?' The model picks search_web. Why?
Modern frontier models can emit multiple tool calls in a single assistant turn. This is critical for latency: if a user asks "what's the weather in Paris and the stock price of AAPL?", a sequential agent makes two round-trips (call weather, wait, call stock, wait). A parallel-capable model emits both calls at once, the orchestrator runs them in parallel, and the user sees one combined response.
Raw token sequence in the Hermes format:
<|im_start|>assistant
<tool_call>{"name": "get_weather", "arguments": {"city": "Paris"}}</tool_call>
<tool_call>{"name": "get_stock", "arguments": {"ticker": "AAPL"}}</tool_call>
<|im_end|>
tool_use content blocks in the same message. The orchestrator dispatches both, awaits both, and feeds both tool_result blocks back in the next user message — paired by tool_use_id.<tool_call> tags in the same assistant turn during SFT will not emit them at inference, no matter how cleverly you prompt. Claude 3+ and GPT-4o/4-Turbo support this; older models often do not. Llama 3.1 supports it; Llama 3.0 does not.Why bother training models with parallel tool calls instead of just looping serially?
Training the model to emit valid tool calls works most of the time. But "most of the time" is not good enough when an agent makes 100 tool calls per task and one malformed call breaks the whole chain. The robust fix is to enforce structure at decode time — to never let the model sample an invalid token in the first place.
The mechanism is called grammar-constrained decoding, and it works at the logit level:
{"name": "get_weather", "arguments": {"ci and the schema demands the next character be one of the letters t-z (to spell city), the decoder will mask every other token down to -inf. The model literally cannot sample a typo.The major implementations:
response_format: {"type": "json_schema"} and Anthropic's tool-call enforcement both run a constrained decoder behind the scenes. If you are calling the API, your tool-call JSON is essentially guaranteed to be syntactically valid.Run that and watch each failure mode get caught by exactly the layer responsible for it: bad JSON by the parser, missing fields by the schema validator, unknown tools by the registry, well-formed calls by the dispatcher. This is the entire backend of a tool-calling system in 80 lines of Python. Everything you see in MCP, in the OpenAI SDK, in Claude's runtime, is a more elaborate version of the same loop.
What is the fundamental difference between Anthropic's MCP and OpenAI's tool-call API?
Tool calling is a primitive. Real agents compose it into one of three shapes.
The simplest loop:
user → model → tool_call → orchestrator runs tool → tool_result → model → final answer
Two model invocations, one tool execution. This is what a typical "look up something for the user" assistant does. The orchestrator code is roughly 30 lines.
The agent loop:
while not done:
response = model.complete(messages)
if response.tool_calls:
for call in response.tool_calls:
result = run(call)
messages.append(tool_result(call.id, result))
else:
return response.content # final answer
tool_use content blocks. This is what every general-purpose agent (Claude Code, Cursor, Devin) actually runs. The ReAct paper (Yao et al. 2022) gave this pattern its name; today nobody calls it ReAct any more, but the structure is universal. See track-09-agents/react-pattern for the historical write-up.mcp__filesystem__read, mcp__postgres__query, mcp__github__create_pr, …), passes that catalog to the model, and lets the same ReAct loop drive a much larger surface area. This is how Claude Desktop, Cursor, and Zed compose tools from independent vendors.Four failure modes show up in production at high enough frequency to deserve names.
{city: string} and the model emits {city: "Tokyo", language: "ja"}. Sometimes catastrophic: the model emits {user_id: "<UNKNOWN>"} because it could not actually find a user_id.additionalProperties: false is set, the validator rejects the call and you either retry with the error message in context ("the field language is not allowed; valid fields are city") or use constrained decoding so the typo is unsamplable.search_web when it should have picked search_internal_docs, or create_pr when the user asked to describe what a PR would do, not actually open one. Tool descriptions are pattern-matched against the user message; descriptions that overlap semantically will get confused.clarify(question) that lets the model ask the user before committing to a destructive action.The model keeps calling the same tool with slight argument variations, never producing a final answer. Common when the tool result is ambiguous or when the model thinks the user is still waiting for more detail.
max_iterations cap (most production agents stop at 20-50 tool calls). Detect repeated (name, hash(arguments)) pairs and inject a system message: "you have called this tool with these arguments already; the result was X. Either use that result or call a different tool." Time budget on total wall-clock.delete_file(path="/etc/passwd") because in training data filesystems usually allow delete; your sandbox does not. Calls web_search when you only wired up search_docs.{"error": "denied"} but {"error": "delete is disabled in this sandbox; you can only read files via read_file()"}. The model will incorporate that into its next attempt.A short tour of the research that built today's tool-calling models.
(user_intent, correct API call), fine-tuned Llama. Showed that with retrieval-augmented training, models can correctly call APIs they have never seen during training, just from their documentation.<|python_tag|> token. Supports parallel calls, built-in tools (brave_search, wolfram_alpha, code_interpreter), and custom user-defined tools.tool_use / tool_result), parallel calls supported since 3.5, computer use (clicking, typing, screen reading) released October 2024.The two big providers expose tool calling with nearly identical semantics under slightly different field names.
| Concept | OpenAI | Anthropic |
|---|---|---|
| Tool list (request) | tools: [{type:"function", function:{name, description, parameters}}] | tools: [{name, description, input_schema}] |
| Tool call (response) | message.tool_calls: [{id, function:{name, arguments}}] | content: [{type:"tool_use", id, name, input}] |
| Tool result (request) | role:"tool", tool_call_id, content | role:"user", content: [{type:"tool_result", tool_use_id, content}] |
| Parallel calls | Yes, default on | Yes, default on |
| Force a specific call |
Functionally the same; cosmetically different. Both convert the JSON Schema into an in-context prompt under the hood. Both run constrained decoding on the tool-call output to guarantee well-formed JSON. Both ship parallel calls. The interesting differences are:
message.content and tool calls in a parallel tool_calls array.parallel_tool_calls: false as a request-level flag if you want to force serial calling. Anthropic does not have a native off-switch (you would set tool_choice per round instead).cache_control on tool definitions so a long tool catalog can sit in the prompt cache.If you are starting from scratch in 2026, write your orchestrator against an abstraction layer (LiteLLM, Anthropic SDK with its OpenAI-compat shim, or your own thin wrapper) and treat the field-name differences as a serialization concern.
{"city": "<string>"} — exactly enough to constrain get_weather arguments.The state set is tiny and the alphabet is small, but everything important about Outlines, xgrammar, lm-format-enforcer, and OpenAI's JSON-mode is in those 40 lines. Real implementations:
$ref boundary, one alphabet per regex pattern, etc.),None of those are conceptual changes. They are engineering optimizations on the same idea: at every decode step, mask out tokens the grammar forbids, then sample.
Putting everything together — here is what actually happens when a user asks Claude "what's the weather in Paris?" with one tool available:
messages list with the user message plus a tools list containing the get_weather schema.tools into a system-prompt insertion (<tools>...</tools>) using the format Claude was trained on.<tool_use_start> special token. A constrained decoder confirms valid emission and the model continues into the structured payload.get_weather. JSON-mode is on; the only sampleable tokens are those legal at the current FSM state.tool_use content block and stops generation with stop_reason: "tool_use".input against the schema once more for safety, runs , and gets back .Two model invocations, one tool call. Everything between is plumbing. This is the loop. Once you have written it once, every agent framework on the planet looks the same.
track-09-agents — tool-use, react-pattern, mcp, building-an-agent, code-agents, computer-use-agents, multi-agent-systems — assumes the mechanics in this lesson. If a single pattern matters, it is this:catalog (JSON Schema) ──▶ model ──▶ structured call ──▶ runtime ──▶ result ──▶ model
(constrained ↓
decoding) (loop or stop)
Memorize that pipeline; everything else is decoration.
tool_choice: {function:{name}}tool_choice: {type:"tool", name} |
| Structured response (no tools) | response_format: {json_schema} | tool with one-call output via tool_choice |
get_weather(city="Paris"){"temp_c": 14.2, ...}tool_use block appended and a new user message containing a matching tool_result block keyed by tool_use_id.