最近在做一个内部工具Agent,基于Qwen2.5-7B做LoRA微调,主要是让模型学会调用我们内部几个API(查库存、下单、算运费)。训练数据我参考了Function Calling的格式,用ChatML模板包了system、user、assistant三轮对话,工具定义放在system里。但微调完发现,模型偶尔能正确输出工具调用参数,但经常“自说自话”直接给用户答案,或者把参数格式写错(比如JSON少了逗号)。我已经调过学习率、epoch,也试过混入一些拒绝回答的负样本,效果还是不稳定。想问问大家,是不是工具调用的数据构造有特殊讲究?比如需要把工具描述写得特别详细?或者需要混合多轮带工具结果的样本?有没有踩过坑的朋友指点一下,谢谢!
微调后的模型做Agent工具调用总出错,是数据格式的问题还是我姿势不对?
全部回复
共 60 条试试把工具调用结果也塞进assistant轮次做多轮样本,光靠system描述模型真记不住。另外检查下是不是温度设太高了,推理时调低点能救不少格式问题。
我之前也踩过类似的坑,尤其是Qwen2.5这种底座,它对工具调用的格式其实挺敏感的。你提到把工具定义塞在system里,但说实话,我后来发现工具描述太长反而容易让模型“分心”,它会觉得这些是背景知识而不是硬性指令,结果就倾向于直接回答而不是触发调用。我试过把工具定义改成更紧凑的伪代码风格,并且在每条user消息里都重复一遍当前可用的工具名,效果比只放system里稳定很多。
另外你说参数格式错乱,我猜可能是训练时机的问题——LoRA微调时如果混合了太多普通对话,模型会自然滑回“聊天模式”。我当时的做法是强制在每条assistant输出前加一个固定的“调用标记”,比如“
还有个细节你试试:把工具调用结果作为下一轮user输入,但不要每次都让模型继续调用,偶尔插入“好的,已为你完成”这种收尾话术,让模型学会终结流程。我怀疑你训练数据里全是“调用-返回-再调用”的循环,模型没见过“调用完就结束”的情况,所以它才会在参数对的时候也硬要补一句废话。
我最近还在折腾一个点,就是数据里工具参数的填充方式——你是不是直接把真实值写死了?我试过在训练时随机替换一些参数值为“未知”或“待确认”,让模型学会反问而不是硬编,这个对减少幻觉参数还挺有用。你可以先跑个几百条的小实验,单独测试工具调用和不调用的比例,看看到底是哪里先崩的。
我最近也踩过类似的坑,后来发现光有对话格式还不够,工具调用那轮得把“思考过程”也写进去,比如先输出“需要查询库存”再给JSON,模型会更稳定。另外你试试把工具描述里的参数约束写具体点,比如枚举值、必填字段都列清楚,能少很多格式错乱。负样本我混了大概15%才见效,太少没用,太多模型会变得太保守。你现在的数据里,带工具结果的轮次占比是多少?
我之前也踩过类似的坑,尤其是参数格式乱掉那块,后来发现大概率不是模型学不会,而是数据里工具调用的“轨迹”不够清晰。你试过把工具描述改成“动作+参数约束+示例”的写法吗?比如在system里直接给一个完整调用范例,比单纯列字段有效得多。另外我怀疑你的负样本混入比例可能太高了,模型会倾向于保守地拒绝调用,反而把“自说自话”当成安全策略。还有个细节,ChatML模板里如果assistant轮的tool_call和tool_result是分两条消息,但训练时没刻意对齐position_id,输出就容易漂移。我后来是把工具结果强制拼进assistant下一条文本里,效果稳定很多。你可以先拿20条badcase看下,是集中在某个API描述模糊,还是所有调用都随机出错,前者改数据,后者就得重新审视模板结构了。多轮带工具结果的混合确实有必要,但别超过总数据量的15%,不然模型会混淆“该调用”和“该回答”的边界。
工具调用数据里多轮工具结果反馈很关键,只给单轮对话模型学不会“先调用再回答”。试试每个样本都带真实API返回值。
工具描述建议直接贴真实API的json schema,别自己精简,格式崩多半是训练时样例太少或太单一。
建议试试把工具调用结果也作为assistant消息回填进训练数据,让模型看到完整闭环,光靠描述格式确实容易飘。
数据格式是一方面,但更可能是工具描述不够细,试试把每个参数必填和可选标清楚,我这么改完稳定多了。
我最近也在搞类似的,踩过一样的坑。你试试把工具描述写成“用户视角”的完整句子,别只列参数,比如“当用户问XX时,调用A并传入B”,模型对语义场景的泛化比纯格式敏感得多。另外,数据里一定要混入“工具结果返回后再让模型总结”的轮次,不然它学不会“调用后继续对话”的闭环。负样本别只加拒绝回答,要加“该调用但没调用”的失败例子,误差会明显少很多。
工具描述确实得写细,但更可能是数据里工具调用和普通回答的比例失衡了,试试多塞点真实调用轨迹。
这个坑我太熟了,当时用7B模型做类似的事也折腾了好久。你试了调参和负样本,但感觉核心问题可能出在数据构造的“对话流”上——单纯把工具定义塞system里,模型其实没学会“什么时候该调、什么时候不该调”的边界感。我后来是把工具调用拆成两轮:第一轮强制模型输出“需要调用工具”的意图,第二轮再让它在assistant里生成严格JSON参数,这样模型对格式的注意力会集中很多。另外你提到参数漏逗号,我猜是不是训练数据里工具返回结果的格式太单一?建议你在每轮工具结果后都接一段assistant的自然语言总结,让模型看到“工具输出→整理成用户能懂的话”这个完整闭环,否则它容易把工具响应当最终答案直接吐出来。还有个细节,工具描述别太啰嗦,但参数说明一定要带类型和取值范围示例,甚至把“必填/选填”直接写进去,不然小模型很容易在枚举值上犯迷糊。最后想问下,你混负样本的时候,有没有故意放一些“工具不可用”的场景?我试过加20%这种样本,模型乱答的情况会明显减少,但代价是偶尔会过度保守,该调工具时也不调了。
我之前也踩过类似的坑,最后发现是工具描述的措辞太“抽象”了,模型根本没法把参数和现实场景对应起来。你试试把每个API的输入输出样例直接写进system里,最好带一两个极端边界值的例子,模型会稳很多。另外你那个ChatML格式里,assistant的回复如果带工具调用,后面一定要跟一个tool角色的结果轮次,不然模型容易学成“自问自答”的坏习惯。负样本混入比例别超过10%,多了模型会变得过于保守,连该调用的都不敢调了。
建议试试把工具调用的few-shot样例直接塞进user消息里,光靠system描述模型真学不进去。
另外检查下是不是训练时把assistant的JSON截断成多段了,我之前就是这么翻车的。
工具描述确实得写细,但更可能是训练时tool call的response格式没对齐,建议检查下模板里assistant回包结构。
数据里多轮工具结果回填的格式挺关键,试试把每轮tool output都按严格JSON截断清洗,别让模型学到脏数据。
我之前也踩过类似的坑,Qwen系对tool calling的格式其实挺敏感的,尤其是system里工具描述和样例的措辞,建议把每个参数的约束和枚举值写死,甚至给一两个“错误格式→正确格式”的对比样本。另外你试过把工具结果直接拼进assistant的上下文里做多轮微调吗?光训练单轮调用很容易让模型觉得“答完就算完”。还有个细节,LoRA的rank和target modules对这类结构化输出影响很大,试试把注意力层的q/k/v都加上,说不定比调学习率管用。
说实话我也踩过类似的坑,最后发现大概率是数据格式的锅,而不是模型本身的问题。你用的ChatML模板没问题,但工具定义放system里有个隐患——模型对超长system的注意力会衰减,尤其当工具描述和对话历史堆在一起时,它容易“忘记”该走工具调用路径。我后来改成把工具定义放在user消息末尾,或者干脆用单独的tool角色轮次,效果立刻稳定不少。另外你的训练数据里,assistant的回复是不是总以工具调用开头?如果模型见过太多“直接回答”的例子,它自然会倾向跳过工具。建议你把工具调用结果也作为下一轮user输入喂回去,强制它学会“先调用再总结”的闭环。还有一个细节:JSON参数格式错误,大概率是你训练时把参数值写得太随意,比如数字没用引号、字符串忘了转义,模型学到的就是“差不多就行”的坏习惯。可以试着在数据里刻意混入一些错误格式的负样本,并标注正确版本,让它学会纠错。最后,你试过temperature调低到0.1以下吗?生成阶段随机性太高也会导致格式崩坏,这跟训练无关。
我最近也在搞类似的,踩过一样的坑。工具调用这块,数据格式真的比想象中敏感,尤其是JSON输出,建议你在训练数据里故意塞一些“残缺对话”样本,让模型学会在工具结果后继续追问或修正,而不是直接一股脑给答案。
另外,工具描述别光写“查库存”,最好把参数类型、必填项、输出示例都写进去,模型对细节的感知力很弱,得喂得足够具体它才不乱编。你负样本加了多少?我试过10%左右效果还行,多了反而会让模型变得太保守。
还有个小技巧,训练时把工具调用结果也包一层assistant的“思考”步骤,比如“好的,我来查一下库存”再输出JSON,这样模型会更自然地走流程。你可以先拿几个bad case反推数据,看看是不是上下文里缺了某轮关键信息。
我之前也踩过类似的坑,后来发现多半是数据里工具调用的“真实分布”不够。你只给了工具定义,但没让模型见过大量“先犹豫再调用”或“调用失败后修正”的样本,它就容易直接瞎编答案。建议把工具描述里的参数约束写得更死,比如枚举值、必填字段标清楚,甚至加一两个错误示例。另外,你试试在训练时混入20%左右的多轮历史对话,让模型学会“看到上一步返回结果再决定下一步”,而不是单轮硬怼。参数格式出错的话,可以检查下是不是tokenizer把JSON里的空格或换行给截断了,我之前调大max_length就明显好转。
我之前也踩过类似的坑,后来发现光是格式对没用,工具描述里参数类型、枚举值、依赖关系写得越具体,模型越不容易瞎编。另外你试试把工具调用结果作为下一轮user输入塞回去,强制模型基于真实返回值做推理,而不是只训练它输出一段JSON。还有个小细节,负样本别只混拒绝回答,可以故意给一些“用户问A但工具该调B”的干扰样本,让模型学会区分意图。你现在的训练数据里,多轮对话的比例占多少?我怀疑单轮样本太多会导致模型缺乏上下文连贯性,参数格式就容易崩。
数据格式只是一半,工具描述别写太满,得让模型学会“先想再调”,不然它容易把调用当闲聊。你试过在assistant回合强制拼接工具结果吗?