大模型评估中的线上漂移监控工程实践:从样本回流、分桶指标看板到告警阈值校准
大模型评估中的线上漂移监控工程实践:从样本回流、分桶指标看板到告警阈值校准的可复现方案
线上评估和离线评测,很多团队都做过。但系统上线一段时间后,效果为什么慢慢变差,往往说不清。问题通常不在模型参数本身,而在输入分布变了、用户提问变了、知识库内容变了,甚至标注口径也变了。
我这篇文章讲一个可复现的做法:把线上漂移监控拆成样本回流、分桶分析、告警校准三部分,然后用一套可以直接落地的数据表和脚本把它跑起来。说实话,真正上线后我才发现,很多“模型退化”最后查出来其实是业务流量结构变了。
短句先说结论:没有分桶,漂移基本看不准。
1. 线上漂移监控到底在监控什么
大模型应用里的“漂移”不是一个指标,它至少有两层:
- 数据漂移:输入问题、上下文、召回文档、用户群体发生变化
- 效果漂移:模型输出质量、任务成功率、人工评分出现下滑
工程里如果只看一个总分,很容易误判。比如总准确率从 82.4% 掉到 80.9%,看着不大,但如果把流量按查询长度切开,可能是长问题桶从 78% 掉到 66%。这就不是“小波动”了。
所以监控目标要落到三件事上:
- 线上样本能不能稳定回流
- 指标能不能按业务维度切开看
- 告警能不能少误报,又别漏报
这三件事缺一块,系统都不稳。
2. 整体方案:数据回流 + 分桶看板 + 双层告警
我用过的稳定方案如下:
- 线上日志埋点:记录请求、上下文、召回结果、模型输出、用户反馈
- 样本回流任务:按时间窗口抽样,生成待评估样本集
- 自动打分与人工补标:规则评分、模型裁判、人工抽检混合执行
- 分桶指标看板:按 query 类型、长度、用户端、业务场景拆开
- 告警阈值校准:静态阈值 + 基线波动阈值一起用
不要一上来就做复杂。先把最小闭环跑通。
下面按工程实现展开。
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 应用,不妨先从“回流表 + 三个分桶维度 + 两条告警规则”开始。小步跑起来,比空谈评估框架更有用。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)