跳至主要内容

A RAG-Based Table Tennis RuleBot System

我們希望能夠建立一個 桌球規則問答系統,透過 RAG(Retrieval-Augmented Generation)技術,讓使用者能用自然語言快速查詢桌球比賽規則,並自動生成精確、可追溯的答案。


系統成果

整個系統流程如下:

  1. 使用者提問:以自然語言輸入問題(例如「桌球的桌子高度應該要多高?」)。
  2. Embedding 檢索
    • bge-m3 將問題轉換成向量
    • 在 PostgreSQL + pgvector 進行 Top-N 語意檢索
  3. Rerankxitao/bge-reranker-v2-m3 對檢索結果重新排序,挑出最相關的 Top-K 條文。
  4. 生成回答qwen3:1.7b 閱讀相關規則段落,輸出 繁體中文、條列式、帶規則編號 的答案。
  5. 前端 UI 呈現:以 Web 介面整合問題輸入、引用條文(Top-N)與最終回答,提供可追溯的規則查詢。

result


目的

本專案的目標,是將 113 年度桌球規則 文件數位化並向量化,結合 pgvectorLLM,實現一個能夠:

  • 以自然語言提問
  • 自動檢索相關規則條文
  • 由大語言模型整理回答並附規則編號

這樣可以取代傳統人工翻閱 PDF 的方式,提升查詢效率與正確性。


資料蒐集與前處理

資料來源

資料來源是中華民國桌球規則的《113 桌球規則》,內容完整且是有權威機構認定。

資料結構分析

由於 PDF 內容包含「章 → 節 → 條/子條」層級結構,還夾雜頁碼與不規則換行,需要做前處理。

我們使用 tta_rules_chunker.py 進行以下步驟:

  1. 使用 PyMuPDF (fitz) 逐頁讀取 PDF
  2. 正則表達式解析章節條文結構
  3. 輸出為 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(可選):資料庫管理工具(建議僅開發期使用,或限制來源)

docker-compose

.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 寫入資料庫。

流程:

  1. 讀取 JSONL:解析條文結構
  2. Embedding:呼叫 Ollama /v1/embeddings,使用 bge-m3 產生向量
  3. Upsert:利用 SQLAlchemy + pgvector 寫入 rules

程式提供 embed_batchupsert_rules,支援批次處理。

ingest-data.png


測試與查詢

完成資料寫入後,透過 answer.py 進入查詢階段。

查詢流程

  1. 使用者輸入問題,例如「發球規則有哪些?」
  2. 系統將問題轉向量
  3. 在資料庫檢索 Top-N 條文(cosine similarity)
  4. 使用 reranker 模型(如 xitao/bge-reranker-v2-m3)排序
  5. 將 Top-K 條文送入 LLM(Qwen3:1.7b)生成回答