# ADR-044: OpenClaw + Nemotron 協作架構

> **狀態**: ✅ **已批准**
> **決策日期**: 2026-03-31
> **批准日期**: 2026-03-31 18:30 (台北時區)
> **決策者**: 首席架構師 + 統帥
> **提案者**: Claude Code
> **相關**: ADR-036 Nemotron Tool Calling, Phase 18 自動修復

## 背景

AWOOOI 目前有兩個 AI 能力：
1. **OpenClaw** - 主要大腦，負責 Root Cause Analysis、風險評估、決策推理
2. **Nemotron** - Tool Calling 專家，83.3% 精準度執行 K8s 操作

統帥需求：在同一個 Telegram 中同時看到兩者的分析結果。

## 問題陳述

如何讓兩個 AI 在 Telegram 中協作，而不會：
- 訊息混亂（誰說了什麼？）
- 責任不清（誰做的決策？）
- 無限迴圈（互相觸發）
- 增加過多延遲

## 決策

### 採用「仲裁-執行分工」架構

```
OpenClaw = 仲裁者 (Arbitrator) - 決定「為什麼」和「風險等級」
Nemotron = 執行者 (Executor) - 決定「怎麼做」和「具體指令」
```

### 職責分離

| 角色 | OpenClaw | Nemotron |
|------|----------|----------|
| **任務** | Root Cause Analysis | Tool Calling |
| **輸出** | 風險等級 + 責任團隊 + 原因推理 | kubectl 指令 + 參數驗證 |
| **模型** | Ollama/Gemini (RCA 任務) | Nemotron-mini (Tool 任務) |
| **信心度** | 0-100% (AI 分析品質) | 驗證狀態 (✅/❌) |
| **備援** | Expert System 規則 | Gemini Tool Calling |

### 流程設計

```
1. Incident 產生
      ↓
2. OpenClaw.generate_incident_proposal()
   → 輸出: risk_level, reasoning, primary_responsibility
      ↓
3. 判斷是否需要 Nemotron
   ├─ LOW 風險 → 跳過 Nemotron
   └─ MEDIUM/HIGH/CRITICAL → 呼叫 Nemotron
      ↓
4. NvidiaProvider.tool_call()
   → 輸出: tool_name, arguments, validation_status
      ↓
5. 組合結果 → 推送 Telegram 卡片
      ↓
6. 用戶簽核 → 執行
```

### 觸發條件

| 風險等級 | OpenClaw | Nemotron | 原因 |
|----------|----------|----------|------|
| LOW | ✅ | ❌ | 低風險操作不需要 Tool 驗證 |
| MEDIUM | ✅ | ✅ | 需要 Tool 驗證操作可行性 |
| HIGH | ✅ | ✅ | 高風險必須雙重驗證 |
| CRITICAL | ✅ | ✅ + HITL | 危險操作必須人工介入 |

## 實作規格

### 1. 擴展 TelegramMessage

```python
@dataclass
class TelegramMessage:
    # 現有欄位...

    # 新增 Nemotron 結果欄位
    nemotron_enabled: bool = False
    nemotron_tools: list[dict] | None = None  # Tool Calling 結果
    nemotron_validation: str = ""  # "✅ 驗證通過" / "❌ 驗證失敗"
    nemotron_latency_ms: float = 0.0
```

### 2. 擴展 generate_incident_proposal

