rag-llm-app / ABLATION_SYSTEM_VERIFICATION.md
babaralishah's picture
docs
db2b990
|
Raw History Blame Contribute Delete
14 kB

Ablation Study System - Complete Wiring Verification βœ…

Executive Summary

The ablation study system is fully wired and operational. All feature flags flow correctly from the Streamlit UI through the FastAPI backend to the RAG pipeline with proper cache isolation per phase.


1. UI Layer (ui.py)

Phase Selector Creation

βœ… Line 278-285: Phase selectbox created in sidebar

phase = st.selectbox(
    "Select experimental phase",
    options=["V1", "V2", "V3", "V4", "V5", "V6"],
    index=5,
    help="V1: Basic RAG | V2: +Query Rewriter | V3: +History | V4: +Hybrid | V5: +Reranker | V6: +Guardrails",
)

Query Payload

βœ… Line 345: Phase included in every query request

payload = {
    "question": question,
    "top_k": top_k,
    "use_reranker": use_reranker,
    "use_hybrid": use_hybrid,
    "phase": phase,  # ← SENT WITH EVERY QUERY
    "history": [...]
}

2. Backend Request Model (api.py)

QueryRequest Definition

βœ… Line 175-180: Phase field properly defined

class QueryRequest(BaseModel):
    question: str
    top_k: int = TOP_K
    use_reranker: bool = True
    use_hybrid: bool = True
    phase: Optional[str] = None        # ← ACCEPTS PHASE FROM UI
    history: Optional[List[ChatMessage]] = None

3. Phase Resolution Logic (api.py)

resolve_phase_flags()

βœ… Line 184-194: Converts phase string to feature flags

def resolve_phase_flags(phase: Optional[str]) -> Dict[str, bool]:
    if not phase:
        return {}
    
    phase_key = phase.strip().upper()  # "v6" β†’ "V6"
    if phase_key not in PHASE_FLAGS:
        logger.warning("Unknown phase '%s', falling back to runtime toggles.", phase_key)
        return {}
    
    return PHASE_FLAGS[phase_key].copy()  # ← RETURNS DICT OF FLAGS

get_query_settings()

βœ… Line 196-209: Routes to correct phase or fallback

def get_query_settings(req: QueryRequest) -> Dict[str, bool]:
    phase_settings = resolve_phase_flags(req.phase)
    if phase_settings:
        return phase_settings  # ← USE PHASE FLAGS IF PROVIDED
    
    # Fallback to individual toggles if no phase specified
    return {
        "use_query_rewriter": True,
        "use_hybrid": req.use_hybrid,
        "use_reranker": req.use_reranker,
        "use_chat_history": True,
        "use_guardrails": False,
    }

4. Phase Definitions (config.py)

PHASE_FLAGS Dictionary

βœ… config.py lines 27-57: All 6 phases defined

PHASE_FLAGS = {
    "V1": {  # Baseline: No features
        "use_query_rewriter": False,
        "use_hybrid": False,
        "use_chat_history": False,
        "use_reranker": False,
        "use_guardrails": False,
    },
    "V2": {  # +Query Rewriter
        "use_query_rewriter": True,
        "use_hybrid": False,
        "use_chat_history": False,
        "use_reranker": False,
        "use_guardrails": False,
    },
    "V3": {  # +Chat History
        "use_query_rewriter": True,
        "use_hybrid": False,
        "use_chat_history": True,
        "use_reranker": False,
        "use_guardrails": False,
    },
    "V4": {  # +Hybrid Search
        "use_query_rewriter": True,
        "use_hybrid": True,
        "use_chat_history": True,
        "use_reranker": False,
        "use_guardrails": False,
    },
    "V5": {  # +Reranker
        "use_query_rewriter": True,
        "use_hybrid": True,
        "use_chat_history": True,
        "use_reranker": True,
        "use_guardrails": False,
    },
    "V6": {  # +Guardrails
        "use_query_rewriter": True,
        "use_hybrid": True,
        "use_chat_history": True,
        "use_reranker": True,
        "use_guardrails": True,
    },
}

5. Query Execution Pipeline (/query endpoint, api.py)

Step 1: Extract Phase Flags

βœ… Line 607: Get flags from selected phase

