CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j-core

Core classes and interfaces of LangChain4j providing foundational abstractions for LLM interaction, RAG, embeddings, agents, and observability

Overview
Eval results
Files

tools.mddocs/

Tools and Function Calling

Package: dev.langchain4j.agent.tool Thread-Safety: Tool instances should be stateless or synchronized Validation: Always validate tool inputs - LLMs can provide invalid data

Annotation-based tool definition for LLM function calling with automatic specification generation. LangChain4j provides a declarative approach to defining tools that LLMs can invoke.

Core Annotations

Tool Annotation

Target: Methods Retention: Runtime Thread-Safety: N/A (annotation)

Marks methods as tools for LLM function calling.

/**
 * Marks a method as a tool that can be called by the LLM
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Tool {
    /**
     * Optional tool name (defaults to method name)
     * @return Tool name (non-null, max 64 chars recommended)
     */
    String name() default "";

    /**
     * Tool description (helps LLM understand when to use it)
     * Multi-line descriptions supported via array
     * @return Tool description (non-null, max 1024 chars recommended)
     */
    String[] value() default {};

    /**
     * Controls whether result is returned to LLM or directly to user
     * TO_LLM (default): Result goes back to LLM for processing
     * IMMEDIATE: Result goes directly to user, bypassing LLM
     * @return Return behavior (non-null)
     */
    ReturnBehavior returnBehavior() default ReturnBehavior.TO_LLM;

    /**
     * JSON string for LLM-provider-specific metadata
     * Can be used to pass additional provider-specific configuration
     * @return JSON metadata string (default empty object)
     * @since 1.10.0
     * @Experimental Currently only supported by langchain4j-anthropic
     */
    String metadata() default "{}";
}

Usage Example:

import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.exception.ToolExecutionException;

public class WeatherTools {

    /**
     * IMPORTANT: Keep tool methods stateless or thread-safe
     * Tool instances may be called concurrently by multiple threads
     */

    `@Tool`("Get current weather for a location")
    public String getCurrentWeather(String city, String country) {
        // Input validation (CRITICAL - LLMs can hallucinate invalid values)
        if (city == null || city.trim().isEmpty()) {
            throw new ToolExecutionException("City name is required");
        }
        if (country == null || !country.matches("[A-Z]{2}")) {
            throw new ToolExecutionException("Country must be 2-letter ISO code (e.g., US, FR)");
        }

        try {
            // Implementation
            WeatherData data = weatherService.getWeather(city, country);
            return String.format("Temperature: %d°C, Condition: %s", 
                data.temperature(), data.condition());

        } catch (ServiceException e) {
            // Always wrap in ToolExecutionException for proper error propagation
            throw new ToolExecutionException("Weather service error: " + e.getMessage(), e);
        }
    }

    `@Tool`(
        name = "get_forecast",
        value = {"Get 5-day weather forecast", "Returns daily forecast for specified location"}
    )
    public String getForecast(String city) {
        // Validate and implement
        if (city == null || city.isEmpty()) {
            throw new ToolExecutionException("City is required");
        }
        return "Mon: 20°C, Tue: 22°C, Wed: 19°C, Thu: 21°C, Fri: 23°C";
    }
}

Best Practices:

  • ✅ Always validate inputs (LLMs can generate invalid data)
  • ✅ Use clear, descriptive tool names and descriptions
  • ✅ Keep tool methods stateless (enables concurrent calls)
  • ✅ Throw ToolExecutionException with clear error messages
  • ✅ Document parameter requirements in @P annotations
  • ✅ Keep descriptions concise but informative (< 1024 chars)
  • ✅ Use appropriate ReturnBehavior for your use case

Common Pitfalls:

  • ❌ Not validating inputs - Always validate (LLMs hallucinate)
  • ❌ Swallowing exceptions - Always propagate as ToolExecutionException
  • ❌ Storing mutable state in tool instances without synchronization
  • ❌ Vague descriptions - Be specific about what the tool does
  • ❌ Not documenting parameter formats/constraints

P Annotation

Target: Method parameters Retention: Runtime Purpose: Provides parameter descriptions for LLM

Annotates tool method parameters with descriptions to help the LLM understand usage.

/**
 * Provides description for tool method parameters
 * Helps LLM understand expected parameter format and constraints
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface P {
    /**
     * Parameter description shown to LLM
     * Should include format, constraints, examples
     * @return Description text (non-null, max 512 chars recommended)
     */
    String value();
}

Usage Example:

import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.P;