```python
async def generate_incident_proposal_with_tools(
    self,
    incident_id: str,
    severity: str,
    signals: list[dict],
    affected_services: list[str],
) -> tuple[dict | None, str, bool]:
    """
    Phase 22: OpenClaw + Nemotron 協作

    Returns:
        (proposal_dict, provider, success)
        proposal_dict 新增:
        - nemotron_tools: Tool Calling 結果
        - nemotron_validation: 驗證狀態
    """
    # Step 1: OpenClaw 仲裁
    proposal, provider, success = await self.generate_incident_proposal(
        incident_id, severity, signals, affected_services
    )

    if not success:
        return proposal, provider, success

    # Step 2: 判斷是否需要 Nemotron
    risk_level = proposal.get("risk_level", "low").lower()
    if risk_level == "low":
        proposal["nemotron_enabled"] = False
        return proposal, provider, True

    # Step 3: Nemotron Tool Calling
    from src.services.nvidia_provider import get_nvidia_provider
    nvidia = get_nvidia_provider()

    tool_result = await nvidia.tool_call(
        messages=[{
            "role": "user",
            "content": f"""
根據以下分析，生成對應的 kubectl 操作：
- Incident: {incident_id}
- 原因: {proposal.get('reasoning', '')}
- 目標資源: {proposal.get('target_resource', '')}
- 建議操作: {proposal.get('action', '')}
"""
        }],
        tools=K8S_OPERATION_TOOLS,
    )

    # Step 4: 驗證 Tool Calling 結果
    validation = await self._validate_tool_calls(tool_result.tool_calls)

    proposal["nemotron_enabled"] = True
    proposal["nemotron_tools"] = [
        {"tool": tc.tool_name, "args": tc.arguments, "valid": tc.valid}
        for tc in tool_result.tool_calls
    ]
    proposal["nemotron_validation"] = validation
    proposal["nemotron_latency_ms"] = tool_result.latency_ms

    return proposal, provider, True
```

### 3. Telegram 卡片格式

```python
def format_with_nemotron(self) -> str:
    """格式化含 Nemotron 結果的訊息"""

    # OpenClaw 區塊
    openclaw_block = f"""
🤖 <b>OpenClaw 仲裁</b>
├ 📊 信心: {self.confidence_emoji} {self.confidence_pct}%
├ 👥 責任: {self.primary_responsibility}
└ 💡 原因: {self.root_cause[:50]}
"""

    # Nemotron 區塊 (如果啟用)
    nemotron_block = ""
    if self.nemotron_enabled and self.nemotron_tools:
        tools_str = "\n".join([
            f"  {'✅' if t['valid'] else '❌'} {t['tool']}: {t['args'][:30]}"
            for t in self.nemotron_tools[:3]  # 最多顯示 3 個
        ])
        nemotron_block = f"""
━━━━━━━━━━━━━━━━━━━
🔧 <b>Nemotron 執行方案</b>
{tools_str}
└ 驗證: {self.nemotron_validation}
"""

    return f"{openclaw_block}{nemotron_block}"
```

### 4. 異步執行 (非阻塞)

```python
async def _push_decision_to_telegram_async(
    incident: Incident,
    proposal_data: dict,
) -> None:
    """
    異步推送，不阻塞主流程

    Phase 22: 如果 Nemotron 延遲過長 (>10s)，先推送 OpenClaw 結果，
    Nemotron 結果後續用 edit_message 更新
    """
    # 先推送 OpenClaw 結果
    message_id = await gateway.send_approval_card(
        # ... OpenClaw 結果
    )

    # 如果需要 Nemotron，異步執行並更新
    if proposal_data.get("risk_level") in ["medium", "high", "critical"]:
        asyncio.create_task(
            _update_with_nemotron_result(message_id, incident, proposal_data)
        )
```

## 後果

### 正面

- **清晰分工**: OpenClaw 和 Nemotron 職責明確
- **可追蹤**: 每個 AI 的貢獻獨立顯示
- **容錯性**: 備援鏈清晰 (Nemotron → Gemini → Expert)
- **效能**: 低風險操作不觸發 Nemotron，節省延遲

### 負面

- **延遲增加**: 高風險操作需要兩輪 LLM
- **複雜度**: 訊息格式需要擴展

### 風險緩解

| 風險 | 緩解 |
|------|------|
| Nemotron 延遲 11-45s | 異步執行，先推送 OpenClaw 結果 |
| Tool Calling 失敗 | Fallback 到 Gemini，再失敗則只顯示 OpenClaw |
| 訊息超長 | 縮寫 Tool 參數，完整內容放 SignOz Link |

## 併發控制 (與 ADR-038 整合)

> **首席架構師 P1 必修項** (2026-03-31)

### 雙 Semaphore 策略

