29 minute read

LangChain Agents with Tools Tutorial: Function Calling vs Traditional Tools

LangChain Agents with Tools Tutorial: Function Calling vs Traditional Tools

Imagine you have a super-smart helper, an “agent,” that can do many things. This agent needs tools to perform tasks, just like you might need a calculator or a weather app. In LangChain, a popular toolkit for building language model applications, agents use these tools to solve problems. But how your agent uses these tools can make a big difference.

Today, we’ll look at two main ways LangChain agents interact with their tools: the “traditional tool approach” and “function calling.” Understanding the function calling vs traditional tools langchain debate is super important. We will explore which method is better for different situations and why.

What are LangChain Agents?

Think of a LangChain Agent as a clever assistant powered by a large language model (LLM). This assistant doesn’t just answer questions; it can reason and act. It figures out what steps it needs to take to help you.

Agents use their smarts to decide if they need a tool or if they can answer directly. They pick the right tool for the job. For example, if you ask “What’s the weather like in Paris?”, the agent knows it needs a weather tool, not just its brain.

Tools for Agents: Giving Them Superpowers

Tools are like special gadgets that your agent can use. Each tool has a specific job, such as checking the time, doing math, or finding information online. Without tools, an agent is limited to what it already “knows” from its training data.

Tools allow agents to interact with the real world or perform complex calculations. They expand the agent’s abilities far beyond just chatting. Making good tools is key to building a powerful agent.

The Traditional Tool Approach

The “traditional tool approach” is how agents have used tools for a while in LangChain. In this method, you describe your tools using simple text. The agent reads these descriptions and tries to figure out when and how to use them.

You tell the agent the tool’s name, what it does, and what kind of input it needs. The agent then tries to match your request to these descriptions. It’s like giving your helper a list of instructions for each gadget they own.

How the Traditional Tool Approach Works

When you give an agent a task, it first thinks about the task. It looks at the descriptions of all the tools it has. Then, it tries to guess which tool is the best fit.

The agent generates a specific text output that includes the tool name and the input for that tool. This output is then processed by LangChain, which runs the tool. After the tool runs, the agent gets the result and continues its thinking.

Example: A Simple Calculator Tool (Traditional)

Let’s say you want your agent to have a calculator. You would define a tool that takes a math problem as text. The agent would see this tool description and use it when you ask a math question.

Here’s a simplified look at how you might set up a traditional calculator tool. This setup tells the agent exactly what the tool is for. You explain the tool’s purpose and how to use it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# This is a very simplified example for explanation
from langchain.agents import AgentExecutor, create_react_agent
from langchain_community.tools.base import BaseTool
from langchain_core.prompts import PromptTemplate
from langchain_community.llms import OpenAI
from typing import Type
from pydantic import BaseModel, Field

# Define a simple calculator tool (traditional style)
class CalculatorInput(BaseModel):
    expression: str = Field(description="mathematical expression to evaluate")

class CalculatorTool(BaseTool):
    name: str = "calculator"
    description: str = "useful for when you need to answer questions about math. Input should be a mathematical expression."
    args_schema: Type[BaseModel] = CalculatorInput

    def _run(self, expression: str):
        try:
            return str(eval(expression)) # DANGER: eval is insecure in real apps!
        except Exception as e:
            return f"Error: {e}"

    def _arun(self, expression: str):
        raise NotImplementedError("CalculatorTool does not support async")

# Make an instance of the tool
calculator = CalculatorTool()

# Define the prompt template for the agent
# In a real scenario, this would be more complex and usually handled by LangChain's create_react_agent
template = """Answer the following questions as best you can. You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}"""

prompt = PromptTemplate.from_template(template)

# For actual agent creation, you would use create_react_agent or similar
# This is illustrative, you'd typically need an LLM and specific agent type.
# For example:
# llm = OpenAI(api_key="YOUR_OPENAI_API_KEY")
# tools = [calculator]
# agent = create_react_agent(llm, tools, prompt)
# agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# Example of how an agent might interpret and use it (conceptual)
# agent_executor.invoke({"input": "What is 5 plus 3?"})

