理解RAG在幹嘛

photo by Reed Mok LLM有時會產生看似合理、但其實不正確的回答,這種不懂裝懂的答案會讓使用者得到錯誤的資訊,還毫不自覺。 這種情況常發生在模型缺乏最新的資訊,像是詢問公司內部的東西,如果是沒有公開的事情,LLM根本不會知道,但是他仍然會亂回答一通。 RAG (Retrieval-Augmented Generation)就是為了解決這類問題而誕生的技術。它的核心概念是: 在LLM回答問題之前,先從指定的資料庫中看一次有沒有相關的文件能參考,讓模型根據可參考內容產生回答。 我自己最近也開始學習RAG,並且實作了一個小專案:MediVector-Chat。 會想做這個專案,其實跟我自己的經驗有關。之前我有腳痛的問題,在查資料或問GPT的時候,我會希望能夠參考更多相關資料,例如衛教資訊或最新論文,再根據這些內容回答我的問題。 這也是我覺得RAG很有價值的地方。一般聊天型LLM雖然很方便,但很多時候它其實只是根據訓練資料去生成看起來合理的文字。如果問題涉及特定領域知識、內部文件或最新資訊,模型未必真的知道答案。 所以我想做一個能夠「先查資料,再回答問題」的AI chatbot。 RAG的流程 文件準備:收集文件、網頁、FAQ、內部知識庫等資料。 切分資料:將長文件切成較小的 chunk,方便後續檢索。 建立索引:使用 embedding model 將 chunk 轉成向量,存入 vector database。 查詢檢索:使用者提問後,系統將問題轉成向量,找出最相關的資料片段。 生成回答:把檢索到的資料片段放進 prompt,讓 LLM 根據參考內容回答。 簡單來說,流程大概像這樣: 文件 / PDF → Chunking → Embedding → 存入 Weaviate → 使用者提問 → Vector Search → Top-K Retrieval → Prompt 組合 → LLM 回答 → 顯示來源段落 在我自己的 MediVector-Chat 專案中,也有實際把這些流程做出來。目前有做到: 文件 chunking embedding vector search Top-K retrieval prompt 組合 顯示來源段落 前後端分離 Docker 啟動本機環境 使用技術主要是: ...

May 20, 2026 · 1 min · Yen Tsai

深入淺出談 Queue:順序很重要

photo by John Cameron 剛好昨天看了小Lin老師的影片,提到委內瑞拉這個國家,天天都在排隊 加油排隊、去超市也排隊 排隊這件事,其實跟系統處理工作的順序一樣 順序一亂,待季就大條了,這時候使用Queue來管理執行順序,是一個很好的方法 什麼是 Queue Queue 是一種 先進先出 (First-In,First-Out,FIFO) 的資料結構 Queue跟Stack一樣,定義了使用者只能用那些操作 換來一致性的時間複雜度 Queue的時間複雜度 操作 Queue 時間複雜度 push 插入資料到最後位置 O(1) pop 刪除最前面的資料 O(1) front 取得最前面的資料 O(1) 使用Queue最佳場景 假設現在有三個Job依序執行,而執行中很可能因為系統當機導致需要重做 這時候應該把Job放在哪種資料結構中: A:List搭配index B:Queue 看起來A、B都可以,實際上會造成很大的落差 情境A List搭配index list = [A, B, C] i = load_index() // 持久化的進度 while i < list.length: job = list[i] do_side_effect(job) // 例如:寄信 / 扣款 / 發送檔案 // 這裡可能成功、也可能一半成功、也可能成功但你還沒來得及記錄 i = i + 1 save_index(i) // 持久化進度 crash可能發生在: ...

February 5, 2026 · 1 min · Yen Tsai

深入淺出談 Hash Table:從理論 O(1) 到實作中的 rehash 現實

photo by Jessica Johnston 什麼是 Hash Table Hash Table 是一種透過 hash function 將 key 映射到 bucket, 以達成 平均 O(1) 查找效率 的資料結構 資料通常以 key–value 的形式儲存, 使用者只需要提供 key,就能快速取得對應的 value 在實作上,Hash Table 內部會維護一組 bucket array, 每個 bucket 可以儲存一個或多個元素 當資料量持續增加、bucket 內元素變得過於擁擠時, Hash Table 會透過 resize / rehash 重新分配資料, 以維持查找效能 為什麼 Hash Table 可以快速查詢資料? Hash Table 能快速查詢資料的關鍵,在於 hash function 當一個 key 被插入或查詢時, Hash Table 會先對 key 計算 hash 值, 再將 hash 值映射成一個 bucket index, 用來決定資料應該存放或查找的位置 ...

