Ian Chou's Blog

構建 Human-friendly RAG:六大 LLM 在結構化知識提取上的表現評測

Claude RAG Benchmark 圖 1

前言:當 RAG 遇上知識圖譜

在構建個人知識庫(如 Logseq)或開發 Human-friendly RAG(檢索增強生成)系統時,我們面臨的最大挑戰往往不是「獲取信息」,而是「信息的結構化」與「精準度」。

最近,我進行了一項實驗:讓六大主流 AI 模型(Claude Opus 4.5, ChatGPT 5.1, Qwen 3 Max, Doubao 1.6, Grok 4.1, Gemini 3.0 Pro)針對 React 中最難懂的概念之一 —— useEffect,生成適合 Logseq 的結構化知識卡片。

實驗結果令人驚訝,雖然各模型都能生成正確代碼,但在 「構建心智模型」「知識結構化」 的能力上,差距顯著。

冠軍揭曉:為什麼 Claude Opus 4.5 是最佳選擇? (9.5/10)

在所有模型中,Claude 展現了「資深架構師」級別的思維。它不僅僅是在解釋語法,而是在重塑我們對技術的理解。

1. 定義的精準度(Precision)

這是 Claude 拉開差距的關鍵。看看它對 useEffect 本質的描述:

Claude: "useEffect 的本質不是 lifecycle hook,而是 「響應式資料管線」 ,用來同步外部系統與 React 狀態。"

其他模型: 多半停留在 "類似於 componentDidMount" 或 "處理副作用" 的層面。

這種表述對於 RAG 系統至關重要,因為它減少了語義檢索時的模糊匹配,提供了極高的解釋性(Explainability)

2. 重塑心智模型(Mental Model)

Claude 對於 Dependency Array 的解釋令人拍案叫絕:

這種深刻的洞察,能直接幫助學習者建立正確的思維路徑,這是構建高質量知識圖譜的核心。

3. 完美的教學結構

Claude 的輸出自帶邏輯層次:基礎概念反模式解決方案高級類比(同步器)。這幾乎可以直接轉化為 Logseq 中的層級結構,無需人工二次整理。

選手綜合評測:各模型的定位與特長

雖然 Claude 奪冠,但其他模型在特定場景下也有不可替代的優勢。以下是詳細的橫向對比:

模型 定位 評分 特點 適用場景
Claude 架構師 9.5 深度理解、結構嚴謹、教學設計強 深度學習、架構設計、建立知識骨架
ChatGPT 實戰專家 9.0 場景豐富、覆蓋面廣、細節多 解決具體 Bug、代碼審查、補充實例
Qwen 創新思考者 8.5 獨特的 "Gap" 分析、視角新穎 尋找技術盲點、啟發新思路
Doubao 實用主義者 8.0 接地氣、中文自然 快速入門、團隊基礎文檔
Grok 精煉實用派 8.0 極度簡潔、直擊重點 快速備忘、Cheatsheet
Gemini 基礎概括者 7.0 核心準確、中規中矩 概念速記
Claude RAG Benchmark 圖 2

實戰建議:如何構建完美的 RAG 知識庫

基於這次評測,對於想要在 Logseq 中構建高質量知識庫的朋友,我推薦採用 「混合策略」

Step 1: 用 Claude 搭建骨架

利用 Claude 極強的結構化能力,建立知識圖譜的核心層級。

useEffect 知識圖譜/
├── 核心概念 (Claude)
│ └── 響應式資料管線
├── 心智模型 (Claude)
│ └── 同步器類比
...

Step 2: 用 ChatGPT 填充血肉

將 ChatGPT 提供的豐富實戰場景(Edge cases)和常見錯誤(Stale Closure 具體案例)掛載到相應節點下。

Step 3: 用 Qwen 尋找盲點

參考 Qwen 的 "Gap" 分析,在筆記中標註「為什麼這裡容易出錯」,增加知識的深度。

總結

在 AI 輔助學習和知識管理的時代,模型的選擇決定了知識的質量

如果你追求的是 「快速解決問題」 ,ChatGPT 依然是王者;但如果你追求的是 「深刻理解本質」 並希望構建一個可長期復用的 結構化知識庫 ,Claude 目前是當之無愧的最佳架構師。