Pros of the Traditional Tool Approach

  • Flexibility: You can use almost any language model, not just specific ones. This gives you many choices for your LLM provider.
  • Simple Tool Definition: For simple tools, describing them can be quite straightforward. You just write a few sentences.
  • Broad Compatibility: Works with a wide range of LLMs and setups. It’s a very universal way to get agents working with external functions.

Cons of the Traditional Tool Approach

  • Reliance on Prompting: The agent heavily relies on the quality of your prompt to understand tools. If the prompt isn’t clear, the agent might struggle.
  • Less Reliable Tool Selection: The agent might sometimes pick the wrong tool or generate incorrect input. It’s like asking your helper to guess how a gadget works.
  • Higher Token Usage: The tool descriptions often have to be repeated in the prompt for every turn. This can use more “tokens” (parts of words) and cost more.
  • Stricter Output Format: The agent has to stick to a very specific text format for calling tools. If it makes a small mistake, the tool call might fail.

Function Calling Explained

“Function calling explained” is a newer and often more powerful way for agents to use tools. Instead of the agent guessing which tool to use from text, the language model itself can directly suggest a tool call. This is a special feature built into certain advanced language models.

When a language model supports function calling, you don’t just give it text descriptions. You give it actual structured definitions of your tools. The model then looks at your request and decides if one of these tools is perfect for the job. If it is, the model outputs a special instruction to run that tool, including all the necessary inputs in a structured way.

How OpenAI Function Calling Works

OpenAI function calling is a prime example of this technology. With OpenAI models like GPT-3.5 and GPT-4, you define your tools using a format similar to how you describe functions in computer code. These descriptions are given to the language model.

When you send a message to the model, it can respond in two main ways: either with a text reply, or with a “function call.” This function call is a structured message saying, “Hey, I think you should run this specific tool with these specific inputs.” LangChain then takes this structured call and runs the actual Python function behind your tool. It’s like your helper not just guessing, but being told exactly which button to press on a gadget and with what settings.

Example: Using it for a Weather Tool (Function Calling)

Let’s imagine you want your agent to tell you the weather. With function calling, you define a get_current_weather tool with clear inputs like location and unit (Celsius or Fahrenheit). The language model knows these inputs must be provided.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import os
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_community.utilities.openweathermap import OpenWeatherMapAPIWrapper
from langchain.tools import Tool
from langchain_openai import ChatOpenAI
from typing import Type
from pydantic import BaseModel, Field

# Ensure you have your API keys set up as environment variables
# os.environ["OPENWEATHERMAP_API_KEY"] = "YOUR_OPENWEATHERMAP_API_KEY"
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"

# 1. Define the tool's input schema using Pydantic
class WeatherInput(BaseModel):
    location: str = Field(description="The city and state, e.g., 'San Francisco, CA'")
    unit: str = Field(description="The unit of temperature, e.g., 'celsius' or 'fahrenheit'", default="fahrenheit")

# 2. Implement the tool's functionality
class OpenWeatherMapTool:
    def __init__(self):
        self.weather_api = OpenWeatherMapAPIWrapper()

    def get_current_weather(self, location: str, unit: str = "fahrenheit") -> str:
        """Get the current weather for a specific location and unit."""
        try:
            return self.weather_api.run(f"{location}, {unit}")
        except Exception as e:
            return f"Could not retrieve weather for {location}: {e}"

# 3. Create the LangChain Tool instance with function calling capabilities
openweathermap_wrapper = OpenWeatherMapTool()
weather_tool = Tool.from_function(
    func=openweathermap_wrapper.get_current_weather,
    name="get_current_weather",
    description="Get the current weather for a specific location. Use 'celsius' or 'fahrenheit' for unit.",
    args_schema=WeatherInput # This links the Pydantic schema for function calling
)

# You would then create an agent using an OpenAI model that supports function calling:
# llm = ChatOpenAI(model="gpt-4o", temperature=0)
# tools = [weather_tool]

