我跑穿了 GraphRAG 的索引流水线:10 步产出、8 轮实测,和那些不报错的失败——彻底搞懂GraphRAG第一篇

8 阅读20分钟

30 秒判断:你正打算把 GraphRAG 用到自己的语料上,或者已经在跑、却总觉得"结果怎么跟预期不太一样"——这篇把索引这一侧从头到尾讲清楚,附 8 轮小语料实测数据。

一句话结论:索引是一条十步流水线,但真正决定质量和成本的只有三步(切块、抽图、写报告)。它最坑的地方不是算得差,是大部分失败根本不报错——报告少了几份、一批实体被悄悄踢出社区,指标却全是绿的。

适合谁读:在搭 RAG / 文档问答 / 知识图谱的同学。不用读过 GraphRAG 论文,知道 embedding 和文本分块就够了。

阅读成本:约 20 分钟。1–2 节是地图,3–4 节可以照着直接跑起来和验收,5 节信息密度最高(参数挂在哪一步、哪些闸门不打招呼),7 节是原始数据。

数字都标了出处:✅ 我自己跑出来的 / 📄 读 graphrag 3.2.0 源码 / 🤔 我的判断。


0. 为什么值得只盯着索引看

平时聊 RAG,注意力基本都在检索和生成上。但 GraphRAG 这条路线有点特别——分量几乎全在索引:它不像普通 RAG 那样只把文档切块、算向量,而是先让 LLM 把整批语料读成一张实体关系图,再把图切成社区、给每个社区写一份报告,最后才轮到检索去用。

代价是索引很贵(要调很多次 LLM),周期也长。所以真正值得先弄清楚的是三件事:

  1. 它到底产出什么,这些东西后面谁在用;
  2. 这条流水线上有哪几个环节可以配、参数挂在哪;
  3. 它会在哪里悄悄出错——这一条尤其重要,因为它不太会报错。

所以我拿两篇万把 token 的文章、8 轮不同的提示词配置,把它从头跑到社区报告,每一步都对着 graphrag 3.2.0 的源码核了一遍。下面说的,都是这一轮里真跑出来或真读到的。

先交代下语料(后面提到就用括号里的简称):

两篇都很小。小语料足以看清"产出形状"和"流程机制",但代表不了真实知识库的规模——凡是跟规模强相关的数字(成本、耗时、社区粒度),请只当量级参考。


1. 地图:一次索引到底产出什么

输入很简单:一个 input/ 目录,丢几个 .txt 进去就能跑(文档标题默认取文件名)。一个文件也能跑。

产出是几张 parquet 表,角色差别挺大。先把这几张表分清,后面大半的困惑都不会出现:

表一行是什么在系统里的角色被谁消费
text_units一个文本块(chunk)事实源——所有引用最终落到这里向量检索、局部检索、报告生成
entities一个实体结构:图的节点社区聚类、检索、报告生成
relationships两个实体间的一条关系结构:图的边同上
communities一个社区(含层级、父子、成员)索引层的组织方式(不是事实)报告生成、查询时选社区
community_reports一个社区一份 LLM 写的报告给 LLM 读的材料(不是给机器查的)全局检索的 map-reduce
documents一篇文档辅助(标题 / 全文 / 时间)切块与溯源

一句话记住分工:chunk 是"事实",图和社区是"导航",报告是"检索时给模型看的摘要"。

1.1 这里的"图",和 RDF 那种知识图谱不是一回事

名字里都有"图谱",容易让人以为它跟 RDF / 本体式知识图谱是一类东西。实际差得挺远,差别集中体现在"一条边怎么表示" :

