Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›LangChain›Custom Tool Creation

Agents and Tools

Custom Tool Creation

Custom tools enable your LLM agents to interact with proprietary APIs, databases, or local file systems that are not covered by pre-built libraries. By defining clear input schemas and structured descriptions, you allow the agent to reason about when and how to invoke your specific logic. Mastering this pattern is essential for transforming a static conversational model into a versatile autonomous system that performs real-world actions.

The Decorator Pattern for Simple Tools

The most fundamental way to define a tool in LangChain is using the @tool decorator. This approach abstracts away the boilerplate logic required to map function signatures to tool schemas. When you wrap a function with @tool, LangChain automatically inspects the type hints, docstring, and function arguments to generate a structured JSON schema. This schema is the bridge between your code and the language model, telling the model exactly what the function does and what parameters it requires. Crucial to this process is the docstring; the agent relies entirely on this text to decide whether your tool is relevant to a specific user request. If your docstring is vague, the agent will frequently misinterpret the tool's purpose. By explicitly defining the types of each argument, you ensure that the input provided by the model is validated before your function executes, preventing runtime errors in your integration logic.

from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Returns the current weather for a specific city name."""
    # Imagine a real API call here
    return f"The weather in {city} is sunny and 25 degrees."

Defining Tools with Tool Classes

While the decorator is efficient for simple tasks, inheriting from the BaseTool class offers complete control over the tool's execution lifecycle. This is particularly useful when you need to manage state, handle complex asynchronous operations, or inject external dependencies like API clients. By defining a class, you explicitly declare the 'name', 'description', and 'args_schema'. The 'args_schema' is a Pydantic model that provides the language model with a rigorous definition of the input parameters, including descriptive field names and default values. This is more robust than using decorator-inferred schemas because it forces you to account for edge cases and input validation rules. When the agent interacts with a BaseTool subclass, it gains access to advanced features like error handling and logging, which are critical when working with unreliable third-party endpoints or non-deterministic data sources that might return malformed data.

from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field

class CalculatorInput(BaseModel):
    a: int = Field(description="First number")
    b: int = Field(description="Second number")

class MultiplyTool(BaseTool):
    name: str = "multiply_numbers"
    description: str = "Use this to multiply two integers."
    args_schema: type[BaseModel] = CalculatorInput

    def _run(self, a: int, b: int) -> int:
        return a * b

Handling Asynchronous Tool Execution

In production environments, blocking the main execution thread while waiting for an I/O-bound task like a network request or database query is detrimental to performance. LangChain supports asynchronous tool execution through the _arun method. When you define a custom tool, you should always implement an asynchronous version of your logic to ensure that your agent can handle concurrent tasks if required. The language model itself does not execute the code directly; instead, it outputs an action request. LangChain's orchestrator receives this request and invokes the appropriate tool function. By providing an _arun method, you allow the framework to use efficient asynchronous event loops. If you fail to define the _arun method, the default behavior might attempt to run the blocking _run method in an async context, which can lead to unexpected delays or deadlocks in highly concurrent environments like web servers or real-time agents.

import asyncio

@tool
async def async_search_db(query: str) -> str:
    """Perform a non-blocking search against the database."""
    # Simulating non-blocking I/O
    await asyncio.sleep(0.1)
    return f"Search results for: {query}"

Managing Tool State and Context

Real-world tools often need more than just the raw input provided by the language model. You might need to maintain an authentication token, a database connection pool, or a specific user's session identifier throughout the tool's lifespan. By using the class-based approach, you can initialize these dependencies in the __init__ method of your tool. This allows your tool to act as a stateful client for your internal services. The key here is encapsulation; the agent should not be responsible for managing your API keys or session tokens. Instead, the tool handles this context internally, exposing only a clean, simple interface to the language model. This separation of concerns simplifies testing, as you can mock the external dependencies in the constructor while keeping the interaction logic with the language model identical. Properly managing state ensures that your tools remain consistent across multiple turns in a single conversation.

class AuthTool(BaseTool):
    api_key: str = "SECRET_123"
    name: str = "secure_query"
    description: str = "Queries the secure API using a managed key."

    def _run(self, query: str) -> str:
        # Accesses the internal api_key
        return f"Querying {query} with {self.api_key}"

Structuring Complex Input with Pydantic

When a tool requires multiple parameters or nested configurations, simple string-based inputs are insufficient. Pydantic models provide a way to pass complex, structured data to your tools. When the language model attempts to call such a tool, it uses its reasoning capabilities to map conversational fragments into the fields of your Pydantic schema. This works because LangChain passes the schema definition to the model as part of the system prompt. For example, if you are building a tool that performs a calendar entry, you can define a Pydantic model with fields like 'start_time', 'end_time', and 'participants'. The model will then try to extract those specific values from the user's intent. Using strict Pydantic models allows you to enforce data integrity, ensuring that if a user provides a malformed date or missing participant, the tool rejects the call before your business logic is triggered.

from pydantic import BaseModel, Field

class CalendarEntry(BaseModel):
    event_name: str = Field(description="Title of the meeting")
    duration: int = Field(description="Duration in minutes")

@tool(args_schema=CalendarEntry)
def create_event(event_name: str, duration: int) -> str:
    """Schedules a new meeting in the calendar."""
    return f"Scheduled {event_name} for {duration} minutes."

Key points

  • The @tool decorator is the fastest way to register a function as an agent-executable tool.
  • Docstrings serve as the primary source of truth for the LLM to understand how and when to use a tool.
  • Inheriting from BaseTool allows developers to implement complex, stateful logic with external dependencies.
  • Input validation is best handled via Pydantic schemas which define the required structure for the LLM.
  • Asynchronous execution via _arun is essential for performance in I/O-bound applications.
  • Encapsulating configuration and keys within tool classes prevents sensitive information from leaking into the prompt.
  • Tools act as a functional bridge between the agent's reasoning capabilities and real-world external APIs.
  • Properly defining error handling within the _run method ensures that agent execution remains stable even if a tool fails.

Common mistakes

  • Mistake: Forgetting to provide a docstring for a custom tool. Why it's wrong: The LLM relies on the docstring to understand what the tool does and when to call it. Fix: Always include a detailed docstring explaining the tool's purpose and input parameters.
  • Mistake: Defining tool inputs without type hints. Why it's wrong: LangChain needs type hints to generate the JSON schema that informs the LLM about the tool's arguments. Fix: Use standard Python type annotations for all tool parameters.
  • Mistake: Handling errors by returning a raw Python exception. Why it's wrong: This can crash the agent loop or confuse the LLM with unparseable stack traces. Fix: Return a descriptive error message as a string so the LLM can attempt to correct its request.
  • Mistake: Not using the @tool decorator effectively. Why it's wrong: Creating manual BaseTool subclasses is often unnecessarily complex and error-prone compared to the decorator. Fix: Use the @tool decorator for simple, functional tool creation.
  • Mistake: Assuming the LLM will always provide the correct argument types. Why it's wrong: LLMs can occasionally hallucinate parameter types or formats. Fix: Implement input validation inside your tool logic to ensure safety.

Interview questions

What is the primary purpose of creating a custom tool in LangChain?

The primary purpose of a custom tool in LangChain is to extend the capabilities of an LLM beyond its pre-trained knowledge. While LLMs are powerful, they cannot inherently interact with real-time data, execute private API calls, or perform specific business logic. By wrapping functions in a tool, we provide the agent with a formal interface to perform actions, effectively allowing the model to bridge the gap between static text processing and dynamic, external system interaction.

How do you implement a simple custom tool using the @tool decorator?

Implementing a custom tool is straightforward using the @tool decorator, which automatically parses the function's signature to create a tool schema for the LLM. You define a Python function with clear type hints and a docstring; the docstring becomes the description used by the agent to decide when to call the tool. For example: '@tool def get_weather(city: str): """Get current weather for a city""" return f"Weather in {city} is sunny."' The decorator handles the JSON schema extraction, allowing the agent to invoke the function seamlessly.

Why is the docstring so critical when defining a LangChain custom tool?

The docstring is arguably the most important part of a tool definition because LangChain agents operate as reasoning engines that rely entirely on these descriptions to perform function calling. The model does not look at your code; it looks at the docstring to understand what the tool does, what parameters it expects, and under what circumstances it should be invoked. If the description is vague or inaccurate, the agent will frequently fail to select the tool or will pass incorrect arguments, rendering the tool useless regardless of how well the underlying Python code is written.

Compare the @tool decorator approach versus subclassing the BaseTool class.

Using the @tool decorator is preferred for simple, stateless functions because it requires minimal boilerplate and automatically infers the tool's schema from type hints and docstrings. However, subclassing the BaseTool class is necessary when you need more control, such as managing internal state, handling complex error logic, or integrating asynchronous operations. BaseTool provides a formal structure to define a 'run' method and an '_arun' method, which is essential for more robust, production-grade applications that require explicit schema definitions or custom lifecycle management.

How do you handle errors within a custom tool to ensure the agent remains functional?

When a custom tool encounters an error, it is vital to return a descriptive string explaining the failure rather than letting the Python process crash. If an error is caught, you should format the output as a clear error message, such as 'Tool failed due to API rate limit: try again in 60 seconds.' This allows the LLM to 'read' the error message, understand why the action failed, and decide whether to retry, switch strategies, or inform the user, thereby maintaining the agent's overall reasoning loop.

How can you pass complex arguments to a tool using Pydantic models for structured data?

For tools requiring complex inputs, LangChain allows you to define the tool's input schema using a Pydantic model. By setting 'args_schema=MyModel' within the @tool decorator or BaseTool subclass, you force the LLM to structure its input into a predefined format. This is superior to basic string arguments because it enforces strict validation on types and fields. It ensures the LLM generates a valid JSON object matching the Pydantic schema, preventing runtime errors that occur when passing loosely typed data into sensitive internal functions or external APIs.

All LangChain interview questions →

Check yourself

1. When defining a custom tool using the @tool decorator, what is the primary purpose of the docstring?

  • A.To provide a description for the user interface
  • B.To define the logic that the tool executes
  • C.To act as the instruction set for the LLM to understand when to invoke the tool
  • D.To satisfy Python syntax requirements
Show answer

C. To act as the instruction set for the LLM to understand when to invoke the tool
The docstring is parsed into the tool's schema, which the LLM reads to decide whether to call the tool. Options 1 and 4 are incorrect because the docstring isn't primarily for the UI or Python syntax, and option 2 is incorrect because the function body, not the docstring, defines the logic.

2. If you need a tool to accept multiple complex arguments, what is the best practice for LangChain integration?

  • A.Pass a single string and manually parse it inside the tool
  • B.Use Pydantic models to define the input schema
  • C.Use only positional arguments in the function definition
  • D.Create a separate tool for every single argument
Show answer

B. Use Pydantic models to define the input schema
Using Pydantic models allows LangChain to strictly enforce and document complex inputs. Option 1 is fragile, option 3 ignores best practices for clarity, and option 4 is inefficient and breaks the tool's modularity.

3. Why should you catch exceptions within your custom tool function?

  • A.To prevent the LLM from seeing any output
  • B.To allow the tool to return a meaningful error message to the LLM
  • C.To automatically retry the tool call
  • D.To improve the speed of the tool execution
Show answer

B. To allow the tool to return a meaningful error message to the LLM
Returning an error message allows the LLM to understand the failure and potentially try again with different inputs. The other options are incorrect because crashing the agent or hiding information prevents the agent from recovering from errors.

4. Which of the following describes the role of the 'args_schema' parameter in a custom tool?

  • A.It defines the return format of the tool
  • B.It determines the authentication method for the tool
  • C.It provides a structured schema for the tool inputs when the decorator's automatic inference is insufficient
  • D.It manages the historical memory of the tool
Show answer

C. It provides a structured schema for the tool inputs when the decorator's automatic inference is insufficient
The args_schema is explicitly used to define how the LLM should format its arguments. Option 1 is for return types, option 2 is for security, and option 4 is irrelevant as memory is managed at the agent level.

5. If an LLM consistently fails to call your tool, what is the first thing you should check?

  • A.The file name where the tool is defined
  • B.Whether the docstring accurately and clearly explains the tool's utility
  • C.The number of characters in the function name
  • D.The operating system where the agent is running
Show answer

B. Whether the docstring accurately and clearly explains the tool's utility
If the docstring is vague, the LLM won't know when to use the tool. The other options relate to infrastructure or style and have no impact on the LLM's decision-making process regarding tool selection.

Take the full LangChain quiz →

← PreviousBuilt-in Tools — Search, Calculator, Python REPLNext →ReAct Agent

LangChain

24 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app