最近看到麻省理工学院和Sakana AI联合放出的一个新框架,研究方向是“用LLM评判器(LLM as Judge)降低自我改进编程智能体的评估成本”。做AI Agent开发的人应该都有同感:编程智能体每迭代一版代码,都要跑一堆测试用例来验证正确性,越往后越烧钱。这个新框架的做法很直接——不再每次都用真实环境跑测试,而是让一个训练有素的LLM直接阅读代码和测试意图,输出“能否通过”的判断。文章会从原理、设计选择、实验倾向、落地实现和踩坑经验几个角度完整拆一遍,适合正在做Agent评估、AI编程工具、LLM评测管线的朋友参考。
1. 自我改进编程智能体为什么越改越贵
1.1 一个典型的自我改进闭环
自我改进编程智能体的核心循环,用一句话概括就是“生成-验证-修正”。智能体拿到任务描述后写第一版代码,接着自己生成测试用例,把测试跑起来,看哪些失败,然后根据失败信息修改代码,进入下一轮。很多系统还不止生成一个候选,而是同时维护一批候选个体,让它们像进化种群一样并行改进,每一代都靠测试执行来筛选优质个体、淘汰劣质个体。
问题恰恰出在这个“筛选”上。测试执行不是一个轻量动作,它通常要拉起一个隔离沙箱,安装依赖、编译代码、启动进程、模拟输入输出,再等待断言结果。这样一个流程下来,单个用例可能几秒,一组用例可能几十秒甚至更久。智能体一轮要验证几十个候选,每个候选要跑一组测试,再乘以迭代轮数,成本就这么被乘出来了。
1.2 成本不光是钱,还有时间和噪声
说到评估成本,很多人第一反应是API费用,但在真实工程里,时间成本和噪声成本往往更致命。真实测试跑得慢,意味着智能体改进循环被拉长,同样一个任务,别人迭代了20轮,你可能只跑了5轮,最终效果自然差距悬殊。
真实测试环境还不稳定,网络依赖挂了、临时文件没清理、测试本身有随机性,都会导致结果抖动。某个候选代码明明能通过,测试却因为环境问题报错;或者反过来,代码根本没实现完,测试却碰巧过了。这种噪声会直接污染智能体的自我改进信号。相比之下,LLM评判器虽然也有判断误差,但至少不会因为“容器启动失败”这类基础设施问题给出假阴性。把一部分验证工作从执行环境搬到模型推理里,表面看只是换了个评估手段,实际上把好几类工程层面的不确定性都绕开了。
1.3 既有降本方案的局限性
在LLM评判器之前,业界其实已经有不少降本尝试:
- 测试选择:只跑和本次改动相关的测试子集,但改动的影响面经常比预期大,容易漏测。
- 结果缓存:重复测试命中的结果直接复用,但代码一旦改动,结果大概率失效,缓存命中率不高。
- 减少候选数量:直接牺牲搜索宽度,可能会错过更好的方案。
- 轻量预过滤:先用静态检查或规则过滤掉明显不合格的候选,再用真实测试收尾,问题在于轻量检查的表达能力很有限,挡不住逻辑层面的错误。
这个新框架本质上是一个新的“预过滤层”,但它比传统静态检查聪明得多,因为它用的不是规则,而是大模型对代码语义和测试意图的理解。它和上面这些方法并不冲突,反而可以组合使用。
2. 新框架的核心设计:四个关键决策
2.1 从“执行验证”到“语义评判”
这个框架最关键的一步,是把“验证”从外部执行变成模型内部推理。评判器接收的是代码变更内容和测试描述,不实际运行任何东西,只基于语义判断“这份代码能不能通过这些测试”。
这背后有两个前提。第一,评判器得有足够强的代码理解能力,能从实现逻辑推断出运行结果,而不是只做语法检查。第二,测试意图必须能被表达成文本输入给评判器,测试是代码也没关系,把测试源码一起喂进去就行。只有当这两个前提满足时,这种静态评判才能逼近真实测试的效果。
不用跑测试,节省的不只是执行时间,还有整个沙箱生命周期管理。我自己的体感是,真实执行环境中90%的故障都和环境有关,而不是被测代码本身。换成LLM评判器之后,这类故障直接归零。
2.2 全局评判和局部评判怎么选
把测试和代码塞给评判器,可以有两种组织方式。
全局评判是把整个代码变更和所有测试描述放在一个Prompt里,让模型一次性判断“整体是否通过”。优点是一次调用解决问题,逻辑上下文完整,但缺点也很明显:如果最终判断是fail,模型很难定位到底是哪个测试触发了失败;而且当代码变更很长时,上下文长度会逼近模型极限,注意力会被稀释。
局部评判则是针对每个测试单独构造Prompt,逐个判断“这个测试能不能过”。这样得到的是一个测试维度上的细粒度结果,能精确定位失败的测试,也能方便地聚合出通过率。缺点是调用次数随测试数量线性增长。
实际落地时,我不建议二选一。比较舒服的组合是先全局粗筛,快速淘汰明显不合格的候选;对“活着”的候选,再挑可疑的测试做局部细查。这样既控制了调用次数,又保留了定位能力。
2.3 输出空间设计:不要只输出一个分数
评判器的输出设计,直接影响整个评估管线的质量。最粗糙的做法是让模型输出一个pass或fail,虽然简单,但丢失了太多信息。
更好的做法是组合输出三层内容:第一层是推理过程,让模型先列出代码里和测试相关的关键逻辑路径,再逐个对照测试意图检查;第二层是结论,pass、fail或者unsure;第三层是置信度,一个0到1之间的数值。
这个设计的好处,一是推理过程能约束模型“先想再答”,减少拍脑袋式判断;二是unsure这个第三态很重要,它给后续人工复核或真实测试留出了接管空间,而不是强迫模型在证据不足时硬猜;三是reason字段可以直接反馈给编程智能体,作为下一轮修改的依据,让评估不只是筛选,还能辅助生成。
2.4 成本模型对比:为什么真的能降下来
用最粗的公式来看,假设有N个候选、T个测试、R轮迭代:
- 真实测试执行成本约等于 N × T × R × C_exec,其中C_exec包含容器启动、依赖安装、测试运行、资源回收等开销。
- 全局评判成本约等于 N × R × C_judge_global,调用次数大幅减少。
- 局部评判成本约等于 N × T × R × C_judge_local,虽然调用次数和真实测试同阶,但单次开销C_judge_local通常比C_exec小一到两个数量级。
所以哪怕局部评判调用次数没有减少,整体成本依然能显著下降。如果再叠加“全局粗筛+局部细查”,只有少数候选会进入局部评判阶段,实际调用次数比N×T×R小得多。这也是这个框架真正有吸引力的地方——它不是偷工减料,而是把昂贵的执行验证替换成了廉价且可扩展的模型推理。
3. 关键取舍与实验倾向
3.1 评判器模型大小:省成本不等于乱选模型
LLM评判器的效果,和模型本身的代码理解能力高度相关。小模型跑得便宜,但很容易漏掉边界条件和异常分支;大模型更稳,但单次调用成本和延迟都更高。研究里常见的做法是先离线对比几个候选模型在一批标注样本上的判断准确率,选一个性价比最高的。
我自己的建议是,至少选择“能正确解释测试意图”这一档的模型。如果评判器连测试想验证什么都不理解,那后面的判断全是空中楼阁。在预算允许的情况下,离线校准用大模型,在线运行用中等模型,是一个很实用的折中策略。
3.2 Prompt结构差异可能造成结果大幅波动
同一模型的评判效果,和Prompt怎么写关系极大。同样是让模型判断代码能否通过测试,不同的措辞可能带来完全不同的结论。
关键是要把测试意图约束清楚。要告诉评判器输入是什么、输出是什么、边界条件是什么、失败判据是什么,而不是泛泛地说“请判断这段代码是否正确”。好的Prompt应该像一个评审工单:被测对象、测试目标、判定标准、输出格式,四要素缺一不可。如果测试本身是代码,最好把测试源码直接贴进去,而不是用自然语言转述,减少信息损耗。
3.3 长上下文的处理方式
当代码文件比较大,或者变更跨多个文件时,把全部内容塞进一个Prompt并不现实。简单截断会丢失关键逻辑,导致评判结果失真。
比较有效的处理方式是按函数拆分。先把变更涉及的每个函数抽取出来,再把调用关系和依赖信息作为辅助上下文提供给评判器;测试描述也按“这一个测试到底在测哪个函数”进行归组。这样整体上相当于把一个大问题拆成多个小问题,每个小问题都由评判器独立判断,最后再汇总。
3.4 投票、阈值与校准
评判器也有随机性,同一个Prompt跑两次,可能得到不一样的结果。多个采样投票可以削弱这种随机性,但边际收益会递减,一般三次采样就差不多了。更值得花精力的是阈值校准——到底judge给出多少分才判定为“通过”?
这个问题不能拍脑袋。稳妥的做法是取几十个样本,既跑真实测试又跑评判器,拿到两组标签后做一次校准,看真实通过样本在评判器分数上落在什么区间,再把这个区间作为判定阈值。这样还能顺带算一下评判器与真实测试的一致性指标,比如排序相关性或分类准确率,心里更有底。
4. 落地实现:搭一个低成本评估管线
4.1 第一步:把patch和测试整理成结构化数据
拿到智能体的一轮产出之后,先不要急着调评判器,先把原始数据整理成干净的中间格式。我一般是解析diff,提取变更的函数列表;解析测试文件,提取每个测试用例的名称和测试体;然后把两者关联起来,形成一条条“被测函数—测试用例”的对应关系。
这一步虽然朴实,但后面所有judge调用都依赖这个结构。如果数据脏,评判器输入就会乱,结果自然不可信。整理完的数据可以存成JSON,方便后续调试。
4.2 第二步:构造评判Prompt模板
下面是一个经过多次修改后稳定下来的评判Prompt模板:
你是一名严格的代码评审专家。下面是一次编程智能体迭代后的代码变更和一组目标测试。 代码变更: {patch} 目标测试: {tests} 请按以下步骤操作: 1. 提取代码变更中与目标测试相关的关键逻辑路径。 2. 逐个测试检查代码是否能满足其预期结果,注意边界条件和异常输入。 3. 如果存在不确定的情况,明确标为unsure。 最后只输出JSON格式(不要输出其他内容): {"overall": "pass|fail|unsure", "test_results": [{"test_id": "...", "verdict": "pass|fail|unsure", "reason": "..."}], "confidence": 0.0-1.0}这个模板的要点是:先让模型思考,再让模型回答,last输出的结构化结果可以被程序直接解析。reason字段很重要,它不只是给人看的,还可以作为反馈信息交给编程智能体修改代码。
4.3 第三步:全局粗筛+局部精排
在实际管线里,我不会对每个候选都做全量局部判断。伪代码如下:
def evaluate_candidates(candidates, tests, judge_model): survived = [] for cand in candidates: # 全局评判:快速判断整体是否可能通过 g = call_judge(judge_model, build_global_prompt(cand, tests)) if g["overall"] == "fail": continue # 直接淘汰,省掉后续所有局部判断 # 局部评判:只对全局评判中“有问题”的候选细查 local_results = [] for t in tests: r = call_judge(judge_model, build_local_prompt(cand, t)) local_results.append(r) survived.append(aggregate(cand, local_results)) return survived这里有个工程细节:全局评判判为pass的候选,我不会直接信任,而是会继续跑局部细查。因为全局评判和真实测试的一致性通常达不到100%,局部细查能抓到不少漏网之鱼。反过来,全局判为fail的候选也未必真的fail,确实存在误杀,所以在预算允许时,可以对判为fail的候选抽样跑真实测试做兜底。
4.4 第四步:用真实测试做锚定校准
无论评判器设计得多完善,我都建议始终保留一个很小的真实测试集做锚定。具体做法是:从历史数据里随机抽20到50个样本,每个样本都同时跑真实测试和LLM评判器,记录两者的标签和分数。
用这批数据可以算出混淆矩阵,也可以画出分数分布曲线,从而确定判定阈值。更重要的是,这批锚定样本能持续监控评判器的漂移。模型升级、Prompt修改、任务类型变化,都可能导致评判器行为改变,这时锚定集就是那个用来发现问题的“对照表”。
5. 常见问题与排查技巧实录
我实际跑这类管线时,踩过一些坑,整理成一张速查表:
| 问题现象 | 典型原因 | 排查思路 | 规避方式 |
|---|---|---|---|
| 评判器把大量不合格代码判为pass | 模型没理解测试意图,或Prompt里测试约束不够明确 | 抽取几条假阳性样本,看评判器的reason字段在说什么 | 强化Prompt中的输入输出约束,增加边界条件描述 |
| 评判器严重误伤可运行代码 | 全局评判上下文过长,关键实现被截断 | 查看长候选的上下文长度和截断位置 | 按函数拆分,局部评判为主 |
| 同一个候选不同批次的评判结果漂移 | 模型采样随机性、阈值设置太靠近边界 | 对比多批次评分分布 | 三次采样投票,校准阈值 |
| 评判器与真实测试结论大面积冲突 | 任务类型超出评判器能力,比如涉及外部IO或并发 | 检查冲突样本是不是都有环境相关特征 | 这类任务直接改用真实测试,不硬用评判器 |
| 评判器输出格式不稳定,解析失败 | 模型输出里混入了额外文字 | 查看原始输出,定位格式破坏点 | 在Prompt里强调“只输出JSON”,并加重试逻辑 |
| 局部评判调用次数过多,成本回升 | 所有候选都跑全量局部细查 | 统计全局粗筛的淘汰率 | 提高全局粗筛的严格度,优化裁剪策略 |
这里面最值得展开说的是最后一条。局部评判虽然单次便宜,但乘上候选数和测试数之后,总量也不容小觑。我见过一个项目,为了拿到细粒度结果,对每个候选的每个测试都跑一次judge,结果成本比原来跑真实测试还高。后来加了全局粗筛,先用一次调用把明显不行的候选筛掉,整体成本立刻降了一个数量级。记住,LLM评判器的意义在于“减少不必要的执行验证”,不是“让执行的次数更多、更便宜”。
还有一个容易被忽略的坑:评判器对unsure的使用。如果模板里允许unsure,模型会倾向于在复杂情况下选择unsure,导致大量样本需要人工介入。遇到这种情况不要慌,可以把unsure视为“需要真实测试兜底”,在管线里自动把这类样本送去执行环境验证。这样既保留了评判器的效率,又保住了判断的可靠性。
6. 实际使用后的几点体会
这个框架对我的最大启发,是评估不该被绑定在“执行”这一个维度上。过去我们习惯性地认为,要判断代码正不正确,就必须运行它。但LLM评判器提供了一种新的可能:当执行代价过高时,先用模型推理做一轮语义判断,把真正需要执行验证的样本缩小到很小的范围。
我实际用下来,最舒服的组合是“LLM评判器做初筛和打分,一个小规模真实测试集做锚定校准,unsure样本自动转真实执行”。在这个组合下,评估成本大概能降一个数量级,而且评判器的分数和真实测试的排序相关性也能维持在比较理想的水平。省下来的预算,我会转而投到增加候选数量和迭代轮数上,这样整体的代码改进效果反而更好。
最后再分享一个小技巧:评判器的reason字段别只拿来调试,可以格式化成结构化的修改建议,直接追加到智能体的下一轮Prompt里。实测下来,有了这些细粒度的失败原因之后,智能体修代码的命中率会有肉眼可见的提升。本质上,这不只是降本,还把原本浪费在“环境准备”上的精力,重新放回了真正有价值的事情上。