附註一:
Claude Opus 4.5 生成的知識點

[
	{
		"id": "kp_01",
		"type": "Concept",
		"topic": "useEffect 的本質是響應式資料管線",
		"core_statement": "useEffect 的本質不是 lifecycle hook,而是「響應式資料管線」,用來同步外部系統與 React 狀態。",
		"implication": "從 class component 轉換過來的開發者需要轉換心智模型,不應套用舊的生命週期思維。",
		"context_scenario": "在設計 useEffect 邏輯時",
		"tags": ["React", "useEffect", "mental-model", "reactive-programming"]
	},
	{
		"id": "kp_02",
		"type": "Concept",
		"topic": "Stale Closure 閉包過期問題",
		"core_statement": "Stale closure 指 effect 內的變數因為閉包特性,永遠保留著舊值而非最新狀態。",
		"implication": "JavaScript 閉包會「捕獲」當下的變數值,若 effect 不重新執行,變數就不會更新。",
		"context_scenario": "當 effect 內使用了 state 或 props 但未加入 dependency array 時",
		"tags": ["React", "useEffect", "closure", "JavaScript", "bug"]
	},
	{
		"id": "kp_03",
		"type": "Anti-Pattern",
		"topic": "故意省略 dependency 以避免重複執行",
		"core_statement": "為了避免 effect 重複執行而故意省略 dependency array 中的依賴項,會導致 stale closure 問題。",
		"implication": "正確解法是用 useCallback、useMemo 或重構邏輯,而非隱藏依賴。",
		"context_scenario": "在 useEffect 的 dependency array 設計時",
		"tags": ["React", "useEffect", "anti-pattern", "dependency-array"]
	},
	{
		"id": "kp_04",
		"type": "Anti-Pattern",
		"topic": "單一 effect 塞入過多邏輯",
		"core_statement": "把所有副作用邏輯塞進同一個 useEffect 中,會讓程式難以維護且違反單一職責原則。",
		"implication": "混雜的 effect 難以追蹤哪個依賴觸發了哪段邏輯,也難以獨立測試。",
		"context_scenario": "當一個 component 有多種不同類型的副作用時",
		"tags": ["React", "useEffect", "anti-pattern", "single-responsibility"]
	},
	{
		"id": "kp_05",
		"type": "Procedure",
		"topic": "依邏輯單位拆分多個 effect",
		"core_statement": "應依照「邏輯單位」將副作用拆分為多個獨立的 useEffect,每個 effect 只負責一件事。",
		"implication": "拆分後每個 effect 有獨立的 dependency array,更容易推理和維護。",
		"context_scenario": "在設計 React component 的副作用架構時",
		"tags": ["React", "useEffect", "best-practice", "separation-of-concerns"]
	},
	{
		"id": "kp_06",
		"type": "Example",
		"topic": "Effect 拆分範例:資料讀取與事件訂閱",
		"core_statement": "一個 effect 專門處理資料讀取(fetch data),另一個 effect 專門處理事件訂閱(event subscription),兩者分開管理。",
		"implication": "資料讀取通常依賴 ID 或 query 參數,事件訂閱通常依賴 handler function,兩者生命週期不同。",
		"context_scenario": "在需要同時 fetch API 又要監聽 WebSocket 或 DOM 事件的 component",
		"tags": [
			"React",
			"useEffect",
			"example",
			"data-fetching",
			"event-subscription"
		]
	},
	{
		"id": "kp_07",
		"type": "Anti-Pattern",
		"topic": "用 useEffect 模擬 componentDidMount",
		"core_statement": "不應該用 useEffect 來模擬 class component 的 componentDidMount 生命週期方法。",
		"implication": "這種思維會讓開發者錯誤理解 useEffect 的用途,導致設計出有問題的副作用邏輯。",
		"context_scenario": "從 class component 遷移到 function component 時",
		"tags": ["React", "useEffect", "anti-pattern", "lifecycle", "migration"]
	},
	{
		"id": "kp_08",
		"type": "Procedure",
		"topic": "使用空 dependency array 實現初始化執行",
		"core_statement": "若只想在 component 初始化時執行一次邏輯,使用空的 dependency array [],但必須確保 effect 內沒有任何會變動的值。",
		"implication": "如果 effect 內用到會變動的值卻給空 array,就會產生 stale closure。",
		"context_scenario": "在需要一次性初始化(如 analytics tracking、第三方 SDK 初始化)時",
		"tags": ["React", "useEffect", "dependency-array", "initialization"]
	},
	{
		"id": "kp_09",
		"type": "Concept",
		"topic": "副作用應盡量純粹",
		"core_statement": "useEffect 中的副作用應該盡量「純粹」,即可預測、可清理、無意外的外部影響。",
		"implication": "純粹的副作用更容易測試、debug,也更容易被 React 的 Strict Mode 檢測出問題。",
		"context_scenario": "在撰寫任何 useEffect 邏輯時",
		"tags": ["React", "useEffect", "purity", "best-practice"]
	},
	{
		"id": "kp_10",
		"type": "Anti-Pattern",
		"topic": "Fetch API 未處理 race condition",
		"core_statement": "在 useEffect 中使用 fetch API 時若未處理 race condition,當使用者快速切換頁面會造成舊請求覆蓋新請求或 memory leak。",
		"implication": "非同步操作的結果回來時,component 可能已經 unmount 或依賴已經改變。",
		"context_scenario": "在 useEffect 內進行 API 請求時",
		"tags": ["React", "useEffect", "fetch", "race-condition", "async"]
	},
	{
		"id": "kp_11",
		"type": "Procedure",
		"topic": "使用 AbortController 解決 race condition",
		"core_statement": "透過 AbortController 在 useEffect 的 cleanup function 中取消進行中的 fetch 請求,以解決 race condition 和 memory leak 問題。",
		"implication": "AbortController 是 Web API 標準的一部分,可搭配 fetch 的 signal 選項使用。",
		"context_scenario": "在 useEffect cleanup 中處理 fetch 請求取消",
		"tags": ["React", "useEffect", "AbortController", "cleanup", "fetch"]
	},
	{
		"id": "kp_12",
		"type": "Anti-Pattern",
		"topic": "Effect 內大量使用 isMounted 布林開關",
		"core_statement": "若發現自己在 effect 內大量使用布林開關(如 isMounted)來控制邏輯,代表程式的資料流設計有根本問題。",
		"implication": "布林開關是治標不治本的 patch,真正的問題在於副作用邏輯沒有正確對應到資料流。",
		"context_scenario": "當開發者試圖用 flag 變數來「修復」 useEffect 的執行問題時",
		"tags": ["React", "useEffect", "anti-pattern", "code-smell", "data-flow"]
	},
	{
		"id": "kp_13",
		"type": "Procedure",
		"topic": "重新思考資料流而非繼續 patch",
		"core_statement": "當 useEffect 出現複雜的控制邏輯時,應該退一步重新思考整體資料流設計,而不是繼續用 workaround 修補。",
		"implication": "好的 React 設計讓資料流向清晰,副作用自然簡單;複雜的 effect 通常是設計問題的症狀。",
		"context_scenario": "當 useEffect 變得難以理解或維護時",
		"tags": ["React", "useEffect", "refactoring", "data-flow", "architecture"]
	},
	{
		"id": "kp_14",
		"type": "Concept",
		"topic": "Dependency array 的核心作用",
		"core_statement": "useEffect 的 dependency array 告訴 React 何時需要重新執行 effect,以確保 effect 內的值與最新狀態同步。",
		"implication": "dependency array 不是「優化」工具,而是「正確性」工具。",
		"context_scenario": "在理解 useEffect 的基本機制時",
		"tags": ["React", "useEffect", "dependency-array", "fundamentals"]
	},
	{
		"id": "kp_15",
		"type": "Analogy",
		"topic": "Effect 如同外部系統的同步器",
		"core_statement": "useEffect 就像一個「同步器」,負責讓 React 的狀態與外部系統(DOM、API、訂閱)保持一致。",
		"implication": "這個類比幫助理解為何 effect 需要 cleanup:同步器在狀態改變時需要先解除舊的同步再建立新的。",
		"context_scenario": "在向他人解釋 useEffect 的設計哲學時",
		"tags": ["React", "useEffect", "analogy", "synchronization", "mental-model"]
	}
]

