Search Tech Journey

Find topics, journeys and posts

6-month learning plan120 / 130
back to blog
llmadvanced 50m read

S120 · LLM Agents — Function Calling, Tools, Planning

How an LLM stops just talking and starts doing — the function-calling protocol, tool schemas, planning strategies (ReAct, Plan-Execute, Reflexion), and the failure modes every agent framework fights.

LLMsM14 · LLMs & Applications· Session 120 of 130 90 min

🎯 Build a small LLM agent with function calling that plans, executes tools, handles errors, and stops when done — using the same primitives every production framework uses.

Why this session exists

"Agents" is the most-hyped and least-understood word in the LLM stack. Under the hood every agent — LangChain, LlamaIndex, CrewAI, AutoGen, OpenAI's Assistants, Anthropic's tool use — is the same three-part loop: LLM plans, LLM calls a tool, we execute the tool and hand back the result. This session strips agents down to the primitives, teaches you the failure modes (infinite loops, tool schema mismatch, planning drift), and shows you exactly what a production agent needs beyond the demo.

You will be able to
  • Explain function calling / tool use as a protocol on top of a chat completion, in one paragraph.
  • Write a JSON schema for a tool that the LLM will call correctly on the first try.
  • Implement a ReAct-style agent in ~60 lines using OpenAI's tool_calls or Anthropic's tool_use.
  • Diagnose the four failure modes: schema mismatch, infinite loop, hallucinated tools, planning drift.
  • Pick between ReAct, Plan-and-Execute, and Reflexion patterns for a new problem.

Prerequisites

  • S116 · Prompting — you need the ReAct pattern mental model.
  • S113 · LLM Sampling — tool calls should almost always run at temperature=0.
  • S118 · Retrieval — retrieval is often ONE of an agent's tools.


(a) Intuition · 5 min

A very literal executive assistant
🌍 Real world

Picture an executive assistant who is smart, cheap, and infinitely patient — but has no arms. They can read, they can talk, they can plan. They cannot pick up a phone, open a browser, or file a document.

You solve this by giving them a whiteboard and a list of things a coworker will do for them if written correctly: 'CALL_CLIENT(name=Alice, message=...) → ok/failed', 'LOOKUP_ORDER(id=A-4821) → {…}'. The assistant writes commands on the whiteboard, the coworker executes each one, writes back the result, and the assistant reads it and decides the next step.

💻 Code world

An LLM agent is that assistant. The whiteboard is the tool-call protocol (OpenAI's tool_calls, Anthropic's tool_use, Google's function_calling — all the same idea). Your code is the coworker. The LLM writes JSON like {"name":"get_weather","arguments":{"city":"Tokyo"}}; you parse it, run the function, and send the result back as the next message.

Loop until the LLM says 'I'm done' (usually by generating a final assistant message with no more tool_calls). That's an agent. Everything else — LangChain, LlamaIndex, CrewAI, AutoGen — wraps this loop with retries, logging, and pretty tracing.

The five moving parts of every agent
  • The LLM — decides which tool to call and with what arguments (planning).
  • The tools — Python functions you registered, each with a JSON schema.
  • The controller loop — your code that parses tool_calls, executes them, feeds results back.
  • The stopping rule — max_steps, or 'no more tool_calls', or 'called finish()'.
  • The memory — the growing message history (system + user + tool_call + tool_result + …).

Timeline of agent-making

  1. 2022
    ReAct paper · Yao et al.
    Reasoning + Acting in prompts. First reliable pattern for LLM tool use.
  2. 2023
    OpenAI function_calling
    GPT-3.5/4 shipped native tool_calls API. Made agents production-usable without regex parsing.
  3. 2023
    LangChain / LlamaIndex peak
    Framework explosion — thousands of stars, hundreds of integrations. Also: 'agents are unreliable' meme peaks.
  4. 2024
    Anthropic tool_use + Claude 3
    Cleaner tool-use protocol; strong reasoning makes multi-step agents more reliable.
  5. 2024
    Reflexion + Plan-Execute
    New patterns — plan first, execute, then reflect and re-plan. Better on hard multi-step tasks.
  6. 2025
    Reasoning models (o1, Claude thinking)
    Long CoT baked into the model. Agents get more reliable because the LLM plans internally before emitting a tool call.

(b) Visual walkthrough · 15 min

The agent loop, drawn

The tool schema — what the LLM actually sees

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather for a city. Returns temperature in Celsius and a short condition string.",
    "parameters": {
      "type": "object",
      "properties": {
        "city": { "type": "string", "description": "City name, e.g. 'Tokyo' or 'San Francisco'" },
        "units": { "type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius" }
      },
      "required": ["city"]
    }
  }
}
1name
Descriptive name

