Skip to main content
Articles/langchain/langgraph-tool-calling-toolnode-and-custom-tools
Series: Stateful Graph Agentsβ€’Part 4 of 4
Scenario Article

LangGraph Tool Calling: ToolNode, Parallel Tools, and Custom Tools

The Stale Knowledge Problem: Why LLMs Need Runtime Tools

Abstract Algorithmsβ€’Mar 28, 2026
XLinkedInReddit
πŸ’‘ Executive Summary

Wire @tool, ToolNode, and bind tools into LangGraph for agents that call APIs at runtime.

🎯 System Scenario & Requirements

System Goal

Wire @tool, ToolNode, and bind tools into LangGraph for agents that call APIs at runtime.

Actors & Components

  • Application or API tier
  • Core service under design
  • Operator or SRE observer

Functional Requirements

  • TLDR: Wire @tool, ToolNode, and bind tools into LangGraph for agents that call APIs at runtime.
  • Your agent confidently tells you the current stock price of NVIDIA.
  • It's from its training data β€” six months out of date.

Constraints & Assumptions

  • Hallucinated Tool Calls LLMs occasionally invent tool call arguments that violate the schema, or call a tool with plausible-sounding but wrong input (e.g., passing "NVIDIA" instead of "NVDA" to a ticker lookup).
  • Pydantic args schema catches schema violations, but semantic errors still slip through.

⚑ Execution Stages & Decision Trace

1

Stage 1 β€” Tool Fundamentals: @tool, Schemas, and the bind_tools() Pattern

Action / Decision:

Before wiring tools into a graph, you need to understand what a "tool" is from LangGraph's perspective: a Python callable decorated with @tool that carries enough metadata for an LLM to decide when and how to call it.

Component Behavior:

Defining a Tool with @tool The @tool decorator from langchain core.tools does three things.

Observable Output: It wraps your function, uses the docstring as the tool description (what the model reads to decide whether to call it), and derives the JSON schema for the function's arguments from Python type hints.
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

Scenario context

Wire @tool, ToolNode, and bind_tools into LangGraph for agents that call APIs at runtime.

Your agent confidently tells you the current stock price of NVIDIA. It's from its training data β€” six months out of date. The model doesn't know what it doesn't know. It has no concept of "today." Every fact it gives you was frozen at training cutoff, and it will recite that frozen snapshot with the same unwavering confidence…

This walkthrough focuses on The Stale Knowledge Problem: Why LLMs Need Runtime Tools.

Before making a design choice, clarify:

  • Your agent confidently tells you the current stock price of NVIDIA.
  • It's from its training data β€” six months out of date.
  • The model doesn't know what it doesn't know.

Concepts in play: llm-agents-and-langgraph, graph-traversal-algorithms, low-level-design-systems, sorting-and-searching-algorithms

Stage 1 β€” Tool Fundamentals: @tool, Schemas, and the bind_tools() Pattern

  • Before wiring tools into a graph, you need to understand what a "tool" is from LangGraph's perspective: a Python callable decorated with @tool that carries enough metadata for an LLM to decide when and how to call it.
  • Defining a Tool with @tool The @tool decorator from langchain core.tools does three things.
  • It wraps your function, uses the docstring as the tool description (what the model reads to decide whether to call it), and derives the JSON schema for the function's arguments from Python type hints.
  • The docstring is not decoration β€” it's the model's only window into what this tool does.
from langchain_core.tools import tool

@tool
def get_stock_price(ticker: str) -> str:
    """
    Fetch the current stock price for a given ticker symbol.
    Returns a formatted string with the latest price.
    """
    # In production, call a real market data API here
    prices = {"NVDA": "118.42", "AAPL": "213.07", "TSLA": "172.30"}
    price = prices.get(ticker.upper(), "unknown")
    return f"{ticker.upper()} is currently trading at ${price}"
from pydantic import BaseModel, Field
from langchain_core.tools import tool

class WebSearchInput(BaseModel):
    query: str = Field(description="The search query string")
    max_results: int = Field(default=5, description="Maximum number of results to return")

