大模型评估中的线上漂移监控工程实践:从样本回流、分桶指标看板到告警阈值校准的可复现方案

线上评估和离线评测,很多团队都做过。但系统上线一段时间后,效果为什么慢慢变差,往往说不清。问题通常不在模型参数本身,而在输入分布变了、用户提问变了、知识库内容变了,甚至标注口径也变了。

我这篇文章讲一个可复现的做法:把线上漂移监控拆成样本回流、分桶分析、告警校准三部分,然后用一套可以直接落地的数据表和脚本把它跑起来。说实话,真正上线后我才发现,很多“模型退化”最后查出来其实是业务流量结构变了。

短句先说结论:没有分桶,漂移基本看不准。


1. 线上漂移监控到底在监控什么

大模型应用里的“漂移”不是一个指标,它至少有两层:

  • 数据漂移:输入问题、上下文、召回文档、用户群体发生变化
  • 效果漂移:模型输出质量、任务成功率、人工评分出现下滑

工程里如果只看一个总分,很容易误判。比如总准确率从 82.4% 掉到 80.9%,看着不大,但如果把流量按查询长度切开,可能是长问题桶从 78% 掉到 66%。这就不是“小波动”了。

所以监控目标要落到三件事上:

  • 线上样本能不能稳定回流
  • 指标能不能按业务维度切开看
  • 告警能不能少误报,又别漏报

这三件事缺一块,系统都不稳。


2. 整体方案:数据回流 + 分桶看板 + 双层告警

我用过的稳定方案如下:

  1. 线上日志埋点:记录请求、上下文、召回结果、模型输出、用户反馈
  2. 样本回流任务:按时间窗口抽样,生成待评估样本集
  3. 自动打分与人工补标:规则评分、模型裁判、人工抽检混合执行
  4. 分桶指标看板:按 query 类型、长度、用户端、业务场景拆开
  5. 告警阈值校准:静态阈值 + 基线波动阈值一起用

不要一上来就做复杂。先把最小闭环跑通。

下面按工程实现展开。


3. 样本回流:先保证线上数据能被评估

很多监控失败,不是算法问题,而是日志压根不够。线上最少要回流这些字段:

字段 说明
request_id 请求唯一 ID
user_query 用户输入
query_type 查询类型,分类器或规则生成
context_docs 召回上下文摘要或文档 ID 列表
prompt_version 使用的 Prompt 版本
model_name 模型名
model_output 模型输出
latency_ms 时延
token_in/token_out Token 消耗
user_feedback 点赞、点踩、追问、复制等反馈
biz_tag 业务标记,如客服、营销、问答
dt 日期分区

3.1 埋点建议

我一般把埋点拆成两层:

  • 在线请求日志:高覆盖,少字段,保证性能
  • 评估样本明细表:异步汇总,保留完整上下文

如果把所有上下文都同步写主日志,请求高峰时会很难受。异步落库更稳。

3.2 样本回流策略

回流不能只做随机抽样。随机抽样经常把高频简单问题抽满,长尾场景几乎看不到。比较实用的方案是:

  • 高频桶按固定比例抽样,比如 1%
  • 中频桶按更高比例抽样,比如 5%
  • 低频桶全量回流
  • 告警桶强制全量保留 24 小时

一个简单的抽样配置可以写成 YAML:

sampling:
  default_rate: 0.01
  by_query_type:
    faq: 0.005
    retrieval_qa: 0.03
    tool_call: 0.05
    long_context: 0.1
  force_keep_conditions:
    - user_feedback == 'thumb_down'
    - latency_ms > 8000
    - output_parse_success == false

这里我建议把“点踩样本”“解析失败样本”“超时样本”强制保留。样本量不大,信息量很高。

3.3 回流表设计

下面给一个简化版 Hive 表结构:

CREATE TABLE llm_online_eval_samples (
  request_id STRING,
  user_id STRING,
  biz_tag STRING,
  query_type STRING,
  query_length INT,
  context_doc_ids ARRAY<STRING>,
  retrieved_doc_count INT,
  prompt_version STRING,
  model_name STRING,
  model_output STRING,
  output_parse_success BOOLEAN,
  latency_ms BIGINT,
  token_in INT,
  token_out INT,
  user_feedback STRING,
  auto_score DOUBLE,
  judge_score DOUBLE,
  human_score DOUBLE,
  sample_source STRING,
  created_at TIMESTAMP
)
PARTITIONED BY (dt STRING);

