Berkeley 那篇 BFCL v3 博客 表面说的是「新增 1000 条多轮题目」,读完源码后我认为真正的动作在别处:它把评测口径从「AST 比对一条正确轨迹」换成了「让模型的函数调用真跑进一个沙盒,拿沙盒对象的属性做 diff」。这是软件测试从 mock 换成 integration test 的那次古老转弯,搬到 Agent 评测里再走了一遍。
我读 v1/v2 时觉得 BFCL 是一个”格式对不对”的榜——生成的 API 调用能不能 parse、参数键值对能不能和标注对上。看到 v3 引入多轮的第一反应也是”哦,加轨迹匹配了吧”,直到把 multi_turn_checker.py 拉下来对着看:一半判据在 diff 对象属性,一半在做 response 的无序子集匹配,还有一段更严格的顺序判据被作者亲手注释掉了。那段注释掉的代码是这次读源码最大的收获——它把这次口径切换的理由,直接写在了未提交的分支上。
名词速查
深度文默认在导语后放这张表;这些词后文会反复出现。
| 术语 | 一句话解释 |
|---|---|
| function calling | 让模型输出结构化的函数调用(名字 + JSON 参数),由外部 runtime 执行,再把结果喂回模型 |
| AST 匹配 | 把模型输出的调用 parse 成语法树,和一个”标准答案”树在结构上比对。BFCL v1/v2 的默认判据 |
| 状态评测 (state-based eval) | 让模型的调用真跑进后端沙盒,再比对沙盒对象的属性——不看走了什么路,看走完之后世界长什么样 |
| 响应评测 (response-based eval) | 状态之外的兜底:对读操作(不改状态)看返回值集合是否覆盖标注答案 |
| subset / 子集匹配 | 模型的调用集合⊇标注答案集合就算过——允许模型额外调、允许换顺序,只要该做的都做了 |
| 多轮 (multi-turn) | 用户和模型多次往返;每一轮内部还可能”多步”(模型自己连续调用多个函数) |
| initial_config | 每条测试题在评测开始前灌到后端的初始状态(文件系统树、tweet 表、订单表……),定义了这局的世界 |
| involved_classes | 这条题用到的沙盒后端类。BFCL v3 有 8 个:文件系统、交易机器人、订机票、车控、发帖、消息、工单、数学 |
| τ-bench | Anthropic 22 年那套多轮工具评测,也走状态比对,是这条路线的早期同行者。见站内τ-bench 姊妹地图 |
一句话主线
BFCL v3 之所以要换口径,是因为多轮任务在同一个目标下天然存在多条合法路径这条约束——任何把标注答案写死成一条轨迹的判据,都会把探索、纠错、冗余但正确的操作全部误判成错误。BFCL v1/v2 的 AST 匹配之所以能撑住,是因为单轮 function calling 里”正确轨迹”就只有一条;进入多轮,这个前提消失。
反事实测试:如果这个约束不存在——比如所有多轮题目都能被压缩成”唯一正确轨迹”——BFCL v3 大可以继续用 AST 匹配,题库扩容就行了。但 200 条基础多轮里我扫过的第一条 multi_turn_base_0(GorillaFileSystem 上的四轮操作:创目录 → 移文件 → grep → sort → diff)已经不可能只有一条走法。判据必须换轴,不然分数只是在惩罚”看起来不像标准答案的正确做法”。
门槛铺垫:从 BFCL v1 讲起,你才能看懂 v3 换了什么
如果你熟悉 v1/v2 可以跳过这节。铺垫不是注水,是为下节的对比留地基。
BFCL v1(2024 年初) 想解决的问题很干净:给模型一堆 API 文档,看它能不能生成能跑的调用。评测方式两档——
- AST 评测:把模型输出的调用 parse 成 AST,和标注答案 AST 逐字段比。参数键值对集合相等、简单表达式(
2+3)按语义等价处理、多函数调用集合无序匹配。 - 可执行评测:少数题跑一遍真实 API,看返回码。
BFCL v2(24 年中) 加了 “live” 分区——用户贡献的真实场景题,补上 v1 单纯合成数据的偏差。评测方式没变,还是 AST + 可执行。
到 v2 为止,BFCL 测的都是单次调用的形状对不对。每道题的答案空间几乎唯一——比如”查上海邮编”,标答就是 get_zipcode_by_city(city="Shanghai"),你写成 get_zip_code(city="Shanghai") 或者 get_zipcode_by_city(城市="Shanghai") 就是错。这个前提下 AST 匹配非常合理。
v3 想加多轮,前提就崩了。多轮题目的目标是”用 GorillaFileSystem 把 final_report.pdf 挪到 temp/,然后 grep ‘budget analysis’”:
- 我可以先
mkdir temp/再mv,也可以先mv(自动创目录)——两条路径达到同一状态。 - 我可以先
pwd探一下当前目录再动手,也可以直接开干——多一次调用不代表错。 - 我可以先
grep定位再sort,也可以先cat全文再肉眼扫——同一结果多种手段。 - 中途
rm错了文件再cp恢复,如果最终状态对,也应该算过。
如果继续用 AST 匹配一条标准轨迹,以上四种情况全部会被打成”错”。你会系统性地把善于探索、善于纠错的模型排在后面——这正是 agent 评测最不该干的事。
根本约束和一次换轴
一句话:当同一目标存在多条合法路径,评测就不能盯路径,只能盯终点。这是从软件测试搬来的老经验——mock-based 测试(interaction testing)看”函数按什么顺序被调用”,integration testing 看”跑完之后数据库/文件系统长什么样”。BFCL v3 是把这个二分复刻进 agent 评测里。
反事实里最朴素的做法先崩在哪儿?我在读源码时构造了这么一个用例:标注答案是 [mkdir("temp"), mv("final_report.pdf", "temp/")],模型输出的是 [mv("final_report.pdf", "temp/")](它假设 mv 自动创目录,这是合理假设)。AST 匹配立刻报错——集合不等,少一个 mkdir。但如果我在沙盒里跑一遍,mv 真的把目录建出来了,最终状态和标答完全一样。这就是 AST 匹配在多轮场景下的第一个数量级失效:假阴率会随轨迹长度指数上升,因为每一步都可能有等价替代。
共同祖先值得挂一下——软件工程里 Kent Beck、Martin Fowler 那批人在 2000 年前后就在讨论 “mockist vs classicist”(Mocks Aren’t Stubs, 2007):Mockist 派用 mock 检查交互轨迹,Classicist 派用真依赖检查最终状态。行业最后收敛到”单元测试用 mock、集成测试用真状态”,因为真实系统的正确路径不唯一。BFCL v3 到 v2 的评测差异,和 mockist → classicist 那一次是同构的。
源码窥探:三个判据、被删掉的第四个
打开 multi_turn_checker.py(全文 314 行),核心是三个函数,按调用顺序:
# 伪代码,对应 multi_turn_checker.py 的 multi_turn_checker() 主干
# (原文 100+ 行,含错误处理、初始化,这里只留决策骨架)
for turn_index, gt_calls in enumerate(ground_truth_list):
# 1) 把模型这一轮的所有函数调用真跑进沙盒
model_instances = execute_multi_turn_func_call(
func_call_list=model_response_list[turn_index],
initial_config=initial_config, # 这一局的初始世界
involved_classes=involved_classes, # 用到的后端类
)
# 2) 同样跑一遍标注答案,拿到 ground_truth_instances
ground_truth_instances = execute_multi_turn_func_call(gt_calls, ...)
# 3) 判据一:状态比对——遍历每个后端类的实例,diff 属性
if not state_checker(model_instances, ground_truth_instances):
return "fail: instance_state_mismatch"
# 4) 判据二:响应比对——把模型这一轮和之前所有轮的返回值合起来,
# 检查标注答案的返回值集合是不是其无序子集
if not response_checker(all_turn_model_execution_results,
gt_execution_results, turn_index):
return "fail: execution_response_mismatch"
# # 5) 判据三:方法调用顺序——已被作者注释掉,见下文
# if not method_invoke_order_checker(model_instances, ground_truth_instances):
# return "fail: method_invoke_order_mismatch"
三个判据的分工非常清晰:
判据一:state_checker(定义在 143 行)。真跑之后,对每个 involved_classes 里的实例做 vars(),逐属性比对。文件系统类 GorillaFileSystem 就 diff 目录树结构;订机票类 TravelBooking 就 diff 预订表。哪个属性不等,报 instance_state_mismatch。这是主力判据,治所有”改状态”的操作。
判据二:response_checker(163 行)。有些函数是纯读——get_current_directory、list_files、get_zipcode_by_city——不改状态,state_checker 抓不到。这时判据二上:把模型截止到这一轮的所有返回值汇成一个列表,标答本轮返回值必须是它的无序子集。注意”截止到这一轮的所有返回值”这个设计——意味着你可以在前几轮就把这个信息读到,后面不用再读一遍,这是有意留给探索型模型的空间。
判据三(死掉的那个):method_invoke_order_checker(定义在 224 行)。这段代码定义完整、逻辑清晰:检查模型实例调用方法的顺序,是标注答案调用顺序的有序子序列——模型可以多调,但不能漏、不能颠倒。你猜怎么着——它的调用点在主循环里被注释掉了(122-127 行)。作者留了一段注释说这个判据”目前只检查方法名不检查参数”。
这段被注释掉的代码是这次读源码的最强证据。它告诉你:
- 作者一开始想要更严的判据——不只是终点对,过程顺序也要对。
- 落地试了之后放弃了。原因大概率是误杀太高:多轮里”该按什么顺序调用”经常有多解。
- 但代码没删,留在源码里当”设计过、拒绝了”的化石。
这是本次的第一条一手结论:BFCL v3 的评测口径不是一次到位的,是一次从 AST 匹配 → 更严的 order 检查 → 最终收敛到 state + response subset 的收缩。作者的路径你直接就能看见。
200 × 4 条题目的实盘核对
Berkeley 博客说 v3 加了 5 类多轮题目,共 1000 条:base(200)+ miss_func(200)+ miss_param(200)+ long_context(200)+ composite(200)。我把数据目录拉下来数了一遍:
# 我实际跑的命令(替换成 curl 版,方便你也跑一次):
for f in multi_turn_base multi_turn_miss_func multi_turn_miss_param multi_turn_long_context; do
n=$(curl -sL "https://raw.githubusercontent.com/ShishirPatil/gorilla/main/berkeley-function-call-leaderboard/bfcl_eval/data/BFCL_v4_${f}.json" | wc -l)
echo "$f: $n 行(每行一条题)"
done
# 输出:
# multi_turn_base: 200
# multi_turn_miss_func: 200
# multi_turn_miss_param: 200
# multi_turn_long_context: 200
四类各 200 条,和博客一致。但 BFCL_v4_multi_turn_composite.json 不存在。可 multi_turn_checker.py:46 里还写着 long_context=("long_context" in test_category or "composite" in test_category)——代码留着钩子,数据没跟上。
这算什么?第二条一手结论:Berkeley 博客里说的 “composite(200 条,把三种增强揉在一起)” 这一类事实上未在公开数据集里发布。可能是当时的开发计划,可能因为标注太贵砍掉,也可能后来演化成了 v4 的其他类(memory / web_search)。读别人的博客要拿源码/数据对一遍,博客描述的能力和实际能跑的能力经常差一节。
顺便看一下四类题目在判据上的分工——这才是真的干货:
| 类别 | 场景 | 关键判据 | 想测什么 |
|---|---|---|---|
| base | 完整信息、正常多轮 | state + response | 基础的多轮 tool use 能不能跑通 |
| miss_param | 用户消息缺关键参数 | irrelevance:该轮 ground truth 为空,模型也必须空调用 | 模型该不该主动追问,还是硬编造参数 |
| miss_func | 需要的函数不在工具列表 | irrelevance + 后续轮补齐工具后的 state | 模型该不该识别”没工具”,而不是硬凑 |
| long_context | 沙盒里塞几百个无关文件/几千条无关记录 | state + response(阈值不变) | 长上下文里能不能保持正确 |
miss_param 和 miss_func 用的是 multi_turn_irrelevance_checker(133 行):这一轮 ground truth 是空列表,那模型的输出也必须为空——不能瞎调。“该问问题时不能瞎调” 被形式化成了 “irrelevance 判据”,非常干净。
那几个真实的翻车样式
Berkeley 博客举了三种典型错误。我把它们和上面的判据分工挂一挂,你就能看出每种错误落在哪个判据上翻车:
-
隐式动作不做:用户说”加最省钱的油”,模型直接
fillFuelTank(50)——没先getFuelLevel()看当前存量。翻在判据一(state):最终油箱状态和标答不一致(标答会先查后加,只加差额)。这是”探索前置”能力缺失的直接体现。 -
状态误判:用户说”在 alex 目录下建 alex 目录”,模型没先
pwd就mkdir alex,结果建到了错误层级。翻在判据一:目录树结构不匹配。不可逆操作的可怕之处在这里露头——如果这是rm而不是mkdir,评测挂了事小,生产环境挂了事大。 -
过度规划:系统已认证,模型还硬调
authenticate()。翻在判据一 + 二:多出的 auth 调用改了 session 状态(判据一),或者返回了不该返回的东西(判据二)。这条最有意思——过度谨慎和过度粗心在 state-based 评测里都会翻车,判据没有偏向哪一头。
三条错误合起来看:BFCL v3 的判据实际上在训练模型学会一件事——先看世界的状态,再决定动作。这个诉求和 ReAct 论文里那个”Reason + Act 交替”的模式是自洽的,只是 BFCL 用评测的方式把它变成了硬约束。
我做 Veral 时能带走的三件事
我在做 Veral 评测平台 时挑评测方案挑过一阵。读完 BFCL v3 源码,我要把这三条写进内部选型文档:
第一,判据分层。 单一判据吃不下所有函数——写操作用 state diff、读操作用 response subset、行为顺序(如果真的重要)单独加一个 optional order check。BFCL v3 把三层拆得干净,直接可以抄它的接口分工。
第二,判据本身要留改造空间。 BFCL v3 的源码里那段注释掉的 method_invoke_order_checker 就是最好的证据——判据不是一次性写死的,而是随着测评经验不断收缩边界。上一篇《先测判据,再测 Agent》里我用作弊脚本给判据体检,BFCL 用”注释掉但不删”给判据留出历史。都是承认判据是活的这个事实。
第三,initial_config 的价值被低估。 BFCL 每条题目都自带一份 initial_config——一个能被沙盒直接加载的世界初值。这个字段的价值不只是”给评测用”,更是让每一条测试题变成一个可复现的最小场景。业务评测经常败在环境不一致(每个人跑一次结果不同),BFCL 用 JSON 里嵌套的初值定义把这个问题从根上解掉了。
「诚实的提醒」段
- 我没跑过完整的 BFCL v3 评测流水线——环境部署、模型接入、跑完 1000 条题目要花一台机器跑一天,本次没做。本文所有数字要么来自我 wc -l 数出来的样本数,要么来自 Berkeley 博客/源码,分类清楚。
- Composite 那类未发布的判断是我全仓检索的结论(仓库根搜过
composite,只在 checker 代码里出现,数据目录里没有对应 JSON)。有可能仓库有历史 tag 里存过,我没翻 git log 追溯。 - 关于
method_invoke_order_checker被注释的具体历史,我看的是当前 main 分支的静态代码。如果你想追溯它被注释的具体 commit,可以git log -L :method_invoke_order_checker:multi_turn_checker.py一句话拿到时间线——一条命令,大约 30 秒。这条追溯我留给了对判据演化最感兴趣的读者做。
小结
三行压缩:
- 根本约束:多轮 agent 任务同一目标有多路径 → 评测口径不能盯轨迹,只能盯终点。
- 崩点:AST 匹配在多轮里假阴率随长度指数上升——每一步都可能有等价替代,轨迹匹配就是在惩罚”看起来不像标答的正确做法”。
- 可带走:判据要分层(state / response / order),要给自己留可撤回的空间(注释而不是删除),要把 initial_config 当一等公民而不是附庸。
5 分钟第一步
你如果只想验证”BFCL v3 的判据真的在做状态 diff”,拿这条命令:
curl -sL https://raw.githubusercontent.com/ShishirPatil/gorilla/main/berkeley-function-call-leaderboard/bfcl_eval/eval_checker/multi_turn_eval/multi_turn_checker.py \
| sed -n '100,145p'
看第 104-107 行的 state_checker 调用、118-121 行的 response_checker 调用、122-127 行被注释掉的 order checker。三段代码 40 行看完,这次口径切换的完整决策链就在你眼前。用时不超过 5 分钟,只需要有 curl。
参考来源
工程实践:
- Berkeley Function Calling Leaderboard(主页 + 榜单)
- BFCL v3 官方博客(本文的深读对象)
- BFCL 源码仓库(Gorilla 项目下)
multi_turn_checker.py(本文核心源码定位)- Fowler:Mocks Aren’t Stubs(mockist vs classicist 的原文)
arXiv 论文:
- Gorilla:Large Language Model Connected with Massive APIs(arXiv:2305.15334)——BFCL 项目的发源论文
- BFCL 正式论文,ICML 2025——把 v1/v2/v3 三个版本的方法学一次说清
- τ-bench(arXiv:2406.12045)——同期状态比对路线的另一条支流
- ReAct(arXiv:2210.03629)——“先看世界再动作”的原始表述
站内回链:
- 大模型 Benchmark 地图:τ-bench 之外——BFCL 在整张评测地图里的位置
- 先测判据,再测 Agent:业务评测样本的构造方法与一次作弊下界实测——判据本身怎么被体检
Hello Agents全书深读——BFCL 作为”工具调用能力”评测在书里被引到- Agent Loop 里的小模型岗位——xLAM 78.94% BFCL 分数的语境