Lab 04 — Support Knowledge Base with RAG — AI Infrastructure for Real Business Problems
Last updated 10/17/2026
Course › Hands-on track › Lab 04

Lab 04 — Support Knowledge Base with RAG

客服知識庫(RAG) · Hands-on track: AI Infrastructure for Real Business Problems

Code: labs/04/ · Uses: data/docs/*.md (7 policy docs), data/eval/support_qa.jsonl (15 questions)

Business Problem — 商業問題

Half of Kettle & Kiln's support email asks questions the policies already answer:

Staff answer from memory and sometimes get it wrong. A generic chatbot is worse: it confidently invents a 60-day return window.

We need a bot that answers only from our documents, cites the policy section, and says "I'm not sure — I'll pass this to our team" when the documents don't cover the question. And we need a number that tells us whether it's good enough to put in front of customers.

Architecture — 系統架構

flowchart TB
    subgraph Ingest["Ingest (startup or POST /v1/ingest)"]
        D[data/docs/*.md] --> CH[chunking.py<br/>split on headings, stable ids]
        CH --> EM1[embed with doc prefix] --> VS[(Qdrant<br/>or pgvector)]
    end
    subgraph Ask["POST /v1/ask"]
        Q[question] --> EM2[embed with query prefix] --> VS
        Q --> BM[BM25 keyword search]
        VS --> RRF[Reciprocal-rank fusion]
        BM --> RRF
        RRF --> TH{best vector score ≥ RAG_MIN_SCORE?}
        TH -- no --> REF[refusal]
        TH -- yes --> LLM[LLM → JSON: answer, citations, can_answer]
        LLM --> GC{grounding check:<br/>cites only real passages}
        GC --> OUT[answer + citations + retrieved]
    end

Design choices:

Prerequisites — 先決條件

Step-by-Step Setup — 逐步設定

cd hands-on/labs/04
uv venv --python 3.12 && source .venv/bin/activate
uv pip install -e ../common -r requirements.txt
cp .env.example .env
docker compose up -d qdrant          # or: set QDRANT_URL= (empty) for embedded mode
python -m app.ingest                 # {"chunks": N, "embedded": N, ...}; run again → 0 re-embedded
uvicorn app.main:app --port 8004

Ask:

curl -s localhost:8004/v1/ask -H 'content-type: application/json' \
  -d '{"question":"Do gift cards expire?"}' | python -m json.tool
curl -s localhost:8004/v1/ask -H 'content-type: application/json' \
  -d '{"question":"What is your CEO'\''s favourite film?"}' | python -m json.tool   # should refuse

Evaluate:

python eval_rag.py                   # reports/rag-<provider>-<time>.md/.json

The eval reports four metrics:

Calibrate RAG_MIN_SCORE for your embedding model. Cosine scores are not comparable across models. Print retrieved[].vector_score for 10 on-topic and 10 off-topic questions, then set the threshold between the two groups. If the groups overlap (they did on the build box with nomic-embed-text on short policy docs), don't rely on the threshold alone; the model's can_answer=false and the grounding check are the second and third lines of defence.

Using nomic-embed-text? Keep the task prefixes from .env.example (search_query: / search_document:). That model is trained with them. Remove them for other embedding models.

How to Test — 測試方法

LLM_PROVIDER=mock pytest -q          # 8 tests

The tests cover:

What was run on the build box — 實測紀錄

Cost & Performance Notes — 成本與效能

Common Failures & Fixes — 常見錯誤與修正

Symptom Cause Fix
Wrong input: Vector dimension error changed embedding model, same collection collection names include model+dim; re-ingest. Delete old collections in the Qdrant dashboard
Everything refused threshold too high for this embedding model / prefixes missing calibrate RAG_MIN_SCORE; check prefixes
Off-topic questions answered threshold too low rely on can_answer + grounding; add off-topic cases to the eval
Citations like "[5]" when 4 passages were retrieved model invented a citation the grounding check marks grounded: false; show the refusal or a human handoff instead
Embedded Qdrant "already accessed by another instance" two processes on one QDRANT_PATH use the Qdrant server, or one process
Stale answers after a policy edit not re-ingested POST /v1/ingest (idempotent) after every docs change, e.g. from CI or n8n

Stretch Exercise — 延伸練習

  1. Add a reranking step: retrieve 10, ask the model (or a cross-encoder) to score relevance, keep 4. Compare cite_ok and latency in eval_rag.py.
  2. Answer in the customer's language: detect zh-TW questions, keep retrieval on the English docs, and add two zh-TW cases to support_qa.jsonl.

Deliverable Checklist — 交付清單

↑