字段不要怕多。后面做归因分析时,会庆幸当初多留了几个。


4. 评估打分:自动打分和人工抽检怎么配

线上漂移监控的难点,是没法像传统分类任务那样随时拿到标准答案。所以我常用混合评估:

  • 规则打分:结构化输出是否合法、是否命中关键词、是否触发安全规则
  • 模型裁判打分:看回答是否相关、是否完整、是否引用了上下文
  • 人工抽检:校准模型裁判偏差,补足高风险样本

4.1 规则打分示例

结构化输出场景很好做,直接看 schema:

from pydantic import BaseModel, ValidationError

class AnswerSchema(BaseModel):
    answer: str
    confidence: float
    cited_doc_ids: list[str]


def score_parse_and_citation(output: dict, retrieved_doc_ids: list[str]):
    try:
        obj = AnswerSchema(**output)
        parse_score = 1.0
    except ValidationError:
        return {
            "parse_score": 0.0,
            "citation_hit_rate": 0.0,
            "final_rule_score": 0.0,
        }

    cited = set(obj.cited_doc_ids)
    retrieved = set(retrieved_doc_ids)
    hit_rate = len(cited & retrieved) / max(len(cited), 1)

    final_score = 0.7 * parse_score + 0.3 * hit_rate
    return {
        "parse_score": parse_score,
        "citation_hit_rate": hit_rate,
        "final_rule_score": round(final_score, 4),
    }

这类分数很粗,但跑得快,适合全量监控。

4.2 模型裁判提示词

模型裁判不要写得太玄。我的做法很直接:给任务定义、给评分标准、给上下文、给待评估答案,只输出 JSON。

你是一个线上问答质检器。
请根据“用户问题、检索上下文、模型回答”给出评分。

评分维度:
- relevance: 是否回答了用户问题,0-5分
- faithfulness: 是否与给定上下文一致,0-5分
- completeness: 是否覆盖关键要点,0-5分

返回格式:
{
  "relevance": 0,
  "faithfulness": 0,
  "completeness": 0,
  "comment": "不超过50字"
}

短一点,反而稳。

4.3 人工抽检比例

人工抽检我通常这样分:

  • 总样本里抽 2% 做日常校准
  • 告警桶额外抽 20%
  • 新 Prompt 版本上线后,前两天把新版本样本抽检提到 10%

这个比例在多数业务里能承受。真要全量人工,成本很快就上去了。


5. 分桶指标看板:不要只盯总分

线上漂移最容易漏掉的点,就是总指标正常,但局部已经掉得很厉害。所以看板一定要按桶拆开。

我常用的分桶维度有这些:

  • query_type:FAQ、检索问答、工具调用、多轮追问
  • query_length:0-20、21-100、100+
  • biz_tag:不同业务线
  • model_name / prompt_version:版本对比
  • user_segment:新用户、老用户、付费用户
  • context_size:召回文档数、上下文 token 长度

5.1 指标建议

一个够用的看板,至少放这些指标:

指标 说明
sample_count 样本量
positive_feedback_rate 点赞率
negative_feedback_rate 点踩率
parse_success_rate 结构化解析成功率
judge_avg_score 裁判平均分
p95_latency_ms P95 时延
avg_token_cost 平均 token 消耗
retrieval_hit_rate 引用命中率或召回命中代理指标

这里有个经验:先看样本量,再看分数。 某个桶昨天只有 17 条样本,分数从 4.3 掉到 3.1,不一定值得立刻拉群。

5.2 分桶聚合 SQL

SELECT
  dt,
  biz_tag,
  query_type,
  prompt_version,
  COUNT(*) AS sample_count,
  AVG(CASE WHEN user_feedback = 'thumb_up' THEN 1 ELSE 0 END) AS positive_feedback_rate,
  AVG(CASE WHEN user_feedback = 'thumb_down' THEN 1 ELSE 0 END) AS negative_feedback_rate,
  AVG(CASE WHEN output_parse_success THEN 1 ELSE 0 END) AS parse_success_rate,
  AVG(judge_score) AS judge_avg_score,
  APPROX_PERCENTILE(latency_ms, 0.95) AS p95_latency_ms,
  AVG(token_in + token_out) AS avg_token_cost
FROM llm_online_eval_samples
WHERE dt BETWEEN '2026-04-10' AND '2026-04-16'
GROUP BY dt, biz_tag, query_type, prompt_version;