public class CalculatorTools {

    `@Tool`("Add two numbers together")
    public double add(
        `@P`("The first number (any decimal value)") double a,
        `@P`("The second number (any decimal value)") double b
    ) {
        return a + b;
    }

    `@Tool`("Calculate compound interest for an investment")
    public double compoundInterest(
        `@P`("Principal amount in dollars (positive value)") double principal,
        `@P`("Annual interest rate as decimal, e.g., 0.05 for 5% (0.0-1.0)") double rate,
        `@P`("Number of years (positive integer)") int years,
        `@P`("Compounding frequency per year, e.g., 12 for monthly (positive integer)") int compoundsPerYear
    ) {
        // Validation
        if (principal <= 0) {
            throw new ToolExecutionException("Principal must be positive");
        }
        if (rate < 0 || rate > 1) {
            throw new ToolExecutionException("Rate must be between 0.0 and 1.0");
        }
        if (years <= 0 || compoundsPerYear <= 0) {
            throw new ToolExecutionException("Years and compounds must be positive");
        }

        return principal * Math.pow(1 + rate / compoundsPerYear, compoundsPerYear * years);
    }
}

Best Practices for @P Descriptions:

  • ✅ Include expected format ("YYYY-MM-DD", "ISO 3166-1 alpha-2")
  • ✅ Specify valid ranges ("0.0-1.0", "positive integer")
  • ✅ Provide examples ("e.g., 0.05 for 5%", "e.g., 'US', 'FR'")
  • ✅ Mention units ("in dollars", "in meters", "in seconds")
  • ✅ Be concise but complete (< 512 chars)

Common Pitfalls:

  • ❌ Omitting @P annotations - LLM won't understand parameters
  • ❌ Vague descriptions ("the value", "a number")
  • ❌ Not mentioning format constraints
  • ❌ Extremely long descriptions (> 512 chars)

ToolMemoryId Annotation

Target: Method parameters Retention: Runtime Purpose: Injects memory/user ID for multi-user scenarios

Marks a parameter as the memory ID for multi-user/multi-session scenarios. The framework automatically injects the current user/session ID.

/**
 * Marks a parameter as the memory/user ID
 * Framework automatically injects current user/session context
 * Parameter type: typically String or Object
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface ToolMemoryId {
}

Usage Example:

import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
import dev.langchain4j.agent.tool.P;

public class UserPreferencesTools {
    private final PreferenceStore preferenceStore;

    public UserPreferencesTools(PreferenceStore store) {
        this.preferenceStore = store;
    }

    /**
     * Save user preference
     * @param userId Automatically injected by framework (not provided by LLM)
     * @param key Preference key
     * @param value Preference value
     * @return Confirmation message
     */
    `@Tool`("Save user preference for current user")
    public String savePreference(
        `@Tool`MemoryId String userId,  // Injected automatically
        `@P`("Preference key (e.g., 'theme', 'language')") String key,
        `@P`("Preference value") String value
    ) {
        // userId is never null - framework guarantees injection
        // Validate other parameters
        if (key == null || key.trim().isEmpty()) {
            throw new ToolExecutionException("Preference key cannot be empty");
        }
        if (value == null) {
            throw new ToolExecutionException("Preference value cannot be null");
        }

        preferenceStore.save(userId, key, value);
        return String.format("Preference '%s' saved for user %s", key, userId);
    }

    `@Tool`("Get user preference for current user")
    public String getPreference(
        `@Tool`MemoryId String userId,  // Injected automatically
        `@P`("Preference key to retrieve") String key
    ) {
        if (key == null || key.trim().isEmpty()) {
            throw new ToolExecutionException("Preference key cannot be empty");
        }

        String value = preferenceStore.get(userId, key);
        if (value == null) {
            return String.format("No preference '%s' found for current user", key);
        }
        return value;
    }
}

Important Notes:

  • ✅ ``@ToolMemoryId parameters are NEVER provided by the LLM
  • ✅ Framework automatically injects the value from invocation context
  • ✅ Useful for multi-user applications to isolate user data
  • ✅ Parameter is never null when injected
  • ✅ Don't include in @P description - LLM should not know about it

Common Pitfalls:

  • ❌ Expecting LLM to provide the memory ID value
  • ❌ Using @P annotation on @ToolMemoryId parameter
  • ❌ Not handling user-specific data properly

ReturnBehavior Enum

Thread-Safety: Immutable enum Values: TO_LLM (default), IMMEDIATE

Controls how tool results are handled.

