Skip to main content

02 - 工具集与轨迹数据

这一篇产出训练集和评测集两个文件。全程在自己电脑上做,不花机时。

时间上它是整个项目里最长的一段 —— 造数据大概要一到两天,训练只要三小时。这个比例是正常的。

一、你手上的聊天记录为什么不能直接用

假设你已经跑了一阵子 Agent,导出了一批对话记录,看起来是这样:

用户:查一下华东区上个月的退款订单
助手:华东区上个月有 12 笔退款,总金额 4.3 万元

拿这个去训练,模型学到的是「听到这句话,就直接编一个数字出来」。它学会了模仿答案,没学会去查。

中间被省掉的是真正重要的部分:助手调了哪个工具、参数怎么填的、工具返回了什么、它拿到结果之后怎么组织成回答。这些在聊天界面上是隐藏的,但恰恰是你要训的东西

带上这些中间步骤的完整记录,叫做一条轨迹(trajectory)。这一篇就是在讲怎么把它凑齐。

二、第 1 步:定下工具集

工具集是这个项目的地基 —— 训练数据、评测集、推理时的 system prompt,三处用的必须是同一份文件

2.1 写几个、怎么写

给第一个项目的建议是 4 到 8 个工具。少于 4 个学不出「选择」这个行为,多于 8 个数据量要求陡增。

工具定义用 OpenAI 那套 JSON Schema 格式,Qwen3 的模板原生认它:

TOOLS = [{
"type": "function",
"function": {
"name": "query_orders",
# description 是模型判断「该不该用这个工具」的唯一依据。
# 写「查询订单」是不够的 —— 模型分不清它和 query_refunds 的区别。
# 要把边界写进去:能查什么、不能查什么。
"description": "按地区和时间范围查询订单列表。只返回订单号和金额,"
"不含商品明细;要明细用 get_order_detail。",
"parameters": {
"type": "object",
"properties": {
"region": {"type": "string", "description": "地区名,如「华东」"},
"start_date": {"type": "string", "description": "YYYY-MM-DD"},
"end_date": {"type": "string", "description": "YYYY-MM-DD"},
},
# required 会直接影响 L3 参数正确率的判分,务必写准
"required": ["region", "start_date", "end_date"],
},
},
}]

description 里要写「不能做什么」。 只写能做什么,模型会把所有沾边的问题都丢给第一个工具 —— 这是 L2 工具选择正确率上不去最常见的原因。

2.2 工具要不要真的能跑

要。至少得是个假的但行为一致的实现:给定参数返回固定结果,参数缺失就报错。

原因在第五节:你需要工具报错,才能造出「报错之后怎么办」的训练数据。

三、第 2 步:看懂一条轨迹的结构

一条轨迹就是一个 messages 数组,记录了一个任务从提问到答完的全过程:

[
{"role": "system", "content": "你是订单助手...", "tools": TOOLS},
{"role": "user", "content": "查一下华东区上个月的退款订单"},
# assistant 这一轮没有回复文本,只有工具调用
{"role": "assistant", "tool_calls": [{
"id": "call_1",
"function": {"name": "query_orders",
"arguments": '{"region":"华东","start_date":"2026-08-01","end_date":"2026-08-31"}'}
}]},
# tool 角色装的是工具执行的真实返回,用 tool_call_id 和上面配对
{"role": "tool", "tool_call_id": "call_1", "content": '{"count":12,"total":43120}'},
{"role": "assistant", "content": "华东区上个月有 12 笔退款,合计 4.31 万元。"}
]

3.1 整条轨迹是一条训练样本,不是五条

初学者常见的第一个疑问:这五轮,是拆成五个样本分别训,还是拼成一个?

拼成一个。 整条轨迹按 chat template 拼成一段长文本,做一次前向。这样效率高得多,而且模型能看到完整的上下文。

3.2 训练时到底在算什么

拼成一段之后会发生什么,得先看清楚 SFT 的一步训练长什么样:整段文本变成 input_ids 喂进模型,模型在每个位置吐出一个词表大小的概率分布,再拿它去对照「下一个 token 实际是什么」,算交叉熵。

SFT 一步训练的数据流:input_ids 喂进模型得到 logits 矩阵,与右移一位的 labels 一起送进交叉熵,得到一个标量 loss
图出自 TRL 官方文档。注意左下角标着 labels 是「shifted input_ids」—— 位置 i 要预测的是位置 i+1 的 token,所以上下两条错开一格,两端各空出一格斜纹。

问题就出在 labels 这一行:它现在覆盖了整段文本,而这段文本里有一部分不是模型说的。 user 的提问是用户说的,tool 的返回是程序给的。照这样算下去,模型会去学「预测用户接下来会说什么」和「预测工具会返回什么数字」。

3.3 掩码:把不该学的位置设成 -100

好在 labels 不必和 input_ids 一一对应地全都参与计算。把某些位置换成一个特殊值(PyTorch 里约定是 -100),交叉熵就会跳过它们。

