Query-Time Entity Disambiguation in Graph RAG: When One Name Means Seventeen Nodes A developer at 2asy.ai built a query-time entity disambiguation system for Graph RAG that resolves ambiguous entity names like 'Hyundai'—which matches seventeen separate nodes in the company's Korean corporate knowledge graph—by combining a frequency prior, context coherence, and temporal activity signals. The system uses a weighted scoring model (35% frequency, 45% context, 20% temporal) to select the correct node, preventing the pipeline from generating coherent-sounding answers about the wrong company. The hardest retrieval problem in Graph RAG is not missing data. It is the query that arrives with an ambiguous entity name. In 2asy.ai https://2asy.ai , a knowledge graph built on Korean corporate data, "Hyundai" matches seventeen separate nodes. A vector search returns all of them ranked by embedding similarity. A graph traversal needs exactly one starting node. The gap between these two realities is where Graph RAG quietly breaks. In a standard Graph RAG pipeline, the flow is: Step 3 is where "Hyundai" needs to resolve to either Hyundai Motor Company, Hyundai Engineering & Construction, Hyundai Steel, Hyundai Merchant Marine, or one of the other thirteen. The wrong choice produces a coherent-sounding answer about the wrong company. python from dataclasses import dataclass from datetime import datetime @dataclass class DisambiguationResult: node id: str display name: str confidence: float signal breakdown: dict str, float def disambiguate mention: str, candidates: list dict , context terms: list str , query time: datetime, - DisambiguationResult: scores = {} for candidate in candidates: freq = frequency prior score mention, candidate ctx = context coherence score candidate, context terms temporal = temporal activity score candidate, query time scores candidate "node id" = { "frequency": freq, "context": ctx, "temporal": temporal, "combined": freq 0.35 + ctx 0.45 + temporal 0.20, } best = max scores, key=lambda nid: scores nid "combined" return DisambiguationResult node id=best, display name=next c "name" for c in candidates if c "node id" == best , confidence=scores best "combined" , signal breakdown=scores best , Without any query context, the entity that appears most often in the source corpus where the mention is unqualified is the right default. In the Korean financial news corpus underlying 2asy.ai, "Hyundai" without a qualifier co-occurs with Hyundai Motor documents about two-thirds of the time. That prior is learnable from the corpus and beats a random guess at the disambiguation step even before any query-specific signals are considered. php def frequency prior score mention: str, candidate: dict - float: """ Fraction of unqualified mentions of mention that refer to this candidate in the training corpus. Stored as a per-mention-string dict on the node. """ priors = candidate.get "mention priors", {} return priors.get mention.lower , candidate.get "default frequency prior", 0.0 The prior is stored on each node at graph-build time. It is not recomputed at query time — that would be slow. What gets stored is the fraction of unambiguous co-occurrences from the corpus. A candidate with a prior of 0.65 means: historically, this name referred to this entity 65% of the time it appeared without additional qualifiers. The mention rarely arrives in isolation. If the same query mentions "battery technology" or "manufacturing capacity," that co-occurrence signal shifts probability mass toward Hyundai Motor. If the query mentions "construction permits" or "project bids," Hyundai Engineering moves up. This is a co-occurrence statistic between the query terms and each candidate's entity description: python import math from collections import Counter def context coherence score candidate: dict, context terms: list str , smoothing: float = 1.0, - float: """ Pointwise mutual information between context terms and candidate description tokens. Higher score = more context terms co-occur with this entity in training data. """ if not context terms: return 0.5 neutral when no context available description tokens = set candidate.get "description tokens", cooccurrence counts = candidate.get "cooccurrence counts", {} total docs = candidate.get "total docs in corpus", 1 pmi sum = 0.0 for term in context terms: p term = cooccurrence counts.get term, 0 / total docs p entity = candidate.get "doc count", 0 / total docs p joint = cooccurrence counts.get f"joint:{term}", 0 / total docs if p joint 0 and p term 0 and p entity 0: pmi = math.log2 p joint / p term p entity + smoothing pmi sum += max 0.0, pmi return min 1.0, pmi sum / len context terms + smoothing In practice, storing full co-occurrence counts per candidate is expensive. We store the top-500 co-occurring terms per entity node, which covers the vast majority of real query patterns. A historical subsidiary with a valid to date should rank below currently-active entities for queries that do not specify a historical time window. The graph already has the structure to express this — active vs. inactive entity status — but the query layer has to use it. php def temporal activity score candidate: dict, query time: datetime - float: """ Returns 1.0 if the entity is active at query time, 0.1 if inactive. Entities without a valid to are assumed ongoing. """ valid from = candidate.get "valid from" valid to = candidate.get "valid to" if valid to is not None: active = valid from <= query time <= valid to else: active = valid from <= query time if valid from else True return 1.0 if active else 0.1 The weight of 0.1 not zero lets historical entities still be retrieved if the other signals are strong enough — useful when a query explicitly references a past entity. But for the common case of a present-tense query, a deactivated entity is strongly suppressed. When disambiguation selects the wrong candidate, the failure is invisible. The graph traversal returns a coherent subgraph, the generation step produces a confident-sounding answer, and nothing in the output signals that retrieval started from the wrong node. The user who asked about "Hyundai's battery supply chain" gets a detailed answer about Hyundai Engineering's infrastructure projects instead. The answer is internally consistent. It is just about the wrong company. This is worse than a retrieval miss. A retrieval miss produces an "I don't have information about that" response that the user recognizes as incomplete. A disambiguation error produces a fluent, confident, wrong answer. The disambiguation machinery is already doing the work of picking a candidate. The only change is surfacing that decision before the answer is rendered. php def rag query user query: str, graph, llm - str: mentions = extract entity mentions user query resolved = for mention in mentions: candidates = graph.get candidates mention if len candidates == 1: resolved.append candidates 0 continue result = disambiguate mention=mention, candidates=candidates, context terms=extract context terms user query, exclude=mentions , query time=datetime.utcnow , resolved.append result Surface the decision before answering if result.confidence < 0.85: print f" disambiguation '{mention}' → {result.display name} " f" confidence: {result.confidence:.0%}, " f"top alternative: {get second best candidates, result.node id } " subgraph = graph.traverse start nodes= r.node id for r in resolved return llm.generate context=subgraph, query=user query Showing disambiguation 'Hyundai' → Hyundai Motor Company confidence: 71%, top alternative: Hyundai E&C before the answer converts a silent failure into something the user can catch and correct. The threshold of 0.85 matters. Above that, the disambiguation is confident enough that surfacing it would be noise. Below it, the user should know the system made a choice. Most Graph RAG evaluation focuses on retrieval recall — whether the right documents or subgraph edges are found. Disambiguation happens before retrieval. An error there means retrieval runs correctly on the wrong entity's neighborhood. The node count in a well-maintained corporate knowledge graph is in the hundreds of thousands. Unqualified company name mentions resolve to the right entity most of the time — until they do not, and then the failure is invisible. Treating disambiguation as a first-class step, with explicit confidence scoring and user-visible decisions, is what keeps it from becoming a silent quality drain. Building 2asy.ai, a knowledge graph for Korean corporate intelligence. More at hannune.ai. Cover: Y @yi2026 on Unsplash