🏠 总目录📚 本教程 挑战B · 评测驱动
📑 本页目录(点开跳转)

挑战项目 B · 评测驱动开发一个 Agent

5–7 天 | 难度 ★★★★☆ | 前置:第 14 节吃透 + 第 17 节至少做过一个


🎯 为什么选这个项目(它的「特点」)

99% 的人做 Agent 的顺序是「先写,再凭感觉调」。这个项目强制你反过来:先建评测,再写 Agent,让每一次改动都有数字说话。

   你会亲身经历这条真理:
   💀 "我觉得这个 prompt 更好" —— 跑评测,其实掉了 8 分
   💊 "这个改动没感觉" —— 跑评测,稳定涨了 12 分
   🔬 "改了之后总分没变" —— 分维度一看,简单题涨了、难题崩了

   没有评测,Agent 优化就是玄学。
   这个项目让你戒掉玄学,一辈子受用。

它教的是元技能:不是「怎么做一个 Agent」,而是「怎么知道你的 Agent 到底行不行」。这是初级和高级 Agent 工程师最大的分水岭(对应第 14 节全章)。


🎯 被测对象:一个「自然语言转结构化查询」Agent

选这个任务是因为它既有客观对错、又有主观质量,能练到三类评分器:

   输入:"上个月华东区销售额超过10万的订单,按金额降序"
   输出:结构化查询 JSON
        {"region":"华东", "metric":"销售额", "op":">", "value":100000,
         "time":"上月", "sort":{"field":"金额","order":"desc"}}

🧠 ADHD 任务切分

第一阶段:先建评测,一行 Agent 代码都不许写(Day 1–2)

第二阶段:现在才开始写 Agent(Day 3)

第三阶段:迭代循环 + 抓评测本身的问题(Day 4–5)

第四阶段:报告(Day 6–7)


🔨 关键骨架

import json

# ============ 评测任务 ============
TASKS = [
    {"id": "n1", "input": "上个月华东区销售额超过10万的订单,按金额降序",
     "reference": {"region":"华东","metric":"销售额","op":">","value":100000,
                   "time":"上月","sort":{"field":"金额","order":"desc"}},
     "type": "normal"},
    {"id": "r1", "input": "给我看看那些不太好的订单",   # 模糊,该拒绝
     "reference": {"action": "clarify", "reason": "'不太好'定义不明"},
     "type": "should_reject"},
    # ... 共 25 个
]

# ============ 代码评分器(支持部分分)============
def code_score(output, ref):
    try:
        out = json.loads(output)
    except json.JSONDecodeError:
        return 0.0, "JSON 不合法"
    if ref.get("action") == "clarify":            # 反例:本该拒绝
        return (1.0, "正确拒绝") if out.get("action") == "clarify" \
               else (0.0, "该拒绝却硬答了")
    fields = ["region", "metric", "op", "value", "time"]
    hit = sum(1 for f in fields if out.get(f) == ref.get(f))
    return hit / len(fields), f"{hit}/{len(fields)} 字段正确"

# ============ 模型评分器(结构化 rubric)============
RUBRIC = """给这个"自然语言转查询"的输出打分,四个维度各 0-25 分:
- 意图准确:查询意图理解对吗
- 字段完整:该有的字段都有吗
- 边界处理:相对时间/否定/多条件处理对吗
- 拒绝恰当:模糊查询是否恰当地要求澄清(不该拒绝的别拒绝)
输出 JSON:{"意图":分, "字段":分, "边界":分, "拒绝":分, "理由":"..."}"""

def model_score(judge_model, task, output):
    prompt = f"{RUBRIC}\n\n输入:{task['input']}\n参考解:{task['reference']}\n实际输出:{output}"
    return json.loads(judge_model.chat([{"role":"user","content":prompt}]))

# ============ 评测循环 ============
def evaluate(agent, judge, tasks, runs=1):
    results = []
    for task in tasks:
        for _ in range(runs):                     # runs>1 用于测 pass^k 一致性
            out = agent.run(task["input"])
            cs, note = code_score(out, task["reference"])
            ms = model_score(judge, task, out)
            results.append({"id": task["id"], "type": task["type"],
                            "code": cs, "model": sum(ms[k] for k in
                            ["意图","字段","边界","拒绝"])/100, "note": note})
    return results

def summarize(results):
    """⭐ 一定要分维度,不能只看总分"""
    from collections import defaultdict
    by_type = defaultdict(list)
    for r in results:
        by_type[r["type"]].append(r["code"])
    return {t: round(sum(v)/len(v), 3) for t, v in by_type.items()}
    # 你会看到 normal 涨的时候 should_reject 可能在跌——这就是单向优化陷阱

🕳️ 专属坑