/**
 * Controls whether tool result goes to LLM or directly to user
 */
enum ReturnBehavior {
    /**
     * Return result to LLM for processing (default)
     * LLM can format, summarize, or contextualize the result
     * Use for: Data retrieval, calculations, queries
     */
    TO_LLM,

    /**
     * Return result immediately to user, skip LLM
     * Result goes directly to user without LLM processing
     * Use for: Actions, commands, system operations
     */
    IMMEDIATE
}

Usage Example:

import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ReturnBehavior;

public class SystemTools {

    /**
     * IMMEDIATE behavior: Result bypasses LLM and goes directly to user
     * Use for: Actions that should happen immediately
     */
    `@Tool`(
        value = {"Shut down the system"},
        returnBehavior = ReturnBehavior.IMMEDIATE
    )
    public String shutdownSystem() {
        performShutdown();
        return "System is shutting down...";
        // This message goes directly to user, LLM never sees it
    }

    /**
     * TO_LLM behavior (default): Result goes to LLM for formatting
     * Use for: Data that needs interpretation or formatting
     */
    `@Tool`("Get system status")
    public String getStatus() {
        return "CPU: 45%, Memory: 60%, Disk: 75%";
        // LLM receives this and can format nicely for user:
        // "Your system is running normally with moderate resource usage..."
    }

    /**
     * IMMEDIATE: Direct response for time-sensitive operations
     */
    `@Tool`(
        value = {"Execute emergency protocol"},
        returnBehavior = ReturnBehavior.IMMEDIATE
    )
    public String emergencyProtocol() {
        executeEmergencyActions();
        return "Emergency protocol activated!";
        // User sees this immediately without LLM delay
    }
}

When to Use Each:

ReturnBehaviorUse CaseExample
TO_LLM (default)Data retrievalWeather, calculations, database queries
TO_LLMResults needing interpretationSystem metrics, logs, complex data
TO_LLMResults LLM should summarizeLong text, multiple items
IMMEDIATEActions/commandsShutdown, restart, execute
IMMEDIATETime-sensitive operationsEmergency alerts, real-time updates
IMMEDIATESimple confirmations"File deleted", "Order placed"

Performance Notes:

  • IMMEDIATE bypasses LLM (faster, lower cost)
  • TO_LLM adds one more LLM call (slower, higher cost)

Tool Specifications

ToolSpecification Class

Thread-Safety: Immutable (after build) Builder: Available via builder()

Complete tool specification for LLM including name, description, and parameter schemas.

/**
 * Complete specification of a tool
 * Immutable after construction
 */
class ToolSpecification {
    /**
     * @return Builder for constructing specifications
     */
    static Builder builder();

    /**
     * @return Tool name (never null, non-empty)
     */
    String name();

    /**
     * @return Tool description (never null, may be empty)
     */
    String description();

    /**
     * @return Parameter specifications (never null, may be empty)
     */
    ToolParameters parameters();
}

Builder Methods:

/**
 * Builder for ToolSpecification
 * Not thread-safe - use from single thread
 */
class Builder {
    /**
     * Sets tool name
     * @param name The tool name (required, non-null, non-empty)
     * @return This builder
     * @throws IllegalArgumentException if name is null or empty
     */
    Builder name(String name);

    /**
     * Sets tool description
     * @param description The tool description (required, non-null)
     * @return This builder
     */
    Builder description(String description);

    /**
     * Sets parameter specifications
     * @param parameters The parameters (nullable)
     * @return This builder
     */
    Builder parameters(ToolParameters parameters);

    /**
     * Adds a single parameter
     * @param name Parameter name (required, non-null, non-empty)
     * @param properties Parameter properties (type, description, etc.)
     * @return This builder
     */
    Builder addParameter(String name, Map<String, Object> properties);

    /**
     * Builds the specification
     * @return ToolSpecification instance (immutable)
     * @throws IllegalStateException if required fields missing
     */
    ToolSpecification build();
}

Usage Example:

import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.agent.tool.ToolParameters;
import java.util.Map;
import java.util.List;

// Manual specification (alternative to `@Tool` annotation)
ToolSpecification spec = ToolSpecification.builder()
    .name("get_weather")
    .description("Get current weather for a location")
    .parameters(ToolParameters.builder()
        .addProperty("city", Map.of(
            "type", "string",
            "description", "City name, e.g., 'Paris', 'New York'"
        ))
        .addProperty("country", Map.of(
            "type", "string",
            "description", "ISO 3166-1 alpha-2 country code, e.g., 'FR', 'US'"
        ))
        .addProperty("unit", Map.of(
            "type", "string",
            "enum", List.of("celsius", "fahrenheit"),
            "description", "Temperature unit"
        ))
        .required(List.of("city", "country"))  // unit is optional
        .build())
    .build();

