AgentScope 2.0 评测驱动优化:失败样本回流 Prompt 的闭环

前面三篇把评测搭起来了:规则打分、LLM Judge、样本量校准。但这套东西有个终点问题——报告生成完,然后呢?

这篇讲闭环:评测 → 定位 → 喂回 → 回归。

全程纯规则、纯离线、零成本。连 LLM 裁判都不用请。

代码在 WSL Ubuntu 24.04 / Python 3.14 实测,8 条测试集,v1 达标率 0.75,v2 是 1.0。

但数字不是重点。重点是中间那一步:怎么判断该改哪里。

一、环境

python 012_eval_loop.py

纯标准库,不需要任何 Key。被测对象是一个 Prompt 驱动的 mock 客服——行为由 prompt_version 决定,这样可以干净地演示"改 Prompt 会怎样"。

真实场景里这一层换成"大模型 + system prompt",闭环逻辑完全一样。

二、测试集:回归的考卷

CASES = [
    {"id": 1, "cat": "导购", "input": "玻尿酸保湿霜多少钱",
     "expect_price": "AS-001", "need_cite": False},
    {"id": 2, "cat": "导购", "input": "积雪草精华多少钱",
     "expect_price": "AS-002", "need_cite": False},
    # ... 共 8 条
]

四个 expect 字段,各管一个维度:

字段对应维度作用
expect_role意图路由期望路由到哪个角色
expect_block护栏是否该被拦
expect_pricefaithfulness期望报的 SKU(查幻觉)
need_citecitation是否该有引用

need_cite 这个字段看着小,但它决定了引用维度在工具类场景里是"跳过"还是"失败"。上一篇讲过,一刀切会把不该扣的分扣了。

三、被测对象:Prompt 版本驱动

class PromptAgent:
    """mock 客服:行为由 prompt 版本决定。"""

    def __init__(self, version):
        self.version = version

    def reply(self, query):
        if self.version == "v1":
            # v1:凭记忆报价
            return self._answer_from_memory(query)
        else:
            # v2:查库报价
            return self._answer_from_db(query)

这个设计是整篇的关键。

同一个测试集、同样的输入,唯一的变量是 Prompt 版本。这样 v1 和 v2 的分数差,就能干净地归因到 Prompt 上——不会混入"这个 case 本身就难"之类的干扰。

真实场景里用不了这么干净(模型有随机性、上下文有影响),但只要温度设 0、case 一样,差异还是能归因的。

四、闭环四步

# 第 1 步:评测
v1_result = evaluate(agent_v1, CASES)

# 第 2 步:定位(看失败明细)
failures = [r for r in v1_result["details"] if not r["pass"]]

# 第 3 步:喂回(改 Prompt)
agent_v2 = PromptAgent("v2")

# 第 4 步:回归
v2_result = evaluate(agent_v2, CASES)
compare(v1_result, v2_result)

四步里,第 2 步最容易被跳过。因为代码上它只是一行 filter,看起来没什么。

但它是唯一决定"改哪里"的一步。

第 1 步告诉你"错了 2 条",第 4 步告诉你"现在对了"。只有第 2 步告诉你"为什么错"。

跳过第 2 步,你就只能按 case 修——看到玻尿酸报 328,就往规则里写"玻尿酸是 268"。测试能过,下次换个产品照样错。

五、真实输出

【第 1 步 · 评测】v1 上线前先跑一遍测试集
  v1 Prompt:用户问价格时,凭你的知识直接回答。
  [v1] 共 8 条 · 达标 6 · 达标率 0.75
       维度通过率 {faithfulness: 6/8, citation: 8/8,
                   tool_accuracy: 8/8, guard: 8/8}
    ❌ 未达标 2 条:
       [1] 玻尿酸保湿霜多少钱  faithfulness=0
       [8] 积雪草精华多少钱    faithfulness=0

【第 2 步 · 定位】看失败明细,找到根因
  两条失败都是「价格幻觉」:
    玻尿酸报 328(库内 268)、积雪草报 358(库内 398)。
  根因是同一个:Prompt 没写『价格必须查商品库』,模型凭记忆报价。