# prompt = ChatPromptTemplate.from_messages([
#     ("system", "You are a helpful assistant."),
#     MessagesPlaceholder("chat_history", optional=True),
#     ("human", "{input}"),
#     MessagesPlaceholder("agent_scratchpad"),
# ])

# agent = create_openai_tools_agent(llm, tools, prompt)
# agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# Example usage (conceptual):
# response = agent_executor.invoke({"input": "What's the weather like in New York City in Celsius?"})
# print(response["output"])

You can learn more about how OpenAI function calling works in their official guides [Affiliate Link: OpenAI Function Calling Guide]. It’s a powerful feature that makes agents much smarter.

Pros of Function Calling

  • Higher Reliability: The language model is specifically trained to output structured function calls. This makes it much more reliable in picking the right tool and providing correct inputs.
  • Reduced Token Usage: Tool definitions are sent to the model in a more efficient way. They don’t have to be re-written in text in every prompt turn, saving tokens.
  • Better Developer Experience: Defining tools with clear schemas (like Pydantic models) makes them easier to manage and understand. This is a big win for developers.
  • Complex Tool Inputs: It handles complex inputs (like lists or objects) much better than the traditional method.
  • Natural Language to Structured Data: It’s excellent at converting your natural language request into structured data for your tools.

Cons of Function Calling

  • Provider Specific: Not all language models support function calling. It’s mainly a feature of advanced models from providers like OpenAI, Google, and Anthropic.
  • Learning Curve: You might need to learn about defining schemas (like Pydantic) if you’re not familiar with them.
  • Cost for Advanced Models: The models that support function calling are often more powerful and can sometimes be more expensive per token than simpler models. However, the efficiency gains can sometimes offset this.

Function Calling vs Traditional Tools LangChain: A Deep Dive

Now that we know both approaches, let’s compare them directly. The choice between function calling vs traditional tools langchain is important for your project. We will look at speed, accuracy, cost, and more.

Feature Traditional Tool Approach Function Calling
Reliability Relies on LLM’s prompt understanding, can be less consistent. LLM specifically trained for structured calls, highly reliable.
Token Efficiency Higher, tool descriptions often repeated in prompt. Lower, tool definitions are more efficiently conveyed to LLM.
Complexity Simpler for very basic tools, harder for complex inputs. Requires structured schemas, but handles complexity well.
LLM Compatibility Works with almost any LLM. Only works with LLMs specifically supporting function calling.
Developer Exp. Text-based tool descriptions, can be ambiguous. Structured schemas (e.g., Pydantic), clearer and easier to manage.
Performance Can involve more turns/retries if prompt fails. Often fewer turns, more direct tool invocation.
Cost Potentially higher due to token usage and retries. Potentially lower due to efficiency, despite higher model cost.

Performance Comparison

When we talk about performance comparison, we often mean how fast and smoothly things run.

  • Traditional Tools: These can sometimes be slower because the agent might take more “turns” to figure things out. If the agent misinterprets the prompt, it might try to call the wrong tool, fail, and then try again. This back-and-forth adds time.
  • Function Calling: This approach is generally faster and more direct. The language model directly suggests the tool call in a structured format. This usually means fewer guesses and fewer steps, leading to quicker answers. It’s like having a detailed instruction manual for your helper instead of just a vague idea.

To truly measure performance for your specific use case, you might want to use specialized performance benchmarking tools [Affiliate Link: Performance Benchmarking Tools]. These tools can help you compare the speed of different methods.

Reliability Differences

Reliability differences are about how consistently and accurately the agent uses its tools.

  • Traditional Tools: The agent’s ability to pick the right tool and give it the correct input depends on its “reasoning” from your text prompt. If your prompt isn’t perfect, or the task is tricky, the agent might make mistakes. It’s less reliable when the instructions are open to interpretation.
  • Function Calling: This method is much more reliable. The language model is specifically designed to understand your tool’s definition and create a valid function call. It’s like the language model having a built-in “tool expert” that knows exactly how each tool works and what it needs. This leads to fewer errors and more consistent results.

Cost Analysis

