nano-graphrag 源码深度解析 v0.0.8.2 · gusye1234/nano-graphrag

核心 ~2900 行 3 种检索模式 可插拔存储
01

项目概览与设计哲学

nano-graphrag 是微软 GraphRAG 的极简重实现:约 1100 行核心代码(不含测试与 Prompt),保留了图索引 + 社区报告 + 多模式检索的全部核心能力。

🧬

小而完整

剔除官方实现的复杂基建(配置系统、流水线框架、CLI),只保留「文档→知识图谱→社区→检索」这条主干,每一行都可读可改。

🔌

全组件可插拔

LLM、Embedding、向量库、图存储、KV 存储、分块函数、实体抽取器全部通过 dataclass 字段注入,换组件只改一个参数。

⚡

异步优先

全链路 asyncio,LLM 并发抽取、嵌入批量并行,用自研轻量限流器(而非 Semaphore)避免 nest_asyncio 依赖。

💾

零外部服务依赖

默认组合 = JSON 文件 + NetworkX + nano-vectordb,pip install 后无需数据库即可跑通全部功能。

🔁

幂等增量写入

文档/分块均以 MD5 作为主键,重复插入自动跳过;LLM 响应落盘缓存,相同请求不再计费。

🎯

预算感知

所有上下文组装都以 token 预算为硬约束,逐项按重要性(度数、权重、评分)排序后截断,保证不超模型窗口。

😩 官方 Microsoft GraphRAG

  • 多层配置系统 + CLI + 流水线编排
  • 代码量数万行,学习曲线陡峭
  • 强绑定 Azure OpenAI 生态
  • 自定义/魔改成本高

😊 nano-graphrag

  • 单文件主流程,数据类即配置
  • 核心 ~1100 行,一晚上可通读
  • OpenAI / Azure / Bedrock / Ollama 均可
  • 改 Prompt、换存储只需动几行
它做了什么(与官方的差异) 完整实现了 local search 与 global search;省略了 covariates(声明/主张追踪,Prompt 仍保留在 prompt.py 但未接入管线);global search 的 map-reduce 实现也做了简化。node2vec 节点嵌入接口预留但默认不参与检索。
02

整体架构

整个系统围绕「三类存储 + 两条管线」构建:写入时把文本炼成知识图谱与社区报告,查询时从图与报告中组装上下文交给 LLM 作答。

分层视图
应用层GraphRAG.insert / query
▼
管线层 _op.pychunking · extract_entities
generate_community_report
local_query · global_query · naive_query
▼
存储抽象 base.pyBaseKVStorage
BaseVectorStorage
BaseGraphStorage
▼
默认实现JsonKVStorage
NanoVectorDBStorage
NetworkXStorage
可替换实现HNSWVectorStorage
Neo4jStorage · Faiss · Milvus …
数据视图 · 写入
原始文档
doc-md5
文本分块
chunk-md5
LLM 实体抽取
best_model
知识图谱
节点/边 + 描述合并
Leiden 聚类
层级社区
社区报告
JSON 报告 + 评分
数据视图 · 查询
Query
local / global / naive
向量召回
实体 or 分块
图扩展
边 · 一跳邻居 · 文本回溯
社区报告
局部引用 / 全局 map-reduce
LLM 合成回答
token 预算内 CSV 上下文

源码地图

文件行数职责
graphrag.py382主类 GraphRAG:配置、存储装配、insert/query 编排
base.py186存储抽象(KV / Vector / Graph)+ QueryParam + Schema 定义
_op.py1140核心算法层:分块、实体抽取、合并、社区报告、三种查询
_llm.py294OpenAI / Azure / Bedrock 客户端、重试、LLM 缓存、Embedding
_utils.py305Tokenizer 包装、token 截断、MD5 主键、JSON 容错解析、并发限流
prompt.py520全部 LLM Prompt(移植自微软 GraphRAG)+ 分隔符常量
_splitter.py94基于分隔符 token 的切分器(langchain 风格)
_storage/~10005 种存储后端实现(JSON KV / nano-vectordb / hnswlib / networkx / neo4j)
entity_extraction/~500DSPy 类型化抽取器扩展(可选,带自反思精炼)
03

