No history yet

Agentic Tool Calling

Beyond Prompts: Making LLMs Act

So far, you've likely used large language models (LLMs) as powerful conversationalists. You ask a question, and they provide a text-based answer. But what if you could give them tools to interact with the outside world? Instead of just telling you the weather, an LLM could look it up. Instead of describing code, it could run it. This leap from answering to acting is the core of agentic AI.

This process transforms the LLM from a static knowledge base into a dynamic problem-solver. The key is giving the model a set of tools and a reasoning framework to decide which tool to use, when to use it, and how to interpret its output.

Teaching an LLM to Use Tools

An LLM can't magically discover a Python function and figure out how to run it. We need to provide a clear, structured description—a schema—that explains what the tool does, what arguments it requires, and what kind of data it returns. This is where modern Python features like and docstrings become essential. They aren't just for human developers; they make your code machine-readable.

Frameworks like LangChain simplify this by providing decorators. A decorator is a special function that wraps another function to add new behavior. In this case, the @tool decorator inspects your function's signature and docstring to generate the schema for the LLM.

from langchain_core.tools import tool
from typing import Annotated

@tool
def multiply(x: Annotated[int, "The first number to multiply."], 
             y: Annotated[int, "The second number to multiply."]) -> int:
    """Multiplies two numbers together."""
    return x * y

print(multiply.name)
print(multiply.description)
print(multiply.args)

Here, the @tool decorator, combined with type hints (int) and descriptive annotations, gives the LLM everything it needs. It knows the function is named multiply, its purpose is to

Multiplies two numbers together

and it requires two integer arguments, x and y. Once the LLM decides to use this tool, your application's backend is responsible for actually executing the Python code with the arguments the LLM provides and then feeding the result back to the model.

The Reason-Act Loop

An agent doesn't just call a tool once and stop. It operates in a cycle, refining its approach based on new information. This is commonly known as the loop, which stands for "Reason + Act." It's a simple but powerful framework that mimics how a person might solve a multi-step problem.

The loop consists of three repeating steps:

  1. Thought: The LLM analyzes the user's request and its current state. It decides what to do next. Should it ask a clarifying question, use a tool, or provide the final answer?
  2. Action: The LLM commits to a plan, choosing a specific tool and the arguments to pass to it.
  3. Observation: The code executes the tool call, and the result (or an error) is returned. This observation becomes the input for the next turn of the loop.

This cycle continues until the LLM concludes it has enough information to fully answer the user's original query.

Building the Loop with LangGraph

While you could code this loop yourself, managing the state between steps can get complex. This is where frameworks like come in. It lets you define agentic workflows as a graph, where each step is a node and the logic connecting them is an edge.

A simple agent graph has two main parts:

  • Nodes: Python functions that represent the core steps. You'll typically have one node for the agent to think and decide on an action, and another node to execute the tool and return the observation.
  • Edges: Logic that directs the flow. A conditional edge checks the agent's last action. Did it call a tool? Then proceed to the tool execution node. Did it decide to finish? Then exit the loop and go to the end node.
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage
import operator

# This defines the state of our graph.
# It's what gets passed between nodes.
class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]

# Node to call the model (the "Reason" part)
def call_model(state):
    messages = state['messages']
    response = model_with_tools.invoke(messages)
    return {"messages": [response]}

# Node to execute tools (the "Act" part)
def call_tool(state):
    last_message = state['messages'][-1]
    action = last_message.tool_calls[0]
    result = tool_invoker.invoke(action)
    # `result` is the Observation
    return {"messages": [ToolMessage(content=str(result), tool_call_id=action['id'])]}

In this setup, we'd add these functions as nodes to a StatefulGraph. Then, we'd add an edge that checks the output of call_model. If the model's response contains a tool_calls attribute, the edge directs the graph to the call_tool node. If not, it means the agent has produced its final answer, and the edge can point to the special END node.

This graph-based approach makes the agent's logic explicit and easier to debug. You can visualize the flow and add more complex branches, such as handling tool errors or asking the user for clarification, simply by adding more nodes and edges.

Quiz Questions 1/4

What is the primary capability that distinguishes an "agentic" AI from a standard conversational large language model (LLM)?

Quiz Questions 2/4

How does an LLM reliably understand what a Python function does, what arguments it needs, and what it returns when it's provided as a tool?

By combining clearly defined tools with a structured reasoning loop, you can build powerful agents that go far beyond simple text generation.