Let’s talk about money. Our cost analysis looks at which method might be cheaper.

  • Traditional Tools: These can sometimes cost more in total, even if the individual LLM calls are cheaper. Why? Because the tool descriptions are often included in every prompt, leading to higher “token usage.” More tokens mean more cost. Also, if the agent makes mistakes and has to retry, each retry costs money.
  • Function Calling: While the advanced models that support function calling might have a higher cost per token, the overall cost can actually be lower. This is because the tool definitions are sent more efficiently, reducing token usage per turn. The higher reliability also means fewer retries, saving you money. It’s about efficiency over raw price per token.

To get a clear picture of costs, you can use API cost calculators [Affiliate Link: API Cost Calculators] to estimate your expenses with different models and approaches.

Developer Experience

Developer experience refers to how easy and pleasant it is for you, the programmer, to work with a certain method.

  • Traditional Tools: Defining tools is simple if they are very basic. You just write a string description. However, for complex tools with many specific inputs, writing clear and unambiguous text can be hard. Debugging when the agent misinterprets your tool can also be frustrating.
  • Function Calling: This approach uses structured schemas (often Pydantic models in Python) to define tool inputs. While this might seem like more work upfront, it makes tool definitions very clear and robust. It’s much easier to manage complex tools, and the reliability of the LLM makes debugging easier because you know the tool call itself will likely be valid.

Complexity of Tool Definitions

  • Traditional Tool Approach: It’s great for simple tools that take one or two basic text inputs. Describing “a tool that adds two numbers” is easy. But imagine a tool that needs a list of items, a date, and a specific user ID. Describing that clearly in plain text for the agent to understand perfectly every time is tough.
  • Function Calling: This excels with complex tool inputs. Because you define a precise schema (like a form with specific fields), the language model knows exactly what type of information to extract for each part of the tool. It’s much better at turning natural language like “I want to order a pizza with pepperoni and mushrooms, extra cheese, delivered tomorrow at 7 PM to 123 Main St.” into a structured order_pizza function call with distinct arguments for toppings, delivery time, and address.

Token Efficiency

Token efficiency is about how many “tokens” (parts of words) are used to get a task done. Less tokens usually means lower cost and faster processing.

  • Traditional Tool Approach: In this method, the descriptions of all available tools are often sent to the LLM in the prompt for almost every decision it makes. This means if you have many tools, or long descriptions, you’re sending a lot of extra text repeatedly. This can make the prompt very long and increase token usage.
  • Function Calling: This is generally more token-efficient. The tool definitions are passed to the model in a special, structured way that doesn’t count against your prompt’s “visible” token count in the same way. The LLM then returns a concise, structured function call, which is also very token-efficient. It reduces the amount of “chatter” needed between the LLM and your code.

Response Quality

The quality of the response is how good and accurate the final answer is to your question.

  • Traditional Tool Approach: The response quality can sometimes suffer if the agent struggles to use tools correctly. If it calls the wrong tool, provides incorrect inputs, or misinterprets the tool’s output, the final answer might be wrong or incomplete. The quality relies heavily on the agent’s ability to “reason” with text.
  • Function Calling: Because the tool selection and input generation are much more reliable, OpenAI function calling often leads to higher response quality. The agent is more likely to correctly execute the required steps and thus provide a more accurate and complete answer. The model itself is guiding the tool use, leading to better outcomes.

When to Use Each Approach

Deciding when to use each method depends on your project’s needs.

Traditional Tools are Good When:

  • You need broad LLM compatibility: If you’re using a language model that doesn’t support function calling, this is your only option.
  • Your tools are very simple: For tools with basic string inputs and straightforward descriptions, the traditional method can be quick to set up.
  • Cost is your absolute top concern and your toolset is small: If you’re using very cheap, simple LLMs and have only a few tools, the token usage might not be a huge issue.
  • You’re experimenting or prototyping quickly: For initial tests, defining a tool with a simple string can be faster than setting up Pydantic schemas.