看板层建议保留日粒度和小时粒度。很多漂移不是一天掉下来的,而是某个版本在 10:00 发布后开始抖。


6. 漂移检测:别只设一个固定阈值

一开始我也用过“分数低于 3.8 就告警”这种简单规则,结果误报很多。工作日和周末的流量结构不同,白天和夜里也不同,用一个死阈值很容易把值班同学搞烦。

更稳的做法,是绝对阈值 + 相对波动阈值一起用。

6.1 绝对阈值

适合看硬性质量底线,比如:

  • parse_success_rate < 0.95
  • negative_feedback_rate > 0.08
  • p95_latency_ms > 6000

这类指标很直观,出问题就得查。

6.2 相对波动阈值

适合看趋势变化,比如和过去 7 天基线比:

  • judge_avg_score 下降超过 12%
  • thumb_down 率上升超过 30%
  • 某 query_type 桶样本占比变化超过 25%

这里不能只比昨天。最好用移动窗口。

6.3 一个可复现的告警判定脚本

from dataclasses import dataclass

@dataclass
class AlertRule:
    metric: str
    abs_low: float | None = None
    abs_high: float | None = None
    rel_drop_ratio: float | None = None
    rel_rise_ratio: float | None = None
    min_sample_count: int = 100


def should_alert(current_value, baseline_value, sample_count, rule: AlertRule):
    if sample_count < rule.min_sample_count:
        return False, "sample_not_enough"

    if rule.abs_low is not None and current_value < rule.abs_low:
        return True, "abs_low_breach"

    if rule.abs_high is not None and current_value > rule.abs_high:
        return True, "abs_high_breach"

    if baseline_value is not None and baseline_value != 0:
        if rule.rel_drop_ratio is not None:
            drop = (baseline_value - current_value) / baseline_value
            if drop > rule.rel_drop_ratio:
                return True, "relative_drop_breach"

        if rule.rel_rise_ratio is not None:
            rise = (current_value - baseline_value) / baseline_value
            if rise > rule.rel_rise_ratio:
                return True, "relative_rise_breach"

    return False, "ok"


rule = AlertRule(metric="judge_avg_score", abs_low=3.8, rel_drop_ratio=0.12, min_sample_count=200)
print(should_alert(3.6, 4.2, 320, rule))

6.4 阈值校准方法

阈值不是拍脑袋定的。我的做法是拿过去 30 天数据回放,统计:

  • 若按这个阈值告警,会触发多少次
  • 其中人工确认的真实异常有多少次
  • 漏掉的异常有多少次

再按业务承受能力调节。比如客服问答系统误报多一点还能接受,但风控辅助场景就不能乱报。

一个简单办法是做告警回放表:

rule_name alert_count true_positive false_positive precision
judge_score_drop_12pct 14 9 5 64.3%
thumb_down_rise_30pct 11 8 3 72.7%
parse_success_below_95 6 6 0 100%

看到这里,一般就知道阈值该往哪边调了。


7. 漂移归因:告警后别停在“分数掉了”

真正有用的监控,不是发条消息说“指标异常”,而是顺手给出排查入口。常见归因方向有几类:

  • 新 Prompt 版本发布
  • 模型切换或参数调整
  • 检索结果分布变化
  • 流量结构变化,某类问题暴增
  • 外部工具超时,导致回答退化

7.1 我常用的归因字段

在看板或明细里把这些字段放出来,排查会快很多:

  • prompt_version
  • model_name
  • retrieval_index_version
  • topk_doc_score_mean
  • tool_name / tool_success_rate
  • query_rewrite_text
  • user_region / client_type

少一个字段,定位时间可能就多半小时。

7.2 一个简单归因查询

SELECT
  prompt_version,
  model_name,
  retrieval_index_version,
  COUNT(*) AS cnt,
  AVG(judge_score) AS avg_score,
  AVG(CASE WHEN user_feedback = 'thumb_down' THEN 1 ELSE 0 END) AS down_rate
FROM llm_online_eval_samples
WHERE dt = '2026-04-16'
  AND biz_tag = 'customer_service'
  AND query_type = 'retrieval_qa'
GROUP BY prompt_version, model_name, retrieval_index_version
ORDER BY down_rate DESC, avg_score ASC;

很多时候,这一条 SQL 就能先把嫌疑范围缩一半。


