iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0

前言

歡迎來到第二週!真虧你看完昨天那篇落落長的鬼東西還沒棄坑,真有你的!我打完看了一下字數約四萬字,自己也傻了一下,但為了系列文的完整度我又不能直接跳過那些程式碼,因此只好做點取捨,日後若我還想寫類似的主題,我會再評估一下呈現的方式,也許直接給一個完整的UI做為起手式就好。

在第一週我們已經完成了 AI 面試官的 MVP ,有個還過得去的頁面設計以及基本的功能, 回答問題也能透過 AI 得到看似不錯的回覆,但我們心裡都清楚,這輛外表帥氣的跑車,引擎蓋底下還有些秘密。目前,我們的 AI 雖然能根據我們在 Day 6 精心設計的 Prompt 做出回饋,但它本身對於「Hoisting」或「React Hooks」的理解,依然是基於它龐大訓練資料中的通用知識。它並不知道我們為每一題精心準備的 keyPoints 到底有多重要,只是將keyPoints 視為 context的一部分。換句話說,我們給了 AI 評分的「標準答案」,但它並沒有真的「讀進去」。

今天,我們要解決這個問題。我們要為 AI 舉辦一場「開卷考試」,在它回答問題之前,把最重要的參考資料(也就是我們的 keyPoints)直接塞到它手上。這個技術,就是當今所有強大 AI 應用(從客服機器人到研究工具)的核心之一:RAG (檢索增強生成)。

今日目標

  • 理解為什麼我們需要 RAG,它解決了什麼根本問題。
  • 解釋 RAG (檢索增強生成) 的運作原理。
  • 搞懂 RAG 的基石——Embedding (嵌入) 到底是什麼。
  • 親手實作:呼叫 Gemini API 產生你的第一個 Embedding 向量。

核心問題:大型語言模型的「幻覺」與「不知情」

讓我們先回到 Day 6 的 Prompt。我們在 ‵<evaluation_criteria>‵ 標籤中傳入了keyPoints,在我們設定的 prompt 中是根據題庫的資料用動態產生的方式嵌入,例如若今天題目一樣是那個 hoisting 題目,產出的<evaluation_criteria>會類似以下這樣:

<evaluation_criteria>
- 變數和函數宣告會被提升到其作用域的頂部
- 只有宣告被提升,賦值不會
- let 和 const 也有 hoisting,但因存在暫時性死區 (TDZ)...
</evaluation_criteria>

你可能會想問我:「這看起來很合理不是?我們不是要盡可能提供 AI 足夠的 context 讓它能正確評斷嗎? 即便你說要導入 RAG ,但 RAG 最終不也是把資料放在 prompt裡面,這樣跟我們目前的做法有什麼差別?」

如果你能問出這樣的問題,那真的非常的優秀,表示你已經對與 LLM 溝通有著相當不錯的認知了,這也是我一開始在接觸這個主題時心中的疑問,後來查閱一些資料後再與 AI 討論求證後我想我比較能說明這個差別了。

我們確實已經把 keyPoints 給 AI 了,但簡單來說,我們 Day 6 的做法就像是:

- 考生問「什麼是 hoisting?」
- 你把整本 JavaScript 教科書丟給他

而 RAG 的做法是:

- 考生問「什麼是 hoisting?」  
- 你幫他翻到第 3 章第 42 頁,指著相關段落

這樣的做法同樣提供了足夠的資料,但減少了不必要的資訊輸入,讓 AI 的回答能被限制(grounded)在某個範圍內,產生更高精度的回覆。

根據 OpenAI 等機構的研究,大型語言模型 (LLM) 產生幻覺的一個主要原因,是它過度依賴其「內部知識」。這些知識是從龐大的訓練資料中統計、壓縮而來,並非一個精確的資料庫,因此可能存在過時、不完整甚至矛盾的內容,加上在訓練過程中會鼓勵語言模型猜測答案而非直接承認不知道或不確定的答案,最終在一些情況下會產生一些看起來嚴謹卻胡說八道的內容。

我們 Day 6 的方法,正暴露了這其中一個弱點:

極限一:提供了無關的上下文,增加了幻覺風險

在 Day 6 的方法中,我們把一道題目所有的 keyPoints 都塞了進去。
想像一個場景:題目是關於 Hoisting,有 3 個 keyPoints (關於 var、let/const、TDZ)。但使用者只簡單回答了關於 var 的部分。此時,Prompt 中關於 let/const 和 TDZ 的那兩條 keyPoints,對於「評估使用者當前回答」這個任務來說,就成了無關的上下文。