flags = get_query_settings(req)
# flags = {"use_query_rewriter": True, "use_hybrid": True, ...}

Step 2: Chat History Based on Flags

βœ… Line 608: Include history only if phase enables it

history = normalize_chat_history(req.history) if flags["use_chat_history"] else []

Step 3: Query Rewriting Based on Flags

βœ… Line 609-611: Use query rewriter only if phase enables it

query_text, query_vector = rewrite_and_embed_query(
    question, use_query_rewriter=flags["use_query_rewriter"]  # ← FLAG PASSED
)
  • If use_query_rewriter=False β†’ question used as-is
  • If use_query_rewriter=True β†’ question rewritten via Gemini

Step 4: Document Search with Flags

βœ… Line 612-618: Pass hybrid & reranker flags to search

retrieved = search_documents(
    store,
    query_text,
    query_vector,
    top_k=req.top_k,
    use_hybrid=flags["use_hybrid"],          # ← Semantic + BM25
    use_reranker=flags["use_reranker"],      # ← Sets retrieve_k
)
  • If use_reranker=True β†’ retrieve 12 chunks, keep top 6 after reranking
  • If use_reranker=False β†’ retrieve exactly top_k chunks

Step 5: Answer Generation with Flags

βœ… Line 629-636: Pass reranker & guardrail flags to LLM pipeline

result = generate_answer_payload(question, retrieved, req, flags, history=history)

Where in generate_answer_payload:

def generate_answer_payload(question: str, retrieved: list, req: QueryRequest, flags: Dict[str, bool], ...):
    out = rag_answer(
        question=question,
        retrieved=retrieved,
        min_score=MIN_SCORE,
        use_reranker=flags["use_reranker"],         # ← RERANKING FLAG
        final_top_k=req.top_k,
        use_guardrails=flags["use_guardrails"],     # ← GUARDRAIL FLAG
        history=history,
    )
    # ...

6. RAG Pipeline Logic (src/rag_pipeline.py)

Reranking Based on Flag

βœ… Lines 75-98: Reranking logic respects flag

if use_reranker and retrieved:
    # Cross-encoder reranks the candidates
    reranker = get_reranker()
    retrieved = reranker.rerank(...)
else:
    logger.info("β›” Re-ranking DISABLED by user")
    for r in retrieved:
        r["final_score"] = r.get("score", 0.0)

Guardrails Based on Flag

βœ… Lines 107-121: Guardrail abstention respects flag

if use_guardrails and retrieved:
    max_score = max((r.get("score", 0.0) for r in retrieved), default=0.0)
    if max_score < min_score:  # min_score = 0.5
        logger.warning("Guardrail triggered: max retrieved score %.3f...", max_score, min_score)
        return {
            "answer": "I don't have enough high-confidence information from the documents...",
            "sources": [],
        }

7. Cache Isolation by Phase (src/cache.py)

Cache Key Includes Phase

βœ… Line 47: Function signature accepts phase

def get_cache_key(question: str, use_hybrid=True, use_reranker=True, top_k=6, phase: str | None = None):

Phase Baked into Hash

βœ… Lines 60-65: Phase included in cache key computation

data = {
    "q": question.strip().lower(),
    "hybrid": bool(use_hybrid),
    "rerank": bool(use_reranker),
    "k": int(top_k),
    "phase": phase.strip().upper() if phase else None,  # ← PHASE IN KEY
}
return hashlib.md5(json.dumps(data, sort_keys=True).encode()).hexdigest()

Cache Key Called with Phase

βœ… api.py line 590-596: Cache key generation includes phase

cache_key = get_cache_key(
    question=question,
    use_hybrid=req.use_hybrid,
    use_reranker=req.use_reranker,
    top_k=req.top_k,
    phase=req.phase,  # ← PHASE PASSED
)

Result: Same question returns different cache keys per phase

  • cache_key_v1("What is ML?") β‰  cache_key_v6("What is ML?")
  • Prevents cross-phase cache contamination βœ…

8. Feature Disable Verification