RDF / 本体式知识图谱GraphRAG 的 relationships 表
一条边长什么样三元组(主语,谓词,宾语),如 Claude foaf:knows X(source,target,description)——根本没有谓词列
谓词从哪来受控词表 / 本体,可枚举、可推理、可约束方向没有词表。"这是哪种关系"只体现在那句自然语言描述里
同一对节点多条关系可以(不同谓词各一条)合并成一条:按 (source, target) 分组,描述接成一段、weight 相加 📄
边上额外的字段一般只有谓词description、weight(LLM 打的关系强度)、combined_degree(两端度数之和)、text_unit_ids(证据块)
方向由本体定义由抽取时的措辞决定,没有约束保证(方向错了,这条边就指向了错的 chunk)
性质断言(说它是事实)索引——事实在 chunk 里,边靠 text_unit_ids 指回去

拿我自己跑出来的两条边看最清楚:

source: 判官            target: LLM-AS-JUDGE
description: “判官”(LLM-AS-JUDGE)是 LLM-as-Judge 评测方法的核心角色,其名称即该方法的简称……
weight: 18.0    combined_degree: 32    text_unit_ids: 2 个块

source: FLIP-RATE       target: JUDGE CONSISTENCY
description: flip-rate 是 judge consistency 的操作化定义
weight: 7.0     combined_degree: 7     text_unit_ids: 1 个块

这两条边的语义完全不同——一条说"是同义简称",一条说"是操作化定义"——但这个差别只存在于那句话里,表里没有任何字段能区分它们。在 RDF 里,"把某类关系全查出来"是一条 SPARQL;在这里你只能对描述文本做模糊匹配。

还有一个反直觉的实测结果:这一轮 120 条边里,没有任何一对 (source, target) 出现两次。多块文本提到同一对实体,是被合并成一条、强度相加的(有 14 条边的证据块不止一个)。也就是说, "同一对实体之间存在多种关系"这件事,在这个数据模型里表达不出来。

所以如果你要的是"可枚举、可推理、按关系类型查"的图,GraphRAG 的产物不能直接当知识图谱用——它更像「带证据锚点的话题导航结构」。


2. 完整流水线:十步,各自在决定什么

官方 --method standard 就是下面这十步。其中真正写向量库的只有第十步(这一点很容易看错,见 2.3):

#步骤它在决定什么贵不贵 / 会不会静默出错
1load_input_documents读 input/ 下的文档 → documents 表(标题默认=文件名)不花钱;多文件源只取第一个文件是已知限制 📄
2create_base_text_units切块 → text_units 表。chunk 是后续一切引用的落点不花钱(纯本地)
3create_final_documents整理出对外版 documents 表不花钱
4extract_graph抽实体与关系——范围由实体类型表(靶子)+ 提示词决定最贵(每块 1+gleanings 次调用,默认 2 次)
5finalize_graph合并与裁剪:丢掉"引用了不存在实体"的边不花钱;会静默丢(只打 warning)
6extract_covariates抽 claim(关于某个实体的、可能带时间限定的断言)默认关闭 📄
7create_communities层次聚类成一片社区森林 → communities 表不花钱;use_lcc 会静默排除一部分实体(见 5.1)
8create_final_text_units给每个 chunk 挂上反向引用(这块里出了哪些实体/关系/claim),就地重写 text_units 表不花钱;与 embedding 无关(见 2.3)
9create_community_reports为每个社区写一份报告较贵(每社区一次);失败静默(见第 4 节)
10generate_text_embeddings算向量并写向量库——唯一的 embedding 步骤按 token 计费

2.1 "社区树"其实是森林

一个容易讲错的地方:官方文档和很多介绍里说"把图切成社区树",听起来像一棵树。实际是一片森林——算法(层次 Leiden)在整张图上一次划出若干互不相交的顶层社区,每个顶层社区再各自往下分层。

证据就是 level = 0 的行不止一个:它们是各棵树的根(parent 为 −1)。✅ 实测根节点数:判官篇 8 个、Anthropic 篇 9 个、两篇混跑 14 个。

顺带一个更有意思的观察:这些根的"深浅"并不一样。判官篇 8 个根里,4 个一个子社区都没有(它的最细粒度就是顶层),另 4 个往下分了 3–4 个子社区。