附註二:
ChatGPT 5.1 生成的知識點

[
	{
		"id": "kp_01",
		"type": "Concept",
		"topic": "useEffect power and risk",
		"core_statement": "React 的 useEffect Hook 是一個非常強大的功能,但也同時非常容易被錯誤使用。",
		"implication": "強大抽象通常也有高誤用成本,需要更嚴謹的使用模式。",
		"context_scenario": "在使用 React 函式元件處理副作用邏輯時",
		"tags": ["react", "useEffect", "side-effects", "frontend"]
	},
	{
		"id": "kp_02",
		"type": "Concept",
		"topic": "dependency array 重要性",
		"core_statement": "useEffect 的 dependency array 是用來告訴 React:哪些值變動時需要重新執行 effect。",
		"implication": "正確填寫 dependency array 是避免錯誤與 stale closure 的關鍵。",
		"context_scenario": "在撰寫 useEffect 並決定其依賴值時",
		"tags": ["react", "useEffect", "dependency-array"]
	},
	{
		"id": "kp_03",
		"type": "Anti-Pattern",
		"topic": "刻意省略依賴",
		"core_statement": "許多開發者會因為擔心 useEffect 重複執行,而刻意在 dependency array 中省略實際使用到的依賴。",
		"implication": "為了避免重新執行而省略依賴會製造隱藏 bug,而不是解決性能問題。",
		"context_scenario": "在優化 useEffect 執行次數時錯誤地移除依賴",
		"tags": ["react", "useEffect", "anti-pattern", "dependency-array"]
	},
	{
		"id": "kp_04",
		"type": "Concept",
		"topic": "stale closure 問題",
		"core_statement": "當 useEffect 的 dependency array 缺少必要依賴時,effect 內的閉包會持續捕捉舊值,導致 stale closure(閉包過期)問題。",
		"implication": "stale closure 會讓 state 或 props 看起來無法更新,導致難以追蹤的邏輯錯誤。",
		"context_scenario": "在 effect 內使用 state 或 props 且未正確列入 dependency array 時",
		"tags": ["react", "useEffect", "closure", "bug"]
	},
	{
		"id": "kp_05",
		"type": "Concept",
		"topic": "stale closure 效果",
		"core_statement": "stale closure 會讓 useEffect 中使用的變數永遠停留在舊值,而不會隨著最新 state 或 props 更新。",
		"implication": "畫面看似渲染正確但邏輯依舊用舊值,會造成難以察覺的行為異常。",
		"context_scenario": "在 effect 中執行計算、API 呼叫或註冊 handler 並依賴 state 值時",
		"tags": ["react", "bug", "closure", "state"]
	},
	{
		"id": "kp_06",
		"type": "Procedure",
		"topic": "依照邏輯單位拆分 effect",
		"core_statement": "正確使用 useEffect 的方式,是依照「邏輯單位」拆分成多個 effect,而不是把所有邏輯塞進同一個 effect。",
		"implication": "拆分 effect 可以提升可讀性、可測試性,並避免彼此依賴混在一起。",
		"context_scenario": "當一個元件內有多種不同的副作用邏輯需要處理時",
		"tags": ["react", "useEffect", "design", "refactoring"]
	},
	{
		"id": "kp_07",
		"type": "Example",
		"topic": "effect 拆分示例",
		"core_statement": "例如可以用一個 useEffect 專門處理資料讀取(data fetching),另一個 useEffect 專門處理事件訂閱(event subscription)。",
		"implication": "將不同 concern 拆分成不同 effect 可以更清楚描述資料流與生命週期。",
		"context_scenario": "在同一個元件中既要抓取遠端資料,又要訂閱 window 或 DOM 事件時",
		"tags": ["react", "example", "data-fetching", "events"]
	},
	{
		"id": "kp_08",
		"type": "Concept",
		"topic": "useEffect 的本質",
		"core_statement": "useEffect 的本質並不是傳統意義上的 lifecycle,而是「響應式資料管線」。",
		"implication": "開發者應從資料依賴與反應式更新的角度思考 useEffect,而不是從 class lifecycle 的角度移植思維。",
		"context_scenario": "在從 class component 過渡到 function component 時設計副作用邏輯",
		"tags": ["react", "useEffect", "reactive", "mental-model"]
	},
	{
		"id": "kp_09",
		"type": "Anti-Pattern",
		"topic": "用 useEffect 模擬 componentDidMount",
		"core_statement": "不應該使用 useEffect 來模擬 componentDidMount 這類 class lifecycle 方法。",
		"implication": "強行模擬舊 lifecycle 會阻礙採用 React Hooks 的正確心智模型並導致錯誤依賴管理。",
		"context_scenario": "從 class component 遷移邏輯時,試圖一比一對應到 useEffect 寫法",
		"tags": ["react", "anti-pattern", "lifecycle", "hooks-migration"]
	},
	{
		"id": "kp_10",
		"type": "Concept",
		"topic": "空 dependency array 的語意",
		"core_statement": "若只想在初始化時執行一次邏輯,可以使用空 dependency array,表示 effect 只在初次 render 後執行一次。",
		"implication": "空 dependency array 是一種強烈保證,代表 effect 不會因任何值變動而重新執行。",
		"context_scenario": "在元件掛載時僅需執行一次的設定、資料預載或初始化邏輯",
		"tags": ["react", "useEffect", "dependency-array", "initialization"]
	},
	{
		"id": "kp_11",
		"type": "Anti-Pattern",
		"topic": "初始化 effect 內含變動值",
		"core_statement": "當使用空 dependency array 時,effect 內不應該包含會逐漸變動的值,否則會造成邏輯與實際執行次數不一致。",
		"implication": "在空 dependency array 中使用變動值等同於把 stale closure 問題變成設計特性。",
		"context_scenario": "在初始化 effect 中使用日後會更新的 props 或 state 卻未重新執行 effect",
		"tags": ["react", "anti-pattern", "closure", "dependency-array"]
	},
	{
		"id": "kp_12",
		"type": "Concept",
		"topic": "副作用應盡量純粹",
		"core_statement": "useEffect 中的副作用邏輯應該盡量純粹,避免不必要的外部依賴與隱藏狀態。",
		"implication": "純粹的副作用較容易推理、重構與測試,也較不容易產生競態或記憶體洩漏。",
		"context_scenario": "在設計資料抓取、事件訂閱、DOM 操作等副作用邏輯時",
		"tags": ["react", "side-effects", "purity", "design"]
	},
	{
		"id": "kp_13",
		"type": "Concept",
		"topic": "fetch 競態條件風險",
		"core_statement": "使用 fetch API 時需要處理 race condition,以避免使用者快速切換頁面或狀態時造成錯誤更新或 memory leak。",
		"implication": "未處理競態可能導致已卸載的元件仍嘗試 setState 或處理過期回應。",
		"context_scenario": "在 useEffect 中進行非同步資料抓取並依結果更新 state 時",
		"tags": ["react", "fetch", "race-condition", "memory-leak"]
	},
	{
		"id": "kp_14",
		"type": "Procedure",
		"topic": "用 AbortController 處理 fetch",
		"core_statement": "處理 fetch API 競態與避免 memory leak 的常見做法,是在 useEffect 中使用 AbortController 來中止不再需要的請求。",
		"implication": "中止過期請求可避免不必要的網路成本與錯誤 UI 更新。",
		"context_scenario": "在使用 useEffect 發送 fetch 請求且元件可能在請求完成前卸載或變更條件時",
		"tags": ["react", "fetch", "AbortController", "cleanup"]
	},
	{
		"id": "kp_15",
		"type": "Anti-Pattern",
		"topic": "在 effect 中大量使用布林開關",
		"core_statement": "如果在 useEffect 內大量使用布林開關(例如 isMounted)來控制流程,代表程式邏輯設計存在問題。",
		"implication": "過多布林旗標通常是資料流設計不良的氣味,而不是應該被更多 patch 掩蓋的問題。",
		"context_scenario": "在 effect 中透過多個布林 state 來避免重複呼叫或判斷是否已初始化時",
		"tags": ["react", "anti-pattern", "code-smell", "state-management"]
	},
	{
		"id": "kp_16",
		"type": "Procedure",
		"topic": "發現布林氣味後的行動",
		"core_statement": "當發現自己在 useEffect 中依賴大量布林開關時,應該重新思考與重構資料流,而不是繼續增加更多條件判斷來 patch。",
		"implication": "重新設計資料流通常能從根本上消除競態、重複執行與初始化判斷問題。",
		"context_scenario": "維護一段 useEffect 內部邏輯變得複雜且充滿 isXXX flag 時",
		"tags": ["react", "refactoring", "architecture", "state-management"]
	}
]

