手刻 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 抽取準確率如何?
經驗上:
- 技能抽取:~90% 準確
- 關係抽取:~80% 準確
- 用
min_confidence=0.7過濾低信心結果
3. skill-graph.json 會無限增長嗎?
不會。只有 type="skill" 的節點會同步回去,且會去重。
實際使用中,技能數量有限(通常 <200)。
總結
| 元件 | 技術 | 功能 |
|---|---|---|
| 圖結構 | NetworkX | 儲存技能關係 |
| 實體抽取 | LangChain + Pydantic | 從素材抽取實體 |
| 向量搜尋 | Voyage-4 1024維 | 種子檢索 |
| 自動同步 | Python | 保持 JSON 更新 |
手刻 GraphRAG 讓你完全掌控架構,同時享受圖結構帶來的間接關聯發現能力。
Career Knowledge Base 是一個本地優先的履歷知識庫系統,使用 Python + LanceDB + NetworkX + LangChain 建構。