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 來看看三種不同的檢索方式如何應對。


最直覺的做法——把 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:「我想做一個 Telegram 聊天機器人,能接 AI 自動回覆」

results = vector_search(
    "我想做一個 Telegram 聊天機器人,能接 AI 自動回覆,有什麼技術可以參考?",
    model
)

for r in results:
    print(f"  [{r['_distance']:.3f}] {r['title'][:60]}")
結果:
  [0.750] Telegram Bot API 整合入門:從建立到部署
  [0.762] 從需求推導 Telegram Bot:別先選工具
  [0.794] Telegram Bot 踩坑紀錄:高階架構設計專案中的血淚教訓
  [0.816] 從 HTTP 到 Telegram Bot:升級你的 Web 工具箱
  [0.861] LLM API 整合入門:OpenAI 與 Anthropic Claude 實戰

看起來不錯?仔細看有什麼問題:

  1. Top 4 全是 Telegram Bot 文章 — 找到了「聊天機器人」但完全沒展開到更深的技術棧
  2. LLM API 勉強擠進第 5 — 至少沾到「AI 回覆」,但排名墊底
  3. 完全漏掉的關鍵素材
    • OpenClaw — 它就是一個「Telegram 聊天機器人 + AI 回覆」的完整參考實作!但文章裡描述為「個人 AI 助理」,跟 query 的用詞完全不同
    • Agent Architecture — 如果要做「AI 自動回覆」,你需要的不是簡單的 API call,而是一個完整的 Agent 架構(ReAct loop、state management)
    • Tool Calling — AI 不只回覆文字,還要「呼叫工具」來查資料、執行動作
    • Node.js — Telegram Bot 和 OpenClaw 的執行環境,但沒有任何一篇 Node.js 文章被拉進來

Vector search 找到了字面上最相似的文章,但漏掉了使用者真正需要但不知道怎麼描述的技術。


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

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

{
  "nodes": ["Telegram Bot", "Discord Bot", "Node.js", "LLM API",
            "Agent Architecture", "OpenClaw", "Tool Calling", "Python"],
  "edges": [
    {"from": "Telegram Bot", "to": "Node.js", "type": "requires"},
    {"from": "Telegram Bot", "to": "OpenClaw", "type": "relatedTo"},
    {"from": "OpenClaw", "to": "LLM API", "type": "requires"},
    {"from": "OpenClaw", "to": "Agent Architecture", "type": "relatedTo"},
    {"from": "Agent Architecture", "to": "Tool Calling", "type": "requires"},
    {"from": "Agent Architecture", "to": "LLM API", "type": "requires"}
  ]
}

用 NetworkX 載入後做 graph traversal:

import networkx as nx
import json

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 []
    
    neighbors = {}
    for node, d in nx.single_source_shortest_path_length(
        graph, skill_name, cutoff=depth
    ).items():
        if node != skill_name:
            neighbors[node] = d
    
    return sorted(neighbors.items(), key=lambda x: x[1])

Demo:從 "Telegram Bot" 出發擴展

related = keyword_graph_search("Telegram Bot", G)

for skill, hops in related:
    print(f"  [{hops} hop] {skill}")
結果:
  [1 hop] Node.js(requires)
  [1 hop] OpenClaw(relatedTo)
  [2 hop] LLM API
  [2 hop] Browser Automation
  [2 hop] Shell Scripting
  [2 hop] Agent Architecture

結構看起來不錯! OpenClaw 被拉進來了,而且透過 OpenClaw 又連到了 Agent Architecture、LLM API、Browser Automation。

但實際拿這些 skill 去導航 chunk 檢索,結果是什麼?

結果(Keyword Graph-Guided):
  [0.750] Telegram Bot API 整合入門:從建立到部署        ← via Telegram Bot, Node.js
  [0.762] 從需求推導 Telegram Bot:別先選工具             ← via Telegram Bot
  [0.794] Telegram Bot 踩坑紀錄:高階架構設計專案中的血淚教訓 ← via Telegram Bot
  [0.816] 從 HTTP 到 Telegram Bot:升級你的 Web 工具箱   ← via Telegram Bot
  [0.861] LLM API 整合入門:OpenAI 與 Anthropic Claude 實戰 ← via LLM API

