Ian Chou's Blog

CtxFST CH10 - 全端實戰:用 Entity Embedding Graph 打敗純 Vector Search,發現你筆記中的未知領域

CtxFST CH10:全端實戰——用 Entity Embedding Graph 打敗純 Vector Search,發現你筆記中的未知領域

在前面 9 個章節中,我們已經建立了從 Schema 到 Entity Profiles 到 Similarity Graph 的完整概念體系。

現在,是時候把所有的理論摺疊起來,攤開一張真正跑得動的完整程式碼展演了。

這篇文章會帶你走完以下流程:

  1. sentence-transformers + LanceDB 為 17 個技能名詞做 Embedding
  2. 不寫任何一條手動邊,讓 Cosine Similarity 自動建出一張語意圖譜
  3. 用 Graph Expansion 實戰破解「Unknown Unknowns(不知道自己不知道的)」場景
  4. 回頭對比純 Vector Search 的結果,看出碾壓級差距

準備好了嗎?讓我們開始!


回顧:建圖的三種方式

在開始之前,快速回顧我們之前介紹過的三種方法:

方式 原理 缺點
方式一:關鍵字匹配 Tags / Keywords 碰在一起就算相似 表面重疊,語意膚淺
方式二:手動寫關係 人工定義 REQUIRES, SIMILAR_TO 慢、無法 Scale、主觀
方式三:Entity Embedding Graph ⭐ 讓 Embedding 自動算出語意邊 本章教的方法!

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


Step 1:Embedding 所有技能

from sentence_transformers import SentenceTransformer
import lancedb
import networkx as nx

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

# ① 定義技能名單
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")

到這一步,17 個技能已經被向量化並存入 LanceDB。它們各自佔據向量空間中的一個座標點。


Step 2:零手動建圖——讓 Embedding 自己畫邊

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)

就這樣,我們沒有手動寫任何一條邊。 系統自動產出了以下關聯:

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 框架!

注意最後一條:FastAPI → Hono(0.71)。一個是 Python 框架,一個是 TypeScript for Edge Framework。它們在 source code 或名稱上完全不相關,但 Embedding 自動發現「它們的使用場景相近——都是輕量級 Web 框架」。這就是語意圖的威力。

另外觀察一條路徑:Telegram Bot → OpenClaw → Agent Architecture → Tool Calling——這整條鏈路是 Embedding 自動跑出來的,不需要人工定義任何一步。


有了圖做為導航,我們可以開發出遠超純 Vector Search 的檢索策略:

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}

    # Step 2: 沿 Graph 擴展 N Hops
    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

    # 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

在每次搜尋中,它做了三件事:

  1. 找起點(Seed):用 Vector Search 確定 Query 最相關的 3 個 Entity
  2. 沿圖擴展(Expansion):沿著語意邊走 N 步,蒐集周邊的 Entity
  3. 導航檢索(Guided Retrieval):用擴展後的 Entity 集合過濾用 Vector 找到的 Chunks

實戰 Demo:真正的碾壓差距

場景一:Telegram Bot 聊天機器人開發

使用者提問「我想做一個 Telegram 聊天機器人,能接 AI 自動回覆,有什麼技術可以參考?」

Graph 導航過程:

Seed skills: {Telegram Bot, LLM API, Discord Bot}
Expanded skills (+4): {OpenClaw, Agent Architecture, Tool Calling, Node.js}
排名 純 Vector Search Entity Graph Search
1 Telegram Bot API 整合入門 Telegram Bot API 整合入門
2 Discord Bot 建置指南 🎯 OpenClaw 安裝與設定
3 LINE Bot 開發教學 LLM API 整合:OpenAI + Claude
4 聊天機器人 FAQ 設計 🎯 LangChain Agent 架構設計
5 對話式 UI 設計模式 🎯 Node.js Event Loop 深入理解

差異總結:

場景二:Unknown Unknowns(未知的未知)——真正的 Aha Moment

這是整篇文章最震撼的段落。

知識庫裡有三篇文章:

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

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

[0.627] FastAPI 高併發框架深度指南          ← 使用者已經會了
[0.793] 從需求推導 FastAPI                  ← 還是 FastAPI
[0.802] Edge Runtime 全面解析                ← 概念性文章,沒推具體框架
[0.825] FastAPI 踩坑紀錄                    ← 還是 FastAPI
[0.829] 從 Python 到 FastAPI:升級你的工具箱  ← 還是 Python 生態

