這是 AI Engineer 學習路上的實戰專案。我們實現了一個基於本地 LLM 的文檔問答系統,支持混合檢索、智能查詢處理等功能。
目標:上傳 PDF 文檔,基於內容進行智能問答
默認數據:道路使用者守則 (Road Users Code 2020)
技術棧:
- 前端:Streamlit (互動式 Web 介面)
- API 伺服器:FastAPI
- LLM:Ollama (本地運行)
- RAG Pipeline:LangChain
- 向量資料庫:ChromaDB
- 檢索方式:混合檢索 (BM25 + Vector Search)
local-rag-chat/
├── app.py # Streamlit 主程式
├── api_server.py # FastAPI 伺服器
├── main.py # 測試腳本
├── config.py # 配置常量
├── document_processor.py # 文檔處理與向量存儲
├── rag_chain.py # RAG 鏈與檢索
├── prompts.py # Prompt 模板
├── hybrid_retriever.py # 混合檢索 (BM25 + Vector)
├── query_processor.py # 查詢處理 (擴展 + 子問題)
├── token_tracker.py # Token 使用追蹤
├── requirements.txt # Python 依賴清單
├── README.md # 說明文檔
├── screenshot/ # 截圖
└── data/ # 存放 PDF 文檔
├── basiclaw_full_text.pdf # 基本法
└── road_users_code_2020_chi.pdf # 道路使用者守則
Ollama 讓你可以在本地運行大型語言模型。
macOS / Linux:
# 安裝 Ollama
curl -fsSL https://ollama.com/install.sh | sh
# 啟動服務
ollama serveWindows:
- 從 https://ollama.com/download/windows 下載安裝
- 安裝完成後自動啟動服務
# 默認模型 (12B)
ollama pull gemma3:12b
# 或使用其他模型
ollama pull qwen3.5:9b# 默認 Embedding 模型
ollama pull bge-m3
# 或使用較小的模型
ollama pull qwen3-embedding:0.6bollama list應該能看到類似輸出:
NAME ID SIZE MODIFIED
gemma3:12b xxx... 7.2GB ...
bge-m3 xxx... 1.2GB ...
cd local-rag-chat# 建立 venv
python -m venv venv
# 啟動虛擬環境
# macOS / Linux
source venv/bin/activate
# Windows (PowerShell)
venv\Scripts\Activate.ps1
# Windows (命令提示字元)
venv\Scripts\activate.batpip install -r requirements.txt確保在另一個終端機視窗執行:
ollama servestreamlit run app.py瀏覽器會自動打開 http://localhost:8501
- BM25:關鍵字匹配,適合精確檢索
- Vector Search:語意相似度檢索
- Reciprocal Rank Fusion (RRF):結合兩種檢索結果
- 查詢擴展:將模糊查詢擴展為完整問題
- 例如:「它」→ 「基本法中關於...的規定」
- 子問題生成:將複雜問題分解為多個簡單問題
- 新增文檔到現有索引
- 重建索引
- 移除特定文檔
除了 Streamlit 介面,本專案還提供了 FastAPI 伺服器,可供其他應用程式调用。
# 安裝額外依賴(如尚未安裝)
pip install fastapi uvicorn python-multipart
# 啟動伺服器
python api_server.py伺服器將在 http://localhost:8000 運行
| 端點 | 方法 | 說明 |
|---|---|---|
| / | GET | 根路徑 |
| /health | GET | 健康檢查 |
| /documents | GET | 列出已索引的文檔 |
| /upload | POST | 上傳 PDF 文檔 |
| /reindex | POST | 重新建立索引 |
| /documents/{doc_id} | DELETE | 刪除指定文檔 |
| /chat | POST | 聊天問答(Streaming) |
| /chat/non-stream | POST | 聊天問答(非Streaming) |
| /conversation/{id} | DELETE | 清除對話歷史 |
| /usage | GET | 取得總體使用統計 |
| /usage/daily | GET | 取得每日使用統計 |
| /usage/models | GET | 取得模型使用統計 |
| /usage/conversation/{id} | GET | 取得對話使用統計 |
| /usage/recent | GET | 取得最近使用記錄 |
| /usage | DELETE | 清除使用統計 |
curl http://localhost:8000/healthcurl -X POST http://localhost:8000/upload \
-F "files=@document.pdf" \
-F "chunk_size=1000" \
-F "embedding_model=bge-m3"curl -X POST http://localhost:8000/chat/non-stream \
-H "Content-Type: application/json" \
-d '{
"message": "什麼是道路使用者守則?",
"use_hybrid": true,
"model": "gemma3:12b",
"temperature": 0.7
}'curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"message": "什麼是道路使用者守則?",
"use_hybrid": true,
"model": "gemma3:12b",
"temperature": 0.7
}'curl -X POST http://localhost:8000/reindex \
-H "Content-Type: application/json" \
-d '{
"chunk_size": 1000,
"chunk_overlap": 200,
"embedding_model": "bge-m3"
}'啟動伺服器後,可訪問 http://localhost:8000/docs 查看互動式 API 文檔(Swagger UI)
- 打開應用:訪問 http://localhost:8501
- 選擇模型:在左側側邊欄選擇 LLM 模型
- 讀取索引:點擊「讀取現有索引」載入已建立的向量數據
- 開始問答:在底部輸入框提問
假設已載入道路使用者守則:
- 「什麼是道路使用者?」
- 「駕駛者在道路上有哪些義務?」
- 「行人應該遵守什麼規則?」
在側邊欄可以調整以下參數:
| 參數 | 說明 | 默認值 |
|---|---|---|
| LLM 模型 | 選擇問答使用的語言模型 | gemma3:12b |
| Embedding 模型 | 選擇向量化模型 | bge-m3 |
| 混合檢索 | 啟用 BM25 + Vector 混合檢索 | 開啟 |
| 查詢擴展 | 啟用模糊查詢擴展 | 開啟 |
| 子問題生成 | 啟用複雜問題分解 | 開啟 |
| Chunk Size | 每個文本塊的大小 | 1000 |
| Chunk Overlap | 文本塊之間的重疊 | 200 |
| 檢索文檔數 | 每次問答檢索的文檔數 | 2 |
| Temperature | 生成創造性 (0-1) | 0.0 |
RAG (Retrieval-Augmented Generation) 是一種讓 AI 能夠回答特定文檔內容的技術。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PDF 文件 │ ──→ │ 文本切分 │ ──→ │ 向量化 │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 生成回答 │ ←── │ Prompt 組裝 │ ←── │ 混合檢索 │
└─────────────┘ └─────────────┘ └─────────────┘
- 文檔攝取 (Ingestion):使用 PyPDFLoader 讀取 PDF
- 文本切分 (Splitting):使用 RecursiveCharacterTextSplitter 將長文本切分成小塊
- 向量化 (Embedding):使用 Ollama 將文本轉為向量
- 混合檢索 (Hybrid Retrieval):
- BM25 檢索
- Vector 相似度檢索
- RRF 結果融合
- 查詢處理 (Query Processing):
- 查詢擴展
- 子問題生成
- 生成回答 (Generation):將檢索到的內容注入 Prompt,讓 LLM 生成答案
所有配置常量,包括:
- 路徑配置
- 模型配置
- 檢索參數
文檔處理功能:
- PDF 載入與處理
- 向量數據庫管理
- 文檔增刪
RAG 鏈構建:
- 創建 RAG 問答鏈
- 文檔檢索
- 上下文格式化
Prompt 模板:
- QA Prompt
- 查詢擴展 Prompt
- 子問題生成 Prompt
- 摘要 Prompt
混合檢索實現:
- BM25 檢索器
- Vector 檢索器
- RRF 結果融合
查詢處理實現:
- 查詢擴展
- 子問題生成
- 多查詢檢索與融合
FastAPI 伺服器實現:
- REST API 端點
- 文件上傳與索引
- 聊天問答(Streaming)
- 對話歷史管理
- Token 使用追蹤
Token 使用追蹤模組:
- 追蹤每個請求的 Token 使用量
- 每日、每週、每月統計
- 按模型分類統計
- 按對話分類統計
錯誤:ConnectionRefusedError
解決:
- 確認 Ollama 服務正在運行:
ollama serve - 確認端口正確(預設 11434)
解決:
- 將 PDF 頁數控制在 200 頁以內
- 減小 Chunk Size
解決:
- 嘗試不同的模型
- 調整 Temperature 參數
- 增加檢索文檔數
- 啟用混合檢索和查詢處理
完成這個專案後,請確保你理解以下概念:
- 為什麼需要向量檢索?
- 如何將文本轉為向量?
- 為什麼要切分文本?
- BM25 的原理
- Vector Search 的原理
- RRF 融合方法
- Document Loaders
- Text Splitters
- Embeddings
- Vector Stores
- Chains
- Retrievers
- 如何設計有效的問答 Prompt?
- 上下文注入的原理?
本專案僅供學習使用。


