# ADR-017: LLMOps Observability

> **狀態**: ✅ 已批准
> **日期**: 2026-03-26
> **決策者**: 統帥
> **Phase**: 15

## 背景

AWOOOI 作為 AI-First 產品，LLM 呼叫是核心業務流程。然而，在 Phase 15 之前存在以下觀測盲點：

### 問題一：LLM 呼叫無追蹤

- Prompt 內容無紀錄，無法重現問題
- Token 消耗無統計，成本失控風險
- 模型 Fallback 鏈路不可見
- AI 幻覺無法追溯來源

### 問題二：Trace Context 斷鏈

- Redis Streams 不會自動傳遞 OTEL Trace ID
- Worker 處理的 LLM 呼叫與原始請求脫節
- 無法從前端錯誤追溯到 AI 決策過程

### 問題三：觀測系統孤島

- Sentry (前端錯誤) 與 SignOz (後端 Traces) 無法互連
- 需要在三個系統間手動搜尋同一個 Trace
- 故障排查效率低下

---

## 決策

採用**三層觀測架構 (The Holy Trinity)**，實現零斷鏈觀測：

```
┌─────────────────────────────────────────────────────────────┐
│                    AWOOOI 觀測分層架構                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Layer 3: AI 決策層       ← Langfuse (100% 取樣)           │
│   ┌─────────────────────────────────────────────────────┐   │
│   │ Prompt 追蹤 │ Token 成本 │ Agent 軌跡 │ 幻覺偵測    │   │
│   └─────────────────────────────────────────────────────┘   │
│                           ▲                                 │
│                    ⚠️ Redis Streams                         │
│                    (手動 Context Injection)                 │
│                           │                                 │
│   Layer 2: 基礎設施層     ← SignOz (10% 取樣, Error 100%)   │
│   ┌─────────────────────────────────────────────────────┐   │
│   │ API 延遲 │ K8s 資源 │ DB 查詢 │ 後端錯誤            │   │
│   └─────────────────────────────────────────────────────┘   │
│                           ▲                                 │
│                    (HTTP Header 自動傳遞)                   │
│                           │                                 │
│   Layer 1: 前端用戶層     ← Sentry (10% 取樣, Error 100%)  │
│   ┌─────────────────────────────────────────────────────┐   │
│   │ JS 錯誤 │ 用戶操作錄影 │ 效能指標 │ 崩潰堆疊        │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

### 核心元件

| 元件 | 部署位置 | 用途 |
|------|---------|------|
| Langfuse | 192.168.0.110:3100 | LLM 呼叫追蹤、Token 成本 |
| SignOz | 192.168.0.188:3301 | API Traces、Metrics、Logs |
| Sentry | 192.168.0.110:9000 | 前端錯誤、Session Replay |

---

## 技術實作

### 1. Langfuse Client (`apps/api/src/services/langfuse_client.py`)

專用 LLM 追蹤 Client，提供：

```python
# Context Manager 追蹤 LLM 呼叫鏈
from src.services.langfuse_client import langfuse_trace

async with langfuse_trace("openclaw_decision") as trace:
    # 記錄 LLM generation
    trace.generation(
        name="ollama_call",
        model="qwen2.5:7b-instruct",
        input=prompt,
        output=result,
        usage={"input": 500, "output": 200},
    )

    # 記錄評分 (用於 Prompt 品質追蹤)
    trace.score("response_quality", value=0.95)
```

**關鍵功能**：
- `LangfuseTraceContext`: 自動整合 OTEL trace_id
- `langfuse_trace()`: Context Manager 包裝
- `langfuse_observe()`: 裝飾器自動追蹤函數
- 自動注入 SignOz Deep Link 到 metadata

### 2. Deep Linking (`apps/api/src/core/deep_linking.py`)

三系統互連 URL 生成器：

```python
from src.core.deep_linking import DeepLinking

# 取得所有可用 Deep Links
links = DeepLinking.get_all_links(
    otel_trace_id="abc123...",
    langfuse_trace_id="lf-xyz...",
    sentry_issue_id="12345",
)
# {
#     "signoz_trace": "http://192.168.0.188:3301/trace/abc123...",
#     "langfuse_trace": "http://192.168.0.110:3100/project/awoooi-openclaw/traces/lf-xyz...",
#     "sentry_issue": "http://192.168.0.110:9000/organizations/sentry/issues/12345/",
# }
```

**URL 格式**：

| 系統 | URL 格式 |
|------|---------|
| SignOz Trace | `http://192.168.0.188:3301/trace/{trace_id}` |
| Langfuse Trace | `http://192.168.0.110:3100/project/awoooi-openclaw/traces/{id}` |
| Sentry Issue | `http://192.168.0.110:9000/organizations/sentry/issues/{id}/` |

