How Tools Turn into Agents: What Actually Happens at Runtime
Last Updated on February 6, 2026 by Editorial Team
Author(s): Vahe Sahakyan
Originally published on Towards AI.

Many AI agent demos look convincing — until they fail in practice.
Tools are defined correctly. Prompts seem reasonable. Yet the agent either calls the wrong tool, fails to call any tool at all, or produces outputs that are impossible to parse reliably. These failures are rarely caused by bad prompts. They usually come from misunderstandings about how agents actually reason and act at runtime.
This article is the second part of a three-part series on tools for agentic AI systems.
In Part 1, we focused on why tools are essential for AI agents and what makes a tool usable by a language model.
In this article, we move from principles to mechanics. We will build tools step by step, expose them to agents, and examine how agent architectures use those tools in practice. The focus is on how tool-calling systems actually work, using LangChain and LangGraph as concrete implementations.
This article is intentionally hands-on. It assumes familiarity with the concepts introduced in Part 1 and focuses on building, inspecting, and extending tool-using agents. Data-analysis-specific agents will be covered separately in Part 3.
From Principles to Practice
In the previous article, we focused on why tools are essential for agentic systems and what makes a tool usable by a language model. That discussion was intentionally conceptual.
In this article, we move to practice. The goal is not to introduce new ideas, but to show how the principles discussed earlier are realized in working systems. We will start from simple building blocks, gradually introduce structure, and then examine how tools are used inside agents in practice.
To make the examples reproducible, we will first define a minimal development setup. We will then use that environment to build and inspect tool-using agents step by step.
Setup and Dependencies
To follow the examples in this article, you need a basic Python environment with LangChain and LangGraph. The setup is intentionally minimal and mirrors what is used in the accompanying notebooks.
Requirements:
— Python 3.10+
— An OpenAI-compatible API key set as an environment variable
Core dependencies:
pip install langchain langgraph langchain-openai python-dotenv
Configuration is handled via environment variables (for example, using a .env file with OPENAI_API_KEY set). Once this is in place, the examples can be run without additional setup.
Building Your First Tool
With the environment in place, we can begin building tools.
We start with a deliberately simple example. The goal here is not to introduce agents yet, but to show how a plain Python function becomes an explicit capability that can later be exposed to a language model.
At this stage, the focus is on structure rather than logic. Clear intent, inputs, and outputs are essential before any agent behavior is introduced.
In the following cell, we implement extract_date with a tool-friendly docstring and a consistent return schema.
def extract_date(text: str) -> dict:
"""
Extracts a single date from natural-language text when the date is written
in ISO format (YYYY-MM-DD).
Parameters:
- text (str): A user message that may contain a date.
Returns:
- dict: {"date": <string or null>}
If a date is found, return the extracted date string.
If no date is found, returns null.
Example Input:
"Schedule a meeting on 2025-03-18."
Example Output:
{"date": "2025-03-18"}
"""
match = re.search(r"\b\d{4}-\d{2}-\d{2}\b", text)
return {"date": match.group(0) if match else None}
Wrapping the Function as a LangChain Tool
Once a function is well-structured and properly documented, it can be exposed to a language model as a tool. LangChain provides two main ways to do this.
Method 1: Using Tool(…)
This is the older, but still supported, approach.
from langchain.agents import Tool
extract_date_tool = Tool(
name="extract_date",
func=extract_date,
description="Extracts an ISO date (YYYY-MM-DD) from text and returns it as a dictionary."
)
This method is fully explicit: you manually specify the tool’s name, description, and underlying function. It can be useful when:
- you need precise manual control,
- you are working with legacy LangChain code,
- or you want to wrap an existing function without modifying it.
However, it has important limitations:
- no structured input schema is generated automatically,
- parameter formats must be inferred from the description,
- integration with function-calling models is limited.
Method 2: Using the @tool Decorator (Recommended)
The modern and recommended approach is the @tool decorator. It automatically extracts:
- the tool name,
- the docstring as the description,
- and a structured argument schema from type annotations.
from langchain_core.tools import tool
import re
@tool
def extract_date(text: str) -> dict:
"""
Extracts a single date from natural-language text when the date is written
in ISO format (YYYY-MM-DD).
Parameters:
- text (str): A user message that may contain a date.
Returns:
- dict: {"date": <string or null>}
If a date is found, return the extracted date string.
If no date is found, returns null.
Example Input:
"Schedule a meeting on 2025-03-18."
Example Output:
{"date": "2025-03-18"}
"""
match = re.search(r"\b\d{4}-\d{2}-\d{2}\b", text)
return {"date": match.group(0) if match else None}
The decorator-based approach has several advantages:
- auto-generated JSON schemas reduce model errors,
- seamless compatibility with OpenAI, Anthropic, and LangGraph,
- cleaner and more readable syntax,
- consistent documentation and argument exposure.
For these reasons, most modern LangChain and LangGraph examples — and nearly all production agent systems — rely on the @tool decorator rather than manual Tool(…) definitions.
Structured Tools with Multiple Typed Inputs
Structured tools are not limited to a single text input. They allow language models to work with multiple parameters, each with a clearly defined type. This significantly improves precision and reduces ambiguity when tools require more than one piece of information.
A simple example is a temperature conversion tool with three typed inputs: a numeric value, a source unit, and a target unit. While the conversion logic itself is straightforward, the structured schema illustrates an important point: the model no longer has to infer how arguments should be formatted or combined.
Instead, the tool definition explicitly constrains what valid inputs look like, making correct tool usage far more reliable in agent workflows.
@tool
def convert_temperature(value: float, from_unit: str, to_unit: str) -> dict:
"""
Converts a temperature from one unit to another.
Parameters:
value (float): The numeric temperature value.
from_unit (str): One of "C", "F", or "K".
to_unit (str): One of "C", "F", or "K".
Returns:
dict: {"converted": float}
Example Input:
{"value": 32, "from_unit": "F", "to_unit": "C"}
Example Output:
{"converted": 0.0}
"""
if from_unit == "C":
base = value + 273.15
elif from_unit == "F":
base = (value - 32) * 5/9 + 273.15
elif from_unit == "K":
base = value
else:
return {"converted": None}
if to_unit == "C":
return {"converted": base - 273.15}
elif to_unit == "F":
return {"converted": (base - 273.15) * 9/5 + 32}
elif to_unit == "K":
return {"converted": base}
return {"converted": None}
This kind of structured interface becomes increasingly important as agents begin to coordinate multiple tools and reason about their inputs explicitly, rather than relying on loosely formatted text.
Choosing an LLM Backbone for Tool-Using Agents
Once tools are defined, the next decision is which language model will control them. Not every model supports tool calling equally well, and their reasoning capabilities can vary significantly.
A minimal setup using an OpenAI-compatible model looks like this (make sure your OPENAI_API_KEY is set in a .env file):
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
import os
load_dotenv()
llm = ChatOpenAI(
model="gpt-4.1-nano",
api_key=os.getenv("OPENAI_API_KEY"),
)
The specific model used here is just one example. In practice, many providers can sit behind the same agent interface. What matters most is not the vendor, but whether the model:
– supports structured tool calling,
– handles tool schemas reliably,
– and has sufficient reasoning ability for the agent’s tasks.
The Agent Reasoning Loop in Practice
In LangChain, an agent can be thought of as a combination of three components:
LLM + tools + a strategy for deciding what to do next.
Instead of responding directly to a prompt, an agent operates in a loop. For each user query, it:
- reasons about the task,
- decides whether a tool is needed,
- calls the appropriate tool and observes the result,
- and uses that result to determine the next step or produce a final answer.
This is the same ReAct pattern discussed earlier, now expressed as a concrete execution model.
In practice, an agent is rarely limited to a single tool. Most real agents are initialized with a set of tools, each representing a distinct capability — for example, retrieval, computation, data transformation, or external actions.
At runtime, the agent does not call all tools blindly. It reasons about the user’s request, selects the most appropriate tool from the available set, invokes it, observes the result, and then decides whether another tool is needed or whether it can produce a final answer.
For example, a single agent might have access to both a calculator tool and a database lookup tool, choosing between them depending on whether a query requires computation or retrieval.
The key point is that adding more tools does not change the reasoning loop. It increases the action space, but the observe → think → act pattern remains the same.
LangChain abstracts this loop behind different agent strategies. Rather than manually implementing the control flow, you select an agent type that defines how reasoning, tool selection, and observation are handled. This is where initialize_agent comes into play.
The primary difference between agent types is how they implement this loop and what tool formats they expect.