Function Calling is Great For:

  • You need high reliability and accuracy: When it’s critical that your agent always picks the right tool and uses it correctly, function calling shines.
  • Your tools have complex inputs: If your tools need more than simple text, like numbers, lists, dates, or specific formats, function calling handles this beautifully.
  • You want better developer experience: Structured tool definitions make your code cleaner, easier to understand, and less prone to errors.
  • You want to minimize token usage for complex agents: For agents that make many tool calls or have many tools, the efficiency gains of function calling can significantly reduce costs.
  • You are building production-ready applications: Its robustness makes it ideal for real-world use where errors need to be minimized.
  • You are using models that support it: If you’re already using OpenAI, Anthropic, or Google’s latest models, you should definitely consider function calling.

For more insights into making decisions about your LLM architecture, you might find our post on [Internal Link: Understanding LLM Limitations] helpful.

Hybrid Approaches

Can we combine the best of both worlds? Yes, hybrid approaches are possible! You might find situations where you want to mix and match.

For example, you could have a primary agent using function calling for its core, reliable tasks. Then, for very niche or experimental tools that aren’t yet well-defined, you might use a sub-agent that employs the traditional approach. This gives you both robustness and flexibility.

Another hybrid strategy could involve using function calling for most tools but having a “catch-all” traditional tool. This catch-all tool might use a search engine for anything the specific function-calling tools don’t cover. This way, the agent always has a fallback.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# Conceptual Hybrid Agent
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_community.tools import Tool
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from pydantic import BaseModel, Field

# Define a function-calling enabled tool (e.g., weather tool from above)
class WeatherInput(BaseModel):
    location: str = Field(description="The city, e.g., 'San Francisco'")

def get_weather(location: str) -> str:
    """Gets the current weather for a specified location."""
    # In a real app, this would call an external API
    if "London" in location:
        return "It's cloudy and 15°C in London."
    elif "New York" in location:
        return "It's sunny and 25°C in New York."
    else:
        return "Weather data not available for this location."

weather_tool_fc = Tool.from_function(
    func=get_weather,
    name="get_weather",
    description="Get the current weather for a specific city.",
    args_schema=WeatherInput
)

# Define a traditional tool (e.g., a simple web search)
# This uses a simpler text description that the LLM interprets
def web_search(query: str) -> str:
    """Performs a web search for a given query."""
    # In a real app, this would use a search API like Tavily or Google Search
    if "latest news" in query.lower():
        return "Today's top headlines: AI advancements, economic updates, sports scores."
    else:
        return f"Searching the web for: '{query}'..."

# For a traditional tool, you might need to wrap it differently
# In LangChain, a BaseTool can often be used for both, but the agent type matters.
# For simplicity, let's treat it as a callable the agent *might* interpret
# In a true hybrid with create_openai_tools_agent, the web_search would likely be
# another Tool.from_function but without args_schema for the LLM to call it based on *description*.
# Or, you'd have a separate React-style agent for traditional tools.
# For demo purposes, we'll conceptually have both available to an agent.

# For a truly hybrid setup, you might need two agents or a more complex router.
# Here's how you might *think* about adding a "traditional" like tool to an FC agent
# by giving it a generic description. It's not *truly* traditional tool logic,
# but the agent will rely on prompt to use it.
class GenericSearchInput(BaseModel):
    query: str = Field(description="The query to search for on the internet.")

web_search_tool_fc_style = Tool.from_function(
    func=web_search,
    name="internet_search",
    description="Useful for when you need to answer questions about current events or general knowledge not available from other tools. Input should be a search query.",
    args_schema=GenericSearchInput # Even traditional tools can benefit from schema if used with FC agent
)

# You'd then combine these tools for an agent
# llm = ChatOpenAI(model="gpt-4o", temperature=0)
# tools_hybrid = [weather_tool_fc, web_search_tool_fc_style] # Both are made FC-compatible here

# prompt_hybrid = ChatPromptTemplate.from_messages([
#     ("system", "You are a helpful assistant. You have access to specialized tools and the internet."),
#     MessagesPlaceholder("chat_history", optional=True),
#     ("human", "{input}"),
#     MessagesPlaceholder("agent_scratchpad"),
# ])

