{
  "source_file": "0930",
  "source_platform": "Dev.to",
  "author": "Tae Kim",
  "title_listed": "Query-Time Entity Disambiguation in Graph RAG: When One Name Means Seventeen Nodes",
  "keywords": "",
  "domain": "大模型算法",
  "content_type": "深度技术解析",
  "is_paid_flag": "否",
  "url": "https://dev.to/hannune/query-time-entity-disambiguation-in-graph-rag-when-one-name-means-seventeen-nodes-4kfg",
  "publish_date": "2026-07-26",
  "corpus_stage": "",
  "notes": "",
  "id": "917c569c6f1c",
  "collected_at": "2026-10-02T14:04:02",
  "title": "Query-Time Entity Disambiguation in Graph RAG: When One Name Means Seventeen Nodes - DEV Community",
  "body_markdown": "The hardest retrieval problem in Graph RAG is not missing data. It is the query that arrives with an ambiguous entity name.\nIn\n2asy.ai\n, 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.\nThe Setup: Where Disambiguation Fits in the Pipeline\nIn a standard Graph RAG pipeline, the flow is:\nParse the natural language query\nExtract entity mentions\nDisambiguate each mention to a specific graph node\n← the problem\nTraverse the graph from that node\nFeed retrieved subgraph into the generation step\nStep 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.\nfrom\ndataclasses\nimport\ndataclass\nfrom\ndatetime\nimport\ndatetime\n@dataclass\nclass\nDisambiguationResult\n:\nnode_id\n:\nstr\ndisplay_name\n:\nstr\nconfidence\n:\nfloat\nsignal_breakdown\n:\ndict\n[\nstr\n,\nfloat\n]\ndef\ndisambiguate\n(\nmention\n:\nstr\n,\ncandidates\n:\nlist\n[\ndict\n],\ncontext_terms\n:\nlist\n[\nstr\n],\nquery_time\n:\ndatetime\n,\n)\n->\nDisambiguationResult\n:\nscores\n=\n{}\nfor\ncandidate\nin\ncandidates\n:\nfreq\n=\nfrequency_prior_score\n(\nmention\n,\ncandidate\n)\nctx\n=\ncontext_coherence_score\n(\ncandidate\n,\ncontext_terms\n)\ntemporal\n=\ntemporal_activity_score\n(\ncandidate\n,\nquery_time\n)\nscores\n[\ncandidate\n[\n\"\nnode_id\n\"\n]]\n=\n{\n\"\nfrequency\n\"\n:\nfreq\n,\n\"\ncontext\n\"\n:\nctx\n,\n\"\ntemporal\n\"\n:\ntemporal\n,\n\"\ncombined\n\"\n:\nfreq\n*\n0.35\n+\nctx\n*\n0.45\n+\ntemporal\n*\n0.20\n,\n}\nbest\n=\nmax\n(\nscores\n,\nkey\n=\nlambda\nnid\n:\nscores\n[\nnid\n][\n\"\ncombined\n\"\n])\nreturn\nDisambiguationResult\n(\nnode_id\n=\nbest\n,\ndisplay_name\n=\nnext\n(\nc\n[\n\"\nname\n\"\n]\nfor\nc\nin\ncandidates\nif\nc\n[\n\"\nnode_id\n\"\n]\n==\nbest\n),\nconfidence\n=\nscores\n[\nbest\n][\n\"\ncombined\n\"\n],\nsignal_breakdown\n=\nscores\n[\nbest\n],\n)\nEnter fullscreen mode\nExit fullscreen mode\nSignal 1: Corpus Frequency Prior\nWithout any query context, the entity that appears most often in the source corpus where the mention is unqualified is the right default.\nIn 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.\ndef\nfrequency_prior_score\n(\nmention\n:\nstr\n,\ncandidate\n:\ndict\n)\n->\nfloat\n:\n\"\"\"\nFraction of unqualified mentions of `mention` that refer to this candidate\n    in the training corpus. Stored as a per-mention-string dict on the node.\n\"\"\"\npriors\n=\ncandidate\n.\nget\n(\n\"\nmention_priors\n\"\n,\n{})\nreturn\npriors\n.\nget\n(\nmention\n.\nlower\n(),\ncandidate\n.\nget\n(\n\"\ndefault_frequency_prior\n\"\n,\n0.0\n))\nEnter fullscreen mode\nExit fullscreen mode\nThe 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.\nSignal 2: Query Context Coherence\nThe 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.\nThis is a co-occurrence statistic between the query terms and each candidate's entity description:\nimport\nmath\nfrom\ncollections\nimport\nCounter\ndef\ncontext_coherence_score\n(\ncandidate\n:\ndict\n,\ncontext_terms\n:\nlist\n[\nstr\n],\nsmoothing\n:\nfloat\n=\n1.0\n,\n)\n->\nfloat\n:\n\"\"\"\nPointwise mutual information between context_terms and candidate description tokens.\n    Higher score = more context terms co-occur with this entity in training data.\n\"\"\"\nif\nnot\ncontext_terms\n:\nreturn\n0.5\n# neutral when no context available\ndescription_tokens\n=\nset\n(\ncandidate\n.\nget\n(\n\"\ndescription_tokens\n\"\n,\n[]))\ncooccurrence_counts\n=\ncandidate\n.\nget\n(\n\"\ncooccurrence_counts\n\"\n,\n{})\ntotal_docs\n=\ncandidate\n.\nget\n(\n\"\ntotal_docs_in_corpus\n\"\n,\n1\n)\npmi_sum\n=\n0.0\nfor\nterm\nin\ncontext_terms\n:\np_term\n=\ncooccurrence_counts\n.\nget\n(\nterm\n,\n0\n)\n/\ntotal_docs\np_entity\n=\ncandidate\n.\nget\n(\n\"\ndoc_count\n\"\n,\n0\n)\n/\ntotal_docs\np_joint\n=\ncooccurrence_counts\n.\nget\n(\nf\n\"\njoint:\n{\nterm\n}\n\"\n,\n0\n)\n/\ntotal_docs\nif\np_joint\n>\n0\nand\np_term\n>\n0\nand\np_entity\n>\n0\n:\npmi\n=\nmath\n.\nlog2\n(\np_joint\n/\n(\np_term\n*\np_entity\n)\n+\nsmoothing\n)\npmi_sum\n+=\nmax\n(\n0.0\n,\npmi\n)\nreturn\nmin\n(\n1.0\n,\npmi_sum\n/\n(\nlen\n(\ncontext_terms\n)\n+\nsmoothing\n))\nEnter fullscreen mode\nExit fullscreen mode\nIn 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.\nSignal 3: Temporal Suppression\nA historical subsidiary with a\nvalid_to\ndate 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.\ndef\ntemporal_activity_score\n(\ncandidate\n:\ndict\n,\nquery_time\n:\ndatetime\n)\n->\nfloat\n:\n\"\"\"\nReturns 1.0 if the entity is active at query_time, 0.1 if inactive.\n    Entities without a valid_to are assumed ongoing.\n\"\"\"\nvalid_from\n=\ncandidate\n.\nget\n(\n\"\nvalid_from\n\"\n)\nvalid_to\n=\ncandidate\n.\nget\n(\n\"\nvalid_to\n\"\n)\nif\nvalid_to\nis\nnot\nNone\n:\nactive\n=\nvalid_from\n<=\nquery_time\n<=\nvalid_to\nelse\n:\nactive\n=\nvalid_from\n<=\nquery_time\nif\nvalid_from\nelse\nTrue\nreturn\n1.0\nif\nactive\nelse\n0.1\nEnter fullscreen mode\nExit fullscreen mode\nThe 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.\nThe Failure Mode: Silent Wrong Answers\nWhen 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.\nThe 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.\nThis 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.\nThe Fix: Make the Disambiguation Decision Visible\nThe disambiguation machinery is already doing the work of picking a candidate. The only change is surfacing that decision before the answer is rendered.\ndef\nrag_query\n(\nuser_query\n:\nstr\n,\ngraph\n,\nllm\n)\n->\nstr\n:\nmentions\n=\nextract_entity_mentions\n(\nuser_query\n)\nresolved\n=\n[]\nfor\nmention\nin\nmentions\n:\ncandidates\n=\ngraph\n.\nget_candidates\n(\nmention\n)\nif\nlen\n(\ncandidates\n)\n==\n1\n:\nresolved\n.\nappend\n(\ncandidates\n[\n0\n])\ncontinue\nresult\n=\ndisambiguate\n(\nmention\n=\nmention\n,\ncandidates\n=\ncandidates\n,\ncontext_terms\n=\nextract_context_terms\n(\nuser_query\n,\nexclude\n=\nmentions\n),\nquery_time\n=\ndatetime\n.\nutcnow\n(),\n)\nresolved\n.\nappend\n(\nresult\n)\n# Surface the decision before answering\nif\nresult\n.\nconfidence\n<\n0.85\n:\nprint\n(\nf\n\"\n[disambiguation]\n'\n{\nmention\n}\n'\n→\n{\nresult\n.\ndisplay_name\n}\n\"\nf\n\"\n(confidence:\n{\nresult\n.\nconfidence\n:\n.\n0\n%\n}\n,\n\"\nf\n\"\ntop alternative:\n{\nget_second_best\n(\ncandidates\n,\nresult\n.\nnode_id\n)\n}\n)\n\"\n)\nsubgraph\n=\ngraph\n.\ntraverse\n(\nstart_nodes\n=\n[\nr\n.\nnode_id\nfor\nr\nin\nresolved\n])\nreturn\nllm\n.\ngenerate\n(\ncontext\n=\nsubgraph\n,\nquery\n=\nuser_query\n)\nEnter fullscreen mode\nExit fullscreen mode\nShowing\n[disambiguation] 'Hyundai' → Hyundai Motor Company (confidence: 71%, top alternative: Hyundai E&C)\nbefore the answer converts a silent failure into something the user can catch and correct.\nThe 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.\nWhy This Matters More Than Retrieval Precision\nMost 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.\nThe 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.\nBuilding\n2asy.ai\n, a knowledge graph for Korean corporate intelligence. More at\nhannune.ai\n.\nCover: Y (@yi2026) on\nUnsplash",
  "metadata": {
    "title": "Query-Time Entity Disambiguation in Graph RAG: When One Name Means Seventeen Nodes - DEV Community",
    "description": "The hardest retrieval problem in Graph RAG is not missing data. It is the query that arrives with an... Tagged with graphrag, knowledgegraph, python, llm.",
    "keywords": "graphrag, knowledgegraph, python, llm, software, coding, development, engineering, inclusive, community",
    "author": null,
    "og:type": "article",
    "og:url": "https://dev.to/hannune/query-time-entity-disambiguation-in-graph-rag-when-one-name-means-seventeen-nodes-4kfg",
    "og:title": "Query-Time Entity Disambiguation in Graph RAG: When One Name Means Seventeen Nodes",
    "og:description": "The hardest retrieval problem in Graph RAG is not missing data. It is the query that arrives with an...",
    "og:site_name": "DEV Community",
    "og:image": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1779636437913-3a6e0ac98a40%3Fw%3D1200%26h%3D630%26fit%3Dcrop",
    "twitter:site": "@thepracticaldev",
    "twitter:creator": "@HannuneTech",
    "twitter:title": "Query-Time Entity Disambiguation in Graph RAG: When One Name Means Seventeen Nodes",
    "twitter:description": "The hardest retrieval problem in Graph RAG is not missing data. It is the query that arrives with an...",
    "twitter:card": "summary_large_image",
    "twitter:widgets:new-embed-design": "on",
    "twitter:image:src": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1779636437913-3a6e0ac98a40%3Fw%3D1200%26h%3D630%26fit%3Dcrop"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/917c569c6f1c/code/body-000.txt",
      "chars": 1092,
      "language": "text"
    },
    {
      "file": "attachments/917c569c6f1c/code/body-001.txt",
      "chars": 374,
      "language": "text"
    },
    {
      "file": "attachments/917c569c6f1c/code/body-002.txt",
      "chars": 1110,
      "language": "text"
    },
    {
      "file": "attachments/917c569c6f1c/code/body-003.txt",
      "chars": 499,
      "language": "text"
    },
    {
      "file": "attachments/917c569c6f1c/code/body-004.txt",
      "chars": 998,
      "language": "text"
    }
  ],
  "images": [],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://2asy.ai/",
      "label": "2asy.ai",
      "type": "a"
    },
    {
      "href": "https://hannune.ai/",
      "label": "hannune.ai",
      "type": "a"
    },
    {
      "href": "https://unsplash.com/",
      "label": "Unsplash",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:49:41Z",
  "source_url": "https://dev.to/hannune/query-time-entity-disambiguation-in-graph-rag-when-one-name-means-seventeen-nodes-4kfg",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 9148,
  "expected_body_images": 0,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/917c569c6f1c/pages/source.html",
  "local_html": "attachments/917c569c6f1c/pages/article.html",
  "body_base_path": "attachments/917c569c6f1c/pages/",
  "body_markdown_path": "attachments/917c569c6f1c/pages/article.md"
}