这个不均匀对查询体验的影响挺直接:按"层级"去指定粒度时,不同顶层社区在同一个层号上代表的东西,大小能差很多。这也是我后来觉得"让用户手填一个 community level"不是个好设计的原因。

2.2 官方三种套餐,以及全部 22 个可配环节

流水线不是写死的程序,而是一份"只跑我要的步骤"的清单(配置项 workflows)。官方内置三种组合:

套餐组成什么时候用
--method standard上面那十步(LLM 抽图 + 图上下文写报告)默认;质量优先
--method fast把"LLM 抽图"换成句法抽图(extract_graph_nlp + prune_graph),报告也换成用文本块原文做上下文不调 LLM 抽图,便宜快;用质量换成本
--method standard-update / fast-update上面两种 + 八个 update_* 步骤增量索引

自己拼的话,可选单步一共 22 个(📄 index/workflows/,graphrag 3.2.0):

环节它做什么
load_input_documents / load_update_documents读全量输入 / 读增量输入
create_base_text_units / create_final_text_units切块 / 给块补反向引用
create_final_documents整理对外版文档表
extract_graph / extract_graph_nlpLLM 抽实体与关系 / 句法抽图(不调 LLM)
prune_graph / finalize_graph剪掉低频节点 / 合并与裁剪
extract_covariates抽 claim
create_communities层次聚类成社区森林
create_community_reports / create_community_reports_text写社区报告(图上下文 / 文本块原文上下文)
generate_text_embeddings算向量、写向量库
update_*(8 个)增量链:文档 / 实体关系 / 文本块 / claim / 社区 / 报告 / 向量 / 状态清理

2.3 embedding 只发生在第十步(这里最容易看错)

我一开始以为"第八步 create_final_text_units 是为 embedding 做准备的",名字实在太像了。读了源码才发现它跟 embedding 一点关系都没有:

  • 它只处理 chunk:流式读 text_units,给每一行补上 entity_ids / relationship_ids / covariate_ids 三个反向引用列,然后就地重写同一张 text_units 表。被嵌入的 text 字段一个字符都没动 📄。
  • 它排在报告生成(第 9 步)之后,逻辑上也不可能成为前面的前置。
  • 它的真实用途在查询与溯源:拿到一个块,能直接知道"这块里出了哪些实体、哪些关系"。

写向量库的只有第十步 generate_text_embeddings,而且它不只嵌 chunk,默认嵌三样(配置项 embed_text.names):

向量取自哪张表 / 哪个字段查询时谁在用它
text_unit_texttext_units.text——每个 chunk 的原文基础检索的"块向量 top-k"
entity_descriptionentities 的标题拼描述(title:description)局部检索找入口实体
community_full_contentcommunity_reports.full_content——每份报告的全文查询时定位相关社区

几个实操细节:

  • 默认嵌入模型 text-embedding-3-large(3072 维),默认向量库 LanceDB 📄;
  • 报告全文那一份通常最贵——报告本身就不短(我测到的中位数在 2–3 千字符量级);
  • 有一条护栏:某个字段要嵌、但它的源表不在产出里(比如没跑报告那步),它就 warning 一句然后跳过,不会崩 📄。

所以"跳过 embedding"只在一种场景下成立:这次只想看索引产出、不打算查。跳过之后产出里没有向量库,将来想查还得补跑。

2.4 最小可跑集合:六步,以及为什么少不了谁

从十步里摘掉四个(第十步 embedding、第八步反向引用、第三步对外版文档、第六步 claim——后者默认本来就不跑),剩下的就是能一路跑出社区报告的最小链,六步:

load_input_documents → create_base_text_units → extract_graph
→ finalize_graph → create_communities → create_community_reports

为什么是这六步——不是随手删的,是按"下一环读什么"倒推出来的(📄 看每个 workflow 打开哪张表就明白了):

