凌晨 3 点被 AI 应用告警吵醒,我才意识到自己根本没在监控它

0 阅读6分钟

凌晨三点。手机震得跟拖拉机一样。

打开看是 PagerDuty——一行字:"error_count > threshold"。再看用户群,已经炸了,几十条消息全在说 "AI 怎么变这么蠢"、"答非所问"、"再这样要退订"。

我懵了大概 20 秒:到底是哪个模型出问题了?是 某海外大模型厂商 还是国内 fallback?是 prompt 改坏了还是上游延迟飙了?

打开 Grafana 一看——我整个 LLM 应用就一个指标:error_count。还是那种只记 HTTP 5xx 的 error_count。

那一刻我就意识到,我把这套 AI 服务上线半年,但从监控角度看,我对它的"健康状况"几乎是瞎子。

我为什么之前没好好做监控

懒。说真的就是懒。

LLM 应用刚上线那会儿我心里想的是"先把功能跑起来,监控以后再说"。HTTP 服务的监控套路我闭眼都能写——latency + error rate + qps,结束。我以为 LLM 也是这套,反正最后都返回 HTTP。

但实际跑半年发现完全不是。HTTP 200 在 LLM 这套体系里几乎不传递任何信息

举几个我当时根本没意识到的故障:

  • 某天 某海外大模型厂商 悄悄改了 gpt-4o 的 tokenizer,我们的输入字数没变,但 token 数多了 40%。账单到月底才发现,多烧了七千块。
  • prompt template 改了一行,模型开始返回空字符串。HTTP 200,下游解析挂了,业务系统看到的是"AI 不响应"。
  • 某模型升级版本变保守了,对一类 query 全开始 refusal。HTTP 200,但用户体验稀烂。

所有这些故障,我那个 error_count 指标都报告"全绿"。

把监控分了 4 层,每层 30 行代码

被那次凌晨告警吓到之后,我花了两个周末把监控重新梳理。原则就一条:简单到不想做也能做完

最后落地是 4 层,每层基本 30 行 Python。从来不写复杂的,因为复杂的就坚持不下来。

L1 - API 层:知道 LLM 接口本身好不好

最基础那层。每次 LLM 调用记三个数:哪个 provider、哪个 model、status 是啥、延迟多少。

@monitor_llm_call(provider="openai", model="gpt-4o")
def call_openai(messages): ...

实现就一个 decorator,30 行。

效果:Grafana 一开就能看到 "openai 的 p99 延迟在 18:00 翻了 3 倍" 这种东西。之前我得 SSH 进服务器 grep 日志才能看到。

踩坑:一开始我只记了 status 没记 provider 和 model。结果一报警,看到"3% 错误率",但不知道是哪个模型——还得回去查日志。后来加了这两个 label,定位时间从半小时变成 30 秒。

L2 - 内容质量层:HTTP 200 不代表能用

这层是我教训最深的一层。之前完全没有,加上之后立刻抓到 3 个隐形故障。

核心就是:模型返回了,但你得检查这玩意儿能不能用。

def check_output_quality(model, output, expect_json=False):
    if not output or len(output.strip()) < 5:
        CONTENT_QUALITY.labels(model, "empty").inc()
        return
    if any(m in output[:200] for m in ["I can't help", "我无法", "抱歉"]):
        CONTENT_QUALITY.labels(model, "refusal").inc()
        return
    if expect_json:
        try: json.loads(output)
        except: CONTENT_QUALITY.labels(model, "json_parse_fail").inc(); return
    CONTENT_QUALITY.labels(model, "ok").inc()

加上当周抓到的故障:

  1. 某次 prompt 升级后 json_parse_fail 从 0.3% 飙到 12%——我忘了在 API 调用里带 response_format
  2. 某模型版本变严了,refusal 占比 0.1% → 4%——切到 fallback
  3. 用户输入异常导致 empty 占比变高——发现是 prompt 拼接漏了空白字符

这层我一周抓到的"看不见的故障"比之前半年还多。

L3 - 成本层:账单不会告警

成本漂移是 LLM 应用最阴险的故障类型。它从来不会让服务挂掉,但月底账单会让你想哭。

需要记的就是 token 用量按 model + 类型(prompt / completion / cached)切片。

TOKEN_USAGE.labels(model, "prompt").inc(input_tokens - cached_tokens)
TOKEN_USAGE.labels(model, "cached").inc(cached_tokens)
TOKEN_USAGE.labels(model, "completion").inc(output_tokens)

告警规则不是"超过阈值",而是"同时段对比涨幅":

  • 今天 14:00 累计 token 比昨天涨 25% → 告警
  • prompt:completion 比例从 3:1 变 6:1 → prompt 在变长,告警
  • cached 比例从 60% 掉到 20% → prompt prefix 被改坏,cache 击穿

最后一个特别关键。改 prompt template 的时候我经常不小心动到可缓存的前缀。靠 cached ratio 这个指标,10 分钟内就能发现"我刚才搞砸了"。

L4 - 业务结果层:AI 到底有没有真的帮到用户

前 3 层都是技术指标。L4 是问:"用户实际有没有从这次对话里拿到他想要的"。

这层最难做,因为信号不是 LLM 给你的,是用户的行为给你的:

  • 发完最后一句 5 分钟没再说话 → completed
  • 主动转人工 → escalated
  • 连发两条相似 query → 重试
  • 中途关闭对话 → abandoned

L4 抓到过一次让我手心冒汗的事故:我们换了个便宜模型,L1-L3 完全没异常,看起来一切美好。但 escalated 占比从 4% 涨到 19%——也就是说用户被这个便宜模型回答得很糟糕,纷纷转人工。我们盯着账单笑了三天,直到运营组提醒"这周客服工单怎么涨这么多"。立刻回滚,省的那点钱全赔进客服成本了。

4 层之间的优先级

理想情况下,问题应该在最低层就被抓到——L1 抓到的是技术故障,L4 抓到的几乎都是产品故障,往往是前 3 层漏抓了什么。

如果你只能加一层,加 L2。我后来复盘过,线上 LLM 故障里至少有一半是"HTTP 200 但内容坏"——这种东西 L1 看不到,但 L2 一抓一个准。

用了哪些东西

工具:Prometheus + Grafana,老掉牙但稳定。

LLM 调用层:所有模型走 TheRouter 做 vendor 路由,前 3 层指标在 router 这边统一采集,业务服务几乎不用埋点。这是改造里最爽的一步——之前每个业务自己埋 metrics,标签都不统一,根本聚合不出来。

prompt 改动验证:我现在改 prompt 之前会先在 ChatBotApp 上手测几个边界 case。这个习惯救过我好几次——比线上跑出来发现问题再回滚便宜得多。

最后一句话

如果你的 LLM 应用还在"只监控 HTTP 状态码"的阶段,今晚就花一小时加 L2。不需要复杂工具,30 行 Python 一个 decorator——明天你就会知道线上到底有多少"看着是 200,其实是垃圾"的输出在悄悄发给用户。

那感觉跟开了灯的房间一样。