Skip to main content

Command Palette

Search for a command to run...

LangGraph4j ReACT Agent: Explicit State Graphs for Tool Orchestration

Updated
4 min readView as Markdown
P

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:

  1. toolsFromObject() takes Object... — not List<Object>. Easy to get wrong.

  2. AsyncGenerator doesn't have forEachRemaining() — use a for-each loop instead.

  3. AgentExecutor.State messages 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.