-- ============================================================================= -- Migration 024: ai_calls — 統一 LLM 呼叫遙測表 -- Operation Ollama-First v5.0 — Phase 1 -- 日期: 2026-05-03 台北 -- 對應戰役: docs/phase0_audit_report_20260503.md(34 個 LLM 呼叫點,AIGenerationHistory 覆蓋率 11.8%) -- ============================================================================= -- 說明: -- 既有 ai_generation_history(4 處)/ ai_usage_tracking(通用)皆未串接其餘 -- 30 個 LLM 呼叫點,無法支撐 Phase 5 Token 日報、Phase 9 預算告警、Phase 11 RAG。 -- ai_calls 為 append-only 遙測表,所有 LLM 調用統一寫入;async fire-and-forget。 -- -- 設計決策(詳見 docs/phase1_db_design_20260503.md): -- 1. BIGSERIAL:90 天保留 ~6.5M 筆,預留向上空間(INT4 上限 21 億夠用,但與 mcp_calls 保持一致用 BIGSERIAL) -- 2. 不 partition(V1):6.5M / 90 天 ≈ 72k/day,PostgreSQL 單表可承受到 ~50M 才需要分區。 -- 若 Phase 5 後實測 query latency 退化或月寫入超 1M,再切 monthly partition。 -- 3. 90 天 hot data:以 created_at < NOW() - INTERVAL '90 days' DELETE,由 scheduler 跑(不 archive) -- 4. cost_usd 預設 0:由 logger 端依 provider+model 試算填入;不可信時保 0 不誤導 -- 5. JSONB meta 不加 GIN index(V1):查詢需求未明,避免寫入放大;待 Phase 5 報表 patten 穩定再評估 -- -- 回滾腳本(緊急用): -- DROP INDEX IF EXISTS idx_ai_calls_called_at; -- DROP INDEX IF EXISTS idx_ai_calls_caller_called_at; -- DROP INDEX IF EXISTS idx_ai_calls_provider_called_at; -- DROP INDEX IF EXISTS idx_ai_calls_request_id; -- DROP INDEX IF EXISTS idx_ai_calls_status_called_at; -- DROP TABLE IF EXISTS ai_calls; -- -- critic-A11 修補(2026-05-03): -- B2 → 026 加 pgcrypto extension -- H1 → provider CHECK 白名單(NOT VALID) -- H2 → meta/error 大小 CHECK -- M3 → status NOT NULL + fallback_to consistency CHECK -- M5 → partial index 精確列舉 ('error','timeout','fallback') -- L3 → duration_ms 範圍 CHECK -- ============================================================================= CREATE TABLE IF NOT EXISTS ai_calls ( id BIGSERIAL PRIMARY KEY, called_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 呼叫點識別(A1 audit 34 點命名表,logger 須限制在白名單;新增需 ADR) -- hermes_analyst, hermes_intent, hermes_ea_prefetch, -- km_embedding_worker, km_embedding_realtime, -- aider_heal, -- mcp_l1_grounding, mcp_l2_grounding, mcp_l3_ollama, -- openclaw_daily, openclaw_weekly, openclaw_monthly, openclaw_meta, openclaw_qa, -- nemotron_dispatch, -- code_review_hermes, code_review_openclaw, code_review_elephant, -- ea_engine, -- ppt_gemini, ppt_ollama, ppt_nim, -- sales_copy, trend_match, trend_qa, product_insights, trend_keywords, -- tg_bot_copy, tg_bot_copy_v2, -- openclaw_bot_main, openclaw_bot_gemini, openclaw_bot_nim, -- bot_api_copy, trend_crawler, ai_provider_generic caller VARCHAR(64) NOT NULL, -- 主機/供應商標籤(A1 audit Section 1.1 主機標記原則) -- gcp_ollama / ollama_111 / gemini / claude / nim / openrouter / nim_via_elephant provider VARCHAR(32) NOT NULL, model VARCHAR(128) NOT NULL, input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0, duration_ms INTEGER, -- ok / fallback / error / timeout / cache_only -- fallback 表示「主路徑失敗,觸發了下游 caller」;下游本身會另寫一筆 ok/error -- M3: status NOT NULL,且 fallback_to 必須與 status='fallback' 一致 status VARCHAR(16) NOT NULL, fallback_to VARCHAR(64), cost_usd NUMERIC(10,6) NOT NULL DEFAULT 0, -- Anthropic / Gemini prompt cache 命中(Phase 5 Token 日報降本指標) cache_hit BOOLEAN NOT NULL DEFAULT FALSE, -- Phase 11 RAG 預留:本次調用是否實質被 RAG 取代/前置攔截 rag_hit BOOLEAN NOT NULL DEFAULT FALSE, -- 串接「同一邏輯請求」的多筆 call(如 Code Review 三鏈、Q&A fallback 鏈) request_id VARCHAR(64), error TEXT, -- prompt_hash / temperature / max_tokens / fingerprint / etc.(不存原始 prompt) meta JSONB, -- ─────── critic-A11 修補:白名單 + PII/膨脹護欄 ─────── -- H1: provider 白名單(NOT VALID 不檢既存資料,僅檢未來寫入) -- 三主機架構(統帥 2026-05-03 確認): -- gcp_ollama = Primary 34.87.90.216 (SSD) -- ollama_secondary = Secondary 34.21.145.224 (SSD) -- ollama_111 = Fallback 192.168.0.111 (HDD/Local) CONSTRAINT chk_ai_calls_provider CHECK ( provider IN ('gcp_ollama','ollama_secondary','ollama_111','gemini','claude', 'nim','openrouter','nim_via_elephant') ), -- M3: status 白名單 + fallback_to 一致性 CONSTRAINT chk_ai_calls_status CHECK ( status IN ('ok','fallback','error','timeout','cache_only') ), CONSTRAINT chk_ai_calls_fallback_consistent CHECK ( (status = 'fallback') = (fallback_to IS NOT NULL) ), -- L3: duration 範圍 (0 ~ 10 分鐘) CONSTRAINT chk_ai_calls_duration_range CHECK ( duration_ms IS NULL OR (duration_ms >= 0 AND duration_ms <= 600000) ), -- H2: meta/error 大小護欄(避免 PII 落地與膨脹) CONSTRAINT chk_ai_calls_meta_size CHECK ( meta IS NULL OR octet_length(meta::text) <= 8192 ), CONSTRAINT chk_ai_calls_error_size CHECK ( error IS NULL OR octet_length(error) <= 4096 ) ); -- ───────────────────────────────────────────────────────────────────────────── -- 索引設計 -- ───────────────────────────────────────────────────────────────────────────── -- (1) 時間範圍掃描(日報/週報「過去 24h / 7d」必用,BRIN 不適合 OLTP 隨機讀) CREATE INDEX IF NOT EXISTS idx_ai_calls_called_at ON ai_calls (called_at DESC); -- (2) GROUP BY caller 報表(日報 Section 3 TOP caller / 全鏈 trace) CREATE INDEX IF NOT EXISTS idx_ai_calls_caller_called_at ON ai_calls (caller, called_at DESC); -- (3) 供應商分布報表(週報 by provider 統計、預算追蹤) CREATE INDEX IF NOT EXISTS idx_ai_calls_provider_called_at ON ai_calls (provider, called_at DESC); -- (4) request_id 串鏈(部分查詢,sparse 欄位不全建) CREATE INDEX IF NOT EXISTS idx_ai_calls_request_id ON ai_calls (request_id) WHERE request_id IS NOT NULL; -- (5) 異常監控(M5: 精確列舉 error/timeout/fallback,避免未知 status 污染) CREATE INDEX IF NOT EXISTS idx_ai_calls_status_called_at ON ai_calls (status, called_at DESC) WHERE status IN ('error','timeout','fallback'); -- ───────────────────────────────────────────────────────────────────────────── -- 權限(沿襲 migration 023 慣例) -- ───────────────────────────────────────────────────────────────────────────── GRANT ALL PRIVILEGES ON ai_calls TO momo; GRANT USAGE, SELECT ON SEQUENCE ai_calls_id_seq TO momo; -- 註: 90 天保留由 scheduler 任務執行 (Phase 5 排程): -- DELETE FROM ai_calls WHERE called_at < NOW() - INTERVAL '90 days'; -- 建議每日 03:00 跑,配合 idx_ai_calls_called_at DESC 倒序掃描可控制成本。 DO $$ BEGIN RAISE NOTICE 'Migration 024 done: ai_calls + 5 indexes (Operation Ollama-First v5.0 P1)'; END $$;