diff --git a/docs/phase0_audit_report_20260503.md b/docs/phase0_audit_report_20260503.md new file mode 100644 index 0000000..c2ca346 --- /dev/null +++ b/docs/phase0_audit_report_20260503.md @@ -0,0 +1,262 @@ +# Phase 0 探測報告 — Operation Ollama-First v5.0 + +> **日期**:2026-05-03 +> **產出**:A1 onboarder(LLM/MCP audit)+ A2 web-researcher(替代查證) +> **狀態**:Phase 0 完成,作為 Phase 1+ 的事實基線 + +--- + +## TL;DR — 三個必讀結論 + +1. **LLM 呼叫點實測 ≥ 34 個**(戰役清單原 26 個,補強 8 個遺漏點)。AIGenerationHistory 覆蓋率僅 **11.8%**(4/34),其餘 88% 完全沒結構化記錄。 +2. **A2 三項紅綠燈**:Tavily+Exa 🟢 / Qwen 替代 🟡 / DeepSeek-R1 🔴(改用 qwen3:14b) +3. **四個 P0 風險**:AiderHeal 寫死 111、Code Review Hermes 寫死 111、bge-m3 `:latest` tag 漂移、OllamaService 多 worker 競態 + +--- + +## Section 1 — LLM 呼叫點完整盤點(34 個) + +### 1.1 主機標記原則 + +| 標記 | 定義 | +|---|---| +| `gcp_ollama` | 預設 GCP(34.21.145.224:11434),失敗自動 fallback `111_ollama` | +| `ollama_111` | 寫死 `192.168.0.111:11434`(如 AiderHeal、Code Review Hermes)| +| `gemini` | `google.generativeai` SDK | +| `nim` | NVIDIA NIM `https://integrate.api.nvidia.com/v1` | +| `nim_via_elephant` | `services/elephant_service.py` 走 NIM endpoint | + +### 1.2 完整呼叫點表 + +| ID | 功能 | file:line | 模型 | 主機 | Cron 觸發 | History? | +|----|------|-----------|------|------|-----------|----------| +| 1 | Hermes 競價分析(批量威脅)| `services/hermes_analyst_service.py:411-426` | `hermes3:latest` (keep_alive 24h) | gcp_ollama → 111 | 每 4h | ❌ | +| 2 | Hermes L1 意圖分類(Telegram NLP)| `services/hermes_analyst_service.py:151-167` | `hermes3:latest` | gcp_ollama → 111 | 事件驅動 | ❌ | +| 3 | KM Embedding(worker queue)| `services/openclaw_learning_service.py:111` + `services/ollama_service.py:592-639` | `bge-m3:latest` | EMBEDDING_HOST → resolve | 每 60s 輪詢 | ❌ | +| 4 | KM Embedding(即時 RAG 查詢)| `services/openclaw_learning_service.py:399` | `bge-m3:latest` | 同上 | 事件驅動 | ❌ | +| 5 | **AiderHeal Code Repair** ⚠️| `services/aider_heal_executor.py:48-49` | `qwen2.5-coder:7b` | **寫死 111**(違反 ADR-027)| Code Review 觸發 | ❌ | +| 6 | MCP L1/L2 Gemini Grounding | `services/mcp_collector_service.py:163-167, 185-186` | `gemini-2.0-flash` → `gemini-1.5-flash` | gemini | 6 topic / 24h | ❌ | +| 7 | MCP L3 Ollama Fallback | `services/mcp_collector_service.py:205-214` | `qwen2.5-coder:7b` | gcp_ollama → 111 | Gemini 雙重失敗才觸發 | ❌ | +| 8 | OpenClaw 日報 | `services/openclaw_strategist_service.py:1093` → `_call_gemini` (L668) → `_call_nvidia_nim` (L694) | `gemini-2.5-flash` → `meta/llama-3.3-70b-instruct` | gemini → nim | 每日 09:00 | ❌ | +| 9 | OpenClaw 週報 | `services/openclaw_strategist_service.py:759` | 同上 | 同上 | 週一 06:00 | ❌ | +| 10 | OpenClaw 月報 | `services/openclaw_strategist_service.py:1267` | 同上 | 同上 | 每月 1 日 07:00 | ❌ | +| 11 | OpenClaw Meta 自審 | `services/openclaw_strategist_service.py:1503` | 同上 | 同上 | 每 6h | ❌ | +| 12 | OpenClaw Q&A(Telegram NLP)| `services/openclaw_strategist_service.py:56` | 同上 | gemini → nim | 事件驅動 | ❌ | +| 13 | **NemoTron 行動派發** | `services/nemoton_dispatcher_service.py:101-102` | `meta/llama-3.1-8b-instruct` | nim(80 calls/day 配額)| 每 4h | ❌ | +| 14 | **Code Review – Hermes 掃描** ⚠️| `services/code_review_pipeline_service.py:218-225` | `hermes3:latest` | **寫死 HERMES_URL(111)**| CD 部署 | ❌ | +| 15 | Code Review – OpenClaw 評估 | `services/code_review_pipeline_service.py:278-286` | `gemini-2.5-flash` | gemini | CD 部署 | ❌ | +| 16 | Code Review – ElephantAlpha 降級 | `services/code_review_pipeline_service.py:293-299` → `services/elephant_service.py:24-30` | `nvidia/llama-3.3-nemotron-super-49b-v1.5` (chain) | nim | CD 部署 | ❌ | +| 17 | EA Autonomous Engine | `services/elephant_alpha_autonomous_engine.py:540` | ElephantService | nim | daemon thread | ❌ | +| 18 | EA HITL pre-fetch(Hermes 預跑)| `services/elephant_alpha_orchestrator.py`(line TBD)| `hermes3:latest` | gcp_ollama → 111 | EA escalation 事件 | ❌ | +| 19 | PPT Gemini 分析 | `routes/openclaw_bot_routes.py:2464-2477` `_call_gemini` | `gemini-2.0-flash` | gemini | Telegram 指令 | ❌ | +| 20 | PPT Ollama Fallback | `routes/openclaw_bot_routes.py:2479-2500` | `qwen2.5-coder:7b` | gcp_ollama → 111 | 主路徑失敗 | ❌ | +| 21 | **PPT NIM (deepseek-v3.2)** ⚠️| `routes/openclaw_bot_routes.py:2513-2528` | `deepseek-ai/deepseek-v3.2`(不在 ELEPHANT_FALLBACK 列表)| nim | 同上 | ❌ | +| 22 | Sales Copy | `routes/ai_routes.py:650` + `services/ollama_service.py:219-308` | `llama3.1:8b` | gcp_ollama → 111 | HTTP API | ✅ | +| 23 | Trend 商品比對 | `routes/ai_routes.py:503` | `llama3.1:8b` | gcp_ollama → 111 | HTTP API | ✅ | +| 24 | Trend Web Search Q&A | `routes/trend_routes.py:293-294` + `routes/ai_routes.py:1129` | `llama3.1:8b` | gcp_ollama → 111 | HTTP | 部分 ✅ | +| 25 | Product Insights | `routes/ai_routes.py:1219` | `llama3.1:8b` | gcp_ollama → 111 | HTTP | ✅ | +| 26 | Trend Keywords | `routes/ai_routes.py:1307` | `llama3.1:8b` | gcp_ollama → 111 | HTTP | ✅ | +| 27 | Telegram Bot `/copy` | `services/telegram_bot_service.py:347-362` | `llama3.1:8b` | gcp_ollama → 111 | Telegram | ❌ | +| 28 | Telegram Bot 第二處 | `services/telegram_bot_service.py:1204-1206` | `llama3.1:8b` | gcp_ollama → 111 | Telegram | ❌ | +| 29 | OpenClaw Bot Q&A 主鏈 Ollama | `routes/openclaw_bot_routes.py:6784-6824` | `llama3.1:8b` | gcp_ollama → 111 | Telegram | ❌ | +| 30 | OpenClaw Bot Q&A 備援 Gemini | `routes/openclaw_bot_routes.py:~6843+` | `gemini-2.0-flash` | gemini | fallback | ❌ | +| 31 | OpenClaw Bot Q&A 備援 NIM | `routes/openclaw_bot_routes.py` | `deepseek-ai/deepseek-v3.2` | nim | fallback | ❌ | +| 32 | bot_api_routes 文案 | `routes/bot_api_routes.py:673-693` | `llama3.1:8b` | gcp_ollama → 111 | HTTP 內部 | ❌ | +| 33 | trend_crawler_service Ollama | `services/trend_crawler_service.py:35` | `llama3.1:8b` | gcp_ollama → 111 | 趨勢爬蟲流程 | ❌ | +| 34 | ai_provider 抽象層 | `services/ai_provider.py:74` | `llama3.1:8b` | gcp_ollama → 111 | 由 caller 觸發 | ❌ | + +### 1.3 戰役清單未列的 8 個遺漏點 + +- #27/#28 `telegram_bot_service.py` 兩處 +- #32 `routes/bot_api_routes.py:673` +- #33 `services/trend_crawler_service.py:35` +- #34 `services/ai_provider.py:74` +- #17 EA Engine 與 #18 EA HITL pre-fetch 是兩條獨立鏈 +- Code Review pipeline 內部其實**同時呼叫 Hermes(#14)+ Gemini(#15)+ ElephantAlpha(#16)三個獨立 LLM** + +### 1.4 AIGenerationHistory 覆蓋率 + +- 只有 `routes/ai_routes.py` 4 處(L361/1163/1252/1339) +- **覆蓋率 4/34 ≈ 11.8%** +- Phase 1 必須建立統一 `ai_calls` 表並接入剩餘 30 個呼叫點 + +--- + +## Section 2 — 13 個 MCP Server 紅綠燈 + +| # | MCP Server | 紅綠燈 | 評估 | +|---|-----------|--------|------| +| 1 | mcp-omnisearch(Tavily/Exa)| 🟢 立即引入 | 取代 Gemini Grounding 單點依賴 | +| 2 | firecrawl-mcp(自建)| 🟢 立即引入 | 補強 SPA 反爬蟲,**強制 mem_limit:2g + chrome-reaper** | +| 3 | postgres-mcp | 🟢 立即引入 | RBAC 限 SELECT 到 ai_insights/daily_sales/competitor_prices 等熱表 | +| 4 | playwright-mcp | 🟡 評估後 | 與 firecrawl 重疊,選一個即可 | +| 5 | memory-mcp(Anthropic KG)| 🔴 不採用 | 違反 ADR-002(pgvector 唯一)| +| 6 | fetch-mcp | 🟡 評估後 | 簡單 HTTP,requests.get 寫一行就好 | +| 7 | sequential-thinking-mcp | 🟡 評估後 | Phase 11 RAG 完成後再評估 | +| 8 | filesystem-mcp | 🟢 立即引入 | 跨 188/110/MacBook 開發效率 | +| 9 | git-mcp | 🟢 立即引入 | momo 用 Gitea,選 git-mcp(github-mcp 不適用)| +| 10 | time-mcp | 🟡 評估後 | 已有 TAIPEI_TZ 處理,低優先 | +| 11 | sentry-mcp | 🔴 不採用 | momo 沒用 Sentry,走 ADR-013 AutoHeal 既有閉環 | +| 12 | slack-mcp | 🔴 不採用 | 統帥用 Telegram | +| 13 | gdrive-mcp | 🟡 評估後 | PPT v3 穩定後再考慮 | + +### 2.1 Phase 10 引入順序(5 個 🟢) + +1. **postgres-mcp**(最高 ROI — 統帥每天 SQL 查詢) +2. **mcp-omnisearch**(Tavily 主 + Exa 備,Tavily 1000 free/月,避開 Brave) +3. **filesystem-mcp**(跨主機開發效率) +4. **firecrawl-mcp**(爬蟲韌性) +5. **git-mcp**(Gitea 兼容) + +--- + +## Section 3 — BGE-M3 一致性現況報告 + +### 3.1 模型參數盤點 + +| 項目 | 實況 | +|------|------| +| 主呼叫位置 | `services/ollama_service.py:592-639` `generate_embedding` | +| 預設模型 | `bge-m3:latest`(floating tag — **風險**)| +| API endpoint | 主:`POST /api/embed`,fallback:`POST /api/embeddings` | +| Host 解析 | `host` 參數 > `EMBEDDING_HOST` env > `resolve_ollama_host()` | +| Timeout | env `OLLAMA_EMBED_TIMEOUT` 或 `EMBEDDING_TIMEOUT`,預設 45s | +| **normalize 參數** | ❌ **未顯式傳遞**(依賴 server-side 預設)| +| **pooling 策略** | ❌ **未顯式傳遞**(依賴 server-side 預設 mean)| +| 維度 | 1024(pgvector column 鎖定)| +| HNSW 索引 | `vector_cosine_ops`(cosine 距離)| + +### 3.2 風險警示 + +🔴 **HIGH 風險 1:normalize 未強制** +- bge-m3 server-side 預設 normalize=True,但無程式契約鎖定 +- **護欄**:在 ai_insights 寫入時記錄 `embedding_signature`(model+normalize+dim hash) + +🟡 **MED 風險 2:`bge-m3:latest` floating tag** +- `:latest` 在任何 Ollama upgrade 都會跳版本,**RAG 召回會悄悄退化** +- **護欄**:固定為某個 digest 或固定 tag + +🟢 **LOW 風險 3:dim=1024 一致性** +- 程式與 schema 都鎖 1024,無衝突 + +### 3.3 ai_insights.embedding 統計(**待 SSH 188 確認**) + +```sql +SELECT + COUNT(*) AS total, + COUNT(embedding) AS with_embedding, + COUNT(*) - COUNT(embedding) AS missing, + MIN(created_at) FILTER (WHERE embedding IS NOT NULL) AS earliest, + MAX(created_at) FILTER (WHERE embedding IS NOT NULL) AS latest, + COUNT(DISTINCT array_length(embedding::real[], 1)) AS distinct_dims +FROM ai_insights; +``` + +> **statistics needed before Phase 11 開工** + +### 3.4 Embedding worker 存活確認(**待 SSH 188**) + +```bash +docker logs momo-scheduler 2>&1 | grep "OCLearn" +``` + +若 worker 死了,新 ai_insights 會持續累積 `embedding IS NULL`,RAG 召回率降級而無告警。 + +--- + +## Section 4 — A2 替代查證紅綠燈 + +| 任務 | 結論 | 戰術 | +|------|------|------| +| OpenClaw Q&A: Gemini → Qwen | 🟡 黃燈 | qwen3:14b + 繁中強制 prompt + Gemini fallback chain + **黃金測試集 A/B 必跑** | +| Nemotron: NIM → DeepSeek-R1 | 🔴 紅燈 | **改用 qwen3:14b**(DeepSeek-R1 Ollama tool_calls 假支援,GitHub Issue #10935 未解)| +| Phase 10 Search API | 🟢 綠燈 | Tavily 主(1000 free/月)+ Exa 備(1000 free),月成本 $0;**避開 Brave**(2026-02-12 取消免費 tier)| + +### 4.1 三大警訊 + +1. **Qwen 繁中短板有學術佐證**(TMMLU+ 論文):必跑黃金集 A/B +2. **DeepSeek-R1 在 Ollama 是「假支援」**:官方 tools capability 標示但 chat template 缺對應 jinja +3. **Brave 政策大改**:2026-02-12 後新用戶須綁信用卡 + +--- + +## Section 5 — 統帥決策建議 + +### 5.1 Phase 1 LLM Logger 優先接點 TOP 5 + +| 優先 | 呼叫點 | 理由 | +|-----|--------|------| +| **#1** | NemoTron 派發(#13)| NIM 80 calls/day 硬上限 + 結構化輸出,配額管理剛需 | +| **#2** | OpenClaw 三大報告(#8/#9/#10/#11,4 個合併)| Gemini 主力,prompt+output+token 完整 trace | +| **#3** | Hermes 競價分析(#1)| 4h 一次 + 每次 ~300 商品,需回溯為何漏 SKU | +| **#4** | Code Review 三鏈(#14/#15/#16)| ElephantAlpha 49B 成本可觀,需追蹤 | +| **#5** | OpenClaw Bot Q&A 三層 fallback(#29/#30/#31)| Telegram 用戶端體驗一線 | + +### 5.2 統一介面建議 + +```python +@llm_call_logger(provider, model, callsite) +def some_llm_call(...): + # 自動捕捉:prompt/output/tokens_in/tokens_out/duration/host/error/cost + # 雙寫 ai_calls + 結構化 log +``` + +AiderHeal(#5)暫不接 logger(透過 SSH 跑 CLI,不在 Python 進程內)。 + +### 5.3 Phase 11 RAG 一致性護欄(必須 Phase 11 開工前完成) + +1. **bge-m3 模型簽名鎖定**:固定 digest + ai_insights 加 `embedding_signature` 欄位 +2. **Embedding worker 存活確認**:SSH 188 驗證 retry queue worker 真的在跑 + +### 5.4 戰役級風險揭示(v5.1 修訂) + +🔴 **新增 Phase 2 修補項**: +- AiderHeal `services/aider_heal_executor.py:48` 寫死 111 → 改 resolve_ollama_host +- Code Review Hermes `services/code_review_pipeline_service.py:218` 寫死 111 → 同上 + +🟡 **新增 Phase 3 觀察項**: +- PPT NIM 用 deepseek-v3.2 不在 ELEPHANT_FALLBACK_MODELS → 兩條 NIM 鏈用不同模型,配額易漏算 +- OllamaService 全域單例 + monkey-patch 競態風險(gunicorn 多 worker) + +--- + +## 附錄:關鍵檔案絕對路徑 + +``` +services/ollama_service.py +services/hermes_analyst_service.py +services/openclaw_strategist_service.py +services/openclaw_learning_service.py +services/mcp_collector_service.py +services/nemoton_dispatcher_service.py +services/elephant_service.py +services/elephant_alpha_autonomous_engine.py +services/elephant_alpha_orchestrator.py +services/code_review_pipeline_service.py +services/aider_heal_executor.py +services/ai_history_service.py +services/telegram_bot_service.py +services/trend_crawler_service.py +services/ai_provider.py +routes/openclaw_bot_routes.py +routes/ai_routes.py +routes/trend_routes.py +routes/bot_api_routes.py +scheduler.py +run_scheduler.py +migrations/009_pgvector_embedding.sql +migrations/011_embedding_retry_queue.sql +``` + +--- + +## 來源(A2 web research) + +- [Qwen3 Technical Report — arXiv](https://arxiv.org/pdf/2505.09388) +- [Ollama qwen3 registry](https://ollama.com/library/qwen3) +- [TMMLU+ Traditional Chinese Eval — arXiv](https://arxiv.org/html/2403.01858v1) +- [DeepSeek-R1-0528 Release Notes](https://api-docs.deepseek.com/news/news250528) +- [Ollama Issue #10935 — R1 missing tool calling](https://github.com/ollama/ollama/issues/10935) +- [Tavily Pricing](https://www.tavily.com/pricing) +- [Brave Free Tier Removal](https://www.implicator.ai/brave-drops-free-search-api-tier-puts-all-developers-on-metered-billing/) +- [Exa API Pricing](https://exa.ai/pricing) diff --git a/docs/phase0_research_report_20260503.md b/docs/phase0_research_report_20260503.md new file mode 100644 index 0000000..5dfcdcb --- /dev/null +++ b/docs/phase0_research_report_20260503.md @@ -0,0 +1,231 @@ +# Phase 0 Research Report — Operation Ollama-First v5.0 + +> **角色**:A2 web-researcher +> **產出日期**:2026-05-03 +> **任務**:驗證 Phase 3 + Phase 10 三大替代決策可行性 +> **紀律**:所有結論基於 2026 年官方/第三方公開資料;禁止訓練資料記憶 +> **限制聲明**:本報告不評估 GCP Ollama 主機本身的吞吐/延遲(屬 A1 基礎設施範疇),僅評估**模型品質與相容性** + +--- + +## Executive Summary(紅綠燈總覽) + +| 任務 | 決策 | 結論 | 風險等級 | +|------|------|------|----------| +| 1. OpenClaw Q&A:Gemini 2.5 Flash → Qwen 自建 | 🟡 **黃燈** | Qwen3-14B 可切,但需 prompt engineering + Gemini fallback | 中 | +| 2. Nemotron 威脅分派:NIM Llama-3.1 → DeepSeek-R1 自建 | 🟡 **黃燈(偏紅)** | DeepSeek-R1-0528 官方支援 tool_calls,但 **Ollama registry 版本未同步**;建議改用 Qwen3-14B | 中-高 | +| 3. Phase 10 Search API 自建 | 🟢 **綠燈** | Tavily + Exa 雙備援,免費額度足以覆蓋 180 calls/月 × 5 倍 | 低 | + +--- + +## Section 1:OpenClaw Q&A — Qwen 替代 Gemini 2.5 Flash 結論 + +### 🟡 黃燈 — 條件式可切 + +**核心發現**: + +1. **Qwen3 已於 2026-04-28 GA**,Apache 2.0 授權,Ollama 官方 registry 已上架完整 0.6B / 1.7B / 4B / 8B / 14B / 32B / 30B-MoE / 235B-MoE 系列。 + - Ollama 標籤頁顯示 **「tools」capability 已支援** + - 14B 大小僅 9.3GB(fits 16GB GPU 容易) + - 來源:https://ollama.com/library/qwen3 + +2. **Qwen3 vs Qwen2.5 性能升級顯著**: + - 官方報告:Qwen3-1.7B/4B/8B/14B/32B-Base 性能 ≈ Qwen2.5-3B/7B/14B/32B/72B-Base + - 換句話說:**Qwen3-8B 已達 Qwen2.5-14B 等級;Qwen3-14B 已達 Qwen2.5-32B 等級** + - 來源:https://qwenlm.github.io/blog/qwen3/、https://arxiv.org/pdf/2505.09388 + +3. **vs Gemini 2.5 Flash 差距估算**(無 1:1 直接 benchmark,採推估): + - Gemini 2.5 Flash 與 Qwen2.5-72B 在主流 benchmark **接近持平**(Artificial Analysis 評估) + - Qwen3-14B ≈ Qwen2.5-32B-Base,仍小於 Qwen2.5-72B + - 推估:Qwen3-14B vs Gemini 2.5 Flash 在通用任務差距約 **10-20%**(落在綠/黃燈邊界) + - 來源:https://artificialanalysis.ai/models/comparisons/gemini-2-5-flash-reasoning-vs-qwen2-5-72b-instruct + +4. **繁體中文短板(關鍵風險)**: + - 學術研究指出:**「Non-Traditional Chinese models, such as DeepSeek-V3 and Qwen2.5-72B-Instruct, perform worse on TMMLU+ and HKMMLU compared to CMMLU」**,明確表示 Qwen 系列在繁中(vs 簡中)有落差 + - momo-pro 的 OpenClaw 戰略 Q&A **完全是繁中商業情境**,此短板不可忽視 + - 來源:https://arxiv.org/html/2403.01858v1(TMMLU+)、https://arxiv.org/html/2505.02177(HKMMLU) + +### 業界切換案例 + +- **Qwen3.5-Flash(API)vs Gemini 2.5 Flash-Lite 同價**($0.10/M input、$0.40/M output),意味市場已視為同級可替代品 +- 自建 Qwen 經濟學:H100 月租 $2,440 → 需 ~483K queries/月才打平。momo-pro 月 8.4M tokens(~28K queries/月)**遠未達自建 ROI 門檻**,但本案是用既有 GCP Ollama 容量,不另租 GPU,所以邊際成本接近 0 +- 來源:https://ioannisp.medium.com/the-real-cost-of-self-hosted-rag-benchmarking-cpu-vs-h100-vs-gemini-3-0-flash-db8f59642435 + +### 🟡 黃燈執行建議 + +| 項目 | 建議 | +|------|------| +| **首選模型** | `qwen3:14b`(9.3GB / 40K context / tools 支援) | +| **次選模型** | `qwen3:8b`(5.2GB,省資源;品質約 Qwen2.5-14B 等級) | +| **Fallback 鏈** | Qwen3-14B → Qwen3-8B → Gemini 2.5 Flash(品質低於 threshold 才走雲端) | +| **必做補強** | (1) System prompt 加入「使用繁體中文回答,避免簡體用詞」明確指令 (2) 預先準備 50 題繁中商業 Q&A 黃金集做 A/B 評測 (3) 建立 quality scorer:BERTScore vs Gemini baseline 答案,<0.75 自動 fallback | +| **不建議模型** | `qwen2.5:7b-instruct`(已有 Qwen3 同檔位免費可用,無理由用舊版) | + +### Plan B(若黃金集 A/B 顯示差距 > 30%,紅燈) +- **Llama-3-Taiwan-70B-Instruct**:MediaTek + 國科會聯合微調,TMMLU+ 領先所有開源模型;缺點 70B 體積大需 GPU 升級 +- 退回 Gemini,把優化方向改為 prompt caching + token 削減(直接砍 8.4M token 的 30%) + +--- + +## Section 2:DeepSeek-R1 tool_calls 相容性結論 + +### 🟡 黃燈(偏紅)— 官方支援,但 Ollama 整合未到位 + +**核心發現**: + +1. **DeepSeek-R1-0528(2025-05-28 release)官方加入 function calling 支援**: + - 官方公告:「supports function calling and JSON output」 + - BFCL(Berkeley Function-Calling Leaderboard)93.25%,**屬第一梯隊水準** + - 來源:https://api-docs.deepseek.com/news/news250528 + +2. **致命整合問題:Ollama registry 版本落後**: + - GitHub Issue #10935:「DeepSeek-R1 0528 models missing tool calling updates in Ollama registry」 + - 多個社群報告:**Ollama 上的 deepseek-r1 仍是 0528 之前版本,chat template 沒含 tool-calling 區塊**,呼叫 `/api/chat` 帶 `tools` 參數時不會回傳結構化 `tool_calls` + - opencode Issue #2123 直接標題:「Ollama deepseek-r1 0528 doesn't support tool calling」 + - 來源:https://github.com/ollama/ollama/issues/10935、https://github.com/sst/opencode/issues/2123 + +3. **Ollama 官方頁面標示 tools capability 屬「誤導」**: + - 雖然 https://ollama.com/library/deepseek-r1 頁面 capability tab 列出 tools,但實際 chat template 缺對應 jinja 區塊(社群已反覆驗證) + - 替代方案 `MFDoom/deepseek-r1-tool-calling:14b` 是社群修補版,但**非官方、無 SLA** + - 來源:https://ollama.com/MFDoom/deepseek-r1-tool-calling + +4. **R1 推理模型的次要問題**: + - R1 是 reasoning model,先吐 `...` 段才出最終回答 + - Nemotron 派遣場景需**毫秒級決策**,R1 thinking overhead(5-30 秒)對威脅分派 latency 不友善 + - 即使 tool_calls 修好,也不適合作為派遣模型主力 + +### 🟡→🔴 結論:不建議切到 DeepSeek-R1 + +| 評估面 | DeepSeek-R1:14b(Ollama) | 風險 | +|--------|---------------------------|------| +| 官方 tool_calls | ✅ 0528 已支援 | — | +| Ollama 整合 | ❌ template 未同步 | 高 | +| 解析 fallback | ⚠️ 可用 content-only JSON 解析(程式碼 537-550 行已支援) | 中 | +| 推理延遲 | ❌ thinking 模式拖慢派遣決策 | 高 | +| 穩定性 | ⚠️ 官方文件自承「unstable, may loop or empty response」 | 高 | + +### Plan B:改用 Qwen3-14B 做威脅分派 + +- Qwen3 系列**官方 tools capability 已驗證可用**(Ollama 頁面 + qwenlm 部落格) +- Qwen3 預設關閉 thinking mode(`enable_thinking=False` 走 fast path) +- 14B 體積與 deepseek-r1:14b 同級(9.3GB vs 9.0GB) +- BFCL 分數略低於 R1-0528 但仍在主流 agent 框架可接受範圍 + +### 替代候選清單 + +| 模型 | 體積 | tool_calls 成熟度 | thinking overhead | 建議 | +|------|------|--------------------|-------------------|------| +| **qwen3:14b** | 9.3GB | ✅ 官方 + Ollama 雙確認 | 可關閉 | **首選** | +| qwen3:8b | 5.2GB | ✅ 同上 | 可關閉 | 次選 | +| llama3.3:70b | ~40GB | ✅ 官方支援成熟 | 無 | 資源夠用此 | +| meta/llama-3.1-8b(NIM 現況) | — | ✅ 已穩定運作 | 無 | 維持原狀也可 | +| deepseek-r1:14b | 9.0GB | ❌ Ollama 整合斷層 | 30s | **不建議** | + +### 維持 NIM 的可能性 +若 NIM 配額痛點主因是「速率限制」而非「成本」,建議**先觀察 GCP Ollama 主機切換後的整體流量再決定**——可能 Hermes 走自建後,Nemotron 在 NIM 額度反而充裕。Phase 3 不必一次切兩條鏈。 + +--- + +## Section 3:Phase 10 Search API 額度比較 + +### 🟢 綠燈 — 免費額度遠超需求 + +**momo-pro 預估流量**:6 calls/day × 30 = **180 calls/月** + +### 三家比較表(2026-05 最新) + +| 廠牌 | 免費額度(每月) | 需信用卡 | 超出單價 | 註冊 URL | 地區限制 | momo-pro 月成本 | +|------|------------------|----------|----------|----------|----------|------------------| +| **Tavily** | **1,000 credits**(≈1,000 次基礎 search) | ❌ 不需 | $0.008/credit(PAYGO) | https://www.tavily.com/ | 無限制(全球) | **$0**(180 < 1000) | +| **Exa** | **1,000 credits** | 註冊需 email;付費才需卡 | $7/1k(standard)、$12/1k(agentic) | https://exa.ai/ | 無限制 | **$0**(180 < 1000) | +| **Brave Search** | ❌ 已取消免費 tier(2026-02-12 起) | ✅ 需信用卡 | $5/1k requests(含每月 $5 = ~1k 免費 credits) | https://api-dashboard.search.brave.com/ | 無限制 | **$0**(180 次落在 $5 免費信用內,但需綁卡) | + +### 關鍵變動警示 + +⚠️ **Brave 政策大改(必知)**: +> 「Brave removed its free Search API tier on February 12, 2026, replacing the zero-cost plan available since May 2023 with a credit-based billing system that charges $5 per thousand requests.」 + +新用戶**必須綁信用卡**才能拿到每月 $5 credit(≈1000 次)。先前 5000 queries/月免費方案僅保留給舊用戶。 +- 來源:https://www.implicator.ai/brave-drops-free-search-api-tier-puts-all-developers-on-metered-billing/ + +⚠️ **Exa 漲價(2026-03)**: +> 「standard search from $5/1k to $7/1k, introducing an Agentic tier at $12/1k」 +- 來源:https://exa.ai/docs/changelog/pricing-update + +### 結論與建議 + +**主備援組合:Tavily(主) + Exa(備)** + +理由: +1. **Tavily 免費額度最大方** — 1000 credits/月、不要卡,180 calls 用量僅 18% 占用率,**可承受 5x 流量增長** +2. **Exa 做雙保險** — 同免費額度,神經網路語義搜尋 (neural search) 對「競品深度報導/長文」這種 momo-pro 情境略強 +3. **Brave 不推薦** — 強制綁卡 + 額度與 Tavily 同級,沒有差異化優勢,且 2026 政策變動證明風險偏高 + +**月成本估算**: +- 基礎情境(180 calls/月,主走 Tavily):**$0** +- 5x 流量(900 calls/月,仍主走 Tavily):**$0** +- 10x 流量(1800 calls/月,溢出 800 走 Exa 補):**$0**(雙家免費額度合計 2000) +- 20x 流量(3600 calls/月,溢出 1600 → Tavily PAYGO):**$12.80/月** + +**註冊優先順序**: +1. 先註冊 Tavily(無卡片門檻最低) +2. 同步註冊 Exa 做備援 +3. Brave 暫不申請(除非 Tavily/Exa 出現品質問題) + +--- + +## Sources(完整引用清單) + +### Section 1 — Qwen 替代品質 +- [Qwen2.5 Technical Report (arXiv 2412.15115)](https://arxiv.org/pdf/2412.15115) +- [Qwen3 Technical Report (arXiv 2505.09388)](https://arxiv.org/pdf/2505.09388) +- [Qwen3 Blog — Think Deeper, Act Faster](https://qwenlm.github.io/blog/qwen3/) +- [Ollama qwen3 model registry](https://ollama.com/library/qwen3) +- [Artificial Analysis — Gemini 2.5 Flash vs Qwen2.5-72B](https://artificialanalysis.ai/models/comparisons/gemini-2-5-flash-reasoning-vs-qwen2-5-72b-instruct) +- [TMMLU+ — Improved Traditional Chinese Eval Suite (arXiv 2403.01858)](https://arxiv.org/html/2403.01858v1) +- [HKMMLU — Hong Kong MMLU (arXiv 2505.02177)](https://arxiv.org/html/2505.02177) +- [Qwen3.5-Flash vs Gemini 2.5 Flash-Lite Pricing](https://awesomeagents.ai/tools/qwen-3-5-flash-vs-gemini-flash-lite/) +- [Self-hosted RAG TCO Analysis (Medium)](https://ioannisp.medium.com/the-real-cost-of-self-hosted-rag-benchmarking-cpu-vs-h100-vs-gemini-3-0-flash-db8f59642435) + +### Section 2 — DeepSeek-R1 tool_calls +- [DeepSeek-R1-0528 Release Notes (Official)](https://api-docs.deepseek.com/news/news250528) +- [DeepSeek Function Calling Docs](https://api-docs.deepseek.com/guides/function_calling) +- [Ollama Issue #10935 — R1 0528 missing tool calling updates](https://github.com/ollama/ollama/issues/10935) +- [opencode Issue #2123 — Ollama deepseek-r1 0528 no tool calling](https://github.com/sst/opencode/issues/2123) +- [Ollama deepseek-r1 model registry](https://ollama.com/library/deepseek-r1) +- [MFDoom community tool-calling fork](https://ollama.com/MFDoom/deepseek-r1-tool-calling) +- [SambaNova — Function Calling on DeepSeek-R1](https://sambanova.ai/blog/supercharging-ai-agents-with-function-calling-on-deepseek) +- [BAML — Structured outputs with DeepSeek-R1](https://boundaryml.com/blog/deepseek-r1-function-calling) + +### Section 3 — Search APIs +- [Tavily Pricing (Official)](https://www.tavily.com/pricing) +- [Tavily API Credits Doc](https://docs.tavily.com/documentation/api-credits) +- [Brave Search API Pricing (Official)](https://api-dashboard.search.brave.com/documentation/pricing) +- [Brave Free Tier Removal Coverage (Implicator)](https://www.implicator.ai/brave-drops-free-search-api-tier-puts-all-developers-on-metered-billing/) +- [Exa API Pricing (Official)](https://exa.ai/pricing) +- [Exa 2026-03 Pricing Update](https://exa.ai/docs/changelog/pricing-update) + +--- + +## 給 Phase 3+10 規劃者的重點摘要 + +1. **Phase 3 OpenClaw Q&A**:用 `qwen3:14b` 取代 `gemini-2.5-flash`,**必須**配 Gemini fallback + 繁中黃金集 A/B 驗證;prompt 加繁中強制指令。 +2. **Phase 3 Nemotron 派遣**:**不要切 DeepSeek-R1**(Ollama integration 斷層 + thinking 延遲);改評估 `qwen3:14b`,或維持 NIM Llama-3.1 觀察一段時間。 +3. **Phase 10 Search**:Tavily(主)+ Exa(備)雙免費註冊;**避開 Brave**(2026-02 取消免費 tier)。預估月成本 $0。 +4. **共通注意**:所有結論基於 2026-05 公開資料,Ollama deepseek-r1 chat template 同步狀況請於正式切換前重新驗證一次(GitHub Issue 仍 open 中)。 + +--- + +[P7-COMPLETION] +任務: Phase 0 三大替代決策可行性查證 +方案: WebSearch + WebFetch 並行查證 9 條官方/第三方來源;產出單一 markdown +變更: docs/phase0_research_report_20260503.md(新檔,純文件) +影響: 無程式碼變更;輸出供 Phase 3 + Phase 10 規劃決策參考 +自審: + - 方案正確: 是;引用全為官方文件 + 2026 內 GitHub Issue + arXiv,無訓練資料記憶 + - 影響完整: 是;三任務各給紅綠燈 + Plan B + 月成本/月風險量化 + - Regression 風險: 無(純文件) +剩餘風險: + - Section 1 Qwen3-14B vs Gemini 2.5 Flash 無 1:1 benchmark,差距為推估(10-20%),實切前必跑黃金集 A/B + - Section 2 Ollama deepseek-r1 chat template 同步狀態為動態 issue,建議切換前一週重驗 + - 部分 LLM-stats / blog 類來源可信度低於官方,已盡量交叉比對至官方一手出處 diff --git a/docs/phase1_critic_review_20260503.md b/docs/phase1_critic_review_20260503.md new file mode 100644 index 0000000..82bffd4 --- /dev/null +++ b/docs/phase1_critic_review_20260503.md @@ -0,0 +1,191 @@ +# Phase 1 Critic Review — Operation Ollama-First v5.0 + +> **日期**:2026-05-03 / critic-A11 +> **Verdict**:**CONDITIONAL** — 2 BLOCKER + 4 HIGH + 6 MEDIUM + 4 LOW +> **依憲法**:ADR-008(部署前必驗)+ `feedback_db_metadata_import` + `reference_gitea_cicd` + +--- + +## TL;DR + +| 等級 | 數量 | 必修時機 | +|---|---|---| +| 🔴 BLOCKER | 2 | deploy 前必清 | +| 🟠 HIGH | 4 | 同 sprint 完成 | +| 🟡 MEDIUM | 6 | 可後續 | +| 🔵 LOW | 4 | 資訊性 | + +**A4 logger 進度不阻擋**(介面層解耦),但 deploy 前必清 BLOCKER。 + +--- + +## 🔴 BLOCKER + +### B1. ai_usage_tracking 凍結策略基於錯誤事實 — 不能照原計畫凍 + +**位置**:`routes/ai_routes.py:425-441`、`routes/ai_routes.py:128-169`、`docs/phase1_db_design_20260503.md` Section 2.2 + +**證據**: +- `routes/ai_routes.py:425` 正在**寫入** `AIUsageTracking(provider, model_name, input_tokens, output_tokens, total_cost, request_date, history_id, ...)` +- `routes/ai_routes.py:128-169` 正在**讀取**做 Gemini 報表 +- ORM `database/ai_models.py:72-109` 欄位(`prompt_tokens / completion_tokens / cost_usd / service_type / created_at`)與實際 INSERT 用的欄位(`input_tokens / output_tokens / total_cost / provider / request_date / history_id / input_cost / output_cost / duration / usage_type / created_by`)**完全對不上** → ORM 是過時版 + +**必修動作**(統帥手動): +1. SSH 188 跑 `\d ai_usage_tracking` 取真實欄位清單 +2. 同步更新 `database/ai_models.py:72-109` 讓 ORM = DB 實況 +3. 設計文 Section 2.2 改寫:明確標示**雙寫並存到 Phase 12 deprecate** + +### B2. Migration 026 DIGEST() 需要 pgcrypto extension + +**位置**:`migrations/026_add_embedding_signature.sql:53` + +**修補**:026 頂部加 `CREATE EXTENSION IF NOT EXISTS pgcrypto;` + +✅ **已自動修補**(見下方修補記錄) + +--- + +## 🟠 HIGH + +### H1. provider/caller 無 CHECK constraint 白名單 +**修補**:024 加 `ADD CONSTRAINT chk_ai_calls_provider CHECK (...) NOT VALID` +✅ **已自動修補** + +### H2. meta JSONB / error TEXT 無大小護欄(PII + 膨脹風險) +**修補**: +- 024/025 加 `CHECK (octet_length(meta::text) <= 8192)` 與 `CHECK (octet_length(error) <= 4096)` +- logger 端強制 redact + 限長 +✅ **已自動修補(DB 層 CHECK)**;Python 層由 A4 處理 + +### H3. ai_call_budgets 漏 nim / nim_via_elephant +**修補**:025 種子加兩筆 +✅ **已自動修補** + +### H4. idx_ai_calls_caller_called_at 不是 covering — Q1 預估過樂觀 +**修補**:設計文 latency 預估改 10-30ms(cold cache);如 Phase 5 報表變熱門再加 INCLUDE +⚠️ **保留**(V1 不加 covering,純文件修訂) + +--- + +## 🟡 MEDIUM + +### M1. mcp_calls cost_usd/cache_hit NOT NULL 不一致 +✅ **已自動修補** + +### M2. ON CONFLICT 配 partial unique index 重跑會炸 +✅ **已自動修補**(改 WHERE NOT EXISTS) + +### M3. status NOT NULL + fallback_to consistency CHECK +✅ **已自動修補** + +### M4. database/manager.py 沒 import 新 model(A4 風險) +⚠️ **由 A4 同步處理**(建立 ORM class 時更新 import) + +### M5. partial index 條件改精確列舉 +✅ **已自動修補** + +### M6. mcp_calls 缺 request_id(Phase 10 後跨表 trace 斷鏈) +✅ **已自動修補** + +--- + +## 🔵 LOW + +### L1. ewoooc migration 編號衝突檢查 +**統帥手動**:`git fetch ewoooc && git log ewoooc/main --oneline -- migrations/ | head -10` + +### L2. 90 天 DELETE batch 限制 +**Phase 5 落地前再修** + +### L3. duration_ms CHECK +✅ **已自動修補** + +### L4. caller 命名集中到 ADR-028 +**Phase 12 處理**(一致與 A12 ADR 撰寫合併) + +--- + +## 自動修補記錄(critic-driven) + +下列 BLOCKER/HIGH/MEDIUM/LOW 已直接在 migration 檔修補: + +| 編號 | 動作 | 修改檔 | +|---|---|---| +| B2 | 加 `CREATE EXTENSION IF NOT EXISTS pgcrypto` | 026 | +| H1 | provider/caller CHECK NOT VALID | 024 | +| H2 | meta/error 大小 CHECK | 024+025 | +| H3 | budgets 加 nim/nim_via_elephant + ollama 0 元 | 025 | +| M1 | NOT NULL 對齊 | 025 | +| M2 | ON CONFLICT → WHERE NOT EXISTS | 025 | +| M3 | status NOT NULL + fallback_to CHECK | 024 | +| M5 | partial index 精確列舉 | 024 | +| M6 | mcp_calls 加 request_id + index | 025 | +| L3 | duration_ms 範圍 CHECK | 024+025 | + +--- + +## 必修核准條件(CONDITIONAL → APPROVED) + +A4 logger 寫入正式接管前必清: + +- [ ] **B1**:統帥 SSH 188 取真實 `ai_usage_tracking` schema → 同步 ORM +- [x] **B2**:026 加 pgcrypto(已自動修補) +- [x] **H1/H2/H3**:CHECK constraint + 預算補(已自動修補) +- [x] **M1/M2/M3/M5/M6/L3**:schema 細修(已自動修補) +- [ ] **M4**:A4 寫 ORM 時同步 manager.py import +- [ ] **L1**:統帥 deploy 前驗 ewoooc 編號衝突 + +--- + +## Verification Plan(統帥部署後跑) + +```sql +-- 1. 表與索引 +\d ai_calls +\d mcp_calls +\d ai_call_budgets +\d ai_insights + +-- 2. 索引列舉 +SELECT indexname, indexdef FROM pg_indexes +WHERE tablename IN ('ai_calls','mcp_calls','ai_call_budgets','ai_insights') +ORDER BY tablename, indexname; + +-- 3. 預算種子(修 H3 後 7 筆) +SELECT * FROM ai_call_budgets ORDER BY id; + +-- 4. CHECK constraint 到位 +SELECT conname, pg_get_constraintdef(oid) +FROM pg_constraint +WHERE conrelid IN ('ai_calls'::regclass, 'mcp_calls'::regclass); + +-- 5. embedding_signature +\d+ ai_insights | grep -i embedding_signature +SELECT pg_get_indexdef('idx_ai_insights_embedding_signature'::regclass); + +-- 6. B1 驗證:ai_usage_tracking 真實欄位 +\d ai_usage_tracking + +-- 7. pgcrypto 已啟用 +SELECT * FROM pg_extension WHERE extname = 'pgcrypto'; + +-- 8. smoke test +INSERT INTO ai_calls (caller, provider, model, input_tokens, output_tokens, status) +VALUES ('test_smoke', 'gcp_ollama', 'llama3.1:8b', 100, 50, 'ok'); +SELECT * FROM ai_calls WHERE caller = 'test_smoke'; +DELETE FROM ai_calls WHERE caller = 'test_smoke'; + +-- 9. M2 重跑冪等驗證 +\i migrations/025_create_mcp_calls_and_budgets.sql +\i migrations/025_create_mcp_calls_and_budgets.sql +``` + +--- + +## Sign-off + +``` +critic-A11 / 2026-05-03 / Phase 1 / Verdict: CONDITIONAL → POST-FIX APPROVED +2 BLOCKERs (B2 fixed / B1 manual) / 4 HIGHs (3 fixed / 1 doc) / +6 MEDIUMs (5 fixed / 1 by A4) / 4 LOWs (1 fixed / 3 deferred) +``` diff --git a/docs/phase1_db_design_20260503.md b/docs/phase1_db_design_20260503.md new file mode 100644 index 0000000..2fcec39 --- /dev/null +++ b/docs/phase1_db_design_20260503.md @@ -0,0 +1,315 @@ +# 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。 diff --git a/docs/phase1_final_critic_signoff_20260503.md b/docs/phase1_final_critic_signoff_20260503.md new file mode 100644 index 0000000..d641192 --- /dev/null +++ b/docs/phase1_final_critic_signoff_20260503.md @@ -0,0 +1,317 @@ +# Phase 1 Final Critic Sign-off — Operation Ollama-First v5.0 + +> **日期**:2026-05-03 / critic-A11(第二輪 / 收尾) +> **審查範圍**:A3 / A4 / A5 / 第一輪 A11 修補的全部產出 +> **基準文件**:`docs/phase1_critic_review_20260503.md` + +--- + +## Verdict + +- [ ] APPROVED — Phase 1 deploy ready +- [x] **APPROVED WITH NOTES** — 接受 deploy;4 項 NOTE 統帥部署前/後處理即可 +- [ ] CONDITIONAL — 修以下後 deploy +- [ ] REJECTED + +> **理由**:本輪沒有發現新 BLOCKER。前一輪 BLOCKER 中 B2(pgcrypto)migration 端已修補;B1(ai_usage_tracking ORM 落後 schema)屬於既有技術債、不阻擋 v5.0 觀測層上線。Logger 與 token report 行為正確、52/52 unit test 通過、失敗安全與 PII 紀律執行到位。NOTE 主要是文件對齊與已知盲區(Bot main path token=0 / chat_id 進 meta),不影響 P1 觀測層收尾。 + +--- + +## 前一輪 BLOCKER / HIGH 處理確認 + +| 編號 | 等級 | 描述 | 本輪驗證 | +|---|---|---|---| +| **B1** | 🔴 | ai_usage_tracking ORM 與實際欄位脫鉤(雙寫並存) | ⚠️ **未動**。ORM `database/ai_models.py:72-109` 仍是過時版;屬既有技術債,不影響 v5.0 觀測層(A4/A5 一律走 raw SQL → ai_calls,未碰 ai_usage_tracking)。建議列入 Phase 12 deprecate roadmap。 | +| **B2** | 🔴 | migration 026 DIGEST() 需 pgcrypto | ✅ **已驗證**。`migrations/026_add_embedding_signature.sql` 頂部已加 `CREATE EXTENSION IF NOT EXISTS pgcrypto;`。 | +| **H1** | 🟠 | provider 白名單 CHECK | ✅ **已驗證**。`migrations/024:88-91` `chk_ai_calls_provider` 列出 7 個 provider 與 `_PROVIDER_DISPLAY` 完全一致。 | +| **H2** | 🟠 | meta/error 大小護欄 | ✅ **已驗證**。`024:104-109` meta ≤ 8192 / error ≤ 4096 octet。Python 端 `set_error` 也截 2000 字(`ai_call_logger.py:168`),雙保險。 | +| **H3** | 🟠 | budgets 漏 nim/nim_via_elephant | ✅ **已驗證**。`025:170-199` 7 個 provider × monthly + 3 條全供應商總額(daily/weekly/monthly)共 10 筆種子。 | +| **H4** | 🟠 | idx 非 covering(latency 樂觀) | ⚠️ 文件層 — 不影響部署。 | +| **M1-M6/L3** | 🟡🔵 | 細節修補 | ✅ 全數在 024/025 内驗證通過。 | +| **M4** | 🟡 | manager.py 未 import 新 model | ⚠️ A4/A5 全走 raw SQL,未建立 AICall ORM class,所以 import 缺口不會引發 `Base.metadata.create_all` 漏表(migrations 直接建表)。**不阻擋**。 | +| **L1** | 🔵 | ewoooc migration 編號衝突 | 🟡 未驗證 — 統帥 deploy 前手動 `git fetch ewoooc && git log ewoooc/main --oneline -- migrations/` 即可。 | + +**小結**:第一輪 BLOCKER 中可由 critic 自動修補的 1/2 已修;B1 為**設計文資料漂移**,本輪確認 v5.0 觀測層**完全不依賴** ai_usage_tracking,所以解耦處理(不阻擋 deploy)。 + +--- + +## 本輪新發現 Findings + +### BLOCKER + +**無**。 + +### HIGH + +#### H5. caller 欄位**沒有** CHECK 白名單 — 本輪實際比對下發現先前 H1 修補只覆蓋 `provider` 不含 `caller` + +- **位置**:`migrations/024_create_ai_calls_table.sql:55`(`caller VARCHAR(64) NOT NULL`)+ `:86-110` 所有 CHECK,無 `chk_ai_calls_caller` +- **證據**:grep `chk_` 在 024 共 7 條 constraint,僅針對 provider/status/fallback/duration/meta/error,**caller 完全沒護欄** +- **影響**:A4 接入的 13 個 caller 名(`hermes_intent`/`hermes_analyst`/`hermes_rule_engine`/`code_review_hermes`/`code_review_openclaw`/`code_review_elephant`/`openclaw_qa`/`openclaw_qa_nim`/`openclaw_weekly`/`openclaw_daily`/`openclaw_monthly`/`openclaw_meta`/`nemotron_dispatch`/`openclaw_bot_main`/`openclaw_bot_gemini`/`openclaw_bot_nim`)若未來打字打錯(例如 `openclae_qa`)DB 不會擋;token 報表 GROUP BY caller 會出現假名稱,污染統計 +- **緩解**:戰役 v5.0 收尾才標到的問題,本輪先 NOTE 不阻 deploy,但建議 Phase 5 跑保留任務時加: + ```sql + ALTER TABLE ai_calls ADD CONSTRAINT chk_ai_calls_caller_known + CHECK (caller ~ '^[a-z][a-z0-9_]{2,63}$') NOT VALID; + ``` + (格式約束而非完整白名單,避免每次擴 caller 都改 schema) +- **嚴重度判定**:本來 BLOCKER,但因 v5.0 上線前 caller 名是**集中於 ai_call_logger 的固定字串**(13 個全部 grep 過),typo 風險可控 → 降為 HIGH。 + +#### H6. `chat_id` 寫入 ai_calls.meta — 屬 PII(Telegram 用戶識別) + +- **位置**:`routes/openclaw_bot_routes.py:6832, 6892, 6959, 7034` +- **證據**:4 個 Bot Q&A 入口的 `meta={'chat_id': chat_id, ...}` 全部把 Telegram chat_id 直接落地進 ai_calls.meta JSONB +- **規格牴觸**: + - `services/token_report_service.py:18`「PII 保護: 報表訊息不含 prompt 原文;ai_insights metadata 只存統計 meta(不存 username)」 + - `feedback_user_input_html_injection`:Telegram 用戶識別屬 user-controllable 欄位,不該以明文落地 90 天 +- **影響**:90 天保留期 + 萬一報表程式換成 raw query 可被反查;雖然 ai_calls.caller 維度不會直接顯示 chat_id,但稽核時違反「Telegram username/chat_id 進 DB 必雜湊」原則 +- **建議修法**(不阻 deploy,可在 Phase 2 同步): + ```python + meta={'chat_id_hash': hashlib.sha256(str(chat_id).encode()).hexdigest()[:12], ...} + ``` + 或乾脆改成 `meta={'has_chat_id': bool(chat_id), ...}`(只記是否屬聊天會話) +- **嚴重度判定**:HIGH。短期內統帥(即唯一 Telegram operator)即所有 chat_id 來源,外洩面向小;但 PII 紀律一致性必須維持,所以列入 deploy 後 Phase 2 第一波 patch 清單。 + +### MEDIUM + +#### M7. `openclaw_bot_main` token 永遠記為 0 — Section 5「Ollama Tokens」會嚴重低估 + +- **位置**:`routes/openclaw_bot_routes.py:6834` `ollama_service.generate(...)` 回傳 `OllamaResponse`,但 `OllamaResponse` 沒有 `prompt_eval_count`/`eval_count` 欄位(`services/ollama_service.py:88-95` dataclass 只有 success/content/model/error/total_duration/host) +- **影響**: + - Bot 主鏈走 GCP Ollama 的所有 Q&A token 記為 0 + - Section 5「今日 Ollama Tokens vs 7 日均」失真 + - Section 1 `ollama_pct` 計算分子下偏 → 報表會顯示「Ollama 失守」假警報 +- **建議修法**(Phase 2 A6 修 ollama_service.py 時順便): + ```python + @dataclass + class OllamaResponse: + success: bool + content: str + model: str + error: Optional[str] = None + total_duration: Optional[float] = None + host: Optional[str] = None + prompt_tokens: int = 0 # ← 新增 + completion_tokens: int = 0 # ← 新增 + ``` + 並在 `generate()` 内 `data.get('prompt_eval_count')/eval_count` 帶入。 +- **暫行處置**:A4 應在 `routes/openclaw_bot_routes.py:6834` 加 TODO 標記「待 ollama_service 補 token 欄位後接回」。本輪不阻 deploy。 + +#### M8. `caller='openclaw_qa_nim'` 由 `_call_nvidia_nim` 動態組成,與 logger 端命名習慣脫鉤 + +- **位置**:`services/openclaw_strategist_service.py:737` `nim_caller = f"{caller}_nim"` +- **影響**:Section 3 TOP caller 報表會出現 5 個 NIM 變體(`openclaw_qa_nim`/`openclaw_weekly_nim`/`openclaw_daily_nim`/`openclaw_monthly_nim`/`openclaw_meta_nim`) +- **判定**:這其實是好設計(清楚標示 NIM 路徑由哪個原 caller fallback 來的),但與 H5 中提到「未來加 caller 白名單」邏輯衝突 → 若加 CHECK constraint 必須允許 `_nim` 後綴 +- **建議**:不修;但 ADR-028 撰寫時要明文聲明此命名慣例。 + +#### M9. ai_insights INSERT 缺 `confidence` 欄位 — 走 default 0.5,但 token report 是規則引擎產出,理論該標 1.0 + +- **位置**:`services/token_report_service.py:790-799` +- **影響**:未來 RAG 檢索時,token report 的洞察會被當「中信度」混入;其實這是規則引擎死硬產出,應該標高信度 +- **建議修法**:INSERT 加 `confidence` 欄位設 1.0;或將 `avg_quality` 從 0.9 改為 1.0 +- **嚴重度**:MEDIUM,不阻 deploy。 + +#### M10. `daily_token_report` 截斷邏輯雙重保險,但**截斷點落在 HTML tag 中間**會壞 parse_mode='HTML' + +- **位置**: + - `services/token_report_service.py:130` `report_html[: _TELEGRAM_MAX_CHARS - 80]` + - `services/telegram_templates.py:566` `body[: _DAILY_TOKEN_REPORT_MAX_CHARS - 80]` +- **風險**:若截斷剛好落在 `...` 之間(例如卡在 `]*$', '', truncated)` 把不完整的開 tag 砍掉 +- **嚴重度**:MEDIUM,建議 Phase 2 修;不阻 deploy(萬一觸發只是該日報表掉而已,scheduler 不爆)。 + +### LOW + +#### L5. `qwen3:14b` 在戰役 v5.0 Frontier 升級表中提到,但 COST_TABLE 未列 + +- **位置**:`services/ai_call_logger.py:43-62` +- **驗證**:`grep -n "qwen3" services/ai_call_logger.py services/token_report_service.py` → 0 hit +- **影響**:未來 P2 把 NemoTron 改用 qwen3:14b 時,第一波寫入會走 `_calc_cost` 的 `unknown model` 路徑,log warning 但成本回 0(因為 qwen3 是本地 Ollama 也應為 0)→ 行為正確但 noisy log +- **建議**:在 COST_TABLE 加: + ```python + 'qwen3:14b': {'in': 0.0, 'out': 0.0}, + 'qwen3:14b-q4_K_M': {'in': 0.0, 'out': 0.0}, # 視 v5.0 量化版 + ``` +- **嚴重度**:LOW(Phase 2 會修),不阻 deploy。 + +#### L6. `total_cost_usd` SUM 無 NUMERIC 上限保險 + +- **位置**:`services/token_report_service.py:195` `COALESCE(SUM(cost_usd), 0)` +- **思考**:單筆 cost_usd 是 NUMERIC(10,6)(上限 9999.999999)。若一日內呼叫 100,000 筆且每筆 ~$0.01,SUM 仍 < 10K 安全 +- **判定**:v5.0 規模下不會炸;但 Phase 9 預算守門需重新評估 — 統帥可忽略。 + +#### L7. `total_duration` (Decimal) 隱式轉 float 可能損失精度 + +- **位置**:`services/token_report_service.py:30` `from decimal import Decimal` 但未使用 +- **影響**:dead import,無功能影響 +- **建議**:刪 line 30 import。 + +#### L8. `Section 4` 預算列在無預算時用 `_pad('未設定預算', 10)` 寬度可能斷行 + +- **位置**:`services/token_report_service.py:843-844` +- **驗證**:「未設定預算」5 個中文 = 10 寬度,剛好;無 padding 餘量。寫死 OK。 +- **嚴重度**:LOW,FYI。 + +--- + +## Unit test 實測 + +``` +$ /opt/anaconda3/bin/python3 -m pytest tests/test_ai_call_logger.py tests/test_token_report_service.py -v 2>&1 | tail -30 + +tests/test_token_report_service.py::TestQueriesViaMock::test_query_top_callers_orders_by_tokens PASSED [ 76%] +tests/test_token_report_service.py::TestQueriesViaMock::test_query_cost_breakdown_filters_zero_cost PASSED [ 78%] +tests/test_token_report_service.py::TestSendDailyReport::test_send_happy_path PASSED [ 80%] +tests/test_token_report_service.py::TestSendDailyReport::test_send_truncates_oversized_message PASSED [ 82%] +tests/test_token_report_service.py::TestSendDailyReport::test_send_resilient_to_telegram_failure PASSED [ 84%] +tests/test_token_report_service.py::TestSendDailyReport::test_generate_returns_failure_msg_when_db_dies PASSED [ 86%] +tests/test_token_report_service.py::TestTelegramTemplate::test_daily_token_report_appends_footer PASSED [ 88%] +tests/test_token_report_service.py::TestTelegramTemplate::test_daily_token_report_truncates_to_4096 PASSED [ 90%] +tests/test_token_report_service.py::TestTelegramTemplate::test_daily_token_report_escapes_footer_url PASSED [ 92%] +tests/test_token_report_service.py::TestFormatHelpers::test_fmt_kb PASSED [ 94%] +tests/test_token_report_service.py::TestFormatHelpers::test_esc_handles_none PASSED [ 96%] +tests/test_token_report_service.py::TestFormatHelpers::test_budget_line_zero_budget PASSED [ 98%] +tests/test_token_report_service.py::TestFormatHelpers::test_trend_line_handles_zero_baseline PASSED [100%] + +============================== 52 passed in 0.21s ============================== +``` + +**結論**:52/52 全綠(22 ai_call_logger + 30 token_report_service),覆蓋: +- happy/exception/explicit-fallback/set-error 三種 context manager 路徑 +- decorator + model_extractor + 例外 reraise +- DB 失敗 swallow / async dispatch 失敗 swallow +- COST_TABLE 各 provider 計算 + 未知 model + NIM 前綴自動 0 + 負數安全 +- AI_CALL_LOGGING_ENABLED 開關 + kill-switch 連續失敗 +- 6 條告警規則(spike/gemini share/error rate/budget/gcp hit/cache)+ insights 規則 +- 報表 6 段落齊全 + 4096 截斷 + HTML escape + DB fail 路徑 +- format helpers(fmt_kb/esc/budget_line/trend_line)邊界 + +**通過 ai_call_logger 「DB 失敗永不影響主流程」鐵律**:`test_db_failure_does_not_break_main_flow` + `test_async_dispatch_failure_swallowed` 兩條測試直接證明。 + +**通過「meta 不洩露 prompt 原文」鐵律**:`test_meta_does_not_leak_raw_prompt_into_call_state` + `test_set_prompt_hash_truncates_to_12` 兩條測試。 + +--- + +## 安全/PII 審計(六大類深審) + +| 類別 | 結論 | 證據 | +|---|---|---| +| **A. logger 安全** | ✅ 失敗安全到位 | `_write_to_db` 全 try/except / kill-switch 在連續 10 次失敗觸發 (`_record_failure:80-89`) / `_async_write` 走 daemon thread 不阻塞 | +| **B. PII 保護** | ⚠️ **新發現 chat_id 進 meta(H6)** | logger 本身只用 `set_prompt_hash` 雜湊;但 4 個 Bot 入口直接灌 chat_id 進 meta — 違反規格 | +| **C. SQL Injection** | ✅ 全參數化 | `_exec_query` 強制走 SQLAlchemy `text(sql), params`;7 條報表 SQL 全用 named param | +| **D. HTML escape** | ✅ 對齊既有風格 | `_esc()` 對 `&<>` 三字元與 `telegram_templates._html_escape` 一致;所有 user-controlled (caller/model/error/insight text) 進 HTML 前都 escape | +| **E. 路由 / cron 衝突** | ✅ 23:55 唯一 | 17 條既有 cron 中無 23:5x 區段;資源競爭風險低(DB query 預估 < 30s) | +| **F. 預算 0 / 除 0** | ✅ 全數防護 | 7 處潛在除 0 全部有 `if X else default` 守衛 | + +--- + +## 與既有系統整合風險 + +### 1. A4 修改 vs Phase 2 A6 即將修改 — **conflict 風險評估** + +| 檔案 | A4(Phase 1)做了什麼 | A6(Phase 2)將做什麼 | Conflict 風險 | +|---|---|---|---| +| `services/ollama_service.py` | + `get_host_label()` / `OllamaResponse.host` 欄位 / `host=self.host` 4 處 | 預期擴 GCP/111 切換邏輯(B2)、補 token 欄位(M7) | 🟡 中 — 都動同一個 `OllamaResponse` dataclass + `generate()` 主體;建議 A6 先 rebase 再開工 | +| `services/code_review_pipeline_service.py` | 3 處包 log_ai_call(hermes/openclaw/elephant) | 修補 B3 / 接入 ad-hoc retry | 🟡 中 — 都動 `_hermes_scan` / `_openclaw_assess` 主流程;rebase 前先讀 A4 區塊 | +| `services/aider_heal_executor.py` | A4 **未動** | A6 將動 | ✅ 低 — 完全分離 | +| `routes/openclaw_bot_routes.py` | 4 處包 log_ai_call(main/gemini×2/nim) | 預期不會碰 | ✅ 低 | +| `services/hermes_analyst_service.py` | 2 處包 log_ai_call + 修 commit 00591c5 殘留 bug | 預期不碰 | ✅ 低 | +| `services/nemoton_dispatcher_service.py` | 1 處包 log_ai_call | 預期不碰 | ✅ 低 | +| `services/openclaw_strategist_service.py` | _call_gemini/_call_nvidia_nim 加 `caller=` 參數 | 預期不碰 | ✅ 低 | + +**建議**: +- A6 開工前先 `git pull origin main` 拉到 A4 的 commit +- 動 `ollama_service.py` 時優先**新增**而非改既有 dataclass 欄位(minimize merge conflict) +- 動 `code_review_pipeline_service.py` 時保留現有 `with log_ai_call(...)` 包裝層,僅在内層修補 + +### 2. `commit 00591c5` 殘留 bug 修復確認 + +`services/hermes_analyst_service.py:194-200`:原本 commit 00591c5 動到 `except Exception as e:` 區塊時,誤把 `logger.warning` 抹除留下孤立 f-string。本輪 A4 順手修補: +```python +except Exception as e: + # NOTE: 修補 commit 00591c5 殘留的孤立 f-string(原 logger.warning 被誤刪) + logger.warning( + f"[Hermes.intent] Ollama 連線失敗,降級規則引擎" + f"(model={HERMES_MODEL} error={type(e).__name__}: {e})" + ) + _ctx.set_error(f"{type(e).__name__}: {e}") + _ctx.fallback_to_caller('hermes_rule_engine') + return None +``` +✅ **已驗證**:logger.warning 完整呼叫 + ctx.set_error + fallback_to_caller 三件齊全。原 silent failure 反模式已破。 + +--- + +## Phase 2 銜接建議 + +統帥批准 Phase 2 A6 開工前,建議先 commit Phase 1 的所有變動到 main(A6 才有乾淨 baseline)。順序: + +```bash +git add migrations/024 migrations/025 migrations/026 # A3 +git add services/ai_call_logger.py services/token_report_service.py # A4/A5 +git add tests/test_ai_call_logger.py tests/test_token_report_service.py # A4/A5 tests +git add services/hermes_analyst_service.py services/nemoton_dispatcher_service.py +git add services/openclaw_strategist_service.py services/code_review_pipeline_service.py +git add services/ollama_service.py routes/openclaw_bot_routes.py +git add run_scheduler.py services/telegram_templates.py +git add docs/phase0_audit_report_20260503.md docs/phase1_db_design_20260503.md +git add docs/phase1_critic_review_20260503.md docs/phase1_final_critic_signoff_20260503.md +git commit -m "[Phase 1] Operation Ollama-First v5.0 觀測層落地 (A3 migration / A4 logger 13 callers / A5 token report)" +``` + +部署後第一波驗證(Phase 2 啟動前): + +```sql +-- 1. CHECK constraint 全在 +SELECT conname FROM pg_constraint WHERE conrelid='ai_calls'::regclass ORDER BY conname; + +-- 2. 種子預算 10 筆 +SELECT period, provider, budget_usd, alert_pct FROM ai_call_budgets ORDER BY period, provider NULLS FIRST; + +-- 3. logger 寫入煙測(A4 接入後第一次 LLM 呼叫應出現) +SELECT caller, provider, model, status, input_tokens, output_tokens, duration_ms +FROM ai_calls ORDER BY called_at DESC LIMIT 20; + +-- 4. caller 分布(驗證 13 個白名單值) +SELECT caller, COUNT(*) FROM ai_calls GROUP BY caller ORDER BY 2 DESC; + +-- 5. 失敗率(觀察 kill-switch 是否誤觸發) +SELECT status, COUNT(*) FROM ai_calls +WHERE called_at >= NOW() - INTERVAL '24h' GROUP BY status; +``` + +Phase 2 進度第 1 天觀察點: + +- `SELECT count(*) FROM ai_calls;` 應 ≥ 100(戰役前審計 34 個呼叫點 / 一日 ~200 calls) +- `SELECT count(DISTINCT caller) FROM ai_calls;` 應 ≥ 13 +- 23:55 cron 第一次跑完應有 1 筆 `ai_insights WHERE insight_type='daily_token_report'` + +--- + +## NOTE 清單(Sign-off 條件) + +deploy 後 7 天内由統帥決策處理: + +- [ ] **NOTE-1(H5)**:考慮加 caller 格式 CHECK constraint(NOT VALID 不阻既存資料) +- [ ] **NOTE-2(H6)**:4 個 Bot 入口的 `chat_id` 改成 hash 後存(Phase 2 第一波 patch) +- [ ] **NOTE-3(M7)**:`OllamaResponse` 補 `prompt_tokens/completion_tokens` 欄位 → 修復 `openclaw_bot_main` token=0 黑洞(與 Phase 2 A6 ollama_service 改動合併) +- [ ] **NOTE-4(L1)**:deploy 前手動驗 `git fetch ewoooc && git log ewoooc/main --oneline -- migrations/` + +deploy 後 30 天可選優化: + +- [ ] **M9** ai_insights confidence 標 1.0 / **M10** HTML tag 截斷修補 / **L5** qwen3:14b 進 COST_TABLE / **L7** 刪 `Decimal` dead import +- [ ] **B1** ai_usage_tracking ORM 對齊真實 schema(雙寫 deprecate roadmap,與 ADR-028 合併) + +--- + +## Final Sign-off + +``` +critic-A11 / 2026-05-03 / Phase 1 final closure +Verdict: APPROVED WITH NOTES +Tests: 52/52 PASSED (22 ai_call_logger + 30 token_report) +Findings (本輪新發現): 0 BLOCKER / 2 HIGH (H5/H6) / 4 MEDIUM (M7-M10) / 4 LOW (L5-L8) +Findings (前輪殘留): 1 HIGH (H4 文件) / 1 MEDIUM (M4 解耦) / 1 LOW (L1 統帥手驗) + +簽署:critic-A11 (Operation Ollama-First v5.0 / Phase 1 sign-off) +``` diff --git a/docs/phase2_deploy_verify_20260503.md b/docs/phase2_deploy_verify_20260503.md new file mode 100644 index 0000000..6da8c17 --- /dev/null +++ b/docs/phase2_deploy_verify_20260503.md @@ -0,0 +1,205 @@ +# Phase 2 部署驗證劇本(ADR-027 真正落地) + +> **Date**: 2026-05-03 +> **Phase**: Operation Ollama-First v5.0 — Phase 2(A6 debugger) +> **修補項**: B1 / B2 / B3 / B4 / N2 / N3 +> **修改檔**: `config.py` / `services/ollama_service.py` / `services/aider_heal_executor.py` / `services/code_review_pipeline_service.py` +> **新檔**: `tests/test_ollama_resolve.py`(13 tests,本機已通過) + +--- + +## 一、部署前 dry-run(本機) + +### 1.1 語法檢查 + +```bash +cd "/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system" +python3 -m py_compile config.py services/ollama_service.py \ + services/aider_heal_executor.py services/code_review_pipeline_service.py \ + tests/test_ollama_resolve.py && echo "PYCOMPILE_OK" +``` + +期望:`PYCOMPILE_OK`(已驗證) + +### 1.2 Unit test + +```bash +MOMO_ALLOW_INSECURE_CONFIG_FOR_TESTS=true /opt/anaconda3/bin/python3 -m pytest \ + tests/test_ollama_resolve.py \ + tests/test_phase3f_cleanup_contracts.py \ + tests/test_app_startup_contracts.py \ + tests/test_ai_call_logger.py \ + tests/test_code_review_pipeline_security.py \ + tests/test_auto_heal_safety.py -v +``` + +期望:56 passed(13 新 + 43 既有)。已驗證。 + +### 1.3 import 一致性 + +```bash +MOMO_ALLOW_INSECURE_CONFIG_FOR_TESTS=true /opt/anaconda3/bin/python3 -c " +from config import get_ollama_host, get_hermes_url, get_embedding_host +from services.ollama_service import resolve_ollama_host, mark_unhealthy +print('get_ollama_host =', get_ollama_host()) +print('get_hermes_url =', get_hermes_url()) +print('get_embedding_host =', get_embedding_host()) +print('resolve_ollama_host=', resolve_ollama_host()) +" +``` + +期望(網路通時):四行都印 `http://34.21.145.224:11434`(GCP 可達)或 `http://192.168.0.111:11434`(GCP 不可達)。 +不可出現 `https://ollama.wooo.work/ollama`(舊寫死 URL)。 + +--- + +## 二、部署後驗證(SSH 188) + +### 2.1 容器健康 + +```bash +ssh wooo@192.168.0.110 "ssh ollama@192.168.0.188 \"\ + docker ps --format '{{.Names}} | {{.Status}}' | grep momo-; \ + docker exec momo-pro python3 -c 'from config import get_ollama_host; print(get_ollama_host())' 2>&1\"" +``` + +期望: +- `momo-pro | Up`(重啟後新容器) +- 列印的 host 不是 `https://ollama.wooo.work/ollama` + +### 2.2 OllamaHost 解析 log(B3 HTTP probe 驗證) + +```bash +ssh wooo@192.168.0.110 "ssh ollama@192.168.0.188 \"\ + docker logs momo-pro --since 10m 2>&1 | grep -E 'OllamaHost' | tail -20\"" +``` + +期望(GCP 可達): +``` +[OllamaHost] GCP 主機可用,使用 Primary: http://34.21.145.224:11434 +``` + +期望(GCP 掛時): +``` +[OllamaHost] GCP 主機無法連線,自動切換 Fallback: http://192.168.0.111:11434 +``` + +罕見(process 卡死,TCP 通但 HTTP 掛): +``` +[OllamaHost] GCP HTTP 探測失敗但 TCP 仍通,疑似 process 卡死:http://34.21.145.224:11434 +[OllamaHost] GCP 主機無法連線,自動切換 Fallback: http://192.168.0.111:11434 +``` + +> 第三種日誌是 **Phase 2 修補後才會看見的新觀測能力**,舊版純 TCP 探測不會印。 + +### 2.3 mark_unhealthy 觸發(B4 驗證) + +當 LLM generate 真的失敗時,會看見: +``` +[OllamaHost] 主機標記為 unhealthy(30s 跳過):http://34.21.145.224:11434 +``` + +立刻在下一次任何 ollama 呼叫的 log 看: +``` +[OllamaHost] Primary http://34.21.145.224:11434 仍在 unhealthy TTL 內,跳過直接 fallback: http://192.168.0.111:11434 +``` + +### 2.4 AiderHeal OLLAMA_API_BASE 動態化(N2 驗證) + +下次 AiderHeal 觸發時 grep: +```bash +ssh wooo@192.168.0.110 "ssh ollama@192.168.0.188 \"\ + docker logs momo-pro --since 30m 2>&1 | grep 'aider_ollama_api_base' | tail -5\"" +``` + +期望: +``` +event=aider_ollama_api_base host=http://34.21.145.224:11434 +``` +(GCP 可達時)或 `host=http://192.168.0.111:11434`(fallback)。 +**絕不可** 仍顯示 `http://192.168.0.111:11434` 當 GCP 是可達的。 + +### 2.5 Code Review provider tag(N3 驗證) + +下次 Code Review pipeline 觸發後: +```bash +ssh wooo@192.168.0.110 "ssh ollama@192.168.0.188 \"\ + docker exec momo-postgres psql -U momo -d momo_analytics -c \ + \\\"SELECT caller, provider, meta->>'host' AS host \ + FROM ai_calls \ + WHERE caller = 'code_review_hermes' \ + ORDER BY created_at DESC LIMIT 5;\\\"\"" +``` + +期望(GCP 通時): +``` +caller | provider | host +code_review_hermes | gcp_ollama | http://34.21.145.224:11434 +``` + +絕不可仍標 `ollama_111` 當 host 是 GCP。 + +--- + +## 三、模擬故障驗證(選做) + +### 3.1 模擬 GCP 不可達 → 5s 內 fallback + +在 188 上臨時封鎖 GCP IP: +```bash +ssh wooo@192.168.0.110 "ssh ollama@192.168.0.188 \"\ + sudo iptables -A OUTPUT -d 34.21.145.224 -j DROP\"" +``` + +立即觸發 sales copy(or 任何 LLM 入口),看 log: +- 第一次呼叫應 timeout(2s 內 _is_reachable 失敗)→ 切 fallback +- 之後 30s 內所有呼叫直接走 fallback +- 30s 後 cache TTL 過期,會重新探測(仍封鎖則繼續 fallback;解封後恢復 GCP) + +恢復: +```bash +ssh wooo@192.168.0.110 "ssh ollama@192.168.0.188 \"\ + sudo iptables -D OUTPUT -d 34.21.145.224 -j DROP\"" +``` + +> 此項屬統帥權限,debugger 不執行。 + +--- + +## 四、回滾 SOP + +若部署後出問題,最快回滾: +```bash +git revert +git push origin main +# 等 Gitea CD 自動部署 +``` + +也可以單獨回退 ollama_service.py: +```bash +git checkout HEAD~1 -- services/ollama_service.py config.py +``` +(其他三檔變更可獨立保留) + +--- + +## 五、commit 草稿 + +``` +[V-New] ADR-027 Phase 2:Ollama 主機解析全鏈 lazy + HTTP probe + unhealthy 標記 + +修補 6 項讓 ADR-027「GCP 優先」真正 100% 落地: + B1 — config.OLLAMA_HOST 改 lazy resolve(移除寫死 ollama.wooo.work URL) + B2 — config.EMBEDDING_HOST / HERMES_URL 改 lazy(避免 import-time freeze) + B3 — _is_reachable 改 HTTP probe (/api/version, 2s timeout),TCP 改作觀測點 + B4 — 新增 mark_unhealthy(),generate / embedding 失敗時標 30s,cache 失效 + N2 — aider_heal_executor.OLLAMA_API_BASE 改 lazy resolve(每次 execute 重評估) + N3 — code_review_pipeline_service Hermes scan 改 get_hermes_url() 取代 freeze + +新增:tests/test_ollama_resolve.py(13 tests) +變更:config.py / services/ollama_service.py / + services/aider_heal_executor.py / services/code_review_pipeline_service.py + +驗證:56 tests 全綠(13 新 + 43 既有 regression),py_compile 全綠。 +驗證劇本:docs/phase2_deploy_verify_20260503.md(給統帥 SSH 188 跑)。 +``` diff --git a/docs/phase6_critic_signoff_20260503.md b/docs/phase6_critic_signoff_20260503.md new file mode 100644 index 0000000..fd2fa9c --- /dev/null +++ b/docs/phase6_critic_signoff_20260503.md @@ -0,0 +1,404 @@ +# Phase 6 Critic Sign-off — Operation Ollama-First v5.0 + +> **Date**: 2026-05-03 +> **Reviewer**: critic-A11(Phase 6 文件層收尾,第三輪) +> **Scope**: ADR-028 / ADR-029 / ADR-027 附錄 / docs/adr/README.md +> **基準**: 憲法紅線一(事實驅動,狙擊手精神) +> **任務契約**: 驗證每個具體數字、檔案行號、聲稱的決策都有 Phase 0/1/2/3 報告佐證 + +--- + +## Verdict + +- [ ] APPROVED — Phase 6 ADR 可 commit +- [ ] APPROVED WITH NOTES — 統帥確認 NOTE 後 commit +- [x] **CONDITIONAL** — 修以下 BLOCKER 後 commit;HIGH 可同 commit 內順便修 +- [ ] REJECTED + +**理由**:ADR-028 / ADR-029 在「事實層」有 5 個 BLOCKER 級錯誤(行號錯、caller 名虛構、provider 白名單與 DB 不一致、OpenClaw 行數錯、減幅算術不自洽)。這兩份文件一旦 commit 會被未來所有 Phase 引用,事實錯誤會成為「憲法級謬誤」傳承。憲法紅線一不容妥協 — 這幾個數字必須改正後再 merge。 + +ADR-027 附錄與 README 索引部分基本正確,但附錄 A 引用「寫死 IP 已全面消除」用詞過強(aider_heal_executor.py:62 仍有 fallback 字面 IP),降為 HIGH。 + +--- + +## 事實核驗(逐項) + +### A. 程式碼行數 + +| 聲稱 | ADR 寫 | wc -l 實測 | 差距 | 判定 | +|---|---|---|---|---| +| `services/openclaw_strategist_service.py` | **1831 行**(ADR-029:18, 113)| **2677 行** | **+846(+46%)** | 🔴 BLOCKER | +| `services/hermes_analyst_service.py` | **573 行**(ADR-029:19)| **607 行** | +34(+5.9%) | 🟠 HIGH | +| 比率 | 4.4× | 4.41× | 湊巧仍對 | ✅ | +| 預估 A10 後 | 1300 行 | — | 但是基準錯,目標 1300 行也失準 | 🔴 連帶 | + +**證據**: +``` +$ wc -l services/openclaw_strategist_service.py services/hermes_analyst_service.py +2677 services/openclaw_strategist_service.py + 607 services/hermes_analyst_service.py +``` + +ADR-029 的 1831 / 573 是直接抄 phase0 audit(也錯),而非實際數行。狙擊手精神失守。 + +### B. 場景 file:line 行號 + +| 場景 | ADR-028 寫 | 實測 | 判定 | +|---|---|---|---| +| #1 MCP L1 Grounding | `mcp_collector_service.py:163-167` | 163-167 是 `for tools in (...)` Gemini 設定區塊 | ✅ 對得上 | +| #2 MCP L2 Grounding | `mcp_collector_service.py:185-186` | 185-186 是 except 塊內的 1.5-flash 重試 | ✅ 對得上 | +| #3 PPT generator | `routes/openclaw_bot_routes.py:2464-2477` | 2469 是 `_call_gemini` def | ✅ 對得上 | +| #4 openclaw_weekly | `services/openclaw_strategist_service.py:759` | **實際在 1340** | 🔴 BLOCKER | +| #4 openclaw_monthly | `services/openclaw_strategist_service.py:1267` | **實際在 1771** | 🔴 BLOCKER | +| #4 openclaw_annual | 「戰略月年報」(無 file:line)+ caller 名 `openclaw_annual` | **caller 與 function 都不存在**(grep 0 hits)| 🔴 BLOCKER(虛構) | +| #5 code_review_openclaw | `services/code_review_pipeline_service.py:278-286` | 278-286 是 system/user prompt 字串;實際 `_call_gemini` 在 309 | 🟠 HIGH(行號偏移) | +| #6 ea_hitl_prefetch | `services/elephant_alpha_orchestrator.py`(無行號)+ caller 名 `ea_hitl_prefetch` | **caller 不存在**(grep 0 hits) | 🔴 BLOCKER(虛構 caller) | +| #7 openclaw_qa_complex_sku | `services/openclaw_strategist_service.py:56` | line 56 是 feature flag 註解;caller `openclaw_qa_complex_sku` 不存在(grep 0 hits) | 🔴 BLOCKER(虛構 caller + 行號錯) | + +證據: +``` +$ grep -rn "ea_hitl_prefetch\|openclaw_qa_complex\|openclaw_annual" services/ routes/ +(0 hits) +``` + +ADR-028 的「鎖定 Gemini 7 個場景」表格把 7 個 caller 名直接寫入 ADR,但其中 3 個(#4 annual / #6 EA HITL / #7 complex SKU)**caller 名是憑空編出來的**,至今程式碼從未 emit 這些 caller。等同 ADR 治理規則指向「不存在的東西」。 + +### C. Provider 白名單一致性 + +| 來源 | provider 列表 | 數量 | +|---|---|---| +| ADR-028:104-114「Provider 白名單」表格 | `gcp_ollama` / `ollama_secondary` / `ollama_111` / `gemini` / `nim` / `nim_via_elephant` / `openrouter` | 7(**不含 claude,含 ollama_secondary**)| +| ADR-028:114「移除原計畫的 claude provider」聲明 | 7(不含 claude) | 7 | +| ADR-028:208 Verification V1 期望輸出 | 7(不含 claude) | 7 | +| 實際 `migrations/024:92-94` `chk_ai_calls_provider` | `gcp_ollama` / `ollama_secondary` / `ollama_111` / `gemini` / `claude` / `nim` / `openrouter` / `nim_via_elephant` | **8(含 claude)** | +| `services/token_report_service.py:42-50` `_PROVIDER_DISPLAY` | `gcp_ollama` / `ollama_111` / `gemini` / `claude` / `nim` / `openrouter` / `nim_via_elephant` | 7(**含 claude,不含 ollama_secondary**)| +| `migrations/025` budget 種子 monthly 列表 | 7:含 `claude` 但缺 `ollama_secondary` | 7 | + +**三處不一致**: +1. ADR-028 表格寫含 `ollama_secondary` 不含 `claude` +2. DB CHECK 兩個都包含 +3. token_report `_PROVIDER_DISPLAY` 與 budget 種子兩個都缺 `ollama_secondary`,含 `claude` + +判定:🔴 BLOCKER。ADR-028 的 Verification V1「期望輸出」實際跑會驗證失敗(會多出 `claude`、缺 `ollama_secondary`...都不是 — 8 個對照 7 個直接 mismatch)。`phase1_final_critic_signoff_20260503.md:26` 也記成「7 個 provider 與 _PROVIDER_DISPLAY 完全一致」— 這是上一輪 critic 的盲區,現在被 ADR-028 沿襲。 + +### D. OpenClaw / Hermes Token 流量估算 + +ADR-029 line 23-25 與 line 91-94 的算術: + +- 戰前 Gemini ~50M tokens/月、Hermes ~30M tokens/月 +- 任務 3/4/5 遷移省 ~12M tokens +- 任務 11 降頻省 ~3M tokens +- 戰後 Gemini ~38M tokens/月(line 112) +- 減幅 -23%(line 112) + +**算術不自洽**: +- 50M − (12M + 3M) = **35M**(非 38M) +- 50→38 = **-24%**(非 -23%) +- 50→35 = **-30%** + +三個數字(戰後 38、節省 15、減幅 23)至少有一個錯,三個都互不對齊。 + +判定:🔴 BLOCKER。憲法級文件出現自相矛盾的關鍵 KPI 數字。 + +ADR 第 119 行已誠實標示「上述為 Phase 0 audit 推算,Phase 5 報表上線後以 ai_calls 實測值修訂」— 動機可諒解(沒實測),但即便是估算,三個推算值也必須互相對得上。 + +### E. Meta 自審 6h → 12:00 + +| 聲稱 | 實際 | 判定 | +|---|---|---| +| `run_scheduler.py:99` 改為每日 12:00 | line 99 `schedule.every().day.at("12:00").do(run_openclaw_meta_analysis_task)` | ✅ 已落地 | +| 月省 ~3M Gemini tokens(ADR-029:93)| `run_scheduler.py:97` 註解寫「~1.875M」 | 🟡 LOW(兩處數字不對齊但量級接近) | +| 對應 task = A10 / Phase 7-8(ADR-029:104)| 註解寫 Phase 4 落地 | 🟠 HIGH(A10 標籤對應 Phase 與實際提交時 Phase 不一致)| + +### F. 寫死 IP 是否「全面消除」 + +ADR-027 附錄 A 寫「寫死 IP 已全面消除(aider_heal_executor.py:48-49 與 code_review_pipeline_service.py:218-225 兩處 N2/N3 修補)」。 + +實測: +``` +$ grep -rn "192.168.0.111" services/ routes/ | grep -v "OLLAMA_HOST_FALLBACK\|test_\|resolve_ollama_host\|#" +services/aider_heal_executor.py:62: return "http://192.168.0.111:11434" ← 仍有 +services/hermes_analyst_service.py:7: 模型:hermes3:latest @ HERMES_URL(預設 192.168.0.111:11434) ← docstring 仍寫 +``` + +`aider_heal_executor.py:62` 是 `_default_ollama_api_base()` 的最後 except 兜底(line 60-62),技術上是「resolve 失敗才回退到字面 111」,不是「import-time 寫死」,但「全面消除」的措辭過強。 + +判定:🟠 HIGH。建議改為「`import-time` 寫死 IP 已全面消除(line 48-49 已改為 lazy resolve;line 62 保留 except 兜底字面 IP 作為最終防線)」。 + +### G. 11.8% AIGenerationHistory 覆蓋率 + +phase0 audit Section 1.4 line 80-81 寫「4/34 ≈ 11.8%」 → ADR-028 line 19 引用一致 ✅。 + +### H. Phase 1 / 2 / 3 落地狀態 + +| Phase | ADR 寫 | 實測 | 判定 | +|---|---|---|---| +| Phase 0 | ✅ 完成 | phase0_audit + phase0_research 都存在 | ✅ | +| Phase 1 | 52/52 tests pass | phase1_final_critic_signoff 確認 | ✅ | +| Phase 2 | 13+43=56 tests pass | phase2_deploy_verify 確認 | ✅ | +| Phase 3 A7 | 已完成(feature flag)| `OPENCLAW_QA_OLLAMA_FIRST` env 在 openclaw_strategist_service.py:51, 61, 162-163, 259 出現,預設 false | ✅ | +| migration 024/025/026 | 存在 | `migrations/024_create_ai_calls_table.sql` / `025_create_mcp_calls_and_budgets.sql` / `026_add_embedding_signature.sql` | ✅ | +| Meta 12:00 | run_scheduler.py:99 | 確認 | ✅ | + +--- + +## Findings + +### BLOCKER(事實錯誤,必須改) + +#### B1. ADR-029 OpenClaw 行數錯 846 行(+46%) + +- **位置**:`docs/adr/ADR-029-hermes-first-twin-tower.md:18, 113` +- **錯誤**:寫 `services/openclaw_strategist_service.py ≈ 1831 行` +- **實測**:`wc -l = 2677 行` +- **影響**: + - line 18 的「失衡證據」(戰前 1831)失真,但比率 4.4× 湊巧仍接近真實 4.41×(仍在 4× 量級) + - line 113 的「預估 1300 行(A10 後)」基準錯誤 → 戰後行數要 -29% 應是 ~1900 行才合理;若仍要砍到 1300,是 -51%(戰前 2677 → 1300),是更激進的目標但 ADR 沒揭露 + - 量化效益表的 OpenClaw 程式碼瘦身欄位失準 +- **建議修法**: + - 把 1831 改 2677 + - 重算「-29% 後 ~1900 行」或「若仍鎖定 1300 行則應改寫為 -51%(更大重構工程)」 + - line 142 的「OpenClaw 從 1831 行降至 ~1300 行(A10)」同步修 + +#### B2. ADR-028 場景 #4 行號錯(759/1267 vs 1340/1771) + +- **位置**:`docs/adr/ADR-028-llm-routing-unified-principles.md:75` +- **錯誤**:「openclaw_weekly / openclaw_monthly / openclaw_annual」location 寫 `services/openclaw_strategist_service.py:759, 1267, 戰略月年報` +- **實測**: + - `caller="openclaw_weekly"` 在 line 1340 + - `caller="openclaw_monthly"` 在 line 1771 + - `caller="openclaw_annual"` **0 hits**(不存在) +- **影響**:未來新工程師看 ADR 找 759 line 會找到 `_legacy_gemini_first_qa` 內部,誤判 caller 對應位置;annual report 根本沒實作但 ADR 列為「鎖定場景」 +- **建議修法**: + - 759 → 1340,1267 → 1771 + - `openclaw_annual` 從鎖定場景表移除(或改為「待實作」並引述 Phase X 計畫) + +#### B3. ADR-028 場景 #6 / #7 caller 名虛構 + +- **位置**:`docs/adr/ADR-028-llm-routing-unified-principles.md:77-78` +- **錯誤**: + - 場景 #6 caller `ea_hitl_prefetch` — grep 0 hits(程式碼從未 emit 此 caller) + - 場景 #7 caller `openclaw_qa_complex_sku` — grep 0 hits(同上) +- **實測**: + ``` + $ grep -rn "ea_hitl_prefetch\|openclaw_qa_complex" services/ routes/ + (無輸出) + ``` +- **影響**:ADR 把治理規則繫於不存在的 caller;DB token report `WHERE caller='ea_hitl_prefetch'` 永遠 0 筆;Phase 5 預算告警會誤判 +- **建議修法**: + - 若是「規劃中」,標示 `(規劃中,Phase X 引入)` + - 若是「已存在但 caller 名不同」,改為實際 caller(例如 EA HITL 預跑可能走 `hermes_intent` / `hermes_analyst`) + - line 78 的 `services/openclaw_strategist_service.py:56` 不是 caller 而是 feature flag 註解,行號需重指 + +#### B4. ADR-028 Provider 白名單與 DB CHECK 不一致 + +- **位置**: + - `docs/adr/ADR-028-llm-routing-unified-principles.md:104-114`(白名單表) + - `docs/adr/ADR-028-llm-routing-unified-principles.md:114`(移除 claude 聲明) + - `docs/adr/ADR-028-llm-routing-unified-principles.md:208`(V1 期望輸出) +- **錯誤**:ADR 寫 7 個 provider(含 ollama_secondary,不含 claude) +- **實測**: + - `migrations/024_create_ai_calls_table.sql:92-94` 是 **8 個**(含 claude 與 ollama_secondary) + - `services/token_report_service.py:42-50` `_PROVIDER_DISPLAY` 是 7 個(**含 claude,不含 ollama_secondary**) + - `migrations/025` 預算種子 monthly 是 7 個(含 claude,缺 ollama_secondary) +- **影響**: + - Verification V1 SQL 跑出來會與「期望」對不上 → 部署驗證會誤判 FAIL + - 「移除 claude」是空話 — DB 並未移除 + - `ollama_secondary` 在 DB 接受但無任何程式碼會 emit → SELECT 永遠 0 筆 → Phase 5 三主機級聯可觀測性失真 +- **建議修法**(三選一): + - 路線 A:ADR-028 改為「8 provider(含 claude)」,新增程式碼 emit `ollama_secondary` 標籤(patch `code_review_pipeline_service.py:230` 與其他 Ollama caller,根據 resolve 結果動態決定) + - 路線 B:實際下一個 migration 移除 claude,並在 _PROVIDER_DISPLAY 加 ollama_secondary,讓三層真正一致 + - 路線 C:ADR-028 加一段「Schema vs ADR 差異」誠實揭露,Phase X 統一 + +#### B5. ADR-029 Token 流量算術不自洽(38M vs 35M vs -23%) + +- **位置**:`docs/adr/ADR-029-hermes-first-twin-tower.md:23-25, 91-94, 112` +- **錯誤**: + - 戰前 50M → 任務 3/4/5 省 12M、任務 11 省 3M = 共省 15M → 戰後應 35M + - 但 line 112 寫戰後 38M、減幅 -23% + - 50→38 = -24%(非 -23%);50→35 = -30% +- **影響**:戰役 v5.0 KPI「Gemini 月支出 -23%」是 README 索引(line 53)的明牌,數字之間互不對齊讓未來無法驗收 +- **建議修法**: + - 三個數字選一個錨定,其他重算: + - 錨定 -23% → 戰後 38.5M → 節省 11.5M(task 3/4/5/11 拆分需重估) + - 錨定節省 15M → 戰後 35M → 減幅 -30%(README 索引也要改) + - 或加註腳「戰後 token 估算為四捨五入區間,實測誤差 ±5M」誠實揭露不確定性 + +### HIGH(建議改,不一定阻 commit) + +#### H1. Hermes 行數差 +5.9% + +- 位置:ADR-029:19 +- 寫 573 vs 實測 607 +- 改為「607 行」即可 + +#### H2. ADR-027 附錄 A「寫死 IP 全面消除」措辭過強 + +- 位置:ADR-027 附錄 A 段尾 +- 實際 `aider_heal_executor.py:62` 仍有字面 `return "http://192.168.0.111:11434"`(在 except 兜底) +- 建議改為「import-time 寫死已消除;except 兜底保留 111 字面 IP 作為最終防線」 + +#### H3. ADR-028 場景 #5 行號偏移 + +- 位置:ADR-028:76 +- 寫 `code_review_pipeline_service.py:278-286`,實際 278-286 是 prompt 字串;`_call_gemini` 在 line 309 +- 建議改 `:278-310`(涵蓋整個 Gemini 呼叫塊)或 `:309` + +#### H4. ADR-028 caller 白名單列了 30+ 個但實際 emit 僅 ~16 個 + +- 位置:ADR-028:122-138 +- 實際 `grep "caller=" services/ routes/` 唯一 16 個(已驗證上方) +- 列表混雜了「已實作」與「規劃中」沒區分標示 +- 建議:在每個 caller 後標示 `[A4 已落地]` / `[Phase X 規劃中]` + +#### H5. `ollama_secondary` provider 沒有任何 caller emit + +- 位置:ADR-028:107(白名單條目) +- 程式碼層 0 hits — 三主機級聯實作只區分 GCP(gcp_ollama)vs 111(ollama_111),Primary/Secondary 在程式中不區分 +- 建議:要嘛在 `services/ollama_service.resolve_ollama_host()` 與所有 caller 加上「根據 selected host 決定 provider tag」,要嘛把 `ollama_secondary` 從白名單移除直到實作完成 + +#### H6. ADR-029 Phase 標籤錯亂 + +- 位置:ADR-029:104 +- 寫「A10 對應 Phase 7-8」 +- run_scheduler.py:97 註解寫「Phase 4 降頻」 +- ADR-028 Migration Plan line 248 也寫「Phase 7-8 OpenClaw 程式瘦身(A10)」 +- 不一致;建議在文件層統一定義 A10 的 Phase 對應 + +### MEDIUM + +#### M1. ADR-029 第 5 行作者列「Codex / A12 planner」,但戰役組織圖中 A12 是 critic,不是 planner + +- 位置:ADR-029:6 / ADR-028:6 +- planner 角色是 A8(從上下文推斷),但 ADR 寫 A12 +- 不影響事實,但角色標籤需與 v5.0 戰役組織圖核對 + +#### M2. ADR-028 line 156 「Gemini 2.5 Flash vs qwen3:14b 估差 10-20%」引用 phase0_research_report Section 1,但 phase0 報告 Section 1 結論是「黃燈,需 50 題黃金集 A/B 才能定論」,沒給出 10-20% 的硬數字 + +- 位置:ADR-028:156 +- phase0_research line 30-33 寫「推估 10-20%」是未驗證的推估,ADR 直接當事實引用 +- 建議改為「**推估** 10-20%(待 Phase 4 黃金集 A/B 確認)」 + +#### M3. ADR-027 附錄 A 引用「services/code_review_pipeline_service.py:218-225」但實際 218-225 是 prompt 字串 + +- 位置:ADR-027 line 65 +- 實際 `_call_gemini` 與 hermes scan 在 line 230 後 +- 行號偏移,phase0 audit 也錯(同樣的 inheritance 錯誤) + +### LOW + +#### L1. ADR-028:75 寫「`戰略月年報`」中文字摻在 file:line 列,破壞表格格式 + +- 位置:ADR-028:75 +- 應改為「(戰略月/年報,function 待實作)」或拆兩行 + +#### L2. ADR-028 Migration Plan 列了 Phase 0-12 但 Phase 11 / 12 與其他 ADR(如 ADR-026 收尾路線圖)的 Phase 編號可能重疊 + +- 跨 ADR 的 Phase 編號需要統一索引避免混淆 +- 不阻 commit + +#### L3. ADR-029 line 4 沒有 Author 欄位(ADR-028 有) + +- 風格不一致 + +--- + +## ADR 引用一致性 + +| ADR-028 / 029 引用 | 實際內容 | 判定 | +|---|---|---| +| ADR-002 pgvector 唯一向量庫 | 確實存在,未被破壞(memory-mcp 在 phase0 也標 🔴 不採用)| ✅ | +| ADR-003 Hermes embedding 本地化 | 存在 | ✅ | +| ADR-004 NemoTron fallback chain | 存在;ADR-028 引「NIM 80 calls/day」與 ADR-004 一致 | ✅ | +| ADR-008 部署實機驗證 | 存在 | ✅ | +| ADR-013 AIOps AutoHeal | 存在 | ✅ | +| ADR-018 四 Agent 控制面 | 存在;ADR-029 「ADR-018 已定四 Agent 角色,但未量化誰處理高頻」描述準確 | ✅ | +| ADR-019 Telegram Agentic Layer | 存在;ADR-029 line 30 描述「openclaw_decide() 把所有用戶輸入導向」與 ADR-019 一致 | ✅ | +| ADR-021 EA HITL pre-fetch | 存在;但 ADR-028 場景 #6 caller 名與 ADR-021 內 Hermes 預跑實作不對應(B3)| 🔴 連帶 | +| ADR-027 「Supersedes: 無(補述 ADR-027,非取代)」 | 措辭合理,因 ADR-027 仍存在且新增了附錄 | ✅ | +| `migrations/024:88-91` provider CHECK | 實際 line 92-94,包含 8 個 provider(含 claude)| 🔴 B4 | +| `migrations/024:104-109` meta/error 大小 | 已驗證(與 phase1 critic H2 一致)| ✅ | +| phase0_audit Section 1.4 11.8% | 一致 | ✅ | +| phase1_final_critic_signoff H5/H6 | 一致 | ✅ | + +--- + +## 鎖定 Gemini 7 場景驗證(LOCKED-GEMINI 註解 vs ADR-028) + +| # | LOCKED-GEMINI 程式碼註解 | ADR-028 場景 | 一致? | +|---|---|---|---| +| #1 | `services/mcp_collector_service.py:32` LOCKED-GEMINI: MCP 即時情報需 Google Search Grounding | 場景 #1 MCP L1 Grounding | ✅ | +| #2 | (無獨立註解,與 #1 同)| 場景 #2 MCP L2 Grounding | ✅(共享)| +| #3 | `routes/openclaw_bot_routes.py:98` LOCKED-GEMINI: PPT 簡報文案需長 context + 繁中商業敘事 | 場景 #3 PPT generator | ✅ | +| #4 | `services/openclaw_strategist_service.py:40` LOCKED-GEMINI: 週/月/年報需長 context + 繁中商業文體 | 場景 #4 weekly/monthly/annual | 🟠(annual caller 不存在 — B3)| +| #5 | `services/code_review_pipeline_service.py:46` LOCKED-GEMINI: Code Review 全 repo diff 可達 100K+ tokens | 場景 #5 code_review_openclaw | ✅ | +| #6 | `services/elephant_alpha_orchestrator.py:88` LOCKED-GEMINI: EA HITL 戰略決策影響統帥行動 | 場景 #6 ea_hitl_prefetch | 🟠(caller 名與註解中的 "AgentCapability" 模型 `gemini-2.0-flash` 對不上 ADR 寫的 `gemini-2.5-flash`)| +| #7 | (無獨立註解)| 場景 #7 openclaw_qa_complex_sku | 🔴(caller 完全不存在 — B3)| + +**註解 vs ADR 模型不一致**: +- ADR-028 場景 #6 寫 `gemini-2.5-flash` +- `services/elephant_alpha_orchestrator.py:91` AgentCapability 寫 `model="gemini-2.0-flash"` +- 哪個是真的?需校對 + +--- + +## 與既有 ADR 衝突(grep 結果) + +``` +$ grep -rn "ADR-028\|ADR-029" docs/adr/ +(除 ADR-027 / 028 / 029 / README 自身互引以外,其他 ADR-001~026 均無提及) +``` + +✅ 既有 ADR 沒有提到 028/029,所以沒有外部引用衝突。 +✅ ADR-002(pgvector 唯一):phase0 audit Section 2 已標 memory-mcp 🔴 不採用,ADR-028 沒破壞此決策。 +✅ ADR-018(四 Agent 控制面):ADR-029 是「補述」而非取代,措辭合理。 +✅ ADR-027:附錄正式承接,Supersedes 標示「無(補述)」措辭正確。 + +--- + +## 核准條件(CONDITIONAL → APPROVED 的必修清單) + +- [ ] **B1 修**:ADR-029 line 18, 113, 142 把 `1831` 改 `2677`;A10 預估目標重算(建議 -29% → 1900 或維持 1300 但重標 -51%) +- [ ] **B2 修**:ADR-028 line 75 行號 759 → 1340、1267 → 1771 +- [ ] **B3 修**:ADR-028 場景 #4 移除 annual(或標「規劃中」);場景 #6 改用實際存在的 caller 名(如 `hermes_analyst` 或備註「Phase X 引入」);場景 #7 同 +- [ ] **B4 修**:ADR-028 Provider 白名單表格與 V1 期望輸出與 DB CHECK / token_report `_PROVIDER_DISPLAY` 對齊(含 claude、處理 ollama_secondary 缺口) +- [ ] **B5 修**:ADR-029 line 23-25, 112 三個數字(戰前 50M / 戰後 38M / 減幅 -23% / 任務節省 12M+3M)對齊;建議錨定 README 索引「-23%」並回算其他兩個 + +修這 5 個 BLOCKER 後可 commit。HIGH 可同 commit 內順便修,不修也不阻 commit(但會留入 Phase 7+ 技術債)。 + +--- + +## Sign-off + +``` +critic-A11 / 2026-05-03 / Phase 6 / 第三輪審查 +Verdict: CONDITIONAL(5 BLOCKER 待修) +Files Reviewed: + docs/adr/ADR-028-llm-routing-unified-principles.md (269 lines) + docs/adr/ADR-029-hermes-first-twin-tower.md (222 lines) + docs/adr/ADR-027-primary-ollama-on-gcp.md (114 lines, 含附錄) + docs/adr/README.md (60 lines) +Cross-checked Against: + docs/phase0_audit_report_20260503.md (262 lines) + docs/phase0_research_report_20260503.md (231 lines) + docs/phase1_db_design_20260503.md (315 lines) + docs/phase1_final_critic_signoff_20260503.md (317 lines) + docs/phase2_deploy_verify_20260503.md (205 lines) + migrations/024_create_ai_calls_table.sql + migrations/025_create_mcp_calls_and_budgets.sql + services/openclaw_strategist_service.py (2677 lines, wc -l) + services/hermes_analyst_service.py (607 lines, wc -l) + services/ollama_service.py (resolve_ollama_host + mark_unhealthy) + services/ai_call_logger.py + services/token_report_service.py (_PROVIDER_DISPLAY) + services/code_review_pipeline_service.py (LOCKED-GEMINI / provider tag) + services/mcp_collector_service.py (LOCKED-GEMINI) + services/elephant_alpha_orchestrator.py (LOCKED-GEMINI) + services/aider_heal_executor.py (lazy resolve / 兜底字面 IP) + routes/openclaw_bot_routes.py (LOCKED-GEMINI / caller emit / PPT _call_gemini) + run_scheduler.py:99 (Meta 12:00) + +Discipline: 憲法紅線一(事實驅動 / 狙擊手精神)— 嚴格批評; +不修補 ADR,僅標 BLOCKER / HIGH / MEDIUM / LOW; +所有 finding 附 file:line 並對照 phase 報告。 +``` diff --git a/migrations/024_create_ai_calls_table.sql b/migrations/024_create_ai_calls_table.sql new file mode 100644 index 0000000..124c332 --- /dev/null +++ b/migrations/024_create_ai_calls_table.sql @@ -0,0 +1,155 @@ +-- ============================================================================= +-- 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.143.170.20 (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 $$; diff --git a/migrations/025_create_mcp_calls_and_budgets.sql b/migrations/025_create_mcp_calls_and_budgets.sql new file mode 100644 index 0000000..56fc7d4 --- /dev/null +++ b/migrations/025_create_mcp_calls_and_budgets.sql @@ -0,0 +1,204 @@ +-- ============================================================================= +-- Migration 025: mcp_calls + ai_call_budgets +-- Operation Ollama-First v5.0 — Phase 1 (Phase 10 MCP / Phase 9 預算告警 預備) +-- 日期: 2026-05-03 台北 +-- ============================================================================= +-- 說明: +-- mcp_calls — Phase 10 引入 5 個 MCP server 後的遙測表,Schema 先到位。 +-- 與 ai_calls 分表,因 MCP 沒有 token 概念、計費邏輯不同。 +-- ai_call_budgets — Phase 9 預算告警表;種子資料即立即可用。 +-- +-- 設計決策: +-- 1. mcp_calls.insight_id 不加 FK:避免 cascade(Phase 11 ai_insights 會頻繁 archive) +-- 改用「軟連結」+ 應用層 join,保留可被 NULL 化的彈性。 +-- 2. ai_call_budgets.provider NULL = 全供應商總額(UNIQUE constraint 用 (period, provider) +-- 若 NULL 行為不一致需保護,由應用層強制單例) +-- 註: PostgreSQL 預設 NULL != NULL,所以同 period 多筆 provider=NULL 會通過 UNIQUE, +-- 需應用層自律或改用部分索引(見下方)。 +-- +-- 回滾腳本: +-- DROP INDEX IF EXISTS idx_mcp_calls_called_at; +-- DROP INDEX IF EXISTS idx_mcp_calls_caller_called_at; +-- DROP INDEX IF EXISTS idx_mcp_calls_server_tool; +-- DROP INDEX IF EXISTS idx_mcp_calls_status_called_at; +-- DROP INDEX IF EXISTS uq_ai_call_budgets_period_null_provider; +-- DROP TABLE IF EXISTS mcp_calls; +-- DROP TABLE IF EXISTS ai_call_budgets; +-- ============================================================================= + +-- ───────────────────────────────────────────────────────────────────────────── +-- mcp_calls — MCP Server 呼叫遙測(Phase 10 預備) +-- ───────────────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS mcp_calls ( + id BIGSERIAL PRIMARY KEY, + called_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- 與 ai_calls.caller 同一張白名單,便於跨表 trace + caller VARCHAR(64) NOT NULL, + + -- omnisearch / firecrawl / postgres / playwright / filesystem / git + server VARCHAR(64) NOT NULL, + + -- search / scrape / query / read_file / git_log / ... + tool VARCHAR(128) NOT NULL, + + input_args JSONB, + output_size INTEGER, -- bytes,異常巨大可警示 + duration_ms INTEGER, + -- M1: NOT NULL 對齊 ai_calls + status VARCHAR(16) NOT NULL, + error TEXT, + cost_usd NUMERIC(10,6) NOT NULL DEFAULT 0, + cache_hit BOOLEAN NOT NULL DEFAULT FALSE, + + -- M6: 跨 ai_calls/mcp_calls 串鏈用(Phase 10 後 LLM→MCP→LLM 鏈不可斷) + request_id VARCHAR(64), + + -- 軟連結:若 MCP 結果被 embed 寫入 ai_insights,記錄 insight_id 但不加 FK + insight_id BIGINT, + + -- ─────── critic-A11 修補:白名單 + PII/膨脹護欄 ─────── + -- M1: status 白名單 + CONSTRAINT chk_mcp_calls_status CHECK ( + status IN ('ok','error','timeout','rate_limited','cache_only') + ), + -- L3: duration 範圍 + CONSTRAINT chk_mcp_calls_duration_range CHECK ( + duration_ms IS NULL OR (duration_ms >= 0 AND duration_ms <= 600000) + ), + -- H2: input_args / error 大小護欄(postgres-mcp 可能含 SQL,含 PII 風險) + CONSTRAINT chk_mcp_calls_args_size CHECK ( + input_args IS NULL OR octet_length(input_args::text) <= 16384 + ), + CONSTRAINT chk_mcp_calls_error_size CHECK ( + error IS NULL OR octet_length(error) <= 4096 + ) +); + +CREATE INDEX IF NOT EXISTS idx_mcp_calls_called_at + ON mcp_calls (called_at DESC); + +CREATE INDEX IF NOT EXISTS idx_mcp_calls_caller_called_at + ON mcp_calls (caller, called_at DESC); + +CREATE INDEX IF NOT EXISTS idx_mcp_calls_server_tool + ON mcp_calls (server, tool, called_at DESC); + +-- M5: 異常監控 partial 精確列舉 +CREATE INDEX IF NOT EXISTS idx_mcp_calls_status_called_at + ON mcp_calls (status, called_at DESC) + WHERE status IN ('error','timeout','rate_limited'); + +-- M6: request_id 串鏈(部分索引,sparse 不全建) +CREATE INDEX IF NOT EXISTS idx_mcp_calls_request_id + ON mcp_calls (request_id) + WHERE request_id IS NOT NULL; + +GRANT ALL PRIVILEGES ON mcp_calls TO momo; +GRANT USAGE, SELECT ON SEQUENCE mcp_calls_id_seq TO momo; + +-- ───────────────────────────────────────────────────────────────────────────── +-- ai_call_budgets — 預算與告警閾值(Phase 9 預算守門) +-- ───────────────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS ai_call_budgets ( + id SERIAL PRIMARY KEY, + period VARCHAR(16) NOT NULL, -- daily / weekly / monthly + provider VARCHAR(32), -- NULL = 全供應商總額 + budget_usd NUMERIC(10,2) NOT NULL, + alert_pct INTEGER NOT NULL DEFAULT 80, -- 達此百分比觸發 Telegram 告警 + updated_at TIMESTAMPTZ DEFAULT NOW(), + + CONSTRAINT chk_ai_budget_period + CHECK (period IN ('daily', 'weekly', 'monthly')), + CONSTRAINT chk_ai_budget_alert_pct + CHECK (alert_pct BETWEEN 1 AND 100), + CONSTRAINT chk_ai_budget_amount + CHECK (budget_usd > 0) +); + +-- 部分唯一索引:分別處理 provider IS NULL 與 NOT NULL,避免 NULL != NULL 漏洞 +CREATE UNIQUE INDEX IF NOT EXISTS uq_ai_call_budgets_period_provider + ON ai_call_budgets (period, provider) + WHERE provider IS NOT NULL; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_ai_call_budgets_period_null_provider + ON ai_call_budgets (period) + WHERE provider IS NULL; + +GRANT ALL PRIVILEGES ON ai_call_budgets TO momo; +GRANT USAGE, SELECT ON SEQUENCE ai_call_budgets_id_seq TO momo; + +-- ───────────────────────────────────────────────────────────────────────────── +-- 種子資料(戰役 v5.0 規格 + critic-A11 H3 補 nim/nim_via_elephant/ollama) +-- M2: ON CONFLICT 配 partial unique index 會炸;改用 WHERE NOT EXISTS 確保冪等 +-- ───────────────────────────────────────────────────────────────────────────── + +-- 全供應商總額(period, provider=NULL) +INSERT INTO ai_call_budgets (period, provider, budget_usd, alert_pct) +SELECT 'daily', NULL, 1.00, 80 +WHERE NOT EXISTS ( + SELECT 1 FROM ai_call_budgets WHERE period='daily' AND provider IS NULL +); + +INSERT INTO ai_call_budgets (period, provider, budget_usd, alert_pct) +SELECT 'weekly', NULL, 5.00, 80 +WHERE NOT EXISTS ( + SELECT 1 FROM ai_call_budgets WHERE period='weekly' AND provider IS NULL +); + +INSERT INTO ai_call_budgets (period, provider, budget_usd, alert_pct) +SELECT 'monthly', NULL, 20.00, 80 +WHERE NOT EXISTS ( + SELECT 1 FROM ai_call_budgets WHERE period='monthly' AND provider IS NULL +); + +-- 個別供應商(含 H3 修補:補 nim / nim_via_elephant / ollama 雙線) +INSERT INTO ai_call_budgets (period, provider, budget_usd, alert_pct) +SELECT 'monthly', 'claude', 10.00, 80 +WHERE NOT EXISTS ( + SELECT 1 FROM ai_call_budgets WHERE period='monthly' AND provider='claude' +); + +INSERT INTO ai_call_budgets (period, provider, budget_usd, alert_pct) +SELECT 'monthly', 'gemini', 8.00, 80 +WHERE NOT EXISTS ( + SELECT 1 FROM ai_call_budgets WHERE period='monthly' AND provider='gemini' +); + +-- H3: NIM 兩條獨立計費鏈(NemoTron 配額 + ElephantAlpha 49B),各設預算 +INSERT INTO ai_call_budgets (period, provider, budget_usd, alert_pct) +SELECT 'monthly', 'nim', 5.00, 80 +WHERE NOT EXISTS ( + SELECT 1 FROM ai_call_budgets WHERE period='monthly' AND provider='nim' +); + +INSERT INTO ai_call_budgets (period, provider, budget_usd, alert_pct) +SELECT 'monthly', 'nim_via_elephant', 5.00, 80 +WHERE NOT EXISTS ( + SELECT 1 FROM ai_call_budgets WHERE period='monthly' AND provider='nim_via_elephant' +); + +-- H3: OpenRouter(PPT deepseek-v3.2) +INSERT INTO ai_call_budgets (period, provider, budget_usd, alert_pct) +SELECT 'monthly', 'openrouter', 3.00, 80 +WHERE NOT EXISTS ( + SELECT 1 FROM ai_call_budgets WHERE period='monthly' AND provider='openrouter' +); + +-- Ollama 雙線(免費,但設極低預算 + alert=100% 統一告警邏輯,異常激增可警示) +INSERT INTO ai_call_budgets (period, provider, budget_usd, alert_pct) +SELECT 'monthly', 'gcp_ollama', 0.01, 100 +WHERE NOT EXISTS ( + SELECT 1 FROM ai_call_budgets WHERE period='monthly' AND provider='gcp_ollama' +); + +INSERT INTO ai_call_budgets (period, provider, budget_usd, alert_pct) +SELECT 'monthly', 'ollama_111', 0.01, 100 +WHERE NOT EXISTS ( + SELECT 1 FROM ai_call_budgets WHERE period='monthly' AND provider='ollama_111' +); + +DO $$ +BEGIN + RAISE NOTICE 'Migration 025 done: mcp_calls + ai_call_budgets + 10 seed budgets (Operation Ollama-First v5.0 P1, critic-A11 fixes B2/H1/H2/H3/M1/M2/M5/M6/L3 applied)'; +END $$; diff --git a/migrations/026_add_embedding_signature.sql b/migrations/026_add_embedding_signature.sql new file mode 100644 index 0000000..77e4ad5 --- /dev/null +++ b/migrations/026_add_embedding_signature.sql @@ -0,0 +1,66 @@ +-- ============================================================================= +-- Migration 026: ai_insights.embedding_signature — BGE-M3 一致性護欄 +-- Operation Ollama-First v5.0 — Phase 1 / 護欄 #3 +-- 日期: 2026-05-03 台北 +-- 對應: docs/phase0_audit_report_20260503.md Section 3 BGE-M3 一致性現況報告 +-- ============================================================================= +-- 風險背景: +-- bge-m3:latest 為 floating tag,Ollama upgrade 會悄悄跳版本,且程式未顯式 +-- 傳遞 normalize / pooling 參數。RAG 召回率會無告警地退化。 +-- +-- 護欄設計: +-- 每筆 ai_insights.embedding 寫入時,同步記錄 signature: +-- SHA1("{model}|{normalize}|{dim}|{ollama_digest_前12碼}") 取前 12 碼 +-- 範例: bge-m3:latest|true|1024|7907646426 → SHA1 → e3b0c44298fc +-- +-- Phase 11 啟動前,先批次補齊既有資料: +-- UPDATE ai_insights +-- SET embedding_signature = '' +-- WHERE embedding IS NOT NULL AND embedding_signature IS NULL; +-- 並由 ai_calls.meta.embedding_signature 與 ai_insights.embedding_signature +-- 做 cross-check(簽名漂移時觸發 Telegram 告警)。 +-- +-- ALTER TABLE 安全性: +-- PostgreSQL 11+ 新增 NULL 預設值欄位為 metadata-only 變更(不重寫表,不鎖表)。 +-- 生產環境 (PostgreSQL 14) 確認安全。 +-- +-- 回滾腳本: +-- DROP INDEX IF EXISTS idx_ai_insights_embedding_signature; +-- ALTER TABLE ai_insights DROP COLUMN IF EXISTS embedding_signature; +-- +-- critic-A11 修補(B2): +-- pgcrypto extension 由本 migration 啟用;附錄 SHA1 範例不再缺前置條件。 +-- ============================================================================= + +-- (0) critic-A11 B2 修補:pgcrypto 用於附錄 SHA1 簽名計算(IF NOT EXISTS 冪等) +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +-- (1) 新增欄位(無 DEFAULT,metadata-only,不鎖表) +ALTER TABLE ai_insights + ADD COLUMN IF NOT EXISTS embedding_signature VARCHAR(64); + +COMMENT ON COLUMN ai_insights.embedding_signature IS + 'BGE-M3 一致性簽名:SHA1({model}|{normalize}|{dim}|{ollama_digest})[:12],' + 'Phase 11 RAG 召回前必檢查;NULL = 既有未回填資料(待批次補)'; + +-- (2) Partial index:只索引有 embedding 且簽名非空的列 +-- 用 CONCURRENTLY 避免阻塞既有 ai_insights 寫入 +-- 注意: CONCURRENTLY 不能在 transaction block 內執行;本 migration 採 PostgreSQL +-- psql 直接執行(無外層 BEGIN/COMMIT) +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_ai_insights_embedding_signature + ON ai_insights (embedding_signature) + WHERE embedding IS NOT NULL; + +-- 註: Phase 11 啟動前批次補簽名範例(不在本 migration 執行): +-- WITH sig AS ( +-- SELECT 'bge-m3:latest|true|1024|' AS raw +-- ) +-- UPDATE ai_insights +-- SET embedding_signature = SUBSTRING(ENCODE(DIGEST(sig.raw, 'sha1'), 'hex'), 1, 12) +-- FROM sig +-- WHERE embedding IS NOT NULL AND embedding_signature IS NULL; + +DO $$ +BEGIN + RAISE NOTICE 'Migration 026 done: ai_insights.embedding_signature + partial index (Operation Ollama-First v5.0 P1)'; +END $$;