Back openDesk Edu for a sovereign, open-source education β every vote counts.
Vote nowComplex questions over large document collections require assembling evidence across sections and documents. GraphRAG offers structured retrieval, but typically uses fixed traversal. Agentic RAG operates over weakly structured interfaces. Both miss the key insight: agents should navigate document structure like humans do.
DocNavRAG organises document hierarchies and cross-region relations into a navigable graph, exposes graph operations for locating, navigating, expanding, and fetching, and maintains an evolving evidence state to guide retrieval until sufficient evidence is collected.
The result: +7.8% answer quality and +17.7% context sufficiency over the strongest baseline across four long-document QA benchmarks.
When you answer a complex question from a large document collection, you don't start from scratch each time. You:
Each step builds on the previous. You maintain a mental state of what you've found, what's missing, and where to look next.
Current RAG systems don't do this. They:
DocNavRAG changes this by making navigation first-class and evidence stateful.
Documents are organised into a hierarchical graph:
βββββββββββββββββββ
β Collection β
ββββββββββ¬βββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
βΌ βΌ βΌ
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Document A β β Document B β β Document C β
ββββββββββ¬ββββββ ββββββββββ¬ββββββ ββββββββββ¬ββββββ
β β β
ββββββββββ΄ββββββ ββββββββββ΄ββββββ ββββββββββ΄ββββββ
β Section 1 β β Section 1 β β Section 1 β
β Section 2 β β Section 2 β β Section 2 β
β Section 3 β β Section 3 β β Section 3 β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β β
ββββββββββ¬βββββββββ
βΌ
ββββββββββββββββββββ
β Cross-reference β
β (citations, refs)β
ββββββββββββββββββββ
Node types:
Edge types:
DocNavRAG exposes five navigation operations:
class DocNavGraph:
def locate(self, query) -> List[Node]:
"""Find starting nodes matching query"""
...
def navigate(self, node: Node, direction: str) -> List[Node]:
"""Move to adjacent nodes (parent, child, sibling, reference)"""
...
def expand(self, node: Node, depth: int) -> List[Node]:
"""Expand subtree to specified depth"""
...
def fetch(self, nodes: List[Node]) -> str:
"""Retrieve content from nodes"""
...
def filter(self, nodes: List[Node], criteria: dict) -> List[Node]:
"""Filter nodes by metadata (date, author, etc.)"""
...
These operations are composable. Agents chain them to build navigation paths.
The evidence state tracks:
class EvidenceState:
def __init__(self, confidence_threshold=0.85, max_nodes=50):
self.evidence = [] # (node, content, relevance)
self.gaps = [] # (query_aspect, missing_info)
self.visited = set() # Node IDs
self.confidence = 0.0
self.last_node = None # Track current position for navigation
self.confidence_threshold = confidence_threshold
self.max_nodes = max_nodes
def add_evidence(self, node, content, relevance):
self.evidence.append((node, content, relevance))
self.visited.add(node.id)
self.last_node = node
self.confidence = self._recalculate_confidence()
def update_gaps(self, query, current_evidence):
missing = self._identify_gaps(query, current_evidence)
self.gaps = missing
def should_stop(self) -> bool:
return self.confidence > self.confidence_threshold or len(self.visited) > self.max_nodes
def _recalculate_confidence(self):
"""Recompute confidence from evidence relevance scores."""
...
def _identify_gaps(self, query, current_evidence):
"""Identify what query aspects are not yet covered by evidence."""
...
DocNavRAG operates in a retrieve-evaluate-expand loop:
βββββββββββββββββββ
β Initial Query β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Locate β
β (find seeds) β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Fetch β
β (get content) β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
βββββββ Evaluate βββββββ
β β (is evidence β β
β β sufficient?) β β
β ββββββββββ¬βββββββββ β
β β β
β YES β NO β
β βΌ β
β βββββββββββββββββββ β
β β Generate β β
β β (final answer) β β
β βββββββββββββββββββ β
β β
β βΌ β
β βββββββββββββββββββ β
βββββββ Expand βββββββ
β (navigate to β
β related nodes)β
βββββββββββββββββββ
Each iteration:
DocNavRAG was evaluated on four long-document QA benchmarks:
| Benchmark | Document Length | Baseline (GraphRAG) | DocNavRAG | Improvement |
|---|---|---|---|---|
| Qasper | 5K-20K words | 0.54 F1 | 0.62 F1 | +14.8% |
| MultiDoc2000 | 10-50 docs | 0.61 EM | 0.68 EM | +11.5% |
| LongBench-Doc | 10K-100K words | 0.49 EM | 0.57 EM | +16.3% |
| DocVQA | Multi-page docs | 0.72 EM | 0.78 EM | +8.3% |
Aggregate results:
Key finding: Performance gains are largest for multi-hop questions that require cross-document reasoning.
Document parsing:
Indexing strategy:
def build_doc_graph(documents):
graph = DocNavGraph()
for doc in documents:
doc_node = graph.add_document(doc.metadata)
for section in doc.sections:
section_node = graph.add_section(doc_node, section)
for paragraph in section.paragraphs:
graph.add_paragraph(section_node, paragraph)
# Add cross-references
for ref in doc.references:
graph.add_reference_edge(doc_node, ref.target_doc)
# Add semantic links
embeddings = embed_all_paragraphs(graph)
for i, j in find_similar_pairs(embeddings):
graph.add_related_edge(i, j, similarity=embeddings[i].similarity(embeddings[j]))
return graph
Common navigation patterns:
# Pattern 1: Deep dive into a topic
nodes = graph.locate("machine learning applications")
nodes = graph.expand(nodes[0], depth=2) # Get full section subtree
content = graph.fetch(nodes)
# Pattern 2: Cross-document comparison
doc_a = graph.locate("author: Smith")
doc_b = graph.locate("author: Jones")
sections_a = graph.navigate(doc_a, "children")
sections_b = graph.navigate(doc_b, "children")
comparison = graph.fetch(sections_a + sections_b)
# Pattern 3: Citation tracing
start = graph.locate("claim: deep learning revolution")
citations = graph.navigate(start, "references")
evidence = graph.fetch(citations)
DocNavRAG trades more navigation steps for better context quality:
For 10K documents: ~15 MB graph, 100ms navigation latency
Here's the minimal architecture:
class DocNavRAG:
def __init__(self, doc_graph, llm, retriever):
self.graph = doc_graph
self.llm = llm
self.retriever = retriever
def answer(self, query):
state = EvidenceState()
while not state.should_stop():
# Step 1: Locate relevant nodes
seeds = self.graph.locate(query)
# Step 2: Navigate based on gaps
if state.gaps:
for gap in state.gaps:
related = self.graph.navigate(state.last_node, "related")
seeds.extend(related)
# Step 3: Fetch content
nodes = [n for n in seeds if n.id not in state.visited]
content = self.graph.fetch(nodes)
# Step 4: Update state
for node, text in zip(nodes, content):
relevance = self._score_relevance(query, text)
state.add_evidence(node, text, relevance)
state.update_gaps(query, state.evidence)
# Step 5: Generate answer
return self.llm.generate(query, state.evidence)
def _score_relevance(self, query, text):
"""Score relevance of text to query (0.0β1.0)."""
...
DocNavRAG reveals three trends:
Document structure matters. Systems that preserve and exploit hierarchy outperform flat retrieval.
One-shot retrieval cannot handle complex questions. Iterative evidence collection with state tracking is the future.
Documents should expose navigation primitives (locate, navigate, expand, fetch) rather than just text. This is the LSP moment for document QA.
DocNavRAG solves the long-document QA problem by:
For document QA systems, the implication is clear: navigation beats retrieval. Structured exploration outperforms one-shot fetch.