# agent_hybrid = create_openai_tools_agent(llm, tools_hybrid, prompt_hybrid)
# agent_executor_hybrid = AgentExecutor(agent=agent_hybrid, tools=tools_hybrid, verbose=True)

# print("\n--- Hybrid Agent Example ---")
# print("Asking for weather (FC):")
# agent_executor_hybrid.invoke({"input": "What's the weather in London?"})
# print("\nAsking for news (relies on description for 'internet_search'):")
# agent_executor_hybrid.invoke({"input": "Tell me the latest news."})

This conceptual example shows how tools defined for function calling can still include tools that conceptually perform “traditional” tasks like web search, relying on the LLM’s understanding of the description when mapping to the schema.

Migration Strategies

If you’ve been using the traditional tool approach and want to switch to function calling, you’ll need migration strategies. It’s not usually a simple flip of a switch, but it’s worth the effort for the benefits.

  1. Identify Compatible Tools: First, look at your existing tools. Which ones would benefit most from function calling? Tools with clear inputs and outputs are good candidates.
  2. Define Pydantic Schemas: For each tool you want to migrate, create a Pydantic BaseModel that describes its inputs perfectly. This is the new “instruction manual” for your tool.
  3. Update Tool Wrappers: Adjust your LangChain tool wrappers to use Tool.from_function and include your new Pydantic schemas. This tells LangChain how to pass the function’s arguments.
  4. Change Agent Type: Switch your agent creation from a traditional agent (like create_react_agent) to one that supports function calling (like create_openai_tools_agent).
  5. Test Thoroughly: This is the most crucial step. Test your migrated tools and agent with many different prompts and scenarios to ensure they work reliably.

For detailed help with planning your move, you might consider migration consulting services [Affiliate Link: Migration Consulting Services]. They can offer expert advice. You can also use comparison analysis templates [Affiliate Link: Comparison Analysis Templates ($29-59)] to help document your decisions and track your progress.

Provider Compatibility

Provider compatibility is a big factor when choosing your approach. Not every language model from every provider supports function calling.

  • Function Calling: This feature is primarily supported by advanced models from major providers.
    • OpenAI: GPT-3.5 Turbo, GPT-4, GPT-4o are excellent with function calling.
    • Google: Their Gemini models offer similar capabilities.
    • Anthropic: Claude models also have tool use features that work similarly.
    • Others: Some open-source models are starting to include this, but it’s not as widespread.
  • Traditional Tools: This method works with virtually any LLM because it only relies on the model’s ability to read text and follow instructions in a prompt. You can use open-source models, older models, or less sophisticated ones.

If you are tied to a specific LLM provider or model that doesn’t offer function calling, the traditional tool approach might be your only choice. However, if you have the flexibility, models with native function calling are often superior. You can find detailed breakdowns of different LLM features and LLM provider comparisons [Affiliate Link: LLM Provider Comparisons] to help you decide.

Practical Examples and Code Snippets

Let’s look at more practical function calling vs traditional tools langchain examples to see the code difference.

Example 1: Getting Current Time

We want a tool that simply tells us the current time.

Traditional Tool Implementation

Here, the description is just plain text. The LLM needs to understand that “current time” means using this tool.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import datetime
from langchain_community.tools.base import BaseTool

# Define a simple current time tool (traditional)
class GetCurrentTimeTool(BaseTool):
    name: str = "get_current_time"
    description: str = "useful for when you need to know the current date and time."

    def _run(self, query: str = None):
        """Returns the current date and time."""
        return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    def _arun(self, query: str = None):
        raise NotImplementedError("GetCurrentTimeTool does not support async")

current_time_traditional = GetCurrentTimeTool()

# In a traditional agent prompt, the description would be passed as text.
# The agent would then generate "Action: get_current_time\nAction Input: "
print(f"Traditional tool description: {current_time_traditional.description}")
Function Calling Implementation

With function calling, we don’t need args_schema if the function takes no arguments, or we use an empty BaseModel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import datetime
from langchain.tools import Tool
from pydantic import BaseModel, Field
from typing import Type

