Ian Chou's Blog

Ch1:為什麼你的 RAG 需要 Skill Entity Graph?

Ch1:為什麼你的 RAG 需要 Skill Entity Graph?

課程系列Entity Embedding Graph RAG 迷你課完整藍圖Ch1(本篇)

目標:10 分鐘內讓你直覺理解「為什麼不能只做 vector search,也不該手寫 skill-graph.json」。不講理論,直接看 demo 差異。


問題場景:Career Knowledge Base

假設你建了一套 Career Knowledge Base——一個幫助工程師規劃學習路徑的 RAG 系統。知識庫裡有 200 篇技術文章(chunks),涵蓋 Python、Node.js、LLM API、browser automation、chat app 整合等各種技能。

使用者會問這樣的問題:

這些問題有一個共通點:答案不只在一篇文章裡,而是散落在多個語意相關的素材中

什麼是 OpenClaw? OpenClaw 是一個跑在本機的個人 AI 助理——用 Node.js 驅動,接 Anthropic / OpenAI 等 LLM API,可以幫你管理日曆、發 email、瀏覽網頁、執行 shell 指令。透過 WhatsApp、Telegram、Discord 等聊天 app 互動,還有社群開發的 Skills & Plugins 擴充系統。

讓我們看看三種不同的檢索方式如何應對。


最直覺的做法——把 query 做 embedding,去 LanceDB 找最相似的 chunks:

import lancedb

db = lancedb.connect("data/career.lance")
chunks_table = db.open_table("chunks")

def vector_search(query, model, top_k=5):
    """純 vector search:query embedding → 找最相似的 chunks。"""
    query_vec = model.encode([query])[0]
    results = chunks_table.search(query_vec).limit(top_k).to_list()
    return results

Demo:「學 OpenClaw 之前需要先會什麼?」

results = vector_search("學 OpenClaw 之前需要先會什麼?", model)

for r in results:
    print(f"  [{r['_distance']:.3f}] {r['title'][:60]}")
結果:
  [0.185] OpenClaw 安裝與設定:從零開始部署你的 AI 助理
  [0.198] 打造自訂 OpenClaw Skills:Plugin 開發指南
  [0.237] LangChain Agent 架構設計與實作
  [0.261] Telegram Bot API 整合入門
  [0.294] Python asyncio 深入理解

問題出在哪?

表面上看結果不錯——確實找到了 OpenClaw 相關的文章。但仔細看:

  1. 缺乏結構:結果只是「按相似度排序的 5 篇文章」,沒有告訴你前置技能的「先後關係」
  2. 只看字面相似:排第一的是 OpenClaw 安裝指南(因為直接提到 OpenClaw),但使用者問的是「先學什麼」——Node.js 基礎和 LLM API 概念才是前置技能,卻被擠到後面或根本沒出現
  3. 漏掉隱含需求:學 OpenClaw 之前需要懂的 browser automation(它的核心功能之一:網頁瀏覽、表單填寫、資料擷取)和 shell 指令基礎(它有 full system access)完全沒出現,因為那些文章在向量空間裡跟 "OpenClaw" 不夠近

純 vector search 擅長「找到字面上相關的東西」,但不擅長「理解事物之間的結構關係」。


方式二:Keyword Graph(手動維護 skill-graph.json)

為了解決結構問題,你可能會建一個手寫的技能關係圖:

{
  "nodes": ["OpenClaw", "Node.js", "LLM API", "Telegram Bot", 
            "Browser Automation", "Shell Scripting", "Python"],
  "edges": [
    {"from": "OpenClaw", "to": "Node.js", "type": "requires"},
    {"from": "OpenClaw", "to": "LLM API", "type": "requires"},
    {"from": "OpenClaw", "to": "Browser Automation", "type": "requires"},
    {"from": "OpenClaw", "to": "Shell Scripting", "type": "requires"},
    {"from": "OpenClaw", "to": "Telegram Bot", "type": "relatedTo"},
    {"from": "Telegram Bot", "to": "Node.js", "type": "requires"}
  ]
}

