在实际 AI 应用开发中,让大语言模型(LLM)学会稳定、可靠地使用外部工具(如 API、数据库、计算器等)是一个关键挑战。模型在预训练阶段学习了海量文本知识,但“知道”某个工具的存在和“会使用”这个工具是两回事。传统的指令微调(Instruction Tuning)或强化学习人类反馈(RLHF)虽然能提升模型的指令遵循能力,但在工具使用的泛化性、鲁棒性和多步推理上仍有不足。一个核心问题是:高质量、多样化的工具使用训练数据从何而来?人工标注成本高昂,且难以覆盖所有可能的工具组合与复杂场景。
MidTool 正是为了解决这一数据瓶颈而提出的方法。其核心思想是在模型训练的“中期”(Mid-training),通过合成(Synthesis)高质量的工具使用数据,来系统性提升模型的工具调用(Tool Use)能力。这种方法不是简单的数据增强,而是通过精心设计的合成策略,让模型在学习过程中“见多识广”,从而在面对新工具或复杂指令时,能更准确地理解意图、规划步骤并执行调用。
本文将从工程实践的角度,深入解析 MidTool 的核心机制、数据合成流程,并提供一个从零构建工具使用数据合成管道的实战指南。我们将探讨如何设计合成规则、生成高质量的数据对,以及如何将这些数据有效地整合到模型训练流程中。无论你是希望提升现有模型工具调用能力的算法工程师,还是正在构建基于 LLM 的智能体(Agent)系统的开发者,理解并实践 Mid-training Data Synthesis 的思路都将大有裨益。
1. 理解 Mid-training Data Synthesis 的核心价值
在深入技术细节之前,我们需要明确为什么要在“中期”进行数据合成,以及它解决了传统方法的哪些痛点。
1.1 工具使用能力训练的挑战
大语言模型的工具使用能力可以分解为几个子任务:
- 工具识别与选择:理解用户指令,判断是否需要调用工具,并从工具库中选择最合适的一个或多个工具。
- 参数解析与构建:根据指令和工具的描述(如函数签名、API 文档),正确提取或推导出调用工具所需的参数。
- 多步规划与执行:对于复杂任务,需要将任务分解为多个子步骤,并按顺序调用不同的工具,同时处理中间结果。
- 结果解释与响应生成:将工具返回的原始结果(如 JSON、数字、文本)整合到自然语言回复中,回应用户。
传统的指令微调数据(如(instruction, response)对)往往只展示了最终的正确调用和结果。模型缺乏对“为什么选这个工具”、“参数怎么来的”、“如果调用失败怎么办”等决策过程的学习。这导致模型在面对分布外(OOD)指令或新工具时,泛化能力弱,容易产生幻觉或错误调用。
1.2 Mid-training 与 Fine-tuning 的定位差异
- 预训练(Pre-training):在海量无标注文本上学习语言建模,获得通用语言理解和生成能力。此时模型对“工具”没有明确概念。
- 中期训练(Mid-training):这是一个介于预训练和下游任务微调之间的阶段。在此阶段,我们向模型注入特定领域或能力(如工具使用、代码生成、数学推理)的知识。数据通常是合成的或从特定来源收集的,目标是在不损害通用能力的前提下,塑造模型的特定行为模式。Mid-training 的数据量通常远小于预训练,但远大于特定任务的微调。
- 微调(Fine-tuning):在特定、高质量的任务数据(如客服对话、代码补全)上进行训练,使模型高度适应该任务。数据通常要求精准,但多样性和数量有限。
MidTool 的“Mid-training”定位意味着,它旨在为模型打下坚实的工具使用“基础”,而不是针对某个具体 API 进行“特化”。这好比在教一个人“如何阅读说明书并操作机器”的通用方法,而不是只教他操作某一台特定的打印机。
1.3 数据合成的优势
相比于人工标注,合成数据具有显著优势:
- 成本与规模:可以自动化、低成本地生成海量数据。
- 多样性与可控性:可以系统地覆盖边缘情况、错误输入、复杂组合,这是人工标注难以穷尽的。
- 知识注入:可以通过规则或模板,将工具使用的“最佳实践”(如参数检查、错误处理)直接编码到训练数据中。
MidTool 的关键在于设计一套有效的合成策略(Synthesis Strategy),确保生成的数据既能教会模型正确的工具使用模式,又能提升其泛化能力和鲁棒性。
2. 构建工具使用数据合成管道
一个完整的 MidTool 数据合成管道包含几个核心组件:工具定义库、指令生成器、思维链(Chain-of-Thought)合成器、回复生成器以及质量过滤器。下面我们逐步拆解每个部分。
2.1 环境与依赖准备
我们将使用 Python 作为主要实现语言。虽然 MidTool 是一个方法论,不依赖特定框架,但我们会利用一些常用库来辅助数据生成和处理。
# 建议创建一个新的虚拟环境 python -m venv midtool-env source midtool-env/bin/activate # Linux/macOS # midtool-env\Scripts\activate # Windows # 安装核心依赖 pip install openai>=1.0.0 # 用于调用大模型生成指令和回复(示例用,可替换) pip install pydantic>=2.0 # 用于定义严谨的工具模式和数据模型 pip install jinja2>=3.0 # 用于模板化生成 pip install datasets # 用于处理和保存生成的数据集 pip install loguru # 用于日志记录如果使用其他 LLM 服务(如 Anthropic Claude、本地部署模型),请安装相应的 SDK。本文示例将使用 OpenAI 格式的 API 进行说明。
2.2 定义工具库(Tool Library)
工具库是所有合成数据的源头。我们需要用一种结构化的方式定义每个工具。这里使用 Pydantic 模型来确保规范性。
# tool_definition.py from enum import Enum from typing import List, Optional, Any, Dict from pydantic import BaseModel, Field class ParamType(str, Enum): STRING = "string" INTEGER = "integer" NUMBER = "number" BOOLEAN = "boolean" ARRAY = "array" OBJECT = "object" class ToolParameter(BaseModel): """工具参数定义""" name: str = Field(..., description="参数名称") type: ParamType = Field(..., description="参数类型") description: str = Field(..., description="参数含义和约束的详细描述") required: bool = Field(default=True, description="是否必须") # 可以添加枚举值示例等 examples: Optional[List[Any]] = Field(default=None, description="参数值示例") class ToolDefinition(BaseModel): """工具定义""" name: str = Field(..., description="工具的唯一名称,如 `get_weather`") description: str = Field(..., description="工具功能的自然语言描述") parameters: List[ToolParameter] = Field(..., description="工具参数列表") returns: str = Field(..., description="返回值的描述,如 'JSON对象,包含温度、湿度和天气状况'") # 可选的执行函数(模拟或占位),用于后续验证 execute: Optional[Any] = Field(default=None, description="模拟执行函数") # 示例:定义几个常用工具 WEATHER_TOOL = ToolDefinition( name="get_weather", description="获取指定城市当前或未来的天气信息。", parameters=[ ToolParameter(name="city", type=ParamType.STRING, description="城市名称,例如:北京、New York", required=True), ToolParameter(name="date", type=ParamType.STRING, description="查询日期,格式为YYYY-MM-DD。默认为今天。", required=False), ], returns="一个JSON对象,包含温度(摄氏度)、湿度(百分比)、天气状况(如晴朗、多云、下雨)和风速。" ) CALCULATOR_TOOL = ToolDefinition( name="calculator", description="执行数学计算。支持加、减、乘、除、幂运算。", parameters=[ ToolParameter(name="expression", type=ParamType.STRING, description="数学表达式,例如:(12 + 5) * 3 / 2", required=True), ], returns="计算结果的数值(浮点数或整数)。" ) DB_QUERY_TOOL = ToolDefinition( name="query_database", description="根据给定的查询条件,从用户数据库中检索信息。", parameters=[ ToolParameter(name="table_name", type=ParamType.STRING, description="要查询的表名", required=True), ToolParameter(name="columns", type=ParamType.ARRAY, description="要返回的列名列表", required=False), ToolParameter(name="conditions", type=ParamType.OBJECT, description="查询条件,键为列名,值为匹配值", required=False), ToolParameter(name="limit", type=ParamType.INTEGER, description="返回结果的最大条数", required=False), ], returns="一个包含查询结果的JSON数组,每条结果是一个对象。" ) TOOL_LIBRARY: Dict[str, ToolDefinition] = { tool.name: tool for tool in [WEATHER_TOOL, CALCULATOR_TOOL, DB_QUERY_TOOL] }这个工具定义库是合成的基石。每个工具的description和参数的description至关重要,它们将是模型学习如何理解和调用工具的主要文本依据。
2.3 设计合成策略与指令生成
指令生成是合成的第一步。我们的目标是生成多样化的用户请求,这些请求有的需要单工具调用,有的需要多工具组合,有的包含模糊或错误信息以训练模型的鲁棒性。
我们可以设计一个基于模板和 LLM 的混合生成器。
# instruction_generator.py import random from typing import List, Dict, Any from jinja2 import Template from .tool_definition import TOOL_LIBRARY, ToolDefinition class InstructionGenerator: def __init__(self, tool_library: Dict[str, ToolDefinition]): self.tool_library = tool_library self.tools = list(tool_library.values()) def generate_by_template(self, pattern: str) -> str: """使用预定义模板生成指令""" templates = { "direct_single": [ "帮我查一下{{city}}的天气。", "计算一下{{expression}}等于多少?", "从{{table}}表中找出{{condition}}的记录。" ], "conversational_single": [ "我打算明天去{{city}},不知道天气怎么样,你能告诉我吗?", "这个式子{{expression}}的结果有点复杂,你能算算吗?", ], "multi_tool": [ "我先要知道{{city1}}和{{city2}}的天气,然后比较一下哪里更暖和。", "帮我算一下{{expr1}}和{{expr2}},然后把两个结果相加。", ], "ambiguous": [ "今天天气如何?", # 缺少城市参数 "算个数。", # 缺少表达式 "查一下用户信息。" # 缺少表名和条件 ], "error_prone": [ "查一下城市叫'New York'(注意有空格)的天气。", # 参数格式可能有问题 "计算123除以0。", # 数学错误 "查询一个不存在的表‘non_existent_table’。", # 资源不存在 ] } # 这里需要实现具体的模板变量填充逻辑,例如从列表中随机选择城市、生成随机表达式等。 # 为简化示例,我们返回一个占位指令。 selected_template = random.choice(templates.get(pattern, ["这是一个测试指令。"])) # 实际应用中,需要用真实数据替换 {{}} 中的变量。 return selected_template def generate_by_llm(self, tool_def: ToolDefinition, complexity: str = "simple") -> str: """使用LLM根据工具定义生成更自然、多样的指令""" # 这是一个调用LLM API的示例提示词模板 prompt_template = """ 你是一个帮助生成训练数据的助手。请根据以下工具的描述,生成一条用户可能提出的、需要调用此工具的自然语言请求。 工具名称:{{tool_name}} 工具描述:{{tool_description}} 工具参数:{% for param in parameters %}- {{param.name}} ({{param.type}}): {{param.description}}{% if not param.required %} (可选){% endif %} {% endfor %} 请生成一条{{complexity}}的指令。指令应尽可能自然、口语化,并且必须包含调用该工具所需的必要信息(参数值可以合理虚构)。 只输出指令本身,不要输出任何解释。 生成的指令: """ from jinja2 import Template tmpl = Template(prompt_template) prompt = tmpl.render( tool_name=tool_def.name, tool_description=tool_def.description, parameters=tool_def.parameters, complexity=complexity ) # 调用LLM API (示例,需替换为实际调用) # instruction = call_llm_api(prompt, model="gpt-3.5-turbo") # 此处为模拟返回 instruction = f"请告诉我{tool_def.name}工具的使用示例。" return instruction def generate_batch(self, num_instructions: int, patterns: List[str] = None) -> List[Dict[str, Any]]: """批量生成指令及其元数据""" if patterns is None: patterns = ["direct_single", "conversational_single", "multi_tool", "ambiguous", "error_prone"] data = [] for _ in range(num_instructions): pattern = random.choice(patterns) if random.random() < 0.7: # 70% 使用LLM生成,30%使用模板 tool = random.choice(self.tools) complexity = random.choice(["simple", "medium", "complex"]) instr = self.generate_by_llm(tool, complexity) required_tools = [tool.name] else: instr = self.generate_by_template(pattern) # 模板生成需要后处理来关联具体工具,这里简化处理 required_tools = self._infer_tools_from_instruction(instr) data.append({ "instruction": instr, "pattern": pattern, "required_tools": required_tools, "source": "llm" if "llm" in locals() else "template" }) return data def _infer_tools_from_instruction(self, instruction: str) -> List[str]: """一个简单的启发式方法,根据关键词推断可能需要的工具(实际应用需要更复杂的NLP)""" inferred = [] if "天气" in instruction or "weather" in instruction.lower(): inferred.append("get_weather") if "算" in instruction or "计算" in instruction or "calculate" in instruction.lower(): inferred.append("calculator") if "查询" in instruction or "查" in instruction or "find" in instruction.lower() or "search" in instruction.lower(): inferred.append("query_database") # 这是一个粗略的推断 return inferred if inferred else ["unknown"]2.4 合成思维链与回复
对于每条指令,我们需要合成模型在响应时应该展现的“思考过程”(Chain-of-Thought, CoT)和最终回复。这是 MidTool 训练的关键,它教会模型如何推理。
# response_synthesizer.py from typing import List, Dict, Any from .tool_definition import TOOL_LIBRARY, ToolDefinition import json import random class ResponseSynthesizer: def __init__(self, tool_library: Dict[str, ToolDefinition]): self.tool_library = tool_library def synthesize_cot_and_response(self, instruction: str, required_tools: List[str]) -> Dict[str, Any]: """ 为给定指令和所需工具合成思维链和最终回复。 这是一个模拟的、基于规则的合成器。在实际项目中,可以结合规则和LLM来生成更高质量的CoT。 """ # 步骤1: 解析指令,确定工具调用序列(这里简化,假设指令只对应一个工具) # 实际中,这里应该有一个更复杂的规划模块。 tool_to_use = required_tools[0] if required_tools else None if not tool_to_use or tool_to_use == "unknown": # 如果无法确定工具,生成一个拒绝或澄清的回复 cot = "用户的问题不明确,无法确定要使用哪个工具。我需要请求澄清。" response = "抱歉,我不太确定您想让我做什么。您能提供更具体的信息吗?例如,您想查询哪个城市的天气?或者您想计算什么?" return {"chain_of_thought": cot, "response": response, "tool_calls": []} tool_def = self.tool_library.get(tool_to_use) if not tool_def: cot = f"用户可能需要使用工具‘{tool_to_use}’,但该工具不在我的知识库中。我需要告知用户我无法处理。" response = f"抱歉,我目前不支持‘{tool_to_use}’功能。" return {"chain_of_thought": cot, "response": response, "tool_calls": []} # 步骤2: 从指令中提取参数(模拟一个简单的提取逻辑) # 这是一个非常简化的模拟,真实场景需要NLP解析或调用一个小的参数提取模型。 extracted_params = self._mock_extract_parameters(instruction, tool_def) # 步骤3: 构建思维链 cot_parts = [] cot_parts.append(f"用户指令是:‘{instruction}’。") cot_parts.append(f"这涉及到‘{tool_def.name}’工具,其功能是:{tool_def.description}。") if extracted_params: param_desc = ", ".join([f"{k}={v}" for k, v in extracted_params.items()]) cot_parts.append(f"我从指令中提取出了参数:{param_desc}。") else: # 参数缺失,需要反问 missing_params = [p.name for p in tool_def.parameters if p.required] cot_parts.append(f"指令中缺少必要参数:{missing_params}。我需要向用户询问这些信息。") response = f"为了使用‘{tool_def.name}’,我需要您提供:{', '.join(missing_params)}。" return {"chain_of_thought": "\n".join(cot_parts), "response": response, "tool_calls": []} # 步骤4: 模拟工具调用并生成结果 cot_parts.append(f"现在,我将使用参数 {extracted_params} 调用‘{tool_def.name}’。") mock_result = self._mock_execute_tool(tool_def, extracted_params) cot_parts.append(f"工具调用成功,返回结果:{mock_result}。") # 步骤5: 根据结果生成面向用户的回复 response = self._generate_user_response(tool_def, extracted_params, mock_result) cot_parts.append(f"最后,我将结果转化为自然语言回复给用户。") # 步骤6: 构建最终的训练数据格式 # 训练时,思维链(CoT)可以作为模型的“思考”部分进行学习。 # 一种常见格式是将CoT和回复拼接,或在特定标记中。 final_cot = "\n".join(cot_parts) # 构建工具调用结构(例如,用于Function Calling格式) tool_call = { "name": tool_def.name, "arguments": extracted_params } return { "chain_of_thought": final_cot, "response": response, "tool_calls": [tool_call], "tool_results": [mock_result] } def _mock_extract_parameters(self, instruction: str, tool_def: ToolDefinition) -> Dict[str, Any]: """模拟参数提取。实际项目应使用更可靠的方法。""" params = {} # 这里只是一个极其简单的关键字匹配示例 if tool_def.name == "get_weather": if "北京" in instruction: params["city"] = "北京" params["date"] = "2023-10-27" elif "上海" in instruction: params["city"] = "上海" params["date"] = "2023-10-27" elif tool_def.name == "calculator": import re # 简单查找数字和运算符 match = re.search(r'(\d+[\+\-\*\/]\d+)', instruction) if match: params["expression"] = match.group(1) # ... 其他工具的模拟提取 return params def _mock_execute_tool(self, tool_def: ToolDefinition, params: Dict[str, Any]) -> Any: """模拟工具执行,返回一个假的结果。""" if tool_def.name == "get_weather": return {"temperature": random.randint(15, 30), "humidity": random.randint(40, 80), "condition": random.choice(["晴朗", "多云", "小雨"])} elif tool_def.name == "calculator": try: # 警告:实际中不要用eval,这里仅用于演示 result = eval(params.get("expression", "0")) return result except: return "计算错误:表达式无效或除零错误。" elif tool_def.name == "query_database": return [{"id": 1, "name": "模拟用户", "age": 25}] return None def _generate_user_response(self, tool_def: ToolDefinition, params: Dict, result: Any) -> str: """根据工具结果生成自然语言回复。""" if tool_def.name == "get_weather": return f"{params.get('city')}的天气是{result['condition']},气温{result['temperature']}摄氏度,湿度{result['humidity']}%。" elif tool_def.name == "calculator": return f"计算结果为:{result}。" elif tool_def.name == "query_database": return f"查询到了{len(result)}条记录。" return f"操作‘{tool_def.name}’已完成,结果为:{result}。"2.5 数据组装与格式转换
生成的数据需要转换成模型训练所需的格式。常见的格式有对话格式(如 ShareGPT)、指令跟随格式(如 Alpaca)或带有特殊标记的格式。
# data_formatter.py from datasets import Dataset from typing import List, Dict, Any def format_for_sft(instruction_data: List[Dict], response_data: List[Dict]) -> Dataset: """格式化为标准监督微调(SFT)格式,例如 Alpaca 格式。""" formatted_data = [] for instr_item, resp_item in zip(instruction_data, response_data): # 将思维链(CoT)作为模型输入的一部分,或者作为多轮对话中的“思考”轮次。 # 这里我们采用一种简单格式:将CoT和回复合并作为输出。 full_output = f"{resp_item['chain_of_thought']}\n\n最终回复:{resp_item['response']}" formatted_example = { "instruction": instr_item["instruction"], "input": "", # 可以为空,或放入工具定义等信息 "output": full_output, # 可以保留元数据供后续分析 "metadata": { "pattern": instr_item["pattern"], "required_tools": instr_item["required_tools"], "tool_calls": resp_item.get("tool_calls", []), "tool_results": resp_item.get("tool_results", []) } } formatted_data.append(formatted_example) return Dataset.from_list(formatted_data) def format_for_function_calling(instruction_data: List[Dict], response_data: List[Dict]) -> List[Dict]: """格式化为函数调用(Function Calling)训练格式。""" formatted_data = [] for instr_item, resp_item in zip(instruction_data, response_data): # 假设训练数据期望模型输出一个包含工具调用的JSON。 # 这需要模型本身支持输出结构化内容。 example = { "messages": [ {"role": "user", "content": instr_item["instruction"]}, {"role": "assistant", "content": json.dumps({ "thought": resp_item["chain_of_thought"], "tool_calls": resp_item.get("tool_calls", []), "response": resp_item["response"] }, ensure_ascii=False)} ] } formatted_data.append(example) return formatted_data2.6 运行合成管道
最后,我们将所有组件串联起来,形成一个完整的数据合成脚本。
# main_synthesis_pipeline.py from tool_definition import TOOL_LIBRARY from instruction_generator import InstructionGenerator from response_synthesizer import ResponseSynthesizer from data_formatter import format_for_sft from datasets import Dataset import json def main(): # 1. 初始化组件 instr_gen = InstructionGenerator(TOOL_LIBRARY) resp_synth = ResponseSynthesizer(TOOL_LIBRARY) # 2. 生成指令 print("正在生成指令...") instructions = instr_gen.generate_batch(num_instructions=100, patterns=["direct_single", "conversational_single", "ambiguous"]) print(f"生成了 {len(instructions)} 条指令。") # 3. 为每条指令合成回复(含CoT) print("正在合成回复与思维链...") all_responses = [] for i, instr_item in enumerate(instructions): if i % 10 == 0: print(f"处理进度: {i}/{len(instructions)}") response_item = resp_synth.synthesize_cot_and_response( instr_item["instruction"], instr_item["required_tools"] ) all_responses.append(response_item) # 4. 格式化数据 print("正在格式化数据...") dataset = format_for_sft(instructions, all_responses) # 5. 保存数据 output_path = "midtool_synthetic_data.jsonl" dataset.to_json(output_path, orient="records", lines=True) print(f"数据已保存至: {output_path}") print(f"数据样例:") print(json.dumps(dataset[0], indent=2, ensure_ascii=False)) if __name__ == "__main__": main()运行此脚本,你将得到一个包含 100 条合成训练数据的 JSONL 文件。每条数据都包含了用户指令、模型内部的思考过程(CoT)以及最终回复。
3. 将合成数据用于模型训练
生成了数据之后,下一步就是将其用于模型训练。Mid-training 通常意味着在一个基础预训练模型上,用这批合成数据(可能混合其他数据)进行继续预训练或指令微调。
3.1 训练配置要点
假设我们使用 Hugging Face Transformers 库和 PyTorch 进行训练。
# training_config_example.py from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer from datasets import load_dataset import torch # 1. 加载模型和分词器 model_name = "meta-llama/Llama-3.2-3B-Instruct" # 示例模型,请替换为你有权使用的模型 tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name) # 设置填充令牌(如果分词器没有) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token # 2. 加载并预处理合成数据 def preprocess_function(examples): # 将指令和输出拼接成模型输入的格式 # 例如,使用 ChatML 格式 texts = [] for instr, out in zip(examples["instruction"], examples["output"]): message = [ {"role": "user", "content": instr}, {"role": "assistant", "content": out} ] # 使用分词器的 apply_chat_template 方法(如果支持) text = tokenizer.apply_chat_template(message, tokenize=False, add_generation_prompt=False) texts.append(text) # 对文本进行分词 model_inputs = tokenizer(texts, truncation=True, padding="max_length", max_length=1024) # 将标签设置为输入ID(对于因果语言建模) model_inputs["labels"] = model_inputs["input_ids"].copy() return model_inputs dataset = load_dataset("json", data_files="midtool_synthetic_data.jsonl", split="train") tokenized_dataset = dataset.map(preprocess_function, batched=True) # 3. 定义训练参数 training_args = TrainingArguments( output_dir="./midtool-finetuned-model", num_train_epochs=3, # Mid-training 轮次可以较少 per_device_train_batch_size=4, per_device_eval_batch_size=4, warmup_steps=100, weight_decay=0.01, logging_dir="./logs", logging_steps=10, save_steps=500, eval_strategy="steps", eval_steps=500, save_total_limit=2, load_best_model_at_end=True, report_to="none", # 或 "tensorboard" fp16=True, # 如果硬件支持 ) # 4. 创建 Trainer 并开始训练 trainer = Trainer( model=model, args=training_args, train_dataset=tokenized_dataset, # eval_dataset=tokenized_dataset_eval, # 如果有验证集 tokenizer=tokenizer, # data_collator=..., # 如果需要自定义数据整理器 ) trainer.train() trainer.save_model() tokenizer.save_pretrained("./midtool-finetuned-model")关键配置说明:
- 学习率:Mid-training 的学习率通常比预训练大,但比下游任务微调小。可以从
5e-5到1e-4开始尝试。 - 训练轮次:由于合成数据量可能远小于预训练数据,3-5 个 epoch 通常足够,需监控验证集损失防止过拟合。
- 数据混合:为了提高通用性,建议将工具使用合成数据与一部分通用指令数据(如 Alpaca、ShareGPT)混合训练,比例可以根据目标调整(例如 7:3,工具数据占 70%)。
- 损失函数:标准的下一个词预测(Causal LM)损失即可。如果希望模型更关注工具调用部分,可以在计算损失时对 CoT 和工具调用相关的 token 给予更高权重。
3.2 评估与迭代
训练完成后,必须对模型的工具使用能力进行评估。评估不应只看验证集损失,而应设计专门的评估集(Benchmark)。
- 构建评估集:包含未见过的指令、新工具(但定义类似)、复杂多步任务、有歧义或包含错误的指令。
- 评估指标:
- 工具选择准确率:模型是否选择了正确的工具。
- 参数提取准确率/F1:提取的参数是否与标准答案匹配。
- 端到端任务成功率:从指令到最终正确回复的比例。
- 幻觉率:模型是否编造了不存在的工具或参数。
- 迭代合成策略:根据评估结果,反推合成数据的不足。例如,如果模型在多工具规划上表现差,就在下一轮合成中增加更多、更复杂的多工具组合数据。
4. 常见问题与排查路径
在实践中,从数据合成到模型训练可能会遇到各种问题。下表列出了一些典型问题及其排查思路。
| 问题现象 | 可能原因 | 检查点与排查方式 | 处理建议 |
|---|---|---|---|
| 模型完全忽略工具,只用内部知识回答 | 1. 合成数据中工具调用模式不够突出。 2. 训练数据中通用对话数据比例过高。 3. 损失函数未对工具调用部分给予足够关注。 4. 模型容量太小,无法同时掌握通用知识和工具使用。 | 1. 检查合成数据样本,看“工具调用”部分是否清晰、格式统一。 2. 检查训练数据混合比例。 3. 在评估集上做消融实验,看模型是否看到了工具定义。 4. 查看训练日志,损失是否正常下降。 | 1. 强化合成数据中工具调用的格式(如使用特殊标记[TOOL_CALL]...[/TOOL_CALL])。2. 调整数据混合比例,增加工具数据权重。 3. 尝试在计算损失时,对工具调用相关的 token 进行加权。 4. 考虑使用更大规模的模型,或在已有工具使用能力的模型上进行微调。 |
| 模型能选择工具,但参数解析错误 | 1. 合成数据中参数提取的多样性不足。 2. 指令生成器生成的指令与参数关联性弱。 3. 思维链合成器中参数提取逻辑有缺陷,导致训练数据本身就有噪声。 | 1. 人工检查一批合成数据,对比“指令”和“提取的参数”是否合理。 2. 分析错误案例,看是参数类型错误、值错误还是缺失。 | 1. 改进指令生成器,使其能生成与工具参数强相关的、多样的指令。 2. 加强思维链合成器中的参数提取模块,可以引入一个小型的NER或信息抽取模型。 3. 在合成数据中增加“参数缺失-反问”的样本,训练模型主动澄清的能力。 |
| 模型在多步工具规划上表现差 | 1. 合成数据中多步任务的比例和复杂度不够。 2. 思维链合成器对多步任务的规划逻辑过于简单或模式单一。 3. 模型在生成长序列的规划步骤时容易出错。 | 1. 统计合成数据中多步任务的占比和步骤数分布。 2. 检查多步任务样本的思维链,是否逻辑清晰、步骤完整。 | 1. 显著增加多工具、多步骤合成数据的比例和复杂度。 2. 使用更强大的规划器(如基于规则的规划器或调用LLM)来生成高质量的多步思维链。 3. 在训练时,可以考虑使用课程学习(Curriculum Learning),先从单步任务开始,逐步增加步骤数。 |
| 训练后模型通用能力下降 | 1. 工具数据与通用数据分布差异过大。 2. 训练轮次过多,在工具数据上过拟合。 3. 学习率设置不当。 | 1. 在通用指令基准(如 MT-Bench 的部分问题)上测试模型表现。 2. 检查验证集(如果包含通用数据)的损失曲线,是否在后期上升。 | 1. 确保数据混合,并在通用任务上保留一部分验证集。 2. 使用更小的学习率,或采用余弦退火等学习率调度策略。 3. 早停(Early Stopping),根据通用能力验证集上的表现来决定停止时机。 |
| 合成数据质量低,包含大量错误或矛盾 | 1. 指令生成模板过于随机,产生无意义指令。 2. LLM生成的指令或思维链未经过滤。 3. 参数提取和结果模拟逻辑有bug。 | 1. 随机采样并人工审查100条合成数据。 2. 建立自动化的数据质量检查规则(如工具名是否在库中,参数类型是否匹配)。 | 1. 引入数据过滤和后处理步骤,剔除明显错误的数据。 2. 对LLM生成的内容,可以设计自洽性检查(如用另一个LLM或规则进行验证)。 3. 采用迭代式数据合成:用小规模高质量数据训练一个初始模型,用这个模型来帮助生成或筛选更多数据。 |
5. 最佳实践与扩展方向
5.1 合成数据质量保障清单
在启动大规模合成前,请对照此清单检查:
- [ ]工具定义清晰:每个工具的
name,description,parameters的描述是否准确、无歧义? - [ ]指令多样性:是否覆盖了直接指令、对话式指令、模糊指令、错误指令、多轮对话上下文?
- [ ]参数覆盖度:合成数据是否覆盖了每个参数的各种可能取值(包括边界值、错误值)?
- [ ]思维链合理性:合成的思考过程是否符合人类推理逻辑?是否清晰展示了“识别工具 -> 提取参数 -> 调用 -> 解释结果”的步骤?
- [ ]结果真实性:模拟的工具返回结果是否合理?例如,计算器工具不会返回文本,天气工具返回的数据在合理范围内。
- [ ]格式一致性:训练数据的格式(如对话模板、工具调用标记)是否在整个数据集中保持一致?
- [ ]噪声控制:是否有机制过滤掉包含事实性错误、逻辑矛盾或低质量语言的数据?
5.2 扩展方向
- 从规则合成到模型合成:初期可以使用规则和模板快速生成基础数据。随着迭代,可以训练一个小型“数据合成模型”,让它来生成更自然、更多样的指令和思维链。这类似于使用 LLM 来自动标注(Self-Instruct 的变种)。
- 引入真实交互数据:在智能体系统上线后,收集真实的用户与模型的成功/失败交互日志。这些数据极其宝贵,可以用来进一步微调模型,或作为种子来生成更多类似的合成数据。
- 支持动态工具库:当前的合成管道假设工具库是静态的。可以扩展系统,使其能够根据新加入的工具描述,自动生成相应的训练数据,实现工具的“即插即用”式学习。
- 多模态工具使用:除了 API 调用,工具还可以是图像处理、音频生成等。合成数据需要包含对多模态输入/输出的描述和处理逻辑。
- 强化学习整合:将 Mid-training 与强化学习(RL)结合。用合成数据初始化模型,然后在模拟环境或真实环境中通过 RL 进一步优化其工具使用策略,例如学习何时不调用工具、如何从失败中恢复。
Mid-training Data Synthesis 是一个强大的范式,它通过创造性的数据生成,将人类的先验知识(如何正确使用工具)系统地注入模型。其成功的关键不在于合成数据的绝对数量,而在于其质量和多样性能否有效覆盖目标能力空间。从定义清晰的结构化工具开始,精心设计合成策略,并建立持续的质量评估与迭代循环,你就能为你的 AI 智能体打造出坚实可靠的工具使用基本功。