-- ============================================================================= -- Migration 027: rag_query_log — RAG 查詢遙測 (audit log) -- Operation Ollama-First v5.0 — Phase 11 -- 日期: 2026-05-03 台北 -- 對應戰役: ADR-029(Hermes-First 雙塔)+ Phase 11 RAG 自主學習迴圈 -- ============================================================================= -- 說明: -- 每次 RAG 召回(hermes_qa / openclaw_qa / etc.)寫一筆,append-only。 -- 核心指標: -- - hit_count : top_k 召回實際命中數(threshold 過濾後) -- - saved_call : 命中且最終未升級到 LLM => 真實節省成本 -- - feedback_score : Telegram 👍/👎 後填回(NULL = 尚未反饋) -- 與 ai_calls / mcp_calls 透過 request_id 串鏈,跨表 trace 同一邏輯請求。 -- -- 設計決策: -- 1. embedding 與 ai_insights 同維度 1024 (bge-m3),可跨表計 cosine -- 2. ivfflat lists=100 對齊既有風格;資料量達 1M 後依 sqrt(N) 重建 -- (009 ai_insights 用 HNSW,但本表寫入頻繁 + 不需即時最近鄰 query -- 採 ivfflat 寫入便宜,weekly 排程 REINDEX 即可,詳見 design doc) -- 3. used_results BIGINT[] 紀錄命中的 ai_insights.id,方便事後召回率分析 -- (不加 FK;ai_insights 可能 archive,避免 cascade) -- 4. query_text 限 4KB;query_text 可能含 PII(用戶問題)— -- 90 天保留 + 後續 PromotionGate 過濾後才允許進 ai_insights -- 5. caller 與 ai_calls.caller 共白名單(不重複定義 CHECK,避免雙寫漂移; -- 由 application logger 端強制;DB 端僅檢長度) -- -- 回滾腳本(緊急用): -- DROP INDEX IF EXISTS idx_rag_query_log_embedding; -- DROP INDEX IF EXISTS idx_rag_query_log_request_id; -- DROP INDEX IF EXISTS idx_rag_query_log_caller; -- DROP INDEX IF EXISTS idx_rag_query_log_queried_at; -- DROP TABLE IF EXISTS rag_query_log; -- ============================================================================= CREATE TABLE IF NOT EXISTS rag_query_log ( id BIGSERIAL PRIMARY KEY, queried_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 與 ai_calls.caller 同一張白名單(hermes_qa / openclaw_qa / ...) caller VARCHAR(64) NOT NULL, -- 用戶查詢(PII 風險,限 4KB;不 normalize 以保留原始查詢樣貌) query_text TEXT NOT NULL, -- bge-m3 embedding(1024 維,與 ai_insights.embedding 同源;可 NULL = embedding 失敗仍記錄此次嘗試) query_embedding VECTOR(1024), -- 召回參數 top_k INTEGER NOT NULL DEFAULT 5, threshold NUMERIC(4,3) NOT NULL DEFAULT 0.85, -- 召回結果 hit_count INTEGER NOT NULL DEFAULT 0, used_results BIGINT[], -- 命中的 ai_insights.id 陣列(軟連結,不加 FK) -- 是否成功避免 LLM 呼叫(核心成本指標) saved_call BOOLEAN NOT NULL DEFAULT FALSE, -- Telegram 👍/👎 反饋(1-5;NULL = 未反饋) feedback_score INTEGER, -- 與 ai_calls.request_id 串鏈 request_id VARCHAR(64), -- ─────── 護欄 (對齊 critic-A11 風格) ─────── CONSTRAINT chk_rag_threshold CHECK ( threshold BETWEEN 0 AND 1 ), CONSTRAINT chk_rag_top_k CHECK ( top_k BETWEEN 1 AND 50 ), CONSTRAINT chk_rag_hit_count CHECK ( hit_count >= 0 AND hit_count <= top_k ), CONSTRAINT chk_rag_query_size CHECK ( octet_length(query_text) <= 4096 ), CONSTRAINT chk_rag_feedback CHECK ( feedback_score IS NULL OR feedback_score BETWEEN 1 AND 5 ), -- saved_call=TRUE 必須有命中(hit_count > 0)才合理 CONSTRAINT chk_rag_saved_consistent CHECK ( (saved_call = FALSE) OR (hit_count > 0) ) ); -- ───────────────────────────────────────────────────────────────────────────── -- 索引設計 -- ───────────────────────────────────────────────────────────────────────────── -- (1) 時間範圍掃描(日報 / 命中率報表) CREATE INDEX IF NOT EXISTS idx_rag_query_log_queried_at ON rag_query_log (queried_at DESC); -- (2) caller 分布(哪個入口 RAG 命中率高) CREATE INDEX IF NOT EXISTS idx_rag_query_log_caller ON rag_query_log (caller, queried_at DESC); -- (3) request_id 串鏈(部分索引,sparse 不全建) CREATE INDEX IF NOT EXISTS idx_rag_query_log_request_id ON rag_query_log (request_id) WHERE request_id IS NOT NULL; -- (4) pgvector ivfflat(cosine similarity;只索引非 NULL embedding) -- 注意: ivfflat 須先有資料才能正確訓練 lists;空表建索引會 fallback exact scan, -- Phase 11 灌入首批查詢後若效能退化,REINDEX CONCURRENTLY 重訓 CREATE INDEX IF NOT EXISTS idx_rag_query_log_embedding ON rag_query_log USING ivfflat (query_embedding vector_cosine_ops) WITH (lists = 100) WHERE query_embedding IS NOT NULL; -- ───────────────────────────────────────────────────────────────────────────── -- 權限 -- ───────────────────────────────────────────────────────────────────────────── GRANT ALL PRIVILEGES ON rag_query_log TO momo; GRANT USAGE, SELECT ON SEQUENCE rag_query_log_id_seq TO momo; -- 註: 90 天保留由 scheduler 任務執行(與 ai_calls 對齊): -- DELETE FROM rag_query_log WHERE queried_at < NOW() - INTERVAL '90 days'; -- 建議 03:30 跑(ai_calls 03:00 之後),避免 IO 尖峰 DO $$ BEGIN RAISE NOTICE 'Migration 027 done: rag_query_log + 4 indexes (ivfflat 1024d) (Operation Ollama-First v5.0 P11)'; END $$;