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 Agentic AI 401

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