我打算用强化学习训练一个小模型做多步工具调用。动手训练之前,先要知道它现在差在哪。这篇记录 Qwen3-1.7B 在 BFCL 上的基线,以及我从日志里看到的失败方式。
结论先说:
- 单轮工具调用它已经做得不错,大多在 80~93 分。
- 多轮任务只有 2~20 分。
- 多轮里最突出的问题是工具报错之后的处理:不开思考时,它多半说一句”让我先检查一下”就停了;开思考时,它会重试,但经常是把同一个错误调用原样再发一遍。
实验设置
| 项目 | 配置 |
|---|---|
| 模型 | Qwen/Qwen3-1.7B,BF16,未量化 |
| 推理 | vLLM 0.11,单张 RTX 4090 |
| 评测 | bfcl-eval 2026.3.23,官方 Qwen FC handler |
| 单轮 | 5 个类别全量 |
| 多轮 | 4 个类别各固定抽 50 条,共 200 条 |
| 对比 | 关闭思考 / 开启思考 |
多轮只跑了子集,所以多轮分数的误差比较大,50 条里多对一条就是 2 分。下面的多轮数字适合看量级,不适合比较小差异。
分数
单轮(准确率 %):
| 类别 | 题数 | 关闭思考 | 开启思考 |
|---|---|---|---|
| simple_python | 400 | 92.25 | 91.75 |
| multiple | 200 | 93.0 | 93.5 |
| parallel | 200 | 82.5 | 87.0 |
| parallel_multiple | 200 | 77.5 | 82.0 |
| irrelevance | 240 | 85.42 | 78.75 |
多轮(50 条中答对的条数):
| 类别 | 关闭思考 | 开启思考 |
|---|---|---|
| multi_turn_base | 5 | 10 |
| multi_turn_miss_func | 1 | 7 |
| multi_turn_miss_param | 3 | 7 |
| multi_turn_long_context | 3 | 6 |
两点观察:
- 单轮和多轮之间是断崖。同一个模型,”选对函数、填对参数”能做到 90 分,”连续做几步并保持状态正确”只有 10 分上下。
- 思考对多轮有帮助(5 → 10),但在 irrelevance 上反而掉了 6.7 分:开了思考之后,它更容易在不该调用工具的时候调用。
多轮的评分方式很严:一条用例有好几轮对话,只要最终环境状态和标准答案不一致,整条算错。第一轮走错目录,后面几轮基本都跟着错。所以 10 分不等于”只有 10% 的步骤是对的”。
工具报错之后发生了什么
多轮用例里,工具会返回真实的执行结果,包括报错。我统计了全部 200 条用例日志里,模型遇到报错后的下一步动作。报错的判定方式是工具返回内容里带 error 字段。
先看有多少轮对话遇到了报错,以及这些轮次是怎么结束的:
| 关闭思考 | 开启思考 | |
|---|---|---|
| 有日志的对话轮数 | 824 | 775 |
| 其中至少遇到一次工具报错 | 309(37.5%) | 235(30.3%) |
| 这一轮最后一次调用仍是报错 | 256(82.8%) | 108(46.0%) |
| 这一轮最后一次调用成功 | 53(17.2%) | 127(54.0%) |
再看每一次报错之后,模型紧接着做了什么:
| 报错后的下一步 | 关闭思考(491 次) | 开启思考(700 次) |
|---|---|---|
| 不再调用工具,直接结束这一轮 | 256(52.1%) | 108(15.4%) |
| 把完全相同的调用再发一遍 | 82(16.7%) | 288(41.1%) |
| 换了一个调用,仍然报错 | 40(8.1%) | 121(17.3%) |
| 换了一个调用,执行成功 | 113(23.0%) | 183(26.1%) |
两种模式失败的方式不一样。
关闭思考:说要检查,然后停下
multi_turn_base_0 的第一轮,用户要求把 document 目录下的 final_report.pdf 移到 document/temp,并先建好目录。当前目录是 workspace。
模型一步发出两个调用:
1 | mkdir(dir_name='temp') |
mv 返回:
1 | {"error": "mv: cannot move 'final_report.pdf': No such file or directory"} |
模型的回复是:
The error indicates that the file
final_report.pdfdoes not exist in the current directory. Let’s first check if the file exists and then proceed with the move operation.
然后这一轮就结束了,它没有发出任何检查的调用。
它对错误的理解是对的,下一步的计划也是对的,但没有执行。正确做法只需要先 cd(folder='document')。结果是 temp 建在了错误的位置,文件没动,后面三轮都建立在错误的状态上。
同一条用例的第二轮还有另一个问题。用户要求在文件里搜索 “budget analysis”,模型调用的是:
1 | grep(file_name='your_file.txt', pattern='budget analysis') |
文件名是它编的占位符,而上一轮刚刚提到过 final_report.pdf。
开启思考:会重试,但重试的是同一个调用
multi_turn_base_24,用户要求把 temp_notes.txt 移到 archives 目录并改名为 notes_2024.txt。
模型调用:
1 | mv(source='temp_notes.txt', destination='archives/notes_2024.txt') |
工具返回:
1 | {"error": "mv: path not allowed in destination. Provide only a file or directory name."} |
报错已经说明了原因:目标参数不能带路径。接下来模型发出的是:
1 | mkdir(dir_name='archives') |
mkdir 报”目录已存在”,mv 报同样的错。之后它把这一组调用一字不差地重复,直到撞上评测的 20 步上限被强制终止。
在遇到报错的轮次里,连续调用 19 步以上的,关闭思考时有 2 轮,开启思考后有 15 轮。也就是说思考让它”不放弃”了,但没有让它”读懂报错并换一种做法”。表里”最后一次调用成功”的 54% 也要打折扣:最后一步成功不代表任务做对了。
我的理解
这些失败可以归成三类,都不是”不会用工具”:
- 不先观察就行动。不看当前目录就直接操作文件,一步里把依赖前一步结果的调用全发出去。
- 不根据报错修正。要么停下,要么原样重试。报错信息里明明有修正所需的全部信息。
- 编造参数。信息不够时填一个看起来合理的值,而不是先查。
单轮评测测不出这些,因为单轮里没有工具返回值,也没有状态。这解释了单轮和多轮之间的断崖。
局限
- 多轮每类只有 50 条,分数只能看量级。
- “报错后的下一步”是按字符串匹配工具返回里的
error统计的,没有逐条人工核对。 - 关闭思考有 3 条、开启思考有 8 条用例因为推理出错没有日志,没计入行为统计。
- 只测了一个模型。这是小模型的共性还是 Qwen3-1.7B 的特点,还不知道。
下一步
针对上面三类问题搭一个可以自动判分的训练环境,任务域不和 BFCL 重叠,训练后再回到 BFCL 上测,看提升能不能迁移过来。
想回答的问题是:用针对性的奖励,能不能让 1.7B 的模型学会”报错之后读懂原因、换一种做法”。