@tool("web_search", args_schema=WebSearchInput)
def web_search(query: str, max_results: int = 5) -> str:
    """Search the web for current information on a topic."""
    # In production: call Tavily, SerpAPI, or Brave Search here
    return f"Top {max_results} results for '{query}': [result1, result2, ...]"

Observable outcome: The docstring is not decoration β€” it's the model's only window into what this tool does.

Stage 2 β€” Wiring Tools into LangGraph: ToolNode, Conditional Routing, and the Tool Loop

  • With tools defined and bound to the LLM, the next step is wiring them into the graph.
  • This requires three components: a state , an agent node , a ToolNode , and conditional routing between them.
  • State and the Agent Node The graph state holds the conversation as a list of messages.
  • The agent node calls the LLM with the current state and appends the model's response.
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]

def agent_node(state: AgentState):
    """Call the LLM with the current message history."""
    response = llm_openai.invoke(state["messages"])
    return {"messages": [response]}
tool_node = ToolNode(tools)  # Pass the same list you used in bind_tools()

Observable outcome: The agent node calls the LLM with the current state and appends the model's response.

Stage 3 β€” Deep Dive: How LangGraph Executes Tool Calls

  • The Internals When the LLM with bound tools generates a response that includes tool calls, it returns an AIMessage with a tool calls field.
  • Each entry in this list is a dict with three keys: id (a unique call identifier), name (the tool function name), and args (a dict of parsed arguments).
  • ToolNode iterates this list and dispatches each call to the matching function by name.
  • It uses the tool registry it was initialized with β€” the same tools list passed to ToolNode(tools) .
# Example AIMessage.tool_calls structure:
[
    {
        "id": "call_abc123",
        "name": "get_stock_price",
        "args": {"ticker": "NVDA"}
    },
    {
        "id": "call_def456",
        "name": "web_search",
        "args": {"query": "NVIDIA Q4 2025 earnings", "max_results": 3}
    }
]

Observable outcome: It uses the tool registry it was initialized with β€” the same tools list passed to ToolNode(tools) .

Stage 4 β€” The Tool Calling Loop: Graph Diagram

  • The core pattern is a tight loop between an agent node and a tool executor, gated by a conditional router: The agent loop: the LLM reasons β†’ calls tools if needed β†’ receives results β†’ reasons again.
  • The cycle exits when the model produces a direct answer.
  • Every iteration of the loop passes the full message history to the LLM, so the model always has context on what tools it called and what results they returned.
  • This is how multi-step reasoning works: the agent sees its own prior actions and builds on them.
flowchart TD
    A([User Input]) --> B[Agent Node LLM + bind_tools]
    B --> C{tools_condition}
    C -- has tool_calls --> D[ToolNode Execute tools]
    C -- no tool_calls --> E([Final Response])
    D --> B
    style A fill:#e8f4f8,stroke:#2196f3
    style E fill:#e8f5e9,stroke:#4caf50
    style C fill:#fff3e0,stroke:#ff9800
    style D fill:#fce4ec,stroke:#e91e63

Observable outcome: This is how multi-step reasoning works: the agent sees its own prior actions and builds on them.

Stage 5 β€” Real-World Applications: How Production Agents Use Tool Calling

  • Financial Research Assistants A hedge fund assistant needs to answer: "Should we increase our NVIDIA position given today's macro environment?" A bare LLM would hallucinate a confident answer based on stale training dat…
  • With tools, the agent calls get stock price , get earnings data , and web search in parallel, receives live data, then synthesizes a grounded analysis.
  • Input: "Evaluate NVIDIA given today's market." Process: Parallel tool calls β†’ stock price + recent news + analyst ratings fetched live Output: Structured memo citing today's actual data, not training-cutoff prices.
  • Customer Support Bots with CRM Access A support agent needs to check an order's shipping status.

Observable outcome: Customer Support Bots with CRM Access A support agent needs to check an order's shipping status.