get_weather beats gw. The LLM decides which tool to call based on name + description; make both self-explanatory.

2desc
Concise description with an example

'Get the current weather for a city.' + example city names. LLMs pattern-match hard on examples in descriptions.

3schema
JSON Schema for parameters

Use enum where possible; the LLM is much better at picking from a fixed set than free-typing.

4required
Mark required parameters

The LLM will hallucinate values for missing required params instead of asking. Make required explicit.

5return
Return string or JSON, not exceptions

Tool failures should return {'error': '...'} strings, not throw. The LLM needs to READ the failure to plan around it.

The three planning patterns

ReAct (Reasoning + Acting)

Think → Act → Observe · loop

  • The default; every OpenAI/Anthropic tool_call loop is ReAct
  • Cheap: one LLM call per step
  • Drifts on tasks with >5–7 steps
  • Best for reactive, well-scoped tasks
Plan-and-Execute

Make a full plan first, then execute steps

  • LLM first outputs a list of steps ('1. Search for X. 2. Read result. 3. Compute Y.')
  • Executor runs each step; can re-plan when a step fails
  • Better on multi-hop research or code tasks
  • Costs more tokens up front; wins on complex tasks
Reflexion

Execute, then critique + retry

  • After an attempt, an LLM critique produces a 'lessons learned' note
  • Next attempt uses that note as memory
  • Big gains on coding/math benchmarks (HumanEval +20 pts)
  • Expensive: N×–3N× cost of a single attempt

The message log — how it grows

What ends up in the messages array

system
'You are a helpful assistant with access to the following tools. Always call finish() when done.'
system
user
The original request.
user
assistant (with tool_calls)
'I need weather. Calling get_weather(city="Tokyo").' + the machine-readable tool_calls array.
assistant
tool (result)
'{"temp_c": 22, "condition": "cloudy"}' — one per tool call, with matching tool_call_id.
tool
assistant (with more tool_calls or final answer)
'Converting to F: 71.6. Final: 72°F, cloudy.'
assistant

Common misconception
✗ What most people think

"An agent that can call tools and loop will eventually solve the task. If it fails, I need a better model or more iterations."

✓ What is actually true

Error compounds multiplicatively across steps. At 95% per-step reliability, a 20-step trajectory succeeds about 36% of the time; at 10 steps it is 60%. Adding iterations without adding verification makes things worse, because a wrong intermediate result is fed forward as fact and the agent then reasons confidently from a corrupted premise. Reliability comes from shortening trajectories and checking steps, not from more looping.

Why the myth is so sticky

Because the myth is true in the demo regime everyone starts in: 3-step tasks, where 0.95³ is 86% and failures look like flukes that a retry fixes. The exponential only bites past roughly ten steps, which is exactly where useful autonomous work begins. It is also masked by the fact that the agent's narration stays fluent and confident even after it has gone off the rails.

Prove it to yourself

Compute the reliability budget for your actual trajectory length before designing anything:

for p in (0.90, 0.95, 0.99):
    for n in (5, 10, 20, 50):
        print(f'p={p}  steps={n:2d}  success={p**n:.3f}')

# Then invert it: what per-step reliability do you need for
# 90% end-to-end success at n steps?
import math
for n in (5, 10, 20, 50):
    print(f'{n:2d} steps needs p >= {0.90 ** (1/n):.4f}')
# 50 steps needs 99.8% per step. That is the real constraint.
From first principles
Start with the question

Why does ReAct interleave reasoning with acting, rather than planning fully upfront and then executing? Plan-then-execute is how humans do project work.

  1. 1
    A plan is a sequence of actions chosen under the information available when the plan was written.
    forced by · that is what planning means — committing to future choices now
  2. 2
    In a tool-using setting, most of the information needed to choose action k is produced by actions 1 through k−1. The search result determines which page to fetch; the query result determines which filter to apply.
    forced by · the environment is only partially observable, and tools are how you observe it
  3. 3
    So a full upfront plan must guess the outcomes of every intermediate step. Each guess has a failure probability, and the guesses compound exactly like the step reliability above.
    forced by · an unobserved outcome is a prediction, and predictions about tool results are unreliable
  4. 4
    Worse, a plan produced before observation is brittle in a specific way: when step 3 returns something unexpected, steps 4–10 are now premised on a false state, and the agent has no mechanism to notice because it is following the plan rather than reading the world.
    forced by · plan-following decouples action selection from current observation
  5. 5
    Interleaving fixes this by making every action conditional on all prior observations: the "plan" is recomputed at each step from the actual state, so an unexpected result changes the next action immediately rather than after ten wasted steps.
    forced by · replanning at every step is the cheapest possible form of error detection