步骤它读什么它写什么少了它会怎样
load_input_documentsinput/documents没有文档,切块没有原料
create_base_text_unitsdocumentstext_units抽图没有输入
extract_graphtext_unitsentities + relationships没有图,聚类无从谈起
finalize_graphentities + relationships清洗后的同一对表后面会拿到"指向不存在实体"的边
create_communitiesentities + relationshipscommunities没有社区,报告无从谈起
create_community_reportscommunities + entities + relationshipscommunity_reports——(终点)

砍得掉的三类,都是收尾或只服务查询期的步骤,不在主线上:

  1. 第十步 embedding——只服务查询期;
  2. 第八步 create_final_text_units——只是给 chunk 补反向引用,embedding 不依赖它;
  3. 第三步 create_final_documents——报告生成并不读 documents(create_community_reports 只打开 communities / entities / relationships 三张表 📄)。

反过来说:这六步里没有一步是"可选"的,每一环的输出正好是下一环的输入。


3. 跑起来:一份最小配置 + 两个必知坑

3.1 最小配置

completion_models:
  default_completion_model:
    model_provider: <provider>        # LiteLLM 前缀,如 deepseek / openai
    model: <model>
    auth_method: api_key
    api_key: ${YOUR_API_KEY}          # 从环境变量注入
    call_args: { timeout: 180, num_retries: 2 }   # 不设超时会卡死(我卡过 8 分钟)

concurrent_requests: 4                # 默认 25 容易把 API 打急

workflows: [ load_input_documents, create_base_text_units, extract_graph,
             finalize_graph, create_communities, create_community_reports ]

cache:     { type: json, storage: { type: file, base_dir: cache } }
reporting: { type: file, base_dir: logs }

注意这里故意没有配 embedding_models,理由见下一条。

3.2 --skip-validation 跳过的是什么(它不是跳过步骤)

graphrag index --root . --skip-validation

--skip-validation 这个参数它跳的是预检,跳的不是任何工作流步骤(📄 对应 validate_config_names)。预检做的事是——给配置里每一个模型发一条测试消息:每个对话模型试一次完成调用,每个嵌入模型试一次嵌入调用。

于是就出现了看起来自相矛盾的情况:

  • 你没配 embedding_models、也没跑 embedding 步骤 ⇒ 预检确实没得可试,安全;
  • 你配了 embedding_models 但填的是占位值(我早期就是这么干的)⇒ 预检会真去调它,直接 401 失败 ✅;
  • 所以口径是二选一:要么"不配 embedding + 加 --skip-validation",要么"把 embedding 配真 + 不加这个开关" 。

上面那份最小配置属于第一种,配它的命令就带上这个开关——这不矛盾。

3.3 settings.yaml 里任何字面 $ 都会炸

它用 string.Template 做环境变量替换,连注释里的 $ 也算。我踩了三次:一次是给文件匹配规则写正则的结尾锚,两次是在注释里写出了占位符的完整写法。报错还挺隐晦(Invalid placeholder in string / Environment variable not found),但原因都在这一个字符上。

3.4 先别动提示词和参数

先用默认跑一轮,拿到"默认长什么样"的基线,再改。不然你没法判断改动到底带来了什么——这一点在下面第 7 节的数据里体现得很明显。默认那版的效果确实不怎么样,但没有它,你就不知道差在哪。


4. 跑完先验收:它的失败大多是静默的

这一节是全文我最想让你看的。

跑完之后,别只看有没有报错。我遇到过三种"不报错但结果不对"的情况:

静默行为实测表现为什么指标看不见
社区报告没生成判官篇 prompt-tune 那版:64 个社区只出了 52 份(缺 12);Anthropic 篇默认:23 → 20(缺 3);两篇混跑:85 → 74(缺 11)失败发生在"LLM 返回 → 解析成 schema"这一步,HTTP 调用是成功的 ⇒ 不进 failed_response_count。指标全绿与丢掉 19% 的报告可以同时存在 ✅
关系被丢弃日志里一行 Dropped N relationship(s) referencing non-existent entities只打 warning,不进任何计数字段
实体根本没进社区判官篇那一轮:抽出 101 个实体,最终只有 74 个能被任何社区报告覆盖不是失败,是 use_lcc 的设计行为——但没有任何提示(见 5.1)