// Use in chat request
ChatRequest request = ChatRequest.builder()
    .messages(messages)
    .toolSpecifications(List.of(spec))
    .build();

Common Pitfalls:

  • ❌ Forgetting to call build() - returns Builder, not ToolSpecification
  • ❌ Not marking required parameters in required() list
  • ❌ Inconsistent parameter types between specification and actual method

ToolParameters Class

Thread-Safety: Immutable (after build) Builder: Available via builder()

Tool parameter specifications with JSON Schema format.

/**
 * Parameters specification for a tool
 * Follows JSON Schema format
 * Immutable after construction
 */
class ToolParameters {
    /**
     * @return Builder for constructing parameters
     */
    static Builder builder();

    /**
     * @return Always "object" (JSON Schema requirement)
     */
    String type();

    /**
     * @return Map of parameter name to parameter schema (never null)
     */
    Map<String, Map<String, Object>> properties();

    /**
     * @return List of required parameter names (never null, may be empty)
     */
    List<String> required();
}

Usage Example:

import dev.langchain4j.agent.tool.ToolParameters;
import java.util.Map;
import java.util.List;

ToolParameters params = ToolParameters.builder()
    // String parameter with constraints
    .addProperty("email", Map.of(
        "type", "string",
        "description", "User email address",
        "pattern", "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
    ))
    // Number parameter with range
    .addProperty("age", Map.of(
        "type", "number",
        "description", "User age in years",
        "minimum", 0,
        "maximum", 150
    ))
    // Enum parameter
    .addProperty("role", Map.of(
        "type", "string",
        "description", "User role",
        "enum", List.of("admin", "user", "guest")
    ))
    // Array parameter
    .addProperty("tags", Map.of(
        "type", "array",
        "description", "List of tags",
        "items", Map.of("type", "string")
    ))
    // Mark required parameters
    .required(List.of("email", "role"))  // age and tags are optional
    .build();

JSON Schema Property Types:

  • string: Text values
  • number: Numeric values (float/double)
  • integer: Integer values
  • boolean: true/false
  • array: List of values
  • object: Nested object
  • enum: Fixed set of values

Common Constraints:

  • pattern: Regex pattern (strings)
  • minimum/maximum: Numeric bounds
  • minLength/maxLength: String length bounds
  • minItems/maxItems: Array size bounds
  • enum: List of allowed values

ToolSpecifications Class

Thread-Safety: Utility class with static methods (thread-safe) Purpose: Extract specifications from annotated classes

Helper for generating specifications from annotated classes.

/**
 * Utility for extracting tool specifications from annotated objects
 * All methods are static and thread-safe
 */
class ToolSpecifications {
    /**
     * Extracts tool specifications from an object's `@Tool` methods
     * Scans all public methods annotated with `@Tool`
     * @param objectWithTools Object containing `@Tool` annotated methods (non-null)
     * @return List of tool specifications (never null, may be empty)
     * @throws IllegalArgumentException if objectWithTools is null
     */
    static List<ToolSpecification> toolSpecificationsFrom(Object objectWithTools);

    /**
     * Extracts tool specifications from a class's `@Tool` methods
     * Scans all public methods annotated with `@Tool`
     * @param classWithTools Class containing `@Tool` annotated methods (non-null)
     * @return List of tool specifications (never null, may be empty)
     * @throws IllegalArgumentException if classWithTools is null
     */
    static List<ToolSpecification> toolSpecificationsFrom(Class<?> classWithTools);
}

Usage Example:

import dev.langchain4j.agent.tool.ToolSpecifications;
import dev.langchain4j.agent.tool.ToolSpecification;
import java.util.List;

public class MyTools {
    `@Tool`("Calculate sum of two numbers")
    public int sum(`@P`("First number") int a, `@P`("Second number") int b) {
        return a + b;
    }

    `@Tool`("Get current timestamp in ISO 8601 format")
    public String getCurrentTime() {
        return Instant.now().toString();
    }

    // Private methods are NOT included
    private void helperMethod() {
        // Not extracted
    }
}

// Extract specifications from instance
MyTools tools = new MyTools();
List<ToolSpecification> specs = ToolSpecifications.toolSpecificationsFrom(tools);

