Loop Engineering for RAG Question Parsing: The Small Loop That Runs Before Retrieval

A user asks the pipeline: “what is the premium?” on a fifty-page insurance policy. The parser looks at the doc profile it just got from the parsing brick:

  • doc_type: insurance_policy
  • n_pages: 47
  • toc_df names General Information, Coverages, Exclusions, Endorsements. No section called Premium.

Naive top-k embeds “premium” and scans the whole document. It returns exclusion clauses that mention the word, endorsement boilerplate, and a few incidental references. The actual premium schedule sits under General Information on page 3, but nothing in the query said so.

A loop-engineered pipeline does something different. The parser sees “premium” has no match in the TOC, holds the pipeline, and sends one plain question back to the user:

“I don’t see a ‘Premium’ section in this policy. Where should I look?”

They might type “General Info” (short), “generale information” (typo), or “try under coverages” (rephrased). None of that matters. The parser re-runs on the enriched question, the LLM fits whatever came back into section_hint or another typed field, and the pipeline continues.

That single turn removes the ambiguity and scopes retrieval.

The fuller mechanism (an explicit list of candidate values, a proposed default, an audit trail so the answer can be cached and applied silently next time) is what Article 6bis develops. This article stays on the smallest possible version: a question in, an answer out, one LLM pass.

Enterprise Document Intelligence builds enterprise RAG on four bricks (document parsing, question parsing, retrieval, generation). This article re-reads brick 2 through the loop-engineering lens, the emerging discipline that names the loops built around the LLM as the place where production quality is won. Articles 6A (thesis), 6B (extraction), and 6C (dispatch) code the brick. Article 6bis codes the clarification loop’s mechanics. This one names why that mechanic is the loop-engineering pattern applied at the smallest possible scope.

where this article sits in the series: brick 6 (question parsing) highlighted, read through the loop-engineering lens – Image by author

📓 The three cases in Section 3 all run in the shipped notebook: reproduce the clarification loop yourself against the arXiv corpus and a broker contract, watch how the LLM’s “what is missing?” question shifts with the doc profile. Repo → doc-intel/notebooks-vol1.

The public companion-code repo at doc-intel/notebooks-vol1 – Image by author

1. From prompt to context to loop engineering

The frame has moved twice in eighteen months.

Prompt engineering (2023). The user does the work. Learn to write the right prompt, add few-shot examples, use “think step by step”. The LLM is a stateless oracle. Quality is a wording problem.

Context engineering (mid-2025). The engineer does the work. Tobi Lütke and Andrej Karpathy rename the practice: “the delicate art of filling the context window with just the right information for the next step.” The prompt is one slot among many. See Article 6quater (context engineering for question parsing) for that framing applied to this brick.

Loop engineering (2026). The engineer designs the loops around the LLM call. LangChain puts it plainly: “the potential in agents is in the loops you build around them.” MindStudio frames it as “designing AI systems that operate in iterative cycles, repeating until a goal is met”, the discipline that “closes the feedback gap.”

The three are not a race. They stack: prompt engineering shapes what one call reads, context engineering picks what enters that call, loop engineering wraps the call in a bounded iteration.

Loops come in sizes. A big loop is agentic RAG (plan, act, observe, replan, many turns). A small loop is a single ask-answer-continue turn. This article is about the smallest useful loop, sitting on question parsing.

2. The loop fills a fixed schema

Before the loop, name the fields it will fill.

Question parsing writes into a fixed set of typed fields, defined once for the whole series and consumed deterministically by retrieval and generation. Vol.1 ships with these:

  • keywords: content noun phrases for retrieval detectors.
  • intent: a bounded enum (factual, listing, section_retrieval, open_scoped, open_corpus_wide).
  • retrieval.section_hint: a section name or number to filter toc_df.
  • retrieval.layout_hint: "table", "figure", or "glossary" when the answer sits in a specific layout.
  • structural_hints.pages_hint: an explicit page list the user pinned in the question.
  • (sheets_hint / slides_hint for Vol.2 formats, unused here.)

These names are not article-local. They live in the docintel.question module and every downstream brick (retrieval detectors, dispatcher, generation schema lookup) reads them. Add a field and the whole pipeline changes. That is a design decision, not a JSON tweak.