主入口:GraphRAG 类

graphrag.py:52 — 一个 @dataclass 承担了「配置中心 + 依赖容器 + 流程编排」三重角色,__post_init__ 完成全部装配。

配置分组一览

分组关键字段说明
工作目录working_dir所有持久化产物的落盘目录,默认带时间戳自动创建
模式开关enable_local / enable_naive_rag决定是否创建 entities_vdb / chunks_vdb;global 恒可用
分块chunk_func / chunk_token_size=1200 / chunk_overlap=100分块函数本身也是可注入字段
抽取entity_extract_max_gleaning=1 / entity_summary_to_max_tokens=500gleaning 轮数与描述摘要阈值
聚类graph_cluster_algorithm="leiden" / max_graph_cluster_size=10Leiden 层级聚类参数
双模型best_model_func / cheap_model_func + 各自 max_token / max_async贵模型做抽取与报告;便宜模型做描述摘要——成本控制的关键设计
嵌入embedding_func / embedding_batch_num=32 / query_better_than_threshold=0.2封装为 EmbeddingFunc(自带维度/上限元数据)
存储*_storage_cls 三个类字段KV / Vector / Graph 后端类,构造时传入 namespace + global_config
LLM 缓存enable_llm_cache=TrueMD5(model, messages) 为键的响应缓存

__post_init__ 装配流程

构造 TokenizerWrapper

按 tokenizer_type 选择 tiktoken(gpt-4o)或 HuggingFace tokenizer,统一 encode/decode/decode_batch 接口。

供应商适配

using_azure_openai / using_amazon_bedrock 为真时,仅当用户未自定义时才替换默认的 model_func / embedding_func(尊重用户注入)。

实例化存储

用 asdict(self) 把整个配置快照传给每个存储的 global_config——存储实例反向读取配置的约定由此建立。依次创建 full_docs、text_chunks、llm_response_cache、community_reports(KV)与 chunk_entity_relation_graph(Graph)。

条件创建向量库

enable_local → entities_vdb(meta_fields 只存 entity_name);enable_naive_rag → chunks_vdb。

函数包装

embedding / best_model / cheap_model 全部套上 limit_async_func_call(max_async) 限流;两个 model_func 再 partial 注入 hashing_kv=llm_response_cache 完成缓存接线。

同步/异步双接口 insert() 与 query() 是同步壳:通过 always_get_an_event_loop() 取得(必要时新建)事件循环,再 run_until_complete 调用 ainsert/aquery。这让库既能嵌入 asyncio 应用,也能在普通脚本里直接用。
04

存储抽象层 base.py

整个项目的可扩展性支点:三个抽象基类 + 一套生命周期回调,任何后端只要实现这几个 async 方法即可接入。

4.1 三类存储接口

KV BaseKVStorage[T]

get_by_id / get_by_ids / filter_keys / upsert / drop
承载:原始文档、文本分块、LLM 缓存、社区报告。语义即「带命名空间的字典 + 落盘」。

Vector BaseVectorStorage

upsert(data) 用 value 的 content 字段做嵌入;query(query, top_k)。字段 embedding_func 与 meta_fields 由基类 dataclass 直接提供。

Graph BaseGraphStorage

单/批节点边的 CRUD + 度数查询 + clustering() + community_schema()。批量方法是性能关键:查询期大量并发 gather。

生命周期回调

所有存储继承 StorageNameSpace,带三个钩子:index_start_callback(插入前,Neo4j 用它建索引)、index_done_callback(插入后统一 commit:JSON 落盘、graphml 写出、向量库存盘)、query_done_callback。GraphRAG 用 asyncio.gather 并发驱动所有存储的回调——这就是「内存中改,结束时一次性写盘」的批式持久化模型。

4.2 默认实现与产物文件

