RAG:讓 AI 從閉卷考變開卷考

RAG:讓 AI 從閉卷考變開卷考

讀過世界上所有書的天才,被關在一間沒有網路的密室裡。問它歷史、哲學,對答如流。但問它「我們公司昨天發布的預算報告重點是什麼」,它只能愣住,甚至開始一本正經地胡說八道。這就是目前大型語言模型(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 萬歐元」

關鍵不是「選一個數字」,而是證明為什麼選這個。代理依賴兩條確定性的規則:

  1. 文件階層final_report(最終報告)的權威性高於 initial_proposal(初始提案)——提案是起點,報告是結論
  2. 時間順序:最終財務報告(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 更新)            │
│                                                         │
│  每個步驟都可點擊展開,查看該步驟撈到的原始資料        │
└─────────────────────────────────────────────────────────┘

設計上的三個重點:

知識缺口的誠實揭露

代理式 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 從「工具」變成「助手」的關鍵。

但這條路上有三個我認為值得關注的下一步,資料來源沒有深入討論:

  1. 評估永遠跟不上建置:大家忙著建 RAG、升級 GraphRAG、套上代理層,卻很少回頭問「檢索的召回率到底多高?」RAG 的品質瓶頸往往不是模型,而是檢索——撈不到正確的區塊,後面再聰明的代理也白搭。實務上我會建議先花一半的心力做檢索評估(Recall@K、MRR),再談要不要上代理層。

  2. 多代理 RAG 是必然趨勢:單一代理式 RAG 解決單一問題,但企業場景很少只有一個問題。當多個 RAG 代理各自檢索、各自判斷時,如何協調彼此的結果、如何避免重複檢索同一份文件,會是下一個工程挑戰。這跟多代理協作的議題會愈走愈近。

  3. 回饋迴路決定長期價值:RAG 系統最常被忽略的是「使用者點擊了哪份來源、捨棄了哪份來源」。這些行為資料如果回饋回去調整檢索權重,系統會愈用愈準;如果不收集,系統就停在建置那一刻的水準。長期價值不在技術,而在能不能閉環。

另外,如果代理式 RAG 已經可以完美辨識檔案新舊、解決複雜的預算衝突、甚至主動上網查證市場情緒——當 AI 在資料收集與交叉驗證上永遠比人類客觀快速,我們人類在職場上剩下的獨特價值到底是什麼?是不可理喻的直覺、天馬行空的創新,還是去提出那個 AI 從未想過要搜尋的好問題?值得我們好好思考。RAG 讓 AI 有能力查資料,但「查什麼資料」這件事本身,也許才是人類最後的優勢。因為當 AI 的效率無限提升時,提問的方向與判斷的品味,反而是最難被自動化的部分。

參考資料


RAG Retrieval Augmented Generation 檢索增強生成 代理式 RAG Agentic RAG A2A Agent to Agent Multi-Agent Agentic Design Pattern Agentic AI AI AI 幻覺 Architecture Design Pattern Vector Database HNSW Hierarchical Navigable Small World 向量資料庫 Embedding

Summer Tang
Sponsored
Summer Tang 一對一諮詢
Summer|TSMC 主任工程師|擁有 15 年資深 Web 架構經驗,職涯歷經跨國頂尖企業與新創。專精於高流量企業級應用開發,核心技術涵蓋微前端、效能優化、測試自動化與 SEO。曾出版多本技術著作,並多次擔任 MOPCON、WebConf 等大型技術年會講者。
了解更多 →