Ian Chou's Blog

手刻 GraphRAG:用 NetworkX + LangChain 建構知識圖譜增強檢索

手刻 GraphRAG:用 NetworkX + LangChain 建構知識圖譜增強檢索

當純向量搜尋找不到間接相關的經驗時,圖結構能幫你找到隱藏的連結。本文分享如何用 NetworkX 圖結構 + LangChain 實體抽取 + Voyage-4 1024 維向量,建構輕量級的 GraphRAG 系統。

什麼是 GraphRAG

傳統 RAG 只用向量相似度找資料:

Query: "LangChain 經驗"
      ↓ 向量搜尋
Result: 只返回明確提到 "LangChain" 的素材

GraphRAG 多一層圖遍歷

Query: "LangChain 經驗"
      ↓ 向量搜尋 (種子檢索)
Seed: ["LangChain 專案"]
      ↓ 圖遍歷
Expand: Python → RAG → Prompt Engineering
      ↓ 擴展檢索
Result: 包含所有相關技能的素材

為什麼不用現成的 GraphRAG 框架

方案 優缺點
Microsoft GraphRAG 功能強大,但依賴重、成本高
LlamaIndex PropertyGraph 較輕量,但還是有學習成本
手刻版 輕量、可控、完全理解架構

對於履歷知識庫這種小規模應用,手刻版更適合。

架構設計

┌─────────────────────────────────────────────────────────────┐
│                    GraphRAG Query Flow                       │
└─────────────────────────────────────────────────────────────┘
          Query: "會 LangChain 的候選人有什麼經驗?"
                              ↓
┌─────────────────────────────────────────────────────────────┐
│  Layer 1: Seed Retrieval (向量檢索)                          │
│  ┌─────────────┐                                            │
│  │  LanceDB    │ ← Voyage-4 (1024維)                        │
│  │  embedding  │ → 找到 Top-5 相關素材                       │
│  └─────────────┘ → 抽取 seed skills: [LangChain, RAG]       │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│  Layer 2: Graph Expansion (圖遍歷)                           │
│  ┌─────────────┐                                            │
│  │  NetworkX   │ ← 從 skill-graph.json 建立                 │
│  │  DiGraph    │ → 2-hop 遍歷                               │
│  └─────────────┘ → LangChain implies Python, LLM            │
│                  → LangChain relatedTo RAG, AI Agent        │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│  Layer 3: Expanded Retrieval (擴展檢索)                      │
│  用 expanded skills 回 LanceDB 找更多素材                    │
│  合併、排序、返回 Top-K 結果                                  │
└─────────────────────────────────────────────────────────────┘

模組結構

py-kb/src/career_kb/graph/
├── __init__.py
├── knowledge_graph.py    # NetworkX 圖封裝
├── entity_extractor.py   # LLM 實體抽取
├── sync.py               # 自動同步到 skill-graph.json
├── traversal.py          # 圖遍歷策略
└── hybrid_retrieval.py   # 向量 + 圖混合檢索

NetworkX 知識圖譜

節點類型

# 技能節點
G.add_node("Python", type="skill", category="language")
G.add_node("LangChain", type="skill", category="framework")

# 其他節點類型
G.add_node("RAG 產品開發", type="project")
G.add_node("Ericsson", type="company")

邊類型(關係)

# implies: 會 A 技術表示也會 B
G.add_edge("LangChain", "Python", relation="implies")
G.add_edge("Kubernetes", "Docker", relation="implies")

# relatedTo: 概念相關(雙向)
G.add_edge("LangChain", "RAG", relation="relatedTo")
G.add_edge("Machine Learning", "Data Science", relation="relatedTo")

載入 skill-graph.json

class CareerKnowledgeGraph:
    def load_from_skill_graph(self) -> int:
        data = json.loads(self.skill_graph_path.read_text())
        
        for skill_name, props in data["skills"].items():
            # 添加節點
            self.graph.add_node(
                skill_name,
                type="skill",
                category=props.get("category"),
            )
            
            # 添加 implies 邊
            for implied in props.get("implies", []):
                self.graph.add_edge(skill_name, implied, relation="implies")
            
            # 添加 relatedTo 邊(雙向)
            for related in props.get("relatedTo", []):
                self.graph.add_edge(skill_name, related, relation="relatedTo")
                self.graph.add_edge(related, skill_name, relation="relatedTo")

LLM 實體抽取

Pydantic 結構化輸出

class EntityType(str, Enum):
    SKILL = "skill"
    COMPANY = "company"
    PROJECT = "project"
    TOOL = "tool"

class RelationType(str, Enum):
    USES = "uses"           # 專案使用技能
    IMPLIES = "implies"     # 技能 A 蘊含 B
    RELATED_TO = "relatedTo"

class EntityRelation(BaseModel):
    source: str
    target: str
    relation: RelationType
    confidence: float = Field(ge=0.0, le=1.0)

LangChain 抽取

def extract_entities_from_text(text: str) -> ExtractionResult:
    llm = get_langchain_model()
    structured_llm = llm.with_structured_output(ExtractionResult)
    
    prompt = f"""Extract entities and relations from:
    
    {text}
    
    Entity types: skill, company, project, tool
    Relation types: uses, implies, relatedTo
    """
    
    return structured_llm.invoke(prompt)

自動同步到 skill-graph.json

每次抽取新關係後,自動寫回 JSON:

