n4nAI

Build a LangChain agent with custom tool calling

A step-by-step guide to langchain agent custom tool calling: define tools, wire a reasoning loop, verify with Python code and pytest for production use.

n4n Team3 min read604 words

Audio narration

Coming soon — every post will get a voice note here.

Building a langchain agent custom tool calling pipeline forces you to think about the contract between model output and executable code. This guide walks through a minimal but production-shaped implementation in Python, from tool schema to verification.

Step 1: Install dependencies and configure the environment

Create a virtual environment and install the required packages. You need the LangChain core, the OpenAI integration (which works with any OpenAI-compatible endpoint), and pytest for verification.

python -m venv .venv
source .venv/bin/activate
pip install langchain langchain-openai langchain-core python-dotenv pytest

Store credentials in a .env file. We will point LangChain at an OpenAI-compatible gateway rather than directly at OpenAI, so the key is issued by that gateway.

# .env
N4N_API_KEY=sk-xxxx

Step 2: Configure the LLM endpoint

LangChain’s ChatOpenAI speaks the OpenAI chat protocol. Any compliant endpoint works. An OpenAI-compatible gateway like n4n.ai exposes one endpoint for 240+ models and handles provider fallback when a backend is rate-limited or degraded, which removes a class of operational toil. It also returns per-token usage metering on each response, which you can capture for cost tracking.

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI

load_dotenv()

llm = ChatOpenAI(
    model="gpt-4o-mini",
    api_key=os.environ["N4N_API_KEY"],
    base_url="https://api.n4n.ai/v1",
    temperature=0,
)

If you prefer a different model, swap the model string. The agent code below does not change.

Step 3: Define custom tools with explicit schemas

Tool calling fails in production when the model emits arguments that your function cannot parse. Use LangChain’s @tool decorator and type hints so the schema is derived from the signature.

from langchain_core.tools import tool

@tool
def get_weather(city: str, units: str = "metric") -> str:
    """Return current temperature for a city.

    Args:
        city: Name of the city.
        units: 'metric' or 'imperial'.
    """
    # Stub: replace with a real API call.
    return f"{city}: 22C (stub)"

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

The docstring matters. The model uses it to decide when to call the tool. Keep it precise and avoid ambiguity. LangChain validates arguments against the inferred JSON schema before calling your function. If validation fails, the executor can be configured with handle_parsing_errors=True to feed the error back to the model. This turns malformed output into a self-healing loop instead of a crash.

Isolate side effects

Wrap network calls in try/except and return strings. The agent executor does not handle raised exceptions gracefully by default; a returned error string lets the model self-correct.

@tool
def get_weather_safe(city: str, units: str = "metric") -> str:
    """Return current temperature for a city, or an error string."""
    try:
        # real call here
        return f"{city}: 22C (stub)"
    except Exception as e:
        return f"error: {e}"

Step 4: Construct the agent and execution loop

Use the prebuilt create_openai_tools_agent with a prompt that instructs the model to use tools. Then wrap it in AgentExecutor. The MessagesPlaceholder for agent_scratchpad is required—it holds the intermediate tool calls and observations.

from langchain.agents import create_openai_tools_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

tools = [get_weather, multiply]

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant. Use tools when needed."),
    ("human", "{input}"),
    MessagesPlaceholder("agent_scratchpad"),
])

agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    handle_parsing_errors=True,
)

verbose=True prints the reasoning trace. Disable it in production and rely on logging. handle_parsing_errors=True ensures a bad tool argument becomes a prompt rather than an unhandled exception.

Step 5: Run the agent on a task

Invoke with a string input. The executor handles the tool-call loop until the model returns a final answer.

result = executor.invoke({"input": "What is 12 times 9, and what is the weather in Berlin?"})
print(result["output"])

Expected behavior: the model calls multiply and get_weather, then synthesizes a final response. If the model hallucinates a tool name, the executor raises ValueError; catch it at the boundary.

try:
    result = executor.invoke({"input": "ping the flux capacitor"})
except ValueError as e:
    print(f"agent failed: {e}")

Step 6: Verify behavior with tests

Write two test layers: unit tests for the tools, and an integration test that mocks the LLM to assert the agent calls the right tool. The langchain agent custom tool calling wiring is correct when both layers pass.

# test_tools.py
from my_agent import multiply, get_weather

def test_multiply():
    assert multiply.invoke({"a": 3, "b": 4}) == 12

def test_get_weather_stub():
    out = get_weather.invoke({"city": "Paris"})
    assert "Paris" in out

For the agent, mock ChatOpenAI with a fixed tool-call response using langchain_core.messages.AIMessage. This avoids burning tokens in CI and proves the executor routes to your function.

# test_agent.py
from langchain_core.messages import AIMessage
from langchain.agents import create_openai_tools_agent, AgentExecutor
from langchain_openai import ChatOpenAI
from my_agent import tools, prompt

def test_agent_calls_multiply():
    fake_llm = ChatOpenAI(model="gpt-4o-mini")
    fake_llm.bind_tools = lambda *a, **k: fake_llm
    fake_llm.invoke = lambda *a, **k: AIMessage(
        content="",
        tool_calls=[{"name": "multiply", "args": {"a": 2, "b": 5}, "id": "1"}]
    )
    agent = create_openai_tools_agent(fake_llm, tools, prompt)
    ex = AgentExecutor(agent=agent, tools=tools, handle_parsing_errors=True)
    res = ex.invoke({"input": "multiply 2 and 5"})
    assert "10" in res["output"]

Run pytest -q. Green tests confirm the langchain agent custom tool calling contract holds under controlled conditions.

Verification checklist

You have a working setup when:

  • pip install completes and .env loads without errors.
  • The agent prints a trace showing Action: multiply and Action: get_weather for the composite question.
  • pytest passes both tool and agent tests.
  • Swapping the model string in Step 2 routes to a different backend without code changes.
  • A malformed tool argument triggers a model retry rather than a process crash.

The langchain agent custom tool calling pattern scales by adding more @tool functions and keeping their schemas tight. Treat the tool layer as a typed API surface, and the model as a dynamic client you do not fully control.

Tagslangchainagentstool-callingpython

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All langchain agents & tool calling posts →