Chunk C(Hono 實戰)完全沒出現。 5 篇裡有 3 篇是使用者已經會的 FastAPI。為什麼?因為 Hono 那篇文章的文字裡沒有 Python、FastAPI、REST API 這些詞——在向量空間中,它離 Query 太遠了。

Seed skills: {FastAPI, Edge Runtime, Python}
Expanded skills (+3): {Hono, Cloudflare Workers, Node.js}
                        ^^^^
       Graph 自動發現:FastAPI ──0.71──> Hono
[0.627] FastAPI 高併發框架深度指南
[0.802] Edge Runtime 全面解析
[0.836] 🎯 Hono 實戰:為 Edge Runtime 設計的極速微型 Web 框架  ← 找到了!

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

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

系統主動搭了一座橋,把使用者「不知道關鍵字是什麼」的技術精準地遞到他面前。
這就是 Unknown Unknowns——你不知道自己不知道什麼,但 Graph 替你知道了。


Entity 從哪裡萃取出來?五大方法比較

要讓 Embedding 幫你建圖,首先你得有乾淨的 Entity (技能節點)。以下是從文件中提取 Entity 的五種主流方法:

方法 原理 代表工具 準確率 成本
1. LLM Prompting (最標準) 直接請 LLM 提取 Entity + 關係描述 GraphRAG Prompt、LangChain LLMGraphTransformer 90%+
2. Rule-based Regex / Pattern 白名單比對 spaCy rule, NLTK chunker 依名單 免費
3. Pipeline (NER+RE) NER → 關係分類器 spaCy NER pipeline、Stanford CoreNLP ~80%
4. Transformer (端到端) BERT/GPT Fine-tune Joint ER REBEL model、T5-RE 95% 高 (需 Train)
5. Bootstrapping 給 Seed Pairs 讓演算法自動擴展 Snowball、Distant Supervision 變異大

推薦策略:Hybrid(白名單 + LLM)

對於 200 個節點規模的技能圖譜,最穩的做法是:

# 使用 Instructor + Pydantic(比 LangChain 更穩定)
import instructor
from pydantic import BaseModel
from openai import OpenAI

class Relation(BaseModel):
    source: str
    target: str
    relationship: str  # "REQUIRES", "IMPLEMENTS", "SIMILAR_TO"

class GraphExtraction(BaseModel):
    nodes: list[str]
    edges: list[Relation]

client = instructor.from_openai(OpenAI())
graph_data = client.chat.completions.create(
    model="gpt-4o",
    response_model=GraphExtraction,
    messages=[{"role": "user", "content": "FastAPI 是一個 Python 框架..."}]
)

標準流程:LLM Extract → Dedup → Embed → Graph


框架選型指南:不只有 LangChain

LangChain 方便但不是唯一選擇。以下是四大替代方案:

方案 靈活性 最適合誰
原生 API + Instructor/Pydantic ⭐⭐⭐ 最高 追求穩定輸出、想看清底層邏輯的開發者
LangChain ⭐⭐ 中等 快速原型、已經在用 LangChain 生態系的人
Microsoft GraphRAG ⭐ 低 (封裝重) 超大文件、需要「全域社群摘要」的場景
LlamaIndex Property Graph ⭐⭐⭐ 高 需要深度整合圖庫(Neo4j、FalkorDB)的場景

業界實證:不只是我們在做

這套「透過 Embedding Similarity 自動連邊,再用 N-hop 做圖擴展」的技術,在業界已經有非常扎實的實踐:


結論:從「段落檢索」進化到「概念導航」

維度 純 Vector Search Entity Embedding Graph
檢索對象 Chunk(段落) Entity(概念)→ 再定位 Chunk
關聯發現 只有 Query 直接語意命中的 沿圖擴展,發現隱含連結
Unknown Unknowns ❌ 永遠找不到 ✅ Graph 自動搭橋
重複率 高(同類 chunk 擠爆排名) 低(不同領域的 chunk 被引入)
建構成本 零(只存向量) 低(Embedding 自動建邊)

以前,你的 RAG 只會回覆你已經知道的東西。
現在,Entity Embedding Graph 會主動把你「不知道自己不知道」的答案遞到你面前。

這就是整個 CtxFST 系列從 CH1 到 CH10 一路走來,最終要到達的目的地。


📌 CtxFST 開源專案github.com/ctxfst