Files
awoooi/docs/adr/ADR-044-openclaw-nemotron-collaboration.md
OG T dd526684ab
All checks were successful
E2E Health Check / e2e-health (push) Successful in 17s
feat(ai): Phase 22 OpenClaw + Nemotron 協作架構 (ADR-044)
統帥批准實作「仲裁-執行分工」架構:
- OpenClaw = 仲裁者 (Why + Risk Level)
- Nemotron = 執行者 (How + kubectl Command)

新增功能:
- config.py: ENABLE_NEMOTRON_COLLABORATION Feature Flag
- openclaw.py: generate_incident_proposal_with_tools()
- openclaw.py: _call_nemotron_tools() Nemotron 呼叫
- telegram_gateway.py: TelegramMessage Nemotron 欄位
- telegram_gateway.py: format_with_nemotron() 雙區塊格式
- decision_manager.py: 整合協作方法
- proposal_service.py: 整合協作方法

觸發條件:
- LOW 風險 → 僅 OpenClaw
- MEDIUM/HIGH/CRITICAL → OpenClaw + Nemotron 雙軌

首席架構師審查: 83/100 條件通過

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-31 18:52:53 +08:00

14 KiB
Raw Blame History

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

@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

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 卡片格式

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. 異步執行 (非阻塞)

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 策略

# 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),過多並發無益

並行執行優化

# 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 必修項

環境變數

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

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

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

回滾計畫

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)

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

修改後 ( DI 模式)

# 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 測試案例

# 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]

整合測試

@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