⇒ Therefore

Therefore ReAct is closed-loop control and plan-then-execute is open-loop control. Closed loop wins exactly when the environment is unpredictable, which is always true for tool use over real systems.

And note what this predicts: plan-then-execute should win back its advantage when the environment is predictable and steps are expensive — because then upfront planning enables parallelism and avoids paying for a reasoning pass before every action. That is precisely the regime where plan-and-execute architectures are used, and why the two patterns coexist rather than one replacing the other.

Mental modelA control loop with a lossy sensor

An agent is a control loop: observe → decide → act → observe. The LLM is the controller, tools are the actuators, and tool outputs are the sensors. What makes it hard is that the controller is stochastic and the sensor readings enter as text in a context window, where they are indistinguishable from the agent's own speculation.

Everything that makes agents reliable is a control-engineering move: shorten the loop, add feedback, bound the state, and put a hard limit on the actuators.

  • Reliability is pn. Reduce n (fewer steps) before trying to raise p (better model).
  • Tool descriptions are the API contract the model programs against. Ambiguous parameter descriptions are the most common cause of wrong tool calls — far more common than wrong tool choice.
  • Tools must return errors as actionable text ("date must be YYYY-MM-DD, got 03/04/24"), not stack traces. The error message is the only feedback channel the controller has.
  • Always bound the loop: max steps, max wall time, max spend. An unbounded agent loop is an unbounded bill and an unbounded blast radius.
🔔 Fires when you see

Fire this model the moment you see: an agent looping on the same failing call · a task requiring more than ten tool calls · tool outputs dumped raw into context · an agent with write access and no confirmation step · someone proposing "just let it retry".

The tradeoff

You are automating a multi-step workflow. Hard-coded pipeline, constrained agent with a fixed tool set, or fully autonomous agent?

Hard-coded pipeline (LLM at specific steps only)
+ you gain deterministic control flow, so it is testable, debuggable and observable with ordinary engineering tools; cost and latency are predictable; failures land in one identifiable stage
− you pay every branch must be anticipated and written; unhandled inputs fall through; and adapting to a new case is a code change and a deploy
pick when the workflow is known and stable, and volume is high enough that per-run reliability and cost matter more than flexibility — which describes most production automation
Constrained agent (fixed tools, bounded steps, verified outputs)
+ you gain handles genuine variation in how a task must be accomplished while keeping the blast radius fixed by the tool set; far fewer branches to write than a pipeline covering the same input space
− you pay non-deterministic, so testing needs statistical evals rather than assertions; observability requires purpose-built tracing; and cost per run varies with trajectory length
pick when inputs vary in structure but the space of legitimate actions is small and enumerable — support triage, data lookup across a few systems, structured research
Fully autonomous agent (open tool use, self-directed)
+ you gain handles tasks nobody anticipated, including ones that require discovering what the task even is; the only option when the action space cannot be enumerated in advance
− you pay compounding error makes long trajectories unreliable; cost is unbounded without hard caps; and it is extremely difficult to test, audit or reason about — you cannot enumerate what it might do
pick when exploratory or research work with a human reviewing output, and where a wrong action is cheap to undo
What a senior engineer actually does

Push work down this list, not up. Anything you can express as a pipeline should be one, because determinism is worth more than flexibility in anything you have to operate at 3am. Use the agent for the part of the problem that is genuinely variable, and wrap it in deterministic scaffolding on both sides.

The concrete design rule that follows from pn: every step you can move out of the agent's trajectory and into fixed code raises end-to-end reliability multiplicatively. A ten-step agent reduced to a three-step agent inside a pipeline is not a small optimisation — at 95% per step it takes you from 60% to 86%.


(c) Hands-on · 25 min

Build a real agent with 3 tools and a step limit. Uses OpenAI's tool_calls, but the pattern maps 1:1 to Anthropic tool_use.

