Tools in LangGraph | Agentic AI using LangGraph
An LLM operating purely on parametric knowledge is limited to conversational responses and static training data. To transform a passive chatbot into an active Agentic AI system, it must be capable of taking real-world actions—such as performing mathematical calculations, fetching live web search results, or querying real-time market APIs. This guide explores how to integrate Tools into LangGraph using cyclic state graphs, prebuilt nodes, and conditional edges to execute deterministic, multi-step actions.
1. The Problem: Parametric Knowledge vs. Action Execution
Standard conversational LLMs rely exclusively on their pre-trained weights (parametric knowledge). While effective for general reasoning and text generation, this architecture fails when tasked with:
- Deterministic Mathematics: LLMs generate text probabilistically, making large-number arithmetic error-prone.
- Real-Time Data Retrieval: LLMs cannot access information beyond their training cutoff (e.g., current news or today's sports scores).
- External API Interaction: LLMs cannot natively query database endpoints or third-party web services (e.g., stock market data).
To bridge this gap, LangGraph enables LLMs to decide when an external tool is required, generate structured execution payloads, and consume tool outputs to formulate grounded answers.
2. Core Mental Model: Naive vs. Cyclic Tool Workflows
Integrating tools into a computational graph can be approached in two ways: linear execution (naive) or cyclic feedback execution (production-grade).
The Naive Architectural Approach (Linear)
In a simple linear workflow, user input flows to the chat model, routes to a tool execution node, and then immediately terminates at the output.
Structural Flaws of the Naive Flow:
- Raw Payload Output: Tools often return technical, unformatted outputs (e.g., raw JSON responses or database dumps). In a naive flow, these unrefined structures are sent directly to the user.
- Inability to Perform Multi-Step Reasoning: Complex user prompts like "Find the owner of YouTube and tell me their stock price" require two sequential tool calls. A linear graph terminates after the first tool execution, preventing multi-step agentic workflows.
The Production Architecture (Cyclic Feedback Graph)
To enable self-correction, output polishing, and multi-step reasoning, the workflow must route the output of the Tool Node back into the Chat Node before reaching the End node.
Real-Life Analogy: The Executive & Specialist
Think of the Chat Node (LLM) as an Executive and the Tool Node as a Specialist Assistant. When a client asks a complex financial question, the Executive doesn't guess. They delegate raw research to the Specialist. The Specialist returns a raw financial report (JSON data). The Executive reads the report, translates it into clear bullet points, and delivers a polished answer to the client. If additional data is needed, the Executive sends the Specialist back out for more details before giving the final answer.
3. Key LangGraph Tooling Primitives
LangGraph provides built-in abstractions to handle tool registration, routing, and execution without writing custom boilerplate logic.
1. ToolNode
ToolNode is a prebuilt node provided by langgraph.prebuilt. It accepts a list of LangChain tools and listens for tool calls generated by the LLM. When triggered, it automatically routes the invocation payload to the correct tool and wraps the tool's return value in a ToolMessage.
2. tools_condition
tools_condition is a prebuilt conditional edge function in langgraph.prebuilt. It inspects the latest message returned by the LLM in the state:
- If the message contains a
tool_callsrequest, it routes execution to the"tools"node. - If no tool call is present, it routes execution to
END.
Primitive Comparison Summary
4. Tool Construction: Prebuilt vs. Custom (@tool)
LangGraph supports both prebuilt library tools and custom Python functions converted into tools using the @tool decorator.
Prebuilt Tools
Prebuilt tools come ready-to-use from external packages (e.g., DuckDuckGoSearchRun for web searching):
from langchain_community.tools import DuckDuckGoSearchRun
search_tool = DuckDuckGoSearchRun()
Custom Tools via @tool Decorator
Custom tools allow developers to expose arbitrary Python functions to the LLM.
Critical Requirement: Every custom tool must contain a detailed docstring. The LLM reads this docstring to understand the tool's purpose, parameters, and when to select it over other tools.
import requests
from langchain_core.tools import tool
@tool
def calculator(first_number: float, second_number: float, operation: str) -> str:
"""
Performs basic mathematical operations on two numbers.
Operations allowed: 'add', 'subtract', 'multiply', 'divide'.
"""
if operation == "add":
return str(first_number + second_number)
elif operation == "subtract":
return str(first_number - second_number)
elif operation == "multiply":
return str(first_number * second_number)
elif operation == "divide":
if second_number == 0:
return "Error: Division by zero."
return str(first_number / second_number)
else:
return "Error: Invalid operation specified."
@tool
def get_stock_price(ticker: str) -> str:
"""
Fetches current stock price data for a given ticker symbol using Alpha Vantage API.
"""
api_key = "YOUR_ALPHA_VANTAGE_API_KEY"
url = f"https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={ticker}&apikey={api_key}"
response = requests.get(url)
if response.status_code == 200:
return response.text
return "Error: Unable to fetch stock data."
5. Building the Multi-Step Reasoning Graph
The code snippet below demonstrates how to bind tools to an LLM, create a state graph, configure nodes and edges, and compile the cyclic execution graph.
from typing import Annotated, TypedDict
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
load_dotenv()
# 1. Define State
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
# 2. Instantiate LLM & Bind Tools
tools = [search_tool, calculator, get_stock_price]
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)
# 3. Define Chat Node
def chat_node(state: State):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
# 4. Construct Graph
builder = StateGraph(State)
# Add Nodes
builder.add_node("chat_node", chat_node)
builder.add_node("tools", ToolNode(tools)) # Tool execution node
# Add Edges
builder.add_edge(START, "chat_node")
builder.add_conditional_edges("chat_node", tools_condition)
builder.add_edge("tools", "chat_node") # Loop tool output back to LLM!
# Compile Graph
graph = builder.compile()
6. Complete Flow Walkthrough: Multi-Step Reasoning Trace
Consider the user prompt: "Who owns YouTube? Find the stock price of that company."
The step-by-step trace below shows how the graph processes this multi-step query through its cyclic loop:
7. Production Edge Cases & UI Refinement
When deploying tool-enabled agentic workflows to front-end applications (e.g., Streamlit), two production pitfalls typically arise:
Pitfall 1: Streaming Raw ToolMessage Objects to the UI
If your front-end streams every message added to the state graph, technical payloads (like raw API JSON structures) will render directly in the user's chat window.
# ❌ INCORRECT: Streams raw JSON tool payloads directly to the user
for chunk in graph.stream({"messages": user_input}, stream_mode="values"):
message = chunk["messages"][-1]
st.write(message.content)
Production Fix: Filtering Messages
Filter message types before streaming to UI containers. Stream only AIMessage content and suppress intermediate ToolMessage payloads.
from langchain_core.messages import AIMessage
# ✅ CORRECT: Stream only polished AI responses
for chunk in graph.stream({"messages": user_input}, stream_mode="updates"):
for node_name, node_update in chunk.items():
if "messages" in node_update:
last_msg = node_update["messages"][-1]
if isinstance(last_msg, AIMessage) and not last_msg.tool_calls:
st.write(last_msg.content)
Pitfall 2: Silent Execution Delays
Web searches and API queries introduce network latency. Without UI feedback, users may assume the application has frozen.
Production Fix: Status Containers
Use UI status containers (such as Streamlit's st.status()) to display live tool execution steps to the user:
import streamlit as st
with st.status("Agent thinking...", expanded=True) as status:
for chunk in graph.stream({"messages": user_input}, stream_mode="updates"):
for node_name, node_update in chunk.items():
if node_name == "tools":
status.write("🛠️ Executing external tool...")
elif node_name == "chat_node":
status.write("🧠 Processing information...")
status.update(label="Response generated!", state="complete", expanded=False)
8. Observability & Debugging via LangSmith Traces
When building multi-node graphs with dynamic edge routing, inspecting state changes manually becomes difficult. LangSmith provides visual execution traces that allow developers to debug tool interactions step-by-step.
Key Metrics to Debug in Traces:
- Tool Invocation Schema: Verify whether the LLM extracted parameter arguments matching your function signatures.
- Tool Output Payload: Check if the raw string returned by your custom tool was correctly added to the state as a
ToolMessage. - Loop Termination: Ensure
tools_conditionproperly routes toENDonce all queries are answered, avoiding infinite execution loops.
9. Complete Production Implementation
Below is the complete, modular code structure split into a production-ready backend graph service and a Streamlit frontend UI.
Backend: langgraph_tool_backend.py
import os
import requests
from typing import Annotated, TypedDict
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_community.tools import DuckDuckGoSearchRun
from langchain_core.tools import tool
from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver
load_dotenv()
# --- 1. Tools Definition ---
search_tool = DuckDuckGoSearchRun()
@tool
def calculator(first_number: float, second_number: float, operation: str) -> str:
"""
Performs basic mathematical operations on two numbers.
Operations allowed: 'add', 'subtract', 'multiply', 'divide'.
"""
if operation == "add":
return str(first_number + second_number)
elif operation == "subtract":
return str(first_number - second_number)
elif operation == "multiply":
return str(first_number * second_number)
elif operation == "divide":
return str(first_number / second_number) if second_number != 0 else "Division by zero"
return "Invalid operation"
@tool
def get_stock_price(ticker: str) -> str:
"""
Fetches live stock price data for a given ticker symbol using Alpha Vantage API.
"""
api_key = os.getenv("ALPHA_VANTAGE_API_KEY", "DEMO_KEY")
url = f"https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={ticker}&apikey={api_key}"
try:
response = requests.get(url, timeout=10)
return response.text
except Exception as e:
return f"Error fetching stock data: {str(e)}"
tools = [search_tool, calculator, get_stock_price]
# --- 2. State & Model Configuration ---
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)
# --- 3. Node Definitions ---
def chat_node(state: State):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
# --- 4. Graph Construction ---
builder = StateGraph(State)
builder.add_node("chat_node", chat_node)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "chat_node")
builder.add_conditional_edges("chat_node", tools_condition)
builder.add_edge("tools", "chat_node")
# Add memory checkpointer for persistence
checkpointer = MemorySaver()
chatbot_graph = builder.compile(checkpointer=checkpointer)
Frontend: app.py (Streamlit Integration)
import streamlit as st
from langchain_core.messages import HumanMessage, AIMessage
from langgraph_tool_backend import chatbot_graph
st.set_page_config(page_title="Agentic AI with Tools", page_icon="🤖")
st.title("Agentic AI Chatbot with Tools")
# Initialize Chat History in Session State
if "messages" not in st.session_state:
st.session_state.messages = []
# Display Conversation History
for msg in st.session_state.messages:
role = "user" if isinstance(msg, HumanMessage) else "assistant"
with st.chat_message(role):
st.write(msg.content)
# Process New User Input
if user_input := st.chat_input("Ask a question or request an action..."):
# Render user prompt
st.session_state.messages.append(HumanMessage(content=user_input))
with st.chat_message("user"):
st.write(user_input)
# Invoke Graph & Stream Output
config = {"configurable": {"thread_id": "user_session_1"}}
with st.chat_message("assistant"):
with st.status("Agent processing...", expanded=True) as status:
final_response = ""
# Stream state graph execution updates
for event in chatbot_graph.stream(
{"messages": [HumanMessage(content=user_input)]},
config=config,
stream_mode="updates"
):
for node_name, node_update in event.items():
if node_name == "tools":
status.write("🛠️ Tool Node: Executing external action...")
elif node_name == "chat_node":
status.write("🧠 Chat Node: Processing output...")
last_msg = node_update["messages"][-1]
if isinstance(last_msg, AIMessage) and not last_msg.tool_calls:
final_response = last_msg.content
status.update(label="Complete!", state="complete", expanded=False)
# Display polished response
if final_response:
st.write(final_response)
st.session_state.messages.append(AIMessage(content=final_response))
Summary and Key Takeaways
- Action Capabilities via Tools: Integrating tools transforms passive conversational chat models into dynamic Agentic AI workflows capable of executing real-world actions.
ToolNodeBridge: The built-inToolNodeacts as a seamless execution bridge between LangGraph states and collections of registered tools.- Dynamic Routing via
tools_condition: The prebuilttools_conditionedge function dynamically inspects LLM responses to determine whether to route execution toToolNodeor end the graph process. - Cyclic Feedback Topology: Connecting the output of
ToolNodeback into the LLM node (tools -> chat_node) enables multi-step tool calls, output polishing, and intermediate reasoning loops. - Docstring Criticality: Custom tools defined using the
@tooldecorator rely heavily on comprehensive docstrings to inform the LLM when and how to invoke them. - Technical Message Isolation: Production frontends must isolate polished
AIMessageoutputs from internalToolMessagepayloads to prevent raw JSON from streaming directly into user interfaces. - Observability with LangSmith: Visualizing state graph cascades with tracing tools like LangSmith is essential for debugging tool argument parsing, node transitions, and execution loops.