January 28, 2026 · 3 min · Yen Tsai

深入淺出談 Stack:為什麼「後進先出」不是限制,而是力量

photo by Reed Mok 玩過河內塔嗎? 這是是一個只能從一個桿子中拿最上面圓盤的遊戲, 然後再把圓盤放到另一個桿子的最上方 看到Stack的結構馬上就想到這個例子 什麼是 Stack 一個定義好行為的資料結構 使用者只能把資料一層一層疊在最上方 使用者一次只能取最上方的資料 為什麼要定義行為 先看看Array跟Stack的差異 操作 Array Stack Search 可以查詢指定index 只能看最上方資料 Insert 可以Insert指定index 只能push資料到最上方 Remove 可以Remove指定index 只能pop最上方資料 可以看到Array跟Stack的差異在於限制了行為:新增、刪除、查詢的位置, 為的是換來操作的一致性, 跟較好的時間複雜度 Stack 的時間複雜度 操作 複雜度 說明 讀取 (Seek) O(1) 讀取最上方資料 插入 (Push) O(1) 插入資料到最上方 刪除 (Pop) O(1) 刪除最上方資料 使用 Stack 的最佳場景 後進先出的場景 剪貼簿的Undo, Redo功能 Rollback 實作Undo, Redo功能 #include <iostream> #include <stack> #include <string> using namespace std; int main() { stack<string> undoStack; stack<string> redoStack; while (true) { cout << "\nChoose:\n" << "1) Add data\n" << "2) Undo\n" << "3) Redo\n" << "4) Exit\n" << ">> "; int choice = 0; if (!(cin >> choice)) break; if (choice == 1) { cout << "Enter data (one word): "; string x; cin >> x; undoStack.push(x); while (!redoStack.empty()) redoStack.pop(); // 清空 redo cout << "Added: " << x << endl; } else if (choice == 2) { if (undoStack.empty()) { cout << "Nothing to undo." << endl; } else { string x = undoStack.top(); undoStack.pop(); redoStack.push(x); cout << "Undo: " << x << endl; } } else if (choice == 3) { if (redoStack.empty()) { cout << "Nothing to redo." << endl; } else { string x = redoStack.top(); redoStack.pop(); undoStack.push(x); cout << "Redo: " << x << endl; } } else if (choice == 4) { cout << "Bye." << endl; break; } else { cout << "Invalid choice." << endl; } cout << "Undo size: " << undoStack.size() << ", Redo size: " << redoStack.size() << endl; if (!undoStack.empty()) cout << "Undo top: " << undoStack.top() << endl; if (!redoStack.empty()) cout << "Redo top: " << redoStack.top() << endl; } return 0; } 程式碼說明: ...

January 27, 2026 · 2 min · Yen Tsai

深入淺出談 Linked List:為什麼它只能一個一個走

photo by Shubham Dhage 你玩過心理測驗嗎? 有些心理測驗的題目是這樣的 你是男生, 請跳到第 8 題, 妳是女生請跳到第 3 題… 這樣照著前一個指令跳到下一個指定位置的遊戲真的很像 Linked List 的結構 什麼是 Linked List 一個資料結構, 每一筆資料除了自己的值外, 還知道下一筆資料是誰, 因此所有操作都必須照順序進行 Linked List 的時間複雜度 操作 複雜度 說明 讀取 (Read) O(n) 必須從頭開始遍歷資料 插入 (Insert) O(1) 在已知插入的位置時, 只需調整前後關係 刪除 (Delete) O(1) 在已知刪除的位置時, 只需修改前後關係 使用 Linked List 的最佳場景 已知要在某個元素的位置, 並且在元素前後頻繁操作插入刪除 這也是 LRU Cache 常使用 Linked List 的原因之一

January 26, 2026 · 1 min · Yen Tsai

深入淺出談 Array:從記憶體配置看時間複雜度

