LangGraph4j ReACT Agent: Explicit State Graphs for Tool Orchestration
I am a Software Engineer from Dallas, Texas, USA, developing cyber security softwares.
When building LLM applications, there's a spectrum between "just call tools automatically" and "I need to see and control every step of the reasoning loop." LangChain4j's built-in tool calling does the former. LangGraph4j does the latter — and the difference is more useful than you'd think.
The ReACT Pattern
ReACT (Reason + Act) is a simple loop: the LLM reasons about what to do, picks a tool, observes the result, and decides whether to continue or answer. Most frameworks hide this loop behind a single chat() call. LangGraph4j exposes it as a first-class state graph.
The graph structure:
__START__ → agent (LLM reasons) → action (tool executes) → agent → ... → __END__
Each node is named, traceable, and inspectable. You can see exactly when the LLM decided to call a tool, what tool it called, what result it got, and how it decided to stop.
What We Built
We integrated LangGraph4j's AgentExecutor into the demo project as a new orchestration endpoint alongside the existing agentic-services patterns (supervisor, chain, parallel, loop, conditional).
@Service
public class ReactAgentService {
private final CompiledGraph<AgentExecutor.State> compiledGraph;
public ReactAgentService(ChatModel chatModel,
CalculatorTool calculatorTool,
DocumentSearchTool documentSearchTool,
WeatherTool weatherTool,
EmbeddingStoreStatsTool storeStatsTool) throws GraphStateException {
StateGraph<AgentExecutor.State> graph = AgentExecutor.builder()
.chatModel(chatModel)
.toolsFromObject(calculatorTool, documentSearchTool, weatherTool, storeStatsTool)
.build();
this.compiledGraph = graph.compile();
}
public ReactResult run(String task) {
// Stream to capture each graph node transition
var generator = compiledGraph.stream(Map.of("messages", UserMessage.from(task)));
for (var item : generator) {
steps.add(item.node()); // "agent", "action", "agent", ...
}
// Get final state
var finalState = compiledGraph.invoke(Map.of("messages", UserMessage.from(task)));
String answer = finalState.get().finalResponse().orElse("No response");
return new ReactResult(task, answer, steps, allMessages);
}
}
How It Differs from Built-in Tool Calling
| LangChain4j Tool Calling | LangGraph4j AgentExecutor | |
|---|---|---|
| Control | Implicit loop | Explicit state graph |
| Traceability | Final result only | Every node transition visible |
| Extension point | Limited | Full graph modification |
| Mental model | "Call this function" | "Build a state machine" |
The explicit graph approach means you can:
Debug reasoning — see exactly why the LLM chose tool A over tool B
Add guardrails — insert nodes between agent and action
Build complex patterns — conditional branching, human-in-the-loop (issues #236, #237)
Monitor performance — track which tools are called, how many loops, where bottlenecks occur
The Pitfalls
LangGraph4j's API has some rough edges:
toolsFromObject()takesObject...— notList<Object>. Easy to get wrong.AsyncGeneratordoesn't haveforEachRemaining()— use a for-each loop instead.AgentExecutor.Statemessages include ALL intermediate steps — the final state has the complete conversation history including every reasoning step, not just the initial prompt and final answer.
Running It
# CLI
./mvnw spring-boot:run
/react compute 2+2
# REST
curl -X POST http://localhost:8080/api/react \
-H "Content-Type: application/json" \
-d '{"message":"What is the weather in Tokyo and what is 15% of 340?"}'
The response includes the full trace:
{
"task": "What is the weather in Tokyo and what is 15% of 340?",
"answer": "The weather in Tokyo is 22°C and partly cloudy. 15% of 340 is 51.",
"steps": ["agent", "action", "agent", "action", "agent"],
"agentMessages": ["I need to check the weather and calculate 15% of 340.", ...]
}
What's Next
This is the foundation for two more patterns:
Stateful Pipeline — persist graph state across invocations, resume from checkpoint
Human-in-the-Loop — pause the graph for human approval before executing sensitive tools
The LangGraph4j integration is the most powerful orchestration pattern we've added — but also the most opinionated. For simple tool use, stick with @AiService. When you need visibility, control, and extensibility, reach for the graph.