Stage 6 β€” Practical Example: Market Research Agent with Parallel Tool Calls

  • This example demonstrates the full tool-calling loop: tool definition, LLM binding, ToolNode wiring, and parallel execution in a single runnable agent.
  • The market research scenario was chosen because it requires live data from multiple independent sources simultaneously β€” exactly the case where LangGraph's parallel ToolNode execution pays off over a sequential loop.
  • As you read through the code, watch for step 2 in the execution trace: the LLM emits two tool calls in a single AIMessage , which ToolNode executes concurrently β€” that is the parallel tool calling pattern in action.
  • Here is the complete market research agent with three tools β€” web search , get stock price , and summarize findings β€” with parallel execution and graceful error handling.
from typing import Annotated
from typing_extensions import TypedDict
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition

# ── Tool definitions ────────────────────────────────────────────────
@tool
def web_search(query: str) -> str:
    """Search the web for recent news and analysis on a topic."""
    # Replace with Tavily or SerpAPI in production
    return f"[Search results for '{query}']: Market analysts expect strong Q1. Supply constraints easing."

@tool
def get_stock_price(ticker: str) -> str:
    """Get the current stock price for a publicly traded company by ticker symbol."""
    mock_prices = {"NVDA": "118.42", "AAPL": "213.07", "MSFT": "415.30"}
    price = mock_prices.get(ticker.upper(), "Price unavailable")
    return f"{ticker.upper()}: ${price}"

@tool
def summarize_findings(raw_data: str) -> str:
    """Condense multiple data points into a brief investment summary."""
    return f"Summary: Based on provided data β€” {raw_data[:80]}... β€” outlook is cautiously positive."

tools = [web_search, get_stock_price, summarize_findings]

# ── LLM with bound tools ─────────────────────────────────────────────
llm = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)

# ── State ─────────────────────────────────────────────────────────────
class ResearchState(TypedDict):
    messages: Annotated[list, add_messages]

# ── Agent node ────────────────────────────────────────────────────────
def agent_node(state: ResearchState):
    response = llm.invoke(state["messages"])
    return {"messages": [response]}

# ── Graph assembly ────────────────────────────────────────────────────
tool_node = ToolNode(tools)

builder = StateGraph(ResearchState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.set_entry_point("agent")
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")
graph = builder.compile()

# ── Run ───────────────────────────────────────────────────────────────
result = graph.invoke(
    {"messages": [HumanMessage(
        content="Research NVIDIA (NVDA): get the current price and recent news. Then summarize."
    )]},
    config={"recursion_limit": 10}
)

for msg in result["messages"]:
    print(f"[{msg.__class__.__name__}] {msg.content[:120]}")

Observable outcome: Here is the complete market research agent with three tools β€” web search , get stock price , and summarize findings β€” with parallel execution and graceful error handling.

End-to-end outcome

After working through the stages, you should be able to explain:

  • Docstrings are the model's tool selection logic.
  • A vague docstring like "Get price" will lead the model to call the wrong tool or skip it entirely.

Alternatives considered

Decision Guide: ToolNode vs Custom Tool Execution vs LangChain AgentExecutor

Decision Guide: ToolNode vs Custom Tool Execution vs LangChain AgentExecutor

Failure and degraded modes

Hallucinated Tool Calls

  • Detection: LLMs occasionally invent tool call arguments that violate the schema, or call a tool with plausible-sounding but wrong input (e.g., passing "NVIDIA" instead of "NVDA" to a ticker lookup).
  • User or operator impact: LLMs occasionally invent tool call arguments that violate the schema, or call a tool with plausible-sounding but wrong input (e.g., passing "NVIDIA" instead of "NVDA" to a ticker lookup).

API Failures and Transient Errors

  • Detection: If get stock price raises an exception, the default ToolNode wraps it in an error message and feeds it back to the agent.
  • User or operator impact: If get stock price raises an exception, the default ToolNode wraps it in an error message and feeds it back to the agent.

Infinite Tool Loops

  • Detection: An agent can get stuck in a loop: call tool β†’ get result β†’ call tool again in the next step indefinitely.
  • User or operator impact: An agent can get stuck in a loop: call tool β†’ get result β†’ call tool again in the next step indefinitely.

Latency Creep at Scale

  • Detection: Every tool call adds a network round trip.
  • User or operator impact: Every tool call adds a network round trip.
Series Navigation β€’ Part 4 of 4

Stateful Graph Agents

View Full Series Overview β†’

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit