前言
你的 RAG 系統「感覺」不錯,但你能量化它有多好嗎?
這是生產環境中最常見的盲區:工程師花大量時間優化 Chunking、調整 Reranker,卻沒有客觀的指標來驗證改動是否真的有效。
這篇是系列的最後一篇,涵蓋三個主題:
- RAG 評估(RAGAS):如何量化 RAG 品質
- GraphRAG:當向量搜尋不夠用時的替代方案
- Agentic RAG:RAG + Agent,讓 AI 自己決定如何搜尋
Part 1:RAG 評估——RAGAS 框架
為什麼評估很難?
RAG 的輸出是自然語言,沒有標準答案可以直接比對。
「這個答案好不好」,需要從多個維度判斷。
RAGAS 的四個核心指標
RAGAS(RAG Assessment) 是目前最流行的 RAG 評估框架,定義了四個指標:
1. Faithfulness(忠實度)
答案是否只根據 context,沒有幻覺?
計算方式:
Step 1: 把答案分解成一組陳述句(claims)
Step 2: 對每個陳述,判斷 context 是否支持它
Step 3: Faithfulness = 有 context 支持的陳述數 / 總陳述數
理想值:接近 1.0
2. Answer Relevancy(答案相關性)
答案是否真的回答了問題?
計算方式:
Step 1: 讓 LLM 根據答案生成 N 個「可能的問題」
Step 2: 計算這些問題與原始問題的向量相似度
Step 3: Answer Relevancy = 平均相似度
理想值:接近 1.0
特別之處:如果答案跑題(回答了別的問題),分數會很低
3. Context Precision(上下文精準度)
檢索到的 context 中,有多少比例是真的相關的?
計算方式:
對每個 context chunk,判斷它是否對生成答案有貢獻
Context Precision = 有貢獻的 chunk 數 / 總 chunk 數
理想值:接近 1.0
代表意義:如果你取回 10 個 chunk,只有 3 個有用,分數就是 0.3
4. Context Recall(上下文召回率)
知識庫中的相關資訊,有多少被成功召回?
需要 ground truth answer(標準答案)
計算方式:
Step 1: 把 ground truth 分解成陳述句
Step 2: 對每個陳述,判斷 context 中是否有支持
Step 3: Context Recall = 有支持的陳述數 / 總陳述數
理想值:接近 1.0
代表意義:如果標準答案有 5 個重點,context 只包含 3 個,分數是 0.6
用 RAGAS 評估你的 RAG
1# pip install ragas langchain-openai
2from ragas import evaluate
3from ragas.metrics import (
4 faithfulness,
5 answer_relevancy,
6 context_precision,
7 context_recall,
8)
9from ragas.llms import LangchainLLMWrapper
10from ragas.embeddings import LangchainEmbeddingsWrapper
11from langchain_openai import ChatOpenAI, OpenAIEmbeddings
12from datasets import Dataset
13
14# 準備評估資料集
15# 格式:問題、生成答案、檢索到的 context、標準答案(optional)
16eval_data = {
17 "question": [
18 "Python 的 GIL 是什麼?",
19 "asyncio 的事件迴圈如何工作?",
20 "如何選擇向量資料庫?",
21 ],
22 "answer": [
23 # 你的 RAG 系統生成的答案
24 "GIL(全域解釋鎖)是 Python 的一個機制,確保同一時間只有一個執行緒執行 Python 位元組碼...",
25 "asyncio 的事件迴圈是一個單執行緒的調度器,透過 selector 監控 I/O 事件...",
26 "選擇向量資料庫需要考慮資料規模、是否需要自託管、Filter 功能需求...",
27 ],
28 "contexts": [
29 # 對應的 retrieved chunks(list of list)
30 ["Python GIL 全域解釋鎖,限制多執行緒...", "CPython 實作中 GIL 的影響..."],
31 ["asyncio 事件迴圈透過 selector 監控..."],
32 ["ChromaDB 適合開發環境...", "Pinecone 是雲端託管的向量 DB...", "Qdrant 支援自託管..."],
33 ],
34 "ground_truth": [
35 # 標準答案(用於 context_recall)
36 "GIL 是 Python 的全域解釋鎖,同時間只有一個執行緒可以執行 Python 位元組碼,影響多執行緒 CPU 密集型任務。",
37 "asyncio 事件迴圈是單執行緒的異步調度器,使用 selector 監控 I/O 事件,調度 coroutine 執行。",
38 "向量資料庫選型需考慮:資料規模、自託管需求、Filter 複雜度、成本。小規模用 Chroma,生產環境用 Qdrant 或 Pinecone。",
39 ],
40}
41
42dataset = Dataset.from_dict(eval_data)
43
44# 設定 LLM 和 Embedding(RAGAS 用它們來做評估)
45llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o-mini", temperature=0))
46emb = LangchainEmbeddingsWrapper(OpenAIEmbeddings(model="text-embedding-3-small"))
47
48# 執行評估
49result = evaluate(
50 dataset=dataset,
51 metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
52 llm=llm,
53 embeddings=emb,
54)
55
56# 輸出結果
57print(result.to_pandas()[["faithfulness", "answer_relevancy", "context_precision", "context_recall"]])
58print(f"\n平均分數:")
59print(f" Faithfulness: {result['faithfulness']:.3f}")
60print(f" Answer Relevancy: {result['answer_relevancy']:.3f}")
61print(f" Context Precision: {result['context_precision']:.3f}")
62print(f" Context Recall: {result['context_recall']:.3f}")
用指標診斷問題
Faithfulness 低 → LLM 有幻覺 → 加強 System Prompt、用 Self-RAG
Answer Relevancy 低 → 答案跑題 → 改善 Prompt 設計、問題分解
Context Precision 低 → 取回太多不相關的 chunk → 加 Reranker、調高相似度閾值
Context Recall 低 → 相關資訊沒被找到 → 改善 Chunking、加 Hybrid Search
建立自動化評估流程
1import json
2from datetime import datetime
3from pathlib import Path
4
5
6def evaluate_and_log(rag_pipeline, test_cases: list[dict], output_dir: str = "./eval_logs"):
7 """對 RAG pipeline 做自動評估並記錄結果"""
8 Path(output_dir).mkdir(exist_ok=True)
9
10 results = []
11 for case in test_cases:
12 question = case["question"]
13 ground_truth = case.get("ground_truth", "")
14
15 # 執行 RAG
16 output = rag_pipeline(question)
17 answer = output["answer"]
18 contexts = output["contexts"]
19
20 results.append({
21 "question": question,
22 "answer": answer,
23 "contexts": contexts,
24 "ground_truth": ground_truth,
25 })
26
27 # 用 RAGAS 評估
28 dataset = Dataset.from_list(results)
29 scores = evaluate(dataset=dataset, metrics=[faithfulness, answer_relevancy])
30
31 # 記錄到檔案(版本追蹤)
32 log = {
33 "timestamp": datetime.now().isoformat(),
34 "scores": scores.to_pandas().to_dict(),
35 "num_cases": len(test_cases),
36 }
37 log_path = Path(output_dir) / f"eval_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
38 log_path.write_text(json.dumps(log, ensure_ascii=False, indent=2))
39 print(f"📊 評估完成,結果存至 {log_path}")
40 return scores
Part 2:GraphRAG
向量搜尋的根本限制
向量搜尋擅長找「語意相似的文件」,但對需要跨文件推理的問題很弱:
問題:「A 公司的 CEO 和 B 公司的 CTO 有什麼共同的工作經歷?」
向量搜尋:
→ 找到關於 A 公司 CEO 的文章
→ 找到關於 B 公司 CTO 的文章
→ 但無法連接兩者之間的關係
GraphRAG:
→ 知識圖譜中有:
[A CEO] --曾任職於--> [Google]
[B CTO] --曾任職於--> [Google]
→ 直接推導:兩人都在 Google 工作過
GraphRAG 的核心架構
文件
↓
[實體提取] → 找出文件中的實體(人、公司、概念)
↓
[關係提取] → 找出實體間的關係(任職、合作、隸屬)
↓
[建立知識圖譜] → 儲存在圖資料庫(Neo4j、NetworkX)
↓
查詢時:
向量搜尋(找相關節點) + 圖遍歷(沿關係推理)
用 Python 建立簡易 GraphRAG
1# pip install networkx openai
2import networkx as nx
3import openai
4import json
5
6client = openai.OpenAI(api_key="your-api-key")
7
8
9def extract_entities_and_relations(text: str) -> dict:
10 """用 LLM 從文字中提取實體和關係"""
11 prompt = f"""從以下文字中提取實體和關係,以 JSON 格式輸出。
12
13格式範例:
14{{
15 "entities": ["Alice", "Google", "Bob"],
16 "relations": [
17 {{"from": "Alice", "relation": "任職於", "to": "Google"}},
18 {{"from": "Bob", "relation": "任職於", "to": "Google"}}
19 ]
20}}
21
22文字:{text}
23
24JSON(只輸出 JSON,不要其他內容):"""
25
26 response = client.chat.completions.create(
27 model="gpt-4o-mini",
28 messages=[{"role": "user", "content": prompt}],
29 temperature=0,
30 response_format={"type": "json_object"},
31 )
32 return json.loads(response.choices[0].message.content)
33
34
35class SimpleGraphRAG:
36 def __init__(self):
37 self.graph = nx.DiGraph()
38 self.doc_map : dict[str, str] = {} # entity → 原始文字片段
39
40 def add_document(self, text: str) -> None:
41 """從文字中提取實體關係,加入知識圖譜"""
42 extracted = extract_entities_and_relations(text)
43
44 for entity in extracted.get("entities", []):
45 self.graph.add_node(entity)
46 self.doc_map[entity] = text # 記錄來源文字
47
48 for rel in extracted.get("relations", []):
49 self.graph.add_edge(
50 rel["from"], rel["to"],
51 relation=rel["relation"],
52 )
53 print(f" ✅ 加入 {len(extracted['entities'])} 個實體,{len(extracted['relations'])} 條關係")
54
55 def find_connected_entities(self, entity: str, hops: int = 2) -> list[str]:
56 """找出距離 entity N hop 以內的所有相關實體"""
57 if entity not in self.graph:
58 return []
59 neighbors = set()
60 frontier = {entity}
61 for _ in range(hops):
62 next_frontier = set()
63 for node in frontier:
64 next_frontier.update(self.graph.successors(node))
65 next_frontier.update(self.graph.predecessors(node))
66 neighbors.update(next_frontier)
67 frontier = next_frontier
68 return list(neighbors - {entity})
69
70 def query(self, question: str) -> str:
71 """GraphRAG 查詢:找實體 → 圖遍歷 → 收集 context → LLM 生成"""
72 # Step 1: 從問題中找出實體
73 extract_prompt = f"""從以下問題中提取所有實體名稱(人名、公司名、產品名等)。
74以 JSON 陣列格式輸出,例如:["Alice", "Google"]
75
76問題:{question}
77實體 JSON:"""
78
79 resp = client.chat.completions.create(
80 model="gpt-4o-mini",
81 messages=[{"role": "user", "content": extract_prompt}],
82 temperature=0,
83 )
84 try:
85 query_entities = json.loads(resp.choices[0].message.content)
86 except Exception:
87 query_entities = []
88
89 # Step 2: 圖遍歷,收集相關實體的文字
90 relevant_texts = set()
91 for entity in query_entities:
92 if entity in self.doc_map:
93 relevant_texts.add(self.doc_map[entity])
94 for connected in self.find_connected_entities(entity, hops=2):
95 if connected in self.doc_map:
96 relevant_texts.add(self.doc_map[connected])
97
98 # Step 3: 也加入圖譜中的關係路徑
99 graph_facts = []
100 for u, v, data in self.graph.edges(data=True):
101 for entity in query_entities:
102 if entity in [u, v]:
103 graph_facts.append(f"{u} --[{data['relation']}]--> {v}")
104
105 context_parts = list(relevant_texts)
106 if graph_facts:
107 context_parts.append("【知識圖譜關係】\n" + "\n".join(graph_facts))
108
109 if not context_parts:
110 return "知識庫中找不到相關資訊。"
111
112 context = "\n\n---\n\n".join(context_parts)
113
114 prompt = f"""根據以下知識庫資料(包含文件和關係圖)回答問題。
115
116{context}
117
118問題:{question}"""
119
120 return client.chat.completions.create(
121 model="gpt-4o",
122 messages=[{"role": "user", "content": prompt}],
123 temperature=0,
124 ).choices[0].message.content
125
126
127# 使用範例
128graph_rag = SimpleGraphRAG()
129
130documents = [
131 "Alice 是 OpenAI 的首席研究員,曾在 Google Brain 擔任 AI 研究員,專注於語言模型研究。",
132 "Bob 是 Anthropic 的 CTO,在創辦 Anthropic 之前曾在 Google Brain 擔任資深工程師。",
133 "Charlie 是 DeepMind 的研究總監,與 Alice 共同發表過多篇關於 Transformer 架構的論文。",
134]
135
136for doc in documents:
137 graph_rag.add_document(doc)
138
139answer = graph_rag.query("Alice 和 Bob 有什麼共同的工作背景?")
140print(answer)
GraphRAG vs 向量 RAG 適用場景對比:
| 問題類型 | 向量 RAG | GraphRAG |
|---|---|---|
| 「X 是什麼?」 | ✅ 直接查詢 | ✅ |
| 「A 和 B 的關係?」 | ❌ 難以跨文件 | ✅ 圖遍歷 |
| 「誰跟 X 有關聯?」 | ❌ | ✅ 鄰居搜尋 |
| 「推理鏈:A → B → C」 | ❌ | ✅ 路徑搜尋 |
| 大規模非結構化文字 | ✅ | 需要額外抽取 |
最佳使用場景:組織架構分析、藥物交互作用、學術引用網路、知識密集型問答
Part 3:Agentic RAG
從被動到主動
傳統 RAG 是被動的:接到問題 → 搜尋一次 → 生成答案。
Agentic RAG 讓 AI Agent 自主決策:
- 是否需要搜尋?
- 要搜尋什麼?
- 搜尋結果夠了嗎?還是要再搜一次?
- 要不要對答案做驗證?
使用者問題
↓
Agent 思考:「這個問題需要什麼資訊?」
↓
Agent 決定:「先搜尋 X」
↓
[工具呼叫] search(X) → 結果 1
↓
Agent 思考:「結果不夠完整,還需要 Y」
↓
[工具呼叫] search(Y) → 結果 2
↓
Agent 思考:「資料足夠了,可以回答」
↓
生成最終答案
用 LangGraph 實作 Agentic RAG
1# pip install langgraph langchain-openai
2from typing import Annotated, TypedDict
3from langgraph.graph import StateGraph, START, END
4from langgraph.prebuilt import ToolNode
5from langchain_core.messages import HumanMessage, AIMessage, BaseMessage
6from langchain_openai import ChatOpenAI
7from langchain_core.tools import tool
8import operator
9
10# 定義 State
11class AgentState(TypedDict):
12 messages: Annotated[list[BaseMessage], operator.add]
13 search_count: int # 追蹤搜尋次數,防止無限迴圈
14
15
16# 定義工具(Agent 可以呼叫的函式)
17@tool
18def search_knowledge_base(query: str) -> str:
19 """搜尋公司知識庫,找出與 query 相關的資訊"""
20 # 實際上你會呼叫你的 RAG retriever
21 # 這裡用 mock 資料示意
22 mock_results = {
23 "Python asyncio": "asyncio 是 Python 的標準非同步框架,使用事件迴圈調度協程。",
24 "API 延遲優化": "使用連線池、非同步 I/O、快取等方式可以降低 API 延遲。",
25 "Redis": "Redis 是記憶體內的 key-value 資料庫,常用於快取和訊息佇列。",
26 }
27 for key, value in mock_results.items():
28 if key.lower() in query.lower():
29 return value
30 return f"找不到關於「{query}」的相關資訊。"
31
32
33@tool
34def search_web(query: str) -> str:
35 """搜尋網路上的最新資訊(當知識庫沒有答案時使用)"""
36 # 實際上會呼叫搜尋 API(如 Tavily、Serper)
37 return f"(網路搜尋結果)關於 {query} 的最新資訊:..."
38
39
40tools = [search_knowledge_base, search_web]
41tool_node = ToolNode(tools)
42
43# 設定 LLM
44llm = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
45
46
47def agent_node(state: AgentState) -> dict:
48 """Agent 決策節點:思考下一步要做什麼"""
49 messages = state["messages"]
50 response = llm.invoke(messages)
51 return {"messages": [response]}
52
53
54def should_continue(state: AgentState) -> str:
55 """決定是否繼續搜尋或結束"""
56 last_message = state["messages"][-1]
57
58 # 如果 LLM 決定呼叫工具,繼續
59 if hasattr(last_message, "tool_calls") and last_message.tool_calls:
60 if state["search_count"] >= 5: # 最多 5 次工具呼叫
61 return "end"
62 return "tools"
63
64 # LLM 沒有呼叫工具,表示它準備好回答了
65 return "end"
66
67
68def increment_search_count(state: AgentState) -> dict:
69 """工具呼叫後增加計數"""
70 return {"search_count": state["search_count"] + 1}
71
72
73# 建立 Graph
74graph = StateGraph(AgentState)
75
76graph.add_node("agent", agent_node)
77graph.add_node("tools", tool_node)
78graph.add_node("counter", increment_search_count)
79
80graph.add_edge(START, "agent")
81graph.add_edge("tools", "counter")
82graph.add_edge("counter", "agent")
83
84graph.add_conditional_edges(
85 "agent",
86 should_continue,
87 {"tools": "tools", "end": END},
88)
89
90agentic_rag = graph.compile()
91
92
93def run_agentic_rag(question: str) -> str:
94 """執行 Agentic RAG"""
95 system_prompt = """你是一個知識庫問答助手。
96回答問題時,先使用 search_knowledge_base 搜尋內部知識庫。
97如果內部知識庫沒有足夠資訊,再使用 search_web 搜尋網路。
98確認有足夠資訊後,才提供完整的回答。"""
99
100 initial_state: AgentState = {
101 "messages": [
102 HumanMessage(content=system_prompt + f"\n\n問題:{question}"),
103 ],
104 "search_count": 0,
105 }
106
107 final_state = agentic_rag.invoke(initial_state)
108 return final_state["messages"][-1].content
109
110
111# 使用範例
112answer = run_agentic_rag("如何用 Python asyncio 搭配 Redis 做高效能的非同步快取?")
113print(answer)
Agentic RAG 的核心優勢
場景 1:知識庫夠用
使用者:「Python 的 GIL 是什麼?」
Agent:search_knowledge_base("Python GIL") → 找到答案 → 直接回答
場景 2:需要多次搜尋
使用者:「比較 Redis 和 Memcached,哪個適合用在 asyncio 程式?」
Agent:
1. search_knowledge_base("Redis") → 找到 Redis 資訊
2. search_knowledge_base("Memcached") → 找到 Memcached 資訊
3. search_knowledge_base("asyncio 快取") → 找到整合建議
4. 綜合三次結果回答
場景 3:知識庫沒有,轉向網路
使用者:「GPT-4.1 有哪些新功能?」(知識庫太舊)
Agent:
1. search_knowledge_base("GPT-4.1") → 無結果
2. search_web("GPT-4.1 features 2025") → 找到最新資訊
3. 根據網路搜尋結果回答
優點:靈活應對各種問題類型;可以多輪搜尋直到資訊充足;容易擴充新工具
缺點:延遲不可預測(可能多次迭代);成本隨迭代次數增加;需要設計防止無限迴圈
最佳使用場景:複雜研究性問題、需要整合多個資料源、問題類型多樣的生產系統
完整技術選型路線圖
你的 RAG 問題是什麼?
│
├── 搜尋找不到正確文件
│ ├── 術語/關鍵字問題 → Hybrid Search (BM25 + 向量)
│ ├── 問題措辭問題 → HyDE 或 Multi-Query
│ └── 跨文件推理問題 → GraphRAG
│
├── 找到了但排名不對
│ └── 加 Cross-Encoder Reranker
│
├── 找到了但 context 有噪音
│ └── Context Compression
│
├── 問題本身難以直接搜尋
│ ├── 具體問題 → Step-Back Prompting
│ └── 複雜多跳問題 → Query Decomposition
│
├── 答案有幻覺
│ └── Self-RAG(自我反思)
│
├── 需要處理多樣化的複雜問題
│ └── Agentic RAG(LangGraph)
│
└── 不知道哪裡出問題
└── 先用 RAGAS 評估,找出指標最差的環節
RAG 系統設計的黃金法則
在五篇文章的最後,整理幾個核心原則:
- 先建立評估,再優化:沒有 RAGAS 評分,你不知道優化有沒有效果
- Chunking 策略比模型更重要:在換更強的 LLM 之前,先把 Chunking 做好
- Reranker 幾乎總是值得加:成本低(用本地模型)、效果顯著
- 不要過度工程化:Naive RAG + 好的 Chunking 在很多場景已經夠用
- GraphRAG 和 Agentic RAG 是進階武器:需要清楚的問題驅動,不要為了用而用
系列總結
| 篇章 | 核心主題 | 關鍵技術 |
|---|---|---|
| 第一篇 | RAG 基礎 | Embedding、向量搜尋、Naive RAG |
| 第二篇 | 資料基礎建設 | Chunking 策略、向量 DB 選型 |
| 第三篇 | 進階搜尋 | Hybrid Search、HyDE、Multi-Query、Reranker |
| 第四篇 | 查詢與壓縮 | Step-Back、Self-RAG、Context Compression |
| 第五篇 | 生產與前沿 | RAGAS 評估、GraphRAG、Agentic RAG |
希望這個系列能幫助你從零建立、優化並評估自己的 RAG 系統。
RAG 的技術仍在快速演進,但這五篇涵蓋的核心原理和思路,在可見的未來都會持續適用。
系列導覽