跳至内容
AI 应用最小可观测性:没有日志就没有工程化

AI 应用最小可观测性:没有日志就没有工程化

July 9, 2026

传统服务挂了看状态码;AI 应用挂了常有人只说「今天变差了」。这不是模型神秘,是观测不够。

1. 每次调用至少留下这些字段

{
  "ts": "2026-07-18T14:02:11+08:00",
  "request_id": "9f3c...",
  "route": "chat",
  "model": "auto",
  "upstream": "openai/gpt-4o-mini",
  "latency_ms": 842,
  "ttft_ms": 210,
  "prompt_tokens": 312,
  "completion_tokens": 88,
  "cost_usd": 0.00012,
  "status": "ok",
  "cache_hit": false,
  "tool_calls": 0,
  "key_alias": "web-prod"
}

有了它们才能回答:变慢?变贵?谁限流?工具没触发?

2. 最小中间件(Python)

# llm_observe.py
from __future__ import annotations
import json, time, uuid, logging
from dataclasses import asdict, dataclass
from typing import Any
from openai import OpenAI

log = logging.getLogger("llm")
logging.basicConfig(level=logging.INFO, format="%(message)s")

@dataclass
class CallMetrics:
    request_id: str
    model: str
    upstream: str
    latency_ms: int
    prompt_tokens: int
    completion_tokens: int
    cost_usd: float
    status: str
    error: str | None = None

PRICE_IN = {"gpt-4o-mini": 0.15 / 1_000_000}
PRICE_OUT = {"gpt-4o-mini": 0.60 / 1_000_000}

def estimate(model: str, pt: int, ct: int) -> float:
    return pt * PRICE_IN.get(model, 0) + ct * PRICE_OUT.get(model, 0)

def chat(client: OpenAI, model: str, messages: list[dict[str, Any]], **kw) -> str:
    rid = str(uuid.uuid4())
    t0 = time.perf_counter()
    try:
        r = client.chat.completions.create(model=model, messages=messages, **kw)
        pt = r.usage.prompt_tokens if r.usage else 0
        ct = r.usage.completion_tokens if r.usage else 0
        m = CallMetrics(
            request_id=rid,
            model=model,
            upstream=getattr(r, "model", model) or model,
            latency_ms=int((time.perf_counter() - t0) * 1000),
            prompt_tokens=pt,
            completion_tokens=ct,
            cost_usd=estimate("gpt-4o-mini", pt, ct),
            status="ok",
        )
        log.info(json.dumps(asdict(m), ensure_ascii=False))
        return r.choices[0].message.content or ""
    except Exception as e:  # noqa: BLE001
        m = CallMetrics(
            request_id=rid,
            model=model,
            upstream="unknown",
            latency_ms=int((time.perf_counter() - t0) * 1000),
            prompt_tokens=0,
            completion_tokens=0,
            cost_usd=0.0,
            status="error",
            error=str(e)[:300],
        )
        log.info(json.dumps(asdict(m), ensure_ascii=False))
        raise

网关侧更理想:所有客户端自动继承同一套字段,不必每个语言重写。

3. 分层看数,别只看平均值

# 假设日志一行一个 JSON
# 今日错误率
grep '"ts":"2026-07-18' llm.log | grep -c '"status":"error"'

# 按 upstream 聚合延迟(粗暴版)
grep '"ts":"2026-07-18' llm.log \
  | jq -r 'select(.status=="ok") | [.upstream, .latency_ms] | @tsv' \
  | awk '{s[$1]+=$2; n[$1]++} END{for(k in s) printf "%s avg=%.0fms n=%d\n",k,s[k]/n[k],n[k]}'

更有用的是 p95、按功能拆成本、工具调用成功率。同一「成功率 99%」可能掩盖某个关键路由 20% 失败。

4. 排障手册

现象先看字段常见原因
全体变慢latency_ms / upstream上游抖动或自己超时设太大
偶发失败status / error429、连接重置
会说话不干活tool_calls兼容层未拼装 / 模型不支持 tools
账单飙升cost_usd / key_alias脚本循环、用错大模型
「和昨天不一样」model / upstreamfallback 静默切到备选

5. 隐私红线

  • 默认不落完整用户原文;必要则脱敏 + TTL
  • 禁止记录 Authorization、Cookie、Provider Key
  • 生产与调试日志分流

6. 案例:p95 从 1.2s 飙到 8s

日志显示 upstream 大量变成备选小模型,且 status=ok——不是挂了,是主模型进入冷却后 fallback。产品体感「变笨变慢」。处理:

  1. 面板告警:fallback 比例 > 10%
  2. 对用户展示「降级中」而不是假装一切正常
  3. 查主上游 429 原因(配额/并发)

没有 upstream 字段时,这场讨论会变成玄学。

7. 清单

  • request_id 贯穿客户端 → 网关 → 上游
  • 结构化日志(JSON)
  • 成本字段按请求估算
  • fallback / cache_hit 可观测
  • 一页排障表

AI 工程化第一课往往不是更炫的 Agent,而是:你能否稳定解释上一次失败为什么发生。