Skip to content

#328: Create Tools for LangGraph

LangGraph gets interesting as soon as we start to integrate it with our tasks. For that we need custom tools so that the LLM can interact with our data. Let us see how we can create our own tools and use them in LangGraph.

The first tool

We can turn any Python function into a tool by adding the @tool decorator:

1
2
3
4
5
6
from langchain_core.tools import tool

@tool
def multiply(a: int, b: int) -> int:
    """Multiply two integers together."""
    return a * b

With this minimalistic tool in place, we can build a LangGraph application that can multiply with our tool.

Use the tool with a custom executor

We can initialise our LLM and build our LangGraph application as we did a few times. There are two important parts that are easy to miss:

  1. We must bind our tools to the LLM - otherwise the LLM does not know about the tools.
  2. We need a node that can run our tool (tool_executor_node in the example).

If we put those two points together, our application can look like this:

import os
from typing import TypedDict, Literal, Annotated
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import BaseMessage, ToolMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

# 1. Define the Tools
@tool
def multiply(a: int, b: int) -> int:
    """Multiply two integers together."""
    return a * b

tools = [multiply]

# 2. Create the LLM
llm = ChatOpenAI(
    base_url="http://localhost:1234/v1",
    api_key="lm-studio",           
    model="openai/gpt-oss-20b",
    temperature=0.1
)

# 3. Bind tools to the model so it knows how to call them
model = llm.bind_tools(tools)

# 4. Define the State
class State(TypedDict):
    # Annotated with add_messages so history is preserved in the loop
    messages: Annotated[list[BaseMessage], add_messages]

# 5. Define the Nodes
def agent_node(state: State):
    """The LLM decides if it needs a tool or can answer directly."""
    print("--- AGENT: Reasoning ---")
    response = model.invoke(state["messages"])
    return {"messages": [response]}

def tool_executor_node(state: State):
    """Manually executes the tool call requested by the LLM."""
    print("--- TOOLS: Executing Tool ---")
    last_message = state["messages"][-1]

    tool_outputs = []
    for tool_call in last_message.tool_calls:
        # Map the tool name to the actual function
        if tool_call["name"] == "multiply":
            result = multiply.invoke(tool_call["args"])
            # Create a ToolMessage to feed the result back to the LLM
            tool_outputs.append(ToolMessage(
                content=str(result), 
                tool_call_id=tool_call["id"]
            ))

    return {"messages": tool_outputs}

# 6. Define the Looping Logic
def should_continue(state: State) -> Literal["continue", "loop"]:
    """Check if the LLM requested a tool or is finished."""
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "loop"
    return "continue"

# 7. Build the Graph
workflow = StateGraph(State)

# Add Nodes
workflow.add_node("agent", agent_node)
workflow.add_node("tool_executor", tool_executor_node)

# Define Edges
workflow.add_edge(START, "agent")

# Add the Looping Conditional Edge
workflow.add_conditional_edges(
    "agent",
    should_continue,
    {
        "continue": END,             # No tool calls? We are done.
        "loop": "tool_executor"      # Tool calls found? Go to executor.
    }
)

# After tool execution, always loop back to the agent to process the result
workflow.add_edge("tool_executor", "agent")

# 8. Compile and Run
app = workflow.compile()

# Print the visual structure
png_bytes = app.get_graph().draw_mermaid_png()
with open("tools.png", "wb") as f:
    f.write(png_bytes)

# Initial state with a math problem
initial_state = {"messages": [("user", "What is 144 multiplied by 2? And what is 4 multiplied by 5?")]}
final_output = app.invoke(initial_state)

print("\n--- FINAL RESULT ---")
print(final_output["messages"][-1].content)

This gives us a graph that looks like this: A simple loop between the LLM and the tool_executor_node.

If we run it, we can get an output like this one:

--- AGENT: Reasoning ---
--- TOOLS: Executing Tool ---
--- AGENT: Reasoning ---
--- TOOLS: Executing Tool ---
--- AGENT: Reasoning ---

--- FINAL RESULT ---
144 multiplied by 2 is **288**.
4 multiplied by 5 is **20**.

The Tools node

While writing an explicit tool_executor_node is possible, it gets cumbersome as soon as we have more than one tool. For this purpose LangGraph offers us the ToolNode.

To use the prebuilt tool node, we first need to import it in our application:

from langgraph.prebuilt import ToolNode

We can then take the script from above and replace parts 5 and 7 with these here:

# 5. Create the agent & tool node
def agent_node(state: State):
    """The LLM decides if it needs a tool or can answer directly."""
    print("--- AGENT: Reasoning ---")
    response = model.invoke(state["messages"])
    return {"messages": [response]}

tool_node = ToolNode(tools)

...

# 7. Build the Graph
workflow = StateGraph(State)

# Add Nodes
workflow.add_node("agent", agent_node)
workflow.add_node("tools", tool_node) # Add the prebuilt node

# Define Edges
workflow.add_edge(START, "agent")

# Use the prebuilt tools node in your edges
workflow.add_conditional_edges(
    "agent",
    should_continue,
    {
        "loop": "tools",  # Matches the node name above
        "continue": END,
    }
)

workflow.add_edge("tools", "agent")

The graph is more or less the same, but our custom tool executor node is replaced by the prebuilt tool node: We get the same loop, but the tools node is replaced.

The output no longer contains our explicit print statement, but the result stays the same:

--- AGENT: Reasoning ---
--- AGENT: Reasoning ---
--- AGENT: Reasoning ---

--- FINAL RESULT ---
144 × 2 = **288**
4 × 5 = **20**

Next

With the @tool decorator we can turn our functions into tools. When we then use the prebuilt tool node, we can use our tools right away with our LLM. No matter what we need our tool to do, if we can write it in Python we can use it with LangGraph. Next week we see how we can ask for permissions before we call our tools.