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 / error | 429、连接重置 |
| 会说话不干活 | tool_calls | 兼容层未拼装 / 模型不支持 tools |
| 账单飙升 | cost_usd / key_alias | 脚本循环、用错大模型 |
| 「和昨天不一样」 | model / upstream | fallback 静默切到备选 |
5. 隐私红线
- 默认不落完整用户原文;必要则脱敏 + TTL
- 禁止记录
Authorization、Cookie、Provider Key - 生产与调试日志分流
6. 案例:p95 从 1.2s 飙到 8s
日志显示 upstream 大量变成备选小模型,且 status=ok——不是挂了,是主模型进入冷却后 fallback。产品体感「变笨变慢」。处理:
- 面板告警:fallback 比例 > 10%
- 对用户展示「降级中」而不是假装一切正常
- 查主上游 429 原因(配额/并发)
没有 upstream 字段时,这场讨论会变成玄学。
7. 清单
- request_id 贯穿客户端 → 网关 → 上游
- 结构化日志(JSON)
- 成本字段按请求估算
- fallback / cache_hit 可观测
- 一页排障表
AI 工程化第一课往往不是更炫的 Agent,而是:你能否稳定解释上一次失败为什么发生。