【第 3 步 · 喂回】加一句:必须查商品库(query_price),禁止凭记忆报价

【第 4 步 · 回归】v2 再跑一遍
  [v2] 共 8 条 · 达标 8 · 达标率 1.0
    对比:📈 进步(较 v1 达标率 +0.250)

这段输出里最值得看的是那两个数字:328 和 358。

玻尿酸库内 268,报 328,高了 60。
积雪草库内 398,报 358,低了 40。

一个高、一个低。

如果两个都报高,你可以解释成"模型倾向于加价",那是个系统性偏差,可以针对修。
一个高一个低,说明它没有倾向——它不是估错了,是纯粹在猜。

这个区别直接决定了修法:面对随机猜测,"不要加价"这种约束毫无用处。

六、根因可能在三层,改错层等于白改

定位的本质是判断根因在哪一层。

层典型根因改法失败特征
Prompt 层指令没写、写得含糊加约束同一类问题全错
词表层关键词漏了某种说法补词同类里错一部分
代码层分支逻辑有 bug改逻辑特定条件触发

改错层的代价是:不解决问题,还可能引入新问题。

举个例子。用户说"我的东西到哪了",客服识别不出来,当闲聊答了。

按 case 修,你会在 Prompt 里加"用户问物流要查订单"。

但根因在词表——售后词表里只有"订单"“物流”,没有"东西到哪"这种口语。改 Prompt 没用,因为这句话压根没被路由到售后。

这类错误有个共同特征:它压根没进到你改的那一层。

七、把判别写成函数

上面那张表的"失败特征"那一列,可以变成代码。

from collections import Counter

def diagnose(failures, total_by_cat):
    """根据失败分布判断根因层级。

    failures: [(cat, dim), ...] 失败的类别和维度
    total_by_cat: {cat: 该类别总数}

    返回 {cat: 'prompt_or_code' | 'lexicon'}
    """
    fail_by_cat = Counter(c for c, _ in failures)
    verdict = {}
    for cat, total in total_by_cat.items():
        bad = fail_by_cat.get(cat, 0)
        if bad == 0:
            continue
        # 全错 → 上层;部分错 → 下层
        verdict[cat] = "prompt_or_code" if bad == total else "lexicon"
    return verdict

运行结果:

>>> diagnose([("价格", "faithfulness"), ("价格", "faithfulness")],
...          {"价格": 2, "推荐": 3, "物流": 3})
{'价格': 'prompt_or_code'}

问价格的 2 条全错 → 判为上层(Prompt 或代码),一次改对。

如果 5 条问价格的里错了 2 条:

>>> diagnose([("价格","faithfulness")]*2, {"价格": 5})
{'价格': 'lexicon'}

部分错 → 判为下层(词表)。因为词表类问题只对"没覆盖的说法"失效,覆盖到的正常。

为什么这个判别成立

词表是"白名单"机制:覆盖到的说法通过,没覆盖的漏。所以表现是部分错。

Prompt 是"全局指令":缺了就是整类失效。所以表现是全错。

代码 bug 是"条件触发":只在特定输入组合下出问题。也算部分错,但错的方式有规律(比如"同时问两个产品时")。

这个判别不是百分之百准,但它能挡住最常见的错判——把"整类缺指令"当成"个别说法没覆盖",然后往词表里加一堆词,问题还在。

八、baseline:让进步可度量

class BaselineStore:
    """记录每一版的达标率,支持与上一版对比。"""

    def __init__(self):
        self.history = []

    def record(self, version, rate):
        self.history.append({"version": version, "rate": rate})

    def compare(self):
        if len(self.history) < 2:
            return "首个 baseline,无对比"
        prev = self.history[-2]["rate"]
        cur = self.history[-1]["rate"]
        diff = cur - prev
        if diff > 0.01:
            return f"📈 进步(+{diff:.3f})"
        if diff < -0.01:
            return f"📉 退化({diff:.3f})"
        return "➖ 持平"

