Why We Don't Just Throw GC Logs at an LLM (And How JVMind's Agent Architecture Works)
TL;DR: Throwing a 30MB GC log directly into an LLM causes context overflow, lost key events, and hallucinated metrics. Pure rule engines only tell you what happened, not why. JVMind uses a three-tier architecture (Deterministic Parser + Hard Rules + LLM Agent) to deliver accurate, hallucination-free GC diagnostics.
The Trap: LLMs vs. Rule Engines
When building an AI-powered JVM diagnostics tool, there are two tempting extremes:
- The Rule Engine Approach: It's fast and deterministic. It can tell you "47 Full GCs happened in the last 30 minutes, averaging 420ms." But it can't tell you why, or whether you should resize the heap or look for a memory leak. It lacks business context.
- The Pure LLM Approach: Just paste the log into ChatGPT. The problem? A production GC log is easily 30MB. You can't fit it into the context window without aggressive sampling—which means you'll miss the exact GC event that caused the OOM. Worse, LLMs are terrible at structured math (percentiles, allocation rates) and will happily hallucinate pause durations that don't exist.
We needed something in between.
The Three-Tier Architecture
At JVMind, we split GC analysis into three distinct layers, each doing only what it's good at:
Tier 1: The Deterministic Parser
We chose to write a pure Regex-based parser instead of using AST generators like ANTLR. Why? Because GC log "syntax" is chaotic. The same field moves positions depending on the collector (G1 vs. Parallel), and JDK 8 PrintGCDetails looks nothing like JDK 9+ Unified Logging.
The parser's only job is to turn text into a structured event stream. It doesn't interpret anything. It just gives us hard facts.
Tier 2: The Hard Rules Layer
Some signals are "red lines" that must 100% trigger an alert. We hardcode these because we cannot trust an LLM to consistently catch them without false negatives.
Examples include: - Heap occupancy ≥ 98% (Imminent OOM) - G1 Full GC triggered by Allocation Failure - Post-GC heap occupancy remaining > 85% after N consecutive Full GCs (Live set saturated)
These rules output structured data: Severity, Title, Details, and Recommended Actions. The LLM cannot override these; it can only use them as context.
Tier 3: The LLM Agent
This is where the magic happens. The Agent acts as a decision-maker. It doesn't read raw logs. It reads the compressed summary and rule alerts from Tiers 1 and 2.
The LLM's job is to answer the "trade-off" questions that rules can't handle: - Is 99.5% throughput bad? (Depends: yes for a trading system, fine for batch processing). - Should I increase the heap? (Depends on hardware cost and latency SLAs).
Designing the Agent's Tools
An Agent is only as good as its tools. We made three critical design decisions here:
1. Minimal Input (file_id instead of file contents)
Tools only accept a file_id string, not file paths or contents. LLMs are bad at "finding files," and passing 30MB of text explodes the context window. By passing an 8-byte ID, tool calls become practically zero-cost.
2. Compressed Output
A full GC report contains hundreds of events and dozens of metrics. If we return all of it to the LLM, one analysis will consume the entire context history. Instead, tools return a strictly compressed <2500 character summary: Collector type, heap capacity, pause percentiles, Top 5 slowest events, and triggered rules.
3. Tiered Drill-Down (query_gc_events)
Initially, our GC tools had a granularity gap. We had tools to analyze a whole log, and tools to read a specific thread in jstack, but nothing in between for GC.
We fixed this by adding query_gc_events. If the user asks, "Show me all Full GCs between 3 AM and 3:30 AM," the LLM calls this tool with filters (category="Full", time_start=..., time_end=...).
Crucially, this tool does not re-parse the log. It queries the already-persisted structured events in the database. It's an in-memory filter taking milliseconds, costing zero LLM tokens for parsing.
Killing Hallucinations in the Prompt
The biggest risk of using LLMs for diagnostics is fabricated numbers. LLMs love to "fill in the blanks" when they lack data.
We enforce a strict rule in the System Prompt:
"The statistics returned by tools are facts. Do not fabricate numbers. If the user asks for information not present in the tool output, explicitly state that more data is needed."
Because the LLM is forced to reference the structured summary we provide, hallucination rates drop to near zero.
The Result: Persistence and Cost Control
Because the parsing and rule-engine layers are deterministic, their results are persisted to the database alongside the LLM's generated diagnosis.
What does this mean for the user? When you open a historical GC report at 3 AM during an incident, it loads instantly. You don't wait 30 seconds for the LLM to re-read a 30MB log. The hard facts and AI conclusions are already there.
By keeping deterministic logic out of the LLM, and keeping LLM logic out of the parser, we built a GC analyzer that is fast, auditable, and actually understands business context.
Try it yourself: Upload your GC log or jstack dump to JVMind and ask it why your app is slow.