The loop’s only job is to fill one of these fields when the parser cannot fill it alone. It never invents a new one.

The flow, six steps and one loop-back arrow:

The small loop on question parsing, one turn, one field at a time. Loop back only from step 3 to step 4. – Image by author

In code, the same flow on the premium question from the intro:

# 1-2. First parse from raw question + doc context.
parsed = parse_question(
    raw="what is the premium?",
    doc_context=doc_context,  # doc_type='insurance_policy', toc_df=..., ...
)

# What comes back:
assert parsed.keywords == ["premium"]
assert parsed.intent == "factual"
assert parsed.retrieval.section_hint is None   # <-- the missing field
assert parsed.structural_hints is None

# 3. Detect the missing field.
if parsed.retrieval.section_hint is None and _topic_looks_sectioned(parsed, doc_context):
    question_to_user = (
        f"I don't see a '{parsed.keywords[0].title()}' section in this policy. "
        "Where should I look?"
    )
    user_reply = ask_user(question_to_user)   # UI round-trip, free-form answer

    # 4-5. Re-parse with the enriched question.
    parsed = parse_question(
        raw=f"{parsed.original_question} (look under {user_reply})",
        doc_context=doc_context,
    )

# By now, section_hint is filled from the user's reply.
plan = dispatch(parsed)                       # step 6, unchanged

Notice what changes across the two parse_question(...) calls: only the field that was missing. Every other field of ParsedQuestion stays where it was. dispatch(parsed) at the end is the exact same call it would have been without a loop, running against the same schema.

Not every field is mandatory: no TOC means no section_hint, and that is fine as long as another field (like pages_hint) scopes the search. The engineering work is picking which field to ask about, given the doc profile, and how to phrase the question for the user’s context.

See Article 6bis for the fuller mechanic that adds candidate enumeration, a proposed default, and caching so the same ask fires silently next time.

3. Three cases, three missing fields

Each case below names the one field the parser cannot fill from the raw question alone, the doc profile that flags the gap, and the plain question that fills it. Same schema across the three, no new field invented.

3.1 Missing section_hint: topic not in the TOC

Picture an insurance analyst opening a policy she has never seen before. Forty-seven pages, four TOC entries, nothing standing out at a glance. She types the question she would otherwise answer with ten minutes of scrolling:

“What is the premium for the first quarter?”

The parser reads the question alongside what document parsing already knows about the file:

  • doc_type: insurance_policy
  • n_pages: 47
  • toc_df names General Information, Coverages, Exclusions, Endorsements. Nothing that carries the word Premium.

On its first pass, parse_question(...) fills what it can:

  • keywords comes back as ["premium"].
  • intent comes back as factual because the answer is one number, not a summary or a list.
  • retrieval.section_hint refuses to fill.

The last one is what stops the pipeline. The user’s topic does not correspond to any TOC label, and the parser will not invent one. Rather than forward a half-filled row to retrieval, it holds and asks.

One question goes back to the analyst:

“I don’t see a ‘Premium’ section in this policy. Where should I look?”

She types whatever comes first. “General Info” if she is rushing. “The general one” if she is unsure of the exact label. “Try general information” if she is being precise. The wording does not matter. The same LLM that parsed the original question re-runs on the enriched string, resolves whichever variant she typed to “General Information”, and writes it into section_hint on the second pass. Every other field on the row stays exactly where the first parse left it.

From that point the pipeline is deterministic again. section_filter_active fires downstream because section_hint is now set. Retrieval reads only the pages inside General Information on toc_df (page 3, in this policy). Generation reads a crisp passage instead of forty-seven pages of noise. The analyst gets the number back in seconds.

3.2 Missing pages_hint: multi-position topic

A different desk, a different pipeline surface. A paralegal asks the assistant to look at a contract she needs to file before the end of the day:

“What is the client’s name?”

The doc profile confirms a contract, forty-seven pages long. The TOC lists numbered clauses but never spells out a Parties header. On the first pass:

  • keywords comes back as ["client name"].
  • intent comes back as factual.
  • structural_hints.pages_hint stays None.