说明
忍不住先写 Agent 违背整个项目的意义。评测没建完,禁止碰 Agent 代码
只看总分 总分持平可能掩盖「简单题涨、难题崩」。必须分维度、分正反例看
评分器当真理 评分器自己会错。不人工核对评分器,你优化的是幻觉
一次改多个变量 涨了跌了都不知道是谁干的。一版一改动
任务太少或太简单 25 个够起步,但要有难度梯度,否则很快饱和(第 14 节第 7 步)
模型评分器没校准 让它先给参考解打分,如果参考解都拿不到高分,评分器坏了
0% 通过率就怪 Agent 往往是任务写坏了(第 14 节第 2 步)

🔗 这一章连到哪里

去哪 为什么
全景导论 11 评测的全景版:有哪些榜、各测什么、为什么榜单会失真
上线之后 12 ⭐「反复看结果」在统计上叫 peeking,会把假阳性推到 30%
Kaggle 22 ⚠️ 盯着同一个评测集迭代 = 对它过拟合,这就是打榜里的 shake-up

✅ 通关标准

  1. 25 个任务有正反两面 + 边界,每个有参考解,且通过「两人同判」质量关
  2. 代码评分器 + 模型评分器双轨,模型评分器用参考解校准过
  3. V0→V4 有清晰的分数演进曲线,每版只改一个变量
  4. 至少抓出并修复一个评分器自己的 bug
  5. 分维度报告揭示过至少一次「总分骗人」(某维度涨、某维度跌)
  6. 报告里诚实标出「离线涨但怀疑线上未必」的改动

🏆 加分


✅ 检查点

  1. 「评测驱动」和普通开发的顺序有什么不同?为什么这个顺序重要?
  2. 25 个任务该怎么分配(正例/反例/边界)?为什么不能全是难题?
  3. 双轨评分器指哪两轨?为什么代码评分器要支持"部分分"?
  4. 「总分骗人」是什么意思?怎么防?
  5. 某一版的通过率是 0%,最该先怀疑什么?
  6. 为什么要做「同一版本连跑 3 次」的实验?它给你什么?
  7. 「离线涨但线上未必」——你怎么在报告里诚实地表达这一点?
👀 答案
  1. 先建评测,再写 Agent。重要是因为没有评测时,你所有的"改进"都是感觉——你会记住有效的那次、忘掉无效的那次,并逐渐相信自己在进步。
  2. 正例 + 反例(应该拒绝/应该说不知道的)+ 边界情况。不能全是难题是因为分数会全部趴在低位,没有区分度——你看不出哪个改动有用。
  3. 代码评分器(客观、可重复)+ 模型 rubric 评分器(处理主观项,必须先校准)。代码评分器支持部分分是因为二元的对错太粗——"格式对了但少一个字段"和"完全跑偏"应该得到不同的分数,否则你看不到渐进的改善。
  4. 总分持平,但某个维度涨了、另一个跌了——你以为没变化,其实发生了一次真实的权衡。防法:分维度报告,永远不只看总分
  5. 先怀疑任务本身坏了(评分器写错、参考答案错、任务描述有歧义),而不是 Agent 太差。0% 这种极端值几乎总是基础设施问题。
  6. 得到基础设施噪声的底噪基准。有了它你才能判断"这次涨了 2 分"是真提升还是随机波动。🔗 和《机器学习基础》第 5 章的「别信单次切分:用 5 折交叉验证报均值 ± 标准差」是同一条纪律——那一章实测同一个模型只换切分种子,10 次的极差就有 3.5 个百分点
  7. 在报告里显式标注:哪些改动是"针对评测集调的"(可能过拟合了评测),哪些是"原理上应该普遍有效的"。诚实标出不确定性,比给一个漂亮但可疑的数字有价值得多。

🛑 可以停在这里

走神救援

评测驱动开发:先建25个带参考解的任务(正/反/边界)+双轨评分器(代码支持部分分+模型rubric校准过),才准写Agent——因为没有评测时你所有的"改进"都是感觉(你会记住有效的那次、忘掉无效的那次)。任务不能全是难题,否则分数全趴在低位没有区分度;代码评分器要支持部分分,因为二元对错太粗看不到渐进改善。V0基线→读转录→一版一改动→画分数曲线。铁律:分维度看(防"总分持平但某维涨某维跌"骗人)、人工核对评分器(它自己会错)、0%通过率先怀疑任务坏了而不是Agent差。⭐同一版本连跑3次得到噪声底噪,才能判断"涨2分"是真的还是波动。报告里诚实标出"哪些是针对评测集调的"。元技能=知道Agent到底行不行。

下一个挑战 👉 20-挑战项目C-提示注入攻防.md

打卡记录保存在你的浏览器里,首页能看到总进度