Files
2025-11-29 18:28:30 +08:00

16 KiB

LangChain4j Tool & Function Calling - Practical Examples

Production-ready examples for tool calling and function execution patterns with LangChain4j.

1. Basic Tool Calling

Scenario: Simple tools that LLM can invoke automatically.

import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.model.openai.OpenAiChatModel;

class Calculator {
    @Tool("Add two numbers together")
    int add(@P("first number") int a, @P("second number") int b) {
        return a + b;
    }

    @Tool("Multiply two numbers")
    int multiply(@P("first number") int a, @P("second number") int b) {
        return a * b;
    }

    @Tool("Divide two numbers")
    double divide(@P("dividend") double a, @P("divisor") double b) {
        if (b == 0) throw new IllegalArgumentException("Cannot divide by zero");
        return a / b;
    }
}

interface CalculatorAssistant {
    String chat(String query);
}

public class BasicToolExample {
    public static void main(String[] args) {
        var chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .temperature(0.0)  // Deterministic for tools
            .build();

        var assistant = AiServices.builder(CalculatorAssistant.class)
            .chatModel(chatModel)
            .tools(new Calculator())
            .build();

        System.out.println(assistant.chat("What is 25 + 37?"));
        System.out.println(assistant.chat("Calculate 12 * 8"));
        System.out.println(assistant.chat("Divide 100 by 4"));
    }
}

2. Multiple Tool Objects

Scenario: LLM selecting from multiple tool domains.

class WeatherService {
    @Tool("Get current weather for a city")
    String getWeather(@P("city name") String city) {
        // Simulate API call
        return "Weather in " + city + ": 22°C, Partly cloudy";
    }

    @Tool("Get weather forecast for next 5 days")
    String getForecast(@P("city name") String city) {
        return "5-day forecast for " + city + ": Sunny, Cloudy, Rainy, Sunny, Cloudy";
    }
}

class DateTimeService {
    @Tool("Get current date and time")
    String getCurrentDateTime() {
        return LocalDateTime.now().toString();
    }

    @Tool("Get day of week for a date")
    String getDayOfWeek(@P("date in YYYY-MM-DD format") String date) {
        LocalDate localDate = LocalDate.parse(date);
        return localDate.getDayOfWeek().toString();
    }
}

interface MultiToolAssistant {
    String help(String query);
}

public class MultipleToolsExample {
    public static void main(String[] args) {
        var chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .build();

        var assistant = AiServices.builder(MultiToolAssistant.class)
            .chatModel(chatModel)
            .tools(new WeatherService(), new DateTimeService())
            .build();

        System.out.println(assistant.help("What's the weather in Paris?"));
        System.out.println(assistant.help("What time is it?"));
        System.out.println(assistant.help("What day is 2024-12-25?"));
    }
}

3. Tool with Complex Return Types

Scenario: Tools returning structured objects.

class UserRecord {
    public String id;
    public String name;
    public String email;
    public LocalDate createdDate;

    public UserRecord(String id, String name, String email, LocalDate createdDate) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.createdDate = createdDate;
    }
}

class UserService {
    @Tool("Look up user information by ID")
    UserRecord getUserById(@P("user ID") String userId) {
        // Simulate database lookup
        return new UserRecord(userId, "John Doe", "john@example.com", LocalDate.now());
    }

    @Tool("List all users (returns top 10)")
    List<UserRecord> listUsers() {
        return Arrays.asList(
            new UserRecord("1", "Alice", "alice@example.com", LocalDate.now()),
            new UserRecord("2", "Bob", "bob@example.com", LocalDate.now())
        );
    }

    @Tool("Search users by name pattern")
    List<UserRecord> searchByName(@P("name pattern") String pattern) {
        return Arrays.asList(
            new UserRecord("1", "John Smith", "john.smith@example.com", LocalDate.now())
        );
    }
}

interface UserAssistant {
    String answer(String query);
}

public class ComplexReturnTypeExample {
    public static void main(String[] args) {
        var chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .build();

        var assistant = AiServices.builder(UserAssistant.class)
            .chatModel(chatModel)
            .tools(new UserService())
            .build();

        System.out.println(assistant.answer("Who is user 123?"));
        System.out.println(assistant.answer("List all users"));
        System.out.println(assistant.answer("Find users named John"));
    }
}

4. Error Handling in Tools