"""mini_agent.py — a ~120-line LLM agent with function calling.
 
Requires: pip install openai
Set OPENAI_API_KEY. Model can be gpt-4o-mini for cheap smoke tests.
"""
from __future__ import annotations
import json
import os
from openai import OpenAI
 
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
MODEL = "gpt-4o-mini"
MAX_STEPS = 8
 
 
# ---------- 1. Tools — plain Python functions ----------
def get_weather(city: str, units: str = "celsius") -> dict:
    """Fake weather — swap for a real API in prod."""
    fake = {"tokyo": 22, "london": 12, "san francisco": 17}.get(city.lower())
    if fake is None:
        return {"error": f"unknown city {city!r}"}
    temp = fake if units == "celsius" else fake * 9 / 5 + 32
    return {"temp": temp, "units": units, "condition": "partly cloudy"}
 
 
def convert_temp(value: float, from_unit: str, to_unit: str) -> float:
    if from_unit == to_unit:
        return value
    if from_unit == "celsius" and to_unit == "fahrenheit":
        return value * 9 / 5 + 32
    if from_unit == "fahrenheit" and to_unit == "celsius":
        return (value - 32) * 5 / 9
    return float("nan")
 
 
def finish(answer: str) -> str:
    """Emit the final answer and stop."""
    return answer
 
 
TOOLS_TABLE = {
    "get_weather":  get_weather,
    "convert_temp": convert_temp,
    "finish":       finish,
}
 
# ---------- 2. JSON schemas the LLM sees ----------
TOOLS_SCHEMA = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city. Returns temperature and condition.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city":  {"type": "string", "description": "City name, e.g. 'Tokyo'"},
                    "units": {"type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius"},
                },
                "required": ["city"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "convert_temp",
            "description": "Convert temperature between celsius and fahrenheit.",
            "parameters": {
                "type": "object",
                "properties": {
                    "value":     {"type": "number"},
                    "from_unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                    "to_unit":   {"type": "string", "enum": ["celsius", "fahrenheit"]},
                },
                "required": ["value", "from_unit", "to_unit"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "finish",
            "description": "Return the final answer to the user and stop. ALWAYS call this when done.",
            "parameters": {
                "type": "object",
                "properties": {"answer": {"type": "string"}},
                "required": ["answer"],
            },
        },
    },
]
 
 
# ---------- 3. The controller loop ----------
def run_agent(user_msg: str) -> str:
    messages = [
        {"role": "system",
         "content": ("You are a concise assistant with tools. "
                     "Reason briefly, call tools when useful, and ALWAYS call "
                     "finish(answer=...) with your final answer. Do not narrate.")},
        {"role": "user", "content": user_msg},
    ]
    for step in range(MAX_STEPS):
        resp = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=TOOLS_SCHEMA,
            tool_choice="auto",
            temperature=0,
        )
        msg = resp.choices[0].message
        messages.append(msg)                # keep assistant turn in history
 
        if not msg.tool_calls:
            print(f"[step {step}] LLM stopped without finish; returning content")
            return msg.content or "(empty)"
 
        for tc in msg.tool_calls:
            name = tc.function.name
            try:
                args = json.loads(tc.function.arguments)
            except json.JSONDecodeError as e:
                result = {"error": f"invalid JSON arguments: {e}"}
            else:
                fn = TOOLS_TABLE.get(name)
                if fn is None:
                    result = {"error": f"unknown tool {name!r}"}
                else:
                    try:
                        result = fn(**args)
                    except TypeError as e:
                        result = {"error": f"bad args: {e}"}
 
            print(f"[step {step}] {name}({args}) -> {result}")
 
            if name == "finish":
                return args["answer"]
 
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": json.dumps(result),
            })
    return "(hit max_steps without finish)"
 
 
if __name__ == "__main__":
    for q in [
        "What's the weather in Tokyo, and what is that in Fahrenheit?",
        "Is it warmer in London or San Francisco right now (report in F)?",
        "What's the weather on Mars?",       # should gracefully say unknown
    ]:
        print(f"\n=== {q}")
        print(">>", run_agent(q))

Anatomy of the agent

What each block does

