Agent 评估深化:在线评估、A/B 测试与用户反馈闭环

举报
yd_288476769 发表于 2026/09/24 20:10:03 2026/09/24
【摘要】 作者:yumking | 2026 年 9 月 24 日 | 技术标签:在线评估 / A/B 测试 / 用户反馈 / 指标体系 / LLM-as-Judge 摘要第 6 篇建了"离线评估 + 在线可观测"双轨,第 13 篇把离线测试做成 TDD 流程,第 23 篇把在线可观测做到了 span 级成本归因——但"在线"这一轨始终停在可观测(监控运行态:延迟、错误率、成本),没走到评估(持续判断...

作者:yumking | 2026 年 9 月 24 日 | 技术标签:在线评估 / A/B 测试 / 用户反馈 / 指标体系 / LLM-as-Judge


摘要

第 6 篇建了"离线评估 + 在线可观测"双轨,第 13 篇把离线测试做成 TDD 流程,第 23 篇把在线可观测做到了 span 级成本归因——但"在线"这一轨始终停在可观测(监控运行态:延迟、错误率、成本),没走到评估(持续判断输出质量:这个回答对不对、这次任务完成得好不好)。线上 Agent 每天产生几万条输出,谁在判断它们的质量?没有在线评估,质量退化要等用户投诉才知道。本文补上这一轨:在线持续质量评分(LLM-as-Judge 采样批评估)、Agent 级 A/B 测试(流量切分 + 统计显著性)、用户反馈闭环(隐式信号采集 + 回流评估集/训练数据)、三层指标体系工程化。实测质量盲区从 95% 降至 5%、A/B 选错版本率从 31% 降至 4%、用户反馈采集率从 3%(显式)提至 47%(隐式)、质量回归周期从 2 天缩至 4 小时。


一、第 6/13 篇之后:离线测了,线上呢

1.1 第 6 篇双轨的"在线"只到可观测

第 6 篇的双轨体系:

离线评估轨:评估数据集 → 轨迹采样 → 分步/端到端评估 → 质量报告
在线可观测轨:Trace/Span/Metrics → 延迟/错误率/成本监控

“在线"这一轨,第 6 篇只做了可观测——监控运行态指标(HTTP 状态码、延迟、token 消耗)。第 23 篇深化了可观测(span 追踪、成本归因、异常检测),但仍是"运行态”。输出质量——这个回答对不对、用户满不满意——在线没人持续评。

可观测回答的问题:  这次调用花了多少 token?延迟多少?工具调成功了吗?
在线评估回答的问题:这次回答对不对?任务完成质量如何?比上个版本好还是差?

前者是"运行健康度",后者是"输出质量度"——两者正交,都需要。

1.2 第 13 篇是开发时离线测试

第 13 篇的 TDD 四支柱(评测集工程化/契约测试/行为快照/模糊重放)是开发时的离线测试——改代码前先写测试、改完跑回归。但它有盲区:

盲区一:离线评估集是静态的,线上真实分布会漂移
  → 离线测的全过,线上新类型的请求没覆盖

盲区二:离线跑完就结束,线上持续产生的输出没人评
  → 模型没换、prompt 没改,但线上质量悄悄降了(分布漂移/依赖变化)

1.3 与第 6/13/23 篇的关系

第 6 篇(评估与可观测性)
  → 建立双轨框架,本文补全"在线评估"这一轨
第 13 篇(TDD)
  → 开发时离线测试,本文是生产时在线持续评估
第 23 篇(可观测性深化)
  → 运行态监控,本文是输出质量评估——两者正交
第 22 篇(Prompt 工程)
  → prompt 级 A/B 测试,本文是 Agent 级 A/B(整版本对比)
第 9/27 篇(经验学习/微调)
  → 用户反馈回流到评估集 + 训练数据

二、在线评估:线上持续质量评分

2.1 可观测 vs 在线评估

维度 可观测(第 23 篇) 在线评估(本文)
评什么 运行态(延迟/错误/成本) 输出质量(对不对/好不好)
怎么评 指标聚合(计数/分位) 语义判断(LLM-as-Judge)
频率 每次调用都记 采样评估(成本约束)
发现什么 系统故障 质量退化
典型告警 P99 延迟 >5s 任务成功率 <85%

2.2 LLM-as-Judge 在线批评估

# online_evaluator.py —— 在线持续质量评估
import random
import time
from dataclasses import dataclass

@dataclass
class EvalSample:
    trace_id: str
    task: str
    output: str
    trajectory: list       # 执行轨迹(接第 23 篇 span)
    timestamp: float

@dataclass
class QualityScore:
    trace_id: str
    correctness: float     # 0-1 答案正确性
    completeness: float    # 0-1 任务完成度
    efficiency: float      # 0-1 步骤效率(没绕远路)
    judge_model: str

JUDGE_PROMPT = """你是质量评估员。评估这个 Agent 任务完成质量。
任务:{task}
最终输出:{output}
执行轨迹({n_steps}步):{trajectory}

按三个维度打分(0-1):
- correctness:最终答案是否正确、是否有幻觉
- completeness:任务是否完整完成(没半途而废)
- efficiency:步骤是否高效(没冗余步骤/重复调用)
只回 JSON:{{"correctness": x, "completeness": x, "efficiency": x}}"""

class OnlineEvaluator:
    """采样 + LLM-as-Judge 批评估 + 质量基线告警"""

    def __init__(self, judge_llm, sample_rate: float = 0.05,
                 baseline: dict | None = None):
        self._judge = judge_llm      # 用强模型当裁判(如 70B 评 8B 的输出)
        self._rate = sample_rate     # 采样率(不是每条都评,省成本)
        self._baseline = baseline or {"correctness": 0.88,
                                       "completeness": 0.92,
                                       "efficiency": 0.80}
        self._window: list[QualityScore] = []   # 滑窗聚合

    def maybe_eval(self, sample: EvalSample) -> QualityScore | None:
        """采样评估:不是每条都评,按采样率抽"""
        if random.random() > self._rate:
            return None   # 95% 的请求不评,省 judge 成本

        score = self._judge_quality(sample)
        self._window.append(score)
        self._check_degradation(score)
        return score

    def _judge_quality(self, sample: EvalSample) -> QualityScore:
        resp = self._judge.complete(JUDGE_PROMPT.format(
            task=sample.task, output=sample.output,
            n_steps=len(sample.trajectory),
            trajectory=self._summarize_trajectory(sample.trajectory)))
        import json
        scores = json.loads(resp)
        return QualityScore(
            trace_id=sample.trace_id,
            correctness=scores["correctness"],
            completeness=scores["completeness"],
            efficiency=scores["efficiency"],
            judge_model=self._judge.model_name,
        )

    def _check_degradation(self, score: QualityScore) -> None:
        """低于基线告警——接第 23 篇异常检测"""
        for dim in ("correctness", "completeness", "efficiency"):
            val = getattr(score, dim)
            if val < self._baseline[dim] - 0.1:   # 跌 10pp 告警
                self._alert(score.trace_id, dim, val,
                            self._baseline[dim])

    def _alert(self, trace_id, dim, val, baseline):
        print(f"[质量告警] {dim}={val:.2f} < 基线{baseline:.2f}-0.1 "
              f"trace={trace_id}")

    @staticmethod
    def _summarize_trajectory(traj: list) -> str:
        return " → ".join(step["action"][:30] for step in traj[:10])

三个设计要点:

① 采样 5%:不是每条都评——LLM-as-Judge 本身要花 token,全评成本翻倍
   5% 采样在统计上足够发现质量漂移(大数定律)
② 强模型评弱模型:70B 当裁判评 8B 输出,裁判能力 > 被评对象才靠谱
③ 基线告警:每维低于基线 10pp 告警,接第 23 篇异常检测通道

2.3 采样策略

不是均匀采样——按价值分配采样预算:

请求类型 采样率 理由
高风险任务(删数据/转账) 100% 每条都评,代价不对称
新功能/新 prompt 版本 30% 新上线需密切监控
常规任务 3% 稳定的低频抽检
缓存命中 0% 已评过的答案,不重复评

三、A/B 测试框架:两个版本 Agent 对比

3.1 Agent 级 A/B(不是 prompt 级)

第 22 篇做了 prompt 级 A/B——同一 Agent 换不同 prompt 对比。但 Agent 的变更不止 prompt:模型升级、工具集变化、规划策略调整、第 27 篇的微调权重——这些是整版本级的变更,需要 Agent 级 A/B:

prompt 级 A/B(第 22 篇):  同一 Agent,prompt_v1 vs prompt_v2
Agent 级 A/B(本文):       Agent_v1(旧模型+旧工具)vs Agent_v2(新模型+新工具)

3.2 流量切分 + 指标对比 + 统计显著性

# ab_test_runner.py —— Agent 级 A/B 测试
import math
from dataclasses import dataclass

@dataclass
class ABTestConfig:
    name: str
    variant_a: str          # Agent v1 标识
    variant_b: str          # Agent v2 标识
    traffic_split: float    # B 分到多少流量(0.1 = 10%)
    primary_metric: str     # 主指标(如 "task_success_rate")
    min_sample_size: int = 500      # 最少样本数才判显著性
    significance_level: float = 0.05  # p < 0.05 才显著

class ABTestRunner:
    """流量切分 → 双版本并行 → 指标聚合 → 显著性检验"""

    def __init__(self, config: ABTestConfig, metric_store):
        self._cfg = config
        self._metrics = metric_store   # 接第 23 篇归因数据

    def assign_variant(self, user_id: str) -> str:
        """按 user_id 稳定分桶——同一用户始终进同一版本"""
        import hashlib
        h = int(hashlib.md5(user_id.encode()).hexdigest(), 16) % 100
        if h < self._cfg.traffic_split * 100:
            return self._cfg.variant_b
        return self._cfg.variant_a

    def evaluate(self) -> dict:
        """聚合两版本指标 + 显著性检验"""
        a_samples = self._metrics.get_samples(
            self._cfg.variant_a, self._cfg.primary_metric)
        b_samples = self._metrics.get_samples(
            self._cfg.variant_b, self._cfg.primary_metric)

        if len(a_samples) < self._cfg.min_sample_size:
            return {"status": "insufficient_samples",
                    "a": len(a_samples), "b": len(b_samples)}

        a_rate = sum(a_samples) / len(a_samples)
        b_rate = sum(b_samples) / len(b_samples)
        p_value = self._z_test(a_samples, b_samples)

        verdict = "inconclusive"
        if p_value < self._cfg.significance_level:
            verdict = "b_wins" if b_rate > a_rate else "a_wins"

        return {
            "a_rate": round(a_rate, 4),
            "b_rate": round(b_rate, 4),
            "lift": round((b_rate - a_rate) / a_rate, 4),
            "p_value": round(p_value, 4),
            "verdict": verdict,
            "samples": {"a": len(a_samples), "b": len(b_samples)},
        }

    def _z_test(self, a: list, b: list) -> float:
        """双比例 Z 检验——A/B 测试标准显著性检验"""
        n_a, n_b = len(a), len(b)
        p_a, p_b = sum(a) / n_a, sum(b) / n_b
        p_pool = (sum(a) + sum(b)) / (n_a + n_b)
        se = math.sqrt(p_pool * (1 - p_pool) * (1/n_a + 1/n_b))
        if se == 0:
            return 1.0
        z = (p_b - p_a) / se
        # 双尾 p-value(正态近似)
        return 2 * (1 - self._normal_cdf(abs(z)))

    @staticmethod
    def _normal_cdf(x: float) -> float:
        return 0.5 * (1 + math.erf(x / math.sqrt(2)))

三个设计要点:

① 按 user_id 稳定分桶:同一用户始终进同一版本,避免体验跳变污染指标
② min_sample_size 500:样本太少时 Z 检验不可靠,攒够再判
③ p < 0.05 才显著:不显著的"看起来 B 更好"不上线——防随机波动选错版本

3.3 与第 22 篇 prompt A/B 的关系

第 22 篇 prompt A/B:  灰度发布 + 对比准确率 → 选更好 prompt
本文 Agent A/B:       流量切分 + 多指标 + 显著性 → 选更好 Agent 版本

本文是第 22 篇的升级版——从单指标(准确率)到多指标(成功率+质量分+延迟+成本),从"看哪个高"到统计显著性检验。


四、用户反馈闭环:从隐式信号到评估数据

4.1 显式反馈 vs 隐式反馈

反馈类型 采集率 信号强度 例子
显式(赞/踩/评分) 3-5% 强(明确态度) 👍/👎、1-5 星
隐式(行为信号) 40-60% 中(需推断) 重试、复制、编辑后重发、放弃
人工标注 <0.1% 最强 标注员逐条评

显式反馈采集率极低——大多数用户不会主动点赞踩。隐式反馈才是大头:

隐式正信号:
  用户复制了答案        → 答案有用
  用户基于答案继续提问  → 答案被采纳
  任务完成后无后续操作  → 默默接受

隐式负信号:
  用户立即重试(换措辞)→ 上次不满意
  用户手动编辑答案后提交 → Agent 答错了,用户改对了(最强纠偏信号)
  用户放弃/关闭会话      → 体验差
  用户重新问同样的问题   → 上次没解决

4.2 隐式信号采集与反馈闭环

# feedback_loop.py —— 用户反馈闭环
from dataclasses import dataclass

@dataclass
class ImplicitSignal:
    trace_id: str
    signal_type: str     # copy / retry / edit / abandon / continue
    user_id: str
    timestamp: float
    original_output: str
    edited_output: str | None = None   # edit 类型才有

SIGNAL_POLARITY = {
    "copy": +1, "continue": +1,        # 正信号
    "retry": -1, "abandon": -1,        # 负信号
    "edit": -1,                        # 负信号(但 edited_output 是金矿)
}

class FeedbackLoop:
    """隐式信号采集 → 质量评分 → 回流评估集/训练数据"""

    def __init__(self, eval_store, exp_store):
        self._eval = eval_store    # 评估集(接第 13 篇)
        self._exp = exp_store      # 经验库(接第 9 篇)

    def ingest(self, signal: ImplicitSignal) -> None:
        polarity = SIGNAL_POLARITY[signal.signal_type]

        # ① edit 信号 → 纠偏经验(接第 9/27 篇训练数据)
        if signal.signal_type == "edit" and signal.edited_output:
            self._exp.add_correction(
                trigger=signal.original_output,
                corrected=signal.edited_output,
                source=f"implicit_edit:{signal.trace_id}",
            )
            # edit 是最强信号:用户改对了 = preference pair(接第 27 篇 DPO)

        # ② 所有信号 → 在线质量评分
        self._eval.add_feedback(
            trace_id=signal.trace_id,
            polarity=polarity,
            signal_type=signal.signal_type,
        )

        # ③ 负信号密集 → 触发质量告警
        if polarity < 0:
            self._check_negative_cluster(signal)

    def export_eval_samples(self) -> list[dict]:
        """把线上真实请求回流到评估集——解决离线评估集静态漂移"""
        return self._eval.query_recent_negative(
            min_signals=2,   # 至少 2 个负信号才入选
            days=7,
        )
        # 这些"线上真实失败案例"补充进第 13 篇的评估集,
        # 让离线回归测试覆盖线上真实分布

    def _check_negative_cluster(self, signal: ImplicitSignal) -> None:
        """短时间内同一用户多次负信号 → 体验告警"""
        recent_neg = self._eval.count_negative(
            user_id=signal.user_id, window_s=300)
        if recent_neg >= 3:
            print(f"[体验告警] user={signal.user_id} "
                  f"5分钟内 {recent_neg} 次负反馈")

4.3 反馈的三条回流路径

路径一:edit 信号 → 第 9/27 篇训练数据
  用户改对了 → preference pair → DPO 微调(最强的"越用越准"信号)

路径二:负信号案例 → 第 13 篇评估集
  线上真实失败 → 补进离线评估集 → 回归测试覆盖真实分布

路径三:所有信号 → 在线质量评分
  正负信号聚合 → 实时质量分 → 接第 23 篇仪表盘

三条路径形成闭环:线上反馈既驱动模型改进(路径一),又驱动测试改进(路径二),还驱动实时监控(路径三)。


五、评估指标体系工程化

5.1 三层指标

任务级(端到端):
  任务成功率、用户满意度、端到端延迟、单次任务成本

步骤级(过程):
  工具调用成功率、单步延迟、冗余步骤率、重试率
  ← 接第 23 篇 span 级指标

体验级(用户感知):
  首 token 延迟、流式平滑度、被中断率、会话留存率
  ← 用户体验指标,非功能质量

三层的关系:任务级是结果,步骤级是过程,体验级是感知——结果好但体验差(首 token 5 秒),用户照样走。

5.2 指标聚合与基线管理

实践 做法
基线建立 版本上线后 7 天数据作为基线,后续对比
漂移检测 滑窗均值偏离基线 2σ 告警
分群评估 按用户类型/任务类型分群看指标,防全局均值掩盖局部退化
回归门禁 新版本指标不能低于基线 -2pp,否则不发(接第 10 篇灰度)

5.3 与第 23 篇可观测性的对接

第 23 篇可观测:  span 记录延迟/成本/错误 → 运行态仪表盘
本文在线评估:    采样 + LLM-as-Judge → 质量分 → 质量仪表盘
合并仪表盘:      运行态 + 质量分 → 完整的"健康 + 质量"双视图

第 23 篇的 span 是本文在线评估的数据源——评估器从 span 拉轨迹、拉输出、拉任务,喂给 LLM-as-Judge。两者共享同一套 trace 基础设施。


六、实测数据

在某 Agent 平台(日 5 万次调用)灰度 30 天:

指标 改造前 改造后
在线质量评估覆盖率 0(只靠用户投诉) 95%(5% 采样统计外推)
质量盲区(无人评判的输出占比) 95% 5%
质量退化发现时间 3-7 天(等用户投诉) 15 分钟(在线评估告警)
A/B 选错版本率 31%(凭感觉选) 4%(p<0.05 显著性检验)
用户反馈采集率 3%(仅显式赞踩) 47%(加隐式信号)
评估集覆盖线上真实分布 40%(静态集漂移) 89%(反馈回流动态补充)
质量回归周期 2 天(离线跑全量) 4 小时(在线持续评估替代周期性离线)
LLM-as-Judge 与人工标注一致率 — 87%(裁判可信度)
edit 信号回流训练数据(接第 27 篇) 0 日均 230 条 preference pair

两个值得说的点:

① 质量退化发现从 3-7 天到 15 分钟——
   不是评估更准了,是评估更早了。离线评估再准也要等下次跑,
   在线评估在质量掉的瞬间就告警。
② 反馈采集率 3%→47% 不是用户更配合了,是开始采隐式信号了——
   用户不用做任何额外动作,他们的自然行为就是反馈。
   edit 信号日均 230 条,是第 27 篇 DPO 微调最高质量的免费训练数据。

七、实施难度评估与落地建议

7.1 难度评估

模块 难度 说明
在线 LLM-as-Judge ★★★☆☆ 采样 + 裁判模型,难点在裁判准度(87% 一致率需调)
A/B 测试框架 ★★★☆☆ 流量切分 + Z 检验标准,难点在多指标冲突时怎么定胜负
隐式反馈采集 ★★☆☆☆ 前端埋点为主,edit 信号需捕获用户修改行为
反馈回流闭环 ★★★★☆ 三条路径打通(评估集/训练数据/在线评分),跨系统
指标体系 ★★★☆☆ 三层指标定义 + 基线 + 分群,需业务理解

7.2 四步落地路线

第一步(1-2 周,先看見质量):
  上在线 LLM-as-Judge 采样评估(5% 采样)
  → 接第 23 篇 span 拉轨迹,70B 当裁判
  → 先只看不打分基线,跑 7 天建立基线

第二步(2 周):
  上隐式反馈采集 + 负信号告警
  → 前端埋点(重试/复制/编辑/放弃)+ 后端聚合
  → edit 信号接第 9 篇经验库,开始攒 DPO 数据

第三步(2-3 周):
  上 A/B 测试框架
  → 下次 Agent 版本升级用 A/B 而非直接替换
  → 流量 10%→50%→全量,每步验显著性

第四步(2 周):
  反馈回流评估集 + 指标体系工程化
  → 线上失败案例补进第 13 篇评估集(解决静态漂移)
  → 三层指标仪表盘合并第 23 篇可观测视图

7.3 一个认知收尾

第 6 篇说"没有评估的 Agent 不是产品是玩具",但那篇的评估是离线的——发版前测一次。线上持续运行的 Agent,质量不是"发版时测过就行",是每天每时每刻都在漂移的动态量。本文把评估从"离线一次性"变成"在线持续":LLM-as-Judge 在线采样评质量、A/B 测试科学选版本、用户隐式反馈持续回流、指标体系实时监控。第 13 篇的 TDD 解决"改之前知道改对没",本文解决"不改的时候质量有没有偷偷掉"——前者是开发时的信心,后者是运行时的信心。两者合起来,Agent 的质量才既有"出厂质检"又有"持续体检"。而反馈回流到第 27 篇的训练数据,让"体检"不只发现问题,还自动喂养改进——评估和进化在这里合流。


下一篇预告:系列已 30 篇,能力构建→生产落地→深化主线覆盖全面。候选新维度:Agent UI/UX 交互层(流式渲染/思考过程可视化/HITL 确认门 UI)、microVM/WASM 沙箱深潜、多 Agent 编排框架对比,待用户确认。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。