目錄
拖到開學才寫完:P。
讓使用者上傳 PDF 並即時學習
要實作的功能是:使用者在網頁上丟一個 PDF 檔案,系統自動讀取、切割文字、轉成向量,然後 AI 下一秒就能回答裡面的內容。
裝套件
要裝兩個東西
pip install pypdf python-multipartpypdf: 用來讀取 PDF 檔案的文字。python-multipart: 讓 FastAPI 可以接收檔案上傳。
改 RAG 服務的函數
再來要調整 rag_service.py,讓它可以吃 PDF。
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
from pypdf import PdfReader # [NEW]
import numpy as np
print("正在載入 Embedding 模型...")
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
# 這次我們不預設資料,改用一個空的列表,等使用者上傳
knowledge_base = []
knowledge_embeddings = None
def add_pdf_to_knowledge_base(file_stream):
"""
讀取 PDF -> 抓文字 -> 切割 -> 轉向量 -> 存入知識庫
"""
global knowledge_base, knowledge_embeddings
# 1. 讀取 PDF 文字
reader = PdfReader(file_stream)
text = ""
for page in reader.pages:
text += page.extract_text() + "\n"
# 2. 文字切割 (Chunking)
# LLM 一次讀不完這麽多字,我們要切成小塊 (例如每 200 字一塊)
chunk_size = 200
chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
print(f"PDF 處理完畢,切成 {len(chunks)} 個片段")
# 3. 更新知識庫
new_items = [{"id": len(knowledge_base) + i, "content": chunk} for i, chunk in enumerate(chunks)]
knowledge_base.extend(new_items)
# 4. 重新計算向量索引 (簡單暴力法:全部重算)
# 在真實系統中,我們會用 Vector DB (如 ChromaDB) 來處理,不用每次重算
texts = [item["content"] for item in knowledge_base]
knowledge_embeddings = model.encode(texts)
return len(chunks)
def search_knowledge_base(query, top_k=3):
"""
搜尋功能 (跟之前一樣,但加了防呆機制)
"""
global knowledge_embeddings
# 如果知識庫是空的,直接回傳空
if not knowledge_base or knowledge_embeddings is None:
return []
query_embedding = model.encode([query])
similarities = cosine_similarity(query_embedding, knowledge_embeddings)
top_indices = similarities[0].argsort()[-top_k:][::-1]
results = []
for idx in top_indices:
score = similarities[0][idx]
if score > 0.3:
results.append(knowledge_base[idx]["content"])
return results改後端
使用者上傳的檔案要用 API 才能傳到後端,因此要修改 server.py
# ... (前面的 import 不變)
from fastapi import UploadFile, File # [NEW] 引入檔案處理模組
from rag_service import search_knowledge_base, add_pdf_to_knowledge_base # [NEW] 引入新函式
# ... (app = FastAPI... 中間設定不變) ...
# [NEW] 上傳檔案的 API
@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
try:
# 把檔案內容傳給 rag_service 處理
chunk_count = add_pdf_to_knowledge_base(file.file)
return {"message": f"成功讀取 {file.filename},新增了 {chunk_count} 個知識片段!"}
except Exception as e:
return {"error": str(e)}
# ... (chat_with_ai 函式保持不變) ...
# 注意:記得確認 chat_with_ai 裡面有呼叫 search_knowledge_base改前端
最後修改前端,先在 HTML 區建一個讓使用者可以按下「📚 知識庫上傳」的按鈕,上傳 PDF
<!-- 在標題下方 -->
<div style="margin-bottom: 10px; padding: 10px; background: #fff3cd; border: 1px solid #ffeeba; border-radius: 5px;">
<h3>📚 知識庫上傳</h3>
<input type="file" id="pdf-upload" accept=".pdf">
<button onclick="uploadPDF()" style="background-color: #ffc107; color: black;">上傳 PDF</button>
<span id="upload-status" style="margin-left: 10px; font-size: 0.9em; color: #666;"></span>
</div>
<div class="chat-box" id="chat-box">
</div>再到 js 區新增接收檔案傳進後端的函數
async function uploadPDF() {
const fileInput = document.getElementById("pdf-upload");
const statusText = document.getElementById("upload-status");
const file = fileInput.files[0];
if (!file) {
alert("請先選擇 PDF 檔案!");
return;
}
statusText.innerText = "正在處理中... (請稍候)";
const formData = new FormData();
formData.append("file", file);
try {
const response = await fetch("http://127.0.0.1:8000/upload", {
method: "POST",
body: formData
});
const data = await response.json();
if (data.message) {
statusText.innerText = "✅ " + data.message;
statusText.style.color = "green";
} else {
statusText.innerText = "❌ 上傳失敗:" + data.error;
statusText.style.color = "red";
}
} catch (error) {
console.error(error);
statusText.innerText = "❌ 連線錯誤";
}
}測試
準備好一個有專屬資訊的PDF (例如課綱)
啟動後端
uvicorn server:app --reload,確認正常運作開啟網站
先輸入一個只有 PDF 有寫的問題,預期輸出為不知道
上傳檔案,再問一次,預期輸出要能回答相關問題
將系統打包成 Docker 容器
讓系統無論在哪都可以跑。
安裝 Docker
首次使用需要此步,Windows 安裝前提條件:
- Windows 10 64-bit:版本 1903 或更新版
- 啟用 WSL 2(Windows Subsystem for Linux)
安裝步驟:
安裝 WSL 2(如尚未安裝)
- 開啟 PowerShell(以系統管理員身份)
wsl --install- 安裝完成後重新啟動電腦。
下載 Docker Desktop for Windows
安裝並啟動 Docker Desktop
- 執行安裝檔
- 勾選「使用 WSL 2」作為後端
- 安裝完成後啟動 Docker Desktop 並登入 Docker Hub(可註冊免費帳號)
確認 Docker 安裝是否成功
docker --version docker run hello-world
產出 requirements.txt
用來告訴 Docker 內的乾淨環境:需要安裝哪些東西。
pip freeze > requirements.txt會看到資料夾多了一個 requirements.txt 檔案,打開來檢查,裡面應該有 fastapi, uvicorn, openai, sentence-transformers 等等。
寫 Dockerfile
寫一個腳本,告訴 Docker 怎麼打包。
在專案根目錄建立一個新檔案,檔名就叫
Dockerfile(沒有副檔名)。貼上以下內容:
# 1. 選擇基底映像檔 (就像選擇要灌什麼作業系統)
# 我們選一個輕量級的 Python 3.12 環境
FROM python:3.12-slim
# 2. 設定工作目錄 (在箱子裡建立一個 /app 資料夾)
WORKDIR /app
# 3. 把你電腦裡的 requirements.txt 複製進箱子
COPY requirements.txt .
# 4. 在箱子裡安裝套件
# --no-cache-dir 可以讓箱子小一點
RUN pip install --no-cache-dir -r requirements.txt
# 5. 把你電腦裡的所有程式碼 (main.py, server.py...) 複製進箱子
COPY . .
# 6. 告訴 Docker,這個程式會開 8000 port
EXPOSE 8000
# 7. 當箱子啟動時,要執行的指令
# 注意:這裡的 --host 0.0.0.0 是關鍵,一定要設成這樣外部才連得進去
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]用指令開始 build
docker build -t my-ai-system .my-ai-system 就是會在 Docker 桌面版的 images 頁面裡看到的名稱。
Build 常見問題
安裝不了套件
有時候前面都很正常但裝到某些套件時突然說裝不了,因為套件的版本沒有這麼新的。這時只要先確認目前使用的虛擬環境的 python 版本,將對應 version 的內容修改成與虛擬環境一致,通常就沒問題了。
如果還有問題可以上DockerHub 上 Ptyhon 官方 image找出對應的版本,除非安裝的版本太新,不然應該都會有。
目前的問題
Docker 包起來的容量很大
最後一次包起來在 Docker images page 上居然顯示 13 GB ,好扯==。因為沒有任何向量資料庫,大成這樣其實很不尋常。
要如何測試 RAG 效能
目前這個的玩具的設定對稍微長一點的文本檢索機制不佳,回應也有幻覺問題。本來想說既然 AI 幫我生出這個小玩具,那我再加入 LangChain 或 Huggingface 的生態系、改一改模型、加入不同的檢索策略跟 Reranker 機制,以及針對特定需求作客製化後,就會是個實用小玩具了,可是要怎麼好好評估這些機制實在很困擾我。雖然之前有寫過製作 RAG 的作業,也跑過 kaggle 上的競賽,但這些都有提供一定數量的測試集,裡面都會附上問題與解答,但現在這個小玩具的情境裡:
我需要針對我落地的情境設計問題與確定解答,但我沒有很多時間製作測試集,可能只能針對提問的種類設計幾個問題與解答,再就這些小樣本推論回應品質。
文本要自行上傳,且資料格式是沒有標準儲存格式的 PDF,1如果讓給其他使用者使用、上傳其他來源的資料,回應的品質會變得難以掌握。除非我能列出所有可能面臨的使用情境,並且設計出好的解決策略,讓使用體驗良好,但這對初次設計的我來說太不切實際了。
我覺得第二個問題是最棘手的,所以我後來決定重新做一個不能上傳資料的封閉式 RAG 系統,先解決資料格式與來源不一的問題。這個小玩具就先放著,以後再想想看要怎麼調整。
後續
後來我就爬了一些舊聞,做了網路考古學家的小玩具,目標是滿足當歷史學家的慾望。不過還是有遇到一些問題,加上後來過年了就偷懶,但還是希望能趕快解決,順利上線後再分享。
然後雖然最後沒辦法上生成式AI應用系統與工程,不過昨天透過學校的 Email 提供的連結旁聽了一下,想不到居然是著重跟 AI Agent 協作的課,跟我(還有 Geimei)之前想的完全不一樣 XD,滿有趣的。雖然我猜這要應用在職場上還需要一段時間(取決於公司),不過有機會還是看看有沒有甚麼小撇步可以偷學XD
腳註
舉例來說,有的 PDF 是圖片轉成的,沒有辦法擷取出文字;也有的 PDF 會有反爬蟲機制,試圖用程式開啟並擷取PDF會爬到一堆亂碼。這些都會影響模型生成回答的正確性。↩︎