Chapter 11

可观测与日志:黑盒里点灯

LLM 本身是黑盒,你唯一能观察的就是"进去什么、出来什么、多慢、多贵、错没错"。把这 5 件事打得透,事故发生前你已经提前知道,事故发生时 5 分钟定位,事故之后可复盘可改进。

要观测什么

LLM 产品必须观测的 5 类信号:

类别典型指标回答什么问题
ThroughputRPS, TPM, 并发承接住了吗?要不要扩容?
LatencyTTFT, p50/p95/p99用户等太久了吗?
Errorrate by code, fallback count哪家 provider 在挂?
Cost$/h, $/request, 命中率钱烧对了吗?
Qualitythumbs up, eval 分, hallucination答得好吗?

前 4 类是传统 SRE + LLM 特色,Prometheus + 日志 + 数据库能解决;第 5 类是 LLM 独有的——answer quality 只有 Langfuse / Arize 这类 LLM 专用平台能做好

Langfuse:开源 LLM 观测首选

Langfuse 是目前最主流的开源 LLM 观测平台,LiteLLM 官方一等公民。一行配置直接接:

import os, litellm

os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-xxxx"
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-xxxx"
os.environ["LANGFUSE_HOST"]       = "https://cloud.langfuse.com"

litellm.success_callback = ["langfuse"]
litellm.failure_callback = ["langfuse"]

completion(
    model="gpt-4o",
    messages=[{"role": "user", "content": "你好"}],
    user="alice",
    metadata={
        "trace_id": "order-12345",
        "session_id": "chat-abc",
        "tags": ["support", "prod"],
    },
)

打开 Langfuse UI,你就有了:

自建 Langfuse:MIT 协议,一条 Helm 就能在自己 K8s 跑起来,敏感数据不出公司。本站 langfuse 教程讲了完整部署。

Proxy 里开 Langfuse

如果用 Proxy 部署,callback 写在 config.yaml 里一次,所有业务自动上报:

litellm_settings:
  success_callback: ["langfuse"]
  failure_callback: ["langfuse"]

  # 细粒度配置
  langfuse_default_tags: ["env:prod", "service:chat"]

general_settings:
  environment_variables:
    LANGFUSE_PUBLIC_KEY: os.environ/LANGFUSE_PK
    LANGFUSE_SECRET_KEY: os.environ/LANGFUSE_SK
    LANGFUSE_HOST: https://langfuse.company.internal

业务调用的时候,把 user / trace_id / session_id / tags 塞进请求 metadata 即可,Proxy 自动转发给 Langfuse。

trace / session / generation 三层

Langfuse 的数据模型层级清晰:

Session(用户一次对话 / 一次任务)
  └─ Trace(业务一次完整请求, e.g. "帮用户下单")
      ├─ Span (业务子步骤: 解析用户意图)
      ├─ Generation (LLM 调用 1: 分类)     ← LiteLLM 自动记录
      ├─ Generation (LLM 调用 2: 回答)     ← LiteLLM 自动记录
      └─ Span (业务子步骤: 写入订单)

从业务代码传 trace_id / session_id,让所有 LLM 调用归到同一个 trace 下,调查时一条链路看完:

trace_id = f"order-{order_id}"

# LLM 调用 1: 意图分类
cls = completion(model="gpt-4o-mini", messages=[...],
                 metadata={"trace_id": trace_id, "generation_name": "intent_classify"})

# LLM 调用 2: 提取参数
args = completion(model="gpt-4o", messages=[...],
                  metadata={"trace_id": trace_id, "generation_name": "extract_slots"})

# LLM 调用 3: 回复用户
reply = completion(model="claude-sonnet", messages=[...],
                   metadata={"trace_id": trace_id, "generation_name": "reply"})

Prometheus:数值指标

Proxy 自带 Prometheus exporter(/metrics),无痛接入 Grafana:

litellm_settings:
  callbacks: ["prometheus"]

打开 http://localhost:4000/metrics,核心指标包括:

指标含义PromQL 示例
litellm_requests_metric请求数 counterrate(litellm_requests_metric[5m])
litellm_total_tokenstoken 总量sum(rate(litellm_total_tokens[5m])) by (model)
litellm_spend_metric花费 counterrate(litellm_spend_metric[1h]) * 3600 = $/h
litellm_llm_api_latency_metric调用延迟直方图histogram_quantile(0.95, ...)
litellm_deployment_cooldowndeployment 是否 cooldown挂 provider 检测
litellm_proxy_total_requests按 status code 的请求数error rate

