A RAG-Based Table Tennis RuleBot System
我們希望能夠建立一個 桌球規則問答系統,透過 RAG(Retrieval-Augmented Generation)技術,讓使用者能用自然語言快速查詢桌球比賽規則,並自動生成精確、可追溯的答案。
系統成果
整個系統流程如下:
- 使用者提問:以自然語言輸入問題(例如「桌球的桌子高度應該要多高?」)。
- Embedding 檢索:
bge-m3將問題轉換成向量- 在 PostgreSQL + pgvector 進行 Top-N 語意檢索
- Rerank:
xitao/bge-reranker-v2-m3對檢索結果重新排序,挑出最相關的 Top-K 條文。 - 生成回答:
qwen3:1.7b閱讀相關規則段落,輸出 繁體中文、條列式、帶規則編號 的答案。 - 前端 UI 呈現:以 Web 介面整合問題輸入、引用條文(Top-N)與最終回答,提供可追溯的規則查詢。

目的
本專案的目標,是將 113 年度桌球規則 文件數位化並向量化,結合 pgvector 與 LLM,實現一個能夠:
- 以自然語言提問
- 自動檢索相關規則條文
- 由大語言模型整理回答並附規則編號
這樣可以取代傳統人工翻閱 PDF 的方式,提升查詢效率與正確性。
資料蒐集與前處理
資料來源
資料來源是中華民國桌球規則的《113 桌球規則》,內容完整且是有權威機構認定。
資料結構分析
由於 PDF 內容包含「章 → 節 → 條/子條」層級結構,還夾雜頁碼與不規則換行,需要做前處理 。
我們使用 tta_rules_chunker.py 進行以下步驟:
- 使用 PyMuPDF (fitz) 逐頁讀取 PDF
- 正則表達式解析章節條文結構
- 輸出為 JSONL,每一筆為一個 chunk,包含:
rule_id(如 3.2.1.1)hier_path(章節路徑)- 條文全文
text
執行流程
conda create -n tt-rag python=3.11
conda activate tt-rag
pip install pymupdf
python tta_rules_chunker.py -i "113桌球規則-5-40.pdf" -o "tta_rules_chunks.jsonl"
輸出檔案:tta_rules_chunks.jsonl。
環境建立
我們透過 Docker Compose 部署以下服務:
- PostgreSQL + pgvector(db):儲存規則條文、章節脈絡與向量索引(vector embeddings),提供語意檢索能力
- Ollama(ollama):提供本地模型推論,包含 embedding 產生與 LLM 回答
- Ollama Pull(ollama-pull):啟動時自動拉取所需模型,避免第一次執行才下載導致服務不可用
- Backend(backend):RAG 核心服務(檢索、rerank、組 prompt、生成 回答),僅在 Compose 內網被 gateway 呼叫
- Nginx Gateway(gateway):唯一對外入口(
8080),負責靜態前端與反向代理後端 API - pgAdmin(可選):資料庫管理工具(建議僅開發期使用,或限制來源)