用 NetworkX 載入後做 graph traversal:

import networkx as nx
import json

# 載入手動維護的 skill graph
with open("data/skill-graph.json") as f:
    data = json.load(f)

G = nx.DiGraph()
for edge in data["edges"]:
    G.add_edge(edge["from"], edge["to"], type=edge["type"])

def keyword_graph_search(skill_name, graph, depth=2):
    """沿著手寫的 keyword graph 找到前置技能。"""
    if skill_name not in graph:
        return []
    
    # 找所有 depth 步以內的鄰居
    predecessors = {}
    for node, d in nx.single_source_shortest_path_length(
        graph, skill_name, cutoff=depth
    ).items():
        if node != skill_name:
            predecessors[node] = d
    
    return sorted(predecessors.items(), key=lambda x: x[1])

Demo:同一個 query

prereqs = keyword_graph_search("OpenClaw", G)

for skill, hops in prereqs:
    print(f"  [{hops} hop] {skill}")
結果:
  [1 hop] Node.js
  [1 hop] LLM API
  [1 hop] Browser Automation
  [1 hop] Shell Scripting
  [1 hop] Telegram Bot
  [2 hop] Node.js(透過 Telegram Bot)

好多了! 現在我們知道 OpenClaw 的前置技能包含 Node.js、LLM API、Browser Automation、Shell Scripting,而且有結構關係。

但 Keyword Graph 的三個致命問題

問題 1:人工維護不 scale

你有 200 個 skill,理論上有 200 × 199 = 39,800 種可能的關係。
你真的要一條一條手寫?

每次加一個新技能(比如 "Hono"),你得想:它跟 200 個 existing skills 中的哪些有關?然後手動加邊。這不可能持續。

問題 2:關係是主觀的

"OpenClaw" --requires--> "Node.js"      ← 你覺得是前置
"OpenClaw" --relatedTo--> "LangChain"   ← 他覺得是平行
"OpenClaw" --implies--> "AI Agent"      ← 她覺得是蘊含

不同人定義出來的圖完全不同,沒有「客觀標準」。

問題 3:只有結構,沒有語意

Keyword graph 是靠字串匹配的——它知道 "OpenClaw" 和 "Node.js" 有關係,但不知道 "browser control"、"Puppeteer"、"Playwright" 都是在講同一類技術,也不知道 OpenClaw 的 Skill 系統跟 LangChain 的 Tool 概念有多接近。


方式三:Entity Embedding Graph(本課程教的方法)

核心想法:不用手寫關係,讓 embedding 的語意相似度自動發現技能之間的連結。

from sentence_transformers import SentenceTransformer
import lancedb
import networkx as nx

model = SentenceTransformer("BAAI/bge-m3")

# ① 對所有 skill 做 embedding
skills = ["OpenClaw", "Node.js", "LLM API", "Anthropic Claude", "OpenAI API",
          "Telegram Bot", "Browser Automation", "Puppeteer", "Playwright",
          "Shell Scripting", "LangChain", "AI Agent", "FastAPI", "Hono"]

vectors = model.encode(skills)

# ② 存進 LanceDB
db = lancedb.connect("data/skills.lance")
skill_table = db.create_table("skills", [
    {"name": s, "vector": v.tolist()} 
    for s, v in zip(skills, vectors)
], mode="overwrite")

# ③ 用相似度自動建圖(零手動!)
def build_entity_graph(table, top_k=5, threshold=0.3):
    """用 embedding cosine similarity 自動建立 skill 之間的語意邊。"""
    skills_data = table.to_pandas()
    graph = nx.Graph()
    
    for _, row in skills_data.iterrows():
        graph.add_node(row["name"])
    
    for _, row in skills_data.iterrows():
        results = table.search(row["vector"]).limit(top_k + 1).to_list()
        for r in results:
            if r["name"] != row["name"] and r["_distance"] < threshold:
                similarity = 1 - r["_distance"]
                graph.add_edge(row["name"], r["name"], weight=round(similarity, 3))
    
    return graph

