📑 本页目录(点开跳转)
挑战项目 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"}}
- 代码评分器能判:JSON 合法吗?字段齐吗?数值对吗?
- 模型评分器能判:意图理解对吗?边界情况处理合理吗?
- 有明确的正反两面:该拒绝的模糊查询有没有拒绝?
🧠 ADHD 任务切分
第一阶段:先建评测,一行 Agent 代码都不许写(Day 1–2)
- [ ] T1 (90min) 从「真实失败」出发造 25 个任务(第 14 节第 0 步):15 个正常查询、5 个模糊该拒绝的、5 个边界(多条件/否定/相对时间)。每个配参考解
- [ ] T2 (45min) 质量关(第 14 节第 2 步):每个任务问自己「两个人看了会得出同样的对错判断吗?」不能的重写
- [ ] T3 (90min) 代码评分器:JSON 可解析 + 必填字段 + 数值/操作符精确匹配。支持部分给分(意图对但漏一个字段 ≠ 完全错)
- [ ] T4 (90min) 模型评分器:给一个结构化 rubric(意图准确/字段完整/边界处理/该拒绝的拒绝了吗),分维度打分。⭐ 用 3-5 个参考解校准它——让它给参考解打分,分数应该都高
- [ ] T5 (45min) 干净 harness(第 14 节第 4 步):每个任务独立跑,不共享状态;防作弊(别让 Agent 看到参考解)
第二阶段:现在才开始写 Agent(Day 3)
- [ ] T6 (60min) V0 基线:最朴素的单 prompt。立刻跑评测存基线分
- [ ] T7 (30min) ⭐ 读转录(第 14 节第 6 步):不看分数,读 5 个失败案例的完整过程,写下你的假设「它为什么错」
- [ ] T8 (60min) V1:针对假设改 prompt(加示例?加字段说明?)→ 跑评测 → 和基线比。涨了留下,跌了回滚
第三阶段:迭代循环 + 抓评测本身的问题(Day 4–5)
- [ ] T9 (90min) 迭代 V2/V3/V4:每版一个改动,每版跑评测,画分数演进折线。规则:一次只改一个变量,否则不知道是谁的功劳
- [ ] T10 (60min) ⭐ 分维度看(第 14 节第 5 步):不要只看总分。是不是简单题涨了、难题崩了?正例好了、反例(该拒绝的)崩了?—— 单向优化是常见陷阱
- [ ] T11 (60min) 抓评分器自己的 bug:人工核对 10 个「评分器判对/判错」的案例。你几乎一定会发现评分器误判——修它。「不读转录,你不知道评分器在不在工作」
- [ ] T12 (45min) 加 pass^k(第 14 节):同一任务跑 5 次,看一致性。发现「平均分不错但方差大」的任务——这是面向用户 Agent 的隐患
第四阶段:报告(Day 6–7)
- [ ] T13 (90min) 报告:V0→V4 分数演进表、分维度雷达图、「我修过的一个评分器 bug」、「一个我以为有用其实有害的改动」
- [ ] T14 (45min) 元反思:哪些改动离线涨了但你怀疑线上未必?为什么?(第 14 节的失真)
🔨 关键骨架
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 |
✅ 通关标准
- 25 个任务有正反两面 + 边界,每个有参考解,且通过「两人同判」质量关
- 代码评分器 + 模型评分器双轨,模型评分器用参考解校准过
- V0→V4 有清晰的分数演进曲线,每版只改一个变量
- 至少抓出并修复一个评分器自己的 bug
- 分维度报告揭示过至少一次「总分骗人」(某维度涨、某维度跌)
- 报告里诚实标出「离线涨但怀疑线上未必」的改动
🏆 加分
- 加 AI 抗性测试(第 14 节 5.5):造几个「训练数据里不太可能有」的刁钻查询,看分数掉多少
- 做基础设施噪声小实验:同一版本 Agent 连跑 3 次,看纯随机波动有多大——这是你判断「涨了 2 分算不算真涨」的底噪基准
✅ 检查点
- 「评测驱动」和普通开发的顺序有什么不同?为什么这个顺序重要?
- 25 个任务该怎么分配(正例/反例/边界)?为什么不能全是难题?
- 双轨评分器指哪两轨?为什么代码评分器要支持"部分分"?
- 「总分骗人」是什么意思?怎么防?
- 某一版的通过率是 0%,最该先怀疑什么?
- 为什么要做「同一版本连跑 3 次」的实验?它给你什么?
- 「离线涨但线上未必」——你怎么在报告里诚实地表达这一点?
👀 答案
- 先建评测,再写 Agent。重要是因为没有评测时,你所有的"改进"都是感觉——你会记住有效的那次、忘掉无效的那次,并逐渐相信自己在进步。
- 正例 + 反例(应该拒绝/应该说不知道的)+ 边界情况。不能全是难题是因为分数会全部趴在低位,没有区分度——你看不出哪个改动有用。
- 代码评分器(客观、可重复)+ 模型 rubric 评分器(处理主观项,必须先校准)。代码评分器支持部分分是因为二元的对错太粗——"格式对了但少一个字段"和"完全跑偏"应该得到不同的分数,否则你看不到渐进的改善。
- 总分持平,但某个维度涨了、另一个跌了——你以为没变化,其实发生了一次真实的权衡。防法:分维度报告,永远不只看总分。
- 先怀疑任务本身坏了(评分器写错、参考答案错、任务描述有歧义),而不是 Agent 太差。0% 这种极端值几乎总是基础设施问题。
- 得到基础设施噪声的底噪基准。有了它你才能判断"这次涨了 2 分"是真提升还是随机波动。🔗 和《机器学习基础》第 5 章的「别信单次切分:用 5 折交叉验证报均值 ± 标准差」是同一条纪律——那一章实测同一个模型只换切分种子,10 次的极差就有 3.5 个百分点。
- 在报告里显式标注:哪些改动是"针对评测集调的"(可能过拟合了评测),哪些是"原理上应该普遍有效的"。诚实标出不确定性,比给一个漂亮但可疑的数字有价值得多。
🛑 可以停在这里
⚡ 走神救援
评测驱动开发:先建25个带参考解的任务(正/反/边界)+双轨评分器(代码支持部分分+模型rubric校准过),才准写Agent——因为没有评测时你所有的"改进"都是感觉(你会记住有效的那次、忘掉无效的那次)。任务不能全是难题,否则分数全趴在低位没有区分度;代码评分器要支持部分分,因为二元对错太粗看不到渐进改善。V0基线→读转录→一版一改动→画分数曲线。铁律:分维度看(防"总分持平但某维涨某维跌"骗人)、人工核对评分器(它自己会错)、0%通过率先怀疑任务坏了而不是Agent差。⭐同一版本连跑3次得到噪声底噪,才能判断"涨2分"是真的还是波动。报告里诚实标出"哪些是针对评测集调的"。元技能=知道Agent到底行不行。
下一个挑战 👉 20-挑战项目C-提示注入攻防.md