类后端落盘文件要点
JsonKVStoragePython dict → JSONkv_store_<ns>.json全量内存字典,done 时整体 dump;简单可靠
NanoVectorDBStoragenano-vectordb(作者同系)vdb_<ns>.jsonupsert 前按 embedding_batch_num 分批并行嵌入;查询带余弦阈值过滤
NetworkXStoragenetworkxgraph_<ns>.graphml默认图后端;内置 Leiden 聚类(graspologic)、稳定最大连通分量预处理
HNSWVectorStoragehnswlib<ns>_hnsw.index + pkl 元数据xxh32 哈希转 uint32 id;参数可经 vector_db_storage_cls_kwargs 注入
Neo4jStorageNeo4j async driver—用 working_dir 哈希出 label 隔离多项目;GDS Leiden;批量 Cypher(UNWIND)优化
GraphML 的一个巧思 NetworkXStorage 把聚类结果(节点属于哪些 level 的哪个 cluster)以 JSON 字符串塞回节点属性 clusters,随 graphml 一起持久化——图与社区归属天然一体,community_schema() 读取时重建「社区 → 节点/边/chunk_ids/occurrence/sub_communities」的完整视图。
05

写入管线(Insert)

graphrag.py:277 ainsert — 七步流水线,每步幂等、可重入。

文档去重

每篇文档 compute_mdhash_id(content, "doc-") 作主键;filter_keys 剔除已存在文档,全部重复则直接返回。

分块

get_chunks() 先整篇 encode 成 token,再交给 chunk_func 切分;每个分块以内容 MD5(chunk- 前缀)为主键,天然去重。若开启 naive_rag,此时先写入 chunks_vdb。

丢弃旧社区报告

community_reports.drop()——当前不支持社区的增量更新,每次插入都重建全部报告(这是已知的成本权衡点,代码留有 TODO)。

实体关系抽取

extract_entities():LLM 并发抽取每个 chunk → 解析记录 → 按实体名/边对聚合 → 合并进图 → 实体写入向量库。

社区聚类

chunk_entity_relation_graph.clustering("leiden") 在稳定最大连通分量上跑层级 Leiden,把 (level, cluster) 写回每个节点。

生成社区报告

generate_community_report() 按层级从细到粗,为每个社区打包实体/边/子报告 → LLM 产出结构化 JSON 报告。

提交

最后才 upsert full_docs 与 text_chunks——若中途失败,文档不会被标记为已处理,下次 insert 可重试。finally 中所有存储 index_done_callback 统一落盘。

5.1 文本分块:两种策略

chunking_by_token_size(默认):滑动窗口硬切,步长 = max_token_size - overlap_token_size。简单、与语义无关,胜在可预测。

# _op.py:31 — 滑动窗口
for start in range(0, len(tokens), max_token_size - overlap_token_size):
    chunk_token.append(tokens[start : start + max_token_size])
    lengths.append(min(max_token_size, len(tokens) - start))

chunking_by_seperators:先用 SeparatorSplitter 按多级分隔符(段落 → 换行 → 中英文句号/感叹/问号 → 空白,见 prompt.py 的 default_text_separator)切出原子片段,再贪心合并到 chunk_size,并对相邻 chunk 强制注入 overlap——切点落在自然边界,对下游抽取更友好。分隔符本身也被 encode 成 token 序列在 token 空间匹配。

5.2 实体关系抽取 extract_entities

这是与 LLM 交互最密集的环节,设计上有四个值得注意的机制:

① 约定式文本协议(而非 JSON)

Prompt 要求 LLM 输出用三种分隔符编码的记录流:

("entity"<|>"ALEX"<|>"PERSON"<|>"Alex is a character who...")##
("relationship"<|>"ALEX"<|>"TAYLOR"<|>"Alex is affected by..."<|>7)<|COMPLETE|>

# tuple_delimiter=<|>  record_delimiter=##  completion_delimiter=<|COMPLETE|>