验收动作其实很简单:拿集合对账,别看指标。

社区集合 − 报告集合 = 没写出报告的社区

一行代码的事。我把它写成了脚本,每次跑完先看这个差是不是空。

这里的教训值得单独说一句:指标的边界由埋点决定,不由问题决定。 你问的是"有没有丢东西",而指标的埋点在"调用层",失败却发生在"产出层"——两者不是一回事。想判断"有没有漏",就得拿应有集合减实有集合。


5. 调优:先看参数挂在哪一步

5.1 一张表:参数的角色,决定你该测什么

属于哪一步参数它的角色因此该测什么
切块chunking.size(默认 1200 token)/ overlap(默认 100)决定"事实源"的粒度与块数抽取精度 × 成本。推荐值会随 LLM 代际过期,不必照搬 🤔
抽图extract_graph.entity_types(默认 organization / person / geo / event)+ 抽取提示词抽取范围的靶子;查询期根本不读 type范围与成本,不是分类正确率
抽图max_gleanings(默认 1)召回开关:先断言"你漏了很多" ,再让模型补一轮召回收益 vs 硬凑噪音(两者同源)
聚类cluster_graph.max_cluster_size(默认 10)/ use_lcc(默认 true)决定社区层级;use_lcc 决定哪些实体根本不进社区覆盖面(见 5.2)
写报告community_reports.graph_prompt / max_length / max_input_length报告的语言与口径、输出长度、上下文预算可读性 / 信息量 vs token
嵌入embed_text.names / embedding_models / vector_store嵌哪三样、用哪个模型、存哪个库查询召回 vs 索引成本
全流程workflows决定跑哪些步骤成本 / 耗时最直接的开关

调参方法上,我只有三条朴素的经验:

  1. 固定其他旋钮,只扫一个——chunk 大小、gleanings、类型表,最后全都落到"抽取范围"上,一起动就说不清是谁的功劳;
  2. 用一份"抽取范围"小金标准对账:必须抽到的命中了几条、明显不该进来的进来了几条。不需要逐条完美,否则维护成本会失控;
  3. 别照抄别人的推荐值——这些参数跟所用的 LLM 强绑定,换模型就得重标 🤔。

顺便说一个关于 entity_types 的判断:它在 GraphRAG 里的角色只是抽取提示词的参数,查询期完全不读(我核过:66 个 .py 文件里 entity.type 零命中)。所以分类框得粗、偶尔归错,都不算缺陷;但如果哪天你要拿它做过滤或路由,归错就变成漏召回,分类正确率立刻升级成硬要求。

这条我吃过一次亏:我一度把"类型桶的分布"当成质量指标去分析,还觉得自己发现了问题。其实那个字段在系统里的职责就不是"分类",用分类正确率去要求它,是用错了尺子。

5.2 use_lcc:决定"哪些实体根本不进社区"的静默闸门

这个配置我建议你专门看一眼:它不报错,但会安静地改变你的覆盖面。

use_lcc 是 largest connected component(最大连通片)的开关,默认 true。它的行为写在 graphs/stable_lcc.py 里,一共做三件事 📄:

  1. 只保留最大连通片上的边——只在关系图上留下那个节点数最多的连通片(连通片 = 在这个无向视角下能互相走到的节点集合),连不到它的节点与边全部丢掉;
  2. 归一化实体名(HTML 反转义、转大写、去空格),让同一实体的不同写法不至于把图撕开;
  3. 把每条边的方向按字典序摆平(名字小的当 source)——也就是说,社区划分实际上是在无向图上做的。这一条其实在 cluster_graph 里已经无条件先做过一遍,源码注释直接写着 Normalize edge direction and deduplicate (undirected graph) 📄。