跟純 Vector Search 的結果一模一樣。 為什麼?雖然 graph 擴展出了 OpenClaw、Agent Architecture 等技能,但 keyword graph 靠的是字串匹配——知識庫的文章裡如果沒有精確出現那些關鍵字,就撈不到對應的 chunks。

但 Keyword Graph 的三個致命問題

問題 1:人工維護不 scale

知識庫有 81 個 skill,但你的 graph 只覆蓋了 18 個(22%)。
18 個節點的理論最大邊數 = 306,你只手寫了 21 條(6.9%)。
剩下的 63 個 skill?全是盲區。

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

問題 2:關係是主觀的

"Telegram Bot" --requires--> "Node.js"           ← 你覺得是前置
"Telegram Bot" --relatedTo--> "OpenClaw"          ← 他覺得是平行
"Telegram Bot" --suggests-->  "Agent Architecture" ← 她覺得是建議

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

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

Keyword graph 是靠字串匹配的——它知道 "Telegram Bot" 和 "OpenClaw" 有關係,但不知道 "個人 AI 助理"、"聊天機器人"、"chat app 整合" 都是在講同一類技術,也不知道 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",
          "Telegram Bot", "Browser Automation", "Puppeteer", "Playwright",
          "Shell Scripting", "LangChain", "Agent Architecture", "FastAPI", "Hono",
          "Tool Calling", "Discord Bot", "Edge Runtime"]

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):
  Telegram Bot ──────0.76──> Discord Bot        ← 同類 chat app
  Telegram Bot ──────0.74──> OpenClaw            ← 都涉及聊天整合
  OpenClaw ───────0.83──> Agent Architecture     ← 都是 AI Agent
  OpenClaw ───────0.78──> LangChain              ← Agent 框架生態
  Agent Architecture ──0.85──> LangChain         ← Agent 建構
  Agent Architecture ──0.77──> LLM API           ← 底層能力
  Agent Architecture ──0.79──> Tool Calling      ← Agent 核心能力
  LLM API ────────0.88──> OpenAI                 ← 同一類
  LLM API ────────0.86──> Anthropic Claude       ← 同一類
  Browser Automation ───0.82──> Puppeteer        ← 同領域工具
  FastAPI ────────0.71──> Hono                   ← 都是輕量 Web 框架!

注意:Telegram Bot → OpenClaw → Agent Architecture → Tool Calling 這條路徑是 embedding 自動發現的。你不需要手寫任何一條邊。

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(
    "我想做一個 Telegram 聊天機器人,能接 AI 自動回覆,有什麼技術可以參考?",
    model, skill_table, skill_graph, chunks_table
)
  Seed skills: {'Telegram Bot', 'LLM API', 'Discord Bot'}
  Expanded skills (+4): {'OpenClaw', 'Agent Architecture',
                          'Tool Calling', 'Node.js'}

結果:
  [0.750] Telegram Bot API 整合入門:從建立到部署
  [0.785] OpenClaw 安裝與設定:從零開始部署你的 AI 助理    🎯 新出現!
  [0.861] LLM API 整合入門:OpenAI 與 Anthropic Claude 實戰
  [0.874] LangChain Agent 架構設計與實作                    🎯 新出現!
  [0.891] Node.js Event Loop 深入理解:從 libuv 到 async/await  🎯 新出現!

差異一目了然


Aha Moment:跨越未知的未知

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

情境設定

知識庫裡有三篇文章:

注意:Chunk C 完全沒提到 Python 或 FastAPI。使用者根本不知道 "Hono" 這個關鍵字存在。

使用者的 Query

「我用 Python FastAPI 寫 REST API 很熟了,想部署到 Cloudflare Workers 降低延遲,有什麼框架推薦?」

純 Vector Search 的結果