元组首字段区分实体/关系;解析时正则 r"\((.*)\)" 取括号内内容再按 tuple_delimiter 切分。相比 JSON,这种协议对开源模型更鲁棒(不易被截断成非法 JSON)。

② Gleaning(补抽循环)

首轮抽取后,追加发送「MANY entities were missed...」继续提示,让 LLM 补充遗漏;并可选地再问一句「还有遗漏吗?YES/NO」决定是否继续。entity_extract_max_gleaning 控制最大补抽轮数(默认 1)。多轮对话历史通过 pack_user_ass_to_openai_messages 拼接维护。

③ 全 chunk 并发 + 全局聚合

# 所有 chunk 的抽取在限流器约束下并发
results = await asyncio.gather(*[_process_single_content(c) for c in ordered_chunks])

# 跨 chunk 聚合:同名实体收拢;无向图——边键统一 sorted(tuple)
maybe_edges[tuple(sorted(k))].extend(v)

④ 记录级校验

_handle_single_entity_extraction 要求至少 4 个字段且首字段为 "entity";关系要求 5 字段,权重字段用 is_float_regex 校验,不合法则回退 1.0。实体名/类型统一 clean_str + upper() 保证图内实体命名一致(这是合并正确性的基石)。

5.3 合并与去重:_merge_nodes / _merge_edges

节点合并 _op.py:182

  • entity_type:新旧类型一起做 Counter 投票,取众数
  • description:去重后按字典序用 <SEP> 拼接
  • source_id:并集拼接(记录来自哪些 chunk)
  • 拼接后若超过 entity_summary_to_max_tokens(500),触发 cheap_model 摘要压缩——描述永远不会无限膨胀

边合并 _op.py:230

  • weight 累加:多次独立提及视为更强关联
  • order 取 min:直接关系优先于间接关系
  • description / source_id 同节点策略
  • 端点缺失时自动补 UNKNOWN 类型节点,保证图闭合
  • 同样触发超长描述摘要

合并完成后,实体以 ent-md5 写入 entities_vdb,content = entity_name + description——向量检索时语义信号同时来自名称与描述。

5.4 Leiden 社区聚类

NetworkX 路径(gdb_networkx.py:230):

稳定最大连通分量

stable_largest_connected_component:取 LCC 后对节点与边做确定性排序重建图(并把节点名统一 upper+unescape)——保证同一张图每次跑出的聚类可复现(random_seed 固定 0xDEADBEEF)。

层级 Leiden

graspologic 的 hierarchical_leiden,max_cluster_size=10,产出多层社区划分。

回写节点

每个节点得到 clusters = [{"level":0,"cluster":12}, {"level":1,"cluster":45}, ...] 的 JSON 属性。

community_schema() 随后重建社区视图:遍历所有节点的 clusters,聚合每个社区的节点集、边集、chunk_ids;occurrence = 社区 chunk 数 / 全图最大社区 chunk 数(衡量社区覆盖语料的广度);跨层通过节点集包含关系推导 sub_communities。Neo4j 版本则用 GDS 的 gds.leiden.write 把 communityIds 写回节点属性,逻辑同构。

5.5 社区报告生成:层级化 + 预算打包

_op.py:625 generate_community_report 是全文最精巧的函数之一。

按层级从细到粗生成

for level in sorted(set(levels), reverse=True):   # level 2 → 1 → 0(细 → 粗)
    this_level_reports = await asyncio.gather(*[
        _form_single_community_report(c, community_datas) for c in this_level])
    community_datas.update(...)  # 细社区的报告会喂给粗社区当输入

粗社区的描述里可以引用子社区的报告摘要——形成自下而上的信息浓缩层级。

_pack_single_community_describe 的预算分配术

触发子社区打包(启发式)

当节点数或边数 > 100 且存在子社区报告时,优先用子社区报告的 CSV 摘要(按 occurrence 降序截断)替代原始节点边明细;剩余预算继续用于装填未被子社区覆盖的节点边。force_to_use_sub_communities 可强制开启。

