当 RAG 遇上知识图谱:GraphRAG 到底强在哪?万悟 UniAI-GraphRAG 源码拆解
📌 本文是《从零吃透企业级 AI 平台:元景万悟源码学习手记》系列第一季·源码学习篇的第 4 篇
🎯 读完本文你将:① 理解 GraphRAG 与传统 RAG 的本质区别 ② 掌握知识图谱构建与图检索的核心原理 ③ 能在万悟中启用 GraphRAG 并对比效果
⏱️ 预计阅读时间:35 分钟 | 动手实践:70 分钟
💻 前置要求:已完成第 3 篇,对传统 RAG Pipeline 有实操经验
一、这篇文章要解决什么问题?
在第 3 篇中,我们用传统 RAG 解决了"单文档精准问答"的问题。但现实企业中,很多问题是跨文档、多跳推理的:
"A 公司的 CEO 和 B 公司的 CTO 是什么关系?他们共同参与过哪些项目?"
这个问题涉及 3 个实体(A公司CEO、B公司CTO、项目)和 2 层关系。传统 RAG 的向量检索只能找到"语义相似"的片段,但无法理解"关系链"。它可能召回了 A 公司 CEO 的介绍和 B 公司 CTO 的介绍,却找不到连接两者的那条关键信息。
GraphRAG = 知识图谱 + RAG。它先把文档中的实体和关系抽取出来,构建成一张"知识网络",检索时沿着网络的边"走"过去,而不是在平面上"搜"出来。
传统 RAG: 问题 → 向量相似度搜索 → 扁平文本块列表GraphRAG: 问题 → 实体识别 → 图上多跳遍历 → 结构化子图 + 文本块
万悟内置了UniAI-GraphRAG模块,这是联通研究院自研的 GraphRAG 引擎。今天我们就把它拆开看。
二、核心概念:用大白话讲清楚
2.1 知识图谱:把文字变成"关系网"
💡 类比:传统 RAG 像图书馆的书架——书按类别排列,你通过书名(关键词/语义)找书。知识图谱像社交网络——每个人是一个节点,"认识""合作""亲属"是连线。你想找"A 和 B 的关系",直接沿着连线走过去就行。
知识图谱的三个基本元素:
2.2 GraphRAG 的三阶段 Pipeline
▲ UniAI-GraphRAG 三阶段流程(图片来源:GitHub 仓库 docs)
【阶段1:图谱构建(离线,一次性)】文档 → LLM 实体/关系抽取 → 三元组(S,P,O) → 写入图数据库(Neo4j/NebulaGraph)→ 同时保留原始文本块(用于最终生成)【阶段2:图检索(在线)】用户问题 → LLM 识别问题中的实体 → 在图上做 N-hop 遍历→ 获取相关子图(节点+边+关联文本块)【阶段3:增强生成(在线)】子图结构化信息 + 关联文本块 + 用户问题 → 组装 Prompt → LLM 生成回答
2.3 为什么 GraphRAG 不是万能药?
💡 经验法则:如果你的问题能用
Ctrl+F或向量搜索解决,别上 GraphRAG。只有当问题涉及"关系链""跨文档关联""全局统计"时,GraphRAG 才有明显优势。
三、万悟是怎么实现的?(源码篇)
3.1 定位源码
internal/rag-service/├── graphrag/│ ├── extractor.go# LLM 实体/关系抽取(核心!)│ ├── builder.go# 图谱构建编排│ ├── retriever.go# 图检索(多跳遍历)│ └── prompt/# 抽取和检索用的 Prompt 模板│ ├── extract.txt# 实体关系抽取 Prompt│ └── retrieve.txt# 问题实体识别 Prompt├── repository/│ └── graph.go# 图数据库操作封装└── service/└── graphrag.go# GraphRAG 对外服务入口
3.2 实体关系抽取(extractor.go)
这是 GraphRAG 最核心的环节——用 LLM 从非结构化文本中提取结构化三元组。
// 文件:internal/rag-service/graphrag/extractor.go// ExtractTriplets 从一段文本中提取实体和关系// 输入:原始文本块// 输出:三元组列表 [(主体, 关系, 客体), ...]func (e *Extractor) ExtractTriplets(ctx context.Context, text string) ([]Triplet, error) {// Step 1: 加载抽取 Prompt 模板// 这个 Prompt 是关键工程,决定了抽取质量promptTemplate := e.loadPrompt(”extract.txt”)// Step 2: 填充模板prompt := strings.Replace(promptTemplate, ”{{TEXT}}”, text, 1)// Step 3: 调用 LLM(要求返回 JSON 格式)resp, err := e.llm.Complete(ctx, []Message{{Role: ”system”, Content: ”你是一个知识图谱抽取专家。严格按JSON格式输出。”},{Role: ”user”, Content: prompt},})if err != nil {return nil, fmt.Errorf(”llm extraction failed: %w”, err)}// Step 4: 解析 LLM 返回的 JSON// 预期格式:{”triplets”: [{”subject”:”张三”,”relation”:”属于”,”object”:”研发部”}]}var result ExtractionResultif err := json.Unmarshal([]byte(resp.Content), &result); err != nil {// LLM 有时返回格式不规范,这里做容错处理return nil, fmt.Errorf(”parse extraction result failed: %w”, err)}// Step 5: 后处理(去重、实体对齐、置信度过滤)triplets := e.postProcess(result.Triplets)return triplets, nil}
📌 关键洞察:GraphRAG 的质量高度依赖抽取 Prompt 的质量。万悟的
extract.txt经过大量调优,包含:① 明确的实体类型定义 ② Few-shot 示例 ③ 输出格式约束 ④ 负例说明。你在实际使用中可能需要根据自己的领域微调这个 Prompt。
3.3 图谱构建编排(builder.go)
// 文件:internal/rag-service/graphrag/builder.go// BuildGraph 构建知识图谱的主入口func (b *Builder) BuildGraph(ctx context.Context, kbID string) error {// Step 1: 获取知识库下所有已解析的文本块chunks, err := b.chunkRepo.ListByKB(ctx, kbID)if err != nil {return err}// Step 2: 批量抽取三元组(并发控制,避免 LLM API 限流)var allTriplets []TripletWithSourcesemaphore := make(chan struct{}, 5) // 最多5个并发for _, chunk := range chunks {semaphore <- struct{}{}go func(c Chunk) {defer func() { <-semaphore }()triplets, err := b.extractor.ExtractTriplets(ctx, c.Text)if err != nil {log.Warnf(”extract failed for chunk %s: %v”, c.ID, err)return}// 记录每个三元组来自哪个文本块(溯源用)for _, t := range triplets {allTriplets = append(allTriplets, TripletWithSource{Triplet: t,ChunkID: c.ID,})}}(chunk)}// Step 3: 实体对齐(同一个实体可能有不同表述)// 例如:”中国联通” 和 ”China Unicom” 应该是同一个节点aligned := b.entityAligner.Align(allTriplets)// Step 4: 写入图数据库err = b.graphRepo.BulkUpsert(ctx, aligned)if err != nil {return fmt.Errorf(”write graph failed: %w”, err)}// Step 5: 更新知识库状态b.kbRepo.UpdateGraphStatus(ctx, kbID, GraphReady)return nil}
💡 工程细节:注意
semaphore的使用。LLM API 通常有 RPM(每分钟请求数)限制,不加并发控制会导致大量 429 错误。万悟默认设为 5,你可以在配置中调整。
3.4 图检索(retriever.go)
// 文件:internal/rag-service/graphrag/retriever.go// Retrieve 根据用户问题在图上检索相关信息func (r *Retriever) Retrieve(ctx context.Context, query string, maxHops int) (*SubGraph, error) {// Step 1: 用 LLM 识别问题中的实体// ”A公司CEO和B公司CTO的关系” → [”A公司CEO”, ”B公司CTO”]entities, err := r.identifyEntities(ctx, query)if err != nil {return nil, err}// Step 2: 在图上做多跳遍历// 从识别出的实体出发,沿边走 maxHops 步subGraph, err := r.graphRepo.MultiHopSearch(ctx, MultiHopParams{StartEntities: entities,MaxHops: maxHops, // 默认 2 跳MaxNodes: 50, // 限制子图大小,防止爆炸EdgeTypes: nil, // nil 表示不限关系类型})if err != nil {return nil, fmt.Errorf(”graph traversal failed: %w”, err)}// Step 3: 获取子图中每个节点/边关联的原始文本块// 这一步让 LLM 生成时有”原文依据”chunkIDs := subGraph.GetAssociatedChunkIDs()chunks, _ := r.chunkRepo.BatchGet(ctx, chunkIDs)subGraph.Chunks = chunksreturn subGraph, nil}
📌 性能关键点:
MaxNodes: 50这个限制非常重要。如果两个热门实体之间有上千条路径,不加限制会导致子图爆炸,检索超时且 Prompt 超长。万悟默认 50,可根据实际数据规模调整。
3.5 GraphRAG vs 传统 RAG 的融合
万悟并不是二选一,而是支持混合模式:
// 文件:internal/rag-service/service/search.gofunc (s *Service) HybridSearch(ctx context.Context, req SearchRequest) (*SearchResponse, error) {var results []ChunkScore// 路线1:传统 RAG(始终执行)ragResults, _ := s.ragSearch(ctx, req)results = append(results, ragResults...)// 路线2:GraphRAG(仅在知识库启用了图谱时执行)if req.KnowledgeBase.GraphEnabled {graphResults, err := s.graphRetriever.Retrieve(ctx, req.Query, 2)if err == nil && len(graphResults.Chunks) > 0 {// 将图检索结果也加入候选集results = append(results, graphResults.Chunks...)}}// 统一 RRF 融合排序(和第 3 篇的逻辑一致)merged := rrfMerge(results, req.TopK)return &SearchResponse{Chunks: merged}, nil}
💡 设计哲学:GraphRAG 是传统 RAG 的增强层,而非替代品。两者互补:传统 RAG 兜底语义搜索,GraphRAG 补充关系推理。
四、动手跑通(实践篇)
4.1 准备适合 GraphRAG 的测试数据
GraphRAG 需要富含实体关系的文档才能发挥价值。推荐:
⚠️ 不要用纯 FAQ 或操作手册测试 GraphRAG——这类文档关系稀疏,传统 RAG 反而更好。
4.2 启用 GraphRAG
进入「知识库」→ 选择目标知识库 → 「设置」 开启「知识图谱」开关 选择图谱构建模型(建议用 GPT-4o 或 Qwen-Max,小模型抽取质量差) 点击「构建图谱」 等待完成(比传统索引慢 3-5 倍,因为要逐块调 LLM)
▲ 知识图谱构建中,可查看实时进度和抽取统计
4.3 可视化知识图谱
构建完成后,进入「知识图谱」→「可视化」:
节点大小 = 关联边数量(越重要越大) 边的颜色 = 关系类型 点击节点可查看关联文本块
📌 建议截图保存。图谱可视化是博客中最吸引读者的内容之一。
4.4 对比实验(核心价值!)
用同一个问题,分别测试三种模式:
再测一个传统 RAG 擅长的问题:
📌 记录你的发现。比如:"对于'张三参与了哪些项目'这类关系查询,GraphRAG 召回了 5 个项目且全部正确;传统 RAG 只召回了 2 个,且混入了同名人的项目。但对于'如何填写报销单'这类操作问题,传统 RAG 响应快 3 倍且准确率相同。"
4.5 调优实验
五、自己造一个 Mini 版(Deep Dive)
🎯 目标:用 Python + NetworkX 实现一个最简 GraphRAG
# mini_graphrag.py# 依赖:pip install openai networkximport openai, json, networkx as nxclient = openai.OpenAI(api_key=”sk-your-key”)# ===== 1. 图谱构建 =====docs = [”张三是研发部总监,负责AI平台项目。”,”李四是研发部工程师,向张三汇报。”,”AI平台项目使用了元景大模型技术。”,”王五是市场部负责人,与张三共同推进AI营销方案。”,]EXTRACT_PROMPT = ”””从以下文本中提取实体和关系,返回JSON格式:{”triplets”: [{”subject”:”...”, ”relation”:”...”, ”object”:”...”}]}文本:{{TEXT}}”””G = nx.DiGraph()for doc in docs:prompt = EXTRACT_PROMPT.replace(”{{TEXT}}”, doc)resp = client.chat.completions.create(model=”gpt-4o-mini”,messages=[{”role”: ”user”, ”content”: prompt}],response_format={”type”: ”json_object”})data = json.loads(resp.choices[0].message.content)for t in data.get(”triplets”, []):G.add_edge(t[”subject”], t[”object”], relation=t[”relation”])# 存储原始文本作为节点属性(简化版)G.nodes[t[”subject”]][”source”] = docG.nodes[t[”object”]][”source”] = docprint(f”图谱构建完成:{G.number_of_nodes()} 节点,{G.number_of_edges()} 边”)# ===== 2. 图检索 =====def graph_retrieve(entity, max_hops=2):”””从指定实体出发,做多跳遍历”””visited = set()queue =subgraph_nodes = set()while queue:node, depth = queue.pop(0)if depth > max_hops or node in visited:continuevisited.add(node)subgraph_nodes.add(node)for neighbor in G.neighbors(node):queue.append((neighbor, depth + 1))return G.subgraph(subgraph_nodes)# ===== 3. 增强生成 =====def graph_answer(question, start_entity):sub = graph_retrieve(start_entity)# 将子图转为文本描述edges_desc = ”\n”.join(f”- {u} --[{d['relation']}]--> {v}”for u, v, d in sub.edges(data=True))sources = ”\n”.join(set(sub.nodes[n].get(”source”, ””) for n in sub.nodes))prompt = f”””基于以下知识图谱信息和原始文档回答问题。## 知识图谱{edges_desc}## 原始文档{sources}## 问题{question}请标注信息来源。”””resp = client.chat.completions.create(model=”gpt-4o-mini”,messages=[{”role”: ”user”, ”content”: prompt}])return resp.choices[0].message.content# 测试print(graph_answer(”张三和李四是什么关系?他们共同参与了什么?”, ”张三”))# 预期:张三和李四是上下级关系(李四向张三汇报)。# 张三负责AI平台项目,李四作为研发部工程师可能参与该项目。
🎉 这 60 行代码就是 GraphRAG 的最小可行实现。万悟在此基础上加了:实体对齐、属性抽取、图数据库持久化、增量更新、检索缓存、Prompt 工程优化……但核心逻辑就是你看到的:抽取 → 建图 → 遍历 → 生成。
六、总结 & 延伸阅读
本文要点回顾
✅ GraphRAG = 知识图谱 + RAG,解决多跳推理和跨文档关联问题 ✅ 三阶段:LLM 抽取三元组 → 构建图数据库 → 图上多跳检索 ✅ 抽取 Prompt 质量决定图谱质量,需针对领域调优 ✅ 万悟采用混合模式:传统 RAG 兜底 + GraphRAG 增强 ✅ GraphRAG 不是万能药,仅在关系密集型问题上优于传统 RAG
课后作业
准备一份关系密集型文档,在万悟中构建知识图谱 完成传统 RAG vs GraphRAG vs 混合模式的对比实验 可视化知识图谱,截图保存 跑通 Mini GraphRAG,理解三阶段核心流程 尝试修改抽取 Prompt,观察图谱质量变化
下一篇预告
第 5 篇:Agent 是怎么"思考"的?万悟 ReAct 推理引擎源码拆解
我们将深入智能体模块,看看 Agent 如何实现"思考→行动→观察"的循环,以及工具调用是如何安全执行的。
参考资源
元景万悟 GitHub:https://github.com/UnicomAI/wanwu Microsoft GraphRAG 论文:From Local to Global: A Graph RAG Approach to Query-Focused Summarization Neo4j 官方教程:https://neo4j.com/docs/ NetworkX 文档:https://networkx.org/documentation/stable/
📮 点赞 / 收藏 / 关注,不错过后续更新
🔔 下一篇:《Agent 是怎么"思考"的?万悟 ReAct 推理引擎源码拆解》
夜雨聆风