Writing · 实操
agent eval 工程实操:给服装设计 agent 搭一把标尺
最近搓设计的 agent,正在调它的 system prompt,调起来非常非常不方便。
因为这个 agent 不是单次推理,套了好几个环节点,我改上游 prompt 的一句话,下游相关节点的输出都会跟着变。端到端跑完一次,我手里只有最终的一组结果——到底是哪个环节变好了、哪个环节其实变差了但被后面环节掩盖了,根本看不清。
所以我停下来,花了些时间搭一套 eval harness。目标只有一个:把"哪个环节贡献了多少质量"和"这版改完到底是变好还是变差"这两件事,从模糊感觉变成可读、可量化。下面把整个过程拆开讲。
一、eval harness 到底是什么
开始时,肯定是先过单测。单测能测出"格式对不对""字段缺没缺""字数有没有超"这些硬约束,但写不出"这个方案设计得好不好""趋势提炼得到不到位"。单测测的是链路通没通,没法告诉你变好了多少、哪里还能再提升。等到深水区,涉及输出质量和稳定性保障时,还是需要 eval 工程。
eval harness 听起来挺玄,其实就是一套给 LLM 输出打分、并把分数沉淀下来横向对比的评估系统。我自己的实现里它由三块组成:
- metrics 函数:从一份模型输出里算出一组数字
- runner:把模型输出喂进 metrics,吐出扁平的一行 dict
- store:把每次的分数存下来(CSV 或 SQLite),让 v3 能跟 v2、v1 对比
跟程序员熟悉的单测做个类比:单测是"代码改完跑一下,看绿不绿";eval 是"prompt 改完跑一组样本,看一组数字往哪个方向走"。但 eval 跟单测有三条本质区别,写代码之前要先明确:
第一,单测看 pass/fail,eval 看分布。LLM 输出是随机变量,同一个 prompt 跑 10 次会得到 10 组数字,你要看的是均值、方差、有没有坍缩到全 5 分或全 10 分这种极端。
第二,单测对单一版本,eval 是多版本横向比较。eval 永远在拿不同 version 比,每个指标涨没涨、跌了多少。
第三,单测不关心成本,eval 必须把质量和成本放一张表。v3 让审计 4/4 通过,但 token 多花 50%、延迟翻倍,这笔账你立刻就要知道——不然你以为是优化,其实是用钱换分。
用一台机器的质检来比喻:单测是齿轮的验收,eval harness 服务的是整机的验收。在 agent 项目中,eval 是你启动 agent 建设前就该搭好的脚手架。从我的实际经验看,eval 本质上也是一场全局视角下的 tradeoff——大部分时候没有所有 metrics 都正向,多数是在不同 metric 之间做取舍和判断。
二、设计 eval 之前我想清楚的 5 件事
先明确 evaluate 的准则——所谓准则就是行动的 guidance,类似纪律和底线要求:
- 先量化,再精修。LLM 输出是分布不是常量,没基线就没改进。"看上去好像更好了"是最大的认知陷阱,今天觉得好明天再看可能就是错觉。
- 能用 Python 直接算的指标优先做完。模型自评看着省事,但它不稳定、有偏好、还贵。基础原则是:能用
len()、re.match()、Counter()算的东西全榨干,再考虑模型评分。 - 同一组 metric 函数必须能测两版。旧版单 prompt 输出和新版 agent 输出,JSON schema 保持兼容是有意为之——就是为了同一组 metric 同时打分两边。很多人重构时随手改 schema,结果新旧版本根本对不上号,evals 直接报废。
- 质量和成本必须放一张表。单看质量会得出"v3 比 v2 好",但如果 v3 token 翻倍、延迟翻倍,那叫"用三倍预算换 5% 提升",要拒绝。同表看的是 Pareto 前沿——同等成本下质量最高的版本,或同等质量下成本最低的版本。
- 指标定义本身也要版本化。迭代到第八九版时你大概率会想加新指标,到时候老数据要么重算、要么标记 schema_version 隔离开。我现在每个 metric 都带一个版本号。
当然,每个人的 guidelines 肯定不同,我这 5 条是踩过类似坑之后蒸馏出来的,不一定适用于所有 agent。
三、指标体系设计:大类和子项
落到我这个项目,我设计了 6 大类,分别指向不同的业务要求,下分共 30 多项子指标:
| 类别 | 例子 | 算法 |
|---|---|---|
| A 结构完整性 | 应得方案数 / 实得方案数 | len() + 字段非空 |
| B 审计 4 项 | 跨面占比、A 档使用、位置多样性、面分布 | 复用审计逻辑 |
| C 字段填充质量 | 七段式 prompt 完整率、≤50 字硬约束 | 正则 + 字符计数 |
| D 比例合规 | 性别比、风格比是否符合用户要求 | Counter 对比 |
| E 弱信号质量 | 模型自评适配度均值与方差 | 字段读取 + 统计 |
| F 成本 | 输入/输出 token、延迟、调用次数 | meta 字段 |
A 到 D 是硬指标,算出来什么就是什么;E 是弱信号——模型自评不能当主决策,但能抓异常(如果一个版本全场打 10 分、另一个全场打 5 分,这种坍缩本身有信息量);F 是成本,跟质量同表。
这里有个关键决策:为什么先不做 LLM-as-judge。其一,judge 模型本身有偏好,自家模型评自家输出会偏高,这是证实过的系统性偏差;其二,不稳定,同一个输入打两次分都不一样;最重要的是——贵!每条方案一次评分,跑一次完整 eval 几美元,迭代成本顶不住。不过我有计划引入模型评测:等 deterministic 指标饱和、开始出现"两个版本数字接近但视觉感受不同"的时候,才考虑。
四、eval/ 模块结构,和一个中途的反思
代码组织上我用了一个独立的 eval/ 包:
prompt-refine-agent/
└── eval/
├── metrics.py # 所有 metric 函数,纯函数
├── runner.py # 模型输出 → 扁平 dict 一行
├── compare.py # 多 row 横向对比表
├── store.py # CSV / SQLite 落盘
└── README.md # schema_version 约定
Claude 一开始给的 metrics.py 顶部第一行写的是 from orchestrator.audit import run_audits——直接复用项目原本的审计代码,"DRY 原则,一份逻辑两处用,多干净"。
但我对 eval 模块的要求是:eval 的运行不应该依赖被它评判的代码。如果哪天 orchestrator 里的 audit 实现悄悄有了 bug,eval 也会跟着错,但你看到的指标依然光鲜亮丽——eval 的全部价值就在于它必须是独立的判官,不能跟被告穿一条裤子。
所以我把 4 条审计逻辑 inline 到 eval/_audit.py,让 eval 自给自足。代价是多了几十行重复代码,但换来 eval 的独立性。这种取舍很多人本能上会拒绝("复用更好不是吗?"),但在 eval 这个场景里,独立性 > DRY。
五、用 legacy baseline 验证 eval 跑通
eval 代码写完,下一步不是立刻烧 API 跑新版,而是先用项目里已有的旧版输出验证 eval 代码本身没 bug。我不想等花几美元跑完一组真 API,结果发现数字是 eval 代码自己算错的——这种返工是双倍的痛。
用 legacy 输出(旧版单节点完成全部端到端推理的 prompt 跑出来的真实结果)直接当 eval 的输入,跑通后我拿到了 baseline:
| 维度 | 数值 |
|---|---|
| 审计 4 项 | 4/4 通过 |
| 跨面方案占比 | 9/9(100%) |
| 字段填充率 | 1.0 |
| 性别比合规 | 男 4 / 女 4 / 中 1 ✓ |
| 自评适配度 | 7.67/10,σ=0.47 |
| 总成本 | 198 秒,109k tokens,1 次调用 |
这就是 baseline。从这一刻起,任何新版本的 prompt 改动都要用这组数字当起跑线。
六、这条 baseline 真正告诉我的事
表面上看,这组数字证明了旧 prompt 调得不错。但 baseline 的真正价值并非"证明旧版好",而是用来做后面所有改动的对照基准。
我的优化决策是拆 agent 的推理节点,把单次推理拆成多节点。拆完之后,至少要打平这些数字才算等价,而且还得关注成本上涨是否值得。如果跨面占比从 9/9 跌到 6/9,说明拆分在"方案规划"环节漏掉了某条约束;如果 token 从 109k 涨到 300k,说明串行调用引入了上下文重复发送;如果字段填充率从 1.0 跌到 0.8,说明某个子节点的 schema 不够严。
所以尽管我的假设是"拆分能带来稳定性和可观测",但要时刻保持怀疑:拆分不一定更好。多节点 agent 看起来"更工程化、更模块化、更易维护",但这三件事都是工程层面的描述,跟"输出质量更高"是两码事。很多人重构系统凭信仰("分层一定比单体好""agent 一定比单 prompt 好"),而真正的判断,发生在能不能在 day 1 就量出"重构后是否退化"——这决定了重构是工程升级还是自嗨。
七、第一次"有据可依"的优化
拿到 baseline 之后,我跑了一版 agent 拆分实现,预期"模块化会更可控"。三个核心指标对比:
| 维度 | Legacy | Agentic v1 | Δ |
|---|---|---|---|
| 审计通过 | 4/4 | 3/4 | ↓ 1 项 |
| 跨面方案占比 | 9/9 | 5/9 | ↓ 44 pt |
| 字段填充率 | 1.0 | 0.78 | ↓ 22 pt |
| Token 总量 | 109k | 287k | ↑ 2.6× |
三项质量指标跌了、成本翻 2.6 倍——拆分版全面退化。没有 eval 系统的话,大概率会"凭感觉"调几条 prompt,跑一次看看。有了 eval,每一处跌点都能精确归因到具体节点:
- 跨面占比 9/9 → 5/9:指标够细,直接看到是"方案规划"节点输出的方案就已经不跨面了。回去看 prompt,发现我把"必须跨面"写成了描述句而不是硬规则,改成"以下方案不满足跨面要求一律视为废稿,重新规划"。
- Token 暴涨 2.6 倍:F 类里有一项 per_node_token_breakdown,一眼看到"字段填充"节点占了 70%。发现每次调用都把整份趋势 JSON 原文重发了,改成首次传全文加 ID、后续只引用 ID。
- 字段填充率 1.0 → 0.78:拆开看是七段式 prompt 经常缺第 4 段,原因是该节点没用 structured output,模型偷懒。改成 JSON Schema 强校验加一次失败重试。
改完再跑:
| 维度 | Legacy | Agentic v1 | Agentic v2 |
|---|---|---|---|
| 审计通过 | 4/4 | 3/4 | 4/4 |
| 跨面占比 | 9/9 | 5/9 | 9/9 |
| 字段填充 | 1.0 | 0.78 | 1.0 |
| Token | 109k | 287k | 142k |
质量回到 baseline,token 比 baseline 高 30%——这是 agent 拆分固有的开销,可以接受。整个调试过程没有"凭感觉",每一处改动背后都有一个数字指着具体的节点和具体的问题。
八、一些 tips 和 tradeoff
这里有个有趣的"小信号",不注意容易漏掉。以下是 V3 版本的质量维度,看起来明显提升:
| 维度 | legacy | agentic_v3 | 解读 |
|---|---|---|---|
| 4 项审计 | 全过 | 全过 | 拆分没掉硬性规则 |
| 跨面方案 | 9/9 | 9/9 | 完全一致 |
| 位置多样性 | 18 种 | 类似量级 | 完全一致 |
| 字段填充 | 100% | 100% | 完全一致 |
| 男/女/中 | 4/4/1 | 4/4/1 | 完全一致 |
| 面分布 | 前6/后8/侧8 | 前7/后7/侧6 | agentic 更均匀 |
| 适配度均值 | 7.67 | 8.93 | ↑1.26(但要警惕自评偏差) |
但适配度均值很可疑:标准差从 0.47 → 0.12,而且 agentic 几乎所有色号都给 8.7–9.1 分。有两种可能——真的好(每次拆分调用都拿到精确的累积状态约束,模型有充分依据评分),或坍缩了(每次只看单色,缺乏多色相对对比,倾向打高分)。这正是弱信号该做的事:抓异常,而不是当主决策。
再看 V3 的成本,是之前的 3.5 倍,明显退化,但我依然判断这是一次正向迭代:
| 指标 | legacy | agentic | 退化倍数 |
|---|---|---|---|
| 总 token | 109k | 380k | 3.5× |
| LLM 调用 | 1 | 20 | 20× |
| Wall-clock 耗时 | 198s | 716s | 3.6× |
| 估算 $(gpt-5.5) | $0.10 | $0.35 | 3.5× |
因为这是为可观测性付出的"token 税"——我把一次性的多维度推理,换成拆 7 个节点独立推理,是为了换 fork-from-node、prompt 独立版本管理、trace 可视化,token 多一些是预期代价。而且 fork-from-node 的杠杆价值远大于这 3.5× 成本:step 4 调整 prompt 只需重跑 1–2 个色号(约 60–120 秒),比之前 step2 整跑(200s)更快、更便宜。
总而言之
eval harness 真正的价值:在你动手改 prompt / agent loop 前,告诉你该往哪儿改。当然,不同阶段的 agent,harness 的价值侧重也不同——路线可行性、prompt 优化、成本优化、风险控制,都是需要考量的。
续集:《eval harness 落地续集:给 Prompt 调试搭一个驾驶舱》 —— 把这套算分引擎扩成一个完整工作台。