photo by Reed Mok 想像你是一位老師,要管理一個新班級的學生名單 人數少的時候,你可以直接用名字來記住每一位學生,但當學生人數一多,要快速找到某一位學生就變得困難 情境 A:變數分散管理 如果我們像這樣單獨宣告變數來存學生名字: string student1 = "Alice"; string student2 = "Bob"; string student3 = "Charlie"; // ... 如果有 50 個學生,變數會多到無法管理 這種方式的問題在於: 沒有順序性:變數之間沒有邏輯上的關聯 無法索引:不能用「第 3 個學生」這種方式來存取 難以自動化:無法用迴圈(Loop)來批次處理這群資料 情境 B:使用 Array 統一管理 這時候,如果替每位學生分配一個固定的「座號」,並依照座號的順序將學生資料排排站(存放起來),只要知道座號,就能直接找到對應的學生 struct Student { string name; }; // 學生名單 (Array) Student students[] = { {"Alice"}, // index 0 (座號 1) {"Bob"}, // index 1 (座號 2) {"Charlie"} // index 2 (座號 3) }; const int numStudents = 3; Student* findStudentBySeatNumber(int seatNumber) { // 檢查座號是否合法 if (seatNumber < 1 || seatNumber > numStudents) { return nullptr; } // 透過索引直接取得資料 (Array Index 從 0 開始) return &students[seatNumber - 1]; } 這就是 Array 的核心精神 ...

January 23, 2026 · 2 min · Yen Tsai

Template Method Pattern

在專案中有個需求要產生 100+ 份報表,格式包含Word與PDF,這些報表有幾個共同特性: 都需要從 Word 樣板產生 樣板中使用 unique token(例如 [XXX])作為預填欄位 產出前需要先組合資料、預填內容 有些最後還要轉成 PDF 很直覺的作法是:每新增一個報表, 就寫一份產生報表邏輯 短期內可以快速交付功能, 但是造成的問題是: 每產生一個報表就會新增一套九成像的程式碼邏輯並散落在專案各處 想改一個共通行為要改很多地方 接手維護的工程師不知道哪個是標準寫法 真正的問題不是報表數量變多,而是同一個產生流程,被複製成太多版本 Template Method Pattern 首先要釐清的兩件事情是: 哪些事情在報表中一定會做 哪些事情每份報表都不一樣 不會變的部分 讀取報表樣板 報表資料預填 輸出檔案 是否轉換成PDF檔案 會變的部分 報表的資料來源 預填欄位的內容與資料格式 輸出的檔案名稱 運用Template Method Patten的核心價值就在於: 把一定會做的流程固定下來 只把會變的部分交給子類別實作 避免每個報表各自定義一套自己的產生流程 實作 Template Method Patten 把報表產生流程抽象化,其實每份報表都在做同一件事: 取得產生報表所需的資料 讀取樣板文件, 填入預填資料 輸出文件 在沒有Template Method Patten情況下, 每一份報表都要實作這三個流程 Template Method Patten作法是這樣的: Base class定義產生流程的順序 子類別負責準備這份報表所需資料 classDiagram class BaseReport { +Generate() #Init() #LoadTemplate() #PrefillContent() #ExportFile() #ConvertToPdfIfNeeded() } class ConcreteReportA { +Init() } class ConcreteReportB { +Init() } BaseReport <|-- ConcreteReportA BaseReport <|-- ConcreteReportB 關鍵在於子類別不能決定流程, 只能提供資料 ...

January 19, 2026 · 1 min · Yen Tsai

Vue3 Composition API

setup Composition API 功能要寫在setup裡面 <script> export defalut { setup(){ //... } } </script> 也可以寫成 <script setup> </script> ref 定義響應性數據 參數名稱要+.value來修改資料 <template> <h1>{{ title }}</h1> </template> <script setup> import { ref } from 'vue'; const title = ref('title'); title.value = 'new title'; </script>

September 25, 2023 · 1 min · Yen Tsai

MSSQL 無法連線至XXX 問題

以下可能原因: ...

September 1, 2023 · 1 min · Yen Tsai

使用Vue Teleport實作提示框功能

實作在右下角顯示多個提示框功能 修改index.html部分, 在body區域新增一個messages用來放置提示框的位置 //index.html <!doctype html> //... <body> <div id="app"></div> <div id="messages"></div> <script type="module" src="/src/main.js"></script> </body> //... ...

August 23, 2023 · 1 min · Yen Tsai