HOW-TO · RAG
How to Build Custom Tools for Agents
Target environment
Ubuntu 24.04 · Ollama 0.4.x
PREREQUISITES
Agent framework, Python 3.10+
What this does
Custom tools extend an agent's capabilities beyond built-in functions. Each tool wraps a Python function with a JSON schema declaration that the LLM uses to understand when and how to call it.
Steps
- Write a tool as a plain function with docstring. The docstring becomes the tool description.
def get_stock_price(symbol: str) -> str:
"""Get the current stock price for a given ticker symbol."""
prices = {"AAPL": "175.32", "GOOGL": "142.50", "MSFT": "378.20"}
return prices.get(symbol.upper(), f"Price for {symbol} not found")
- Wrap with LangChain's
@tooldecorator. This auto-generates the JSON schema.
from langchain.tools import tool
@tool
def get_stock_price(symbol: str) -> str:
"""Get the current stock price for a given ticker symbol."""
prices = {"AAPL": "175.32", "GOOGL": "142.50", "MSFT": "378.20"}
return prices.get(symbol.upper(), f"Price for {symbol} not found")
print(get_stock_price.name) # get_stock_price
print(get_stock_price.args) # {'symbol': {'type': 'string'}}
- Create a tool class for complex tools. Extend
BaseToolfor tools with state.
from langchain.tools import BaseTool
from typing import Type, Optional
class DatabaseQueryTool(BaseTool):
name: str = "query_database"
description: str = "Execute SQL queries against the analytics database"
db_connection: str = "sqlite:///analytics.db"
def _run(self, sql: str) -> str:
import sqlite3
conn = sqlite3.connect(self.db_connection)
cursor = conn.cursor()
cursor.execute(sql)
rows = cursor.fetchall()
conn.close()
return str(rows)
def _arun(self, sql: str) -> str:
raise NotImplementedError("Async not implemented")
- Add structured arguments with Pydantic. Define an
ArgsSchemafor validation.
from pydantic import BaseModel, Field
class SendEmailInput(BaseModel):
to: str = Field(description="Recipient email address")
subject: str = Field(description="Email subject")
body: str = Field(description="Email body content")
priority: str = Field(default="normal", enum=["low", "normal", "high"])
@tool(args_schema=SendEmailInput)
def send_email(to: str, subject: str, body: str, priority: str = "normal") -> str:
"""Send an email to a recipient."""
return f"Email sent to {to} with priority {priority}"
- Include error handling inside the tool. Tools should never raise unhandled exceptions.
@tool
def safe_file_read(path: str) -> str:
"""Read the contents of a file safely."""
import os
if not os.path.exists(path):
return f"Error: file {path} not found"
if not path.endswith(".txt"):
return f"Error: only .txt files allowed"
try:
with open(path, "r") as f:
return f.read()
except Exception as e:
return f"Error reading file: {e}"
Verification
python -c "
from langchain.tools import tool
@tool
def add(a: int, b: int) -> int:
'''Add numbers.'''
return a + b
print(add.invoke({'a': 3, 'b': 4}))
# Expected: 7
"
Common failures
- Missing type hints. Without type annotations, the schema generator cannot infer parameter types, leading to
Anytypes that confuse the LLM. - Side effects without idempotency. Tools that insert records or send emails may be called multiple times on retry. Design tools to be idempotent.
- Tool output too verbose. Returning megabytes of data exhausts the context window. Limit output length and provide pagination options.
- Version mismatch - The installed package or runtime differs from the command shown; check the version first and rerun the smallest verification command.
- Local environment drift - Another service, virtual environment, model, or path is being used; print the active binary path and configuration before changing the guide steps.
Related guides
- How to Register and Call Custom Tools in Agents
- How to Use OpenAI Function Calling with Tools