一个容易混的细节:这三件里只有前两件受 use_lcc 控制。关掉它,连通片过滤和实体名归一就都不做了(后者不做,意味着大小写/空格不同的同一实体可能被当成两个,把图撕成更多小片);方向摆平无论如何都会发生。

于是 use_lcc: true(默认)意味着:不在最大连通片上的实体,在聚类之前就被排除了,永远不会属于任何社区,也就永远不会被任何社区报告覆盖。

实测后果(判官篇那一轮):

抽取到实体            101 个
其中出现在关系里的      99 个   (另 2 个一条边都没有)
最大连通片上的节点      74 个   ⇒ 25 个节点在聚类前被丢掉
最终能被社区覆盖的实体   74 个   ⇒ 27 个实体一个社区都进不去

多出来的 27 个实体,就这么无声无息地消失了。 它们是谁?看名字就知道为什么连不上主片:一串虚构地名和人名(白丘国、碧石监狱、苏小满、林柏舟……),加上仓库路径这类"语料里只出现过一次"的字符串——各自三两相连、自成小片。

所以这个闸门是双面的:好处是自动去噪,代价是它不区分"噪声"和"稀但真实的旁支"。

怎么选:

  • 想保住覆盖面、宁可多留噪声 ⇒ 设 use_lcc: false,代价是那些孤立小片各自成社区,报告数量和成本一起上涨;
  • 想要干净、能接受"稀有的东西看不见" ⇒ 保持默认。

关键是知道这道闸门存在,并且它不打招呼。

附带一个自查方法:跑完对一下"实体表行数"和"被任何社区收录的实体数",差多少就是被 use_lcc 挡掉的量。

5.3 prompt-tune 应该是调优的第一步,不是可选项

官方默认提示词是给英文通用语料写的。换一篇别的语言、别的领域的语料,产出会明显变形——我第一次跑判官篇(中文),图和报告全变英文,实体类型也还是英文通用那四类。

所以 prompt-tune 不该被当成"可选工具",它是调优的起点:

graphrag prompt-tune --root . --selection-method top --limit 7 --output prompts-tuned

它用你自己的语料反推一份适配的提示词,我这两篇原料一次跑的成本大约 $0.04 ✅。产出六样东西(都落成文件):领域推断、语言检测、persona(角色设定)、实体类型表、few-shot 示例、评分标准(带锚点) ,写成三份提示词:

prompts-tuned/extract_graph.txt              # 抽图提示词(含实体类型表与示例)
prompts-tuned/summarize_descriptions.txt     # 实体描述摘要提示词
prompts-tuned/community_report_graph.txt     # 社区报告提示词(含评分口径)

采样逻辑是:先全量切块(本地、零 token),再按选法抽块(📄 prompt_tune/loader/input.py):

--selection-method实现性质
top取前 --limit 块(默认 15)系统性偏向文档开头 / 排在前面的那一篇(实测两篇混跑时,示例全取自第一篇)
random(默认)随机抽 --limit 块无偏但样本小,且没有固定种子、不可复现
auto随机抽 ≤--n-subset-max(默认 300)块去嵌入 → 取离质心最近的 --k(默认 15)块意图是"选最主流 / 最有代表性";实现有缺陷(见下) ;也是唯一会真调 embedding 的选法
all全部块样本最大、也最贵

两处已知实现缺陷(📄 同文件):

  1. auto 行号错位:抽样后的块存进了另一个变量,但最后取"离质心最近"时用的仍是原始顺序的整张表、拿位置索引去取 ⇒ 取到的不是离质心最近的那些,实际退化成文档开头那几块;
  2. 块数少于 15 会直接崩:越界时它先把 limit 回落成 15,再无条件 sample(15)。所以语料很小的时候,默认的 random 选法反而是最危险的那个。