當模型面對這些與當前任務無關的資訊時,它可能會試圖將它們也「融合」進回饋中,從而導致:

  • 偏離重點:試圖在回饋中也提及 let/const,即使使用者的回答根本沒碰到那部分。
  • 產生幻覺:為了將使用者模糊的回答和我們提供的所有keyPoints強行關聯起來,它可能會編造一些看似合理、實則錯誤的過渡性解釋。

極限二:無法應對「未知」與「延伸」

這個方法完全依賴於我們手動為每一道題都準備好keyPoints。如果使用者問了一個我們題庫裡沒有的延伸問題,例如「那 Hoisting 在 class 宣告中是怎樣的?」,我們的系統就束手無策了。因為沒有可提供的上下文,AI 只能退回到它自己龐大的、可能不準確的內部知識庫中去尋找答案,這大大增加了「不知情」或「胡說八道」的風險。

RAG 的核心思想,就是通過動態檢索,確保我們提供給 AI 的上下文,永遠是與使用者查詢最相關的,並且是來自我們可信賴的外部知識庫。這能有效地將 AI 的回答「錨定」(Grounding) 在事實上,而不是讓它在自己龐雜的記憶中猜測。

RAG (檢索增強生成) 到底是什麼?

根據 OpenAI 和 IBM 等公司的定義,RAG 的全名是 Retrieval-Augmented Generation。它是一個框架,通過從外部知識庫檢索資訊,來增強 LLM 的回答品質。

我們可以把它拆成兩部分來理解:

  • 檢索 (Retrieval):這一步就像一個「超級圖書館管理員」。當使用者提出問題時,我們不是直接去問博學的朋友 (AI),而是先讓這位管理員,根據問題去我們專屬的「知識庫」(目前就是 data/questions.json)中,快速找出最相關的那幾頁資料。

  • 增強生成 (Augmented Generation):這一步是把管理員找到的相關資料,連同使用者的原始問題,一起遞給那位博學的朋友 (AI),並告訴他:「請根據這幾頁資料來回答這個問題。」

整個流程就像這樣:

使用者提問 -> [圖書館管理員] -> (1. 檢索最相關的幾頁書) -> (2. 把問題和這幾頁書一起交給專家) -> [博學的專家] -> 更精準、更有根據的回答

RAG 讓 AI 從一個「閉卷考試的考生」變成了一個「開卷考的學霸」。它不需要記住所有事,只需要學會如何快速查找參考資料,並根據資料做出高品質的回答。

RAG 的基石:Embedding

你可能會問:「那位『超級圖書館管理員』是怎麼做到快速又準確地找到相關資料的?靠書名一個個比對嗎?」

這就是 RAG 最神奇的地方。傳統的關鍵字搜尋非常脆弱,如果使用者問「貓咪為什麼會發出呼嚕聲?」,但書裡的章節標題是「家貓喉部發聲機制探討」,關鍵字搜尋可能就找不到了。

我們需要的是語意搜尋 (Semantic Search)——一種能理解「意思」而非「字面」的搜尋方式。而實現語意搜尋的技術,就是 Embedding。

Embedding 簡單來說,就是把一段文字,透過一個 AI 模型,轉換成一組能代表其「語意」的特定座標,類似像是這樣的轉換,把每一個詞都根據其特徵分類轉為某組數字。

"貓咪" -> 座標 (1.2, -0.5, 3.8, ...)
"小狗" -> 座標 (1.1, -0.4, 3.9, ...)
"汽車" -> 座標 (-5.7, 8.2, 0.1, ...)

這組座標(也就是向量 Vector)就像文字在一個巨大「語意地圖」上的 GPS 定位。意思相近的文字,它們在地圖上的位置也就會非常接近。

回到圖書館找書的例子,想像一下圖書館的書籍分類。所有「電腦科學」類的書都會被放在同一個區域,而「歷史文學」類的書則在另一個遙遠的區域。

Embedding 做得更絕,它不僅區分大類,還能在「電腦科學」區裡,把「網路技術」和「資料庫」的書放得更近,同時讓它們離「人工智慧」的書稍遠一些。它建立了一個無比精細的「概念關聯地圖」。

有了 Embedding,我們的「檢索」步驟就變成了:

  1. 把使用者的問題,轉換成它在「語意地圖」上的 GPS 座標 A。

  2. 我們知識庫裡的所有資料,也早就標記好了各自的 GPS 座標 B, C, D...

  3. 計算座標 A 和所有其他座標之間的「直線距離」,找出距離最近的那幾個。

