diff --git a/apps/api/src/services/decision_manager.py b/apps/api/src/services/decision_manager.py
index 584aa822..8a2e997f 100644
--- a/apps/api/src/services/decision_manager.py
+++ b/apps/api/src/services/decision_manager.py
@@ -240,6 +240,17 @@ async def _push_decision_to_telegram(
# 非同步執行,不阻塞主流程
asyncio.create_task(_send_log_summary(incident))
+ # MCP Phase 4a: NemoClaw second opinion (2026-04-11 Claude Sonnet 4.6)
+ # 若 proposal_data 有 advisory_note,用 NemoClaw bot 身分追加一條訊息
+ _advisory_note = proposal_data.get("advisory_note", "")
+ if _advisory_note:
+ asyncio.create_task(
+ gateway.send_as_nemotron(
+ f"🤔 NemoClaw 第二意見 (信心={confidence:.2f})\n"
+ f"{_advisory_note}"
+ )
+ )
+
# 🔴 發送成功後設置去重 key (TTL 10 分鐘)
await redis.setex(dedup_key, 600, "1")
@@ -259,6 +270,62 @@ async def _push_decision_to_telegram(
)
+async def _nemoclaw_second_opinion(incident: "Incident", primary_result: dict) -> str | None:
+ """
+ MCP Phase 4a: NemoClaw second opinion — 信心 < 0.7 時觸發
+ ============================================================
+ 用 deepseek-r1:14b (Ollama 188) 對同一份資料做獨立推理,
+ 輸出純文字 advisory_note,不執行任何操作。
+
+ 2026-04-11 Claude Sonnet 4.6 Asia/Taipei
+ """
+ try:
+ from src.core.config import settings
+ import httpx as _httpx
+
+ ollama_url = getattr(settings, "OLLAMA_URL", "http://192.168.0.188:11434")
+ model = "deepseek-r1:14b"
+
+ signals_summary = ""
+ if incident.signals:
+ lbl = incident.signals[0].labels
+ signals_summary = (
+ f"alertname={lbl.get('alertname','?')} "
+ f"severity={lbl.get('severity','?')} "
+ f"instance={lbl.get('instance','?')}"
+ )
+
+ prompt = (
+ f"你是資深 SRE,請對以下告警做獨立分析並提出建議。\n"
+ f"告警摘要: {signals_summary}\n"
+ f"受影響服務: {', '.join(incident.affected_services or [])}\n"
+ f"主 AI 已提出: {primary_result.get('action','?')} "
+ f"(信心={primary_result.get('confidence',0):.2f})\n"
+ f"主 AI 根因: {primary_result.get('reasoning','')[:200]}\n\n"
+ f"請用繁體中文,100 字以內,給出你的獨立判斷與建議(若同意主 AI 則說明原因)。"
+ )
+
+ async with _httpx.AsyncClient(timeout=30.0) as client:
+ resp = await client.post(
+ f"{ollama_url}/api/generate",
+ json={"model": model, "prompt": prompt, "stream": False},
+ )
+ resp.raise_for_status()
+ data = resp.json()
+
+ advisory = data.get("response", "").strip()
+ # 截取 ... 後的正文(deepseek-r1 CoT 格式)
+ if "" in advisory:
+ advisory = advisory.split("", 1)[-1].strip()
+
+ return advisory[:300] if advisory else None
+
+ except Exception as e:
+ import structlog as _sl
+ _sl.get_logger(__name__).debug("nemoclaw_second_opinion_error", error=str(e))
+ return None
+
+
async def _fetch_metrics_snapshot(incident: Incident) -> dict:
"""
ADR-071-I: 從 Prometheus 抓取與此 incident 相關的指標快照
@@ -993,10 +1060,25 @@ class DecisionManager:
provider=provider,
kb_rag=bool(kb_context),
)
- return {
- **llm_result,
- "source": f"llm_{provider}",
- }
+ result = {**llm_result, "source": f"llm_{provider}"}
+
+ # MCP Phase 4a: 信心 < 0.7 → NemoClaw second opinion (2026-04-11 Claude Sonnet 4.6)
+ _conf = float(result.get("confidence", 1.0))
+ if _conf < 0.7:
+ try:
+ _advisory = await _nemoclaw_second_opinion(incident, result)
+ if _advisory:
+ result["advisory_note"] = _advisory
+ logger.info(
+ "nemoclaw_second_opinion_added",
+ incident_id=incident.incident_id,
+ confidence=_conf,
+ )
+ except Exception as _soe:
+ logger.warning("nemoclaw_second_opinion_failed",
+ incident_id=incident.incident_id, error=str(_soe))
+
+ return result
except Exception as e:
logger.warning(