要观测什么
LLM 产品必须观测的 5 类信号:
| 类别 | 典型指标 | 回答什么问题 |
|---|---|---|
| Throughput | RPS, TPM, 并发 | 承接住了吗?要不要扩容? |
| Latency | TTFT, p50/p95/p99 | 用户等太久了吗? |
| Error | rate by code, fallback count | 哪家 provider 在挂? |
| Cost | $/h, $/request, 命中率 | 钱烧对了吗? |
| Quality | thumbs 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,你就有了:
- 按 user / session / trace / tag 任意筛查
- 每条请求的 prompt + response 全文(带 token / cost / 延迟)
- 多轮对话用 session_id 自动串成时间线
- Evaluation:对历史请求批量跑 eval(LLM-as-a-judge),看新 prompt 对不对
- prompt management:把 prompt 当代码版本化管理
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 | 请求数 counter | rate(litellm_requests_metric[5m]) |
litellm_total_tokens | token 总量 | sum(rate(litellm_total_tokens[5m])) by (model) |
litellm_spend_metric | 花费 counter | rate(litellm_spend_metric[1h]) * 3600 = $/h |
litellm_llm_api_latency_metric | 调用延迟直方图 | histogram_quantile(0.95, ...) |
litellm_deployment_cooldown | deployment 是否 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:
| 插件 | 类型 | 适用 |
|---|---|---|
| langfuse | LLM 观测 | 开源 / 自托管,最主流 |
| helicone | LLM 观测 | SaaS,easy onboarding |
| arize | LLM + ML eval | 企业级,强 eval |
| lunary | LLM 观测 | 轻量,UI 漂亮 |
| galileo | LLM 观测 + eval | 企业 |
| datadog | APM | 已有 DD 账号 |
| prometheus | metrics | 自建 SRE 栈 |
| otel | distributed tracing | 跨服务 |
| s3 | archive | 合规留存 |
| sentry | error tracking | 异常专项 |
| slack | alerting | 告警通知 |
组合是常见做法:["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/...
建议分级:
- Slack #llm-prod:cooldown、budget 80%、每日汇报——只看不动手。
- Slack #llm-oncall:error rate > 5%、p95 > 10s——值班看。
- PagerDuty:Proxy 完全挂了、所有 provider 全 cooldown、budget 100%——叫人起床。
SLO:把"好不好"写成数字
没 SLO 的服务永远在救火。一个健康的 LLM 服务 SLO 通常是:
| SLI | SLO 目标 | 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 答得好不好,只能靠:
- 用户反馈:每条回答带 👍👎 按钮,埋点写进 trace
score。Langfuse 原生支持。 - LLM-as-a-judge:另一个(更便宜/更强的)LLM 评估回答是否符合要求,定时跑。
- 规则校验:JSON 结构合法性、有没有输出 PII、长度合理、有没有幻觉关键词。
- 离线 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。
常见坑位
- 所有业务一个 trace_id:Langfuse 里全粘一起,没法查。每次业务请求一个新 trace_id,多轮用 session_id。
- Langfuse 连不通时阻塞主流程:Langfuse SDK 是异步 flush 的,但如果你自己写 callback 同步调,挂了全家跟着挂。callback 永远 async + 超时。
- Prometheus 指标 cardinality 爆炸:把 user_id、trace_id 当 label 塞进 metric,Prometheus 内存爆。高基数字段只进日志,别进 metric label。
- 日志里打了 API key:很多库默认把整个 headers 日志化。LiteLLM 自带脱敏,但自定义 callback 要自己 mask。
- 忘了对 Proxy 做 traceparent 透传:otel 打开了但链路断在 Proxy 前。检查客户端有没有 OTel instrument。
- 告警没分级:PagerDuty 半夜叫起床,结果是"某 provider 慢了 1 秒"。严格分级,人睡觉的级别只留"服务完全不可用"。
- 只看均值延迟:均值 2s,p99 可能 30s。LLM 延迟必须看 p95/p99,绝不能只看 mean。
- 不跟 eval 集合跑:prompt 改了以为只是"小优化",上线后幻觉率翻倍。发版前跑 eval,分数跌就卡住。
本章小结
- LLM 要观测 5 类:throughput / latency / error / cost / quality,前 4 类靠 SRE 栈,第 5 类靠 LLM 专用平台
- Langfuse 是 LLM 观测首选,开源自托管,trace/session/generation 三层结构
- Prometheus 出数值指标、OTel 出分布式 trace,自定义 callback 补缺
- 告警分三级:Slack 看板 / Slack oncall / PagerDuty 叫人,按严重度写死
- SLO 四条底线:可用性、TTFT、完整延迟、error rate,error budget 耗尽停发布
- 质量监控是 LLM 独有:用户反馈 + LLM-judge + 规则校验 + 金标 eval 四路并用
- 敏感场景用
turn_off_message_logging或自定义 PII mask,1% 全文抽样是常见混合方案