migrations 024/025/026 — 統一 LLM 遙測 + 預算告警 + RAG 一致性護欄 - 024: ai_calls 表 + 5 索引 + 6 CHECK constraint(H1/H2/M3/L3) - 025: mcp_calls + ai_call_budgets + 10 種子預算(含 ollama_secondary) - 026: ai_insights.embedding_signature + pgcrypto + CONCURRENTLY index A11 critic 三輪審查記錄完整保留: - Phase 1 schema review: 2 BLOCKER + 4 HIGH + 6 MEDIUM 全處理 - Phase 1 final sign-off: 0 BLOCKER + 2 HIGH + 4 MEDIUM - Phase 6 ADR review: 5 BLOCKER + 6 HIGH 全修 Operation Ollama-First v5.0 / Phase 0+1+6 護欄 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
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
- 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 跑保留任務時加:
(格式約束而非完整白名單,避免每次擴 caller 都改 schema)
ALTER TABLE ai_calls ADD CONSTRAINT chk_ai_calls_caller_known CHECK (caller ~ '^[a-z][a-z0-9_]{2,63}$') NOT VALID; - 嚴重度判定:本來 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 同步):
或乾脆改成
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:6834ollama_service.generate(...)回傳OllamaResponse,但OllamaResponse沒有prompt_eval_count/eval_count欄位(services/ollama_service.py:88-95dataclass 只有 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 時順便):
並在
@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:737nim_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:130report_html[: _TELEGRAM_MAX_CHARS - 80]services/telegram_templates.py:566body[: _DAILY_TOKEN_REPORT_MAX_CHARS - 80]
- 風險:若截斷剛好落在
<b>...</b>之間(例如卡在<b後 1 字元),Telegram sendMessage 會回400 can't parse entities→ 整則訊息 fail,scheduler log 出現 telegram_send error - 緩解:實務上 4000 字截斷觸發時,落在 HTML tag 中間機率 < 5%(tag 密度低),但仍是已知 corner case
- 建議修法:截斷後跑
re.sub(r'<[^>]*$', '', 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 加:
'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:195COALESCE(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:30from 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 順手修補:
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)。順序:
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 啟動前):
-- 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_maintoken=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 刪
Decimaldead 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)