Stop Infinite ReAct Loops: Deterministic Cycle Detection in Spring AI with Java 21 Record Patterns
Runaway ReAct agent loops are the single fastest way to blow through a enterprise LLM budget in a single deployment. If you rely on soft system prompt instructions or simple max-message limits to stop infinite agent execution, your production setup is a liability.
Why Most Developers Get This Wrong
-
Relying on hard iteration caps: Setting
max-iterations=10in your orchestrator just defers the bill spike without solving deterministic tool loops. - Naive string matching: Checking raw LLM text output misses cycles when tool arguments contain dynamic values like timestamps or ephemeral tracing IDs.
- Prompt-level guardrails: Pleading with models ("do not execute the same tool twice") fails probabilistically under heavy context windows.
The Right Way
Catch execution cycles at the runtime level by pairing a custom Spring AI CallAroundAdvisor with Java 21 record patterns across a rolling sliding window.
-
Intercept at the Advisor boundary: Intercept
AdvisedRequestpayloads before Spring AI dispatches state back to your model provider. -
Deconstruct tool calls with Record Patterns: Extract tool signatures and arguments cleanly using Java 21 pattern matching (
case ToolCall(String name, Map args)). - Sliding Window Fingerprinting: Hash sanitized tool signatures over a 5-step rolling window to instantly catch $A \rightarrow B \rightarrow A$ cyclic tool execution.
- Short-circuit execution: Throw a typed runtime exception immediately to break the loop before incurring another API token charge.
I built javalld.com while prepping for senior roles — complete LLD problems with execution traces, not just theory.
Show Me The Code
public record ToolCall(String name, Map<String, Object> args) {}
public class CycleDetectionAdvisor implements CallAroundAdvisor {
private final SlidingWindowCache<Integer> window = new SlidingWindowCache<>(5);
@Override
public AdvisedResponse around(AdvisedRequest req, CallAroundAdvisorChain chain) {
if (req.attributes().get("last_tool") instanceof ToolCall(String name, var args)) {
int signatureHash = Objects.hash(name, sanitize(args));
if (window.containsAndAdd(signatureHash)) {
throw new AgentCycleDetectedException("Loop detected for tool: " + name);
}
}
return chain.nextAround(req);
}
}
Key Takeaways
- Enforce guardrails in code, not prompts: LLMs are probabilistic, but cycle detection in your backend pipeline must be strictly deterministic.
- Leverage Java 21 Record Patterns: Cleanly destruct agent state without messy reflection or verbose instance checks.
-
Fail fast at the framework edge: Catch tool looping inside Spring AI's
CallAroundAdvisorbefore firing unnecessary LLM token calls.
Top comments (1)
Deterministic loop detection is the right framing. A ReAct loop is not only a bad answer; it is a control-system failure where the agent keeps proving it cannot change state. I like that this treats the loop as something observable in the trace instead of something you hope the model notices.