
Langchain4j Tool Function Calling Patterns
- 1.8k installs
- 318 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
Patterns for annotating Java methods as LLM-callable tools, registering them with AI services, validating parameters, and handling execution errors in LangChain4j.
About
Teaches patterns for building tool-calling AI agents in LangChain4j using @Tool and @P annotations to expose Java methods as LLM-callable functions. Covers tool registration with AiServices, parameter validation, error handling, concurrent execution, and dynamic tool provisioning. Essential for building agents that integrate external services like weather APIs, databases, or business systems where the LLM needs to invoke actions beyond text generation. Includes timeout configuration, hallucinated tool detection, and audit logging patterns for production safety.
- Annotate methods with @Tool and @P to define callable functions with LLM-readable descriptions
- Register tool instances with AiServices.builder().tools() to enable LLM invocation
- Handle execution errors, timeouts, and hallucinated tool names with dedicated error handlers
- Enable concurrent tool execution and timeouts to prevent hangs and optimize performance
- Validate parameters inside tools and return safe error messages instead of exposing stack traces
Langchain4j Tool Function Calling Patterns by the numbers
- 1,800 all-time installs (skills.sh)
- +144 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #713 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
langchain4j-tool-function-calling-patterns capabilities & compatibility
- Capabilities
- define tool specifications with @tool and @p ann · register tools with ai services and chat models · handle tool execution errors and timeout conditi · validate tool parameters before invocation · execute tools concurrently for independent calls · implement dynamic tool providers based on user c
- Use cases
- api development · orchestration · code review
- Platforms
- macOS · Windows · Linux
- Runs
- Runs locally
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill langchain4j-tool-function-calling-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 318 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
What it does
Expose Java methods as callable functions for LLM-driven agents to perform external API calls, database queries, and business system integrations.
Who is it for?
Building AI agents that call databases, REST APIs, weather services, stock feeds, or business system integrations; defining function specifications for LLM tool use; managing large conditional tool sets.
Skip if: Simple chatbots that only generate text; agents that do not need external integrations; synchronous request-response systems without AI decision-making.
When should I use this skill?
Starting to build an AI agent; adding tool invocation to an existing LangChain4j service; scaling tool sets to handle many functions or conditional access.
What you get
Developers can build AI agents that reliably invoke external tools, APIs, and business systems with validated parameters, graceful error handling, and production-safe error messages.
- Tool class with @Tool-annotated methods
- AiServices instance with registered tools
- Error handlers for execution failures
By the numbers
- Supports @ToolMemoryId for context-aware tool execution with conversation IDs
- Provides .toolExecutionTimeout() for configurable per-tool execution deadlines
- Includes .executeToolsConcurrently() for parallel invocation of independent tools
Files
LangChain4j Tool & Function Calling Patterns
Provides patterns for annotating methods as tools, configuring tool executors, registering tools with AI services, validating parameters, and handling tool execution errors in LangChain4j applications.
Overview
LangChain4j uses the @Tool annotation to expose Java methods as callable functions for AI agents. The AiServices builder registers tools with a chat model, enabling LLMs to perform actions beyond text generation: database queries, API calls, calculations, and business system integrations. Parameters use @P for descriptions that guide the LLM.
When to Use
- Building AI agents that call external tools (weather, stocks, database queries)
- Defining function specifications for LLM tool use (
@Tool,@Pannotations) - Registering and managing tool sets with
AiServices.builder().tools() - Handling tool execution errors, timeouts, and hallucinated tool names
- Implementing context-aware tools that inject user state via
@ToolMemoryId - Configuring dynamic tool providers for large or conditional tool sets
Instructions
1. Annotate Methods with @Tool
Define a tool class with methods annotated @Tool. Provide a description as the first parameter. Use @P for each parameter description.
public class WeatherTools {
private final WeatherService weatherService;
public WeatherTools(WeatherService weatherService) {
this.weatherService = weatherService;
}
@Tool("Get current weather for a city")
public String getWeather(
@P("City name") String city,
@P("Temperature unit: celsius or fahrenheit") String unit) {
return weatherService.getWeather(city, unit);
}
}Validate: Create an instance and confirm the class loads without errors.
2. Register Tools with AiServices
Use AiServices.builder() to register tool instances with the chat model.
MathAssistant assistant = AiServices.builder(MathAssistant.class)
.chatModel(chatModel)
.tools(new Calculator(), new WeatherTools(weatherService))
.build();Validate: Call assistant.chat("What is 2 + 2?") and verify the LLM responds without throwing.
3. Test Tool Invocation End-to-End
Send a prompt that triggers tool usage and verify the tool executes and its result is incorporated.
String response = assistant.chat("What is the weather in Rome?");
System.out.println(response);Validate: Check logs for tool invocation and confirm the response uses the tool output.
4. Handle Tool Execution Errors
Add error handlers to gracefully manage failures without exposing stack traces.
AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(new ExternalServiceTools())
.toolExecutionErrorHandler((request, exception) -> {
logger.error("Tool '{}' failed: {}", request.name(), exception.getMessage());
return "An error occurred while processing your request";
})
.hallucinatedToolNameStrategy(request ->
ToolExecutionResultMessage.from(request,
"Error: tool '" + request.name() + "' does not exist"))
.toolArgumentsErrorHandler((error, context) ->
ToolErrorHandlerResult.text("Invalid arguments: " + error.getMessage()))
.build();Validate: Trigger an error condition and confirm the LLM receives a safe error message.
5. Optimize for Performance and Scale
Enable concurrent tool execution and set timeouts for long-running tools.
AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(new DbTools(), new HttpTools())
.executeToolsConcurrently(Executors.newFixedThreadPool(5))
.toolExecutionTimeout(Duration.ofSeconds(30))
.build();Validate: Run concurrent requests and confirm no thread contention or deadlocks.
Examples
Calculator Tool with Full Class
public class Calculator {
@Tool("Perform basic arithmetic")
public double calculate(
@P("Expression like 2+2 or 10*5") String expression) {
// Parse and evaluate expression
return eval(expression);
}
}
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(ChatModel.builder()
.apiKey(System.getenv("API_KEY"))
.model("gpt-4o")
.build())
.tools(new Calculator())
.build();Immediate Return Tool (No LLM Response)
@Tool(value = "Send email notification", returnBehavior = ReturnBehavior.IMMEDIATELY)
public void sendEmail(@P("Recipient email address") String to,
@P("Email subject") String subject,
@P("Email body") String body) {
emailService.send(to, subject, body);
}Dynamic Tool Provider
ToolProvider provider = request -> {
if (request.userContext().contains("admin")) {
return List.of(new AdminTools());
}
return List.of(new UserTools());
};
AiServices.builder(Assistant.class)
.chatModel(chatModel)
.toolProvider(provider)
.build();Best Practices
- Descriptive `@Tool` names: Use imperative verbs ("Get", "Send", "Calculate") with clear scope
- Precise `@P` descriptions: Include format, constraints, and valid values — vague descriptions cause incorrect LLM calls
- Safe error handling: Never expose stack traces; return user-friendly error strings
- Timeout configuration: Always set
.toolExecutionTimeout()for external service calls - Concurrent execution: Enable
.executeToolsConcurrently()when tools are independent - Input validation: Validate parameters inside the tool method; return descriptive errors
- Permission checks: Perform authorization inside the tool, not at the AI service level
- Audit logging: Log tool name, parameters, and execution result for debugging and compliance
Common Issues and Solutions
| Issue | Solution |
|---|---|
| LLM calls non-existent tool | Add .hallucinatedToolNameStrategy() returning a safe error message |
| Tools receive wrong parameters | Refine @P descriptions; add .toolArgumentsErrorHandler() |
| Tool execution hangs | Set .toolExecutionTimeout(Duration.ofSeconds(N)) |
| Rate limit errors from external API | Add retry logic or rate limiter inside the tool method |
| LLM ignores tool output | Ensure the tool returns a string the LLM can interpret |
See references/error-handling.md for resilience patterns and references/core-patterns.md for parameter and return type details.
Quick Reference
| Annotation / API | Purpose |
|---|---|
@Tool | Marks a method as a callable tool |
@P | Describes a tool parameter for the LLM |
@ToolMemoryId | Injects conversation/user ID into the tool |
AiServices.builder() | Creates AI service with registered tools |
ReturnBehavior.IMMEDIATELY | Execute tool without waiting for LLM response |
ToolProvider | Dynamic tool provisioning based on context |
executeToolsConcurrently() | Run independent tool calls in parallel |
toolExecutionTimeout() | Timeout for individual tool calls |
Constraints and Warnings
- Sensitive data: Never pass API keys, passwords, or credentials in
@Toolor@Pdescriptions - Side effects: Tools that modify data should warn in their description; AI models may call them multiple times
- Large tool sets: Excessive tools confuse LLM models — use
ToolProviderfor conditional registration - Blocking operations: Tools should not perform long synchronous I/O without timeout configuration
- Stack trace exposure: Always route exceptions through error handlers that return safe strings
- Parameter precision: Vague
@Pdescriptions directly cause incorrect tool calls — be specific about formats and constraints - Concurrent safety: Ensure tool classes are stateless or thread-safe when using
executeToolsConcurrently()
Related Skills
langchain4j-ai-services-patterns— High-level AI service configurationlangchain4j-rag-implementation-patterns— RAG retrieval with tool integrationlangchain4j-spring-boot-integration— Tool registration in Spring Boot applications
References
- [references/setup-configuration.md](references/setup-configuration.md) — Maven setup, chat model configuration, first tool registration
- [references/core-patterns.md](references/core-patterns.md) — Basic tool definition, complex parameters, return types
- [references/advanced-features.md](references/advanced-features.md) — Memory context, dynamic tool providers, streaming, immediate return
- [references/error-handling.md](references/error-handling.md) — Error handlers, retry logic, monitoring
- [references/integration-examples.md](references/integration-examples.md) — Database, REST API, and context-aware tool examples
Advanced Features
Memory Context Integration
Access user context using @ToolMemoryId:
public class PersonalizedTools {
@Tool("Get user preferences")
public String getPreferences(
@ToolMemoryId String userId,
@P("Preference category") String category) {
return preferenceService.getPreferences(userId, category);
}
}Dynamic Tool Provisioning
Create tools that change based on context:
public class ContextAwareToolProvider implements ToolProvider {
@Override
public ToolProviderResult provideTools(ToolProviderRequest request) {
String message = request.userMessage().singleText().toLowerCase();
var builder = ToolProviderResult.builder();
if (message.contains("weather")) {
builder.add(weatherToolSpec, weatherExecutor);
}
if (message.contains("calculate")) {
builder.add(calcToolSpec, calcExecutor);
}
return builder.build();
}
}Immediate Return Tools
Return results immediately without full AI response:
public class QuickTools {
@Tool(value = "Get current time", returnBehavior = ReturnBehavior.IMMEDIATE)
public String getCurrentTime() {
return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
}Streaming with Tool Execution
interface StreamingAssistant {
TokenStream chat(String message);
}
StreamingAssistant assistant = AiServices.builder(StreamingAssistant.class)
.streamingChatModel(streamingChatModel)
.tools(new Tools())
.build();
TokenStream stream = assistant.chat("What's the weather and calculate 15*8?");
stream
.onToolExecuted(execution ->
System.out.println("Executed: " + execution.request().name()))
.onPartialResponse(System.out::print)
.onComplete(response -> System.out.println("Complete!"))
.start();Tool Specification Builder
ToolSpecification toolSpec = ToolSpecification.builder()
.name("advanced_search")
.description("Search across multiple data sources")
.addParameter("query", type("string"), description("Search query"))
.addParameter("sources", type("array"), description("Data sources to search"))
.addParameter("filters", type("object"), description("Search filters"), required(false)
.build();Core Patterns
Basic Tool Definition
Use @Tool annotation to define methods as executable tools:
public class BasicTools {
@Tool("Add two numbers")
public int add(@P("first number") int a, @P("second number") int b) {
return a + b;
}
@Tool("Get greeting")
public String greet(@P("name to greet") String name) {
return "Hello, " + name + "!";
}
}Parameter Descriptions and Validation
Provide clear parameter descriptions using @P annotation:
public class WeatherService {
@Tool("Get current weather conditions")
public String getCurrentWeather(
@P("City name or coordinates") String location,
@P("Temperature unit (celsius, fahrenheit)", required = false) String unit) {
// Implementation with validation
if (location == null || location.trim().isEmpty()) {
return "Location is required";
}
return weatherClient.getCurrentWeather(location, unit);
}
}Complex Parameter Types
Use Java records and descriptions for complex objects:
public class OrderService {
@Description("Customer order information")
public record OrderRequest(
@Description("Customer ID") String customerId,
@Description("List of items") List<OrderItem> items,
@JsonProperty(required = false) @Description("Delivery instructions") String instructions
) {}
@Tool("Create customer order")
public String createOrder(OrderRequest order) {
return orderService.processOrder(order);
}
}Return Types
Tool methods can return various types:
// Simple types
@Tool("Get current time")
public long getCurrentTime() {
return System.currentTimeMillis();
}
// JSON/POJO
@Tool("Get user profile")
public UserProfile getUserProfile(@P("User ID") String userId) {
return userProfileService.findById(userId);
}
// Collections
@Tool("Search products")
public List<Product> searchProducts(@P("Search query") String query) {
return productService.search(query);
}
// Maps
@Tool("Get system status")
public Map<String, Object> getSystemStatus() {
return monitoringService.getStatus();
}Error Handling and Resilience
Tool Error Handling
Handle tool execution errors gracefully:
AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(new ExternalServiceTools())
.toolExecutionErrorHandler((request, exception) -> {
if (exception instanceof ApiException) {
return "Service temporarily unavailable: " + exception.getMessage();
}
return "An error occurred while processing your request";
})
.build();Resilience Patterns
Implement circuit breakers and retries:
public class ResilientService {
private final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("external-api");
@Tool("Get external data")
public String getExternalData(@P("Data identifier") String id) {
return circuitBreaker.executeSupplier(() -> {
return externalApi.getData(id);
});
}
}Parameter Validation Errors
.toolArgumentsErrorHandler((error, context) -> {
return ToolErrorHandlerResult.text("Invalid arguments: " + error.getMessage());
})Hallucinated Tool Names
.hallucinatedToolNameStrategy(request -> {
return ToolExecutionResultMessage.from(request,
"Error: Tool '" + request.name() + "' does not exist");
})Timeout Handling
AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(new ExternalTools())
.toolExecutionTimeout(Duration.ofSeconds(30))
.build();Retry Configuration
public class RetryingToolService {
private final Retry retry = Retry.ofDefaults("tool-retry");
@Tool("Fetch remote data")
public String fetchData(@P("URL") String url) {
return retry.executeSupplier(() -> {
return webClient.get()
.uri(url)
.retrieve()
.bodyToMono(String.class)
.block();
});
}
}Monitoring and Logging
AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(new BusinessTools())
.toolExecutionListener(new ToolExecutionListener() {
@Override
public void onToolExecuted(ToolExecution execution) {
log.info("Tool {} executed in {}ms",
execution.request().name(),
execution.duration().toMillis());
// Record metrics
meterRegistry.timer("tool.execution",
"tool", execution.request().name())
.record(execution.duration());
}
@Override
public void onToolExecutionError(ToolExecutionRequest request, Throwable error) {
log.error("Tool {} execution failed: {}",
request.name(), error.getMessage());
// Record error metrics
meterRegistry.counter("tool.errors",
"tool", request.name(),
"error", error.getClass().getSimpleName())
.increment();
}
})
.build();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
LangChain4j Tool & Function Calling - Implementation Patterns
Comprehensive implementation patterns for tool and function calling with LangChain4j.
Core Tool Definition Patterns
Basic Tool Definition with @Tool Annotation
The @Tool annotation converts regular Java methods into tools that LLMs can discover and execute.
Basic Tool Definition:
public class CalculatorTools {
@Tool("Adds two given numbers")
public double add(double a, double b) {
return a + b;
}
@Tool("Multiplies two given numbers")
public double multiply(double a, double b) {
return a * b;
}
@Tool("Calculates the square root of a given number")
public double squareRoot(double x) {
return Math.sqrt(x);
}
@Tool("Calculates power of a number")
public double power(double base, double exponent) {
return Math.pow(base, exponent);
}
}Advanced Tool with Parameter Descriptions:
public class WeatherService {
@Tool("Get current weather conditions for a specific location")
public String getCurrentWeather(@P("The name of the city or location") String location) {
try {
WeatherData weather = weatherClient.getCurrentWeather(location);
return String.format("Weather in %s: %s, %.1f°C, humidity %.0f%%, wind %.1f km/h",
location, weather.getCondition(), weather.getTemperature(),
weather.getHumidity(), weather.getWindSpeed());
} catch (Exception e) {
return "Sorry, I couldn't retrieve weather information for " + location;
}
}
}Parameter Handling and Validation
Optional Parameters:
public class DatabaseTools {
@Tool("Search for users in the database")
public List<User> searchUsers(
@P("Search term for user name or email") String searchTerm,
@P(value = "Maximum number of results to return", required = false) Integer limit,
@P(value = "Sort order: ASC or DESC", required = false) String sortOrder) {
int actualLimit = limit != null ? limit : 10;
String actualSort = sortOrder != null ? sortOrder : "ASC";
return userRepository.searchUsers(searchTerm, actualLimit, actualSort);
}
}Complex Parameter Types:
public class OrderManagementTools {
@Description("Customer order information")
public static class OrderRequest {
@Description("Customer ID who is placing the order")
private Long customerId;
@Description("List of items to order")
private List<OrderItem> items;
@Description("Shipping address for the order")
private Address shippingAddress;
@Description("Preferred delivery date (optional)")
@JsonProperty(required = false)
private LocalDate preferredDeliveryDate;
}
@Tool("Create a new customer order")
public String createOrder(OrderRequest orderRequest) {
try {
// Validation and processing logic
Order order = orderService.createOrder(orderRequest);
return String.format("Order created successfully! Order ID: %s, Total: $%.2f",
order.getId(), order.getTotal());
} catch (Exception e) {
return "Failed to create order: " + e.getMessage();
}
}
}Memory Context Integration
@ToolMemoryId for User Context
Tools can access conversation memory context to provide personalized and contextual responses:
public class PersonalizedTools {
@Tool("Get personalized recommendations based on user preferences")
public String getRecommendations(@ToolMemoryId String userId,
@P("Type of recommendation: books, movies, restaurants") String type) {
UserPreferences prefs = preferenceService.getUserPreferences(userId);
List<String> history = historyService.getSearchHistory(userId, type);
return recommendationEngine.getRecommendations(type, prefs, history);
}
}Dynamic Tool Provisioning
ToolProvider for Context-Aware Tools
public class DynamicToolProvider implements ToolProvider {
@Override
public ToolProviderResult provideTools(ToolProviderRequest request) {
String userId = extractUserId(request);
UserPermissions permissions = permissionService.getUserPermissions(userId);
String userMessage = request.userMessage().singleText().toLowerCase();
ToolProviderResult.Builder resultBuilder = ToolProviderResult.builder();
// Always available tools
addBasicTools(resultBuilder);
// Conditional tools based on permissions
if (permissions.canAccessFinancialData()) {
addFinancialTools(resultBuilder);
}
if (permissions.canModifyUserData()) {
addUserManagementTools(resultBuilder);
}
return resultBuilder.build();
}
}Programmatic Tool Definition
public class ProgrammaticToolsService {
public Map<ToolSpecification, ToolExecutor> createDatabaseTools(DatabaseConfig config) {
Map<ToolSpecification, ToolExecutor> tools = new HashMap<>();
// Query tool
ToolSpecification querySpec = ToolSpecification.builder()
.name("execute_database_query")
.description("Execute a SQL query on the database")
.parameters(JsonObjectSchema.builder()
.addStringProperty("query", "SQL query to execute")
.addBooleanProperty("readOnly", "Whether this is a read-only query")
.required("query", "readOnly")
.build())
.build();
ToolExecutor queryExecutor = (request, memoryId) -> {
Map<String, Object> args = fromJson(request.arguments());
String query = args.get("query").toString();
boolean readOnly = (Boolean) args.get("readOnly");
return databaseService.executeQuery(query, readOnly);
};
tools.put(querySpec, queryExecutor);
return tools;
}
}AI Services as Tools
AI Services can be used as tools by other AI Services, enabling hierarchical architectures:
// Specialized Expert Services
interface DataAnalysisExpert {
@UserMessage("You are a data analysis expert. Analyze this data and provide insights: {{data}}")
@Tool("Expert data analysis and insights")
String analyzeData(@V("data") String data);
}
// Router Agent that delegates to experts
interface ExpertRouter {
@UserMessage("""
Analyze the user request and determine which expert(s) should handle it:
- Use the data analysis expert for data-related questions
- Use the security expert for security-related concerns
User request: {{it}}
""")
String routeToExperts(String request);
}
@Service
public class ExpertConsultationService {
public ExpertConsultationService(ChatModel chatModel) {
// Build expert services
DataAnalysisExpert dataExpert = AiServices.create(DataAnalysisExpert.class, chatModel);
// Build router with experts as tools
this.router = AiServices.builder(ExpertRouter.class)
.chatModel(chatModel)
.tools(dataExpert)
.build();
}
}Advanced Tool Patterns
Immediate Return Tools
public class DirectResponseTools {
@Tool(value = "Get current user information", returnBehavior = ReturnBehavior.IMMEDIATE)
public String getCurrentUserInfo(@ToolMemoryId String userId) {
User user = userService.findById(userId);
return String.format("""
User Information:
Name: %s
Email: %s
Role: %s
""", user.getName(), user.getEmail(), user.getRole());
}
}Concurrent Tool Execution
public class ConcurrentTools {
@Tool("Get stock price for a company")
public String getStockPrice(@P("Stock symbol") String symbol) {
try {
Thread.sleep(1000);
return stockApiService.getPrice(symbol);
} catch (InterruptedException e) {
return "Error retrieving stock price";
}
}
@Tool("Get company news")
public String getCompanyNews(@P("Company symbol") String symbol) {
// Similar implementation
}
}
// Configure for concurrent execution
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(new ConcurrentTools())
.executeToolsConcurrently() // Execute tools in parallel
.build();Error Handling and Resilience
Tool Execution Error Handling
public class ResilientTools {
private final CircuitBreaker circuitBreaker;
private final RetryTemplate retryTemplate;
@Tool("Get external data with resilience patterns")
public String getExternalData(@P("Data source identifier") String sourceId) {
return circuitBreaker.executeSupplier(() -> {
return retryTemplate.execute(context -> {
try {
return externalApiService.fetchData(sourceId);
} catch (ApiException e) {
if (e.isRetryable()) {
throw e; // Will be retried
}
return "Data temporarily unavailable: " + e.getMessage();
}
});
});
}
}Graceful Degradation
public class FallbackTools {
@Tool("Get weather information with fallback providers")
public String getWeather(@P("Location name") String location) {
// Try primary provider first
for (DataProvider provider : dataProviders) {
try {
WeatherData weather = provider.getWeather(location);
if (weather != null) {
return formatWeather(weather, provider.getName());
}
} catch (Exception e) {
// Continue to next provider
}
}
return "Weather information is currently unavailable for " + location;
}
}Streaming and Tool Execution
Streaming with Tool Callbacks
interface StreamingToolAssistant {
TokenStream chat(String message);
}
StreamingToolAssistant assistant = AiServices.builder(StreamingToolAssistant.class)
.streamingChatModel(streamingChatModel)
.tools(new CalculatorTools(), new WeatherService())
.build();
TokenStream stream = assistant.chat("What's the weather in Paris and calculate 15 + 27?");
stream
.onToolExecuted(toolExecution -> {
System.out.println("Tool executed: " + toolExecution.request().name());
System.out.println("Result: " + toolExecution.result());
})
.onPartialResponse(partialResponse -> {
System.out.print(partialResponse);
})
.start();Accessing Tool Execution Results
interface AnalyticsAssistant {
Result<String> analyze(String request);
}
AnalyticsAssistant assistant = AiServices.builder(AnalyticsAssistant.class)
.chatModel(chatModel)
.tools(new DataAnalysisTools(), new DatabaseTools())
.build();
Result<String> result = assistant.analyze("Analyze sales data for Q4 2023");
// Access the response
String response = result.content();
// Access tool execution details
List<ToolExecution> toolExecutions = result.toolExecutions();
for (ToolExecution execution : toolExecutions) {
System.out.println("Tool: " + execution.request().name());
System.out.println("Duration: " + execution.duration().toMillis() + "ms");
}Complete Tool-Enabled Application
Spring Boot Integration
@RestController
@RequestMapping("/api/assistant")
@RequiredArgsConstructor
public class ToolAssistantController {
private final ToolEnabledAssistant assistant;
@PostMapping("/chat")
public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest request) {
try {
Result<String> result = assistant.chat(request.getUserId(), request.getMessage());
ChatResponse response = ChatResponse.builder()
.response(result.content())
.toolsUsed(extractToolNames(result.toolExecutions()))
.tokenUsage(result.tokenUsage())
.build();
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.badRequest().body(
ChatResponse.error("Error processing request: " + e.getMessage())
);
}
}
}
interface ToolEnabledAssistant {
Result<String> chat(@MemoryId String userId, String message);
List<ToolInfo> getAvailableTools(String userId);
}Performance Optimization
Tool Performance Monitoring
@Component
public class ToolPerformanceMonitor {
@EventListener
public void handleToolExecution(ToolExecutionEvent event) {
// Record execution metrics
Timer.Sample sample = Timer.start(meterRegistry);
sample.stop(Timer.builder("tool.execution.duration")
.tag("tool", event.getToolName())
.tag("success", String.valueOf(event.isSuccessful()))
.register(meterRegistry));
// Record error rates
if (!event.isSuccessful()) {
meterRegistry.counter("tool.execution.errors",
"tool", event.getToolName(),
"error_type", event.getErrorType())
.increment();
}
}
}Testing Framework
@Component
public class ToolTestingFramework {
public ToolValidationResult validateTool(Object toolInstance, String methodName) {
try {
TestAssistant testAssistant = AiServices.builder(TestAssistant.class)
.chatModel(testChatModel)
.tools(toolInstance)
.build();
String response = testAssistant.testTool(methodName);
return ToolValidationResult.builder()
.toolName(methodName)
.isValid(response != null && !response.contains("Error"))
.response(response)
.build();
} catch (Exception e) {
return ToolValidationResult.builder()
.toolName(methodName)
.isValid(false)
.error(e.getMessage())
.build();
}
}
}Integration Examples
Multi-Domain Tool Service
@Service
public class MultiDomainToolService {
@Autowired
private Assistant assistant;
public String processRequest(String userId, String request, String domain) {
String contextualRequest = String.format("[Domain: %s] %s", domain, request);
Result<String> result = assistant.chat(userId, contextualRequest);
// Log tool usage
result.toolExecutions().forEach(execution ->
analyticsService.recordToolUsage(userId, domain, execution.request().name()));
return result.content();
}
}Database Integration
@Component
public class DatabaseTools {
private final CustomerRepository repository;
@Tool("Get customer information by ID")
public Customer getCustomer(@P("Customer ID") Long customerId) {
return repository.findById(customerId)
.orElseThrow(() -> new IllegalArgumentException("Customer not found"));
}
@Tool("Update customer email address")
public String updateEmail(
@P("Customer ID") Long customerId,
@P("New email address") String newEmail) {
Customer customer = repository.findById(customerId)
.orElseThrow(() -> new IllegalArgumentException("Customer not found"));
customer.setEmail(newEmail);
repository.save(customer);
return "Email updated successfully";
}
}REST API Integration
@Component
public class ApiTools {
private final WebClient webClient;
@Tool("Get current stock price")
public String getStockPrice(@P("Stock symbol") String symbol) {
return webClient.get()
.uri("/api/stocks/{symbol}", symbol)
.retrieve()
.bodyToMono(String.class)
.block();
}
@Tool("Create payment intent")
public String createPayment(@P("Amount") Double amount, @P("Currency") String currency) {
return webClient.post()
.uri("/api/payments")
.bodyValue(Map.of("amount", amount, "currency", currency))
.retrieve()
.bodyToMono(String.class)
.block();
}
}Context-Aware Tools
public class UserPreferencesTools {
@Tool("Get user preferences for a category")
public String getPreferences(
@ToolMemoryId String userId,
@P("Preference category (e.g., theme, language)") String category) {
return preferencesService.getPreferences(userId, category);
}
@Tool("Set user preference")
public String setPreference(
@ToolMemoryId String userId,
@P("Preference category") String category,
@P("Preference value") String value) {
preferencesService.setPreference(userId, category, value);
return "Preference saved";
}
}Dynamic Tool Provider
public class DynamicToolProvider implements ToolProvider {
private final Map<String, ToolWithExecutor> availableTools = new HashMap<>();
public void registerTool(String name, ToolSpecification spec, ToolExecutor executor) {
availableTools.put(name, new ToolWithExecutor(spec, executor));
}
@Override
public ToolProviderResult provideTools(ToolProviderRequest request) {
var builder = ToolProviderResult.builder();
String message = request.userMessage().singleText().toLowerCase();
// Dynamically filter tools based on user message
if (message.contains("weather")) {
builder.add(weatherToolSpec, weatherExecutor);
}
if (message.contains("calculate") || message.contains("math")) {
builder.add(calculatorToolSpec, calculatorExecutor);
}
return builder.build();
}
}Complete Service Example
@Service
public class CustomerSupportAssistant {
private final SupportAssistant assistant;
public CustomerSupportAssistant(ChatLanguageModel chatModel) {
this.assistant = AiServices.builder(SupportAssistant.class)
.chatModel(chatModel)
.tools(new CustomerTools(), new OrderTools(), new BillingTools())
.toolExecutionErrorHandler(this::handleErrors)
.chatMemoryProvider(MessageWindowChatMemory.withMaxMessages(20))
.build();
}
public String handleCustomerQuery(String customerId, String query) {
Result<String> result = assistant.chat(customerId, query);
// Log tool usage for analytics
result.toolExecutions().forEach(execution -> {
logToolUsage(customerId, execution.request().name());
});
return result.content();
}
private String handleErrors(ToolExecutionRequest request, Throwable exception) {
log.error("Tool execution failed: {} - {}",
request.name(), exception.getMessage());
if (exception instanceof CustomerNotFoundException) {
return "I couldn't find that customer. Please verify the customer ID.";
}
return "I encountered an issue processing your request. Please try again.";
}
interface SupportAssistant {
String chat(String userId, String message);
}
}LangChain4j Tool & Function Calling - API References
Complete API reference for tool and function calling with LangChain4j.
Tool Definition
@Tool Annotation
Purpose: Mark methods that LLM can call.
@Tool(value = "Description of what this tool does")
ReturnType methodName(ParameterType param) {
// Implementation
}
// Examples
@Tool("Add two numbers together")
int add(int a, int b) { return a + b; }
@Tool("Query database for user information")
User getUserById(String userId) { ... }
@Tool("Send email to recipient")
void sendEmail(String to, String subject, String body) { ... }@P Annotation
Purpose: Describe tool parameters for LLM understanding.
@Tool("Transfer money between accounts")
void transfer(
@P("source account ID") String fromAccount,
@P("destination account ID") String toAccount,
@P("amount in dollars") double amount
) { ... }Builder Configuration
AiServices Builder Extensions for Tools
AiServices.builder(AssistantInterface.class)
// Register tool objects
.tools(Object... tools) // Multiple tool objects
.tools(new Calculator()) // Single tool
.tools(new Calculator(), new DataService()) // Multiple
// Dynamic tool provider
.toolProvider(ToolProvider toolProvider)
// Error handlers
.toolExecutionErrorHandler(ToolExecutionErrorHandler)
.toolArgumentsErrorHandler(ToolArgumentsErrorHandler)
.build();Error Handlers
ToolExecutionErrorHandler
Purpose: Handle errors during tool execution.
@FunctionalInterface
interface ToolExecutionErrorHandler {
String handle(ToolExecutionRequest request, Throwable exception);
}
// Usage
.toolExecutionErrorHandler((request, exception) -> {
logger.error("Tool " + request.name() + " failed", exception);
return "Error executing " + request.name() + ": " + exception.getMessage();
})ToolArgumentsErrorHandler
Purpose: Handle errors in tool argument parsing/validation.
@FunctionalInterface
interface ToolArgumentsErrorHandler {
String handle(ToolExecutionRequest request, Throwable exception);
}
// Usage
.toolArgumentsErrorHandler((request, exception) -> {
logger.warn("Invalid arguments for " + request.name());
return "Invalid arguments provided";
})Tool Provider
ToolProvider Interface
Purpose: Dynamically select tools based on context.
@FunctionalInterface
interface ToolProvider {
List<Object> getTools(ToolProviderContext context);
}
// Context available
interface ToolProviderContext {
UserMessage userMessage();
List<ChatMessage> messages();
}Dynamic Tool Selection
.toolProvider(context -> {
String message = context.userMessage().singleText();
if (message.contains("calculate")) {
return Arrays.asList(new Calculator());
} else if (message.contains("weather")) {
return Arrays.asList(new WeatherService());
} else {
return Collections.emptyList();
}
})Tool Execution Models
ToolExecutionRequest
interface ToolExecutionRequest {
String name(); // Tool name from @Tool
String description(); // Tool description
Map<String, String> arguments(); // Tool arguments
}ToolExecution (for streaming)
class ToolExecution {
ToolExecutionRequest request(); // The tool being executed
String result(); // Execution result
}Return Types
Supported Return Types
Primitives:
@Tool("Add numbers")
int add(@P("a") int x, @P("b") int y) { return x + y; }
@Tool("Compare values")
boolean isGreater(@P("a") int x, @P("b") int y) { return x > y; }
@Tool("Get temperature")
double getTemp() { return 22.5; }String:
@Tool("Get greeting")
String greet(@P("name") String name) { return "Hello " + name; }Objects (will be converted to String):
@Tool("Get user")
User getUser(@P("id") String id) { return new User(id); }
@Tool("Get user list")
List<User> listUsers() { return userService.getAll(); }Collections:
@Tool("Search documents")
List<Document> search(@P("query") String q) { return results; }
@Tool("Get key-value pairs")
Map<String, String> getConfig() { return config; }Void:
@Tool("Send notification")
void notify(@P("message") String msg) {
notificationService.send(msg);
}Parameter Types
Supported Parameter Types
Primitives:
int, long, float, double, boolean, byte, short, charStrings and wrapper types:
String, Integer, Long, Float, Double, BooleanCollections:
List<String>, Set<Integer>, Collection<T>Custom objects (must have toString() that's meaningful):
@Tool("Process data")
void process(CustomData data) { ... }Dates and times:
@Tool("Get events for date")
List<Event> getEvents(LocalDate date) { ... }
@Tool("Schedule for time")
void schedule(LocalDateTime when) { ... }Annotation Combinations
Complete Tool Definition
class DataService {
// Basic tool
@Tool("Get user information")
User getUser(@P("user ID") String userId) { ... }
// Tool with multiple params
@Tool("Search users by criteria")
List<User> search(
@P("first name") String firstName,
@P("last name") String lastName,
@P("department") String dept
) { ... }
// Tool returning collection
@Tool("List all active users")
List<User> getActiveUsers() { ... }
// Tool with void return
@Tool("Archive old records")
void archiveOldRecords(@P("older than days") int days) { ... }
// Tool with complex return
@Tool("Get detailed report")
Map<String, Object> generateReport(@P("month") int month) { ... }
}Best Practices for API Usage
Tool Design
1. Descriptive Names: Use clear, actionable names
// Good
@Tool("Get current weather for a city")
String getWeather(String city) { ... }
// Avoid
@Tool("Get info")
String getInfo(String x) { ... }2. Parameter Descriptions: Be specific about formats
// Good
@Tool("Calculate date difference")
long daysBetween(
@P("start date in YYYY-MM-DD format") String start,
@P("end date in YYYY-MM-DD format") String end
) { ... }
// Avoid
@Tool("Calculate difference")
long calculate(@P("date1") String d1, @P("date2") String d2) { ... }3. Appropriate Return Types: Return what LLM can use
// Good - LLM can interpret
@Tool("Get user role")
String getUserRole(String userId) { return "admin"; }
// Avoid - hard to parse
@Tool("Get user info")
User getUser(String id) { ... } // Will convert to toString()4. Error Messages: Provide actionable errors
.toolExecutionErrorHandler((request, exception) -> {
if (exception instanceof IllegalArgumentException) {
return "Invalid argument: " + exception.getMessage();
}
return "Error executing " + request.name();
})Common Patterns
Validation Pattern:
@Tool("Create user")
String createUser(@P("email") String email) {
if (!email.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
return "User created: " + email;
}Batch Pattern:
@Tool("Bulk delete users")
String deleteUsers(@P("user IDs comma-separated") String userIds) {
List<String> ids = Arrays.asList(userIds.split(","));
return "Deleted " + ids.size() + " users";
}Async Pattern (synchronous wrapper):
@Tool("Submit async task")
String submitTask(@P("task name") String name) {
// Internally async, but returns immediately
taskExecutor.submitAsync(name);
return "Task " + name + " submitted";
}Integration with AiServices
Complete Setup
interface Assistant {
String execute(String command);
}
public class Setup {
public static void main(String[] args) {
var chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.temperature(0.0) // Deterministic
.build();
var assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
// Register tools
.tools(
new Calculator(),
new WeatherService(),
new UserDataService()
)
// Error handling
.toolExecutionErrorHandler((request, exception) -> {
System.err.println("Tool error: " + exception.getMessage());
return "Tool failed";
})
// Optional: memory for context
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
// Use the assistant
String result = assistant.execute("What is the weather in Paris?");
System.out.println(result);
}
}Resource Links
Setup and Configuration
Basic Tool Registration
// Define tools using @Tool annotation
public class CalculatorTools {
@Tool("Add two numbers")
public double add(double a, double b) {
return a + b;
}
}
// Register with AiServices builder
interface MathAssistant {
String ask(String question);
}
MathAssistant assistant = AiServices.builder(MathAssistant.class)
.chatModel(chatModel)
.tools(new CalculatorTools())
.build();Builder Configuration Options
AiServices.builder(AssistantInterface.class)
// Static tool registration
.tools(new Calculator(), new WeatherService())
// Dynamic tool provider
.toolProvider(new DynamicToolProvider())
// Concurrent execution
.executeToolsConcurrently()
// Error handling
.toolExecutionErrorHandler((request, exception) -> {
return "Error: " + exception.getMessage();
})
// Memory for context
.chatMemoryProvider(userId -> MessageWindowChatMemory.withMaxMessages(20))
.build();Chat Model Configuration
// For OpenAI
ChatLanguageModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName(GPT_4_O_MINI)
.build();
// For HuggingFace
ChatLanguageModel chatModel = HuggingFaceChatModel.builder()
.accessToken(System.getenv("HUGGINGFACE_API_KEY"))
.modelId("mistralai/Mistral-7B-Instruct-v0.2")
.build();Complete Setup Example
@Configuration
public class LangChain4jConfig {
@Bean
public ChatLanguageModel chatLanguageModel() {
return OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName(GPT_4_O_MINI)
.temperature(0.7)
.build();
}
@Bean
public MathAssistant mathAssistant(ChatLanguageModel chatModel) {
return AiServices.builder(MathAssistant.class)
.chatModel(chatModel)
.tools(new CalculatorTools())
.build();
}
}Related skills
Forks & variants (1)
Langchain4j Tool Function Calling Patterns has 1 known copy in the catalog totaling 21 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 21 installs
How it compares
Use this skill for Java LangChain4j tool design; pick Python agent skills when the stack is not on the JVM.
FAQ
What is the @Tool annotation for?
@Tool marks a Java method as a callable function the LLM can invoke. It requires a description string that guides the LLM on when to call it.
How do I prevent the LLM from calling tools that don't exist?
Use .hallucinatedToolNameStrategy() to catch requests for non-existent tools and return a safe error message instead of failing.
How do I run tools without waiting for the LLM to process the result?
Set ReturnBehavior.IMMEDIATELY on the @Tool annotation to execute the tool without returning control to the LLM.
Is Langchain4j Tool Function Calling Patterns safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.