skill_graph = build_entity_graph(skill_table)

自動發現的語意邊

不用手寫,embedding 自動產生這樣的圖:

自動產生的邊(基於 cosine similarity):
  OpenClaw ─────0.83──> AI Agent           ← 都是 AI 助理
  OpenClaw ─────0.78──> LangChain          ← Agent 框架生態
  OpenClaw ─────0.74──> Telegram Bot       ← 聊天 app 整合
  AI Agent ─────0.85──> LangChain          ← Agent 建構
  AI Agent ─────0.77──> LLM API            ← 底層能力
  LLM API ──────0.88──> OpenAI API         ← 同一類
  LLM API ──────0.86──> Anthropic Claude   ← 同一類
  Browser Automation ──0.82──> Puppeteer   ← 同領域工具
  Browser Automation ──0.80──> Playwright  ← 同領域工具
  FastAPI ──────0.71──> Hono               ← 都是輕量 Web 框架!

注意到了嗎?OpenClaw 和 LangChain 被自動連在一起——它們都是 AI Agent 框架生態的一部分。Browser Automation 自動跟 Puppeteer、Playwright 連上——因為語意空間知道它們是同一類東西。這些關係你手動寫 skill-graph.json 不一定會全部想到。

Demo:同一個 query,用 Entity Graph

def entity_graph_search(query, model, skill_table, skill_graph, 
                        chunks_table, top_k=5, hops=2):
    """
    Entity Embedding Graph 檢索流程:
    Query → Seed Skills → Graph Expansion → Guided Chunk Retrieval
    """
    query_vec = model.encode([query])[0]
    
    # Step 1: 找 seed skills(entity-level)
    seed_results = skill_table.search(query_vec).limit(3).to_list()
    seed_skills = {r["name"] for r in seed_results}
    print(f"  Seed skills: {seed_skills}")
    
    # Step 2: 沿 graph 擴展 N hop
    expanded = set()
    for skill in seed_skills:
        if skill in skill_graph:
            neighbors = nx.single_source_shortest_path_length(
                skill_graph, skill, cutoff=hops
            )
            expanded.update(neighbors.keys())
    
    all_skills = seed_skills | expanded
    print(f"  Expanded skills (+{len(expanded - seed_skills)}): {expanded - seed_skills}")
    
    # Step 3: 用 skill set 導航 chunk 檢索
    chunks = chunks_table.search(query_vec).limit(top_k * 3).to_list()
    guided = [c for c in chunks 
              if any(s.lower() in c["text"].lower() for s in all_skills)][:top_k]
    
    return guided
results = entity_graph_search(
    "學 OpenClaw 之前需要先會什麼?", 
    model, skill_table, skill_graph, chunks_table
)
  Seed skills: {'OpenClaw', 'AI Agent', 'LangChain'}
  Expanded skills (+5): {'LLM API', 'Node.js', 'Telegram Bot', 
                          'Browser Automation', 'Anthropic Claude'}

結果:
  [0.185] OpenClaw 安裝與設定:從零開始部署你的 AI 助理
  [0.221] Node.js 非同步程式設計:Event Loop 與 Promise 深入理解
  [0.245] LLM API 整合實戰:Anthropic Claude 與 OpenAI 呼叫模式
  [0.267] Browser Automation 入門:Puppeteer 網頁操控與資料擷取
  [0.278] Telegram Bot 開發:從 BotFather 到自動化工作流

差異一目了然


Aha Moment:跨越未知的未知

上面的例子已經不錯,但還不夠震撼。真正讓你看出 Graph 威力的,是 Unknown Unknowns 場景——使用者不知道自己不知道什麼。

情境設定

知識庫裡有三篇文章:

注意:Chunk C 完全沒提到 Python 或 FastAPI

使用者的 Query

「我平常用 Python 寫 FastAPI,現在想在 JavaScript/Edge 生態找一個類似的輕量、高效能 API 框架,有什麼學習資源?」

純 Vector Search 的結果

