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 installcompletes and.envloads without errors.- The agent prints a trace showing
Action: multiplyandAction: get_weatherfor the composite question. pytestpasses both tool and agent tests.- Swapping the
modelstring 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.