# How GraphRAG Works in ReportEngine for Knowledge Graph Construction

> Learn how GraphRAG in ReportEngine builds knowledge graphs from JSONs, stores them, and enables LLM queries for enriched reports. Discover cross-engine insights now.

- Repository: [BaiFu/bettafish](https://github.com/666ghj/bettafish)
- Tags: deep-dive
- Published: 2026-02-23

---

**GraphRAG in ReportEngine constructs a structured knowledge graph from engine state JSONs, persists it to [`graphrag.json`](https://github.com/666ghj/bettafish/blob/main/graphrag.json), and enables LLM-driven multi-round queries to enrich report chapters with cross-engine insights.**

The bettafish repository (666ghj/bettafish) implements GraphRAG as an optional module within its Report Engine, transforming parsed states from Insight, Media, and Query engines into a traversable knowledge structure. This system allows large language models to perform targeted retrieval across interconnected data sources before generating report content.

## Enabling GraphRAG in ReportEngine

GraphRAG functionality is controlled through environment variables defined in [`ReportEngine/utils/config.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/utils/config.py) (lines 70-76). The system checks `GRAPHRAG_ENABLED` (defaulting to `false`) and `GRAPHRAG_MAX_QUERIES` to determine whether to initialize the graph construction pipeline and how many query rounds to execute per chapter.

When enabled, the `ReportAgent` instantiates the graph builder and executes the `GraphRAGQueryNode` during chapter generation. Configure these settings before initializing the engine:

```python

# ReportEngine/utils/config.py

settings = Settings()
settings.GRAPHRAG_ENABLED = True          # Activate the module

settings.GRAPHRAG_MAX_QUERIES = 3         # Limit to 3 retrieval rounds per chapter

```

## Constructing the Knowledge Graph

The `GraphBuilder` class in [`ReportEngine/graphrag/graph_builder.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/graphrag/graph_builder.py) (lines 40-70) orchestrates graph construction by consuming the report topic and a dictionary of parsed engine states (`ParsedState`). It generates a heterogeneous graph representing the relationships between analytical components.

### Node and Edge Architecture

The builder creates five distinct node types:

- **topic**: The root node representing the user's original query
- **engine**: Nodes for insight, media, query, and optional host engines
- **section**: Child nodes under each engine representing content sections
- **search_query**: Deduped per section, storing executed search strings
- **source**: URL-hashed nodes representing discovered references

Edges encode four semantic relationships: `analyzed_by`, `contains`, `searched`, and `found`. When forum logs are present, the builder additionally creates a host engine node with summary sections.

```python
from ReportEngine.graphrag.graph_builder import GraphBuilder

# engine_states contains ParsedState objects from each engine

graph = GraphBuilder().build(
    topic="疫情舆情分析",
    states=engine_states,
    forum_entries=forum_logs  # Optional

)

```

## Persisting the Graph to Disk

Once constructed, the `Graph` object passes to `GraphStorage.save()` in [`ReportEngine/graphrag/graph_storage.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/graphrag/graph_storage.py) (lines 40-65). This serializes the entire graph structure—including node metadata, edge relationships, and identifiers—to a portable JSON file named [`graphrag.json`](https://github.com/666ghj/bettafish/blob/main/graphrag.json) stored adjacent to the chapter output directory.

The persistence layer stores `task_id` and `report_id` within the JSON structure, enabling later retrieval and inspection via the optional Flask API.

```python
from pathlib import Path
from ReportEngine.graphrag.graph_storage import GraphStorage

graph_path = GraphStorage().save(
    graph=graph,
    task_id="run_20240223",
    run_dir=Path("final_reports/chapters/run_20240223")
)

```

## Querying the Knowledge Graph

The `QueryEngine` class in [`ReportEngine/graphrag/query_engine.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/graphrag/query_engine.py) (lines 15-33) provides the retrieval interface for the LLM. It operates on the in-memory graph structure without requiring external graph databases.

### Retrieval Capabilities

The query engine supports four filtering mechanisms:

- **Keyword matching**: Searches node attributes including `name`, `title`, `query_text`, and `summary`
- **Node-type filtering**: Restricts results to specific types (`section`, `search_query`, `source`)
- **Engine filtering**: Limits scope to specific engines (`insight`, `media`, `query`, `host`)
- **Depth expansion**: After initial keyword matches, traverses neighbors up to `depth` hops to capture contextual relationships

To prevent token overflow in downstream prompts, the engine applies hard caps: `max_sections`, `max_queries`, and `max_sources`.

## LLM-Driven Multi-Round Retrieval

The `GraphRAGQueryNode` in [`ReportEngine/nodes/graphrag_query_node.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/nodes/graphrag_query_node.py) (lines 95-140) orchestrates the retrieval workflow, executing up to `GRAPHRAG_MAX_QUERIES` rounds (default 3) per chapter.

### Decision-Making Architecture

Each round begins by constructing a decision prompt using templates from [`ReportEngine/graphrag/prompts.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/graphrag/prompts.py) (lines 9-62): `GRAPHRAG_QUERY_DECISION_SYSTEM` and `GRAPHRAG_QUERY_DECISION_USER`. This prompt includes:

- Chapter metadata and graph statistics from `query_engine.get_node_summary()`
- Section titles per engine
- Sample search queries
- Full query history from previous rounds

The LLM must return a JSON object specifying `should_query`, `keywords`, `node_types`, `engine_filter`, `depth`, and `reasoning` fields.

### History-Aware Iteration

The node maintains a `QueryHistory` object that feeds prior rounds back into subsequent prompts (lines 70-85), preventing redundant queries while encouraging novel retrieval angles. The execution loop follows this sequence:

1. Build decision prompt with current context
2. Parse LLM response into `QueryParams`
3. Execute `QueryEngine.query()` with specified parameters
4. Log results via `knowledge_logger`
5. Append round details to `QueryHistory`

```python
from ReportEngine.nodes.graphrag_query_node import GraphRAGQueryNode
from ReportEngine.llms.base import LLMClient

llm = LLMClient()
graph_query_node = GraphRAGQueryNode(llm)

results = graph_query_node.run(
    section=current_chapter,
    context=report_context,
    graph=graph,
    max_queries=3
)

```

## Merging Results and Generating Insights

After completing all query rounds, the `_merge_results()` method (lines 290-340 of [`graphrag_query_node.py`](https://github.com/666ghj/bettafish/blob/main/graphrag_query_node.py)) deduplicates sections, queries, and sources across the entire retrieval history. It calculates `total_nodes` retrieved and generates **cross-engine insights**—metacognitive annotations indicating when multiple engines contributed to the context or when searches span diverse analytical domains.

## Injecting Graph Context into Chapter Prompts

The merged results flow through `format_graph_results_for_prompt()` defined in [`ReportEngine/graphrag/prompts.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/graphrag/prompts.py) (lines 72-88), which applies the `USER_PROMPT_GRAPH_RESULTS_TEMPLATE` to structure the retrieval data.

The `ChapterGenerationNode` prepends two components to the final LLM prompt:

1. `SYSTEM_PROMPT_CHAPTER_GRAPH_ENHANCEMENT`: Instructions for incorporating graph-derived context
2. The formatted graph results containing deduplicated sources, queries, and cross-engine insights

This injection enables the chapter generation model to reference specific sources discovered by the Query engine while maintaining awareness of analytical sections generated by the Insight engine.

```python
from ReportEngine.graphrag.prompts import format_graph_results_for_prompt

enhanced_prompt = format_graph_results_for_prompt(results)

# System prompt SYSTEM_PROMPT_CHAPTER_GRAPH_ENHANCEMENT is prepended automatically

```

## End-to-End Orchestration

The `ReportAgent` class in [`ReportEngine/agent.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/agent.py) (lines 600-710) coordinates the complete GraphRAG pipeline:

1. Loads input files and parsed engine states
2. Builds the knowledge graph if `GRAPHRAG_ENABLED` is `True`
3. Executes `GraphRAGQueryNode` for each chapter section
4. Injects `graph_enhancement_prompt` into the generation context
5. Produces final chapter text with cross-engine citations

The persisted [`graphrag.json`](https://github.com/666ghj/bettafish/blob/main/graphrag.json) remains available for post-hoc analysis or external API consumption after report generation completes.

## Summary

GraphRAG in ReportEngine implements a complete retrieval-augmented generation pipeline through these key mechanisms:

- **Pure-Python graph model**: Uses lightweight dataclasses (`Node`, `Edge`, `Graph`) requiring no external graph database
- **Deterministic persistence**: Serializes complete graph state to [`graphrag.json`](https://github.com/666ghj/bettafish/blob/main/graphrag.json) via `GraphStorage`
- **LLM-driven query planning**: Delegates semantic judgment to the language model while keeping graph traversal lightweight
- **History-aware retrieval**: Prevents redundant queries through `QueryHistory` tracking across multiple rounds
- **Token-safe limits**: Enforces caps on returned nodes via `QueryEngine` to protect downstream prompts
- **Cross-engine integration**: Merges insights from Insight, Media, Query, and Forum engines into unified chapter context

## Frequently Asked Questions

### What file format does ReportEngine use to store the GraphRAG knowledge graph?

ReportEngine persists the knowledge graph as a JSON file named [`graphrag.json`](https://github.com/666ghj/bettafish/blob/main/graphrag.json) using the `GraphStorage.save()` method in [`ReportEngine/graphrag/graph_storage.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/graphrag/graph_storage.py). This file contains serialized node metadata, edge relationships, and identifiers including `task_id` and `report_id`, stored adjacent to the chapter output directory for portability and later inspection.

### How does the GraphRAGQueryNode prevent redundant queries across multiple rounds?

The `GraphRAGQueryNode` maintains a `QueryHistory` object that records all previous retrieval rounds. According to lines 70-85 of [`ReportEngine/nodes/graphrag_query_node.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/nodes/graphrag_query_node.py), this history feeds back into subsequent decision prompts, allowing the LLM to recognize previously queried keywords and node types. This design encourages novel retrieval angles while avoiding duplicate graph traversals within the same chapter generation cycle.

### What types of nodes are created during GraphRAG knowledge graph construction?

The `GraphBuilder` in [`ReportEngine/graphrag/graph_builder.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/graphrag/graph_builder.py) creates five node types: **topic** (root), **engine** (insight, media, query, host), **section** (content divisions), **search_query** (deduped search strings per section), and **source** (URL-hashed references). Edges encode relationships including `analyzed_by`, `contains`, `searched`, and `found` to link these entities semantically.

### How does the QueryEngine protect against token overflow when retrieving graph data?

The `QueryEngine` in [`ReportEngine/graphrag/query_engine.py`](https://github.com/666ghj/bettafish/blob/main/ReportEngine/graphrag/query_engine.py) applies explicit caps through parameters including `max_sections`, `max_queries`, and `max_sources`. These limits constrain the total nodes returned per type during keyword matching, depth expansion, and filtering operations, ensuring that formatted graph results remain within token budgets for downstream LLM prompts.