```python
# apps/api/src/core/circuit_breaker.py 擴展
class OpenClawGuard:
    def __init__(self):
        self.openclaw_semaphore = asyncio.Semaphore(3)   # 原有
        self.nemotron_semaphore = asyncio.Semaphore(2)   # 新增 (NVIDIA API 較慢)
```

**設計原因**:
- Nemotron 併發限制為 2 (低於 OpenClaw 的 3)
- NVIDIA NIM 免費 tier 有 RPM 限制
- Nemotron 延遲較高 (11-45s)，過多並發無益

### 並行執行優化

```python
# Step 3 優化: OpenClaw + Nemotron 並行而非串行
import asyncio

async def generate_incident_proposal_with_tools(...):
    # 並行啟動 OpenClaw 和 Nemotron (減少延遲)
    openclaw_task = asyncio.create_task(
        self.generate_incident_proposal(incident_id, severity, signals, affected_services)
    )

    # 先等待 OpenClaw 完成，判斷是否需要 Nemotron
    proposal, provider, success = await openclaw_task

    if not success or proposal.get("risk_level", "low").lower() == "low":
        return proposal, provider, success

    # 需要 Nemotron - 此時 OpenClaw 已完成，立即啟動 Nemotron
    nemotron_result = await self._call_nemotron_tools(proposal)

    # 組合結果
    return self._combine_results(proposal, nemotron_result), provider, True
```

**延遲對比**:

| 場景 | 串行 | 並行 | 改善 |
|------|------|------|------|
| MEDIUM 風險 | 3s + 15s = 18s | max(3s, 15s) = 15s | -3s |
| HIGH 風險 | 5s + 30s = 35s | max(5s, 30s) = 30s | -5s |

---

## Circuit Breaker 整合

### 雙層 Circuit Breaker 協調

```
┌─────────────────────────────────────────┐
│ OpenClawGuard (ADR-038)                  │
│ - 管理請求佇列                           │
│ - 長期熔斷 (5 分鐘)                      │
└─────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────┐
│ NvidiaProvider.CircuitBreaker           │
│ - NVIDIA API 短期熔斷 (60s)              │
│ - 失敗 3 次後 OPEN                       │
└─────────────────────────────────────────┘
```

### 熔斷策略

| 層級 | 觸發條件 | 恢復時間 | 影響 |
|------|---------|---------|------|
| OpenClawGuard | 佇列滿 (>10) | 5 分鐘 | 停止新請求 |
| NvidiaProvider | 連續 3 失敗 | 60 秒 | Fallback 到 Gemini |

---

## Feature Flag 支援

> **首席架構師 P1 必修項**

### 環境變數

```bash
# 啟用/停用 Nemotron 協作 (預設 true)
ENABLE_NEMOTRON_COLLABORATION=true

# Nemotron 呼叫超時 (預設 45s)
NEMOTRON_TIMEOUT_SECONDS=45

# 強制使用異步更新 (先推 OpenClaw，後更新 Nemotron)
NEMOTRON_ASYNC_UPDATE=true
```

### 回滾計畫

```python
async def generate_incident_proposal_with_tools(...):
    # Feature Flag 檢查
    if not settings.ENABLE_NEMOTRON_COLLABORATION:
        return await self.generate_incident_proposal(...)  # 原流程

    # ... 協作邏輯
```

**回滾步驟**:
1. 設置 `ENABLE_NEMOTRON_COLLABORATION=false`
2. Rollout restart awoooi-api
3. 無需代碼回滾

---

## DI 模式重構

> **首席架構師 P1 必修項** - 避免函數內 import

### 修改前 (❌ 違反 DI)

```python
# Step 3: Nemotron Tool Calling
from src.services.nvidia_provider import get_nvidia_provider  # ❌ 函數內 import
nvidia = get_nvidia_provider()
```

### 修改後 (✅ DI 模式)

```python
# apps/api/src/services/openclaw.py
from src.services.nvidia_provider import INvidiaProvider

class OpenClawService:
    def __init__(
        self,
        nvidia_provider: INvidiaProvider | None = None,  # DI 注入
    ):
        self._nvidia = nvidia_provider or get_nvidia_provider()

    async def generate_incident_proposal_with_tools(
        self,
        incident_id: str,
        severity: str,
        signals: list[dict],
        affected_services: list[str],
    ) -> tuple[dict | None, str, bool]:
        # ... 使用 self._nvidia 而非 import
```