按角色打掩码后的训练序列:上行 input_ids 里 user 段是红色、assistant 段是绿色;下行 labels 里 user 段被画成斜纹表示已设为 -100 不参与 loss,只有 assistant 段是实心的蓝色格子
同样出自 TRL 文档。下面一行的斜纹格子就是被设成 -100 的位置。它们仍然完整参与前向计算 —— 模型看得见这些内容,把它们当上下文用,只是不去预测它们。这张官方图只画了 user 和 assistant 两种角色;轨迹数据里还多一个 tool 角色,它和 user 一样要被掩掉。

这套「只在 assistant 说的段落上算 loss」的做法,就叫 loss mask

掩码写错不会报错,只会让模型学会编造

如果把 tool 那几段也算进 loss,模型学到的是「预测工具会返回什么数字」—— 推理时它可能压根不调工具,直接把编好的「返回」和「回答」一起吐出来。

LlamaFactory 会替你打这个掩码,你不用手写。但它是整个项目里最容易静默出错的一处:错了不报错,只是训出来的模型行为诡异。03 篇会讲怎么在开跑前把掩码打印出来验一遍。

Qwen3 的思考块要单独决定

Qwen3 可以在回答前先输出一段思考内容。训练数据里带不带这段,必须现在就定死。

给第一个项目的建议是不带 —— 数据里去掉思考块,推理时也关掉思考模式。带上会让序列长度翻倍(显存和时间都翻倍),而工具调用的收益主要来自判断层,不来自思考层。

真正的坑是两边不一致:训练数据不带、推理时开着思考模式,模型会在该输出工具调用的位置输出思考文本,L1 格式合规率直接崩。

四、第 3 步:数据从哪来

这是整个项目最容易卡住的地方,因为它看起来是个鸡生蛋问题:要训一个会用工具的模型,得先有几千条「正确使用工具」的轨迹;可这些轨迹本来就该是模型产出的。

破局点是把一条轨迹拆成两半:

半边内容谁来提供
任务用户会问什么,但只要问题,不要答案
轨迹怎么一步步做完、调了什么、参数填了什么强模型生成,由你的工具真实执行来验对错

看清这个拆法之后,问题就从「哪来几千条标注数据」变成了「哪来两百条用户问题」。后者容易得多 —— 而且不需要你懂正确答案是什么,答案由工具执行结果决定。

4.1 任务从哪来:三条路

来源怎么做质量什么时候用
线上真实日志从已有 Agent 的对话记录里抽用户的第一句话最高,分布是真的你的 Agent 已经在跑
从工具定义反向生成把工具集喂给强模型,让它编「什么样的用户问题会用到这些工具」中,覆盖面好但偏理想化冷启动,这是主力
公开集改造拿公开数据集的任务模板,把工具换成你的低,语义常对不上补某个类别的缺口

反向生成是冷启动时唯一可行的路。关键在提示词里必须显式点名要哪几类

# 不加这段约束,强模型只会生成一帆风顺的理想任务 —— 而评测集六个类别里
# 有两个(不该调工具、工具会报错)专门测的就是理想之外的情况。
# 训练集缺这两类,训出来的模型就是「顺境很好、逆境卡死」。
PROMPT = f"""下面是一个订单助手可用的工具集:
{json.dumps([t["function"] for t in TOOLS], ensure_ascii=False, indent=2)}

生成 40 条用户可能会说的话,按这个比例分配:
- 12 条 单轮直球:一个工具就能答
- 8 条 需要从话里抽参数:日期说成「上个月」、地区说成口语
- 6 条 不该调任何工具:闲聊、问规则、问常识
- 6 条 会让工具报错:不存在的订单号、非法的地区写法
- 8 条 需要连调两次:先查 A 拿到 id,再用 id 查 B

只输出用户说的话本身,不要输出答案,每行一条。"""

只要问题不要答案 —— 这是这一步能便宜的原因。强模型编问题很在行,编答案则可能编错,而错的答案混进训练集比没有更糟。

4.2 轨迹怎么生成并验证

拿到任务之后,让强模型带着你的工具集去跑,把跑对的过程留下来。这套流程有现成的参考 —— Salesforce 做 xLAM 数据集时的 APIGen 流水线:

APIGen 数据生成流水线:API 库与种子问答数据分别采样后填进提示词模板,交给 LLM 生成查询和函数调用答案,再依次经过格式校验、执行校验、语义校验三级过滤,通过的数据回流成新的种子
图出自 APIGen 项目页(Salesforce,xLAM 数据集就是这么造的)。左边两个虚线框是输入:API 库就是你的工具集,种子问答数据就是 4.1 节那些任务。右下角绿色那一大块是关键,三级校验缺一不可。

图里的三级校验,正好对应 gen_trajectories.py 里做的三件事:

图里的脚本里的拦住什么
Format Checker (S1)json.loads(tc.function.arguments)输出根本不是合法 JSON
Execution Checker (S2)T.call() 真的执行一遍参数格式合法但跑不通(比如 region 写成「华东地区」)
Semantic Checker (S3)judge() 对照任务里的 expect跑通了,但答的不是问的那件事