Scenario: Graceful handling of tool errors.

class DatabaseService {
    @Tool("Execute read query on database")
    String queryDatabase(@P("SQL query") String query) {
        // Validate query is SELECT only
        if (!query.trim().toUpperCase().startsWith("SELECT")) {
            throw new IllegalArgumentException("Only SELECT queries allowed");
        }
        return "Query result: 42 rows returned";
    }

    @Tool("Get user count by status")
    int getUserCount(@P("status") String status) {
        if (!Arrays.asList("active", "inactive", "pending").contains(status)) {
            throw new IllegalArgumentException("Invalid status: " + status);
        }
        return 150;
    }
}

interface ResilientAssistant {
    String execute(String command);
}

public class ErrorHandlingExample {
    public static void main(String[] args) {
        var chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .build();

        var assistant = AiServices.builder(ResilientAssistant.class)
            .chatModel(chatModel)
            .tools(new DatabaseService())
            
            // Handle tool execution errors
            .toolExecutionErrorHandler((toolCall, exception) -> {
                System.err.println("Tool error in " + toolCall.name() + ": " + exception.getMessage());
                return "Error: " + exception.getMessage();
            })
            
            // Handle malformed tool arguments
            .toolArgumentsErrorHandler((toolCall, exception) -> {
                System.err.println("Invalid arguments for " + toolCall.name());
                return "Invalid arguments";
            })
            
            .build();

        System.out.println(assistant.execute("Execute SELECT * FROM users"));
        System.out.println(assistant.execute("How many active users?"));
    }
}

5. Streaming Tool Execution

Scenario: Tools called during streaming responses.

import dev.langchain4j.service.TokenStream;

interface StreamingToolAssistant {
    TokenStream execute(String command);
}