System.out.println("Found " + specs.size() + " tools");  // Prints: Found 2 tools
for (ToolSpecification spec : specs) {
    System.out.println("Tool: " + spec.name());
    System.out.println("Description: " + spec.description());
    System.out.println("Parameters: " + spec.parameters().properties().keySet());
}

// Extract from class (alternative)
List<ToolSpecification> specsFromClass = 
    ToolSpecifications.toolSpecificationsFrom(MyTools.class);

Important Notes:

  • ✅ Only scans public methods
  • ✅ Methods must be annotated with @Tool
  • ✅ Automatically generates JSON schemas from @P annotations
  • ✅ Thread-safe utility methods
  • ⚠️ Performance: Reflection-based, cache results if calling frequently

Common Pitfalls:

  • ❌ Using private/protected methods with @Tool (won't be found)
  • ❌ Repeatedly calling in hot paths (cache the results)
  • ❌ Not handling empty list result

Tool Execution

ToolExecutionRequest Class

Thread-Safety: Immutable Source: Created by LLM during function calling

Request to execute a tool (created by LLM).

/**
 * Request from LLM to execute a tool
 * Immutable value object
 */
class ToolExecutionRequest {
    /**
     * @return Builder for constructing requests
     */
    static Builder builder();

    /**
     * @return Unique ID for this tool call (never null, non-empty)
     * Generated by LLM, used to correlate request with result
     */
    String id();

    /**
     * @return Name of the tool to execute (never null, non-empty)
     */
    String name();

    /**
     * @return Tool arguments as JSON string (never null, may be empty "{}")
     * Format: JSON object with parameter names as keys
     */
    String arguments();
}

Usage Example:

import dev.langchain4j.agent.tool.ToolExecutionRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import java.util.Map;

// Typically received from LLM in AiMessage
AiMessage aiMessage = chatResponse.aiMessage();

if (aiMessage.hasToolExecutionRequests()) {
    List<ToolExecutionRequest> requests = aiMessage.toolExecutionRequests();

    for (ToolExecutionRequest request : requests) {
        System.out.println("Tool: " + request.name());
        System.out.println("ID: " + request.id());
        System.out.println("Args: " + request.arguments());

        // Parse arguments (JSON string to Map)
        ObjectMapper mapper = new ObjectMapper();
        try {
            Map<String, Object> args = mapper.readValue(
                request.arguments(),
                new TypeReference<Map<String, Object>>() {}
            );

            // Extract typed values
            String city = (String) args.get("city");
            String country = (String) args.get("country");

            // Execute tool (your implementation)
            String result = executeWeatherTool(city, country);

            // Create result message
            ToolExecutionResultMessage resultMsg =
                ToolExecutionResultMessage.from(request, result);

        } catch (JsonProcessingException e) {
            // Handle malformed JSON from LLM
            System.err.println("Invalid JSON arguments: " + e.getMessage());
        }
    }
}

Argument Parsing Pattern:

/**
 * Safe argument parsing with type checking and validation
 */
public class ToolArgumentParser {

    public static Map<String, Object> parseArguments(String json) 
            throws ToolArgumentsException {
        try {
            ObjectMapper mapper = new ObjectMapper();
            return mapper.readValue(json, new TypeReference<Map<String, Object>>() {});
        } catch (JsonProcessingException e) {
            throw new ToolArgumentsException("Failed to parse arguments: " + e.getMessage(), e);
        }
    }

    public static String getRequiredString(Map<String, Object> args, String key) 
            throws ToolArgumentsException {
        Object value = args.get(key);
        if (value == null) {
            throw new ToolArgumentsException("Missing required parameter: " + key);
        }
        if (!(value instanceof String)) {
            throw new ToolArgumentsException("Parameter " + key + " must be string");
        }
        return (String) value;
    }

    public static Integer getRequiredInteger(Map<String, Object> args, String key)
            throws ToolArgumentsException {
        Object value = args.get(key);
        if (value == null) {
            throw new ToolArgumentsException("Missing required parameter: " + key);
        }
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        throw new ToolArgumentsException("Parameter " + key + " must be integer");
    }
}

Common Pitfalls:

  • ❌ Not handling JSON parsing errors
  • ❌ Assuming arguments are correctly typed (LLM may send wrong types)
  • ❌ Not validating required parameters are present
  • ❌ Forgetting to send result back to LLM with ToolExecutionResultMessage

Exceptions

ToolExecutionException

Thread-Safety: Immutable (exception object) Usage: Throw when tool execution fails

Exception thrown during tool execution.

/**
 * Thrown when tool execution fails
 * Signals to LLM that tool failed (LLM can inform user)
 * Extends LangChain4jException (unchecked)
 */
class ToolExecutionException extends LangChain4jException {
    /**
     * Creates exception with message
     * @param message Error message (should be user-friendly)
     */
    public ToolExecutionException(String message);

    /**
     * Creates exception with message and cause
     * @param message Error message
     * @param cause The underlying cause
     */
    public ToolExecutionException(String message, Throwable cause);

    /**
     * Creates exception with message and error code
     * @param message Error message
     * @param errorCode Optional error code for specific failure types
     */
    public ToolExecutionException(String message, Integer errorCode);

    /**
     * Creates exception with cause and error code
     * @param cause The underlying cause
     * @param errorCode Optional error code
     */
    public ToolExecutionException(Throwable cause, Integer errorCode);

    /**
     * @return Optional error code for specific failure types (may be null)
     */
    public Integer errorCode();
}

Usage Example:

import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.exception.ToolExecutionException;

public class DatabaseTools {

    `@Tool`("Query database")
    public String query(String sql) {
        // Validation
        if (sql == null || sql.trim().isEmpty()) {
            throw new ToolExecutionException("SQL query cannot be empty");
        }
        
        // Security check
        if (sql.toUpperCase().contains("DROP") || sql.toUpperCase().contains("DELETE")) {
            throw new ToolExecutionException("Destructive SQL operations not allowed", 403);
        }

        try {
            // Execute query
            ResultSet results = database.executeQuery(sql);
            return formatResults(results);

        } catch (SQLException e) {
            // Database error - wrap in ToolExecutionException
            throw new ToolExecutionException(
                "Database query failed: " + e.getMessage(), 
                e
            );
        } catch (TimeoutException e) {
            // Timeout - include error code
            throw new ToolExecutionException("Query timed out", 408);
        }
    }
}

Error Code Conventions:

  • 400-499: Client errors (invalid input, unauthorized)
  • 500-599: Server errors (service failure, timeout)
  • Custom codes: Use consistently across your tools

Best Practices:

  • ✅ Use clear, user-friendly error messages (LLM shows to user)
  • ✅ Include error codes for programmatic handling
  • ✅ Chain original exception as cause for debugging
  • ✅ Validate inputs and throw early
  • ❌ Don't leak sensitive information in messages
  • ❌ Don't swallow exceptions silently

ToolArgumentsException

Thread-Safety: Immutable (exception object) Usage: Throw when tool arguments are invalid

Exception thrown when tool arguments are invalid or cannot be parsed.

/**
 * Thrown when tool arguments are invalid or cannot be parsed
 * Signals to LLM that arguments are malformed (LLM may retry with corrections)
 * Extends LangChain4jException (unchecked)
 */
class ToolArgumentsException extends LangChain4jException {
    /**
     * Creates exception with message
     * @param message Error message describing argument issue
     */
    public ToolArgumentsException(String message);

    /**
     * Creates exception with message and cause
     * @param message Error message
     * @param cause The underlying cause (e.g., JsonProcessingException)
     */
    public ToolArgumentsException(String message, Throwable cause);

    /**
     * Creates exception with message and error code
     * @param message Error message
     * @param errorCode Optional error code (e.g., 400, 422)
     */
    public ToolArgumentsException(String message, Integer errorCode);

    /**
     * Creates exception with cause and error code
     * @param cause The underlying cause
     * @param errorCode Optional error code
     */
    public ToolArgumentsException(Throwable cause, Integer errorCode);

    /**
     * @return Optional error code for specific argument errors (may be null)
     */
    public Integer errorCode();
}

Usage Example:

import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.exception.ToolArgumentsException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class DataTools {

    private final ObjectMapper mapper = new ObjectMapper();

    `@Tool`("Process JSON data")
    public String processData(String jsonData) {
        // Validate not empty
        if (jsonData == null || jsonData.trim().isEmpty()) {
            throw new ToolArgumentsException("JSON data cannot be empty", 400);
        }

        try {
            // Parse JSON
            Map<String, Object> data = mapper.readValue(
                jsonData, 
                new TypeReference<Map<String, Object>>() {}
            );

            // Validate required fields
            if (!data.containsKey("id")) {
                throw new ToolArgumentsException("Missing required field: 'id'", 422);
            }
            if (!data.containsKey("type")) {
                throw new ToolArgumentsException("Missing required field: 'type'", 422);
            }

            // Validate field types
            if (!(data.get("id") instanceof Number)) {
                throw new ToolArgumentsException("Field 'id' must be numeric", 422);
            }

            // Process data
            return processValidData(data);

        } catch (JsonProcessingException e) {
            // JSON parsing failed
            throw new ToolArgumentsException(
                "Invalid JSON format: " + e.getMessage(), 
                e, 
                400
            );
        }
    }
}

When to Use ToolArgumentsException vs ToolExecutionException:

ExceptionUse WhenExample
ToolArgumentsExceptionArgument format/type wrongInvalid JSON, wrong type, missing field
ToolArgumentsExceptionArgument validation failsOut of range, invalid format, constraint violation
ToolExecutionExceptionTool execution failsService error, timeout, permission denied
ToolExecutionExceptionBusiness logic errorInvalid operation, conflict, not found

Best Practices:

  • ✅ Use ToolArgumentsException for input validation errors
  • ✅ Use ToolExecutionException for execution failures
  • ✅ Include specific field names in error messages
  • ✅ Use error codes for categorization
  • ❌ Don't confuse argument errors with execution errors

Complete Example

import dev.langchain4j.agent.tool.*;
import dev.langchain4j.model.chat.*;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.data.message.*;
import dev.langchain4j.exception.ToolExecutionException;
import dev.langchain4j.exception.ToolArgumentsException;
import java.util.List;
import java.util.Map;

/**
 * Complete tool usage example with error handling
 */
public class CompleteToolExample {

    // Define tools with proper validation
    public static class CalculatorTools {

        `@Tool`("Add two numbers")
        public double add(
            `@P`("First number") double a,
            `@P`("Second number") double b
        ) {
            return a + b;
        }

        `@Tool`("Multiply two numbers")
        public double multiply(
            `@P`("First number") double a,
            `@P`("Second number") double b
        ) {
            return a * b;
        }

        `@Tool`("Calculate square root")
        public double sqrt(`@P`("The number (must be non-negative)") double x) {
            if (x < 0) {
                throw new ToolExecutionException(
                    "Cannot calculate square root of negative number");
            }
            return Math.sqrt(x);
        }

        `@Tool`("Divide two numbers")
        public double divide(
            `@P`("Numerator") double a,
            `@P`("Denominator (must be non-zero)") double b
        ) {
            if (b == 0) {
                throw new ToolExecutionException("Division by zero not allowed");
            }
            return a / b;
        }
    }

    public static void main(String[] args) {
        // Initialize chat model
        ChatModel chatModel = /* OpenAiChatModel, etc. */;
        
        // Initialize tools
        CalculatorTools tools = new CalculatorTools();

        // Extract tool specifications
        List<ToolSpecification> toolSpecs =
            ToolSpecifications.toolSpecificationsFrom(tools);

        System.out.println("Loaded " + toolSpecs.size() + " tools");

        // Create chat request with tools
        ChatRequest request = ChatRequest.builder()
            .messages(
                SystemMessage.from("You are a helpful calculator assistant."),
                UserMessage.from("What is 15 multiplied by 7, then add 23, then take the square root?")
            )
            .toolSpecifications(toolSpecs)
            .build();

        // Send to LLM
        ChatResponse response = chatModel.chat(request);
        AiMessage aiMessage = response.aiMessage();

        // Process tool calls (may be multiple)
        List<ChatMessage> conversationHistory = new ArrayList<>();
        conversationHistory.add(SystemMessage.from("You are a helpful calculator assistant."));
        conversationHistory.add(UserMessage.from("What is 15 multiplied by 7, then add 23, then take the square root?"));

        while (aiMessage.hasToolExecutionRequests()) {
            conversationHistory.add(aiMessage);

            List<ToolExecutionRequest> requests = aiMessage.toolExecutionRequests();
            System.out.println("\nLLM wants to execute " + requests.size() + " tool(s)");

            for (ToolExecutionRequest toolRequest : requests) {
                System.out.println("- Tool: " + toolRequest.name());
                System.out.println("  Arguments: " + toolRequest.arguments());

                try {
                    // Execute tool
                    String result = executeTool(tools, toolRequest);
                    System.out.println("  Result: " + result);

                    // Add result to conversation
                    conversationHistory.add(
                        ToolExecutionResultMessage.from(toolRequest, result));

                } catch (ToolExecutionException e) {
                    System.err.println("  Execution error: " + e.getMessage());
                    conversationHistory.add(
                        ToolExecutionResultMessage.from(toolRequest, 
                            "Error: " + e.getMessage()));

                } catch (ToolArgumentsException e) {
                    System.err.println("  Argument error: " + e.getMessage());
                    conversationHistory.add(
                        ToolExecutionResultMessage.from(toolRequest,
                            "Invalid arguments: " + e.getMessage()));
                }
            }

            // Send tool results back to LLM
            ChatRequest followUpRequest = ChatRequest.builder()
                .messages(conversationHistory)
                .toolSpecifications(toolSpecs)
                .build();

            response = chatModel.chat(followUpRequest);
            aiMessage = response.aiMessage();
        }

        // Final answer from LLM
        System.out.println("\nFinal answer: " + aiMessage.text());
    }

    /**
     * Execute tool with argument parsing and validation
     */
    private static String executeTool(CalculatorTools tools, ToolExecutionRequest request)
            throws ToolExecutionException, ToolArgumentsException {
        
        try {
            ObjectMapper mapper = new ObjectMapper();
            Map<String, Object> args = mapper.readValue(
                request.arguments(),
                new TypeReference<Map<String, Object>>() {}
            );

            switch (request.name()) {
                case "add":
                    double a = getNumber(args, "a");
                    double b = getNumber(args, "b");
                    return String.valueOf(tools.add(a, b));

                case "multiply":
                    a = getNumber(args, "a");
                    b = getNumber(args, "b");
                    return String.valueOf(tools.multiply(a, b));

                case "sqrt":
                    double x = getNumber(args, "x");
                    return String.valueOf(tools.sqrt(x));

                case "divide":
                    a = getNumber(args, "a");
                    b = getNumber(args, "b");
                    return String.valueOf(tools.divide(a, b));

                default:
                    throw new ToolExecutionException("Unknown tool: " + request.name());
            }

        } catch (JsonProcessingException e) {
            throw new ToolArgumentsException("Failed to parse arguments", e);
        }
    }

    /**
     * Extract and validate numeric argument
     */
    private static double getNumber(Map<String, Object> args, String key)
            throws ToolArgumentsException {
        Object value = args.get(key);
        if (value == null) {
            throw new ToolArgumentsException("Missing required parameter: " + key);
        }
        if (!(value instanceof Number)) {
            throw new ToolArgumentsException("Parameter " + key + " must be numeric");
        }
        return ((Number) value).doubleValue();
    }
}

Output Example:

Loaded 4 tools

LLM wants to execute 1 tool(s)
- Tool: multiply
  Arguments: {"a":15,"b":7}
  Result: 105.0

LLM wants to execute 1 tool(s)
- Tool: add
  Arguments: {"a":105.0,"b":23}
  Result: 128.0

LLM wants to execute 1 tool(s)
- Tool: sqrt
  Arguments: {"x":128.0}
  Result: 11.313708498984761

Final answer: The result is approximately 11.31

Best Practices Summary

Tool Design

  • ✅ Keep tools stateless (enable concurrency)
  • ✅ Use clear, descriptive names and descriptions
  • ✅ Always validate inputs (LLMs hallucinate)
  • ✅ Throw specific exceptions (ToolExecutionException, ToolArgumentsException)
  • ✅ Include units and formats in @P descriptions
  • ✅ Use appropriate ReturnBehavior

Performance

  • ✅ Cache ToolSpecification results (reflection is expensive)
  • ✅ Keep tool execution fast (< 1 second ideally)
  • ✅ Use async operations for long-running tasks
  • ⚠️ Tool execution blocks LLM response

Security

  • ✅ Validate all inputs (never trust LLM outputs)
  • ✅ Implement authorization checks for sensitive operations
  • ✅ Sanitize inputs to prevent injection attacks
  • ✅ Don't leak sensitive data in error messages
  • ✅ Use @ToolMemoryId for user context

Error Handling

  • ✅ Use ToolExecutionException for execution failures
  • ✅ Use ToolArgumentsException for invalid arguments
  • ✅ Provide clear, user-friendly error messages
  • ✅ Include error codes for categorization
  • ✅ Chain underlying exceptions as cause

See Also

  • Chat Models - Using tools with chat models
  • Exception Hierarchy - Error handling
  • Observability - Monitoring tool executions
  • Request and Response Types - Tool choice strategies
  • Other Types - Tool annotations (@Tool, @P, @ToolMemoryId)

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j-core@1.11.0

docs

guardrails.md

index.md

memory.md

observability.md

tools.md

tile.json