Skip to main content

ADR-0030: LLM Structured Entity Extraction Replaces Dictionary Gating

Date: 2026-02-13 | Status: Accepted | Supersedes: ADR-0014 (extends)

Context

The ZOL RAG system (Lewis et al., 2020) routed graph queries based on static dictionaries: CONDITION_ALIASES, SEARCH_ALIASES, DEPT_CONDITION_MAP, and keyword trigger lists.

Problems with Dictionary-Gated Routing

  1. Brittle vocabulary coverage: Every new colloquial term requires a code change. We went through 16 iterations of SEARCH_ALIASES additions. "hartkloppingen" was missing from DEPT_CONDITION_MAP despite being in CONDITION_ALIASES since v6.

  2. Monolingual: All dictionaries are Dutch-only. ZOL serves a multilingual population (Dutch, French, Turkish, German, Romanian, Greek).

  3. Keyword gating blocks valid queries: search_typed_nodes() requires specific Dutch keywords ("dokter", "arts", "behandeling") to decide which Cypher patterns to run. "diabetes" or "borstkanker" triggers no graph query.

  4. Duplicate key bugs: Python dict literals silently overwrite duplicate keys. reumatologie was defined 3 times in DEPT_CONDITION_MAP.

  5. LLM intelligence wasted: The intent classification LLM already understands medical semantics but only outputs {intent, rewritten_query}, losing its understanding.

Decision

Extend the existing intent classification LLM call to output structured medical entities alongside intent and rewritten query. Use these entities as primary input for graph query routing, with static dictionaries as cold fallback.

Related concept

This ADR records the decision to emit {intent, rewritten_query, entities} from a single LLM call. The rewritten_query field — canonical reformulation, the template set, and follow-up resolution — is explained in full on the Query Rewriting page.

New LLM Output

{
"intent": "condition_information",
"confidence": 0.95,
"rewritten_query": "Welke arts behandelt hartkloppingen bij ZOL?",
"entities": {
"condition": "Palpitaties",
"department": "Cardiologie",
"treatment": null,
"examination": null,
"doctor": null,
"campus": null,
"service": null
}
}

How It Works

  1. Prompt extension: The intent+rewrite prompt gains entity extraction instructions with a compact taxonomy reference list (~500 tokens).

  2. Graph routing uses entities directly: Instead of dictionary scan → Cypher, the LLM's structured entities feed directly into typed node queries.

  3. Fallback chain: When LLM returns null entities: first resolve_search_query() (dictionary-based), then _dispatch_by_keywords().

  4. No extra LLM call: Entity extraction added to the existing intent+rewrite prompt. Same API call, ~50 extra output tokens.

Data Flow

Before:

User query → LLM (intent + rewrite) → plain text
→ resolve_search_query() → scan 6 dictionaries → maybe find entity
→ keyword check → maybe run Cypher → results

After:

User query → LLM (intent + rewrite + entities) → structured JSON
→ entities.condition = "Palpitaties" → run Cypher directly → results
→ fallback: resolve_search_query() if LLM entities are null

Consequences

Positive

  • Language-independent: Any language the LLM understands produces correct entity resolution
  • Novel term handling: Slang, abbreviations, misspellings handled without dictionary entries
  • Eliminates keyword gating: Graph queries fire based on structured entities, not keyword matching
  • Zero marginal cost: Uses the existing LLM call
  • Eliminates dictionary drift: Canonical names come from the LLM, not alias chains
  • Structured output: No regex parsing of rewritten queries

Negative

  • LLM dependency: If LLM unavailable, graph routing degrades to dictionary fallback
  • Prompt size increase: ~500 tokens for taxonomy reference (negligible cost)
  • Entity hallucination risk: Mitigated by constrained reference list + validation