results = vector_search(
    "我平常用 Python 寫 FastAPI,想找 JavaScript Edge 類似的輕量高效能 API 框架",
    model
)
  [0.201] FastAPI 是一個基於 Python 的現代 Web 框架...     ← Chunk A ✓ 但使用者已經知道了
  [0.234] Node.js 伺服器效能調校與架構優化指南...          ← Chunk B △ 沾邊但不對
  [0.267] Express.js 路由設計與中間件...                   ← 老框架,不是使用者要的
  [0.289] Python asyncio 深入理解...                       ← 完全無關
  [0.301] Django REST Framework API 設計...                ← 還是 Python,沒幫助

Chunk C(Hono 實戰)完全沒出現。為什麼?因為 Hono 那篇文章的文字沒有提到 Python、FastAPI——在向量空間裡,它跟 query 的距離太遠了。

Entity Embedding Graph 的結果

results = entity_graph_search(
    "我平常用 Python 寫 FastAPI,想找 JavaScript Edge 類似的輕量高效能 API 框架",
    model, skill_table, skill_graph, chunks_table
)
  Seed skills: {'FastAPI', 'Python', 'Edge Runtime'}
  Expanded skills (+3): {'Hono', 'Node.js', 'OpenClaw'}
                          ^^^^
                          Graph 自動發現:FastAPI → Hono(都是輕量 Web 框架)

結果:
  [0.201] FastAPI 是一個基於 Python 的現代 Web 框架...     ← Chunk A
  [0.234] Node.js 伺服器效能調校...                        ← Chunk B
  [0.245] Hono 實戰:為 Edge Runtime 設計的極速微型框架... ← Chunk C 🎯 找到了!

Chunk C 出現了! Graph 做了一件 vector search 做不到的事——它沿著語意邊從 FastAPI 走到 Hono:

FastAPI ──0.71──> Hono(都是輕量 Web 框架)
                      ↓
            skill "Hono" 裡的 chunks 被撈出來
                      ↓
            Chunk C:Hono 實戰 🎯

系統主動搭了一座橋,把使用者「不知道關鍵字是什麼」的技術精準地遞到他面前。

Graph 不是取代向量搜尋,而是向量搜尋的「導航圖」。


三種方式的完整對比

面向 純 Vector Search Keyword Graph Entity Embedding Graph
建構成本 零(只需 embedding) 高(手動維護 JSON) 低(~40 行自動建圖)
關係發現 ❌ 無結構 ✅ 有但靠人定義 ✅ 自動語意發現
新技能加入 自動 需手動加邊 自動連到相似技能
Unknown Unknowns ❌ 找不到 △ 看你有沒有加邊 ✅ 自動跨越
需要新套件
可維護性 低(邊越多越難管) 高(重新 embed 即可)

核心概念一句話

用語意相似度取代關鍵字匹配來建構圖的邊。

不是手動定義 "OpenClaw" --requires--> "Node.js",而是讓 embedding 模型告訴你:cosine_similarity(embed("OpenClaw"), embed("AI Agent")) = 0.83,自動建邊。

你現有的 LanceDB + NetworkX 已經完全夠用。零新依賴,~40 行 Python 就能實現。


這門課接下來教什麼?

章節 標題 你會學到
Ch1 為什麼需要 Skill Entity Graph?(本篇) 三種方式的差異 + Aha Moment
Ch2 Embedding 夠用的基礎 sentence-transformers、cosine similarity、模型選擇
Ch3 Skill List → Entity Table 用 LanceDB 存 skill embeddings
Ch4 自動建 Graph(核心) kNN → NetworkX,threshold 調參
Ch5 接回 Hybrid RAG Pipeline Chunk Enrichment + Graph-guided Retrieval
Ch6 Bonus: Learning Path 推薦 PageRank、最短路推薦下一步技能

動手試試:Ch1 作業

用你自己的知識庫(或用以下 mock data),跑一個 query,觀察 vector search 漏掉了什麼:

from sentence_transformers import SentenceTransformer
import lancedb

