Ian Chou's Blog

從 LangChain 遷移到 LlamaIndex:PropertyGraphIndex 整合實戰

從 LangChain 遷移到 LlamaIndex:PropertyGraphIndex 整合實戰

當 LLM 編排庫越來越多,選擇正確的工具變得至關重要。本文分享將 career-kb 專案從 LangChain 遷移到 LlamaIndex 的完整過程,以及如何將 LlamaIndex 的 PropertyGraphIndex 與現有的 Rust petgraph 高效能後端整合。

為什麼遷移?

career-kb 原本使用 LangChain 進行 LLM 編排,但隨著專案發展,我們發現幾個痛點:

痛點 LangChain 現況 LlamaIndex 優勢
GraphRAG 需要額外的 LangGraph 原生 PropertyGraphIndex
程式碼重複 每個檔案都有 get_langchain_model() 統一 Provider 模組
Structured Output with_structured_output().invoke() structured_predict() 更簡潔
資料導向 流程編排為主 RAG/資料處理為主

架構概覽

遷移後的架構:

graph TB
    subgraph LlamaIndex["LlamaIndex 編排層"]
        PGI["PropertyGraphIndex"]
        SP["structured_predict"]
    end
    
    subgraph Adapter["Adapter 層"]
        PPGS["PetgraphPropertyGraphStore"]
    end
    
    subgraph Backend["高效能後端"]
        PG["petgraph (Rust)
~10-100x 更快"] LDB["LanceDB (Arrow)
向量 + FTS 存儲"] end PGI --> PPGS SP --> PPGS PPGS --> PG PPGS --> LDB PG <--> LDB

Phase 1: 依賴更新

pyproject.toml

-    "langchain>=1.0",
-    "langchain-openai>=0.3",
-    "langchain-google-genai>=2.0",
+    "llama-index>=0.14",
+    "llama-index-llms-openai>=0.5",
+    "llama-index-llms-google-genai>=0.4",

Phase 2: 統一 LLM Provider

之前每個檔案都有類似的重複程式碼:

# nli.py, reflection.py, entity_extractor.py 都有這段
def get_langchain_model():
    if os.getenv("XAI_API_KEY"):
        from langchain_openai import ChatOpenAI
        return ChatOpenAI(...)
    elif os.getenv("GOOGLE_API_KEY"):
        from langchain_google_genai import ChatGoogleGenerativeAI
        return ChatGoogleGenerativeAI(...)
    # ... 20+ 行

現在統一到 llm/provider.py

"""Unified LLM provider for career-kb using LlamaIndex."""

from llama_index.llms.openai import OpenAI
from llama_index.llms.google_genai import GoogleGenAI
import os

def get_llm(temperature: float = 0.1):
    """Get LlamaIndex LLM with xAI/Google/OpenAI fallback."""
    if os.getenv("XAI_API_KEY"):
        return OpenAI(
            api_key=os.getenv("XAI_API_KEY"),
            api_base=os.getenv("XAI_BASE_URL", "https://api.x.ai/v1"),
            model=os.getenv("XAI_MODEL", "grok-4-1-fast-reasoning"),
            temperature=temperature,
        )
    elif os.getenv("GOOGLE_API_KEY"):
        return GoogleGenAI(
            api_key=os.getenv("GOOGLE_API_KEY"),
            model=os.getenv("GOOGLE_MODEL", "gemini-3-flash-preview"),
            temperature=temperature,
        )
    else:
        return OpenAI(
            api_key=os.getenv("OPENAI_API_KEY"),
            model=os.getenv("OPENAI_MODEL", "gpt-5-mini"),
            temperature=temperature,
        )

所有其他檔案只需一行 import:

from career_kb.llm.provider import get_llm

Phase 3: Structured Output 遷移

LangChain 寫法 (Before)

llm = get_langchain_model()
structured_llm = llm.with_structured_output(NLIVerificationResult)
prompt = NLI_PROMPT_TEMPLATE.format(skill=skill, evidence=evidence_str)
result = structured_llm.invoke(prompt)

LlamaIndex 寫法 (After)

from llama_index.core.prompts import PromptTemplate

llm = get_llm()
prompt = PromptTemplate(NLI_PROMPT_TEMPLATE)

result = llm.structured_predict(
    NLIVerificationResult,
    prompt,
    skill=skill,
    evidence=evidence_str,
)

關鍵差異:


Phase 4: PropertyGraphIndex 整合

這是最有價值的部分:將 LlamaIndex 的 PropertyGraphIndex 與 Rust petgraph 整合。

設計理念

不是取代 petgraph,而是在上面加一層編排:

# LlamaIndex 負責:Query 編排、LLM 呼叫、Retriever 組合
# Petgraph 負責:高效能圖運算、BFS/DFS、Leiden 算法

PetgraphPropertyGraphStore

實作 LlamaIndex 的 PropertyGraphStore 介面:

from llama_index.core.graph_stores.types import PropertyGraphStore, EntityNode, Relation, Triplet

class PetgraphPropertyGraphStore(PropertyGraphStore):
    """PropertyGraphStore backed by petgraph (Rust)."""
    
    def __init__(self, knowledge_graph: CareerKnowledgeGraph | None = None):
        self._kg = knowledge_graph or CareerKnowledgeGraph()
        self._kg.load_from_skill_graph()
    
    def get_triplets(self, entity_names=None, relation_names=None, **kwargs) -> list[Triplet]:
        """Get triplets from petgraph."""
        triplets = []
        for source, target, relation in self._get_all_edges():
            if entity_names and source not in entity_names and target not in entity_names:
                continue
            if relation_names and relation not in relation_names:
                continue
            
            triplets.append((
                EntityNode(name=source, label="entity"),
                Relation(source_id=source, target_id=target, label=relation),
                EntityNode(name=target, label="entity"),
            ))
        return triplets
    
    def get_rel_map(self, graph_nodes, depth=2, **kwargs) -> dict:
        """Leverage petgraph's efficient BFS traversal."""
        rel_map = {}
        for node in graph_nodes:
            neighbors = self._kg.get_neighbors(node.name, hops=depth)
            # ... build paths
        return rel_map

使用範例

from career_kb.graph.property_graph_index import create_property_graph_index

# 建立 index(底層用 petgraph)
index = create_property_graph_index(use_llm_extractor=False)

# 查詢
retriever = index.as_retriever()
nodes = retriever.retrieve("Python backend experience")

Phase 5: CLI 整合

新增兩個 CLI 命令:

# 顯示 PropertyGraphStore 資訊
career-kb graph property-store

# 使用 PropertyGraphIndex 查詢
career-kb graph property-query "Python backend"

執行結果

$ career-kb graph property-store

        PropertyGraphStore Info        
┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
┃ Metric         ┃ Value              ┃
┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
│ Backend        │ rust (petgraph)    │
│ Node Count     │ 93                 │
│ Edge Count     │ 217                │
│ Node Types     │ skill              │
│ Relation Types │ implies, relatedTo │
└────────────────┴────────────────────┘

Sample Triplets:
  (Python) --[relatedTo]--> (Data Science)
  (LangChain) --[implies]--> (Python)

遷移的模組清單

模組 變更
llm/provider.py [NEW] 統一的 LLM provider
llm/nli.py get_langchain_model()get_llm()
llm/reflection.py Writer/Critic/Reviser 改用 structured_predict
graph/entity_extractor.py 移除重複的 LLM 初始化
graph/evidence_selection.py ChatPromptTemplatePromptTemplate
graph/property_graph_store.py [NEW] PropertyGraphStore adapter
graph/property_graph_index.py [NEW] PropertyGraphIndex 整合

效能驗證

LLM 呼叫

$ career-kb verify -s "Python,FastAPI" --enhanced-nli

              Skill Verification Results               
┏━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
┃ 技能    ┃ 狀態    ┃ 嚴重性 ┃ 證據                   ┃ 來源  ┃
┡━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
│ Python  │ COVERED │   ✅   │ Developed Chat-Native  │ LLM+1 │
│ FastAPI │ COVERED │   ✅   │ Positioned Python as   │ LLM+1 │
└─────────┴─────────┴────────┴────────────────────────┴───────┘

圖運算

# Rust petgraph 效能
g.get_neighbors("Python", hops=2)  # 20 nodes, <1ms

未來擴展

Arrow 零拷貝整合

目前 petgraph binding 使用標準 PyO3 型別轉換:

pub fn get_all_nodes(&self) -> Vec<(String, String)> {
    // 每次都會複製數據
}

未來可以用 Arrow 實現零拷貝:

pub fn get_all_nodes_arrow(&self) -> PyResult<PyObject> {
    // 返回 Arrow RecordBatch,Python 端直接讀取
}

LLM Entity Extraction

目前手動呼叫 entity_extractor.py,未來可以啟用 LlamaIndex 的自動抽取:

index = create_property_graph_index(
    documents=docs,
    use_llm_extractor=True,  # 自動抽取實體
)

總結

遷移項目 效益
LangChain → LlamaIndex 更簡潔的 API、資料導向設計
統一 Provider 減少 ~100 行重複程式碼
PropertyGraphIndex 標準介面、易於擴展
petgraph 保留 高效能圖運算不受影響

關鍵心得:好的遷移不是推翻重來,而是在現有架構上疊加更好的抽象層。


Career Knowledge Base 是一個本地優先的履歷知識庫系統,使用 Python + LanceDB + petgraph (Rust) + LlamaIndex 建構。