public class StreamingToolsExample {
    public static void main(String[] args) {
        var streamingModel = OpenAiStreamingChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .build();

        var assistant = AiServices.builder(StreamingToolAssistant.class)
            .streamingChatModel(streamingModel)
            .tools(new Calculator())
            .build();

        assistant.execute("Calculate (5 + 3) * 4 and explain")
            .onNext(token -> System.out.print(token))
            .onToolExecuted(execution -> 
                System.out.println("\n[Tool: " + execution.request().name() + "]"))
            .onCompleteResponse(response -> 
                System.out.println("\n--- Complete ---"))
            .onError(error -> System.err.println("Error: " + error))
            .start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

6. Dynamic Tool Provider

Scenario: Select tools dynamically based on query context.

interface DynamicToolAssistant {
    String help(String query);
}

class MathTools {
    @Tool("Add two numbers")
    int add(@P("a") int a, @P("b") int b) { return a + b; }
}

class TextTools {
    @Tool("Convert text to uppercase")
    String toUpper(@P("text") String text) { return text.toUpperCase(); }

    @Tool("Convert text to lowercase")
    String toLower(@P("text") String text) { return text.toLowerCase(); }
}

public class DynamicToolProviderExample {
    public static void main(String[] args) {
        var chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .build();

        var assistant = AiServices.builder(DynamicToolAssistant.class)
            .chatModel(chatModel)
            
            // Provide tools dynamically
            .toolProvider(context -> {
                if (context.userMessage().contains("math") || context.userMessage().contains("calculate")) {
                    return Collections.singletonList(new MathTools());
                } else if (context.userMessage().contains("text") || context.userMessage().contains("convert")) {
                    return Collections.singletonList(new TextTools());
                }
                return Collections.emptyList();
            })
            
            .build();

        System.out.println(assistant.help("Calculate 25 + 37"));
        System.out.println(assistant.help("Convert HELLO to lowercase"));
    }
}

7. Tool with Memory Context

Scenario: Tools accessing conversation memory.

class ContextAwareDataService {
    private Map<String, String> userPreferences = new HashMap<>();

    @Tool("Save user preference")
    void savePreference(@P("key") String key, @P("value") String value) {
        userPreferences.put(key, value);
        System.out.println("Saved: " + key + " = " + value);
    }

    @Tool("Get user preference")
    String getPreference(@P("key") String key) {
        return userPreferences.getOrDefault(key, "Not found");
    }

    @Tool("List all preferences")
    Map<String, String> listPreferences() {
        return new HashMap<>(userPreferences);
    }
}

interface ContextAssistant {
    String chat(String message);
}

public class ToolMemoryExample {
    public static void main(String[] args) {
        var chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .build();

        var dataService = new ContextAwareDataService();

        var assistant = AiServices.builder(ContextAssistant.class)
            .chatModel(chatModel)
            .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
            .tools(dataService)
            .build();

        System.out.println(assistant.chat("Remember that I like Java"));
        System.out.println(assistant.chat("What do I like?"));
        System.out.println(assistant.chat("Also remember I use Spring Boot"));
        System.out.println(assistant.chat("What are all my preferences?"));
    }
}

8. Stateful Tool Execution

Scenario: Tools that maintain state across calls.

class StatefulCounter {
    private int count = 0;

    @Tool("Increment counter by 1")
    int increment() {
        return ++count;
    }

    @Tool("Decrement counter by 1")
    int decrement() {
        return --count;
    }

    @Tool("Get current counter value")
    int getCount() {
        return count;
    }

    @Tool("Reset counter to zero")
    void reset() {
        count = 0;
    }
}

interface CounterAssistant {
    String interact(String command);
}

public class StatefulToolExample {
    public static void main(String[] args) {
        var chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .build();

        var counter = new StatefulCounter();

        var assistant = AiServices.builder(CounterAssistant.class)
            .chatModel(chatModel)
            .tools(counter)
            .build();

        System.out.println(assistant.interact("Increment the counter"));
        System.out.println(assistant.interact("Increment again"));
        System.out.println(assistant.interact("What's the current count?"));
        System.out.println(assistant.interact("Reset the counter"));
        System.out.println(assistant.interact("Decrement"));
    }
}

9. Tool Validation and Authorization

Scenario: Validate and authorize tool execution.

class SecureDataService {
    @Tool("Get sensitive data")
    String getSensitiveData(@P("data_id") String dataId) {
        // This should normally check authorization
        if (!dataId.matches("^[A-Z][0-9]{3}$")) {
            throw new IllegalArgumentException("Invalid data ID format");
        }
        return "Sensitive data for " + dataId;
    }

    @Tool("Delete data (requires authorization)")
    void deleteData(@P("data_id") String dataId) {
        if (!dataId.matches("^[A-Z][0-9]{3}$")) {
            throw new IllegalArgumentException("Invalid data ID");
        }
        System.out.println("Data " + dataId + " deleted");
    }
}

interface SecureAssistant {
    String execute(String command);
}

public class AuthorizationExample {
    public static void main(String[] args) {
        var chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .build();

        var assistant = AiServices.builder(SecureAssistant.class)
            .chatModel(chatModel)
            .tools(new SecureDataService())
            
            .toolExecutionErrorHandler((request, exception) -> {
                System.err.println("Authorization/validation failed: " + exception.getMessage());
                return "Operation denied: " + exception.getMessage();
            })
            
            .build();

        System.out.println(assistant.execute("Get data A001"));
        System.out.println(assistant.execute("Get data invalid"));
    }
}

10. Advanced: Tool Result Processing

Scenario: Process and transform tool results before returning to LLM.

class DataService {
    @Tool("Fetch user data from API")
    String fetchUserData(@P("user_id") String userId) {
        return "User{id=" + userId + ", name=John, role=Admin}";
    }
}

interface ProcessingAssistant {
    String answer(String query);
}

public class ToolResultProcessingExample {
    public static void main(String[] args) {
        var chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .build();

        var assistant = AiServices.builder(ProcessingAssistant.class)
            .chatModel(chatModel)
            .tools(new DataService())
            
            // Can add interceptors for tool results if needed
            // This would be in a future LangChain4j version
            
            .build();

        System.out.println(assistant.answer("What is the role of user 123?"));
    }
}

Best Practices

  1. Clear Descriptions: Write detailed @Tool descriptions for LLM context
  2. Strong Typing: Use specific types (int, String) instead of generic Object
  3. Parameter Descriptions: Use @P with clear descriptions of expected formats
  4. Error Handling: Always implement error handlers for graceful failures
  5. Temperature: Set temperature=0 for deterministic tool selection
  6. Validation: Validate all parameters before execution
  7. Logging: Log tool calls and results for debugging
  8. State Management: Keep tools stateless or manage state explicitly
  9. Timeout: Set timeouts on long-running tools
  10. Authorization: Validate authorization before executing sensitive operations