The last one is the trap. On a contract, the client’s name lives in three canonical positions:

  • the cover page;
  • the running header at the top of every page;
  • the signatory block at the end.

Forwarding the question without a page hint sends retrieval into the whole document and drags in every boilerplate mention of “the client” in between. The right passage is on page 1. The signal-to-noise ratio collapses before generation ever runs.

The parser writes back:

“Contracts often carry the client’s name in a few places (cover, header, signatories). Where do you want me to look?”

The paralegal answers “cover”, or “page 1”, or “the first page”. The exact phrasing does not matter. On the second parse, the LLM resolves whichever variant she typed into pages_hint: [1]. keywords and intent stay where they were. pages_hint_active fires downstream, retrieval reads page 1 and only page 1, and the LLM generates against one clean candidate.

3.3 Missing pages_hint: no TOC on a long doc

A researcher opens an internal risk paper and asks the assistant:

“Summarize the risk section.”

The doc profile flags a research_paper, thirty-two pages, and toc_df is empty. Visually the paper has section headings, but the parsing brick could not extract them cleanly (nested styling, inconsistent numbering, whatever caused the miss). That kills section_hint before the loop can even try. There is no TOC entry to bind to.

But it does not kill pages_hint. The researcher may know roughly where in the paper the risk discussion lives, and a rough page range is all retrieval needs to narrow the search. That gives the parser one useful thing to ask about:

“This paper has no clean table of contents. Do you know roughly which pages cover the risk section?”

Two answers send the pipeline to two very different places:

  • “No idea” keeps pages_hint: None. The pipeline honestly falls back to a full-document scan. Retrieval reads all thirty-two pages, generation runs on the widest possible context. Expensive, but at least the user was told what would happen.
  • “Around the middle” resolves to pages_hint: [11, 12, 13, ..., 22] on the second parse. pages_hint_active fires. Retrieval reads one third of the document instead of all of it. Generation sees a shorter, more relevant context. The summary arrives faster and the LLM bill is a third of what it would have been.

It is the same brick chain running on a smaller working set.

Notice what stays constant across the three. Every field the loop touches (section_hint, pages_hint) already existed on the schema. The loop’s contribution is choosing which one to ask about given the doc profile. Same downstream code, same dispatcher, same activation flags. Nothing new to plumb through the pipeline.

4. Where the loop fits, and why it is not agentic RAG

The loop sits inside question parsing. The rest of the pipeline never sees it: retrieval only starts once question parsing has produced a full-confidence ParsedQuestion, whether on the first pass or after one clarification turn.

The loop lives inside question parsing; retrieval and generation only ever see a filled ParsedQuestion – Image by author

Retrieval and generation see one interface: a filled ParsedQuestion. Whether the pipeline needed a turn or not is invisible to them. The loop is small, engineered, and lives entirely inside the question brick’s edge with the user.

That placement is also why the small loop looks like an agent turn but is not one:

The distinction matters because the industry framing of loop engineering often assumes the multi-turn agentic case. On the question side of a single-document pipeline, the loop that pays off is the smallest one that exists.

5. Out of scope

Three things this article deliberately does not cover.

Each of these is a different loop, at a different scope, for a different receiver, using the same vocabulary at a different scale.

6. Sources and further reading

The loop-engineering framing arrived in 2026 from several sources at once, converging on the same term after context engineering had settled the vocabulary.

The primary industry anchors:

The foundation the loops build on:

  • Anthropic, “Building Effective AI Agents”: research post. The base patterns (evaluator-optimizer, reflection) and the temperance rule (add loops only when they demonstrably improve outcomes).
  • Yao et al., “ReAct: Synergizing Reasoning and Acting in Language Models”: arXiv 2210.03629. The founding reason-act-observe loop.
  • Madaan et al., “Self-Refine: Iterative Refinement with Self-Feedback”: arXiv 2303.17651. Generate → self-feedback → refine.

The series articles that code the pieces this one frames:

  • When RAG users ask vague questions (Article 6bis). The clarification loop’s mechanics: ClarificationRequest, ClarificationDefault, the ask-then-learn cache.
  • Context Engineering for RAG (Article 7bis). The pipeline-wide context-engineering framing this article extends into loop-engineering.

Similar Posts

Leave a Reply