Agentic RAG is a graph with a grade, not a fancier prompt

Classic RAG retrieves once, stuffs the top-k, and hopes. DZone named the loop. LangChain4j 1.18 already has the types. Java should own whether the evidence is sufficient.

Classic RAG retrieves once, stuffs the top-k into a prompt, and hopes those chunks contain the answer. That is not a beginner mistake. That is the architecture. It works for FAQ. It silently fails on a multi-part question, a bad paraphrase, or a fact that lives in SQL instead of a paragraph. Building Agentic RAG, Step by Step is the right diagnosis. Retrieval becomes a tool. An agent plans, grades, reformulates, synthesizes, and self-checks. The article is Python-shaped. I ship Java. LangChain4j 1.18 already has the types for that loop. The job is not to wrap it in one giant prompt and call it agentic. Retrieve once is a ceiling. It is not a style choice. What the article got right Balaji Venkatasubramaniyar names seven hops. I will not rename them for sport. Tools, not just an index. Vector, keyword or BM25, SQL or an API, web if you are allowed. A retrieval plan before the first tool call. Sub-questions. Which tool. One pass or several. Grade before you generate. RELEVANT, PARTIALLY_RELEVANT, IRRELEVANT. Drop the last bucket. Reformulate and retry when the grade is empty. Cap the attempts. Surface exhaustion. Synthesize across sources , cite, flag contradictions, say insufficient instead of filling gaps. Self-check that claims trace to context. Failed claims become new sub-questions. Orchestrate as a graph , not a monologue, so you can see which node lied. That loop is the whole value. It costs tokens and latency. It converts a system that fails quietly into one that tries, then stops. Figure 1. One shot versus a grade. Naive RAG generates anyway. Agentic RAG grades first. I already argued that unverified loops are the thing that died . Agentic RAG is the same valve, pointed at retrieval. The Java mapping In LangChain4j 1.18.1, with agentic 1.18.1-beta28 , I would not implement this as “give the model tools and hope.” I would give each hop a type. DZone hop LangChain4j 1.18 type Tools EmbeddingStoreContentRetriever , WebSearchContentRetriever , experimental SqlDatabaseContentRetriever , or a @Tool Plan LanguageModelQueryRouter , or a planner agent writing into AgenticScope On demand RetrievalAugmentor every turn, or RAG-as-a-tool so search is optional Grade ReRankingContentAggregator plus a ScoringModel , then a GradeVerdict record Reformulate ExpandingQueryTransformer , or loopBuilder rewriting the query in scope Synthesize Synthesizer agent; DefaultContentInjector for the evidence pack Verify Verifier agent; compact constructor rejects unsupported claims Graph sequenceBuilder + loopBuilder + conditionalBuilder ; AgentListener Figure 2. DZone named the hops. 1.18 already has the types. The interesting choice is not Python versus Java It is whether the grade is a prompt or a type. The model proposing RELEVANT is a status update. A GradeVerdict record is a check. I already paid for the toolbox. I had not paid for the loop. In SYJ RAG, product search is not “embed the question and take five neighbors.” It fuses pgvector with pg_trgm on SKU and parent rows, then re-ranks with intent filters and live stock. Vector is one drawer. Trigram is another. Stock is a third signal. That is DZone step 1 in production, without calling it agentic. The ecommerce product chat on the leather storefront is the same lesson under a different classloader: embeddings live in an index, the widget still has to refuse a confident answer when retrieval is empty. What those systems do not do, on purpose, is let an LLM rewrite the query three times and grade its own evidence. Hybrid retrieval is cheap and testable. An agentic retry is a budget. Figure 3. Four tools. A vector store is not a toolbox. LanguageModelQueryRouter reads the labels. LangChain4j’s advanced RAG pipeline is the static version of the same idea: QueryTransformer → QueryRouter → retrievers → ContentAggregator → ContentInjector . RetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder() .queryTransformer(new ExpandingQueryTransformer(chatModel)) .queryRouter(LanguageModelQueryRouter.builder() .chatModel(chatModel) .retrieverToDescription(retrievers) .build()) .contentAggregator(ReRankingContentAggregator.builder() .scoringModel(scoringModel) .minScore(0.6) .build()) .build(); That is a better pipeline. It is not yet the DZone loop. The augmentor still runs because you invoked the assistant. It does not decide to stop and say the corpus cannot answer. It does not own a cap. For FAQ, that is enough. For “compare Q3 volume to Q2 and explain the driver,” it is not. Plan in Java. Do not rent a supervisor for a graph you can name. LanguageModelQueryRouter is the small way. Vector for “how does X work.” Keyword for an error code. SQL for a current fact. An explicit sequence is the honest way when you already know the shape. Planner agent writes RetrievalPlan into AgenticScope . A conditionalBuilder skips SQL if the plan did not ask for it. sequenceBuilder runs retrieve, then grade. A supervisor that “figures it out” is the same conflict of interest as an agent that grades its own homework. Name the graph. Keep supervisorBuilder for work whose topology you cannot write down yet. Grade is a record. Reformulate is a loop with a door. DZone’s grader answers RELEVANT / PARTIALLY_RELEVANT / IRRELEVANT per chunk. Keep that enum. Do not let the synthesis prompt see IRRELEVANT . ReRankingContentAggregator plus a ScoringModel is the cheap grade: a number, a minScore , drop the tail. When the question is high-stakes, add a grader agent that returns a typed verdict. The compact constructor recomputes. If every chunk is IRRELEVANT , sufficient is false even if the model wrote otherwise. public record GradeVerdict( List<GradedChunk> chunks, boolean sufficient, int relevantCount ) { public GradeVerdict { chunks = chunks == null ? List.of() : List.copyOf(chunks); relevantCount = (int) chunks.stream() .filter(c -> c.grade() != Grade.IRRELEVANT) .count(); sufficient = relevantCount > 0; } } If sufficient is false, rewrite the query, up to three attempts, then return empty and admit it. That is loopBuilder . UntypedAgent retrieveUntilGraded = AgenticServices.loopBuilder() .subAgents(retrieverAgent, graderAgent, reformulatorAgent) .maxIterations(3) .exitCondition(scope -> { GradeVerdict grade = scope.readState("gradeVerdict", GradeVerdict.empty()); return grade.sufficient(); }) .build(); Figure 4. Three tries, then stop. The cap lives in config. The model does not get to extend it. What this costs, and when I would not pay DZone is honest about the bill, and I will be too. Grading, reformulation, and verification are extra calls. I have not measured that multiplier on my corpus. Treat 2–5× as their report, not my benchmark. When I would pay for the loop Product judgment, not a dataset. FAQ stays single-pass. Multi-part, SQL-plus-paragraph, and “wrong in a way a customer will remember” get the graph. Question shape Path Why Store hours, stable FAQ Naive RAG One good chunk is enough SKU / error code / identifier Toolbox Keyword drawer, maybe no loop Live stock or current status SQL + grade Structured lookup, then refuse if empty Compare Q3 to Q2 and explain Full loop Plan, two sources, cap, INSUFFICIENT Anything that can miss fluently Full loop “I don’t know” beats a confident miss If you cannot name the exit ( sufficient , verified , INSUFFICIENT ), you do not have agentic RAG. You have a more expensive stuff-the-prompt. Takeaways Treat retrieve-once as the FAQ path. Treat plan-grade-retry as the path for questions that can miss. Give the agent a toolbox: vector, keyword, structured lookup. Put the grade in a Java record. The model proposes relevance. Your code decides whether to generate. Cap reformulation in loopBuilder...