一张 Grafana 板子上放五件套:RPS、成本速率、p95 延迟、错误率、命中率。每个图配一条红线,超阈值告警。

OpenTelemetry:跨系统 tracing

当 LLM 只是业务链路里的一环——用户 → API Gateway → 业务服务 → LiteLLM → OpenAI——你想在一张 trace 图上看完所有 span。这是 OTel 的强项:

litellm_settings:
  callbacks: ["otel"]

general_settings:
  environment_variables:
    OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector.observability:4318
    OTEL_SERVICE_NAME: litellm-proxy
    OTEL_RESOURCE_ATTRIBUTES: deployment.environment=prod

注意关键点:业务服务发请求时,要把 W3C traceparent header 传给 Proxy,Proxy 会把这个 trace_id 接下去。很多 HTTP 客户端自带 OTel instrumentation,加 3 行代码即可。

from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
HTTPXClientInstrumentor().instrument()

# 之后所有 httpx 请求自动注入 traceparent, Proxy 会延续同一个 trace
import openai
client = openai.OpenAI(base_url="http://litellm-proxy/v1", api_key="sk-...")
client.chat.completions.create(model="gpt-4o", messages=msgs)

这样在 Jaeger / Grafana Tempo 里看到的是:Gateway → API → LiteLLM → OpenAI 上游,一条链完整端到端

其他 callback 插件

LiteLLM 内置 20+ 个 callback:

插件类型适用
langfuseLLM 观测开源 / 自托管,最主流
heliconeLLM 观测SaaS,easy onboarding
arizeLLM + ML eval企业级,强 eval
lunaryLLM 观测轻量,UI 漂亮
galileoLLM 观测 + eval企业
datadogAPM已有 DD 账号
prometheusmetrics自建 SRE 栈
oteldistributed tracing跨服务
s3archive合规留存
sentryerror tracking异常专项
slackalerting告警通知

组合是常见做法:["langfuse", "prometheus", "s3"]——观测 + 指标 + 归档全覆盖。

自定义 callback

内置插件不够用,或你有自建系统,直接写 Python 类继承 CustomLogger:

from litellm.integrations.custom_logger import CustomLogger
import litellm

class MyKafkaLogger(CustomLogger):
    def log_success_event(self, kwargs, resp, t0, t1):
        producer.send("llm_logs", {
            "model": resp.model,
            "user": kwargs.get("user"),
            "cost": resp._hidden_params.get("response_cost", 0),
            "latency_ms": (t1 - t0).total_seconds() * 1000,
            "prompt_tokens": resp.usage.prompt_tokens,
            "completion_tokens": resp.usage.completion_tokens,
            "trace_id": kwargs.get("metadata", {}).get("trace_id"),
        })

    def log_failure_event(self, kwargs, exc, t0, t1):
        producer.send("llm_errors", {
            "model": kwargs.get("model"),
            "error": str(exc),
            "user": kwargs.get("user"),
        })

    async def async_log_success_event(self, kwargs, resp, t0, t1):
        # 异步版本, 推荐实现这个避免阻塞
        await async_producer.send("llm_logs", {...})

litellm.callbacks = [MyKafkaLogger()]
优先实现 async_log_*LiteLLM 默认异步调 callback,同步实现会跑在线程池里——IO 多的话线程池打满,主调用跟着卡。生产级永远用 async。

告警:Slack + PagerDuty

Proxy 内置 Slack alerting,任何异常状况推 webhook:

general_settings:
  alerting: ["slack", "pagerduty"]
  alerting_threshold: 60        # 请求慢于 60s 告警

  alert_types:
    - "llm_exceptions"                # provider 异常
    - "llm_too_slow"                  # 慢请求
    - "llm_requests_hanging"          # 卡住请求
    - "budget_alerts"                 # 预算 80%/100%
    - "db_exceptions"                 # DB 连不上
    - "daily_reports"                 # 每日汇报
    - "cooldown_deployment"           # 某 provider 被熔断
    - "new_model_added"               # 配置变更

environment_variables:
  SLACK_WEBHOOK_URL: https://hooks.slack.com/services/...