這些距離最近的資料,就是語意上最相關的參考資料,接著我們只要把這些參考資料一起整理後連同我們的system prompt 送給 AI 處理就能得到更高精度的回答了!

小試身手:親手產生你的第一個 Embedding

理論說了這麼多,不如親手來產生一個看看。我們來寫一個獨立的、小小的 Node.js 腳本,實際呼叫 Gemini API,看看文字是如何變成向量的。

第一步:安裝 dotenv

為了讓 Node.js 腳本能讀取 .env.local 檔案,如果還沒安裝,請在你的終端機中執行:

npm install dotenv

第二步:建立測試腳本

在你的專案根目錄(ai-frontend-interviewer/)下,建立一個新檔案叫做 test-embedding.js,並貼上以下程式碼:

// test-embedding.js
import { GoogleGenAI } from '@google/genai';

const apiKey = '你的API Key'; // 本地直接用明碼測試沒關係,這檔案不在next/app裡面,它不會自動讀取env.local的環境變數,我也懶得裝dotenv
if (!apiKey) {
  throw new Error('GEMINI_API_KEY is not set in .env.local');
}

const ai = new GoogleGenAI({ apiKey });

async function run() {
  console.log('正在產生 Embeddings...');

  const textsToEmbed = ['變數提升', 'Hoisting', 'React Hooks'];

  try {
    const response = await ai.models.embedContent({
      model: 'text-embedding-004', // 目前最新的embedding模型
      contents: textsToEmbed,
    });

    const embeddings = response.embeddings;

    embeddings.forEach((embedding, index) => {
      const vector = embedding.values;
      console.log(`\n--- ${textsToEmbed[index]} ---`);
      console.log(`向量維度 (Dimensions): ${vector.length}`);
      console.log(`向量預覽: [${vector.slice(0, 5).join(', ')}, ...]`);
    });
  } catch (error) {
    console.error('產生 Embedding 時出錯:', error);
  }
}

run();

第三步:執行腳本

儲存檔案後,打開終端機,執行:

node test-embedding.js

第四步:觀察結果

稍待片刻,你應該會看到類似這樣的輸出:

正在產生 Embeddings...

--- 變數提升 ---
向量維度 (Dimensions): 768
向量預覽: [-0.010790561, 0.037765387, 0.00715581, 0.01840769, 0.052377373, ...]

--- Hoisting ---
向量維度 (Dimensions): 768
向量預覽: [-0.022727268, -0.018676361, -0.07868326, -0.0026614175, 0.05143915, ...]

--- React Hooks ---
向量維度 (Dimensions): 768
向量預覽: [0.026751732, 0.0015598036, -0.0643659, -0.029465836, 0.049701426, ...]

深入解析:這些神秘數字到底是什麼?向量與維度詳解

好的,我們看到了 test-embedding.js 腳本的輸出結果:一串由 768 個數字組成的陣列。但這到底是什麼意思?為什麼是 768?

讓我們用你最熟悉的座標系統來理解這一切。

  1. 從 2D 座標到向量
    在日常生活中,最簡單的向量就是一個 2D 座標。當你在地圖上標記一個點時,你就在定義一個位置。這個位置可以用一個包含兩個數字的陣列來表示:
[經度, 緯度] -> [121.46, 25.01]

這就是一個 2 維向量 (2-Dimensional Vector)。它需要 2 個數字(經度, 緯度)來精準地描述一個點在 2 維空間(地圖平面)中的位置。

  1. 擴展到 3D 空間
    現在,我們加上「海拔高度」。你要描述一座山峰的位置,就需要:
[經度, 緯度, 海拔] -> [121.51, 25.17, 1120]

這是一個 3 維向量 (3D Vector)。它需要 3 個數字來描述一個點在 3 維空間中的位置。

所以,「維度 (Dimension)」 指的就是描述一個點的位置所需要的座標軸數量。

  1. 歡迎來到 768 維的「語意空間」
    現在,回到我們 Embedding 的結果。text-embedding-004 這個模型,會將每一段文字,映射到一個高達 **768 維的「語意空間」**中的一個精確座標點上。

如果說 3D 空間的座標軸是 [左右, 前後, 上下]。那麼,這個 768 維的語意空間,它的座標軸代表什麼?它們代表的是 768 個抽象的「語意特徵」,想像一下你今天要替你的網站以數字做特徵的描述,結果大概會是類似這樣的東西。

