Demo 19:用 LlamaIndex.TS 实现 GraphRAG
GitHub 源码:demo19-LlamaIndex-GraphRAG 这个示例参考
demo7-LlamaIndex的本地知识库写法,为当前项目增加一个可观察、可运行的 GraphRAG 流程:
本地文档
├─ LlamaIndex SentenceSplitter 切块
├─ LLM 抽取实体与关系 → 内存属性图
└─ LlamaIndex VectorStoreIndex → 语义召回
↓
种子实体 + 三跳图遍历
↓
关系链 + 对应原文片段
↓
LlamaIndex ResponseSynthesizer
↓
最终答案它不是简单地把“知识图谱”几个字加进提示词:程序会真正构建实体和关系,按问题与向量召回结果选择种子实体,最多遍历三跳,再把图关系对应的原文证据交给 LlamaIndex 合成答案。
为什么实现了一层内存属性图
本仓库锁定的是 llamaindex@0.12.1。这个 TypeScript 包提供文档切块、向量索引、Retriever 和 ResponseSynthesizer,但没有导出 Python 版的 PropertyGraphIndex、SimpleLLMPathExtractor 等 GraphRAG API。因此本 demo 保留 LlamaIndex.TS 的 RAG 主干,只补上实体/关系存储与图遍历层。
内存图很适合教学和小数据验证;生产环境可以保持查询流程不变,将 InMemoryPropertyGraph 换成 Neo4j、NebulaGraph 或其他图数据库实现。
核心原理与代码执行流程
这个 demo 具体演示了什么
普通向量 RAG 擅长找出“和问题语义相似的片段”,但不一定能稳定连接分散在多个文档中的事实。这个 demo 在向量检索之外增加了一张真正的实体关系图,使查询可以从命中的实体继续沿关系向外扩展。
整个程序分成两个阶段:
构建阶段:文档 -> 切块 -> LLM 抽取实体/关系 -> 内存图
\-> Embedding -> 向量索引
查询阶段:问题 -> 向量召回 + 实体匹配 -> 三跳图遍历
-> 合并图关系与对应原文 -> LLM 合成答案Chat 模型在这里承担两项工作:从文档中抽取图谱,以及根据召回证据生成最终答案。Embedding 模型只负责把文本转换成向量,用于语义相似度检索。
1. 初始化模型与配置
index.ts 先从项目根目录的 .env 读取模型、接口地址、超时和重试配置,然后写入 LlamaIndex 的全局 Settings:
Settings.llm = new OpenAI({ ... });
Settings.embedModel = new OpenAIEmbedding({ ... });Settings.llm会被图谱抽取和最终答案合成使用。Settings.embedModel会被VectorStoreIndex使用。temperature: 0用于降低图谱抽取结果的随机性。- 超时、SDK 重试次数和片段级重试次数都可以通过环境变量调整。
2. 读取文档并切块
buildGraphRAGEngine() 使用 SimpleDirectoryReader 读取 documents/,再通过 SentenceSplitter 把文档切成多个 TextNode:
const splitter = new SentenceSplitter({
chunkSize: 320,
chunkOverlap: 50,
});chunkOverlap 会让相邻片段保留部分重复上下文,减少一句关系描述刚好被切断的概率。每个节点还保留文件名、页码等 metadata,后面可以追溯答案来源。
3. 从片段抽取实体和关系
extractGraphFromChunk() 会把每个文档片段交给 Chat 模型,要求只返回如下结构:
{
"entities": [
{ "name": "Atlas 服务", "type": "服务", "description": "..." }
],
"relationships": [
{
"source": "Atlas 服务",
"target": "韩梅",
"relation": "技术负责人",
"description": "..."
}
]
}模型输出不能直接信任,因此代码还会:
- 去掉可能出现的 Markdown JSON 代码块和前后说明。
- 用
JSON.parse()解析内容。 - 用 Zod 的
extractionSchema校验字段。 - 限制每个片段最多写入 20 个实体和 20 条关系。
- 对临时连接错误或无效 JSON 做退避重试。
4. 内存属性图如何存储数据
graph.ts 中的 InMemoryPropertyGraph 主要维护三张 Map:
entities 实体 key -> 实体信息与来源片段
relationships 关系 ID -> 起点、终点、关系和来源片段
chunkEntities 片段 ID -> 该片段出现过的实体normalizeEntityName() 会统一兼容字符、大小写,并去掉空白和标点。因此 Atlas 服务 和 Atlas服务 可以得到相同的内部 key。展示时仍然保留第一次写入的原始名称。
每个实体和关系都会保存 chunkIds。这不仅能合并多个片段重复抽取出的事实,还能在图遍历后找到关系对应的原文证据。
5. 查询如何连接向量与图
GraphRAGQueryEngine.query() 是混合检索的核心,按以下顺序执行:
vectorRetriever.retrieve(question)先召回语义最相似的两个片段。findEntityKeys(question)找出问题中直接出现的已知实体。entityKeysForChunks()把向量命中片段中的实体也加入图遍历种子。traverse(seedKeys, 3)从种子实体开始进行最多三跳的广度优先搜索。getSourceChunkIds()找回遍历关系涉及的原文片段。- 合并向量来源与图来源,去重后最多保留八个片段。
- 把格式化后的图关系和原文一起交给
ResponseSynthesizer生成答案。
向量检索在这里负责“找到入口”,图遍历负责“沿事实关系扩展”。即使问题没有准确写出某个实体名称,向量召回的片段也可以把其中的实体接到图上。
6. 三跳图遍历
InMemoryPropertyGraph.traverse() 使用广度优先搜索。搜索时会把关系视为可从两侧进入,例如已有关系:
Atlas 服务 --[技术负责人]--> 韩梅程序既可以从 Atlas 服务 找到 韩梅,也可以从 韩梅 找回 Atlas 服务,但最终输出仍保留关系原始方向。visited 防止实体在环中被重复访问,frontier 表示当前这一跳需要继续扩展的实体。
默认问题最终需要连接三条分散的事实:
Aurora 项目 --[依赖]--> Atlas 服务
Atlas 服务 --[技术负责人]--> 韩梅
韩梅 --[属于]--> 数据平台部7. 回填原文并生成答案
程序不会只把三元组交给模型,还会把关系对应的原文片段一并放进 contextNodes:
- 图关系节点的分数设为
1,突出结构化关系链。 - 向量命中的片段沿用相似度分数。
- 仅由图扩展得到的片段使用较低的默认分数。
- 最终提示词要求模型只依据这些证据回答,证据不足就明确说不知道。
终端中的向量来源、种子实体和多跳关系属于 QueryTrace,用于观察检索过程,不会作为另一套隐藏推理逻辑。
8. 教学实现的边界
这个 demo 展示了 GraphRAG 的核心数据流,但还不是生产级实现:
- 图谱和向量索引都只保存在内存,进程退出后会丢失。
- 每次启动都会重新调用模型抽取图谱并生成 Embedding。
- 实体消歧主要依赖名称归一化,没有别名表或实体对齐模型。
- 图谱质量受模型抽取稳定性影响。
- 三跳和召回数量是固定参数,没有根据问题动态规划。
- 数据量增大后,应改用图数据库和持久化向量数据库,并按文档 hash 增量更新。
1. 环境要求
- Node.js 20 或更高版本
- pnpm 11
- 同时支持 Chat Completions 和 Embeddings 的 OpenAI 或 OpenAI 兼容接口
进入项目根目录:
cd ai-agent-langgraph-demo
pnpm install2. 配置环境变量
与 demo7-LlamaIndex 共用项目根目录的 .env:
OPENAI_API_KEY=sk-你的密钥
MODEL=gpt-4o-mini
EMBEDDING_MODEL=text-embedding-3-small
# 第三方兼容接口才需要配置,通常包含 /v1
# OPENAI_BASE_URL=https://example.com/v1
# 可选:慢速模型或兼容接口可调大单次请求超时(毫秒)
# OPENAI_TIMEOUT_MS=180000
# OPENAI_MAX_RETRIES=2
# GRAPH_EXTRACTION_MAX_ATTEMPTS=2
# GRAPH_EXTRACTION_RETRY_DELAY_MS=1000不要把真实 API Key 提交到 Git。
3. 运行
执行默认的三跳问题:
pnpm demo19默认问题是:
Aurora 项目依赖哪个服务?这个服务由谁负责,负责人属于哪个部门?这个答案需要连接分散在三个文件中的关系:
Aurora 项目 --[依赖]--> Atlas 服务
Atlas 服务 --[负责人]--> 韩梅
韩梅 --[属于]--> 数据平台部自定义问题:
pnpm demo19 -- "Atlas 通过哪个服务接收告警?两个服务分别由谁负责?"默认会把向量召回、种子实体和实际遍历到的关系输出到终端,便于观察 GraphRAG 是否真的工作。只看答案时可以使用:
pnpm demo19 -- --no-trace "Aurora 项目的负责人是谁?"4. 验证
类型检查:
pnpm typecheck:demo19图存储与三跳遍历的本地测试不调用模型,也不会产生 API 费用:
pnpm test:demo195. 替换成自己的资料
把 .txt、.md、.csv、.html、.pdf 或 .docx 文件放进:
demo19-LlamaIndex-GraphRAG/documents/然后重新运行。当前 demo 每次启动都会重新抽取图谱和生成 Embedding,图与向量索引都只保存在内存中。
为了得到稳定关系,资料中最好明确写出主语、关系和宾语,例如“Atlas 服务的技术负责人是韩梅”,不要只依赖跨页代词。
6. 代码结构
demo19-LlamaIndex-GraphRAG/
├── index.ts # 文档加载、图谱抽取、混合检索与答案合成
├── graph.ts # 内存属性图、实体匹配与多跳遍历
├── graph.test.ts # 不调用 API 的图遍历测试
├── tsconfig.json
└── documents/ # 示例知识库7. GraphRAG 与普通向量 RAG 的区别
普通向量 RAG 主要取“和问题最像的片段”。GraphRAG 会先命中实体,再沿关系扩展到语义并不相似、但结构上有关联的资料。因此它更适合组织关系、系统依赖、供应链、人物事件等多跳问题。
本 demo 采用混合检索:向量召回负责找到入口,属性图负责扩展关系链,最后回填原文降低仅凭关系三元组回答造成的信息损失。
8. 常见问题
图谱抽取 JSON 失败
程序会自动重试一次。若仍失败,说明当前聊天模型不擅长按 JSON 输出;请换用指令遵循更好的模型。这里没有强依赖 OpenAI response_format,是为了兼容更多第三方接口。
APIConnectionTimeoutError: Request timed out
本 demo 默认把 LlamaIndex 的单次请求超时从 60 秒提高到 180 秒,并会在某个片段发生临时连接错误时进行片段级退避重试。慢速推理模型或繁忙的兼容接口仍然可能需要更长时间,可在 .env 中继续调大 OPENAI_TIMEOUT_MS,例如 300000。
OPENAI_MAX_RETRIES 控制底层 SDK 对临时错误的重试次数;GRAPH_EXTRACTION_MAX_ATTEMPTS 控制片段抽取(包括无效 JSON)的尝试次数。认证失败、请求参数错误等非临时错误不会反复重试。
Embedding 返回 404 或 model not found
聊天模型和 Embedding 模型是两类模型。确认接口支持 Embeddings,并把 EMBEDDING_MODEL 改为服务商真实提供的模型 ID。
为什么每次运行都比较慢
每次启动都要逐片段抽取实体/关系,再生成 Embedding。这是为了让教学流程完整可见。生产环境应持久化图数据库和向量数据库,并按文档 hash 做增量更新。
为什么图关系有时命名不一致
实体和关系由 LLM 抽取。真实项目应加入固定 schema、实体消歧、别名表和人工评估;本示例只做了大小写、空白和标点归一化。