---

## 測試策略

### E2E 測試案例

```python
# tests/test_openclaw_nemotron_collaboration.py

@pytest.mark.asyncio
async def test_low_risk_skips_nemotron():
    """LOW 風險不觸發 Nemotron"""
    result = await openclaw.generate_incident_proposal_with_tools(...)
    assert result[0]["nemotron_enabled"] is False

@pytest.mark.asyncio
async def test_medium_risk_enables_nemotron():
    """MEDIUM 風險啟用 Nemotron"""
    result = await openclaw.generate_incident_proposal_with_tools(...)
    assert result[0]["nemotron_enabled"] is True
    assert result[0]["nemotron_tools"] is not None

@pytest.mark.asyncio
async def test_nemotron_failure_fallback():
    """Nemotron 失敗時 fallback 到 Gemini"""
    # Mock NVIDIA 失敗
    with patch("nvidia_provider.tool_call", side_effect=Exception):
        result = await openclaw.generate_incident_proposal_with_tools(...)
        # 應該有結果 (來自 Gemini fallback)
        assert result[2] is True

@pytest.mark.asyncio
async def test_feature_flag_disabled():
    """Feature Flag 停用時走原流程"""
    with patch.dict(os.environ, {"ENABLE_NEMOTRON_COLLABORATION": "false"}):
        result = await openclaw.generate_incident_proposal_with_tools(...)
        assert "nemotron_enabled" not in result[0]
```

### 整合測試

```python
@pytest.mark.integration
async def test_telegram_message_with_nemotron():
    """Telegram 訊息包含 Nemotron 區塊"""
    msg = TelegramMessage(
        nemotron_enabled=True,
        nemotron_tools=[{"tool": "restart_deployment", "args": {...}, "valid": True}],
    )
    formatted = msg.format_with_nemotron()
    assert "Nemotron 執行方案" in formatted
    assert "✅ restart_deployment" in formatted
```

---

## 實作排程 (詳細)

| 階段 | 內容 | 時間 | 檔案 | 依賴 |
|------|------|------|------|------|
| **22.1** | TelegramMessage 擴展 | 2h | `telegram_gateway.py` | 無 |
| **22.2a** | OpenClawGuard 雙 Semaphore | 1h | `circuit_breaker.py` | 無 |
| **22.2b** | DI 模式重構 | 1h | `openclaw.py` | 22.2a |
| **22.2c** | `generate_incident_proposal_with_tools` | 2h | `openclaw.py` | 22.2a, 22.2b |
| **22.3a** | Feature Flag 支援 | 1h | `config.py` | 無 |
| **22.3b** | 異步推送邏輯 | 2h | `decision_manager.py` | 22.1, 22.2c |
| **22.4a** | 單元測試 | 2h | `test_openclaw_nemotron*.py` | 22.2c |
| **22.4b** | E2E 測試 | 2h | `test_e2e_collaboration.py` | 22.3b |
| **總計** | | **13h (~1.5 天)** | | |

---

## 首席架構師審查結論

> **審查日期**: 2026-03-31 (台北時區)
> **分數**: 83/100 → **條件通過**

### P1 必修項 (已補充)

| 編號 | 項目 | 狀態 |
|------|------|------|
| P1-1 | 併發控制整合 | ✅ 已補充 |
| P1-2 | DI 模式 | ✅ 已補充 |
| P1-3 | Feature Flag | ✅ 已補充 |

### P2 建議項 (後續迭代)

| 編號 | 項目 | 說明 |
|------|------|------|
| P2-1 | 並行優化 | 已納入設計 |
| P2-2 | Pydantic Model | Phase 22.5 |
| P2-3 | NemotronBlock | Phase 22.5 |

---

## 相關文件

- ADR-036: Nemotron Tool Calling 整合
- ADR-038: OpenClaw 併發治理
- Phase 18: 失敗自動修復閉環
- `feedback_ai_rate_limiter.md`: AI 用量控制

---

**Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>**