去重过滤

已被子社区报告覆盖的节点/边从明细中过滤,避免重复占用 token。

按重要性排序

节点按度数、边按 edge_degree(两端度数和)降序排序——最重要的结构信息排在前面。

动态预算切分

剩余预算扣除 CSV 表头开销后,按节点/边条目数比例 node_ratio 分配各自 token 配额,再用 truncate_list_by_token_size 各自截断。三段(子报告 / 实体 / 关系)拼进固定模板。

报告 Schema

LLM 以 JSON 返回:title / summary / rating(0-10) / rating_explanation / findings(5-10 条 summary+explanation)。_community_report_json_to_str 再把 JSON 渲染成 Markdown 报告字符串,两份都存进 community_reports KV。rating 在 global 查询里充当过滤与排序信号。

成本敏感点 社区报告使用 best_model(贵模型)且每次 insert 全量重建。缓解手段:LLM 缓存(相同社区内容不重复计费)、cheap model 摘要、以及分块大小控制社区规模。插入大语料前建议先评估预算。
06

查询管线(Query)

QueryParam.mode 决定三种检索路径,共享同一套 token 预算与「CSV 上下文注入」的回答风格。

模式入口适用问题核心数据源
locallocal_query关于具体实体/事实的细节问题实体向量召回 → 图邻域 → 报告/原文
globalglobal_query宏观主题、全库总结、跨社区问题全部社区报告的 map-reduce
naivenaive_query传统向量 RAG 基线对照分块向量库 top-k 原文

6.1 local 模式:以实体为锚的四源上下文组装

_op.py:844 _build_local_query_context — 经典的 GraphRAG 局部检索:先向量召回 top_k(默认20)个相关实体,再沿图结构扩展出四类证据。

Query 向量检索
entities_vdb top-20
实体节点 + 度数
rank = degree
▼ 分四路并行扩展 ▼
社区报告
按命中频次+rating 排序
预算 3200 tokens
关联边
rank=度数和+weight 排序
预算 4800 tokens
文本分块
source_id 回溯 + 一跳邻居关联度
预算 4000 tokens
实体本体
名称/类型/描述/度数

三个扩展函数各自藏着排序智慧:

  • _find_most_related_community_from_entities:统计命中实体指向的社区(level ≤ param.level),按命中频次 → 报告 rating 二级排序截断。
  • _find_most_related_edges_from_entities:取全部实体的一跳边去重,按 (edge_degree, weight) 降序截断。
  • _find_most_related_text_unit_from_entities:从实体 source_id 回溯原始分块;关键创新是计算每个分块与一跳邻居共享的关联数(relation_counts),排序键 (order, -relation_counts)——既保召回实体的原文优先,也让「多方共同佐证」的分块胜出。

四路内容最终渲染为四段 CSV(Reports / Entities / Relationships / Sources)塞进 local_rag_response 系统提示词。CSV 用 list_of_list_to_csv(逗号+tab 分隔、字段加引号并转义)——token 高效且 LLM 容易理解。

only_need_context 开关 QueryParam.only_need_context=True 时跳过 LLM,直接返回组装好的上下文(CSV 或要点文本)。适合调试检索质量、或把 nano-graphrag 当纯检索组件嵌入其他系统。

6.2 global 模式:社区报告上的 Map-Reduce

筛选社区

level ≤ param.level 的社区,按 occurrence 降序取前 global_max_consider_community(512) 个,加载报告后按 rating ≥ 阈值过滤,再按 (occurrence, rating) 排序。

Map:分组并行找要点

报告按 token 预算(global_max_token_for_community_report=16384)切分为多个组,每组一次 LLM 调用(global_map_rag_points,强制 JSON 输出),产出带 0-100 重要性评分的要点列表。各组并行,相当于多个「分析师」各自审读一部分社区。

要点聚合

所有组的要点拍平,过滤 score ≤ 0,按 score 降序,再按 token 预算截断保留最重要的要点。

Reduce:合成最终回答