建议分级:

SLO:把"好不好"写成数字

没 SLO 的服务永远在救火。一个健康的 LLM 服务 SLO 通常是:

SLISLO 目标Error Budget(30 天)
可用性(2xx 响应率)99.5%3.6 小时
TTFT p95< 2.5s约 36 小时
完整延迟 p95< 10s约 36 小时
Error rate< 0.5%同上
# 可用性 SLI
sum(rate(litellm_proxy_total_requests{status=~"2.."}[5m])) /
sum(rate(litellm_proxy_total_requests[5m]))

# p95 延迟
histogram_quantile(0.95,
  sum(rate(litellm_llm_api_latency_metric_bucket[5m])) by (le))

Error budget 烧完就停止非必要发布,把工程资源砸到 reliability 上。这是 Google SRE 的玩法,在 LLM 场景照样管用。

质量监控:LLM 独有的一块

传统服务"结果是否正确"靠业务逻辑判断;LLM 答得好不好,只能靠:

  1. 用户反馈:每条回答带 👍👎 按钮,埋点写进 trace score。Langfuse 原生支持。
  2. LLM-as-a-judge:另一个(更便宜/更强的)LLM 评估回答是否符合要求,定时跑。
  3. 规则校验:JSON 结构合法性、有没有输出 PII、长度合理、有没有幻觉关键词。
  4. 离线 eval:金标测试集,发版前跑一遍,分数掉了卡发布。
# 用户反馈打分示例 (Langfuse)
from langfuse import Langfuse
lf = Langfuse()

# 用户点了 👍
lf.score(
    trace_id="order-12345",
    name="user_feedback",
    value=1,              # 1 = 赞, -1 = 踩
    comment="回答准确",
)

# LLM-as-a-judge 打分示例
judge = completion(
    model="gpt-4o",
    messages=[{"role":"user","content":f"问:{q}\n答:{a}\n打分 1-5,只输出数字"}],
)
lf.score(trace_id=trace_id, name="llm_judge", value=int(judge.choices[0].message.content))

日志脱敏

observability 平台里留存 prompt/response 有风险:里面可能有用户私信、手机号、合同全文。两个做法:

# 方案 1: 关闭记录内容 (只留 metadata)
litellm_settings:
  turn_off_message_logging: true     # Langfuse/Helicone 只记 token 数和 cost
# 方案 2: PII 脱敏后再上报
import re

class PIIMaskingLogger(CustomLogger):
    async def async_log_success_event(self, kwargs, resp, t0, t1):
        msgs = kwargs.get("messages", [])
        for m in msgs:
            m["content"] = re.sub(r"1[3-9]\d{9}", "[PHONE]", m["content"])
            m["content"] = re.sub(r"\d{17}[\dXx]", "[ID]", m["content"])
        await upstream.send({"messages": msgs, ...})

生产环境强烈推荐"部分 trace 全文 + 大部分 metadata"的混合策略:抽样 1% 全文留存用于定位问题,99% 只记 metadata。

常见坑位

  1. 所有业务一个 trace_id:Langfuse 里全粘一起,没法查。每次业务请求一个新 trace_id,多轮用 session_id。
  2. Langfuse 连不通时阻塞主流程:Langfuse SDK 是异步 flush 的,但如果你自己写 callback 同步调,挂了全家跟着挂。callback 永远 async + 超时
  3. Prometheus 指标 cardinality 爆炸:把 user_id、trace_id 当 label 塞进 metric,Prometheus 内存爆。高基数字段只进日志,别进 metric label
  4. 日志里打了 API key:很多库默认把整个 headers 日志化。LiteLLM 自带脱敏,但自定义 callback 要自己 mask
  5. 忘了对 Proxy 做 traceparent 透传:otel 打开了但链路断在 Proxy 前。检查客户端有没有 OTel instrument。
  6. 告警没分级:PagerDuty 半夜叫起床,结果是"某 provider 慢了 1 秒"。严格分级,人睡觉的级别只留"服务完全不可用"。
  7. 只看均值延迟:均值 2s,p99 可能 30s。LLM 延迟必须看 p95/p99,绝不能只看 mean。
  8. 不跟 eval 集合跑:prompt 改了以为只是"小优化",上线后幻觉率翻倍。发版前跑 eval,分数跌就卡住。

本章小结