LangChain4j Agents & Tools
Build tool-using agents in LangChain4j: the @Tool annotation, how the agent loop works, bounding iterations, safe write tools and the ReAct pattern — with production-ready code.
On this page
Tools turn a LangChain4j AI Service from a text generator into something that can act — look up data, call an API, run a calculation. This tutorial covers defining tools, how the agent loop runs them, and the guardrails that make a tool-using agent safe enough to deploy.
Key Takeaways
- Annotate a method with
@Tool; the description is the entire interface the model sees. - LangChain4j runs the ReAct loop for you — reason, act, observe, repeat.
- Bound the loop with
maxSequentialToolsInvocations; return errors as values. - Write tools authorize in code and require confirmation for irreversible actions.
Defining a tool
class OrderTools {
private final OrderRepository orders;
OrderTools(OrderRepository orders) {
this.orders = orders;
}
// The description is the contract the model reads. Say when to use it and
// what it returns — the model never sees the method body.
@Tool("""
Look up the delivery status of an order by its ID.
Use when a customer asks where their order is or when it will arrive.
Returns the status and expected date, or NOT_FOUND.
""")
String orderStatus(String orderId) {
return orders.findById(orderId)
.map(o -> "%s, expected %s".formatted(o.status(), o.expectedDelivery()))
.orElse("NOT_FOUND: no order " + orderId);
}
}Register it on the AI Service:
interface SupportAgent {
String handle(String message);
}
SupportAgent agent = AiServices.builder(SupportAgent.class)
.chatModel(chatModel)
.tools(new OrderTools(repository))
// Cap the loop so a confused agent cannot spin forever.
.maxSequentialToolsInvocations(5)
.build();How the loop runs
You wrote no routing, no intent parsing, and no loop. The model decides when to call the tool; the library executes it and feeds the result back.
Writing descriptions the model can use
The description is the whole skill. It must explain when to use the tool, not how it works:
// Poor — describes the implementation.
@Tool("Queries the orders table")
// Good — when to use it, what it needs, what it returns.
@Tool("""
Look up an order's delivery status by ID. Use when a customer asks about
the location or arrival of an existing order. Requires the order ID.
Returns status and expected date, or NOT_FOUND.
""")Errors as values
A tool error is conversation, not an exception. Return a message that tells the model what to do:
@Tool("Issue a refund for an order. Returns the refund reference.")
String refund(String orderId) {
Order order = orders.findById(orderId).orElse(null);
if (order == null) {
// Keeps the model in the loop so it can ask for a correct ID.
return "ERROR: no order " + orderId + ". Ask the customer to check the ID.";
}
if (order.isRefunded()) {
return "ERROR: order " + orderId + " was already refunded.";
}
return "REFUNDED: reference " + refunds.issue(order).reference();
}A thrown exception aborts the whole request; a returned error lets the agent recover gracefully.
Safe write tools
Read tools are low risk. Write tools — anything that changes state — need real guardrails:
@Tool("""
Cancel the current customer's subscription at period end.
Only call after the customer has explicitly confirmed.
""")
String cancelSubscription(String confirmation) {
// 1. Identity from the session, never a tool argument.
String userId = SecurityContextHolder.getContext().getAuthentication().getName();
// 2. Authorization checked in code.
if (!subscriptions.canCancel(userId)) {
return "ERROR: this account cannot self-cancel.";
}
// 3. Confirmation for the irreversible action.
if (!"CONFIRM".equals(confirmation)) {
return "CONFIRMATION_REQUIRED: confirm with the customer, then call again "
+ "with confirmation=CONFIRM.";
}
subscriptions.cancelAtPeriodEnd(userId);
return "CANCELLED: ends on " + subscriptions.periodEnd(userId);
}Read tools vs write tools
Start with read-only tools. They deliver most of the value at a fraction of the risk and let you learn how your model behaves before you hand it anything destructive. When you add write tools, register them per request rather than globally, so a support question does not have the refund tool in scope at all.
Combining tools with memory and RAG
An AI Service can have tools, memory and retrieval at once:
SupportAgent agent = AiServices.builder(SupportAgent.class)
.chatModel(chatModel)
.tools(new OrderTools(repository))
.chatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(20))
.contentRetriever(retriever) // RAG for policy questions
.maxSequentialToolsInvocations(5)
.build();This is a capable support agent: it answers policy questions from documents, looks up real order data through tools, and remembers the conversation.
Next
- What is Agentic AI? — the concepts behind the loop
- Agent architecture patterns
- LangChain4j chat memory
Frequently Asked Questions
How do I create a tool in LangChain4j?
How does the agent loop work in LangChain4j?
How do I prevent an agent from looping forever?
What is the ReAct pattern?
Related tutorials
- LangChain4j Retrievers & RAGBuild RAG in LangChain4j with ContentRetriever: attach retrieval to AI Services, transform queries, re-rank results, and assemble an advanced RAG pipeline with the RetrievalAugmentor.
- LangChain4j Chat MemoryAdd conversation memory to LangChain4j AI Services: message and token windows, per-user memory with @MemoryId, persistent stores, and why unbounded memory breaks in production.
- LangChain4j Embedding StoresStore and search vectors in LangChain4j: the in-memory store for tests, PgVector for production, Redis and Elasticsearch, plus metadata filtering and picking the right store.
- LangChain4j Structured OutputReturn typed objects from LangChain4j AI Services: POJO and record return types, enums, lists, JSON schema mode and validation — no manual parsing of model responses.