ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

工具调用数据合成实战:从ToolLLM到Kimi K2的演进与微调指南

2026/9/9 0:37:54 拓冰建站 浏览量
工具调用数据合成实战:从ToolLLM到Kimi K2的演进与微调指南 很多做 Agent 和 Function Calling 的团队最后真正卡住他们的往往不是模型结构而是训练数据。开源模型如 Qwen2.5、Llama 3.1 都官方支持了工具调用但真要微调成自己业务里好用、能稳定“按格式输出、按结果行动”的模型手里没有几千条高质量工具调用样本是起步不了的。ToolLLM 是最早把工具调用数据合成方法成体系地讲清楚的方案而 Kimi K2 这类新模型又在数据配比和评判信号上做了更系统的优化。这篇文章就把工具调用数据合成这件事从 ToolLLM 到 K2 的演进梳理一遍附上可直接参考的对话/训练数据构造伪代码主要面向想自己造数据微调工具调用模型的开发者。我自己做工具调用微调踩过不少坑比如参数格式漂移、多轮轨迹状态冲突、模型把工具返回值抄进最终回答等这些都不是靠调 loss 能解决的必须从数据源头规避。所以这篇不会只讲理论会把数据结构、合成流程、过滤信号、常见坑都放到一起说。1. 工具调用数据合成到底在解决什么问题1.1 工具调用能力的本质模型要学的是“动作规范”先想清楚一件事工具调用和普通对话不一样它不是让模型去写更长的自然语言而是要让模型学会在特定时机输出一个“动作”——这个动作有严格的格式约束比如函数名、参数 JSON、工具调用的顺序以及拿到工具结果之后怎么把结果纳入后续推理。普通对话数据教会模型的是“说什么”工具调用数据教会模型的是“做什么、什么时候做、做了之后怎么接着做”。这是两个维度的能力。为什么很多模型聊天很流畅但一接工具就崩就是因为训练数据里缺少“动作层面”的监督信号。ToolLLM 的核心贡献就是把“工具调用”这件事从自然语言生成任务里抽出来看成一系列关联动作的有序组合。比如用户问“帮我查一下今天北京到上海的高铁”模型需要依次做调用查询车次工具、传入参数日期、出发地、目的地、读取工具返回的 JSON、判断要不要再调用余票查询工具、最后用自然语言组织最终答案。这里面每一步都有明确的“动作标准”数据合成就是为了给模型提供这些标准样例。1.2 为什么人工标注撑不起工具调用数据规模有人会问那我直接找人标注不就行了答案是工具调用数据的标注成本远高于普通对话而且质量很难保证。一个工具调用样本至少包含四层信息工具本身的描述和参数 schema用户意图和工具调用时机的对应关系多次工具调用之间的状态依赖前一次的结果可能决定后一次的参数最终回答如何从工具返回结果中提取和组织让人工去逐条写这种“思维链动作链”单条成本普遍在几元到几十元不等而且人写的 JSON 参数经常不符合工具 schema。更麻烦的是同一个工具的调用方式在不同训练样本中必须保持一致靠人肉对齐基本不现实。所以业界普遍选择合成数据先用大模型GPT-4 级别的配合工具描述自动生成用户指令和调用轨迹再用规则和模型双重过滤保证质量。ToolLLM 用的是叫 ToolBench 的流程Kimi K2 虽然细节没公开但从论文和开源信息看也是“工具描述经过筛选和聚类后再让模型的衍生版本生成对话和调用轨迹”这个路子。1.3 合成数据要覆盖的核心能力维度我给合成数据做分类时习惯分成四个维度缺一个后面微调都会出问题意图识别与工具选择用户说了一句话模型能不能知道该不该调工具、调哪个工具。这个维度最容易解决甚至不做合成也能靠 zero-shot 顶一顶。参数抽取与格式规范把用户自然语言里的信息正确抽到 JSON 参数里。这里容易出现参数冗余、格式错误、枚举值不匹配的问题需要大量正反例。多轮调用中的状态维护前一个工具返回的结果影响下一次调用参数。比如先查天气再根据天气决定要不要查限行这种复合逻辑必须靠多轮轨迹数据教。结果总结与追问判定拿到工具返回值后是直接回答用户还是继续追问澄清还是再调别的工具。这条最容易被忽略但恰恰是用户体验的分水岭。把这四个维度在数据合成阶段的占比分配好比盲目堆量重要得多。2. 从 ToolLLM 看工具调用数据合成的主流范式2.1 ToolBench 的整体流程检索→指令生成→行为注释ToolLLM 的做法本质上是给工具调用数据合成树了一个可复用的流水线模板。它的名字叫 ToolBench流程可以拆成三个阶段。第一阶段是工具收集和描述预筛选。ToolLLM 从 RapidAPI 这样的平台上抓了大量真实 API然后对工具描述做了二值分类这个 API 能不能被模型准确理解、参数是否完整、调用是否安全。这一步很关键因为工具描述质量直接决定后续生成样本的上限。描述含糊的 API不管模型多强都生成不出规范调用。第二阶段是 API 检索。成千上万个工具不可能一次全放进 promptToolLLM 用 BM25 这类检索算法根据用户指令检索出最相关的工具集合一般控制在 15 个以内再交给大模型去生成调用轨迹。这个“先检索再生成”的设计其实也给很多企业做私有 Agent 提供了思路你的工具库再大真正和当前用户问题相关的也就是少数几个把检索器和生成器分开数据合成和在线推理都能受益。第三阶段是两阶段注释。先用 ChatGPT 生成“单工具调用”的指令和答案再由 ChatGPT 根据已有指令生成“多工具调用”的复杂指令。单工具样本保证模型先把基本格式学扎实多工具样本才让模型学会组合调用。这个循序渐进的数据构造思路后来被很多工作沿用。2.2 工具调用数据里最关键的几个组成字段不管 ToolLLM 还是后来的方案一条完整工具调用训练样本在结构上几乎都有这五个部分系统提示和工具描述模型需要“知道”自己有这些工具可以用以及每个工具的功能和参数约束。用户对话历史包含上下文让模型能理解当前请求的来龙去脉。模型的中间推理可选部分方案会把“为什么调这个工具”的思考写出来用来增强可解释性但推理内容也可能让模型过度依赖“自言自语”要看情况取舍。工具调用请求模型输出一个结构化动作包含工具名和参数。工具返回结果与最终回答模拟工具真实返回的 JSON以及模型读取结果后的最终自然语言回复。训练时工具调用请求部分需要单独计算 loss不能让模型把“调用动作”和“自然语言回复”混在一起学。我会在后面的伪代码部分展示怎么构造这种带分隔的对话格式。2.3 ToolLLM 的局限指令泛化和真实环境差距ToolLLM 这套方案在当时是开创性的但用下来有明显的局限主要在三处第一工具描述直接来自公开 API 平台和真实业务 API 的 schema 风格差异不小。公开 API 参数一般比较规范但企业内部系统的参数往往有各种历史包袱比如必填项标注不对、字段命名混乱、枚举值缺失。直接拿 ToolLLM 那套流程套业务场景生成的样本很容易出现模型臆造参数的情况。第二ToolLLM 对工具返回结果的处理是“模拟的”没有真正执行工具调用。这意味着模型学到的结果总结能力是建立在一个理想化的返回值之上的。一旦真实返回里有异常字段、空结果、错误码模型就麻爪了。第三ToolBench 数据里模型思维链的比重较高训练出的模型在推理时会输出大段解释性内容。这在学术 benchmark 里没问题但放到线上 Agent 场景延迟和 token 成本都会翻倍。这些局限恰恰是后来 Kimi K2 这类模型做数据合成时重点改进的方向。3. Kimi K2 在工具调用数据上再做对了什么3.1 从“文本问答”转向“行动数据”优先Kimi K2 对外沟通时特别强调了一个策略训练语料里合成数据占了很大比重而合成数据里又优先保证“行动类数据”——也就是工具调用、代码执行这类能让模型产生实际动作的样本。核心逻辑其实很朴素如果模型只是读了一堆文档和问答它学到的世界是静态的但工具调用要求模型面对动态反馈立刻做决策。要教好这种能力只能靠大量带反馈的“动作样本”。K2 在数据配比上刻意加大这种动作样本的比例而不是像早期模型一样把工具调用当成问答数据的一个子类。这一点对我的启发很大做工具调用微调数据质比量重要但“决策密度”比“文本长度”重要。一条用户在多个工具之间切换的轨迹数据抵得上几十条单轮问答。3.2 数据合成里的“三路并行”执行反馈、批评信号、轨迹规划Kimi K2 没有完全公开 ToolCall 数据合成的完整细节但从论文和技术报告能看出三个关键设计我觉得是当前最值得抄作业的第一路是让模型生成候选行动后真的去执行工具代码拿到真实的返回结果。这条“执行反馈”链路让数据里的工具返回值不再靠模拟而是真实环境跑出来的模型学到的总结能力自然更贴近线上。第二路是引入“批评者”对模型生成的整条对话/行动轨迹做细粒度批评指出来哪一步调错了参数、哪一步结果总结遗漏了关键信息再把批评后的修正结果作为训练数据。这比单纯让模型自己打分过滤更可靠批评者模型其实扮演了一个“数据质检员”的角色。第三路是把复杂任务拆成规划子目标分别生成规划数据和子任务执行数据。这让模型在高难度任务上能学会“先分步规划、再逐步调用”而不是一上来就试图一步调用到结果。这三路组合起来正好解决了我前面说的 ToolLLM 的三个局限真实执行解决返回值理想化的问题批评信号解决质量控制问题轨迹规划解决复杂调用泛化问题。3.3 新旧方案对照从离线注释到闭环合成维度ToolLLM 范式Kimi K2 风格数据来源基于 API 描述离线生成合成候选 真实验证闭环工具返回结果人工构造或模拟实际执行工具获得质量过滤规则过滤 模型评分批评者模型逐环节点评数据组织指令-调用-回答三段式行动轨迹 规划子目标适用场景学术benchmark、冷启动线上Agent、复杂工具编排这张表不是要分高下而是想说清楚演进逻辑当你要快速拿到一批可用数据ToolLLM 流程完全够用当你要把模型推到线上做复杂 AgentK2 这套“执行批评规划”的思路更值得投入。4. 对话/训练数据结构设计与伪代码实现4.1 训练样本的对话格式把工具调用和自然语言分开无论用 ToolLLM 还是 K2 思路最终都得落成一条条具体的训练样本。我推荐用类似 OpenAI 消息数组的 JSONL 格式组织角色字段至少包含user、assistant、tool三种其中assistant消息里用tool_calls字段单独承载结构化调用动作。这样在训练时可以只对tool_calls里的内容计算 loss避免模型把工具动作和普通回复混在一起学。我实际构造样本时习惯先定死一个模板每条样本都是一个 JSON 对象顶层字段包含messages、tools和可选的metadata。metadata用来记录这条样本的类型比如是单轮调用还是多轮轨迹这样训练时可以按比例做课程采样。{ messages: [ {role: user, content: 帮我把上海到北京的机票和酒店一起查一下}, {role: assistant, content: null, tool_calls: [ {id: call_001, type: function, function: {name: search_flight, arguments: {\from\: \上海\, \to\: \北京\, \date\: \2025-08-01\}}}, {id: call_002, type: function, function: {name: search_hotel, arguments: {\city\: \北京\, \checkin\: \2025-08-01\, \checkout\: \2025-08-03\}}} ]}, {role: tool, tool_call_id: call_001, content: {\flights\: [{\flight_no\: \CA1858\, \price\: 1200}]}}, {role: tool, tool_call_id: call_002, content: {\hotels\: [{\name\: \XX酒店\, \price\: 600}]}}, {role: assistant, content: 为你找到了以下航班和酒店国航CA1858价格1200元XX酒店每晚600元。需要我帮你预订吗} ], tools: [ {name: search_flight, description: 查询航班信息, parameters: {type: object, properties: {from: {type: string}, to: {type: string}, date: {type: string}}}}, {name: search_hotel, description: 查询酒店信息, parameters: {type: object, properties: {city: {type: string}, checkin: {type: string}, checkout: {type: string}}}} ], metadata: {type: multi_tool_parallel} }这种结构最大的好处是训练框架如 LLaMA-Factory、TRL 的OpenAIFunctionCallingFormat可以直接按tool_calls字段做 mask模型在生成自然语言回复时不会误学到“工具动作的输出格式”。4.2 单轮指令生成伪代码用大模型产出候选数据接下来是数据合成的核心环节。先看最简单的单轮指令生成这一步主要覆盖“意图识别与参数抽取”维度。伪代码如下def generate_single_turn_samples(api_schema, llm_client, num_samples_per_tool20): samples [] for tool in api_schema: prompt ( f你现在是一个数据标注助手。请根据给定的工具描述生成{num_samples_per_tool}条用户指令。\n f工具名称{tool[name]}\n f工具描述{tool[description]}\n f参数JSON Schema{json.dumps(tool[parameters], ensure_asciiFalse)}\n\n 要求\n 1. 每条用户指令必须能明确触发该工具调用但表达方式要多样化包含口语、书面语、省略语\n 2. 参数抽取要完整凡是用户指令中给了的参数都要体现在调用里\n 3. 如果用户指令缺少必填参数模型应该生成追问而不是强行调用工具\n 4. 同时生成期望的模型回复包括工具调用和最终回答。\n 输出格式JSONList每个元素包含 user_instruction, tool_call, final_answer。 ) response llm_client.chat(prompt) parsed parse_json_list(response) for item in parsed: samples.append({ messages: [ {role: user, content: item[user_instruction]}, {role: assistant, content: None, tool_calls: [item[tool_call]]}, {role: tool, tool_call_id: item[tool_call][id], content: mock_tool_result(item[tool_call])}, {role: assistant, content: item[final_answer]} ], tools: [tool], metadata: {type: single_turn} }) return deduplicate_for_tool(samples)这里的mock_tool_result在 ToolLLM 流程里通常是规则构造的模拟结果但在 K2 风格里应该替换成真实执行工具返回真实结果。两种方式我都试过如果工具是纯计算型或者可安全调用的外部 API强烈建议走真实执行数据质量完全不是一个量级。注意生成的时候要用“JSONList”这样严格的输出说明并自己写健壮的 JSON 解析函数。大模型偶尔会输出 Markdown 代码块包裹的 JSON甚至夹杂解释文字解析失败时不要直接丢弃重试一次比重新生成一整批更经济。4.3 多轮轨迹合成伪代码让模型学会组合调用多轮轨迹数据是工具调用数据合成的重头戏。这时候不是简单地把两个单轮样本拼在一起而是要让前一次工具调用的返回结果成为下一次调用的依据。伪代码思路如下def generate_multi_turn_trajectory(api_schema_list, llm_client, planner_prompt): # 第一步让规划器为多个工具组合设计一个复合任务 user_task llm_client.chat( f给定以下工具集{api_schema_list}\n 请设计一个需要至少两次工具调用才能解决的用户任务确保两次调用之间存在依赖关系。 ) # 第二步让执行模型逐步决策每一步都基于当前对话历史和真实工具返回 messages [{role: user, content: user_task}] trajectory [] for step in range(MAX_STEPS): # 让模型根据当前上下文决定下一步动作 reply llm_client.chat(messages available_tools_prompt) if reply.has_tool_calls(): # 记录工具调用动作 trajectory.append(reply.tool_calls) messages.append({role: assistant, content: None, tool_calls: reply.tool_calls}) for call in reply.tool_calls: # 真实执行工具拿到返回结果 real_result execute_tool(call.function.name, call.function.arguments) messages.append({role: tool, tool_call_id: call.id, content: real_result}) else: # 模型认为不需要继续调用工具输出最终回答轨迹结束 trajectory.append(reply.content) break # 第三步质量检查 if not trajectory_ends_with_answer(trajectory): return None return build_training_sample(messages, api_schema_list)我实际跑下来很关键的一点必须限制最大步数否则模型会在一些模糊任务上无限循环调用。比如查一个不存在的城市天气模型可能反复调用同一个工具。这时候如果强行把轨迹数据喂给模型等于教会模型“无效调用也没关系”。我自己常用的规则是在生成阶段如果同一工具被连续调用三次且参数无变化直接丢弃这条轨迹。另外一个细节是要在 planner 阶段就刻意引入“必须先做什么、再做什么”的依赖设计。比如用户问“从上海飞北京订离机场近的酒店”工具调用顺序必须是先查航班、拿到落地机场和到达时间再查酒店并带上位置偏好。这种数据教出来的模型在线运行时的规划能力要明显好于依赖自然语言推理硬扛的模型。4.4 数据过滤与评判信号K2 风格的质量关卡刚才提到 K2 会引入批评者模型做细粒度点评这在代码里其实不复杂。核心逻辑是候选轨迹生成完之后不让它直接进训练集而是丢给一个批评者模型让它从工具选择、参数正确性、结果利用、最终回答四个角度逐项打分和纠错。def critique_and_filter(pred_trajectory, tool_schemas, critic_llm): critique_res critic_llm.chat( f以下是模型生成的工具调用轨迹请从以下四个维度进行批评修正\n f1. 是否选择了正确的工具\n 2. 参数是否满足JSON Schema且与用户意图一致\n 3. 是否合理利用了上一步工具返回的结果\n 4. 最终回答是否完整、准确、无多余自然语言。\n f工具列表{tool_schemas}\n f轨迹{pred_trajectory}\n 如果存在错误请输出修正后的完整轨迹如果没有错误输出OK。 ) if critique_res.startswith(OK): return pred_trajectory else: corrected parse_trajectory(critique_res) # 简单校验修正后的轨迹里工具名和参数必须能被schema解析 if validate_tool_calls(corrected, tool_schemas): return corrected else: return None这里有个实操体会批评者模型的输出经常是“大致正确但格式不符合 schema”所以必须加一层强规则校验不能只信模型的判断。我的做法是每一步修正都重新解析函数名和参数解析失败就直接丢弃该条数据。宁可少吃不消化也不要让脏数据污染模型。有些团队还会对修正后的样本做二次采样保证同一个工具调用的参数分布不要太集中。比如查询天气工具如果生成的数据里 80% 都是查北京、上海、广州模型学出来的地理知识就偏了。用参数热度做下采样能改善泛化。4.5 数据比例和课程学习别在一条轨迹上死磕数据合成完之后最后一道工序是配比组合。我的经验是单轮工具调用数据和多轮轨迹数据的比例大概控制在 3:1 到 4:1 之间。多轮数据太多模型会养成“遇事不决先调工具”的毛病单轮数据太少模型又不够果断复杂场景不敢连续调用。另外需要混入一部分“不调用工具也行的样本”。比如用户只是闲聊“今天天气不错”模型不应该调用任何工具。如果数据集里全是必须调用工具的样本模型就成了“工具复读机”这在实际产品里非常致命。K2 那类做法里这种“拒绝调用”的样本一般来自纯对话语料里筛选出的非工具意图样本。5. 工具调用数据合成的常见问题与排查技巧5.1 参数 JSON 格式漂移模型把字符串和数字搞混微调模型最常见的现象工具定义了date: {type: string, format: date}模型却输出date: 20250801。很多是生成候选数据时大模型本身就没按 schema 来。排查方法写一个 schema 校验器对合成数据里每一个tool_calls的 arguments 做jsonschema.validate。不用管模型最终学到什么样先保证训练数据里没有格式漂移的样本。只要训练集参数类型干净模型生成时大概率也会干净这比任何后处理都管用。额外技巧对日期、数字这类字段可以在工具描述里加示例值比如date: {type: string, description: 日期格式YYYY-MM-DD例如2025-08-01}。大模型对“描述 示例”的遵循度远高于只有 type 的 schema。5.2 工具返回结果状态冲突前一步没跑后一步就用了多轮轨迹数据里最容易出现的逻辑错误是“工具 B 用到了工具 A 的返回结果但轨迹里根本没有先调用工具 A”。原因是生成阶段用了并行采样模型在一步内生成了多个 tool_calls其中一个调用的参数依赖另一个调用刚返回的数据。解决办法在生成阶段强制串行依赖。只有参数里包含“查询上一步结果”这种意图时工具调用必须放在前一步结果回填之后不允许并行。加一条规则如果某个工具调用的参数里出现{{这类模板占位符号说明生成器偷懒了直接丢弃。5.3 合成数据重复度过高看起来量大实际信息量低大模型生成数据时有个天然倾向同一个工具、同一种句式反复出现。比如查天气的工具生成的 200 条指令里可能有 150 条都是“帮我查下北京天气”。这不是不能解决但要在一开始就控制。生成 prompt 里明确要求“避免和已有指令重复”或者每生成一批后就做一次语义去重用 embedding 相似度阈值过滤。我在跑数据合成时通常用一个简单的set存指令的语义哈希相似度超过 0.9 就替换成新生成的指令。这样最后的训练集多样性会明显好很多。5.4 训练后模型“乱调用”工具问题大多在数据正负例不平衡不少人微调完工具调用模型发现它连“你好”都要调一个工具非常头痛。绝大多数原因是训练集里“不调用工具”的负样本太少。模型学到的模式是“有用户输入就必须动作”自然就倾向乱调。解决思路合成数据时除了工具调用样本要单独构造一批“无工具响应样本”。可以从纯对话数据集里抽一批用户问题让模型生成不用工具的普通回答混入训练集。比例我给个参考值10:1 到 20:1工具调用样本:无工具样本之间比较稳妥。另外工具选择阶段如果工具列表为空或和用户问题无关要明确在训练数据里体现“模型应该直接回复”这也是 K2 那类做法里隐含的重点。5.5 评判过滤的“过拟合”批评者模型把自己的风格带偏了引入批评者模型修正数据后有一个隐蔽问题修正后的轨迹会带上批评者的表达习惯。如果批评者是个话很多的模型修正后的样本里自然语言回复可能冗长到让人崩溃如果批评者喜欢过度解释模型也会学出这种毛病。我的对策是批评者只负责“指出错误 给出修正要点”不要让它直接重写整段自然语言回复。最后一步的最终回答宁可让原始的指令生成模型重新生成或直接截取模板回复也不要用批评者的改写版本。这听起来很反直觉但实际对控制模型风格非常有效。6. 从 ToolLLM 到 K2我学到的数据合成心法这套数据合成流程我前后跑了有小半年从最开始照着 ToolLLM 的 ToolBench 硬抄到后来慢慢加入执行反馈和评判过滤走了不少弯路也累积了一些自己的判断。如果只让我留三条经验我会选这三条第一条工具调用的数据合成不是“生成对话”而是“构造动作和反馈的闭环”。你给模型看到的工具返回结果越真实模型在线上就越稳。纯靠模拟返回值省下来的成本会在线上事故里加倍还回去。第二条质量关卡一定要有“规则 模型”双保险。模型评判能发现语义问题但格式校验必须靠jsonschema这类硬规则兜底。没有硬规则把关的合成数据最后进了训练集模型肯定会学歪。第三条也是我反复踩坑后最深的体会别追求一次性生成海量完美数据而是先造一批小样本快速训一个 v0 模型再拿 v0 模型去生成下一批数据。这样数据分布会和目标模型的能力边界匹配得更好训练出来的最终模型往往比直接全量合成更可控。如果你现在正准备给业务场景造工具调用数据我的建议是先拿 20 个左右的代表性工具按 ToolLLM 的流程全量走一遍手工检查 50 条生成样本确认格式、逻辑、风格都没问题再放大量。这个前置质检步骤能帮你省下后面所有返工的时间。