# Phase 1 DB Design — Operation Ollama-First v5.0 > **日期**:2026-05-03 > **作者**:A3 db-expert > **產出**:3 個 migration(024/025/026)+ 設計理由 + 效能評估 > **依據**:`docs/phase0_audit_report_20260503.md` 34 個 LLM 呼叫點 / 11.8% 覆蓋率 > **狀態**:SQL 檔已產出於 `migrations/`,**未自動 apply**,待統帥 review 後手動執行 --- ## TL;DR | 交付物 | 路徑 | 影響 | |--------|------|------| | `ai_calls` 統一 LLM 遙測表 | `migrations/024_create_ai_calls_table.sql` | 接 30 個未覆蓋呼叫點 | | `mcp_calls` MCP 遙測表 | `migrations/025_create_mcp_calls_and_budgets.sql` | Phase 10 預備 | | `ai_call_budgets` 預算閾值 | 同上(含 5 筆種子) | Phase 9 預算告警 | | `ai_insights.embedding_signature` | `migrations/026_add_embedding_signature.sql` | BGE-M3 一致性護欄 | **結論**:Schema 設計已完備,無 schema 衝突。**A4 接 logger 工作可立即啟動**,唯一前置條件是統帥手動 apply 這 3 個 migration。 --- ## Section 1 — Schema 設計理由 ### 1.1 ai_calls 欄位選擇邏輯 | 欄位 | 為何必要 | 為何這個型別 | |------|---------|-------------| | `id BIGSERIAL` | 90 天 ~6.5M,年累積會超 INT4 21 億的 3% — 提早用 BIGSERIAL 避免將來改型別 | 與 mcp_calls 一致 | | `called_at TIMESTAMPTZ` | 報表查詢核心欄位 | 用 TIMESTAMPTZ(不是 TIMESTAMP),因為 momo 三主機跨時區(GCP UTC / 188 Asia/Taipei) | | `caller VARCHAR(64)` | 必白名單管控;新增需 ADR | 64 足夠(最長 `code_review_elephant` 20 字) | | `provider VARCHAR(32)` | A1 audit 列舉的 7 種主機標籤 | 32 足夠 | | `model VARCHAR(128)` | NIM 模型名可達 50+(如 `nvidia/llama-3.3-nemotron-super-49b-v1.5`) | 128 留 buffer | | `input_tokens / output_tokens` | Token 日報核心;NOT NULL DEFAULT 0 確保 SUM() 不爆 | INT 足夠(單次最大 200K,一年累積一個 caller 也只到 ~10B,INT4 上限 21 億夠) | | `duration_ms INT` | 監控 LLM 慢查;可為 NULL(AiderHeal 走 SSH 拿不到精確值) | INT | | `status` | ok/fallback/error/timeout/cache_only — 串接 fallback 鏈關鍵 | VARCHAR(16) | | `fallback_to` | 「主路徑失敗,下游 caller」串接邏輯;下游本身另寫一筆 | VARCHAR(64) 同 caller | | `cost_usd NUMERIC(10,6)` | Phase 9 預算用;6 位小數可記到 $0.000001(OpenRouter 細粒計費需要) | NUMERIC 不用 FLOAT,避免累計誤差 | | `cache_hit BOOLEAN` | Anthropic prompt cache / Gemini cache(成本降 90%)必追蹤 | 預設 FALSE | | `rag_hit BOOLEAN` | Phase 11 RAG 攔截率核心 KPI | 預設 FALSE | | `request_id VARCHAR(64)` | Code Review 三鏈、Q&A fallback 三層必須 trace 同一邏輯請求 | UUID4 takes 36, 加 prefix 也夠 | | `error TEXT` | 錯誤原文,可長 | TEXT | | `meta JSONB` | prompt_hash, temperature, top_p, fingerprint, embedding_signature 等彈性擴展 | JSONB(非 JSON)支援索引 | ### 1.2 索引設計理由(5 個) | Idx | Cols | 用途 | partial? | |-----|------|------|---------| | `idx_ai_calls_called_at` | (called_at DESC) | 全表時間切片,日報週報必用 | 否 | | `idx_ai_calls_caller_called_at` | (caller, called_at DESC) | TOP caller / 單 caller 趨勢 | 否 | | `idx_ai_calls_provider_called_at` | (provider, called_at DESC) | by provider 統計 / 預算追蹤 | 否 | | `idx_ai_calls_request_id` | (request_id) | trace 單一 request 全鏈 | **WHERE request_id IS NOT NULL** | | `idx_ai_calls_status_called_at` | (status, called_at DESC) | 異常監控 | **WHERE status <> 'ok'**(90%+ 是 ok,partial 大幅縮體) | **未建立的索引**: - `meta JSONB` 的 GIN index — V1 不建。GIN 寫入放大 ~3-5x,且尚未確定查詢 pattern;Phase 5 報表穩定後再評估。 - `model` 單欄索引 — 報表需求都會帶 called_at,已含 idx_ai_calls_called_at,再加 `(model, called_at)` 在 V1 邊際效益低。 ### 1.3 是否 partition by called_at — 決策:**V1 不分區** | 評估面 | 數字 | 結論 | |--------|------|------| | 月寫入量 | 50 ins/min × 60 × 24 × 30 ≈ 2.16M | 中等 | | 90 天保留量 | ~6.5M | PostgreSQL 14 單表健康範圍 | | 索引大小估算(5 個) | ~800MB | 在 momo-db 容器資源內 | | Partition 維護成本 | 須 cron 自動 CREATE 下月 partition + DROP 過期 | **+1 維護負擔** | **決策**:V1 不分區,但留好觸發升級條件: - **觸發升級門檻**:月寫入超 5M、單表超 30M、或日報查詢 latency p95 > 500ms - **升級路徑**:DECLARATIVE PARTITIONING by RANGE(called_at) monthly,配合 `pg_partman` ### 1.4 保留策略 — 90 天 hot data,DELETE 不 archive | 選項 | 優劣 | 結論 | |------|------|------| | 直接 DELETE | 簡單,free space 由 autovacuum 回收 | **採用** | | 移到 ai_calls_archive 表 | 多一份儲存,需另寫查詢 | 否 | | 匯出 JSON 到 S3/GCS | 完整保留,可重建 | Phase 5 後若有合規需求再加 | **理由**:ai_calls 是遙測,30 天前的單筆價值低;trend 已在週報/月報沉澱到 ai_insights。 **清理任務**(scheduler 每日 03:00): ```sql DELETE FROM ai_calls WHERE called_at < NOW() - INTERVAL '90 days'; ``` 配合 `idx_ai_calls_called_at DESC` 倒序掃描,DELETE 範圍小(每日 ~72k),不會 lock。 --- ## Section 2 — 是否與既有 schema 衝突 ### 2.1 與 `ai_generation_history`(4 處 ai_routes.py) - 用途不同:ai_generation_history 是 **產品功能側**(is_favorite / is_used / created_by 都是業務欄位) - ai_calls 是 **基礎設施側遙測** - **共存策略**:A4 接 logger 時,ai_routes.py 那 4 處 **同時雙寫** 兩張表(既有 history 不破壞),ai_calls 是 superset ### 2.2 與 `ai_usage_tracking`(database/ai_models.py L72) - ai_usage_tracking 已存在但**完全沒被 30 個呼叫點接入**(A1 audit 已驗證) - 設計欄位(service_type / request_type / user_id)與 v5.0 戰役所需(caller / provider / fallback_to / request_id)不符 - **建議**:A4 logger 統一寫 ai_calls,ai_usage_tracking **凍結**(不寫入但不刪表,避免 model import 鏈斷裂);待 Phase 5 報表驗證 ai_calls 完整後,Phase 12 再 deprecate ### 2.3 與 `ai_insights.embedding_signature` - 既有 ai_insights 表**沒有** embedding_signature 欄位(已驗證 `database/ai_models.py:111-151`) - 新增為 NULL,**metadata-only ALTER TABLE**,不鎖表(PostgreSQL 11+ 安全) - **無衝突** --- ## Section 3 — 三個查詢效能預估 模擬負載:90 天滿載 ~6.5M 筆,索引 warm。 ### 查詢 1:過去 24h 某 caller 的 token 累計 + 成本(Telegram 日報) ```sql SELECT caller, SUM(input_tokens + output_tokens) AS total_tokens, SUM(cost_usd) AS total_cost, COUNT(*) AS call_count, AVG(duration_ms) AS avg_latency FROM ai_calls WHERE called_at >= NOW() - INTERVAL '24 hours' AND caller = 'openclaw_daily' GROUP BY caller; ``` **預期執行計畫**: ``` Aggregate └─ Index Scan using idx_ai_calls_caller_called_at on ai_calls Index Cond: (caller = 'openclaw_daily' AND called_at >= ...) ``` **預期 latency**:< 5ms(單 caller 24h ~144 筆,索引完全命中) **鎖風險**:無,純 SELECT。 **OLTP 衝擊**:無。 ### 查詢 2:過去 7 天 by provider 統計(週報) ```sql SELECT provider, COUNT(*) AS call_count, SUM(input_tokens + output_tokens) AS total_tokens, SUM(cost_usd) AS total_cost, SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS error_cnt, SUM(CASE WHEN status='fallback' THEN 1 ELSE 0 END) AS fallback_cnt, SUM(CASE WHEN cache_hit THEN 1 ELSE 0 END) AS cache_hits FROM ai_calls WHERE called_at >= NOW() - INTERVAL '7 days' GROUP BY provider ORDER BY total_cost DESC; ``` **預期執行計畫**: ``` Sort └─ HashAggregate └─ Index Scan using idx_ai_calls_provider_called_at on ai_calls Index Cond: (called_at >= ...) ``` **預期 latency**:~50-150ms(7 天 ~500k 筆,6 個 provider HashAggregate) **鎖風險**:無。 **優化建議**:若 latency 退化到 > 200ms,可加 covering index `(provider, called_at, input_tokens, output_tokens, cost_usd)` — V1 先不做。 ### 查詢 3:TOP 10 caller by token(日報 Section 3) ```sql SELECT caller, SUM(input_tokens + output_tokens) AS total_tokens, SUM(cost_usd) AS total_cost FROM ai_calls WHERE called_at >= NOW() - INTERVAL '24 hours' GROUP BY caller ORDER BY total_tokens DESC LIMIT 10; ``` **預期執行計畫**: ``` Limit └─ Sort (top-N) └─ HashAggregate └─ Index Scan using idx_ai_calls_called_at on ai_calls Index Cond: (called_at >= ...) ``` **預期 latency**:~30-80ms(24h ~72k 筆,35 個 caller) **鎖風險**:無。 ### 查詢效能總表 | 查詢 | 預期 latency | 主要索引 | 改善空間 | |------|-------------|---------|---------| | Q1 caller 24h | < 5ms | idx_ai_calls_caller_called_at | 已最佳 | | Q2 provider 7d | 50-150ms | idx_ai_calls_provider_called_at | 可加 covering index | | Q3 TOP-10 caller 24h | 30-80ms | idx_ai_calls_called_at | OK | --- ## Section 4 — 寫入吞吐評估 ### 4.1 尖峰負載 - **峰值**:50 inserts/min ≈ 0.83 ins/sec - **單筆 insert 預估**:5 個索引 × ~1ms WAL flush ≈ 3-8ms - **目標**:p99 < 50ms ✅(極大 buffer) ### 4.2 風險點 | 風險 | 機率 | 影響 | 緩解 | |-----|-----|------|-----| | async fire-and-forget 失敗無告警 | 中 | log 漏寫 | logger 端用 try/except + 告警 channel;連續 5 次失敗觸發 Telegram | | 5 個索引導致寫入放大 | 低 | 同步寫入慢 | partial index 已縮減;50 ins/min 下不會擠壓 OLTP | | autovacuum 跟不上 90 天 DELETE | 低 | 表膨脹 | 每日 03:00 DELETE,配 autovacuum_scale_factor=0.05 | ### 4.3 connection pool 衝擊 A4 logger 採 **fire-and-forget**(非同步寫,不阻塞 caller),須使用獨立 thread + dedicated session pool(建議 size=2,與主應用 pool 隔離),避免擠壓 OLTP。 --- ## Section 5 — 風險與限制 ### 5.1 已知限制 1. **ai_calls 不分區(V1)**:月寫入超 5M 或日報 latency p95 > 500ms 時須升級到 monthly partition 2. **JSONB meta 無 GIN index**:未來若要 `WHERE meta->>'prompt_hash' = ...` 查詢,需另加 GIN 3. **保留策略硬刪除**:30+ 天前的個別呼叫無法回溯(trend 須先進 ai_insights) 4. **ai_call_budgets.provider NULL 唯一性**:靠部分索引強制(PostgreSQL 標準 UNIQUE 不認 NULL) ### 5.2 護欄缺口(待後續 phase 補) - **Phase 5**:ai_calls 寫入失敗的告警通道未定(建議走 EventRouter L0) - **Phase 9**:ai_call_budgets 的 alert_pct 預設 80% 是否合理待實測;budget 超標的 hard-stop 邏輯由應用層實作 - **Phase 11**:embedding_signature 既有 ~XX 萬筆需批次回填(待 SSH 188 跑統計) ### 5.3 ALTER TABLE 026 安全性確認 - PostgreSQL 14(momo-db 容器版本,待 SSH 確認) - 11+ 之後 ADD COLUMN 無 DEFAULT 為 metadata-only:**不鎖表,不重寫** - CREATE INDEX CONCURRENTLY 不阻塞既有寫入,但**不能在 transaction 內**(migration 026 註記已標明) --- ## Section 6 — 部署 Checklist(給統帥) 執行順序與檢查(憲法 ADR-008 — 部署前必驗): - [ ] **SSH 188 確認 PostgreSQL 版本** ≥ 14(migration 026 ALTER TABLE 安全前提) - [ ] **SSH 188 確認 momo-db 磁碟剩餘空間** ≥ 5GB(90 天滿載 ~3GB + headroom) - [ ] **SSH 188 確認 ai_insights 既有筆數**:`SELECT COUNT(*), COUNT(embedding) FROM ai_insights;`(評估 Phase 11 回填工作量) - [ ] 跑 024:`psql -U momo -d momo_pro -f migrations/024_create_ai_calls_table.sql` - [ ] 跑 025:`psql -U momo -d momo_pro -f migrations/025_create_mcp_calls_and_budgets.sql` - [ ] **跑 026 須注意**:含 `CREATE INDEX CONCURRENTLY`,**不能用 BEGIN/COMMIT 包**;用 `psql -1` 會失敗,須用一般 `psql` - [ ] 026 後驗證:`\d ai_insights` 看到 embedding_signature 欄位 + idx_ai_insights_embedding_signature 索引 - [ ] 跑 sanity:`SELECT * FROM ai_call_budgets ORDER BY id;`(確認 5 筆種子) - [ ] 通報 A4:可開始接 logger --- ## Section 7 — Commit Message 草稿(不自動 commit) ``` db: ai_calls/mcp_calls/budgets schema + bge-m3 signature (Operation Ollama-First v5.0 P1) - migrations/024: ai_calls 統一 LLM 遙測表 (5 indexes, partial idx for sparse cols) - migrations/025: mcp_calls + ai_call_budgets (Phase 10/9 預備, 含 5 筆種子預算) - migrations/026: ai_insights.embedding_signature + partial index (BGE-M3 護欄) - docs/phase1_db_design_20260503.md: 設計理由 + 查詢效能預估 + 部署 checklist 無 schema 衝突;ai_usage_tracking 凍結待 Phase 12 deprecate;A4 logger 可啟動。 依據: docs/phase0_audit_report_20260503.md (34 LLM 呼叫點 / 11.8% 覆蓋率) ``` --- ## DB Expert Report(最終結論) ### 審查範圍 - 新增檔案:`migrations/024_create_ai_calls_table.sql`、`migrations/025_create_mcp_calls_and_budgets.sql`、`migrations/026_add_embedding_signature.sql` - 影響資料表:`ai_calls`(新)、`mcp_calls`(新)、`ai_call_budgets`(新)、`ai_insights`(ADD COLUMN) ### 問題清單 無 BLOCKER。 #### 🟡 NOTE 1 — ai_usage_tracking 重疊 - 位置:`database/ai_models.py:72-109` - 說明:既有但未被 30 個呼叫點使用,欄位語意不對齊。 - 風險:A4 寫 logger 時若誤雙寫此表會造成數據混亂。 - 緩解:在設計文 Section 2.2 已明示「凍結,Phase 12 再 deprecate」。 #### 🟡 NOTE 2 — Migration 026 不能用 BEGIN/COMMIT 包 - 位置:`migrations/026_add_embedding_signature.sql` - 說明:`CREATE INDEX CONCURRENTLY` 不能在 transaction block 內執行。 - 緩解:已在檔頭註記,部署 checklist 已標明不用 `psql -1`。 ### 效能分析 - Q1 caller-24h:< 5ms(idx_ai_calls_caller_called_at) - Q2 provider-7d:50-150ms(idx_ai_calls_provider_called_at + HashAggregate) - Q3 TOP-10 caller:30-80ms(idx_ai_calls_called_at + Top-N Sort) - 寫入:3-8ms p50,p99 < 50ms 達標 ### 結論 **APPROVED WITH NOTES** — Schema 已備妥,無阻擋 A4 logger 啟動的問題。 ### 回滾路徑 三份 migration 檔頭皆附完整回滾 SQL;測試環境可一鍵 DROP。