产出能信到什么程度(✅ 实测):

  • 类型与 persona 能跨领域融合:Anthropic 篇(英文·安全)+ 判官篇(中文·评测)一起跑,得到一份 13 类、覆盖两边的类型表;
  • 语言只能挑一个:检测到中文就把三份文件都写成中文 ⇒ 英文那一半的描述与报告会被译成中文。对"报告服务于大模型阅读但是不作为可信数据源"的定位来说可以接受,但换语料必须重跑;
  • 示例取自语料自身 ⇒ 它调的是"这一篇"的适配,不是"整个库"的适配;
  • 评分口径不保证有区分度:见 6.3。

6. 我撞到的几条边界

6.1 实体类型不参与检索

entity.type 在查询期完全不被读取(📄 66 个 .py 文件零命中)。别指望靠它做过滤或路由,它就是个抽取靶子。

6.2 社区报告会缺,而且每次缺的不一样

同一批语料里丢哪几个社区,每次都不一样:判官篇第三版丢 #4,第四版丢 #9 / #18,prompt-tune 那版丢了 12 个。这是调用级的抖动,不是某个社区本身有问题。所以,某次少了一份报告,不用去怀疑某个社区有问题;但也别指望它会主动告诉你。

6.3 报告里的 rank 不保证有区分度

rank 是社区报告里的重要性评分,口径由报告提示词决定。prompt-tune 生成的那版把口径改成"与领域的相关性"之后,判官篇 48/52 份报告的 rank 都 ≥ 8(0–10 量表)✅——评分标准变得"更专业",反而把排序能力吃掉了。

所以想拿 rank 排序或过滤的话,口径得选一个在你这批语料里有方差的维度。挑口径的时候先看一眼分数分布——全挤在一起,就说明这个口径对你没用。

6.4 提示词不能跨语料移植

语言、领域、类型、示例全都被烧进提示词文本里。我把为判官篇调好的提示词原样套到 Anthropic 篇上,结果是:报告 100% 变中文(英文原文被翻了),标题里出现"判官 / 金标准"这类另一篇的主题词,还长出了新的垃圾桶——44% 的实体被归进 TECHNICAL TERM ✅。

6.5 多文件源只处理第一个文件

这是我踩到的另一个静默行为:input/ 里放多文件时,只取第一个文件 📄。不是配置问题,是这一版工作流的既有行为。多文件语料要自己先合并,或按文件拆成多次跑。


7. 八轮实测数据

下面是全部 8 轮的原始数据(语料就前面那两篇,每轮只改提示词或语言设置,其余参数不动)。

规模列=实体 / 关系 / 社区 / 报告数:

轮次语料提示词 / 设置规模得到什么
1Anthropic 篇官方默认提示词(英文、四类实体)128 / 185 / 23 / 20看清产出形状;发现示例串味;发现静默失败;撞上 provider 的 schema 卡点
2判官篇官方默认提示词95 / 109 / 16 / 15报告语言:无约束时会回落到英文
3判官篇只改语言那一句("用英文输出"→"跟随原文语言")133 / 153 / 33 / 31语言回来了,但实体名大小写与类型标签还不服帖
4判官篇再改三处:中文示例 + 去掉"首字母大写"要求 + 加"不要翻译"101 / 120 / 21 / 20抽取侧终于服帖:实体描述含中文比例 11%→91% 、关系描述 3%→98% (换成更严的"纯中文、不含拉丁字母"口径则是 52% / 46%);但报告语言仍未稳定(标题 12/20、正文 16/20 含中文)
5判官篇再把报告提示词整篇译成中文(不改语义)101 / 120 / 21 / 19报告标题 19/19、正文 19/19 全是中文(此前有整篇英文的报告);报告长度中位 2,749→1,946 字符(顺带省 token)。抽取侧与第 4 轮完全相同,说明这轮只动了报告
6判官篇prompt-tune 自动生成的三份提示词(13 类实体)273 / 431 / 64 / 52类型表贴合语料:EVENT 这类垃圾桶从 61% 降到最多 14%;成本 0.055→0.055→0.178;同时暴露 12/64 的静默丢报告
7Anthropic 篇把第 6 轮为判官篇调好的提示词原样移植过来277 / 381 / 63 / 54报告 100% 中文 + 主题串味 + 新垃圾桶(TECHNICAL TERM 占 44%)⇒ 提示词不能跨语料移植
8两篇一起两篇混跑 prompt-tune368 / 581 / 85 / 74类型 / persona 能跨领域融合;语言仍只能挑一个;示例偏偏全取自第一篇