### 3. Redis Trace Context 傳遞

解決 Redis Streams 斷鏈問題：

```python
# Producer (webhooks.py) - 注入 Trace Context
async def publish_to_redis(signal: Signal):
    from src.core.telemetry import get_trace_context

    trace_ctx = get_trace_context()  # {"trace_id": "xxx", "span_id": "yyy"}
    payload = {
        **signal.dict(),
        "_trace_id": trace_ctx["trace_id"],
        "_span_id": trace_ctx["span_id"],
    }
    await redis.xadd("stream:signals", payload)

# Consumer (signal_worker.py) - 還原 Trace Context
async def consume_from_redis():
    from src.core.telemetry import restore_trace_context

    message = await redis.xreadgroup(...)
    trace_id = message.pop("_trace_id", None)
    span_id = message.pop("_span_id", None)

    # 重建 OTEL Context (W3C traceparent 格式)
    with restore_trace_context(trace_id, span_id):
        # 處理邏輯，Langfuse 會繼承此 trace_id
        await process_signal(message)
```

### 4. 連結方向圖

```
Sentry Error
    │ tags.otel_trace_id
    │ contexts.signoz.trace_url
    ▼
SignOz Trace
    │ span.langfuse.trace_id
    │ span.langfuse.trace_url
    ▼
Langfuse Generation
    │ metadata.otel_trace_id
    │ metadata.signoz_trace_url
    ▲
    └── 雙向連結完成
```

---

## 部署配置

### Langfuse Self-Hosted (192.168.0.110:3100)

| 項目 | 值 |
|------|-----|
| 主機 | 192.168.0.110 (DevOps 金庫) |
| Port | 3100 |
| 映像 | langfuse/langfuse:2 |
| 資料庫 | PostgreSQL 15 (內建容器) |
| 部署層級 | 容器層 (Docker Compose) |

### 環境變數

```yaml
# K8s awoooi-secrets
LANGFUSE_ENABLED: "true"
LANGFUSE_URL: "http://192.168.0.110:3100"
LANGFUSE_PUBLIC_KEY: "pk-lf-..."
LANGFUSE_SECRET_KEY: "sk-lf-..."
```

### 專案配置

| 項目 | 值 |
|------|-----|
| 專案名稱 | awoooi-openclaw |
| 管理帳號 | admin@awoooi.local |

---

## 取樣率策略

### 成本控制原則

| 工具 | 正常流量 | Error 發生時 | 原因 |
|------|---------|-------------|------|
| Sentry | 10% | 100% | 前端流量大 |
| SignOz | 10% | 100% | API 流量大 |
| Langfuse | **100%** | 100% | AI 決策必須完整記錄 |

### 動態取樣配置

```python
# Sentry traces_sampler
def traces_sampler(sampling_context):
    if sampling_context.get("error"):
        return 1.0  # Error 必定記錄
    return 0.1      # 正常流量 10%
```

---

## 成本估算

| 項目 | 月費 | 說明 |
|------|------|------|
| Langfuse | $0 | Self-hosted |
| 額外儲存 | ~$5 | PostgreSQL Volume |
| **總計** | **~$5/月** | 幾乎免費 |

---

## 後果

### 優點

- **完整追溯**: 從前端錯誤追溯到 AI 決策
- **成本可見**: Token 消耗即時監控
- **一鍵跳轉**: 三系統 Deep Linking
- **Prompt 版本化**: 支援 A/B 測試

### 缺點

- **額外延遲**: Langfuse 寫入 ~5ms
- **儲存成本**: 100% 取樣累積資料快

### 風險

- **Redis Streams 斷鏈**: 需手動維護 Context Injection
- **Langfuse 故障**: LLM 呼叫仍正常，僅失去追蹤

---

## 相關檔案

| 檔案 | 說明 |
|------|------|
| `apps/api/src/services/langfuse_client.py` | Langfuse Client 包裝 |
| `apps/api/src/core/deep_linking.py` | Deep Linking URL 生成器 |
| `apps/api/src/core/telemetry.py` | Trace Context 工具函數 |
| `apps/api/src/core/config.py` | LANGFUSE_* 設定 |

---

## 參考

- [Langfuse Documentation](https://langfuse.com/docs)
- [OpenTelemetry Trace Context](https://www.w3.org/TR/trace-context/)
- Memory: `project_phase15_llmops_observability.md`
- Memory: `project_phase15_langfuse.md`
