From APIs to Agents: Function Calling and the ReAct Loop
How a plain chat completion becomes an agent — function/tool calling, the ReAct think-act-observe loop, and why stateless context makes prompt_tokens balloon.
This article has an interactive companion
Don't just read it — step through it interactively, one state change at a time.
A language model, on its own, can only do one thing: read a pile of text and predict the next token. It cannot check the weather, query your database, or add two numbers reliably. An "agent" is not a smarter model — it is the same model wired into a loop where it can ask for real code to run and then read the result.
The whole trick is a data format. The model emits a small blob of JSON that says "call search with {query: "Einstein"}." Your code runs the actual function, gets a real answer, and hands it back as the next message. Repeat that until the model says it's done. That is function calling, and stacking it inside a reasoning loop is the ReAct pattern.
This guide walks the arc YeetCode's companion deep dive covers: how we got from autocomplete to goal-driven agents, exactly how tool calling works at the message level, the THOUGHT → ACTION → OBSERVATION loop traced on a real example, and the catch nobody warns you about — the model has no memory, so every turn re-sends the entire transcript and your token bill climbs quadratically.
How did we get from autocomplete to agents?
The 2021–2025 arc is best read as one question: who holds the loop?
- Autocomplete (2021). You hold everything. The model completes a line; you decide what to do with it. It has no goal and no tools.
- Chat (2023). The model writes a whole answer when asked, but you still hold the loop — you plan the request, copy code out to run it, and check the result by hand. Writing got automated; planning, running, and checking did not.
- Agent (2025). The model holds the Plan / Write / Run / Check cycle. Given a goal, it decides which tool to call, your harness executes it, and the result feeds back automatically. The human sets the objective and inspects the trail.
The deep dive frames this as a countable Plan/Write/Run/Check split rather than a made-up "autonomy percentage." Autocomplete and Chat share the exact same split — Write is the model's, Plan/Run/Check stay yours — so the jump from chatbot to agent isn't writing getting smarter, it's the model finally taking over Plan, Run, and Check too: deciding the next step, triggering real execution, and reading what came back, in a loop you no longer sit inside of.
What is function calling, really?
An agent is LLM + Tools + a Loop. Function calling is the bridge that grows hands on a plain chat completion, and it is more constrained than it sounds. The model never runs your code. It cannot invent a tool. It can only request a call to a function you declared up front as a JSON schema.
The mechanics mirror the Anthropic Messages API exactly. You send the user's question plus a tools array describing each function's name and input shape. If the model decides it needs one, the response comes back with stop_reason: "tool_use" and a content block naming the tool and its arguments:
// 1. you offer tools + the user's question
const res = await client.messages.create({
model,
tools, // JSON schemas you declared
messages: [{ role: "user", content: question }],
});
// 2. the model REQUESTS a call — it emits JSON, it does not run anything
// res.stop_reason === "tool_use"
const call = res.content.find((b) => b.type === "tool_use");
// call = { id: "toolu_01…", name: "search", input: { query: "Einstein" } }
// 3. YOUR code runs the real function
const result = await search(call.input.query);
// 4. feed the real return value back as a tool_result message
messages.push({ role: "assistant", content: res.content });
messages.push({
role: "user",
content: [{ type: "tool_result", tool_use_id: call.id, content: result }],
});
// 5. call the model again with the appended history → it reads the resultTwo boundaries matter. The tool_use block is just data — nothing executes until step 3. And the tool_result must carry the same tool_use_id the model generated, so a multi-tool turn matches each answer to the right request. Get either wrong and the model hallucinates a result or loses track of which call you answered.
The ReAct loop: THOUGHT, ACTION, OBSERVATION
One tool call is a party trick. Real tasks need several, chained, with the model deciding each next step from what the last one returned. That structure is ReAct — Reasoning + Acting — from Yao et al., 2022. The model interleaves three phases and repeats them until it can answer:
THOUGHT: reason about what you know and what you still need
ACTION: call exactly one tool with concrete arguments
OBSERVATION: read the tool's REAL return value
… loop back to THOUGHT …
FINAL_ANSWER: give the answer, grounded in the observationsThe THOUGHT is the model talking to itself in plain text, which is why ReAct agents feel legible — you can read the reasoning that led to each call. The ACTION is a tool_use block. The OBSERVATION is your tool_result. Crucially, the OBSERVATION is not generated by the model; it is computed by real code. When the tool multiplies two numbers or measures a distance, the model reads a fact it could not have reliably produced itself.
A well-built loop also has an escape hatch: a max iteration count (the deep dive's agent caps at 6) and an honest HALT when a tool returns "no match." An agent that can't find a fact should say so, not guess.
Walkthrough: "Who lived longer, Einstein or Newton?"
Here is the exact trace the interactive agent runs for that goal, against its small local corpus. Einstein: born 1879, died 1955. Newton: born 1643, died 1727. The model plans; the four tools (search, lifespan, calculate, get_distance_km) genuinely execute, so every OBSERVATION is computed, not narrated.
| # | THOUGHT (model reasons) | ACTION (tool the model requests) | OBSERVATION (real return) |
|---|---|---|---|
| 1 | Need Einstein's dates — search the knowledge base | search({ query: "Einstein" }) | Einstein: born 1879, died 1955 (Physicist) |
| 2 | Compute Einstein's lifespan from the real dates | lifespan({ born: 1879, died: 1955 }) | 76 years (1955 − 1879) |
| 3 | Need Newton's dates — search again | search({ query: "Newton" }) | Newton: born 1643, died 1727 |
| 4 | Compute Newton's lifespan | lifespan({ born: 1643, died: 1727 }) | 84 years (1727 − 1643) |
| 5 | Have both (76, 84) — take the difference | calculate({ expression: "84-76" }) | 8 years |
FINAL_ANSWER: Newton lived longer — 84 years vs 76, a difference of 8 years.
Notice what the model never did: it never asserted that Einstein lived 76 years — it called lifespan and the tool returned the subtraction. Reasoning picks which facts to fetch and how to combine them; tools supply the facts. Ask "Einstein or Hawking?" instead and step 3's search returns no match — the loop HALTs honestly ("Hawking is not in the knowledge base") rather than inventing a birth year.
Why does context keep growing? The cost of statelessness
The model has no memory between calls. Every messages.create is a blank slate, so the harness re-sends the entire transcript — system prompt, every prior THOUGHT, ACTION, and OBSERVATION — on every single turn.
Walk the Einstein/Newton trace above and count what gets sent as prompt_tokens:
Turn 1: system
Turn 2: system + thought1 + action1 + observation1
Turn 3: system + turn1 + turn2 messages
Turn 4: system + turn1 + turn2 + turn3 messages
Turn 5: system + everything before itThe system prompt gets billed on turn 1, then billed again on turn 2, again on turn 3 — a single turn's prompt_tokens grows only linearly (it adds one more turn of history), but because every turn re-bills everything before it, the cumulative prompt_tokens summed across the whole session climbs roughly quadratically with the number of turns. A 30-step coding agent can spend most of its token budget re-reading its own history.
That single fact is why "context engineering" (summarizing old turns, dropping stale observations, retrieving only what's needed) and prompt caching exist. Cache the stable prefix and the provider charges the full rate once, a fraction on every reuse — same transcript, the difference between paying for each token once versus on every turn it survives.
What to remember
- An agent is LLM + Tools + a Loop. A chatbot already writes; an agent also takes over Plan, Run, and Check — deciding the next step, triggering real code, reading the result.
- Function calling is a data contract: the model emits a
tool_useJSON block; your code runs the function and returns atool_resultwith the matchingtool_use_id. The model never executes anything. - ReAct = THOUGHT → ACTION → OBSERVATION, repeated until FINAL_ANSWER. Observations are computed by tools, so the answer is grounded in real values, not the model's guesses.
- Always cap iterations and HALT honestly on a missing fact. A grounded agent says "I don't know" instead of hallucinating.
- The model is stateless, so every turn re-sends the whole transcript. Per-turn
prompt_tokensgrows linearly, but the cumulative prompt_tokens billed over the session grows quadratically — the reason context engineering and prompt caching are load-bearing, not optional.
Keep going
- Agent = Model + Harness: Context Engineering and Evals — what wraps the loop in production and how you measure whether it works.
- AI Agent Memory: Beyond RAG to State That Persists — how agents beat statelessness with durable memory instead of ever-growing transcripts.
- LangGraph Explained: Agents as State Machines — turning the ReAct loop into an explicit, inspectable graph.
- What Is RAG? Retrieval-Augmented Generation from the Ground Up — the retrieval half of grounding, complementary to tool calling.
Want to watch it run? Open the interactive deep dive and drive a real ReAct agent: type any goal, watch it plan, call real tools, and compute an answer — then break it with an out-of-corpus name and see the token bill climb turn by turn.
FAQ
What is the difference between function calling and an AI agent?
Function calling is one primitive: the model requests a single tool by emitting JSON, and your code runs it. An agent is the loop built on top of that primitive. It takes a goal, calls tools repeatedly, reads each real result, and decides the next step until the goal is met. Put simply, function calling gives the model hands; the agent loop lets it use those hands more than once without a human in between.
Does the language model actually run the tool code?
No, and this is the most common misunderstanding. The model only predicts a tool_use JSON block naming a function and its arguments — it never executes anything. Your harness reads that block, runs the real function in your own environment, and sends the return value back as a tool_result message. This separation is what makes tools safe and deterministic: the model can request calculate("84-76"), but a real arithmetic evaluator produces the 8, not the token predictor.
What are the THOUGHT, ACTION, and OBSERVATION steps in ReAct?
They are the three repeating phases of the ReAct loop from Yao et al., 2022. THOUGHT is the model reasoning in plain text about what it knows and what it still needs. ACTION is a single tool call with concrete arguments. OBSERVATION is the tool's real return value, computed by your code rather than generated by the model. The loop repeats — thought, action, observation — until the model has enough grounded facts to emit a FINAL_ANSWER.
Why do agent token costs grow so fast?
Because the model is stateless: it remembers nothing between API calls. To continue a task, your harness re-sends the entire transcript — system prompt plus every prior thought, action, and observation — on every turn. The system prompt alone gets billed once per turn, so the cumulative prompt_tokens grows roughly quadratically with the number of steps. Prompt caching and context engineering (summarizing or dropping old turns) exist specifically to attack this tax.
When should I build an agent instead of just prompting a model?
Reach for an agent when the task needs facts the model doesn't reliably have — live data, exact arithmetic, database lookups, code execution — or when it takes several dependent steps where each step's tool result shapes the next. If a single, self-contained prompt gets a correct answer, an agent only adds latency, cost, and failure modes. The signal for going agentic is "the model must act on the world and read what came back," not "the answer is hard."
Make it stick: run this one yourself
Don't just read it — step through it interactively, one state change at a time.