成本量级(供估算,✅ 实测,用的是 DeepSeek 的对话模型;换别家价格与质量都要重估):

动作成本
单篇索引一轮(只到社区报告)$0.05–0.18
prompt-tune 一次≈ $0.04
两篇混跑一轮(含 74 份报告)≈ $0.3

读表前先说几点:

  • 第 1 轮 vs 第 6 轮:规模从 128/185 涨到 273/431,不是模型变强了,是类型表变贴合了——EVENT 这个垃圾桶被拆开以后,原本被吞掉的实体都出来了;
  • 第 6 轮 vs 第 7 轮:同一份提示词换语料,规模不掉,但内容质量掉得很明显(串味 + 新垃圾桶)——"能跑通"和"跑得对"是两件事;
  • 第 4 轮 vs 第 5 轮是最能说明问题的一组对照:第 4 轮改的是抽取侧提示词(图变了:描述含中文率 91% / 98%),报告语言并没有跟着变好(标题 12/20、正文 16/20);第 5 轮只改报告侧提示词,图一模一样(规模、中文率都相同),报告立刻变成 19/19 全中文。结论是两条链路的语言各自管自己的,互不代劳;
  • 还有一条反过来的观察:报告即使"全中文",正文里仍会保留必要的英文术语——第 4/5 轮报告正文的字符级中文占比中位数约 60% 。这通常不是问题,因为剩下那些英文基本是原文本来就在用的专有名词。

8. 小结:什么时候值得上 GraphRAG

回到最开始那个问题——值不值得。

它的强项是"全局性、跨文档的问题":把一片语料里散落的主题收拢起来做综述、找跨源连接,这类问题向量检索确实给不出(向量检索必然要套 top-k,输出恒为"点集")。

它的代价也很实在,有两条我建议提前想清楚:

  1. 索引贵(要调很多次 LLM)而且慢。本文这两篇小文章只是"看清形状",真实规模的语料要按千次调用、小时级去估。
  2. 社区检测是全局优化,增量更新很难做干净。这一版的增量索引本质是"把新内容单独成图、再和旧表拼接"——不重算社区、不重算度数。对一个内容持续更新的知识库来说,这意味着主题会被切成互不相连的两半、同一个主题反复出现、"全面性"看起来变好其实是重复计入。

反过来,如果你手上的语料是一段时间内相对稳定、隔一段时间整体重建一次的(比如一批报告、一批论文、一次尽调),GraphRAG 的这套产出就很合适;如果你的知识库是每天有新内容进来的,那在现有版本下要做的是把"索引刷新周期"和"语料演化速度"对齐,而不是期待它能增量维护。

最后再说一遍那个最实用的动作:跑完先做集合对账,别信指标。 报告少了几份、27 个实体没进社区、关系被丢了一批——这些都不会报错。


附:实验环境与复现

  • GraphRAG 版本:graphrag 3.2.0(Python 3.11)
  • 对话模型:DeepSeek deepseek-chat;嵌入模型:默认 text-embedding-3-large
  • 语料:前面那两篇文章,各约 2.7–2.9 万字符
  • 完整脚本、每一轮的提示词与产出、以及本文引用的所有审计脚本,都在开源仓库里:github.com/lipeidonggz…(实验数据在 data/tmp/graphrag-exp* 下)

如果你也在跑 GraphRAG,欢迎在评论区说说你遇到的静默失败——我很想看看这份清单还缺哪些。