Feature V1 V2 V3 V4 V5 V6 Disabled When
Query Rewriter ❌ βœ… βœ… βœ… βœ… βœ… Line 609: rewrite_and_embed_query(..., use_query_rewriter=False)
Hybrid Search ❌ ❌ ❌ βœ… βœ… βœ… Line 617: search_documents(..., use_hybrid=False) β†’ only semantic
Chat History ❌ ❌ βœ… βœ… βœ… βœ… Line 608: history = [] if flag is False
Reranker ❌ ❌ ❌ ❌ βœ… βœ… Line 612: use_hybrid=False sets retrieve_k = top_k; Line 85 skips reranking
Guardrails ❌ ❌ ❌ ❌ ❌ βœ… Line 632: use_guardrails=False β†’ no score threshold check

9. Tested Scenarios

All verified in tests/test_phase_flags.py:

βœ… Test 1: Phase keys V1-V6 present in PHASE_FLAGS βœ… Test 2: resolve_phase_flags() returns correct dictionaries for each phase βœ… Test 3: get_query_settings() properly resolves phases βœ… Test 4: Cache keys differ per phase (prevents cross-phase hits) βœ… Test 5: Guardrail abstention triggers at score 0.2 (< 0.5 threshold) when enabled βœ… Test 6: Normal LLM flow occurs at score 0.95 even when guardrails enabled βœ… Test 7: Guardrails don't trigger in V5 (guardrails disabled)


10. Complete Request Flow Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ STREAMLIT UI (ui.py)                                         β”‚
β”‚ User selects phase (V1-V6) and asks a question             β”‚
β”‚ Sends: {"question": "...", "phase": "V6", ...}             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
                       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ FASTAPI /query ENDPOINT (api.py:583)                       β”‚
β”‚ 1. Extract phase from request β†’ "V6"                       β”‚
β”‚ 2. Call get_query_settings(req)                            β”‚
β”‚    β†’ resolve_phase_flags("V6")                             β”‚
β”‚    β†’ Returns PHASE_FLAGS["V6"]                             β”‚
β”‚    β†’ {use_query_rewriter: True, use_hybrid: True, ...}    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β–Ό             β–Ό             β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ History β”‚  β”‚ Query   β”‚  β”‚ Search &     β”‚
    β”‚ Filter  β”‚  β”‚ Rewrite β”‚  β”‚ Rerank       β”‚
    β”‚         β”‚  β”‚         β”‚  β”‚              β”‚
    β”‚ use_... β”‚  β”‚ use_... β”‚  β”‚ use_hybrid   β”‚
    β”‚ history β”‚  β”‚ rewriterβ”‚  β”‚ use_reranker β”‚
    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚             β”‚              β”‚
         β–Ό             β–Ό              β–Ό
   [filtered or ] [rewritten or]  [retrieve+rerank
    full history] original query   or just semantic]
         β”‚             β”‚              β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β–Ό
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚ generate_answer_payload()    β”‚
         β”‚ Calls rag_answer() with:     β”‚
         β”‚ - use_reranker               β”‚
         β”‚ - use_guardrails             β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ rag_pipeline.rag_answer()        β”‚
    β”‚                                  β”‚
    β”‚ If use_reranker:                 β”‚
    β”‚   Cross-encode score candidates  β”‚
    β”‚ If use_guardrails:               β”‚
    β”‚   Check max_score >= 0.5        β”‚
    β”‚   If not β†’ abstain               β”‚
    β”‚                                  β”‚
    β”‚ Generate answer + evaluate       β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Cache with phase in key     β”‚
  β”‚ (prevent cross-phase hits)  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Return answer to UI         β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

11. Conclusion

βœ… System Status: FULLY OPERATIONAL

All wiring verified:

  • UI phase selector β†’ sends phase string
  • QueryRequest accepts phase
  • Phase resolution β†’ correctly maps to PHASE_FLAGS
  • Feature flags β†’ properly extracted and passed through pipeline
  • Each flag β†’ correctly disables/enables the corresponding feature
  • Cache isolation β†’ prevents cross-phase contamination
  • Tests β†’ all passing and verified

You can now:

  1. Start the backend: python -m uvicorn src.api:app --reload
  2. Start the UI: streamlit run ui.py
  3. Select phase V1-V6 in the dropdown
  4. Submit queries and observe feature behavior change per phase
  5. Run full ablation study with high confidence in feature isolation