results = vector_search(
    "我用 Python FastAPI 寫 REST API,想部署到 Cloudflare Workers 降低延遲,有什麼框架推薦?",
    model
)
  [0.627] FastAPI 高併發框架深度指南:從 Pydantic 到 Production   ← Chunk A ✓ 但使用者已經會了
  [0.793] 從需求推導 FastAPI:別先選工具                           ← 還是 FastAPI,沒幫助
  [0.802] Edge Runtime 全面解析:為什麼要把 Server 搬到邊緣?      ← 概念性文章,沒推薦具體框架
  [0.825] FastAPI 踩坑紀錄:高階架構設計專案中的血淚教訓           ← 還是 FastAPI
  [0.829] 從 Python 到 FastAPI:升級你的 Web 工具箱               ← 還是 Python 生態

Chunk C(Hono 實戰)完全沒出現。5 篇結果中有 3 篇是 FastAPI 相關文章——使用者已經會的東西。為什麼找不到 Hono?因為 Hono 那篇文章的文字沒有提到 Python、FastAPI、REST API——在向量空間裡,它跟 query 的距離太遠了。

有趣的是,#3 的 Edge Runtime 文章在 metadata 裡其實標記了 Hono 這個 skill,但 Hono 自己的實戰文章卻排不進 top-5。使用者不知道 "Hono" 這個關鍵字,所以無論怎麼改寫 query 都找不到它。這就是 Unknown Unknown

Entity Embedding Graph 的結果

results = entity_graph_search(
    "我用 Python FastAPI 寫 REST API,想部署到 Cloudflare Workers 降低延遲,有什麼框架推薦?",
    model, skill_table, skill_graph, chunks_table
)
  Seed skills: {'FastAPI', 'Edge Runtime', 'Python'}
  Expanded skills (+3): {'Hono', 'Cloudflare Workers', 'Node.js'}
                          ^^^^
                          Graph 自動發現:FastAPI ──0.71──> Hono

結果:
  [0.627] FastAPI 高併發框架深度指南:從 Pydantic 到 Production      ← Chunk A
  [0.802] Edge Runtime 全面解析:為什麼要把 Server 搬到邊緣?
  [0.836] Hono 實戰:為 Edge Runtime 設計的極速微型 Web 框架          ← Chunk C 🎯 找到了!

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

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

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

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


三種方式的完整對比

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

核心概念一句話

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

不是手動定義 "Telegram Bot" --relatedTo--> "OpenClaw",而是讓 embedding 模型告訴你:cosine_similarity(embed("Telegram Bot"), embed("OpenClaw")) = 0.74,自動建邊。

你現有的 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 作業

作業 1:跑 Vector Search Demo

用 200 篇知識庫跑 vector search,觀察它找到什麼、漏掉什麼:

# 安裝依賴
pip install lancedb sentence-transformers

# 生成 200 篇 mock data
python generate_career_kb_mock_data.py --out data/career_kb_chunks.jsonl --n 200

# 跑 demo
python demo_vector_search.py

# 額外跑 Aha Moment(FastAPI → Hono 盲區)
python demo_vector_search.py --query2

思考

作業 2:手動算 Skill Similarity

from sentence_transformers import SentenceTransformer
import numpy as np

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

# 計算幾組 skill 的 cosine similarity
skill_pairs = [
    ("Telegram Bot", "OpenClaw"),          # 都涉及聊天 app
    ("Telegram Bot", "Discord Bot"),       # 同類 chat platform
    ("OpenClaw", "Agent Architecture"),    # 都是 AI Agent
    ("OpenClaw", "Photoshop"),             # 完全無關
    ("Browser Automation", "Puppeteer"),   # 同領域工具
    ("FastAPI", "Hono"),                   # 不同語言的輕量框架
    ("LLM API", "Anthropic Claude"),       # 上下位概念
]

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 上的邊。哪些你直覺覺得有關,但 similarity 卻很低?


本章重點回顧

  1. 純 Vector Search 找到的是「字面相似的文章」,但缺乏結構——使用者問 chatbot + AI 回覆,卻找不到 OpenClaw(完整參考實作)和 Agent Architecture(正確的架構模式)
  2. Keyword Graph(手動 JSON)有結構,但不 scale——90+ 個 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 系統。