RAG:讓 AI 從閉卷考變開卷考
02 Aug 2026
讀過世界上所有書的天才,被關在一間沒有網路的密室裡。問它歷史、哲學,對答如流。但問它「我們公司昨天發布的預算報告重點是什麼」,它只能愣住,甚至開始一本正經地胡說八道。這就是目前大型語言模型(LLM)的真實處境。不管模型多聰明,它的知識是靜態的——聰明才智僅限於訓練截止那一刻。一旦問題超出記憶範圍,或需要最新的內部資料,它就無能為力,甚至產生所謂的 AI 幻覺(Hallucination)。
閉卷考的困境
LLM 回答問題的方式,就像一個學生參加極度困難的閉卷考——只能靠腦海裡死記硬背的知識作答。
| 問題類型 | 閉卷考(LLM)的表現 |
|---|---|
| 通用知識(法國首都是哪裡) | 對答如流,訓練資料裡有 |
| 訓練截止後的時事 | 卡住或幻覺 |
| 企業內部機密文件 | 完全不知道,可能瞎掰 |
| 需要精確數字(某產品型號規格) | 容易張冠李戴 |
閉卷考的核心問題是:考題一旦超出記憶範圍,就只能猜。LLM 的「猜」不是空白不答,而是用聽起來很合理的語氣產生錯誤內容——這比不回答還危險。
RAG:讓 AI 翻書作答
RAG(Retrieval Augmented Generation,檢索增強生成)的概念很直覺:不再讓 AI 單純依賴預訓練知識,而是在回答前先去查資料。
使用者提問
│
↓
┌─────────────┐
│ 查詢轉換 │ 把問題轉成向量
└──────┬──────┘
│
↓
┌─────────────┐
│ 知識庫檢索 │ 在向量資料庫找最相關的片段
└──────┬──────┘
│
↓
┌─────────────┐
│ 上下文增強 │ 把檢索到的片段 + 原始問題一起丟給 LLM
└──────┬──────┘
│
↓
┌─────────────┐
│ LLM 生成 │ 根據實際資料產生回答
└─────────────┘
用考試來比喻:標準 RAG 就像開卷考——學生可以翻書找答案,不用死背。但 RAG 的「翻書」不是按 Ctrl+F 做關鍵字比對那麼簡單,它需要理解語意。
嵌入(Embeddings):讓 AI 理解語意的數學
要讓 AI 學會「查資料」,第一步是讓它理解意義而非字面。這靠的是嵌入(Embeddings)——將文字轉換成一組數字列表,稱為向量(Vector)。
為了容易理解,我們把高維空間簡化成二維:
y
↑
4 │ ● 小貓 (2.1, 3.1)
3 │ ● 貓 (2, 3)
2 │
1 │ ● 車子 (8, 1)
0 └──────────────────────────→ x
0 2 4 6 8
「貓」和「小貓」在語意上接近,座標距離很近。「車子」跟貓毫無關係,座標遠遠落在另一個角落。
實際應用中,嵌入不是在二維空間運作,而是在數百甚至數千個維度的高維空間中。這讓 AI 對語意的理解極度細緻。
這帶來一個強大的能力:語意搜尋。假設使用者搜尋「想養一隻冬天會窩在懷裡的寵物」,知識庫裡寫的是「家貓——毛茸茸的貓科動物伴侶」。這兩段文字在字面上幾乎沒有共同的詞彙,傳統搜尋引擎只會比對字面重疊,絕對找不到。但在向量空間裡,它們的座標距離極短,模型判斷它們指的是同一件事。
import { Document } from '@langchain/core/documents'
import { OpenAIEmbeddings } from '@langchain/openai'
import { MemoryVectorStore } from '@langchain/classic/vectorstores/memory'
const embeddings = new OpenAIEmbeddings({
model: 'text-embedding-3-small',
})
const store = new MemoryVectorStore(embeddings)
await store.addDocuments([
new Document({ pageContent: '家貓——毛茸茸的貓科動物伴侶', metadata: { source: 'pet-wiki' } }),
new Document({ pageContent: '電動車是未來交通的主流選擇', metadata: { source: 'tech-news' } }),
new Document({ pageContent: '黃金獵犬是最受歡迎的家庭犬之一', metadata: { source: 'pet-wiki' } }),
])
const results = await store.similaritySearch('想養一隻冬天會窩在懷裡的寵物', 2)
// 輸出:
// [
// { pageContent: '家貓——毛茸茸的貓科動物伴侶', ... },
// { pageContent: '黃金獵犬是最受歡迎的家庭犬之一', ... }
// ]
同樣地,「法國的首都是什麼?」和「哪個城市是法國的首都?」結構完全不同,但嵌入模型會算出極為接近的向量——系統看穿了表面措辭,直接抓住核心意圖。
這個「看穿表面措辭」的能力,其實是 RAG 跟傳統搜尋最大的分野,也是最值得玩味的一點。傳統搜尋靠的是「字面重疊」,RAG 靠的是「概念距離」——前者像查字典,後者像懂一個人。但這也帶來一個隱藏風險:語意搜尋太「懂」了,反而可能過度解讀。當使用者問的是模糊概念,系統可能把不相干的文件也視為「概念相近」而撈回來,這正是前面會提到的檢索噪音問題的根源之一。懂語意是好事,但懂得太多,也需要一個把關的機制。
資料切塊(Chunking):把大檔案切成可管理的區塊
就算 AI 能理解語意,我們也不可能把一整本 50 頁的工業機具說明書直接塞進它的上下文視窗。運算會崩潰,成本會爆表,而且大部分內容跟目前的問題根本無關。所以必須先切塊(Chunking)——將大型文件分解成更小、更易管理的部分。
切塊的刀法非常講究:必須保留資訊的完整上下文和語意。不能把一句話硬生生切斷,也不能把故障排除和安裝指南混在同一個區塊裡。
| 切塊策略 | 做法 | 適用場景 |
|---|---|---|
| 固定大小 | 每 512 tokens 切一刀 | 快速原型,文字結構簡單 |
| 語意切塊 | 依段落/章節的自然邊界切分 | 技術文件、法律條文 |
| 遞歸切塊 | 先按大標題切,太大再按小標題切,依此類推 | 階層式文件(如說明書) |
| 重疊切塊 | 相鄰區塊保留 10-20% 的重疊內容 | 避免重要資訊剛好被切斷 |
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500,
chunkOverlap: 50,
separators: ['\n## ', '\n### ', '\n\n', '\n', ' ', ''],
})
const rawDoc = `# 故障排除手冊
## 紅燈閃爍
當機器亮起紅色指示燈時,表示過熱保護已啟動。
請立即關機並等待 15 分鐘冷卻。
## 異音處理
若運轉時出現異常噪音,請檢查進料口是否卡住異物。`
const chunks = await splitter.splitText(rawDoc)
[
'# 故障排除手冊\n\n## 紅燈閃爍\n當機器亮起紅色指示燈時...',
'## 異音處理\n若運轉時出現異常噪音...'
]
當使用者焦急地問「為什麼機器亮紅燈」,RAG 系統只會精準提取「故障排除」那個區塊,而不是把安裝步驟也扯進來。
原始文件
┌──────────────────────────────────────────┐
│ # 安裝指南 # 故障排除 │
│ 步驟 1... 紅燈閃爍... │
│ 步驟 2... 異音處理... │
│ 步驟 3... 錯誤代碼 E01... │
└──────────────────────────────────────────┘
│ Chunking
↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 安裝指南 │ │ 紅燈閃爍 │ │ 異音處理 │
│ 步驟 1-3 │ │ 過熱保護 │ │ 進料口 │
└──────────┘ └──────────┘ └──────────┘
│ Embedding
↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ [0.2, │ │ [0.8, │ │ [0.7, │
│ 0.5, …] │ │ 0.1, …] │ │ 0.3, …] │
└──────────┘ └──────────┘ └──────────┘
存入向量資料庫
向量資料庫與 HNSW:光速檢索的秘密
切塊完成、轉換成向量後,需要一個專屬的家——向量資料庫。使用者查詢也被轉成向量後,系統要在數百萬筆向量中找出距離最近的幾筆。一個個比對當然慢到無法忍受。向量資料庫用一種叫 HNSW(Hierarchical Navigable Small World) 的演算法來加速:
向量資料庫生態非常多元,依部署方式可分三類:
| 類型 | 代表 | 特點 |
|---|---|---|
| 託管資料庫 | Pinecone、Weaviate | 免維護,開箱即用 |
| 開源方案 | ChromaDB、Milvus、Qdrant | 自架部署,可完全掌控 |
| 既有資料庫擴充 | Redis、Elasticsearch、Postgres(pgvector) | 不換資料庫,加上向量搜尋能力 |
底層的檢索機制通常由 FAISS(Meta AI)或 ScaNN(Google Research)等函式庫支撐,這些函式庫決定了系統的檢索效率。本質上,其他技術搜尋單詞,而向量資料庫搜尋含義。
HNSW 分層搜尋示意
Layer 0(最粗): A ──────── D ──────── G
\ /
Layer 1(中等): B ──── E ──── F
\ /
Layer 2(最細): C H
\ /
目標 ✓
HNSW 的概念就像分層地圖:不會一條街一條街慢慢走,而是先從國道高速公路的層級看起,直接跳過完全不相干的城市,空降到目標所在的社區,最後進入巷弄找到最精準的座標。
import { ChromaClient } from 'chromadb'
const client = new ChromaClient({ path: 'http://localhost:8000' })
const collection = await client.createCollection({
name: 'company_docs',
metadata: { 'hnsw:space': 'cosine' },
})
await collection.add({
ids: ['chunk-001', 'chunk-002', 'chunk-003'],
documents: [
'紅燈閃爍表示過熱保護已啟動,請立即關機冷卻',
'異音通常是進料口卡住異物造成',
'每月保養請使用指定的潤滑油型號 LU-200',
],
metadatas: [
{ source: 'troubleshooting.md', page: 3 },
{ source: 'troubleshooting.md', page: 5 },
{ source: 'maintenance.md', page: 1 },
],
})
const results = await collection.query({
queryTexts: ['機器亮紅燈怎麼辦'],
nResults: 2,
})
// results.documents[0] = [
// '紅燈閃爍表示過熱保護已啟動,請立即關機冷卻',
// '每月保養請使用指定的潤滑油型號 LU-200'
// ]
混合搜尋:BM25 + 語意的互補
語意搜尋擅長捕捉概念相關性,但有時候我們就是需要精確匹配某個產品型號或專有名詞。這時只看語意反而會漏掉。實務上通常採用混合搜尋(Hybrid Search)——把傳統關鍵字演算法 BM25 和現代語意搜尋結合起來:
| 搜尋方式 | 強項 | 弱項 |
|---|---|---|
| BM25(關鍵字) | 精確匹配型號、專有名詞 | 無法理解同義詞或換句話說 |
| 語意搜尋(向量) | 理解語意、捕捉概念相關性 | 可能漏掉字面精確但語意遠的詞 |
| 混合搜尋 | 兩者兼顧 | 需要調權重,系統較複雜 |
import { OpenAIEmbeddings } from '@langchain/openai'
import { WeaviateStore } from '@langchain/weaviate'
const store = await WeaviateStore.fromExistingIndex(embeddings, {
client: weaviateClient,
indexName: 'ProductDocs',
textKey: 'content',
metadataKeys: ['source', 'product_id'],
})
// Weaviate 原生支援 hybrid search
// alpha=1 純語意,alpha=0 純 BM25,0.5 各半
const results = await store.hybridSearch('錯誤代碼 E-4012 怎麼處理', {
alpha: 0.5,
limit: 5,
})
「錯誤代碼 E-4012」這種精確術語靠 BM25 抓住,「怎麼處理」這種概念性問題靠語意搜尋補強,兩者互補。
混合搜尋的實務心得是:別急著追求複雜的權重演算法,先搞清楚使用者怎麼描述問題。如果使用者習慣貼上完整型號或錯誤代碼,BM25 的權重要調高;如果使用者習慣用「東西壞了」「跑不動」這種模糊說法,語意搜尋才是主力。這個判斷沒有標準答案,只能靠實際查詢紀錄去校準——這也是為什麼前面說檢索評估比建置更重要。
實務檢索參數:像混音器一樣調校
RAG 系統有兩個最常用的檢索旋鈕,直接控制「撈多少」和「撈多近」:
| 參數 | 作用 | 比喻 |
|---|---|---|
| similarity_top_k | 決定撈回前幾個最相似的結果 | 混音器的音量旋鈕——3 或 10 決定資料量 |
| vector_distance_threshold | 設定語意距離上限,太遠的結果直接丟掉 | 濾波器——確保抓到的資料不會太離題 |
以 Google Vertex AI RAG 搭配 ADK(改寫為 TypeScript)為例:
import { LlmAgent, VertexRagRetrievalTool } from '@google/adk'
const ragTool = new VertexRagRetrievalTool({
ragResources: [{
ragCorpus: 'projects/your-gcp-project-id/locations/us-central1/ragCorpora/your-corpus-id',
}],
// 控制要檢索的最相似結果數量
similarityTopK: 5,
// 限制檢索結果的語意距離,超過此門檻的結果會被過濾掉
vectorDistanceThreshold: 0.7,
})
const ragAgent = new LlmAgent({
name: 'support_agent',
model: 'gemini-flash-latest',
tools: [ragTool],
})
SIMILARITY_TOP_K 控制資料量,VECTOR_DISTANCE_THRESHOLD 控制資料品質——兩個旋鈕配合,開發者就能像操作混音器一樣精準調整 RAG 的檢索行為。
Google Search 工具:RAG 的最簡單形態
RAG 涉及存取外部資訊,Google Search 工具是最直接的內建檢索機制——代理接到問題直接上網查,不需要自建知識庫:
import { GOOGLE_SEARCH, LlmAgent } from '@google/adk'
const searchAgent = new LlmAgent({
name: 'research_assistant',
model: 'gemini-flash-latest',
instruction: 'You help users research topics. When asked, use the Google Search tool',
tools: [GOOGLE_SEARCH],
})
這是「外部知識」最輕量的注入方式:代理在推理過程中呼叫搜尋工具取得即時資訊。自建知識庫的 RAG 適合需要精確控制來源的企業場景,而 Google Search 工具適合即時、開放式的研究問題——兩者互補而非取代。
標準 RAG 的盲點:跨檔案的碎片化答案
標準 RAG 有一個結構性盲點:答案散落在不同地方時,系統會抓到不完整的上下文,甚至引入噪音。
假設回答一個問題需要綜合三份文件:
文件 A(第 14 頁) 文件 B(第 3 頁) 文件 C(第 7 頁)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 專案 Alpha 預算 │ │ 專案 Alpha 進度 │ │ 專案 Alpha 風險 │
│ 總額 500 萬 │ │ 延遲 3 個月 │ │ 供應鏈中斷 │
│ │ │ │ │ 影響預算 15% │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└────────────────────────┼────────────────────────┘
│
標準 RAG 檢索
│
可能只抓到 A 和 B,漏掉 C
→ 不知道預算已經被供應鏈影響追加 15%
標準 RAG 把每份文件當作獨立的區塊比對,看不見文件之間的關聯。這就是 GraphRAG 要解決的問題。
GraphRAG:用知識圖譜看懂因果關係
GraphRAG 放棄了單純的向量比對,轉而建立知識圖譜(Knowledge Graph)。這就像警匪片裡偵探在牆上貼滿照片,用圖釘和紅線把人物、地點、事件全部串聯起來的那個線索板:
知識圖譜示意
┌──────────┐
│ 專案Alpha │
└────┬─────┘
│
┌───────┼───────┐
│ │ │
▼ ▼ ▼
┌────────┐┌──────┐┌──────────┐
│預算500萬││延遲3月││供應鏈中斷│
└───┬────┘└──────┘└────┬─────┘
│ │
└───── 因果 ────────┘
追加 15% 預算
標準 RAG 看到三份獨立文件,GraphRAG 看出背後錯綜複雜的網路。在金融分析中,它可以把一間公司的財報、某個市場事件、一位高階主管的異動,全部用紅線連起來看出因果關係。在基因與疾病關聯的科學研究中,這種能力是殺手級應用。
但從工程實務的角度,GraphRAG 的定位可能會有不同看法:它與其說是一個「升級版 RAG」,不如說是一個「不同取向的取捨」。標準 RAG 把建置成本花在「嵌入品質」,GraphRAG 把成本花在「關係抽取」。同樣的預算,前者買的是覆蓋率,後者買的是洞察力。對多數企業而言,文件更新頻繁、關係又多變,知識圖譜的維護成本會吃掉 RAG 帶來的效率紅利——這不是技術不好,而是投資報酬率的問題。GraphRAG 真正的適用場景,是那些關係比文件本身更值錢的領域,而不是把每份文件都硬塞進圖譜。
但知識圖譜的代價也高:
| 面向 | 標準 RAG | GraphRAG |
|---|---|---|
| 建置成本 | 低,切塊 + 嵌入即可 | 高,需人工或 AI 抽取實體關係 |
| 維護難度 | 低 | 高,關係變動需同步更新圖譜 |
| 靈活性 | 高,加檔案即可 | 低,圖結構建好後難以快速調整 |
| 查詢延遲 | 毫秒級 | 秒級(需遍歷圖結構) |
| 依賴品質 | 低,頂多撈到無關區塊 | 極高,圖結構品質決定一切 |
| 適用場景 | FAQ、文件檢索 | 金融分析、科學研究、供應鏈追蹤 |
查個公司規定用 GraphRAG 就太大材小用了——殺雞不用牛刀。
代理式 RAG(Agentic RAG):給 RAG 裝上大腦
前面介紹的 RAG 不管標準還是 Graph 版本,都是被動地把檢索到的資料丟給 LLM。如果抓回來的資料有衝突、過期、或根本答非所問,系統也照單全收。代理式 RAG(Agentic RAG) 是一場典範轉移——在檢索和生成之間加入了一個具備推理和決策能力的代理層。
標準 RAG vs 代理式 RAG
標準 RAG:
查詢 → 檢索 → 丟給 LLM → 回答
(被動,不管資料品質)
代理式 RAG:
查詢 → 代理分析問題 → 拆解子任務
│
┌───────────────┼───────────────┐
↓ ↓ ↓
檢索來源 A 檢索來源 B 呼叫外部 API
│ │ │
└───────────────┼───────────────┘
│
代理驗證資料品質
├── 日期是否過期?
├── 來源之間是否衝突?
└── 資訊是否完整?
│
↓
組裝後交給 LLM
自我驗證:過濾過期資料
假設使用者問「公司最新的遠距工作政策是什麼?」標準 RAG 可能把 2020 年的舊部落格和 2025 年的官方政策一起撈出來,LLM 把它們混為一談,產生一個充滿矛盾的答案。
代理式 RAG 不急著回答,而是先看日期。讓我們把這個情境拆開來看。向量資料庫用語意搜尋撈出 10 個相關區塊,其中兩個信心最高:
| 區塊 | 內容摘要 | 日期 | 來源類型 |
|---|---|---|---|
| chunk-001 | 「遠距工作每週 2 天,需主管同意」 | 2020-03-15 | 部落格 |
| chunk-002 | 「遠距工作每週 3 天,可申請跨國遠距」 | 2025-06-01 | 官方政策 |
標準 RAG:照單全收
標準 RAG 沒有日期概念。它把兩個區塊的內容原封不動塞進 context,丟給 LLM:
查詢:公司最新的遠距工作政策是什麼?
│
▼
向量搜尋(top-k=2)
│
├── chunk-001(2020 部落格):每週 2 天
└── chunk-002(2025 官方):每週 3 天
│
▼
LLM 生成 → 「遠距工作為每週 2-3 天,需主管同意,
特殊情況下可申請跨國遠距。」
↑
前半段是 2020 年舊政策,後半段是 2025 年新政策
混在一起,使用者根本不知道哪個才是「現在」的規則
LLM 是機率模型,不會自動區分「這份文件比較舊」——它只看見兩份都「看起來很相關」的文件,然後把兩者的資訊平滑地融合。使用者得到的是一個四不像的答案:既有舊政策的「每週 2 天、需主管同意」,又有新政策的「跨國遠距」。問「最新」的政策,得到的卻是一份時間錯亂的大雜燴。
代理式 RAG:先檢查後回答
代理式 RAG 多了一道驗證層,在把資料交給 LLM 之前先做兩件事:
查詢:公司最新的遠距工作政策是什麼?
│
▼
代理拆解 → 需要「最新」的政策,先檢索再驗證
│
▼
向量搜尋(top-k=10)
│
├── chunk-001(2020 部落格)
├── chunk-002(2025 官方)
└── ...(其他 8 筆)
│
▼
代理驗證
├── Step 1:計算每筆與今天的時間差
├── Step 2:過濾超過 365 天的舊資料
│ → chunk-001(2020)被丟棄 ✗
├── Step 3:依來源權威性排序
│ → 官方政策 > 備忘錄 > 部落格
└── Step 4:只保留最可信的版本
→ chunk-002(2025 官方政策)✓
│
▼
組裝後交給 LLM → 「遠距工作每週 3 天,
可申請每年最多 30 天跨國遠距。」
對應的程式邏輯是 filterAndRank:先算每個區塊的 ageDays,超過一年的過濾掉,剩下的依來源權威性(官方政策 > 備忘錄 > 部落格)排序:
type RetrievedChunk = {
content: string
metadata: {
source: string
date: string
authority: 'blog' | 'official_policy' | 'memo'
}
}
function filterAndRank(chunks: RetrievedChunk[]): RetrievedChunk[] {
const now = new Date()
return chunks
.map(chunk => ({
...chunk,
ageDays: Math.floor(
(now.getTime() - new Date(chunk.metadata.date).getTime()) / 86400000
),
}))
.filter(chunk => chunk.ageDays < 365)
.sort((a, b) => {
const authorityScore = { official_policy: 3, memo: 2, blog: 1 }
return authorityScore[b.metadata.authority] - authorityScore[a.metadata.authority]
})
}
const rawChunks = await vectorStore.similaritySearch('遠距工作政策', 10)
const verified = filterAndRank(rawChunks)
// chunk-001(2020 部落格):ageDays 超過 365 → 被過濾掉
// chunk-002(2025 官方政策):保留,權威性最高 → 排最前面
驗證層的過濾結果
把 10 個檢索結果丟進 filterAndRank,實際輸出差異:
| 區塊 | 日期 | ageDays | 權威性 | 過濾後 |
|---|---|---|---|---|
| chunk-001 | 2020-03-15 | 約 1,960 天 | blog | ✗ 過期 |
| chunk-002 | 2025-06-01 | 約 60 天 | official_policy | ✓ 採用 |
| chunk-003 | 2025-02-20 | 約 160 天 | memo | ✓ 保留 |
| chunk-004 | 2021-08-10 | 約 1,450 天 | memo | ✗ 過期 |
| chunk-005 | 2024-11-05 | 約 270 天 | blog | ✓ 保留 |
過濾後只剩 3 筆一年內的資料,代理再把它們依權威性排序後交棒給 LLM。關鍵差別在於:資料品質的把關從 LLM 的機率判斷,移到確定性的程式邏輯——ageDays < 365 和權威性分數是明確的規則,不會因為模型當天狀態好壞而改變結果。
衝突解決:辨識資料矛盾
財務分析師問「Alpha 專案的預算是多少?」系統同時抓到了寫著 5 萬歐元的初始提案,和寫著 6.5 萬歐元的最終財務報告。哪個才是對的?
把這個情境拆開來看。向量資料庫撈出兩份高度相關的文件:
| 區塊 | 內容 | 金額 | 文件類型 | 日期 |
|---|---|---|---|---|
| budget-001 | Alpha 專案初始提案 | €50,000 | initial_proposal | 2025-01-15 |
| budget-002 | Alpha 專案最終財務報告 | €65,000 | final_report | 2025-06-30 |
兩份文件都「很相關」,但金額差了 15,000 歐元。如果直接丟給 LLM,它無法確定哪份才是定案數字。
標準 RAG:矛盾沒被發現
標準 RAG 把兩份文件都塞進 context,LLM 看到的是「50,000」和「65,000」兩個數字同時存在:
查詢:Alpha 專案的預算是多少?
│
▼
向量搜尋(top-k=2)
│
├── budget-001:€50,000(初始提案)
└── budget-002:€65,000(最終財務報告)
│
▼
LLM 生成 → 「Alpha 專案預算為 5 萬歐元,最終財務報告
顯示為 6.5 萬歐元。」
↑
兩個數字都「正確引用」了來源,
但使用者還是不知道實際預算是多少
LLM 如實引用兩份文件,看起來很誠實,實際上等於沒回答——它把矛盾原封不動丟回給使用者。財務分析師拿到「50,000 還是 65,000?你自己決定」這種答案,根本無法做任何決策。
代理式 RAG:辨識矛盾並仲裁
代理式 RAG 不只檢索,還多做了衝突偵測與仲裁:
查詢:Alpha 專案的預算是多少?
│
▼
代理拆解 → 需要「定案」的預算數字
│
▼
向量搜尋
│
├── budget-001:€50,000(initial_proposal)
└── budget-002:€65,000(final_report)
│
▼
代理驗證
├── Step 1:偵測到金額矛盾
│ → budget-001 與 budget-002 數字不一致
├── Step 2:比較文件階層
│ → final_report(最終報告)> amendment(修正案)
│ > initial_proposal(初始提案)
├── Step 3:確認時間順序
│ → budget-002 日期較新,是最終定稿
└── Step 4:採用 final_report 的 €65,000
│
▼
組裝後交給 LLM → 「Alpha 專案最終預算為 6.5 萬歐元」
關鍵不是「選一個數字」,而是證明為什麼選這個。代理依賴兩條確定性的規則:
- 文件階層:
final_report(最終報告)的權威性高於initial_proposal(初始提案)——提案是起點,報告是結論 - 時間順序:最終財務報告(2025-06-30)晚於初始提案(2025-01-15),代表之後的修訂都已包含在內
對應的程式邏輯是 resolveConflict,用 docType 的權威性分數選出最高階的文件:
type BudgetDoc = {
amount: number
currency: string
docType: 'initial_proposal' | 'final_report' | 'amendment'
date: string
}
function resolveConflict(docs: BudgetDoc[]): BudgetDoc {
const priority: Record<BudgetDoc['docType'], number> = {
final_report: 3,
amendment: 2,
initial_proposal: 1,
}
return docs.reduce((best, current) =>
priority[current.docType] > priority[best.docType] ? current : best
)
}
const budgetDocs: BudgetDoc[] = [
{ amount: 50000, currency: 'EUR', docType: 'initial_proposal', date: '2025-01-15' },
{ amount: 65000, currency: 'EUR', docType: 'final_report', date: '2025-06-30' },
]
const answer = resolveConflict(budgetDocs)
// { amount: 65000, docType: 'final_report' } → 回答 6.5 萬歐元
仲裁結果的比較
把 resolveConflict 套用在不同情境,看它如何處理各類矛盾:
| 情境 | 檢索到的文件 | 仲裁結果 | 理由 |
|---|---|---|---|
| 版本衝突 | 初始提案 €50,000 vs 最終報告 €65,000 | €65,000 | 最終報告階層最高 |
| 修正案 | 最終報告 €65,000 vs 修正案 €70,000 | €70,000 | 修正案晚於報告,包含追加 |
| 同階層 | 兩份都是備忘錄,金額不同 | 取日期較新者 | 階層相同,比時間 |
第三種情況是 resolveConflict 的盲點——如果兩份文件階層相同,它無法判斷。實務上要加上日期比較做第二層仲裁,這也說明了為什麼代理式 RAG 的驗證邏輯需要精心設計,而不是一組規則打天下。
這個衝突解決過程如何呈現給使用者,我們留到後面的 對 UI/UX 的影響 段落討論。
多步驟推理:拆解大問題
如果使用者問「我們的產品跟競爭對手 X 相比,功能和價格有什麼差異?」標準 RAG 可能不知所措,但代理式 RAG 會自動拆解:
大問題:「我們 vs 對手 X,功能和價格差異?」
│
代理拆解為 4 個子任務
│
├── 1. 檢索我方產品功能
├── 2. 檢索我方產品價格
├── 3. 檢索對手 X 功能
└── 4. 檢索對手 X 價格
│
代理拼湊結果
│
↓
┌─────────────────────────────────┐
│ 比較表 │
│ ┌────────┬────────┬──────────┐ │
│ │ │ 我方 │ 對手 X │ │
│ ├────────┼────────┼──────────┤ │
│ │ 功能 │ A, B, C│ A, B, D │ │
│ │ 價格 │ $299 │ $349 │ │
│ └────────┴────────┴──────────┘ │
└─────────────────────────────────┘
│
交給 LLM 寫分析報告
type SubTask = {
question: string
source: 'internal' | 'external'
}
function decomposeQuestion(originalQuery: string): SubTask[] {
return [
{ question: '我方產品有哪些功能', source: 'internal' },
{ question: '我方產品的定價', source: 'internal' },
{ question: '競爭對手 X 的產品功能', source: 'external' },
{ question: '競爭對手 X 的產品定價', source: 'external' },
]
}
async function agenticRAG(query: string) {
const subTasks = decomposeQuestion(query)
const results = await Promise.all(
subTasks.map(async (task) => {
if (task.source === 'internal') {
return internalStore.similaritySearch(task.question, 3)
}
return externalSearchAPI(task.question)
})
)
const verified = results.map((chunks, i) => ({
subTask: subTasks[i].question,
chunks: filterAndRank(chunks),
}))
return llm.generate(buildComparisonPrompt(query, verified))
}
主動使用外部工具:知道什麼時候該上網
代理式 RAG 最具突破性的能力:當內部資料不夠用時,它知道要去外面找。
如果使用者問「昨天新發布的產品市場反應如何?」公司內部資料庫不會有今天的新聞。標準 RAG 只能回答「找不到相關資料」,代理式 RAG 會意識到這個知識缺口,主動啟動外部搜尋 API,抓取最新的新聞和社群媒體情緒,整合成完整的答案。
type Tool = {
name: string
description: string
execute: (query: string) => Promise<string>
}
const tools: Tool[] = [
{
name: 'internal_search',
description: '搜尋公司內部文件庫',
execute: (q) => internalStore.similaritySearch(q, 5).then(r => JSON.stringify(r)),
},
{
name: 'web_search',
description: '搜尋網際網路上的最新資訊',
execute: (q) => tavilySearchAPI(q),
},
{
name: 'sentiment_analysis',
description: '分析社群媒體對特定主題的情緒',
execute: (q) => sentimentAPI(q),
},
]
async function agenticRAGWithTools(query: string) {
const plan = await llm.plan({
query,
availableTools: tools.map(t => ({ name: t.name, description: t.description })),
})
// plan = [
// { tool: 'internal_search', input: '產品發布新聞稿' },
// { tool: 'web_search', input: '產品市場反應 評價' },
// { tool: 'sentiment_analysis', input: '產品 Twitter 討論' },
// ]
const results = []
for (const step of plan) {
const tool = tools.find(t => t.name === step.tool)!
results.push(await tool.execute(step.input))
}
return llm.generate(buildAnswerPrompt(query, results))
}
兩種 RAG 的比喻
| 標準 RAG | 代理式 RAG | |
|---|---|---|
| 比喻 | 很盡責但有點死腦筋的圖書館員 | 高薪聘請的資深研究助理 |
| 行為 | 點什麼關鍵字就去書架把相關的書全部抱來,不管內容有沒有衝突 | 先過濾掉過期雜誌,交叉比對衝突資料,圖書館沒有就自己跑出去查 |
| 成本 | 低 | 高(推理、驗證、呼叫工具都消耗 token 和時間) |
| 風險 | 資料過期或衝突時給出錯誤答案 | 代理本身成為新的錯誤來源——邏輯沒寫好可能陷入無限迴圈、誤解任務、或不正確地丟棄相關資訊 |
| 回應速度 | 快(毫秒級) | 較慢(秒級,多輪推理) |
RAG 技術演進總覽
閉卷考(純 LLM)
│
│ 加入檢索
↓
標準 RAG(嵌入 + 向量搜尋)
│
│ 解決跨檔案盲點
↓
GraphRAG(知識圖譜)
│
│ 加入推理和決策
↓
代理式 RAG(Agentic RAG)
├── 自我驗證來源
├── 衝突解決
├── 多步驟推理
└── 主動使用外部工具
| 技術 | 核心能力 | 解決的問題 | 代價 |
|---|---|---|---|
| 嵌入(Embeddings) | 語意理解 | 字面不同但意思相同的搜尋 | 高維計算成本 |
| 切塊(Chunking) | 上下文管理 | 大文件無法全部塞進 context | 切錯會丟失語意 |
| 向量資料庫 + HNSW | 光速檢索 | 數百萬筆資料中快速找最近鄰 | 索引維護成本 |
| 混合搜尋 | 精確 + 語意 | 專有名詞的精確匹配 | 權重調校複雜度 |
| GraphRAG | 跨文件關聯 | 答案散落多處、需要因果推理 | 建置和維護成本極高 |
| 代理式 RAG | 批判性思考 | 資料過期、衝突、多步驟問題 | 運算成本和回應延遲 |
實際應用場景:RAG 在哪裡發揮作用
RAG 不是只在實驗室裡的概念,它已經在各種產業落地。但如果把應用場景攤開來看,會發現真正讓 RAG 發光發熱的,不是那些炫技的案例,而是四個可能「無聊但很痛」的場景:
| 應用場景 | 做法 | 帶來的價值 |
|---|---|---|
| 企業搜尋與問答 | 用 HR 政策、技術手冊、產品規格建立內部聊天機器人 | 員工隨時查詢內部規定,不用等 HR 或翻舊文件 |
| 客戶支援和幫助台 | 存取產品手冊、FAQ、歷史支援票證 | 對客戶查詢給出精確一致的回應,減少人工介入 |
| 個人化內容推薦 | 依使用者偏好與互動歷史做語意檢索 | 推薦更相關的內容,而非只是關鍵字配對 |
| 新聞和時事摘要 | 串接即時新聞源,提示時自動檢索最近文章 | 產生最新摘要,克服訓練資料過期的限制 |
以企業搜尋為例,這是 RAG 最典型的落地場景。員工問「今年的特休規定是什麼?」系統從 HR 政策文件中撈出相關段落,附上出處回覆。HR 不用再每天回答同樣的問題,員工也能隨時取得最新版本——這就是前面提到的「遠距工作政策」情境的實務版。
個人化內容推薦則展現了語意搜尋的威力。傳統推薦系統靠關鍵字配對——使用者看過「跑鞋」,系統就推薦含「跑鞋」字眼的商品。RAG 式推薦理解語意概念——使用者看過「週末路跑訓練」,系統能推薦「輕量緩震運動鞋」,即使兩段文字沒有任何共同關鍵字。
新聞和時事摘要解決的是訓練資料過期的問題。模型知識停在訓練截止日,但新聞每天在發生。透過 RAG 串接即時新聞源,系統就能對「今天早上發布的晶片出口管制」這類問題,檢索最近的文章產出摘要。
何時該用 RAG:經驗法則
當需要 LLM 回答「不屬於它原始訓練資料」的特定、最新或專有資訊時,就用 RAG。它特別適合:
- 基於內部文件的問答系統(公司政策、技術規格)
- 客戶支援機器人(需要可驗證、有引用的事實回應)
- 任何需要引用來源、確保事實正確的應用
但就經驗來說,如果答案靠常識就能回答,RAG 不只幫不上忙,甚至會扯後腿。想想看,問「台灣的首都是哪裡」,RAG 系統卻去檢索「台北市發展史」的舊文件,反而把確定的事情變模糊了。RAG 的價值是「補充模型不知道的知識」,不是「重複模型已經知道的」。開頭閉卷考的比喻同樣適用在這裡——只有需要翻書的題目,才需要準備書。
對 UI/UX 的影響:讓使用者知道答案從哪來
RAG 系統的 UI/UX 設計核心是信任感——使用者需要知道 AI 的回答是基於什麼資料,而不是又在「一本正經地胡說八道」。
來源引用與信心指標
每一句 AI 回答都應該標示它的資料來源。使用者點擊就能看到原始文件的相關段落,而不是只看到最終答案。
以企業內部知識庫的問答介面為例:
┌─────────────────────────────────────────────────────────┐
│ 💬 公司知識助手 │
├─────────────────────────────────────────────────────────┤
│ │
│ 使用者:今年的遠距工作政策有什麼改變? │
│ │
│ AI: │
│ 根據 2025 年最新政策 [1],遠距工作天數從每週 2 天 │
│ 放寬至 3 天,且新增「跨國遠距」選項,每年可申請 │
│ 最多 30 天在海外工作 [2]。 │
│ │
│ ┌─ 參考來源 ─────────────────────────────────────┐ │
│ │ [1] HR-Policy-2025-v3.pdf p.12 │ │
│ │ 來源類型:官方政策 | 日期:2025-03-15 │ │
│ │ 信心:94% █████████░░ │ │
│ │ │ │
│ │ [2] Remote-Work-Guidelines-2025.pdf p.3 │ │
│ │ 來源類型:執行細則 | 日期:2025-04-01 │ │
│ │ 信心:89% █████████░░ │ │
│ │ │ │
│ │ ⚠ 已排除過期來源: │ │
│ │ work-from-home-blog-2020(部落格,2020 年) │ │
│ │ 過期天數超過一年,判定為失效 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ⚠ 未找到 2025 年之前的政策對比資料,無法提供變更容易 │
│ 程度分析。 │
└─────────────────────────────────────────────────────────┘
把「被排除的舊資料」也顯示出來,比只顯示最終答案更有價值——使用者不只得到正確答案,還能理解系統為什麼沒把 2020 年的舊政策當一回事。這正是 RAG 建立信任的關鍵:答案的依據不只是「用了哪些來源」,也包括「排除了哪些來源」。
衝突解決的可視化
衝突解決不該默默進行——使用者需要看到「兩個數字矛盾,系統選擇了哪個、為什麼」。
為什麼這對財務場景特別重要?預算數字差 15,000 歐元不是小事。如果系統直接回覆「Alpha 專案預算為 6.5 萬歐元」而沒提到另一份文件,財務分析師只會得到兩個後果:一是完全信任這個數字,日後發現原始提案寫的是 5 萬歐元時質疑系統的可靠性;二是系統看起來「挑了一個數字」,卻無法核對挑選的理由。無論哪一種,信任都建立在沙地上。
把衝突解決的過程攤開,有三個層次的資訊可以呈現:
| 層次 | 內容 | 解決的問題 |
|---|---|---|
| 結果 | 最終採用哪個數字 | 「預算是多少」 |
| 理由 | 為什麼選它(階層 + 日期) | 「憑什麼是這個數字」 |
| 脈絡 | 衝突的其他來源、金額落差 | 「系統有沒有注意到矛盾」 |
以財務分析儀表板為例:
┌─────────────────────────────────────────────────────────┐
│ 💰 Alpha 專案預算查詢 │
├─────────────────────────────────────────────────────────┤
│ │
│ 使用者:Alpha 專案的預算是多少? │
│ │
│ AI: │
│ Alpha 專案最終預算為 6.5 萬歐元。 │
│ │
│ ┌─ 資料衝突已解決 ────────────────────────────────┐ │
│ │ ⚠ 偵測到 2 份文件金額不一致: │ │
│ │ │ │
│ │ 初始提案 €50,000 2025-01-15 (低階) │ │
│ │ 最終財務報告 €65,000 2025-06-30 (高階) ✓ │ │
│ │ │ │
│ │ → 採用最終財務報告:階層最高 + 日期最新 │ │
│ │ │ │
│ │ 金額落差:€15,000(需主管知悉) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────────────────────┐ │
│ │ 檢視全文 │ │ 查看全部衝突來源 │ │
│ └──────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
這個介面有三個設計決策:
1. 結果與脈絡分層
答案「6.5 萬歐元」放在最上面,是使用者最想知道的。衝突細節收在下方面板,想深入的人才展開——不會讓所有使用者都被 15,000 歐元的落差訊息轟炸。
2. 採用與否都有視覺標記
採用的一邊標 ✓,被排除的一邊標 (低階)。使用者掃一眼就知道系統「看過另一份文件」,而不是漏掉了它。這正是信任的關鍵:系統的可靠,來自於它承認有另一份文件存在。
3. 金額落差單獨標示
金額落差:€15,000 這一行不是裝飾——它把「矛盾的重要性」直接量化。對財務人員來說,「兩個數字差了一點點」和「差了 15,000 歐元」是截然不同的情境。落差愈大,愈需要人類介入確認。
反面對照:如果系統不顯示衝突,財務分析師只會看到一句「預算為 6.5 萬歐元」,沒有來源、沒有矛盾資訊。一旦日後他翻到初始提案文件發現金額不同,對系統的信任會瞬間崩塌——因為他無從得知系統「有意識地」選擇了最終報告,只會以為系統「忽略了」另一份文件。衝突可視化不只是呈現真相,更是在保護系統自己的可信度。 把「有衝突」這件事攤開來,比假裝沒看見更可靠——財務分析師看到系統主動偵測到 15,000 歐元的落差,會比看到一個乾淨漂亮的數字更信任它。
檢索過程透明化
代理式 RAG 的多步驟推理過程也應該對使用者可見,讓他們理解 AI 是怎麼得到這個答案的。
為什麼這很重要?使用者問「我們的產品跟競爭對手 X 相比,功能和價格有什麼差異?」代理式 RAG 會在背景拆解成四個子任務、分別檢索、最後組裝。如果不把過程攤開,使用者只會看到一個看起來「一下子就生出來」的比較報告——但這份報告背後跑了四個搜尋、比對了兩輪資料,這些工作使用者完全看不到。可見的推理過程,是把「黑箱魔法」變成「可以核對的流程」。
標準 RAG:一次檢索賭一把
標準 RAG 看到這個問題只做一次語意搜尋:
查詢:我們跟對手 X 的功能和價格差異?
│
▼
一次向量搜尋(top-k=5)
│
└── 撈到 5 個「最相關」的區塊
├── 我方產品功能(可能)
├── 我方產品價格(可能)
├── 對手 X 的新聞稿(可能)
└── ...(相關度高的都被撈出來,不分主題)
│
▼
LLM 生成 → 從混合的區塊硬擠出一個答案
問題在於「最相關的 5 個區塊」可能全都落在同一個主題——例如全都講我方產品功能,而對手 X 的價格資料根本沒被撈到。一次檢索賭的是「五個區塊剛好涵蓋所有答案」,但這個問題同時需要四個主題的資料,賭輸的機率很高。
代理式 RAG:拆解 → 分工檢索 → 組裝
代理式 RAG 把大問題先拆成子任務,每個子任務做一次精準的檢索:
查詢:我們跟對手 X 的功能和價格差異?
│
▼
Step 1:拆解問題
│
├── 子任務 1:我方產品功能
├── 子任務 2:我方產品定價
├── 子任務 3:對手 X 的產品功能
└── 子任務 4:對手 X 的產品定價
│
▼
Step 2-5:分工檢索(可平行執行)
│
├── 內部搜尋「我方功能」 → 產品規格表
├── 內部搜尋「我方定價」 → 價目表
├── 外部搜尋「對手 X 功能」 → 競品分析報告
└── 外部搜尋「對手 X 定價」 → 官方網站
│
▼
Step 6:驗證每份資料的時效與來源
│
▼
Step 7:組裝比較表 → 交給 LLM 生成報告
對應的程式邏輯是 decomposeQuestion 把問題拆成子任務,再用 Promise.all 平行檢索(程式碼可參考多步驟推理)。
拆解 vs 不拆解的差異
以「我方價格」這類子任務為例,拆解後的效果差異很明顯:
| 面向 | 標準 RAG(一次檢索) | 代理式 RAG(拆解檢索) |
|---|---|---|
| 檢索次數 | 1 次 | 4 次(平行) |
| 主題涵蓋 | 賭 top-k 剛好涵蓋所有主題 | 每個主題保證一次檢索 |
| 漏抓風險 | 高——5 個區塊可能都落在同一主題 | 低——四個子任務各自搜尋 |
| 回應延遲 | 快(單次) | 較慢(多輪,但平行化抵銷部分成本) |
關鍵洞察:拆解後,檢索的召回率(Recall)大幅提升。標準 RAG 的 top-k 是「整段文件最相關的區塊」,拆解後的 top-k 是「每個主題各自最相關的區塊」——後者保證了資訊的多樣性,不會因為五個區塊剛好都是同一個主題而漏掉對手資訊。
把推理過程變成一張可核對的流程圖
這個過程要對使用者可見,不是丟一個終端機 log,而是用介面語言呈現成「使用者看得懂的故事」:
┌─────────────────────────────────────────────────────────┐
│ 🔍 推理過程 │
├─────────────────────────────────────────────────────────┤
│ │
│ 問題:我們跟對手 X 的產品差異? │
│ │
│ 拆解成 4 個子任務: │
│ │
│ Step 1 ✓ 搜尋內部文件:我方產品功能 0.8s │
│ Step 2 ✓ 搜尋內部文件:我方產品定價 0.6s │
│ Step 3 ✓ 網路搜尋:對手 X 產品功能 1.2s │
│ Step 4 ✓ 網路搜尋:對手 X 產品定價 1.1s │
│ Step 5 ✓ 驗證資料時效性 0.3s │
│ Step 6 ✓ 組裝比較表 + 生成報告 2.1s │
│ │
│ 總耗時:6.1s | 使用來源:4 份內部文件 + 2 篇外部報導 │
│ │
│ ⚠ 資料衝突提醒: │
│ 對手 X 定價來源 A ($349) vs 來源 B ($329) │
│ → 採用來源 A(官方網站,2025-07-28 更新) │
│ │
│ 每個步驟都可點擊展開,查看該步驟撈到的原始資料 │
└─────────────────────────────────────────────────────────┘
設計上的三個重點:
- 步驟可展開:每個 Step 點開能看到「這個子任務撈到了哪些區塊」,使用者可以深入核對,而不是只看到一行打勾
- 衝突提醒獨立顯示:定價衝突這種需要人為判斷的資訊不藏在流程裡,而是單獨標出來
- 耗時可見:6.1 秒不是「慢」,而是「做了六個步驟所以花六秒」——理解過程後,使用者對延遲的接受度大幅提高
知識缺口的誠實揭露
代理式 RAG 最有價值的 UI 設計之一是誠實揭露知識缺口。當系統找不到足夠資料時,不該硬凹一個答案,而是明確告知使用者缺了什麼:
┌─────────────────────────────────────────────────────────┐
│ ⚠ 知識缺口報告 │
├─────────────────────────────────────────────────────────┤
│ │
│ 問題:Q3 東南亞市場的銷售趨勢? │
│ │
│ ✓ 已找到: │
│ • Q3 全球銷售總報表(內部,2025-07-30) │
│ │
│ ✗ 未找到: │
│ • 東南亞市場分區報表 │
│ • Q3 東南亞競品分析 │
│ │
│ 建議: │
│ 1. 請將東南亞分區報表上傳至知識庫 │
│ 2. 或授權系統存取 Sales Analytics API │
│ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ 上傳文件 │ │ 授權外部 API 存取 │ │
│ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
前後端技術怎麼落地:把信任感做出來
前面幾個 UI 設計決策很美,但真正的問題是:這些效果在技術上怎麼做出來? 信任感不是前端畫得漂亮就行,它需要後端把資料結構設計對,前端才有東西可以呈現。以下從後端到前端,分別拆解。
後端:用事件串流把「過程」送給前端
先講後端。RAG 的 UI/UX 影響有一個本質矛盾:過程資訊(檢索了哪些來源、排除了哪些、有沒有衝突)和最終答案,產生時間完全不同。檢索在模型生成前就完成了,引用來源也是。如果後端等全部完成再一次回傳,前端只能一次渲染「完整答案 + 完整引用」,做不到「先看到引用出現,答案再逐字跑出來」的漸進效果。
解法是讓後端把每個階段當作事件,透過 SSE(Server-Sent Events) 即時推給前端:
// 後端:把檢索過程拆成事件串流
import { createServer } from 'node:http'
createServer(async (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
})
const send = (event: string, data: unknown) => {
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
}
// 1. 檢索完成,先推引用來源
send('citations', {
sources: [
{ id: '[1]', title: 'HR-Policy-2025-v3.pdf', page: 12, confidence: 0.94 },
{ id: '[2]', title: 'Remote-Work-Guidelines-2025.pdf', page: 3, confidence: 0.89 },
],
excluded: [{ title: 'work-from-home-blog-2020', reason: '超過一年失效' }],
})
// 2. LLM 生成時,逐字推送
for (const token of await streamAnswer()) {
send('token', token)
}
send('done', {})
})
SSE 的好處是單向、簡單,瀏覽器原生支援,不需要像 WebSocket 那樣管理連線狀態——很適合「後端單向餵資料給前端」的 RAG 場景。關鍵不是用了什麼協定,而是後端把「引用」和「答案」拆成不同的事件,前端才能分開渲染。
前端:分層渲染 + 可展開元件
後端把事件送出來了,前端的工作就是把每一種事件渲染成對應的 UI:
// 前端(React):把 SSE 事件渲染成漸進式答案
import { useEffect, useState } from 'react'
type Citation = { id: string; title: string; page: number; confidence: number }
function RagAnswer() {
const [answer, setAnswer] = useState('')
const [citations, setCitations] = useState<Citation[]>([])
const [excluded, setExcluded] = useState<string[]>([])
useEffect(() => {
const es = new EventSource('/api/rag-query')
es.addEventListener('citations', (e) => {
const data = JSON.parse(e.data)
setCitations(data.sources)
setExcluded(data.excluded)
})
es.addEventListener('token', (e) => {
setAnswer((prev) => prev + JSON.parse(e.data))
})
return () => es.close()
}, [])
return (
<div>
<p>{answer}</p>
<div className="citation-list">
{citations.map((c) => (
<button key={c.id} title={`${c.title} p.${c.page}`}>
{c.id} · {c.title} · 信心 {Math.round(c.confidence * 100)}%
</button>
))}
</div>
{excluded.length > 0 && (
<details>
<summary>已排除的來源({excluded.length})</summary>
<ul>{excluded.map((t) => <li>{t}</li>)}</ul>
</details>
)}
</div>
)
}
注意幾個設計決策:
| 前端手法 | 對應的 UI/UX 效果 |
|---|---|
EventSource 接收引用事件 |
引用先出現,答案後逐字跑——使用者先建立「有根據」的預期 |
<details> 可展開 |
被排除的來源收在折疊區,需要的人再看,不會轟炸所有人 |
| 信心百分比直接渲染 | 把後端的 confidence 數字轉成看得懂的介面 |
| 引用做成可點擊按鈕 | 滿足「點擊就能看原始段落」的需求 |
衝突與流程:前端只是「呈現」,判斷在後端
最後一個重點:前端絕對不能自己決定「哪份文件比較有權威性」。前面 衝突解決的可視化 說的「採用最終報告」這種判斷,必須在後端用確定性規則算好,前端只負責把結果和理由呈現出來。原因很簡單:仲裁邏輯(resolveConflict)放在後端,才能跟 UI 分開測試、被其他系統(API、報表)重複使用;放前端,換個前端就得重寫一次判斷邏輯。
所以正確的分工是:
後端(資料與判斷) 前端(呈現與互動)
┌─────────────────────┐ ┌─────────────────────┐
│ 檢索 → 驗證 → 仲裁 │ │ 漸進式渲染答案 │
│ 決定採用哪份來源 │───▶│ 渲染引用與信心指標 │
│ 計算金額落差 │ │ 可展開衝突面板 │
│ 產生推理步驟事件 │ │ 渲染流程圖與耗時 │
└─────────────────────┘ └─────────────────────┘
只管「資料對不對」 只管「怎麼呈現」
這一段的取捨反映整個 RAG 系統的架構哲學:後端負責「判斷」,前端負責「信任」。後端把資料品質把關好(過期、衝突、缺口都處理掉),前端的工作就單純很多——把這些決策透明地畫出來,使用者自然就信了。
總結
RAG 讓 AI 從閉卷考的死背考生,進化成隨時可以翻書的開卷考生。技術演進從嵌入(理解語意)→ 切塊(管理上下文)→ 向量搜尋(光速檢索)→ GraphRAG(跨文件關聯)→ 代理式 RAG(批判性思考),每一步都在解決前一代的盲點。
代理式 RAG 是最令人興奮的發展。它不再只是被動地把資料丟給 LLM,而是具備了過濾過期資料、解決資料衝突、拆解複雜問題、甚至主動上網查證的能力——就像高薪聘請的資深研究助理,不只負責找書,還會先過濾掉舊雜誌、交叉比對衝突資料、圖書館沒有就自己跑出去查。但代價也很明確:推理和驗證會顯著增加運算成本和回應延遲,邏輯沒寫好甚至可能陷入無限迴圈。這一切的目標只有一個:讓 AI 不再一本正經地胡說八道。
RAG 的下一步
把這條技術演進線拉遠來看,我認為 RAG 正在經歷一個典範轉移:從「檢索」走向「判斷」。標準 RAG 解決了「AI 有沒有資料可查」的問題,代理式 RAG 則在解決「AI 知不知道該相信哪份資料」的問題——後者才是真正讓 AI 從「工具」變成「助手」的關鍵。
但這條路上有三個我認為值得關注的下一步,資料來源沒有深入討論:
-
評估永遠跟不上建置:大家忙著建 RAG、升級 GraphRAG、套上代理層,卻很少回頭問「檢索的召回率到底多高?」RAG 的品質瓶頸往往不是模型,而是檢索——撈不到正確的區塊,後面再聰明的代理也白搭。實務上我會建議先花一半的心力做檢索評估(Recall@K、MRR),再談要不要上代理層。
-
多代理 RAG 是必然趨勢:單一代理式 RAG 解決單一問題,但企業場景很少只有一個問題。當多個 RAG 代理各自檢索、各自判斷時,如何協調彼此的結果、如何避免重複檢索同一份文件,會是下一個工程挑戰。這跟多代理協作的議題會愈走愈近。
-
回饋迴路決定長期價值:RAG 系統最常被忽略的是「使用者點擊了哪份來源、捨棄了哪份來源」。這些行為資料如果回饋回去調整檢索權重,系統會愈用愈準;如果不收集,系統就停在建置那一刻的水準。長期價值不在技術,而在能不能閉環。
另外,如果代理式 RAG 已經可以完美辨識檔案新舊、解決複雜的預算衝突、甚至主動上網查證市場情緒——當 AI 在資料收集與交叉驗證上永遠比人類客觀快速,我們人類在職場上剩下的獨特價值到底是什麼?是不可理喻的直覺、天馬行空的創新,還是去提出那個 AI 從未想過要搜尋的好問題?值得我們好好思考。RAG 讓 AI 有能力查資料,但「查什麼資料」這件事本身,也許才是人類最後的優勢。因為當 AI 的效率無限提升時,提問的方向與判斷的品味,反而是最難被自動化的部分。