model = SentenceTransformer("BAAI/bge-m3")

# Mock 知識庫
chunks = [
    {"id": 1, "title": "OpenClaw 安裝與設定指南", 
     "text": "OpenClaw 是跑在本機的個人 AI 助理,用 Node.js 驅動,接 Anthropic 或 OpenAI API。支援 WhatsApp、Telegram 等聊天 app 互動。"},
    {"id": 2, "title": "Node.js Event Loop 深入理解", 
     "text": "Node.js 事件迴圈機制:libuv、非阻塞 I/O、Promise 與 async/await 的執行順序。"},
    {"id": 3, "title": "Puppeteer 網頁自動化實戰", 
     "text": "Puppeteer 是 Google 開發的 headless Chrome 控制工具,可以自動瀏覽網頁、填表單、截圖、擷取資料。"},
    {"id": 4, "title": "LLM API 整合入門", 
     "text": "如何呼叫 OpenAI 和 Anthropic Claude API:token 管理、streaming response、function calling。"},
    {"id": 5, "title": "FastAPI 高併發框架入門", 
     "text": "FastAPI 是基於 Python 的現代 Web 框架,使用 Pydantic 進行資料驗證,支援 async/await 高併發。"},
    {"id": 6, "title": "Hono 實戰指南", 
     "text": "Hono 是為 Edge Runtime 設計的超輕量 Web 框架,支援 Cloudflare Workers、Deno Deploy。極速路由、零依賴。"},
]

# Embed 並存入 LanceDB
vectors = model.encode([c["text"] for c in chunks])
for c, v in zip(chunks, vectors):
    c["vector"] = v.tolist()

db = lancedb.connect("/tmp/ch1_demo")
table = db.create_table("chunks", chunks, mode="overwrite")

# 查詢
query = "學 OpenClaw 之前需要先會什麼?"
query_vec = model.encode([query])[0]
results = table.search(query_vec).limit(5).to_list()

print("=== 純 Vector Search 結果 ===")
for r in results:
    print(f"  [{r['_distance']:.3f}] {r['title']}")

思考:Node.js、Puppeteer(browser automation)、LLM API 這些前置技能有出現在 top 5 嗎?排名第幾?

作業 2:手動算 Skill Similarity

# 計算幾組 skill 的 cosine similarity
skill_pairs = [
    ("OpenClaw", "AI Agent"),          # 同類產品
    ("OpenClaw", "LangChain"),         # Agent 生態
    ("OpenClaw", "Photoshop"),         # 完全無關
    ("Browser Automation", "Puppeteer"),  # 同領域工具
    ("FastAPI", "Hono"),               # 不同語言的輕量框架
    ("LLM API", "Anthropic Claude"),   # 上下位概念
]

import numpy as np

for s1, s2 in skill_pairs:
    v1 = model.encode([s1])[0]
    v2 = model.encode([s2])[0]
    sim = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
    print(f"  {s1}{s2}: {sim:.3f}")

思考:哪些 pair 的 similarity > 0.7?這些自然就會變成 entity graph 上的邊。


本章重點回顧

  1. 純 Vector Search 擅長「找到字面相關」,但缺乏結構,容易漏掉語意相關但字面不同的素材
  2. Keyword Graph(手動 JSON)有結構,但不 scale——200 個 skill 就要手動維護上千條關係
  3. Entity Embedding Graph 是兩者的最佳折衷:
    • 自動建邊(零手動維護)
    • 有語意結構(支援 multi-hop traversal)
    • 能發現 Unknown Unknowns(FastAPI → Hono)
    • 零新依賴(LanceDB + NetworkX 就夠)

下一章Ch2 — Embedding 夠用的基礎,我們會深入了解 embedding model 如何選擇、similarity 如何衡量,為 Ch4 的自動建圖打好地基。


延伸閱讀


Entity Embedding Graph RAG 迷你課是一個 project-based 教學系列,使用 Python + LanceDB + NetworkX 零新依賴打造帶語意圖的 Career RAG 系統。