const 網站特徵 = {
  // 視覺特徵
  主色調_紅: 0.2,      // 0-1 表示程度
  主色調_藍: 0.8,
  主色調_綠: 0.3,
  
  // 排版特徵
  字體大小_小: 0.1,
  字體大小_中: 0.7,
  字體大小_大: 0.2,
  
  // 內容特徵
  包含表單: 0.9,
  包含影片: 0.1,
  包含圖片: 0.6,
  
  // ... 還有 759 個其他特徵
};

// 轉成陣列就是 768 維向量
const vector = [0.2, 0.8, 0.3, 0.1, 0.7, 0.2, 0.9, 0.1, 0.6, ...];

那為什麼語言會需要768個維度?因為語言超級複雜!我們無法給這 768 個軸都起個名字,但可以想像它的軸可能是「與程式設計的相關性」、「是否為抽象概念」、「帶有正面/負面情感」等等。模型透過海量資料的訓練,自己學會了 768 個最能有效區分不同文字語義的特徵軸。

核心結論:你不需要去理解每一個維度代表的確切意義。你只需要記住最重要的一點:這個 AI 模型確保了「意思相近的文字,它們在 768 維語意空間中的座標點也會彼此靠近」。 電腦可以快速計算這些高維度座標點之間的「距離」,從而判斷語意的相似度。

補充說明:為什麼是768維度

維度並不是越高越好,模型維度是如何決定的?Embedding 維度的選擇是工程上的權衡,主要考慮三個因素:

  1. 資訊保留能力
維度太低(如 128):無法捕捉語言的細微差異
維度適中(如 768):能保留大部分語意資訊
維度過高(如 3072):邊際效益遞減

2.計算成本

javascript// 相似度計算的時間複雜度是 O(d),d 是維度
// 768 維:需要 768 次乘法運算
// 1536 維:需要 1536 次乘法運算(2倍時間)
// 3072 維:需要 3072 次乘法運算(4倍時間)

// 儲存成本也是線性增長
// 100萬筆 768 維向量 ≈ 3GB
// 100萬筆 1536 維向量 ≈ 6GB
  1. 維度災難(Curse of Dimensionality)

根據學術研究,當維度過高時,所有點之間的距離都會變得差不多,這種現象稱為「維度災難」,表示高維度轉換出的向量已經失去意義,無法真正找到相關的內容。

不同公司基於自己的測試選擇了不同維度,而我們剛剛測試的模型則是選擇了768維度,在精度與成本上做到平衡的一個常見的選項,其他例如 OpenAI 的類似 API 也有提供1536甚至是3072維度的選擇,網站上也有相應的說明讓你確認哪一個維度適合你,所以並不是768就是最好的答案,每個團隊目前針對不同的情境都有不一樣的結論,很有意思的東西吧? 至少我是這樣覺得的!

今日回顧

相較於昨天的程式碼炸彈,今天根本是完全的理論課程,回顧一下今天的內容:

✅ 我們理解了 RAG 的核心價值:解決 AI 的「不知情」問題,讓它能基於特定知識庫回答。
✅ 我們用「開卷考試」和「圖書館管理員」的比喻,搞懂了 RAG 的運作流程。
✅ 我們認識到 Embedding 是實現語意搜尋的關鍵,它就像文字的「語意座標」。
✅ 我們親手呼叫 API,將文字轉成了向量,見證了魔法的發生。

感覺如何?雖然都是新概念,但希望透過文章中的說明與舉例你能感覺到它們並沒有那麼遙不可及,向量工程也並不是什麼全新的東西,早在 AI 模型肆虐前就存在向量資料庫的概念了,只是因為 LLM 的使用情境再次將它放在聚光燈下。

明天預告

觀念通了,明天我們就要來真的了!

Day 9,我們將再次串接 Google Gemini Embedding API,將我們的keyPoints 和使用者的回答,統統轉換成語意向量。我們將親眼見證看似無關的文字,如何在數學的世界裡展現出它們驚人的相似性。

這是從理論邁向實作的關鍵一步,我們明天見!🚀

參考資料

OpenAI Cookbook: Retrieval augmented generative question answering with Pinecone
IBM Research: What is retrieval-augmented generation?
Google Cloud: Generate grounded answers with RAG
AWS AI Blog: Detect hallucinations for RAG-based systems
OpenAI - why language models hallucinate
OpenAI - embeddings guides


上一篇
整合與部署:將專案整合一週的內容後推上Vercel吧!
系列文
前端工程師的AI應用開發實戰:30天從Prompt到Production - 以打造AI前端面試官為例8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言