Tool Calling in LangChain
Large Language Models (LLMs) excel at reasoning and natural language generation, but they fundamentally lack the ability to take actions, interact with external systems, or fetch real-time state. Tool calling bridges this gap, allowing language models to interface with external APIs, databases, and custom Python routines to build actionable generative systems and autonomous agents.
1. The Core Limitation of LLMs and the Need for Tools
LLMs possess two primary capabilities:
- Reasoning: Deconstructing complex user prompts and planning how to address them.
- Output Generation: Accessing parametric memory (weights acquired during pre-training) to synthesize coherent, contextually relevant textual responses.
Despite these strengths, base LLMs operate in a closed sandbox. They cannot natively:
- Execute queries or updates against external databases.
- Post content to social platforms or external endpoints (e.g., LinkedIn, X).
- Query real-time data sources (e.g., live weather reports or dynamic currency conversion rates).
- Reliably perform strict mathematical operations or execute shell scripts.
Real-Life Analogy
An LLM can be compared to a brilliant human who possesses deep domain knowledge and persuasive speech, but has no hands or feet. The person can reason and formulate the ideal plan, but cannot physically pick up a phone, type on a terminal, or modify the physical world. Tools serve as the hands and feet of the model, allowing its cognitive capabilities to interact with external environments.
2. The Four Stages of the Tool Calling Lifecycle
Connecting an LLM to external operations is not a single function call. It follows a structured four-stage architecture:
3. Tool Creation and Schema Inspection
In LangChain, tools are typically constructed using the @tool decorator from langchain_core.tools. The decorator converts a standard Python function into a BaseTool (an instance of Runnable).
A valid tool requires:
- Clear Function Signature: Explicit Python type hints (
int,str,float) defining what the function accepts. - Detailed Docstring: Clear instructions explaining the tool's purpose. The LLM reads this description to determine when the tool is relevant.
from langchain_core.tools import tool
@tool
def multiply(a: int, b: int) -> int:
"""Given two numbers a and b, this tool returns their product."""
return a * b
Every registered tool exposes metadata attributes that define its API contract:
multiply.name: The canonical identifier used by the LLM ("multiply").multiply.description: The semantic manual used by the LLM to evaluate relevance.multiply.args: The Pydantic-derived JSON schema specifying parameters and expected types.
# Inspecting tool properties
print(multiply.name)
# Output: multiply
print(multiply.description)
# Output: Given two numbers a and b, this tool returns their product.
print(multiply.args)
# Output: {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
4. Tool Binding
Tool binding registers the tool's metadata with the model. This is executed using the .bind_tools() method on supported chat models (such as ChatOpenAI).
Note: Not all LLMs support tool binding natively. The underlying provider and architecture must support tool/function calling APIs (e.g., OpenAI GPT-3.5/4 models, Anthropic Claude, Google Gemini).
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Registering tools with the LLM
llm_with_tools = llm.bind_tools([multiply])
During binding:
- LangChain converts the Python tool signatures into the provider's expected JSON format (e.g., OpenAI function schemas).
- The model receives these definitions alongside system prompts, making it aware of available tools and their required arguments.
5. Tool Calling Mechanics
When an LLM bound to tools receives a user message, its reasoning step evaluates whether the user's query can be resolved directly or requires external execution.
Critical Architecture Insight: The LLM Does Not Execute Code
A common misconception among engineers is that the LLM invokes the function, connects to the database, or runs the calculation itself.
The LLM does not execute anything. It functions strictly as an advisory engine. It inspects the prompt, matches it against available tool schemas, and emits a structured output specifying:
- Which tool to invoke (
name). - What parameters to pass (
args). - A unique correlation identifier (
id).
response = llm_with_tools.invoke("Can you multiply 3 with 10?")
print(response.tool_calls)
# Output:
# [{'name': 'multiply', 'args': {'a': 3, 'b': 10}, 'id': 'call_12345xyz', 'type': 'tool_call'}]
Why this design matters: Granting language models direct, unmoderated code execution access presents severe safety, security, and deterministic reliability risks. Delegating execution to the application layer gives developers complete control over sandboxing, rate-limiting, authentication, input sanitization, and human-in-the-loop approvals.
6. Tool Execution and Message Architecture
Once the LLM emits a tool call, the host application executes the function. The output must then be returned to the model within a structured message sequence.
LangChain relies on four primary message abstractions:
HumanMessage: Represents user input.AIMessage: Represents model output, including potentialtool_calls.SystemMessage: Sets core behavioral instructions.ToolMessage: Represents the output of a completed tool execution.
Executing with the Full Tool Call Payload
Passing the entire tool call dictionary into tool.invoke() wraps the execution result inside a formal ToolMessage, binding it to the original tool call ID:
tool_call = response.tool_calls[0]
tool_message = multiply.invoke(tool_call)
print(type(tool_message))
# Output: <class 'langchain_core.messages.tool.ToolMessage'>
print(tool_message)
# Output: ToolMessage(content='30', name='multiply', tool_call_id='call_12345xyz')
7. The Complete End-to-End Orchestration Loop
To complete the flow, the application aggregates the entire interaction history—HumanMessage, AIMessage (with tool calls), and ToolMessage—and passes it back to the LLM. With this full context, the model synthesizes a final natural language answer.
from langchain_core.messages import HumanMessage
# Initialize conversation history
messages = [HumanMessage(content="Can you multiply 3 with 1000?")]
# Step 1: Query the model with bound tools
ai_response = llm_with_tools.invoke(messages)
messages.append(ai_response)
# Step 2: Check for tool calls and execute
if ai_response.tool_calls:
for tool_call in ai_response.tool_calls:
if tool_call["name"] == "multiply":
tool_output_msg = multiply.invoke(tool_call)
messages.append(tool_output_msg)
# Step 3: Send complete context back to the LLM for final response synthesis
final_response = llm_with_tools.invoke(messages)
print(final_response.content)
# Output: "The product of 3 and 1000 is 3000."
8. Real-World Application: Dynamic Currency Conversion
A complete implementation demonstrates how to build an application that combines real-time API integrations with dependency handling between multiple tools.
The Problem
When a user asks: "What is the conversion factor between USD and INR, and what is 10 USD in INR?", two distinct operations are required:
- Query a live API to get the current exchange rate.
- Multiply the target amount by that dynamic rate.
Because the exchange rate fluctuates constantly, relying on the model's static training data produces incorrect, outdated figures.
Implementing the Tools
Using the ExchangeRate-API endpoint:
https://v6.exchangerate-api.com/v6/{API_KEY}/pair/{BASE}/{TARGET}
import requests
from langchain_core.tools import tool
API_KEY = "your_actual_api_key_here"
@tool
def get_conversion_factor(base_currency: str, target_currency: str) -> float:
"""This function fetches the currency conversion factor between a given base currency and target currency."""
url = f"https://v6.exchangerate-api.com/v6/{API_KEY}/pair/{base_currency}/{target_currency}"
response = requests.get(url)
data = response.json()
return data["conversion_rate"]
9. Dependency Management via InjectedToolArg
The Multi-Tool Dependency Trap
When presented with the compound query, the LLM attempts to plan both actions simultaneously and generates two tool calls:
get_conversion_factor(base_currency="USD", target_currency="INR")convert(base_currency_value=10, conversion_rate=???)
Because the LLM has not yet received the output from get_conversion_factor, it cannot know the true rate. Left unconstrained, it will fall back to its internal parametric memory, hallucinating an outdated rate (e.g., or ) to satisfy the argument schema:
The Solution: Annotating Injected Arguments
To prevent the model from guessing the dynamic rate, LangChain provides InjectedToolArg. Tagging a parameter with Annotated[..., InjectedToolArg] instructs the LLM to omit that argument during tool calling. The developer then programmatically injects the dynamic value at runtime.
from typing import Annotated
from langchain_core.tools import InjectedToolArg
@tool
def convert(
base_currency_value: int,
conversion_rate: Annotated[float, InjectedToolArg]
) -> float:
"""Given a currency conversion rate, this function calculates the target currency value from a given base currency value."""
return base_currency_value * conversion_rate
By hiding conversion_rate from the LLM's required schema, the model populates only base_currency_value:
# LLM tool call output for 'convert':
# {'name': 'convert', 'args': {'base_currency_value': 10}, ...}
10. End-to-End Currency Conversion Workflow
Here is the complete implementation that binds the tools, coordinates their dependencies, executes the calls, and generates the final response:
import json
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
# 1. Instantiate Model and Bind Tools
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools([get_conversion_factor, convert])
# 2. Setup Message History with the User Query
query = "What is the conversion factor between USD and INR, and based on that can you convert 10 USD to INR?"
messages = [HumanMessage(content=query)]
# 3. First LLM Invocation
ai_message = llm_with_tools.invoke(messages)
messages.append(ai_message)
# 4. Sequential Execution with Dynamic Injection
extracted_conversion_rate = None
for tool_call in ai_message.tool_calls:
if tool_call["name"] == "get_conversion_factor":
# Execute Tool 1
t_msg_1 = get_conversion_factor.invoke(tool_call)
messages.append(t_msg_1)
# Parse the JSON string to extract the dynamic conversion rate
parsed_content = json.loads(t_msg_1.content) if isinstance(t_msg_1.content, str) else t_msg_1.content
extracted_conversion_rate = parsed_content
elif tool_call["name"] == "convert":
# Manually inject the extracted rate into Tool 2's arguments
tool_call["args"]["conversion_rate"] = extracted_conversion_rate
# Execute Tool 2
t_msg_2 = convert.invoke(tool_call)
messages.append(t_msg_2)
# 5. Final LLM Invocation with Complete History
final_summary = llm_with_tools.invoke(messages)
print(final_summary.content)
Execution Output:
The exchange rate between USD and INR is approximately 85.34.
Based on this rate, converting 10 USD yields 853.40 INR.
11. Deterministic Chains vs. Autonomous AI Agents
While this implementation utilizes tools, tool calling, and dynamic injection, it is not an autonomous AI agent. It is a deterministic orchestration chain.
Summary and Key Takeaways
- LLM Capabilities & Limitations: Models excel at linguistic reasoning and text generation, but cannot execute code, fetch dynamic state, or run computational tasks on their own.
- The Role of Tools: Tools serve as external interfaces (APIs, databases, scripts) that provide the execution layer for language models.
- Tool Binding vs. Execution:
bind_tools()only exposes functional schemas to the model. The LLM suggests invocations through structured JSON payloads; the developer's runtime environment handles execution. - ToolMessage Role: Results from a tool invocation are wrapped inside a
ToolMessagewith a matchingtool_call_idand returned to the model. - The Full Context Window: The LLM synthesizes natural language answers only after the complete message history—
[HumanMessage, AIMessage, ToolMessage(s)]—is passed back to its context window. - Handling Injected Arguments: When tools depend on upstream dynamic data, mark those parameters with
Annotated[..., InjectedToolArg]. This hides them from model generation and lets you safely inject the values at runtime. - Chains vs. Agents: Orchestrating tools with manual loops produces deterministic workflows. True AI agents execute autonomously using iterative reasoning loops (e.g., ReAct) without requiring hardcoded orchestration.