All checks were successful
CD Pipeline / deploy (push) Successful in 3m30s
Operation Ollama-First v5.0 / Phase 11 RAG + 自主學習 migrations/027 — rag_query_log(每次 RAG 查詢的 audit log) - query_text 4KB CHECK + 90 天保留 - VECTOR(1024) bge-m3 embedding (與 ai_insights 一致簽名) - ivfflat lists=100 索引 - saved_call 欄位追蹤「成功攔截 LLM 呼叫」次數 - feedback_score 1-5(NULL=未反饋) - 6 條 CHECK 含 chk_rag_saved_consistent migrations/028 — learning_episodes(蒸餾池 → ai_insights 前哨) - 8 狀態機:pending/approved/awaiting_review/rejected_*4/expired - weight 0-1(>=0.8 觸發 PromotionGate Stage 4 人工驗收) - 9 條 CHECK 含 chk_le_approved_consistent / chk_le_review_consistent - partial index idx_le_status WHERE in (pending, awaiting_review) - distilled_text 16KB 上限 docs/phase11_db_design — 設計文檔 - 6 大決策(兩表分離 / ivfflat / partial index / 軟連結 / 90天保留 / 應用層白名單) - 6 大風險評估(R1 PII / R2 蒸餾失誤 / R3 ivfflat 退化 / R4 dangling FK / R5/R6 trade-off) - Phase 11 上線後驗收 SQL(EXPLAIN ANALYZE) PromotionGate 4 階段(v5.0 護欄 #1, ADR-033): Stage 1: quality_score >= 0.7 Stage 2: 無幻覺檢測(規則引擎,零 LLM) Stage 3: 與既有 insight 相似度 < 0.95 Stage 4: weight >= 0.8 必經 Telegram 👍/👎(24h 無回應 → expired) A4 fullstack-engineer 同時在寫 services/rag_service.py + learning_pipeline.py, service 完成後一起部署啟用。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
170 lines
8.4 KiB
SQL
170 lines
8.4 KiB
SQL
-- =============================================================================
|
||
-- Migration 028: learning_episodes — 蒸餾池 / 知識庫前哨
|
||
-- Operation Ollama-First v5.0 — Phase 11
|
||
-- 日期: 2026-05-03 台北
|
||
-- 對應戰役: ADR-029(Hermes-First)+ Phase 11 PromotionGate 4 階段過濾
|
||
-- =============================================================================
|
||
-- 說明:
|
||
-- LLM/MCP 結果先寫入 learning_episodes(蒸餾池),過 4 階段 PromotionGate
|
||
-- 才晉升 ai_insights(知識庫主檔)。設計目的:
|
||
-- - 隔離未驗證內容,避免直接污染 RAG 召回語料
|
||
-- - 保留 raw + distilled,方便事後重訓
|
||
-- - 高權重(>=0.8)走人工驗收,低權重走自動晉升
|
||
--
|
||
-- PromotionGate 狀態機:
|
||
-- pending
|
||
-- ├─[Stage 1: quality<0.7]→ rejected_quality
|
||
-- ├─[Stage 2: 規則檢測幻覺]→ rejected_hallucination
|
||
-- ├─[Stage 3: 與既有 insight cosine>0.95]→ rejected_duplicate
|
||
-- ├─[Stage 4a: weight<0.8 + 過 1-3]→ approved → 寫 ai_insights → insight_id 回填
|
||
-- └─[Stage 4b: weight>=0.8]→ awaiting_review → Telegram 推播
|
||
-- ├─[人工 👍]→ approved
|
||
-- ├─[人工 👎]→ rejected_human
|
||
-- └─[24h 無反饋]→ expired (weight 降為 0.5 重走 Stage 4a)
|
||
--
|
||
-- 設計決策:
|
||
-- 1. insight_id 軟連結(不加 FK)—— ai_insights archive 不應 cascade 影響蒸餾池
|
||
-- 2. source_table + source_id 軟連結到 ai_calls / mcp_calls,方便事後重訓溯源
|
||
-- 3. embedding 與 rag_query_log 同 1024 維,跨表 cosine 一致
|
||
-- 4. 不設 90 天保留(蒸餾池長期保留;approved/rejected_* 進冷儲檔由後續 ADR 定)
|
||
-- —— 短期內暴增風險:靠 partial index + monthly archive scheduler 控制
|
||
-- 5. promotion_status 用 VARCHAR(32) + CHECK 白名單;不上 ENUM 因新增狀態方便
|
||
-- 6. rejected_reason CHECK 強制 rejected_* 狀態必填,避免「沒原因的拒絕」
|
||
-- 7. human_approver 存 Telegram username 的 SHA1[:8],避免 PII 落地
|
||
--
|
||
-- 回滾腳本(緊急用):
|
||
-- DROP INDEX IF EXISTS idx_le_embedding;
|
||
-- DROP INDEX IF EXISTS idx_le_insight_id;
|
||
-- DROP INDEX IF EXISTS idx_le_episode_type;
|
||
-- DROP INDEX IF EXISTS idx_le_status;
|
||
-- DROP INDEX IF EXISTS idx_le_created_at;
|
||
-- DROP TABLE IF EXISTS learning_episodes;
|
||
-- =============================================================================
|
||
|
||
CREATE TABLE IF NOT EXISTS learning_episodes (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
||
-- 來源類型
|
||
-- mcp_result = MCP server 抓回的事實(grounding / search / db query)
|
||
-- llm_response = LLM 生成的洞察 / 摘要(hermes_analyst / openclaw 等)
|
||
-- user_feedback = 用戶 Telegram 直接告知的事實(高 weight,需人工確認)
|
||
-- manual_curated = 人工手動入庫(最高 weight,跳 PromotionGate)
|
||
episode_type VARCHAR(32) NOT NULL,
|
||
|
||
-- 軟連結來源(不加 FK)
|
||
source_table VARCHAR(32), -- 'ai_calls' / 'mcp_calls' / NULL
|
||
source_id BIGINT, -- 對應 source_table 的 id
|
||
|
||
-- 蒸餾後的精煉文本(≤16KB;raw 不存在此表,由 source_table 透過 source_id 回查)
|
||
distilled_text TEXT NOT NULL,
|
||
embedding VECTOR(1024), -- 與 ai_insights / rag_query_log 同維
|
||
|
||
-- 蒸餾品質評分(0-1)
|
||
-- <0.7 → Stage 1 直接 rejected_quality
|
||
-- >=0.7 → 進 Stage 2-3
|
||
quality_score NUMERIC(4,3) NOT NULL DEFAULT 0.0,
|
||
|
||
-- 權重(影響晉升路徑)
|
||
-- <0.8 → Stage 4a 自動晉升
|
||
-- >=0.8 → Stage 4b 人工驗收
|
||
weight NUMERIC(4,3) NOT NULL DEFAULT 0.5,
|
||
|
||
-- PromotionGate 狀態(見上方狀態機)
|
||
promotion_status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||
|
||
-- 晉升結果
|
||
insight_id BIGINT, -- 晉升後對應 ai_insights.id(軟連結,無 FK)
|
||
rejected_reason TEXT, -- promotion_status=rejected_* 時必填
|
||
human_approver VARCHAR(64), -- Telegram username SHA1[:8]
|
||
reviewed_at TIMESTAMPTZ,
|
||
|
||
-- ─────── 護欄 (對齊 critic-A11 風格) ───────
|
||
CONSTRAINT chk_le_quality CHECK (
|
||
quality_score BETWEEN 0 AND 1
|
||
),
|
||
CONSTRAINT chk_le_weight CHECK (
|
||
weight BETWEEN 0 AND 1
|
||
),
|
||
CONSTRAINT chk_le_episode_type CHECK (
|
||
episode_type IN ('mcp_result','llm_response','user_feedback','manual_curated')
|
||
),
|
||
CONSTRAINT chk_le_status CHECK (
|
||
promotion_status IN (
|
||
'pending','approved','awaiting_review',
|
||
'rejected_quality','rejected_hallucination','rejected_duplicate','rejected_human',
|
||
'expired'
|
||
)
|
||
),
|
||
CONSTRAINT chk_le_distilled_size CHECK (
|
||
octet_length(distilled_text) <= 16384
|
||
),
|
||
CONSTRAINT chk_le_rejected_reason CHECK (
|
||
(promotion_status NOT LIKE 'rejected_%') OR (rejected_reason IS NOT NULL)
|
||
),
|
||
-- approved 必須有 insight_id;其他狀態不應有
|
||
CONSTRAINT chk_le_approved_consistent CHECK (
|
||
(promotion_status = 'approved') = (insight_id IS NOT NULL)
|
||
),
|
||
-- source_table + source_id 一致性(要嘛兩個都 NULL,要嘛兩個都有)
|
||
CONSTRAINT chk_le_source_consistent CHECK (
|
||
(source_table IS NULL AND source_id IS NULL)
|
||
OR (source_table IS NOT NULL AND source_id IS NOT NULL)
|
||
),
|
||
CONSTRAINT chk_le_source_table CHECK (
|
||
source_table IS NULL OR source_table IN ('ai_calls','mcp_calls')
|
||
),
|
||
-- 人工驗收時 reviewed_at 必填
|
||
CONSTRAINT chk_le_review_consistent CHECK (
|
||
(human_approver IS NULL) OR (reviewed_at IS NOT NULL)
|
||
)
|
||
);
|
||
|
||
-- ─────────────────────────────────────────────────────────────────────────────
|
||
-- 索引設計
|
||
-- ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
-- (1) 時間範圍掃描(蒸餾池規模監控)
|
||
CREATE INDEX IF NOT EXISTS idx_le_created_at
|
||
ON learning_episodes (created_at DESC);
|
||
|
||
-- (2) 待處理佇列查詢(PromotionGate worker / 人工驗收 dashboard)
|
||
-- partial index 縮體積:只關心 pending / awaiting_review 兩種「活躍」狀態
|
||
CREATE INDEX IF NOT EXISTS idx_le_status
|
||
ON learning_episodes (promotion_status, created_at DESC)
|
||
WHERE promotion_status IN ('pending','awaiting_review');
|
||
|
||
-- (3) 來源類型分布報表
|
||
CREATE INDEX IF NOT EXISTS idx_le_episode_type
|
||
ON learning_episodes (episode_type, created_at DESC);
|
||
|
||
-- (4) insight_id 反查(從 ai_insights 反推蒸餾來源)
|
||
CREATE INDEX IF NOT EXISTS idx_le_insight_id
|
||
ON learning_episodes (insight_id)
|
||
WHERE insight_id IS NOT NULL;
|
||
|
||
-- (5) pgvector ivfflat(Stage 3 重複檢測 cosine query 主用)
|
||
CREATE INDEX IF NOT EXISTS idx_le_embedding
|
||
ON learning_episodes
|
||
USING ivfflat (embedding vector_cosine_ops)
|
||
WITH (lists = 100)
|
||
WHERE embedding IS NOT NULL;
|
||
|
||
-- ─────────────────────────────────────────────────────────────────────────────
|
||
-- 權限
|
||
-- ─────────────────────────────────────────────────────────────────────────────
|
||
GRANT ALL PRIVILEGES ON learning_episodes TO momo;
|
||
GRANT USAGE, SELECT ON SEQUENCE learning_episodes_id_seq TO momo;
|
||
|
||
-- 註: expired 狀態降權 worker(24h 無反饋)由 scheduler 跑:
|
||
-- UPDATE learning_episodes
|
||
-- SET promotion_status='expired', weight=0.5
|
||
-- WHERE promotion_status='awaiting_review'
|
||
-- AND created_at < NOW() - INTERVAL '24 hours';
|
||
-- 之後由 PromotionGate Stage 4a 重跑該批 expired 走自動晉升路徑。
|
||
|
||
DO $$
|
||
BEGIN
|
||
RAISE NOTICE 'Migration 028 done: learning_episodes + 5 indexes + 9 CHECK constraints (Operation Ollama-First v5.0 P11)';
|
||
END $$;
|