S2 是这套流水线真正的支点。 如果只有 S1 和 S3,你是在用一个语言模型判断另一个语言模型对不对 —— 两边可能一起错,而且错得一致(同源模型的盲区是一样的)。让工具真的跑一遍,是整条链上唯一不依赖模型判断的一环。

这就是 2.2 节要求工具必须可执行的原因:不是为了好看,是为了让 S2 存在。

4.3 蒸馏为什么成立,以及它的天花板

原理:学生模型不需要重新发现「什么时候该调哪个工具」。判断已经被老师做完了,学生要学的是在同样输入下模仿老师的输出分布。这比从零学容易一个量级 —— 也是几千条数据就够的根本原因。

天花板也就在这里

蒸馏出来的学生不会超过老师。 三级校验只是把老师做错的那部分滤掉,让学生学到的是老师最好的一面 —— 但天花板还是老师画的。

想突破只有两条路:换更强的老师(简单,加钱),或者上强化学习(下一个专题)—— RL 之所以有可能超过老师,是因为它的奖励信号来自环境的真实反馈,不来自任何一个模型的示范

理解这一点,也就理解了为什么这三个项目的顺序是 SFT 在前、RL 在后:SFT 把学生拉到老师的水平,RL 才有个够格的起点往上爬。

4.4 公开数据集只用来打底

以下四个是 2026-09-03 实测存在且仍在被下载的:

数据集许可特点
Salesforce/xlam-function-calling-60kcc-by-4.0(需接受条款)6 万条,就是上面那套流水线的产物,质量整齐但基本是单轮
NousResearch/hermes-function-calling-v1apache-2.0含多轮,格式和 Qwen 的 hermes 解析器对得上
Team-ACE/ToolACEapache-2.0工具种类多,覆盖面好
glaiveai/glaive-function-calling-v2apache-2.0老牌,量大,质量参差

为什么它们只能打底、不能当主力:里面的工具不是你的工具。模型从中学到的是「工具调用这件事大概长什么样」这种通用格式感 —— 有用,但学不到「你这 6 个工具该怎么选」,而后者才是 L2 和 L3 的分数来源。

混进去 30% 左右。 全用公开数据,训出来的模型在你自己的工具集上不会有明显提升。

4.5 采样与筛选:三堆里留两堆

五、第 4 步:别把失败轨迹全丢掉

这是初学者最容易犯、后果又最明显的一个错。

直觉上,训练数据当然要用「做对了」的例子。于是把所有中途报错的轨迹全删掉,留下一批一次成功的干净数据。

训出来的模型会在工具报错时卡死。 因为在它见过的世界里,工具从来不报错 —— 它没有任何关于「报错之后该干什么」的样本。而线上工具报错是家常便饭:参数格式不对、订单不存在、接口超时。

要留的是这种形状的轨迹:

assistant → 调 query_orders,region 填了「华东地区」
tool → 报错:region 只接受「华东」这类简称
assistant → 改成「华东」重新调 ← 这一步是最值钱的训练信号
tool → 返回 12 条
assistant → 正常回答

比例控制在两成左右。 太少学不到,太多会让模型养成「先随便试一次再纠正」的坏习惯 —— 白白多花一轮工具调用。

六、第 5 步:数据量和去污染

6.1 要多少条

用途条数
训练集3000 到 8000 条
验证集200 到 500 条
评测集01 篇那 20 到 50 条

不需要几万条。 LoRA 训的参数很少,几千条足够让它在一个 6 到 8 个工具的固定工具集上学出稳定行为。数据从 3000 加到 30000,收益远小于把这 3000 条的质量提上去。

6.2 三个集合不能重叠

评测集里的任务,一条都不能出现在训练集里。听起来是废话,但有个隐蔽的漏法:你用同一批种子任务去蒸馏数据,又从里面挑了几十条当评测集。

这样测出来的分数是虚高的,模型只是背下了答案。

正确的顺序是:先把评测任务单独划出来锁死,再拿剩下的去蒸馏。 划分之后写个脚本按任务文本查一遍重复,确认零交集。

七、验收清单

  • 工具定义文件写完,4 到 8 个,每个的 description 都写了「不能做什么」
  • 工具有可执行的实现,参数错误时会报错
  • 决定了思考块带不带(建议不带),并记在配置里
  • 评测任务在造数据之前就已经划出来锁死
  • 训练集 3000 条以上,其中约两成是「报错后纠正」的轨迹
  • 公开数据集混入比例不超过三成
  • 脚本验过训练集与评测集零交集

八、记下来

工具个数待填
蒸馏用的强模型与版本待填
蒸馏 API 花费待填
训练集条数 / 验证集条数待填
其中报错纠正类占比待填
公开数据集混入比例与来源待填
轨迹平均 token 数待填

最后一行别漏。 它直接决定 03 篇cutoff_len 该设多少 —— 设小了长轨迹被截断,模型永远学不到最后那个回答。