Tools as plain Python functions
Nothing fancy. The framework is a dict {name → callable}. This is exactly what LangChain wraps.
tools
JSON schemas per tool
The LLM literally sees these strings. Descriptions and enums here determine calling accuracy. Bad schemas = bad agent, no matter the model.
schema
tool_choice='auto', temperature=0
'auto' lets the LLM decide when to call vs answer. temperature=0 = deterministic tool selection (crucial for reliable agents).
sampling
Loop with max_steps
Every step: LLM call → parse tool_calls → run tools → append results → repeat. Stop when finish() is called or max_steps hit.
loop
Errors returned as {'error': msg}, not raised
The LLM needs to READ the failure to plan around it. Raising kills the loop; returning a message lets the model try again with different args.
errors
finish() as an explicit stop
Makes stopping deterministic. Without it, models sometimes ramble past a valid answer or over-call tools.
stop
Try itBreak the agent, then fix it

Try three experiments:

# 1. Set MAX_STEPS = 2. Watch agents get truncated.
# 2. Delete the 'ALWAYS call finish' line from system. Some queries hang.
# 3. Change get_weather to raise ValueError instead of returning {"error":...}.
#    The loop crashes. That's why tools return, not raise.

Each experiment is 30 seconds and teaches a durable lesson about production agents.

💡 Hint · Remove the description from the finish() tool and rerun. The LLM will often forget to call it and just print a chat message — hitting your 'stopped without finish' fallback. Restore the description, or move it into the system prompt, and see how much more reliable stopping becomes.

(d) Production reality · 15 min

War story OpenAI · function calling launch· 2023millions of API customers within weeks
🔥 What broke

Pre-function-calling, developers coaxed GPT-3.5 to emit JSON with prompt tricks and regex parsers. Every third response was 'Sure! Here's the JSON: {...' — parseable by luck.

🧯 The fix

OpenAI shipped native function_calling: JSON schema in, guaranteed structured tool_call out. Under the hood it's constrained decoding — token probabilities are masked to enforce schema.

Reliability jumped from ~85% to >99% on tool calls. Every LLM provider has since copied the interface.

🎓 Lesson to steal
Function calling isn't 'the LLM figuring out JSON' — it's the runtime forcing structured output via constrained decoding. That's why you should always use the native API rather than regex-parsing free text.
Post-mortem
War story Common agent failure — the infinite tool loopevery framework's GitHub issues
🔥 What broke

An agent given a search tool and a browse tool searches for a topic, browses the top result, gets confused, searches again with slightly different terms, browses, gets confused, searches again... 40 minutes and $8 later.

🧯 The fix

Non-negotiable production controls: max_steps (5–10 typical), max_wall_clock (60s), max_cost (measure input+output tokens). Log every step. Any agent without all three will burn money in production within a week.

Anthropic's 'Building Effective Agents' post makes this the first recommendation: prefer explicit workflows over open-ended agents unless dynamic planning is really required.

🎓 Lesson to steal
Every agent needs three limits: steps, time, cost. Log every step. 'Autonomous' in production means 'autonomous within a fence you built'.
Post-mortem
War story Anthropic engineering blog · workflows vs agents· 2024internal guidance published externally
🔥 What broke

Teams reached for 'agents' as a default for every new problem. Most were slower, less reliable, and more expensive than a simple deterministic workflow that hardcoded the sequence of steps.

🧯 The fix

Anthropic's public advice: use workflows (predefined step sequences with LLM calls at each step) whenever the flow is known. Reserve agents for problems where the sequence genuinely depends on runtime decisions — dynamic routing, self-correction, open-ended research.

🎓 Lesson to steal
'Agent' is not automatically better than 'workflow'. Determinism, testability, and cost predictability are worth a lot. Start with a workflow; escalate to an agent only when you can't enumerate the steps in advance.

Where this shows up next

Agents wire together everything from earlier sessions
S118 · Retrieval
Retrieval is often ONE tool in an agent's toolbox (search_docs).
S121 · Multi-Agent Orchestration
One agent per role, coordinated by a manager agent. Same tool_call loop, nested.
S122 · LLM Evaluation
Agents are hard to eval — you need trajectory metrics (tool-call accuracy, step count) not just final-answer accuracy.
S124 · LLM Serving
Agents multiply per-request cost by N steps; serving math changes dramatically.
S129 · Safety & Alignment
Agents can take real actions (send emails, edit files) — the blast radius of a hallucination grows enormously.
S127 · Streaming Analytics
Long-running agents look like streaming jobs — you need checkpointing and resumability.

(e) Recall + stretch · 10 min

Quick recall · click to reveal
★ = stretch question

Explain-out-loud test

  1. What is the loop that runs inside every LLM agent?
  2. What are the three limits you must enforce in production?
  3. When should you use a workflow instead of an agent?

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.