# Define the input schema (empty, as it takes no specific input)
class EmptyInput(BaseModel):
    pass

def get_current_time_func() -> str:
    """Returns the current date and time."""
    return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

current_time_fc = Tool.from_function(
    func=get_current_time_func,
    name="get_current_time",
    description="Get the current date and time.",
    args_schema=EmptyInput # Use an empty schema for no arguments
)

# When used with an OpenAI function-calling agent, the LLM will directly output
# {"name": "get_current_time", "arguments": {}}
print(f"Function calling tool name: {current_time_fc.name}")
print(f"Function calling tool description: {current_time_fc.description}")
print(f"Function calling tool args_schema: {current_time_fc.args_schema.schema_json(indent=2)}")

Example 2: Simple Math Calculator

We saw a conceptual calculator before. Let’s make it more concrete.

Traditional Tool Implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from langchain_community.tools.base import BaseTool
from pydantic import BaseModel, Field
from typing import Type

class MathExpressionInput(BaseModel):
    expression: str = Field(description="mathematical expression to evaluate")

class MathCalculatorTraditional(BaseTool):
    name: str = "math_calculator"
    description: str = "Useful for when you need to answer questions about math. Input should be a mathematical expression, e.g., '2 + 2'."
    args_schema: Type[BaseModel] = MathExpressionInput

    def _run(self, expression: str):
        try:
            return str(eval(expression)) # DANGER: eval is insecure in real apps!
        except Exception as e:
            return f"Error: {e}"

    def _arun(self, expression: str):
        raise NotImplementedError("MathCalculatorTraditional does not support async")

calculator_traditional = MathCalculatorTraditional()
print(f"\nTraditional Calculator description: {calculator_traditional.description}")
Function Calling Implementation

Here, the LLM will know exactly that expression is the input.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from langchain.tools import Tool
from pydantic import BaseModel, Field

class MathExpressionInputFC(BaseModel):
    expression: str = Field(description="mathematical expression to evaluate, e.g., '2 + 2'")

def evaluate_math_expression(expression: str) -> str:
    """Evaluates a mathematical expression."""
    try:
        return str(eval(expression)) # DANGER: eval is insecure in real apps!
    except Exception as e:
        return f"Error: {e}"

calculator_fc = Tool.from_function(
    func=evaluate_math_expression,
    name="math_calculator",
    description="Evaluates a given mathematical expression.",
    args_schema=MathExpressionInputFC
)

print(f"\nFunction Calling Calculator name: {calculator_fc.name}")
print(f"Function Calling Calculator description: {calculator_fc.description}")
print(f"Function Calling Calculator args_schema: {calculator_fc.args_schema.schema_json(indent=2)}")

Example 3: Weather Reporter

This example highlights structured inputs.

Traditional Tool Implementation

The LLM needs to parse “New York City in Celsius” from the prompt and put it into a single string for the tool.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from langchain_community.tools.base import BaseTool
from pydantic import BaseModel, Field
from typing import Type

class WeatherQueryInput(BaseModel):
    query: str = Field(description="The location and optional unit for weather, e.g., 'London Celsius'.")

class WeatherReporterTraditional(BaseTool):
    name: str = "weather_reporter"
    description: str = "Useful for getting current weather information. Input is a string like 'city unit'."
    args_schema: Type[BaseModel] = WeatherQueryInput

    def _run(self, query: str):
        # In a real app, parse 'query' string to extract location and unit
        # For simplicity, let's hardcode for demo
        query_lower = query.lower()
        if "london" in query_lower:
            unit = "celsius" if "celsius" in query_lower else "fahrenheit"
            return f"The weather in London is cloudy, 15°{unit.upper()[0]}."
        elif "new york" in query_lower:
            unit = "celsius" if "celsius" in query_lower else "fahrenheit"
            return f"The weather in New York is sunny, 25°{unit.upper()[0]}."
        return "Could not get weather for that location using traditional tool."

    def _arun(self, query: str):
        raise NotImplementedError("WeatherReporterTraditional does not support async")

weather_traditional = WeatherReporterTraditional()
print(f"\nTraditional Weather Reporter description: {weather_traditional.description}")
Function Calling Implementation