把「按重要性降序排列的分析师要点」交给 global_reduce_rag_response 提示词,LLM 综合去重、按要求格式输出最终回答。

与官方实现的差异:官方是「每个社区一个分析师 + 投票排序」的多分析师框架;nano 简化为「按 token 分组的并行 map + 全局 score 排序 reduce」,调用次数更少、逻辑更直白,效果在多数场景足够。

6.3 naive 模式:最朴素的向量 RAG

chunks_vdb 向量检索 top_k 分块 → 取回原文 → token 截断 → 用 --New Chunk-- 拼接 → naive_rag_response 提示词作答。价值在于提供一条无图基线:同一语料下对比 local/global/naive 的效果,量化知识图谱带来的增益(examples/benchmarks 里即用此思路)。

QueryParam 预算总表

字段默认值作用域
level2local/global 允许引用的社区层级上限(0 最粗)
top_k20local 实体召回数 / naive 分块召回数
local_max_token_for_text_unit4000local 原文分块预算
local_max_token_for_local_context4800local 边描述预算
local_max_token_for_community_report3200local 社区报告预算
global_max_consider_community512global 最多考虑的社区数
global_min_community_rating0global 报告评分过滤线
global_max_token_for_community_report16384global map 分组 / reduce 要点预算

设计模式:local 的三路预算 4000+4800+3200 ≈ 12000 tokens,加上实体表与提示词模板正好填满一个 16k 级模型的舒适区——预算是显式工程,不是碰运气。

07

LLM 调用层 _llm.py

对多家供应商的统一 async 封装,核心是三件事:单例客户端、指数退避重试、基于内容哈希的响应缓存。

统一函数签名

async def openai_complete_if_cache(model, prompt, system_prompt=None, history_messages=[], **kwargs) -> str

所有 complete 函数遵循同一签名:prompt + system_prompt + history_messages → str。kwargs 里弹出 hashing_kv(由 GraphRAG 在装配时 partial 注入)。基于此,create_amazon_bedrock_complete_function(model_id) 用工厂模式为任意 Bedrock 模型动态生成符合签名的函数——这正是「换 LLM 只改一个字段」的实现基础。

LLM 缓存机制

# 1. 组装 messages 后算哈希
args_hash = compute_args_hash(model, messages)          # md5(str((model, messages)))
if_cache_return = await hashing_kv.get_by_id(args_hash)
if if_cache_return is not None:
    return if_cache_return["return"]                    # 命中:零成本返回

# 2. 未命中:调 API,写回缓存并立即落盘
response = await openai_async_client.chat.completions.create(...)
await hashing_kv.upsert({args_hash: {"return": ..., "model": model}})
await hashing_kv.index_done_callback()

缓存粒度是「完整 messages 列表」——相同的抽取 prompt、相同的社区内容在重复 insert 时完全免费。这也解释了为什么「丢弃旧社区报告后重跑」在语料不变时成本趋近于零。

重试与容错

  • tenacity 装饰器:RateLimitError / APIConnectionError 时最多重试 5 次,指数退避 4~10 秒。
  • 全局单例客户端(get_openai_async_client_instance 等)避免重复建连。
  • Embedding 函数用 wrap_embedding_func_with_attrs 附带 embedding_dim / max_token_size 元数据——存储层构造时读取维度初始化索引,无需用户重复声明。
08

工具库 _utils.py

305 行里藏着六个支撑全局的设计,逐一拆解:

1. TokenizerWrapper

tiktoken 与 HuggingFace tokenizer 的统一适配器,懒加载;decode_batch 对 HF 走 batch_decode 提速。整个系统的 token 预算计算全部经过它,换 tokenizer 不影响任何调用方。

2. truncate_list_by_token_size

按序累加 key(data) 的 token 数,超预算即截断返回前缀。因为输入列表都已按重要性排序,「前缀截断」=「保留最重要的」。每项 +1 的防御性补偿模拟换行拼接开销。全文出现 10+ 次,是预算感知的总开关。