.env
OSTGRES_DB=ttrules
POSTGRES_USER=******
POSTGRES_PASSWORD=******
PG_URL=postgresql+psycopg2://******:******@db:5432/ttrules
OLLAMA_URL=http://ollama:11434
EMB_MODEL=bge-m3
RERANK_MODEL=xitao/bge-reranker-v2-m3
LLM_MODEL=qwen3:1.7b
TOP_N=30
TOP_K=5
# 前端來源(本機 nginx 伺服器域名/port)
CORS_ALLOW_ORIGINS=*
docker-compose.yaml
version: "3.9"
services:
db:
image: pgvector/pgvector:pg16
container_name: rag_pg
environment:
POSTGRES_DB: ttrules
POSTGRES_USER: ******
POSTGRES_PASSWORD: ******
# 不對外暴露,僅內網
volumes:
- dbdata:/var/lib/postgresql/data
- ./config/pgvector.sql:/docker-entrypoint-initdb.d/01_pgvector.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-rag} -d ${POSTGRES_DB:-ttrules}"]
interval: 5s
timeout: 3s
retries: 20
ollama:
image: ollama/ollama:latest
container_name: rag_ollama
# 不對外暴露
volumes:
- ollama:/root/.ollama
healthcheck:
test: ["CMD","ollama","list"]
interval: 10s
timeout: 5s
start_period: 30s
retries: 15
ollama-pull:
image: ollama/ollama:latest
container_name: rag_ollama_pull
depends_on:
ollama:
condition: service_healthy
environment:
OLLAMA_HOST: http://ollama:11434
EMB_MODEL: bge-m3
LLM_MODEL: qwen3:1.7b
entrypoint: ["/bin/sh","-lc","echo 'Pulling' $EMB_MODEL '...' ; ollama pull \"$EMB_MODEL\" ; echo 'Pulling' $LLM_MODEL '...' ; ollama pull \"$LLM_MODEL\" ; echo pulled"]
volumes:
- ollama:/root/.ollama
restart: "no"
backend:
build:
context: ./backend
container_name: rag_backend
depends_on:
db:
condition: service_healthy
ollama:
condition: service_healthy
ollama-pull:
condition: service_completed_successfully
environment:
PG_URL: ${PG_URL:-postgresql+psycopg2://rag:ragpass@db:5432/ttrules}
OLLAMA_URL: ${OLLAMA_URL:-http://ollama:11434}
EMB_MODEL: ${EMB_MODEL:-bge-m3}
RERANK_MODEL: ${RERANK_MODEL:-xitao/bge-reranker-v2-m3}
LLM_MODEL: ${LLM_MODEL:-qwen3:1.7b}
TOP_N: ${TOP_N:-30}
TOP_K: ${TOP_K:-5}
USE_RERANK: "1"
# 正式同源,不需 CORS;若仍要限制可改 allow 清單
CORS_ALLOW_ORIGINS: ""
volumes:
- ./backend:/app
- ./data:/data:ro
command: ["gunicorn", "-c", "gunicorn.conf.py", "app:app"]
# 不對外暴露 port
gateway: # 唯一對外入口:Nginx
build:
context: ./frontend
container_name: rag_gateway
depends_on:
backend:
condition: service_started
ports:
- "8080:80" # 對外只開這個
# 可選:若你想把 dist 直接掛進去,也可用 volumes 覆蓋 /usr/share/nginx/html
volumes:
dbdata:
ollama:
資料表結構
我們在 pgvector.sql 中定義了 rules 資料表,用來儲存規則條文、結構化資訊與向量嵌入,並加上索引提升檢索效率。
pgvector.sql
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS rules (
rule_id text PRIMARY KEY,
doc_id text NOT NULL,
version_date date,
jurisdiction text,
source text,
language text,
chapter text,
chapter_title text,
section_id text,
section_title text,
hier_path text[] NOT NULL,
page_start int,
page_end int,
chunk_type text DEFAULT 'rule',
text text NOT NULL,
embedding vector(1024),
meta jsonb
);
CREATE INDEX IF NOT EXISTS idx_rules_doc ON rules (doc_id);
CREATE INDEX IF NOT EXISTS idx_rules_section ON rules (chapter, section_id);
寫入資料
透過 ingest.py,將前處理的 JSONL 寫入資料庫。
流程:
- 讀取 JSONL:解析條文結構
- Embedding:呼叫 Ollama
/v1/embeddings,使用bge-m3產生向量 - Upsert:利用 SQLAlchemy + pgvector 寫入
rules表
程式提供 embed_batch 與 upsert_rules,支援批次處理。

測試 與查詢
完成資料寫入後,透過 answer.py 進入查詢階段。
查詢流程
- 使用者輸入問題,例如「發球規則有哪些?」
- 系統將問題轉向量
- 在資料庫檢索 Top-N 條文(cosine similarity)
- 使用 reranker 模型(如
xitao/bge-reranker-v2-m3)排序 - 將 Top-K 條文送入 LLM(Qwen3:1.7b)生成回答