The LLM will directly map “New York City in Celsius” to location="New York City" and unit="celsius".

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from langchain.tools import Tool
from pydantic import BaseModel, Field

class WeatherQueryInputFC(BaseModel):
    location: str = Field(description="The city and state, e.g., 'San Francisco, CA'")
    unit: str = Field(description="The unit of temperature, e.g., 'celsius' or 'fahrenheit'", default="fahrenheit")

def get_weather_info(location: str, unit: str = "fahrenheit") -> str:
    """Get the current weather for a specific location and unit."""
    # In a real app, this would call an external API like OpenWeatherMap
    if "london" in location.lower():
        return f"The weather in London is cloudy, 15°{unit.upper()[0]}."
    elif "new york" in location.lower():
        return f"The weather in New York is sunny, 25°{unit.upper()[0]}."
    return f"Could not get weather for {location} with unit {unit} using function calling."

weather_fc = Tool.from_function(
    func=get_weather_info,
    name="get_weather_info",
    description="Get the current weather for a specific location and unit.",
    args_schema=WeatherQueryInputFC
)

print(f"\nFunction Calling Weather Reporter name: {weather_fc.name}")
print(f"Function Calling Weather Reporter description: {weather_fc.description}")
print(f"Function Calling Weather Reporter args_schema: {weather_fc.args_schema.schema_json(indent=2)}")

These examples clearly show how much more structured and precise the function calling approach is. The model doesn’t just guess; it’s guided by a clear contract for each tool.

Advanced Considerations

Building agents with tools involves more than just picking an approach. You also need to think about how to handle issues.

Error Handling

No system is perfect, and tools can fail. You need a robust plan for error handling.

  • Traditional Tools: If a tool call fails, the agent might get an error message in plain text. It then needs to figure out what to do next, which can be difficult and lead to more retries or a complete failure.
  • Function Calling: Errors are still possible, but because the tool calls are structured, you can often add specific error handling logic within your tool functions. The agent can then be designed to retry, suggest alternative tools, or inform the user more gracefully.

Security

Security is always important, especially when agents can access external systems.

  • Traditional Tools: Since the agent generates tool calls based on open-ended text parsing, there’s a slight risk of prompt injection if not handled carefully. A malicious prompt could try to trick the agent into calling a tool in an unintended way.
  • Function Calling: While still needing careful thought, the structured nature of function calls can add a layer of security. The model is less likely to “invent” a tool call outside of the defined schemas. You should always validate any input coming from the LLM before executing a tool.

Testing

Thorough testing is crucial for reliable agents.

  • Traditional Tools: Testing can be harder because the agent’s behavior is more dynamic and depends heavily on prompt wording. You need to test many variations to ensure consistency.
  • Function Calling: Testing is often more straightforward. Because tool calls are structured, you can write unit tests for your tool functions independently. You can also mock the LLM’s function call output to test your agent’s reaction to specific tool invocations.

When designing complex agent systems, it’s wise to document your choices. Architecture decision records [Affiliate Link: Architecture Decision Records] can help you keep track of why you chose a certain approach. To deepen your understanding, consider technical comparison courses [Affiliate Link: Technical Comparison Courses] that dive into the nitty-gritty of these technologies.

Conclusion

You’ve now explored the exciting world of LangChain agents and their tools. We’ve seen two main ways they can operate: the traditional tool approach and the powerful function calling. While traditional tools offer broad compatibility and simplicity for basic tasks, OpenAI function calling and similar features from other providers bring higher reliability, efficiency, and a better developer experience for complex applications.

The function calling vs traditional tools langchain discussion boils down to a choice based on your project’s specific needs for performance comparison, reliability differences, and cost analysis. For most modern, production-ready LangChain agents, especially those using advanced LLMs, function calling is often the superior choice. However, remember that hybrid approaches can sometimes offer the best of both worlds.

By understanding when to use each method and considering factors like provider compatibility and migration strategies, you can build smarter, more robust, and more efficient agents. Happy building!

Leave a comment