def sync_to_skill_graph(graph: CareerKnowledgeGraph):
    skill_graph = load_skill_graph()
    
    for skill_name in graph.get_nodes_by_type("skill"):
        if skill_name not in skill_graph["skills"]:
            # 新技能:添加
            skill_graph["skills"][skill_name] = {
                "category": "technique",
                "implies": [],
                "relatedTo": [],
            }
        
        # 同步新關係
        for _, target, data in graph.graph.out_edges(skill_name, data=True):
            if data["relation"] == "implies":
                if target not in skill_graph["skills"][skill_name]["implies"]:
                    skill_graph["skills"][skill_name]["implies"].append(target)
    
    save_skill_graph(skill_graph)

圖遍歷策略

BFS 擴展

def graph_expansion(graph, seed_nodes, hops=2, max_nodes=20):
    visited = set()
    queue = deque()
    
    for node in seed_nodes:
        queue.append((node, 0))
        visited.add(node)
    
    result = []
    while queue and len(result) < max_nodes:
        node, depth = queue.popleft()
        result.append(node)
        
        if depth >= hops:
            continue
        
        for _, neighbor, _ in graph.out_edges(node, data=True):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, depth + 1))
    
    return result

加權擴展

不同關係類型有不同權重:

RELATION_WEIGHTS = {
    "implies": 1.0,     # 最強:會 LangChain 一定會 Python
    "relatedTo": 0.7,   # 中等:概念相關
    "uses": 0.5,        # 較弱:專案使用
}

混合檢索

def hybrid_search(query: str, graph: CareerKnowledgeGraph):
    # Step 1: 向量檢索種子
    seed_materials = seed_retrieval(query, top_k=5)
    
    # Step 2: 抽取種子技能
    seed_skills = extract_skills_from_materials(seed_materials)
    
    # Step 3: 圖遍歷擴展
    expanded_skills = weighted_expansion(graph, seed_skills, hops=2)
    
    # Step 4: 用擴展技能再檢索
    for skill in expanded_skills:
        secondary_materials.extend(seed_retrieval(skill, top_k=2))
    
    # Step 5: 合併去重
    return merge_and_rank(seed_materials + secondary_materials)

CLI 使用

# 顯示圖譜統計
uv run career-kb graph show

# 輸出:
#      Knowledge Graph Statistics
# ┏━━━━━━━━━━━━━━━┳━━━━━━━┓
# ┃ Total Nodes   ┃ 93    ┃
# ┃ Total Edges   ┃ 195   ┃
# ┃   Skill Nodes ┃ 29    ┃
# ┃ Relations     ┃       ┃
# ┃   relatedTo   ┃ 150   ┃
# ┃   implies     ┃ 45    ┃
# └───────────────┴───────┘
# GraphRAG 混合搜尋
uv run career-kb graph query "LangChain RAG experience"

# 輸出:
# Seed Skills: LangChain, RAG
# Expanded Skills: Python, LLM, Prompt Engineering
# Retrieved Materials: (5 results with graph context)
# 從素材建構圖譜
uv run career-kb graph build

# 輸出:
# ✓ Extracted 45 unique entities
# ✓ Found 23 relations
# Auto-synced to skill-graph.json

Embedding 維度升級

為了更好的語意精度,從 512 維升級到 1024 維:

# embedding.py
MODEL_NAME = "voyage-4"
DIMENSIONS = 1024  # Upgraded from 512

遷移步驟

# 1. 匯出現有資料
python export_resume_chunks.py  # → materials/exported/

# 2. 備份舊資料庫
mv data/lancedb data/lancedb_backup_512

# 3. 重新 ingest(1024 維)
uv run career-kb ingest ...

# 4. 驗證
uv run career-kb db-info
# Table: resume_chunks, 164 rows

與現有系統整合

┌─────────────────────────────────────────────────────────────┐
│                  Career KB Architecture                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  skill-graph.json ◄────────────────┐                        │
│        ↓                           │                        │
│  [NetworkX Graph] ─── 自動同步 ────┘                        │
│        ↓                                                    │
│  GraphRAG Query ─────────────────────────────────┐          │
│                                                  ↓          │
│  NLI Verification ◄── Hybrid Search ◄── LanceDB (1024維)   │
│        ↓                                                    │
│  Resume Generation                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

效能考量

操作 時間 備註
Graph Load ~50ms 從 JSON 載入 93 nodes
2-hop Expansion ~5ms NetworkX 原生操作
Hybrid Search ~2s 向量 + 圖 + 二次檢索
Entity Extraction ~3s/file LLM 呼叫

常見問題

1. 為什麼用 NetworkX 而非專門圖資料庫?

方案 優點 缺點
Neo4j 強大查詢語言 需要額外服務
ArangoDB 多模型 部署複雜
NetworkX 純 Python、零部署 大規模效能差

對於技能圖譜(<1000 nodes),NetworkX 完全足夠。

2. LLM 抽取準確率如何?

經驗上:

3. skill-graph.json 會無限增長嗎?

不會。只有 type="skill" 的節點會同步回去,且會去重。
實際使用中,技能數量有限(通常 <200)。

總結

元件 技術 功能
圖結構 NetworkX 儲存技能關係
實體抽取 LangChain + Pydantic 從素材抽取實體
向量搜尋 Voyage-4 1024維 種子檢索
自動同步 Python 保持 JSON 更新

手刻 GraphRAG 讓你完全掌控架構,同時享受圖結構帶來的間接關聯發現能力。


Career Knowledge Base 是一個本地優先的履歷知識庫系統,使用 Python + LanceDB + NetworkX + LangChain 建構。