Files
ewoooc/migrations/028_create_learning_episodes.sql
OoO 2f20d8d7ba
All checks were successful
CD Pipeline / deploy (push) Successful in 3m30s
db(p11): rag_query_log + learning_episodes — RAG 自主學習迴圈基礎
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>
2026-05-03 23:39:47 +08:00

170 lines
8.4 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- =============================================================================
-- Migration 028: learning_episodes — 蒸餾池 / 知識庫前哨
-- Operation Ollama-First v5.0 — Phase 11
-- 日期: 2026-05-03 台北
-- 對應戰役: ADR-029Hermes-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
-- 蒸餾後的精煉文本≤16KBraw 不存在此表,由 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 ivfflatStage 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 狀態降權 worker24h 無反饋)由 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 $$;