附註三:
測試用的 System Prompt

You are an expert Knowledge Engineer and card-designer for spaced repetition.

Your job:
Given an article or note, decompose it into **atomic Knowledge Points (KPs)** and output them as a **JSON array**.  
Do NOT summarize the text. Instead, extract individual, reusable knowledge units.

---

## CORE RULES

1. **Atomicity**

   - One KP = one main idea.
   - Do NOT mix multiple ideas into one KP.

2. **Self-contained**

   - Each KP must be understandable without seeing the original text.
   - Avoid pronouns like "this", "it", "they". Replace them with explicit nouns.

3. **Full spectrum of knowledge**

   - Do NOT only extract definitions and facts.
   - Always look for procedures, pitfalls, examples, analogies, and open questions.

4. **Max count**
   - Extract up to 30 KPs, focusing on the most useful ones for learning and thinking.

---

## KNOWLEDGE TYPE CLASSIFICATION

For each KP, choose exactly ONE `type` from:

1. **Concept** – Definitions, facts, properties. (“What is it?”)
2. **Procedure** – Step-by-step methods, algorithms, workflows. (“How to do it?”)
3. **Anti-Pattern** – Common mistakes, traps, things to avoid. (“What goes wrong?”)
4. **Analogy** – Metaphors, comparisons to other domains. (“X is like Y because…”)
5. **Example** – Concrete cases, scenarios, code examples, historical events. (“For example…”)
6. **Gap** – Limitations, open problems, future work, unknowns. (“We still don’t know…”)