3. compute_mdhash_id / compute_args_hash

内容寻址的根基:doc-/chunk-/ent- 前缀 + MD5。相同内容永远映射到相同 ID,让去重、增量、缓存三件事共享同一套机制。

4. limit_async_func_call

自研并发限流:nonlocal 计数器 + while sleep 自旋,达到 max_size 后新调用排队。作者注释明说:不用 asyncio.Semaphore 是为了避免嵌套事件循环场景下的兼容问题——朴素但稳。

5. convert_response_to_json

JSON 容错三连击:先用栈式扫描提取第一个完整平衡的 {...} 严格解析;失败则退化为正则启发式抽取 key:value(容忍无引号、嵌套对象);再失败记错误返回 None。开源模型输出不稳时的救命稻草,且作为 convert_response_to_json_func 字段可整体替换。

6. list_of_list_to_csv / enclose_string_with_quotes

把二维表渲染成 LLM 友好的 CSV:数字不加引号、字符串加引号并把内部引号翻倍转义,分隔符用「逗号+tab」。所有上下文注入(查询、社区打包)统一走这里,格式高度一致。

clean_str 与实体归一化 参考官方 GraphRAG:html.unescape + 删除控制字符。配合抽取阶段的 upper(),保证 "Alex" / "ALEX " / "Alex\r" 在图中是同一个节点——实体合并正确性的第一道防线。
09

Prompt 工程 prompt.py

520 行 Prompt 全部移植自微软 GraphRAG,集中在一个 PROMPTS 字典里,运行时可整体覆盖——魔改行为最经济的入口。

Key用途消费方
entity_extraction实体关系抽取主提示(含 3 个 few-shot 示例)extract_entities
entiti_continue_extraction / entiti_if_loop_extractiongleaning 的续抽与「还有吗」判断extract_entities
summarize_entity_descriptions超长实体/边描述合并摘要_handle_entity_relation_summary(cheap model)
community_report社区报告生成(严格 JSON schema + grounding 规则)generate_community_report
local_rag_responselocal 查询的系统提示local_query
global_map_rag_points / global_reduce_rag_responseglobal 的 map 找要点 / reduce 合成global_query
naive_rag_responsenaive 查询的系统提示naive_query
claim_extraction声明抽取(未接入管线,covariates 的遗留)—

Prompt 设计的三个共性

  • Grounding 规则反复强调:「Do not include information where the supporting evidence for it is not provided」「Do not make anything up」在每个回答类提示里至少出现两次(目标段前后各一次)——抗幻觉的提示层手段。
  • 目标长度/格式二次注入:{response_type} 在提示头尾各出现一次,强化格式遵循。
  • 分隔符即协议:DEFAULT_TUPLE_DELIMITER=<|>、RECORD=##、COMPLETION=<|COMPLETE|>、GRAPH_FIELD_SEP=<SEP>——四个常量贯穿抽取、解析、合并、存储全链路。
10

DSPy 扩展抽取器

entity_extraction/ — 可选的「类型化抽取」替代路径:用 DSPy Signature + Pydantic 模型取代文本协议解析。

默认路径:Prompt + 文本协议

extract_entities:LLM 输出分隔符记录流 → 正则解析。优点是零额外依赖、对弱模型友好;缺点是解析失败只能丢弃该记录,schema 约束靠 Prompt「祈祷」。

DSPy 路径:Signature + 自反思

extract_entities_dspy:CombinedExtraction 签名声明输入输出类型(list[Entity] / list[Relationship],Pydantic 强约束 importance_score∈[0,1]、order∈[1,3]);可选 self_refine 循环 = Critique(批评)→ Refine(精炼)迭代改进。通过 entity_extraction_func 字段注入。

DSPy 版抽取后仍复用 _merge_nodes_then_upsert / _merge_edges_then_upsert——抽取器可替换、合并语义保持统一,这正是 base 层抽象的价值。另有 generate_dataset 可把抽取结果固化为 dspy.Example 数据集用于后续编译优化。