A Zero-Shot ReAct Agent With a Simple Tool
To see how agents actually use tools, let’s build a small ReAct-style agent in LangChain.
Here we use the zero-shot-react-description agent type. This agent expects simple, string-based tools, which is why we use the Tool(…) class rather than the @tool decorator:
- Tool(…) creates a string-input tool compatible with text-based ReAct agents
- @tool creates a StructuredTool with typed parameters, which zero-shot-react-description cannot parse
Step 1. Define a plain function
import re
def sum_values_from_text(inputs: str) -> dict:
"""
Extracts numeric values from a text description and returns their sum.
Parameters:
inputs (str): Text containing numbers
(e.g. "Q1: 1.2M, Q2: 1.5M, Q3: 1.3M").
Returns:
dict: {"result": float} - the sum of extracted numbers.
"""
matches = re.findall(r"-?\d+(?:\.\d+)?", inputs)
nums = [float(x) for x in matches] if matches else []
return {"result": sum(nums) if nums else 0.0}
Step 2. Wrap it using Tool(…):
from langchain.agents import Tool
sum_tool = Tool(
name="SumTool",
func=sum_values_from_text,
description="Sums all numeric values found in a text description."
)
Step 3. Create the zero-shot ReAct agent
from langchain.agents import initialize_agent
agent = initialize_agent(
tools=[sum_tool],
llm=llm,
agent="zero-shot-react-description",
verbose=True,
handle_parsing_errors=True,
)
Step 4. Use the agent
agent.invoke({"input": "The values were 1.2, 1.4, and 1.1. What is the total?"})
At runtime, the agent:
- reads the question,
- reasons that it needs to sum values,
- calls SumTool,
- observes the tool’s output,
- and produces the final answer in natural language.
This demonstrates the classic ReAct loop in action:
Reason → Act → Observe → Answer.
Matching Agent Types to Tool Formats
Many agent failures are not caused by bad prompts or weak models, but by subtle mismatches between the agent type and the tool schema it expects.
One of the most important — and often overlooked — aspects of agent design is that the agent type must match the tool schema. Not all agents expect the same input and output formats, and mismatches here are a common source of failures.
LangChain provides several agent types, each with different expectations:
- zero-shot-react-description
Expects tools that accept and return plain strings.
Best suited for simple, text-based tools. - structured-chat-zero-shot-react-description
Supports StructuredTools with typed inputs and structured (JSON-like) outputs.
Better for tools with multiple parameters or richer schemas. - openai-functions
Designed specifically for models that natively support function calling (e.g., GPT-4.x).
Pairs well with tools defined via @tool and proper type hints.
The practical implication is straightforward:
- If your tools take a single string and return simple outputs, zero-shot-react-description often works well.
- If your tools require multiple typed parameters or return structured JSON, you should use structured-chat-zero-shot-react-description or openai-functions, depending on the model.
This is why some combinations of tool + model + agent type work smoothly, while others fail unexpectedly: the agent expects one schema, but the tool provides another.
Beyond initialize_agent: Why LangGraph Is Becoming the Standard
So far, we have used LangChain’s initialize_agent to construct agents such as:
- zero-shot-react-description
- structured-chat-zero-shot-react-description
- openai-functions
These predefined strategies are useful starting points. However, real-world agent systems often require more than what a single abstraction can provide. Common needs include:
- finer control over the reasoning flow,
- custom prompts and system behavior,
- multiple tools acting in sequence,
- visibility into intermediate steps,
- and agents that can be extended or composed into larger workflows.
This is where LangGraph becomes important.
LangGraph treats agents not as a black box that reacts to prompts, but as explicit, inspectable graphs of reasoning steps. Each node in the graph can represent an LLM reasoning step, a tool call, or a conditional branch. This makes the control flow visible and configurable, rather than implicit.
LangGraph’s create_react_agent builds on the familiar ReAct pattern, but exposes it as a first-class structure. You can shape the prompt, inject custom logic, inspect intermediate state, and integrate tools in a controlled way. The reasoning loop remains the same; the difference is that it is no longer hidden.
Because of this explicit control and composability, LangGraph is increasingly used as the foundation for more robust, production-grade agent systems.
Creating a ReAct Agent in LangGraph
With structured tools in place, we can now build a ReAct-style agent using LangGraph.
We start by defining a structured tool. This example deliberately returns a richer output schema to illustrate how LangGraph handles structured results and intermediate state.
from typing import Dict, Union
import re
from langchain_core.tools import tool
@tool
def sum_numbers_with_complex_output(inputs: str) -> Dict[str, Union[float, str]]:
"""
Extracts and sums all integers and decimal numbers from the input string.
Parameters:
- inputs (str): A string that may contain numeric values.
Returns:
- dict: A dictionary with the key "result". If numbers are found, the value is their sum (float).
If no numbers are found or an error occurs, the value is a corresponding message (str).
Example Input:
"Add 10, 20.5, and -3."
Example Output:
{"result": 27.5}
"""
matches = re.findall(r"-?\d+(?:\.\d+)?", inputs)
if not matches:
return {"result": "No numbers found in input."}
try:
nums = [float(x) for x in matches]
return {"result": sum(nums)}
except Exception as e:
return {"result": f"Error: {str(e)}"}
With the tool defined, we can construct a LangGraph-based ReAct agent:
from langgraph.prebuilt import create_react_agent
agent_exec = create_react_agent(
model=llm,
tools=[sum_numbers_with_complex_output],
prompt="You are a helpful mathematical assistant that performs operations accurately."
)
To interact with the agent:
msgs = agent_exec.invoke({
"messages": [("human", "Add the numbers -10, -20, -30.")]
})
LangGraph returns both:
- the final answer, and
- the full reasoning trace, including tool calls.
This traceability is a key advantage. Unlike initialize_agent, LangGraph exposes the full execution path, making it possible to inspect how the agent reasoned, which tools were used, and how intermediate results influenced the final output.
To extract the final response:
final_answer = msgs["messages"][-1].content
Conclusion and What Comes Next
In this article, we moved from principles to practice. Starting with simple functions, we wrapped them as tools, examined how tool schemas interact with different agent types, and built ReAct-style agents that can reason, select tools, and act. We also saw why higher-level abstractions eventually become limiting, and how LangGraph provides the control and visibility needed for more robust agent behavior.
The key takeaway is not any specific framework or API, but the pattern itself. Agents are not defined by prompts alone. They emerge from the interaction between a reasoning model, well-designed tools, and a control loop that makes decisions explicit and observable.
So far, the examples have focused on small, self-contained tools. In the next article, we will extend these ideas to data-centric agents. We will look at agents that work with tabular data, interact with dataframes, and perform more complex analytical tasks — where tool structure, schema design, and agent control become even more critical.
This article showed how tools become agents.
The next one explores how agents work with real data.
Join thousands of data leaders on the AI newsletter. Join over 80,000 subscribers and keep up to date with the latest developments in AI. From research to projects and ideas. If you are building an AI startup, an AI-related product, or a service, we invite you to consider becoming a sponsor.
Published via Towards AI
Towards AI Academy
We Build Enterprise-Grade AI. We'll Teach You to Master It Too.
15 engineers. 100,000+ students. Towards AI Academy teaches what actually survives production.
Start free — no commitment:
→ 6-Day Agentic AI Engineering Email Guide — one practical lesson per day
→ Agents Architecture Cheatsheet — 3 years of architecture decisions in 6 pages
Our courses:
→ AI Engineering Certification — 90+ lessons from project selection to deployed product. The most comprehensive practical LLM course out there.
→ Agent Engineering Course — Hands on with production agent architectures, memory, routing, and eval frameworks — built from real enterprise engagements.
→ AI for Work — Understand, evaluate, and apply AI for complex work tasks.
Note: Article content contains the views of the contributing authors and not Towards AI.