---

## INFERENCE RULES (IMPLICIT KNOWLEDGE)

Besides what is explicitly written, infer short implications when they are **obvious to a domain expert**:

- If the text describes a **solution**, the hidden KP may be the **problem** it solves.
- If the text describes a **benefit**, the hidden KP may be the **trade-off or cost**.
- If a specific tool/term is used, infer its broader **category or role** when helpful.
- If nothing meaningful can be inferred, use an empty string "" for `implication`.

---

## OUTPUT FORMAT (JSON ONLY)

Return ONLY valid JSON. No explanations, no comments.

Each KP must have this structure:

[
{
"id": "kp_01",
"type": "Concept | Procedure | Anti-Pattern | Analogy | Example | Gap",
"topic": "Short 2-6 word title of the idea",
"core_statement": "One atomic statement capturing the essence of this KP.",
"implication": "Very short hidden context, trade-off, or 'why this matters'. Empty string if none.",
"context_scenario": "Where or when this KP applies (e.g., 'In React useEffect hooks'). Empty string if not needed.",
"tags": ["tag1", "tag2", "tag3"]
}
]

- Use English for `type` and `tags`.
- `core_statement` and `context_scenario` can be in the same language as the input text.
- If some fields are not applicable, set them to an empty string "" (except `id`, `type`, `topic`, `core_statement` which are required).

附註四:
測試用 Sample Artcle

React 的 useEffect Hook 是最強大的功能之一,但也經常被錯誤使用。最常見的問題出現在 dependency array。許多開發者會因為擔心 effect 重複執行,而故意省略依賴,導致 stale closure(閉包過期)問題。這會讓 effect 裡的變數永遠使用舊值。

另一個常見錯誤是把所有邏輯塞進同一個 effect 中。正確的做法是依照「邏輯單位」拆分多個 effect。例如:一個 effect 處理資料讀取,另一個 effect 處理事件訂閱。

useEffect 的本質其實不是 lifecycle,而是「響應式資料管線」。所以不要用它模擬 componentDidMount。若你只想在初始化時執行一次邏輯,應該用空 dependency array,但要確保 effect 裡沒有任何逐漸變動的值。

此外,副作用應該盡量純粹。例如:fetch API 要處理 race condition,以避免使用者快速切換頁面時造成 memory leak。這通常透過 AbortController 解決。

最後,若你發現自己在 effect 內大量使用布林開關(e.g., isMounted),那代表你的程式邏輯設計有問題,應該重新思考資料流,而不是繼續 patch。