8. 一个简化版看板实现

如果你暂时没有完整的监控平台,也能先用 Python + Pandas 跑一个日报。

import pandas as pd


def build_bucket_report(df: pd.DataFrame):
    grouped = df.groupby(["dt", "biz_tag", "query_type", "prompt_version"]).agg(
        sample_count=("request_id", "count"),
        judge_avg_score=("judge_score", "mean"),
        negative_feedback_rate=("user_feedback", lambda x: (x == "thumb_down").mean()),
        parse_success_rate=("output_parse_success", "mean"),
        p95_latency_ms=("latency_ms", lambda x: x.quantile(0.95)),
    ).reset_index()
    return grouped


def compare_with_baseline(today_df: pd.DataFrame, baseline_df: pd.DataFrame):
    merged = today_df.merge(
        baseline_df,
        on=["biz_tag", "query_type", "prompt_version"],
        suffixes=("_today", "_base")
    )
    merged["score_drop_ratio"] = (
        (merged["judge_avg_score_base"] - merged["judge_avg_score_today"]) /
        merged["judge_avg_score_base"].clip(lower=1e-6)
    )
    return merged.sort_values("score_drop_ratio", ascending=False)

日报里我一般只放两类表:

  • 今日异常桶 Top N
  • 本周波动最大的桶 Top N

这样看的人不会被一堆数字淹没。


9. 实测中的一组对比

下面给一组我在问答场景里用过的监控方式对比,数据做了脱敏,但趋势是真实的。

方案 A:只看总平均分

  • 日均样本:12 万
  • 指标:总 judge_avg_score
  • 告警准确率:41%
  • 平均定位耗时:2.6 小时

方案 B:按 query_type 和 prompt_version 分桶

  • 日均样本:12 万
  • 指标:总分 + 分桶分数 + 点踩率
  • 告警准确率:68%
  • 平均定位耗时:58 分钟

方案 C:再加入样本占比变化和解析成功率

  • 日均样本:12 万
  • 指标:总分 + 分桶分数 + 点踩率 + 流量占比变化 + parse_success_rate
  • 告警准确率:79%
  • 平均定位耗时:31 分钟

没想到的是,最后拉开差距的不是更复杂的裁判模型,而是把桶切对了,再把异常样本快速捞出来给人看。


10. 落地时容易踩的坑

10.1 样本回流延迟太高

如果样本要隔 6 小时才能进入评估表,很多线上问题已经过去了。建议核心指标控制在 15 分钟到 30 分钟内更新。

10.2 只看反馈,不看无反馈样本

很多业务的点赞点踩率很低,只盯反馈类样本会偏得很厉害。无反馈样本仍然要抽样进评估集。

10.3 分桶过细

桶切得太细,每桶样本量不足,图上到处都是“暴涨暴跌”。我一般先从 5 到 8 个主桶开始,稳定后再细分。

10.4 模型裁判口径漂移

如果裁判模型版本改了,历史分数可能不可比。处理办法很简单:裁判模型版本也要记录,必要时做双跑对照。

这里承认一个局限:模型裁判对“表达风格”会有偏好,某些业务里会高估措辞完整但信息不准的回答,所以人工抽检不能省。


11. 一套最小可用实施清单

如果你想在一周内先落地一个版本,我建议按这个顺序做:

  • 接入线上请求明细表,补齐 request_id、query_type、prompt_version、model_output、feedback
  • 建一个每日回流任务,对点踩、超时、解析失败样本强制保留
  • 跑规则打分和裁判打分,先别追求太多维度
  • 做一个按 biz_tag、query_type、prompt_version 分桶的日报
  • 给 parse_success_rate、点踩率、judge_avg_score 配绝对阈值和相对阈值
  • 告警消息里附带异常桶样本链接和版本信息

做到这里,已经能解决大部分“线上效果怎么突然变差了”的排查问题。


12. 结语

大模型评估做离线集很常见,但真正让系统稳定运行的,往往是线上漂移监控这部分工程细节。关键不在于指标名字多高级,而在于你能不能把样本回流回来,能不能按桶看清问题,能不能把告警阈值校到合适位置。

一句实话:监控系统的价值,不是报了多少警,而是把排查时间从两小时压到半小时。

如果你手里正有一个已经上线的 LLM 应用,不妨先从“回流表 + 三个分桶维度 + 两条告警规则”开始。小步跑起来,比空谈评估框架更有用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