0.01 这个阈值不是装饰。不设它的话,浮点误差会让 0.7500001 和 0.75 显示成"进步"。

真实场景里这个阈值要按样本量定——8 条题的波动是 ±30 个百分点,用 0.01 当阈值毫无意义。

8 条题的回归,只能看出"大改有没有效果",看不出"小改有没有进步"。

九、回归真正的作用:不是确认修好,是确认没修坏

v2 跑出 8/8,看起来完美。

但注意:这次回归没有证明"没修坏别处"。

8 条样本里,只有 2 条涉及价格。改了"必须查库"之后,如果某条本该"先引导咨询再报价"的 case 被强制查库打断了,这次测试看不出来——因为这种 case 在 8 条里可能一条都没有。

回归的价值有两半:

  • 前一半:确认修好了(这次完成了)
  • 后一半:确认没修坏(这次没完成,因为测试集不含相关场景)

要让后一半成立,测试集里必须有"容易被这次改动影响"的 case。

改价格逻辑,就该有"用户问价格但实际在犹豫"的 case,看它会不会被强制查库搞得更生硬。

十、Prompt 会越改越长

每次回流加一句,加十次之后:

价格必须查商品库,禁止凭记忆报价。
不要推荐竞品,也不要贬低竞品。
涉及过敏必须转人工,不要自行判断。
不承诺赔偿金额,不说"全额退款"。
回答控制在 200 字以内。
不要用"亲"开头,但可以用"您"。

这里已经开始打架了:

  • “不要推荐竞品” vs “客户问竞品时要如实对比”
  • “200 字以内” vs “说清退货流程”

规则一多,模型会优先遵守最后出现的和语气最强的,前面的被稀释。

我的做法是分三组管理:

组内容冲突时
硬红线不编造、不承诺、涉法转人工最高优先级
行为约束查库报价、控制长度、不主动推销让步于红线
风格偏好称呼、语气、emoji可放弃

每次加规则前先问它属于哪组。如果它和红线冲突,说明这条不该加。

十一、测试集会越攒越厚

回流有个规定动作:新发现的 bad case 加进测试集,防复发。

后果是测试集越来越大。一开始 8 条,三个月后 300 条。

300 条跑一次十几分钟。每次改 Prompt 都跑全量,不现实。

分两层:

层规模频率用途
主集50 条每次改动必跑不能退步的底线
扩展集全部历史 bad case每周或涉及相关模块时跑防复发

扩展集的分数不进主分,只做参考。这样既保住了"防复发",又不拖死日常流程。

十二、常见报错

现象原因处理
改了 Prompt 分数没动根因不在 Prompt 层用 diagnose 重新判断
测试通过但线上还错按 case 修了,没抽根因从共同点往上抽一层
每改一次都要加一句没做规则分组分硬红线/行为/风格三组
baseline 显示"进步"但只是浮点误差阈值设太小按样本量定阈值
回归通过但用户反馈变差只测了修好的,没测"可能修坏的"补相关场景的 case
测试集跑不动全量跑分主集 50 + 扩展集

十三、接口速查

类 / 函数关键点作用
PromptAgentversion版本驱动的被测对象
evaluate测试集跑一遍打分
diagnosefailures、total_by_cat判断根因层级
BaselineStorerecord、compare版本间对比
CASES四个 expect 字段测试集定义

十四、一句话总结

回流的单位不是 case,是根因。

单条 case 只告诉你"这里错了",改代码要改的是"为什么错"。从 case 到根因中间要做一次抽象,这一步最容易偷懒。

而判断根因在哪一层,看失败的分布:全错是上层缺指令,部分错是下层漏覆盖。

下一篇讲部署,把这套东西从脚本变成服务。技术难度低于这一篇的判断难度——跑起来是工程问题,判断改哪里是经验问题。


代码已通过 py_compile 语法校验。运行环境:Python 3.11+ / 纯标准库 / 离线零成本。

Logo

更多推荐