何时用哪个 默认路径适合快速上手与 OpenAI 系模型;DSPy 路径适合:需要稳定 schema(下游程序化消费抽取结果)、愿意为 self_refine 付出额外调用、或想用 DSPy 的编译/评估生态持续优化抽取质量。
11

关键设计决策总结

通读全部源码后,值得带走的八个设计决策:

#决策动机与效果
1dataclass 即架构配置、依赖注入、默认值三合一;asdict 快照传给存储层,避免全局配置对象。代价是配置项扁平(40+ 字段),但换来零框架心智负担。
2三存储抽象KV / Vector / Graph 的切分与数据形态严格对应;生命周期回调实现「内存工作 + 终点落盘」,写放大只发生一次。
3内容寻址(MD5 主键)去重、增量、幂等三合一。文档/分块/实体/LLM 缓存四层全部内容寻址,任何一层重放都安全。
4双模型分工best_model 做理解密集任务(抽取/报告),cheap_model 做机械压缩(描述摘要)。成本结构上把大头压到可选的贵调用里。
5token 预算显式化truncate_list_by_token_size + 排序优先级,每个上下文组装点都有硬预算。模型窗口变化只需调 QueryParam 数字。
6文本协议优先于 JSON抽取用分隔符元组流而非 JSON:抗截断、抗转义灾难、对弱模型宽容;仅在必须结构化处(社区报告、map 要点)强制 JSON + 容错解析兜底。
7社区层级复用聚类层级 = 报告生成顺序 = 查询过滤维度;粗社区引用细社区报告,同一层级结构服务索引与检索两端,信息零浪费。
8自研限流而非 Semaphore牺牲一点优雅换取事件循环兼容性(嵌套 loop、Jupyter、子线程场景);与 always_get_an_event_loop 一起构成「到处能跑」的异步策略。

已知局限

  • 社区报告不支持增量更新,每次 insert 全量重建(有 LLM 缓存兜底,但聚类本身会重算)。
  • covariates(claim 追踪)只有 Prompt 残留,功能未实现。
  • NetworkX 全内存模型适合中小图;超大图需换 Neo4j 后端。
  • JSON KV 存储整读整写,多 GB 语料时建议换嵌入式数据库实现。
  • node2vec 节点嵌入是预留接口,local 查询实际依赖实体文本向量而非图结构嵌入。
12

源码阅读路线图

如果你要亲自读一遍,推荐这条 90 分钟路径:

15 分钟 · 主干

graphrag.py 全文:只看 GraphRAG 类的字段列表、__post_init__、ainsert、aquery。建立「配置→存储→流程」的骨架认知。

10 分钟 · 契约

base.py:三个抽象类的方法签名 + QueryParam + TextChunkSchema/CommunitySchema。此后读任何实现都有地图。

35 分钟 · 算法核心

_op.py 按写入顺序读:chunking_by_token_size → extract_entities → _merge_nodes/edges → generate_community_report(重点看 _pack_single_community_describe);再读 local_query 链路。global/naive 可略读。

15 分钟 · 支撑件

_utils.py 的 TokenizerWrapper、truncate_list_by_token_size、limit_async_func_call、convert_response_to_json;_llm.py 的 openai_complete_if_cache。

10 分钟 · 存储实现

kv_json.py(最简单,46 行)→ vdb_nanovectordb.py → gdb_networkx.py 的 clustering 与 community_schema 两段。

5 分钟 · 跑一遍

用 examples/no_openai_key_at_all.py 或接自己的 LLM 跑通 insert + 三种 query,观察 working_dir 下的产物文件——把代码与磁盘形态对上,理解闭环。

一句话总结 nano-graphrag 用「可插拔存储抽象 + 内容寻址幂等 + token 预算治理 + 层级社区报告」四根柱子,把微软 GraphRAG 的方法论压缩进 1100 行可运行的教学级实现——它最好的用法不仅是使用,更是当作 GraphRAG 的可执行论文来读。