最近在做一个内部工具Agent,基于Qwen2.5-7B做LoRA微调,主要是让模型学会调用我们内部几个API(查库存、下单、算运费)。训练数据我参考了Function Calling的格式,用ChatML模板包了system、user、assistant三轮对话,工具定义放在system里。但微调完发现,模型偶尔能正确输出工具调用参数,但经常“自说自话”直接给用户答案,或者把参数格式写错(比如JSON少了逗号)。我已经调过学习率、epoch,也试过混入一些拒绝回答的负样本,效果还是不稳定。想问问大家,是不是工具调用的数据构造有特殊讲究?比如需要把工具描述写得特别详细?或者需要混合多轮带工具结果的样本?有没有踩过坑的朋友指点一下,谢谢!
微调后的模型做Agent工具调用总出错,是数据格式的问题还是我姿势不对?
全部回复
共 60 条工具描述换成JSON Schema试试,我之前也这样,后来把每个参数示例都塞进去就稳多了。
我最近也在弄类似的,踩过一样的坑。后来发现工具描述写得太简短确实不行,得把参数类型、边界情况都写进去,不然模型很容易自己发挥。另外你试试在训练数据里把“不调用工具直接回复”的样本比例调低点,我这边从一半降到两成之后,乱答的情况明显少了。还有个细节是JSON格式错误,可以试试在loss里对格式错误部分加权重,或者干脆用代码约束输出再解析,比纯靠模型学要稳。
我之前也踩过类似的坑,后来发现光有ChatML格式不够,工具调用的结果轮次必须单独做,而且system里工具描述最好给几个“参数示例”,模型才会更稳。另外你试试把训练数据里“直接回答”的比例降到10%以下,强制它多走工具路径,我这边这样调完准确率提升挺明显的。还有个坑是LoRA的rank值,太低了学不会参数间的逗号这种细节,调高到32试试?
我之前也遇到过类似情况,LoRA微调工具调用特别容易在格式上翻车。后来发现数据里工具描述的详细程度影响很大,建议把参数类型、枚举值、边界条件都写清楚,甚至给一两个示例,模型更容易对齐。另外可以试试在训练时随机截断一些多轮对话,逼它学会根据上下文判断该不该调用工具,而不是每次都硬接。你负样本混了多少比例?我试过10%左右效果还行,再多反而会让模型变懒不爱调工具。
数据格式大概率没问题,问题可能出在工具描述不够具体,试试把参数约束和示例直接写进system里。
另外建议检查下训练数据里工具调用的占比,太少的话模型容易学偏。
工具描述确实得写细,尤其参数类型和枚举值,不然模型容易瞎猜。另外试试把拒绝回答的样本比例再调高些。
数据格式肯定是有讲究的,但我觉得你更大的问题可能出在“工具结果回填”这个环节。LoRA微调时如果只有调用动作没有后续的tool response,模型就学不会“调用完再根据结果说话”这个闭环逻辑,自然会瞎编。另外你的负样本混入比例是多少?我试过大概10%-15%的拒绝样本效果最好,太多会让模型变怂。还有个小坑,工具描述里别堆太多参数细节,把“什么时候该用这个工具”写清楚比写参数规范重要得多。
我之前也踩过类似的坑,试下来感觉数据格式比超参更关键。工具描述别写太短,但也不用特别详细,关键是样例里得覆盖各种边界情况,比如参数缺失、多轮对话里工具结果怎么引用。另外你试试把工具调用结果也拼进下一轮assistant里,做成多轮工具链,模型会更稳定。负样本比例我控制在10%左右,太高反而容易让模型不敢调用工具。
我之前也踩过类似的坑,后来发现问题多半出在训练数据里工具描述和真实调用格式的一致性上。你试试把工具定义里的参数示例写得更具体,比如直接给一个完整的JSON样例,模型会更容易模仿。另外,多轮对话里如果工具返回结果后没有强制要求模型基于结果再回复,它确实容易“飘”回自由聊天模式,可以试着在assistant带工具结果的轮次加一些固定引导词。还有个小技巧,负样本别只混拒绝回答,可以故意给一些格式错乱的工具调用让模型学会修正,效果可能比单纯调超参更明显。
我最近也在搞类似的,感觉光调格式真不够,工具描述的详细程度影响特别大,你得把参数含义、边界条件都写清楚,不然模型容易瞎猜。另外你是不是没做工具调用结果回填?就是assistant输出调用后,得接一个tool角色的真实返回,再让模型基于这个结果继续生成,不然它学不会“调用完还要跟用户解释”的节奏。负样本混入比例也别太高,我试过10%就明显影响正常调用了,建议先纯正向数据跑通流程再说。
这问题我太有同感了,之前拿7B模型做类似的事,折腾了大半个月,最后发现坑全在数据构造上。你提到的ChatML模板没问题,但工具描述那部分其实特别关键——光写“查库存”这种一句话肯定不行,得把参数类型、必填项、取值范围甚至边界情况全塞进去,模型才学得会“什么时候该调、调的时候该填什么”。另外我怀疑你负样本混入的方式可能有点问题,别光加“拒绝回答”,得让模型看到“用户问A但工具只能解决B”这种场景,不然它还是会硬调。还有个细节,多轮对话里工具返回的结果一定要接上,而且下一轮user的提问得和工具结果强相关,不然模型会以为工具调用是独立事件。你试试把工具调用的那轮assistant输出改成严格JSON,后面再接一个assistant的总结性回复,等于让模型先学会“调工具”再学会“用结果说话”,这两步分开训练比混在一起稳得多。至于参数格式错,我猜是tokenizer对数字和逗号的处理有点飘,可以在数据里故意多放一些带小数和长数字的样本,让它多见见世面。你要是还不行,可以试试把工具数量砍到两个以内跑通全流程,再加复杂度,不然模型可能根本没建立起“工具选择”的注意力。
我之前也踩过类似的坑,尤其是参数格式错乱那块儿,大概率不是LoRA本身的问题,而是你跟模型“沟通”工具的方式不对。你试试把工具描述从JSON Schema换成更接近人类阅读的自然语言,比如“当用户想查库存时,请输出参数:商品ID为数字类型,数量为整数”,模型反而更听话。另外,多轮对话里工具结果的拼接方式特别关键,如果前面几轮调用的返回结果格式不统一,模型学到的就不是“调用工具”这个动作,而是“模仿各种烂格式”的坏习惯。还有一个细节,训练时最好把真实用户可能说的模糊表达(比如“帮我看看那个蓝色的还有货吗”)跟严格参数抽取混在一起,不然模型只认得你模板里的标准问法。负样本混入比例我觉得5%到10%就够,太多会让模型变得过于保守,动不动就拒绝。你可以先拿20条你实际测试中失败的case,手动修正后单独加进训练集跑一遍,看看是不是立刻有改善——我那次就是这么救回来的。要是还不行,说不定得检查一下你tokenizer对中文和数字的切分,有时候工具名里的下划线会被拆得乱七八糟,直接影响输出稳定性。
工具描述确实得写细点,你试试把参数示例直接塞进system里,模型输出会稳很多。另外多轮工具结果回填的样本比例也得提上来。
我之前也踩过类似的坑,后来发现问题多半出在数据格式的“一致性”上。比如工具描述里如果参数类型写得太笼统,模型就容易在生成时自己脑补格式。建议你把每个API的参数示例都写死成完整JSON,最好在assistant轮里强制要求先输出“调用工具”再给结果,这样能减少它直接回答的倾向。
另外多轮对话里的工具结果回填也很关键,我之前只做单轮微调,模型一遇到多轮就崩,后来混入一些带工具结果和用户追问的数据,稳定性明显好了不少。负样本的比例也别太高,试过10%左右就够,太多会让模型变得过于保守。
你用的ChatML里tool_call_id和role有没有严格对齐?有时候是这些小字段没对上,模型才在格式上犯浑。可以再检查下,或者拿几个坏case出来,看它是在哪一步开始歪的。
说实话你这个情况我也踩过差不多的坑,LoRA微调做工具调用最大的问题往往不是模型学不会,而是数据里隐含的“决策逻辑”没给够。你光把工具描述放system里,但模型其实分不清什么时候该调工具、什么时候该直接回答,它可能觉得用户问一句“库存多少”直接回个数字更省事,所以你得在训练数据里大量强化“看到这类问题必须先调用工具”的映射,比如把assistant的回复统一写成“我来查一下库存,请稍等”然后紧跟工具调用,让它形成条件反射。另外你提到JSON格式不稳定,我怀疑是你数据里工具参数那块太“干净”了,建议你故意混一些带错误格式的负样本,让模型学会纠错,而不是只教它输出正确格式——因为真实场景里模型自己生成的中间结果经常是脏的。还有一个细节,工具描述的措辞真的会影响参数抽取,我之前把“商品ID”写成“物品唯一标识符”,模型就老抽错字段,后来改成跟API文档一模一样的名字和类型说明,效果立竿见影。你试试把工具定义里每个参数都加上取值范围或示例值,比如“库存数量:整数,例如5”,这样模型生成时会有锚点。另外多轮对话里的工具结果回填也特别关键,你如果只训练了单轮调用,模型就不知道上一步返回的数据该往哪里塞,导致它后续自己编答案。我建议你检查一下ChatML里tool role的message是不是完整保留了工具返回的原始内容,别把它简化了。最后想问下你负样本是怎么混的?如果只是随机插几句“不知道”,那模型可能学会了拒绝但没学会“为什么拒绝”,最好在负样本里也标注清楚“因为缺少参数所以无法调用”这层逻辑。
我之前也踩过一模一样的坑,Qwen系模型对工具调用的格式敏感度比想象中高很多。你用的ChatML模板本身没问题,但关键可能在于工具定义的放置位置和描述粒度。我试过把工具描述从简单的一句话扩成带参数示例、边界条件甚至错误提示的完整段落,模型输出稳定性明显上来了,尤其是JSON格式错误少了很多。另外有个细节,多轮对话里如果之前已经调用过工具,后续轮次的assistant回复里最好带上“根据工具结果”这类承接句,不然模型容易忘记自己是在做工具调用,直接跳回普通问答模式。你提到的负样本混入,我猜可能是比例或位置不对,我后来是把拒绝样本放在对话末尾,而不是随机插中间,效果才变好。还有个小技巧,训练时可以把工具调用的assistant回复里加一个特殊前缀标记,比如“ACTION:”,推理时再解析掉,能强制模型走工具分支。不过说到底,7B模型做多工具调用确实吃力,如果API数量超过三个,建议考虑把参数校验逻辑放到外部代码里,模型只负责输出意图和关键参数,剩下的JSON格式化交给程序兜底。你试过把工具定义改成JSON Schema格式而不是自然语言描述吗?那个对格式稳定性的帮助也很大。
说实话你这个情况我也踩过坑,Qwen2.5-7B对工具调用的格式其实挺敏感的,尤其是LoRA微调时,如果数据里工具定义和对话轮次的拼接方式不够统一,模型很容易学成“选择性失明”——有时候它觉得直接回答更省事,有时候又卡在JSON序列化上。我后来发现一个关键点:工具描述不能只写参数和功能,最好把每个参数的取值范围、必填可选、甚至典型示例都塞进去,模型对“具体例子”的模仿能力比对抽象规则强得多。
另外你提到混了负样本,这个方向对,但负样本的构造得讲究“接近但不完全正确”,比如模拟模型已经调用了工具但返回异常,然后让它转述错误或请求重试,而不是简单拒绝回答。我试过在每条正样本后面随机跟一条“工具结果为空”的assistant回复,让模型学会基于结果再生成,这样比单纯加拒绝类样本稳定多了。
还有个容易忽略的点:你的训练数据里,多轮对话的轮次比例是不是太偏了?我猜你大部分是单轮直接调用,但实际agent场景中,用户会追问“那换个大号呢”这种省略上下文的指令。如果微调数据里缺少这种跨轮次引用前文参数的样本,模型就会在长对话里把参数搞丢。你可以手动构造一些带历史工具结果的dialogue,让模型学会从之前的执行结果里取数,而不是每次都重新生成。
最后,检查一下你的LoRA是不是只挂了q_proj和v_proj,我试过加上gate_proj和up_proj效果会好不少,特别是对结构化输出这种任务,MLP层的学习很关键。如果还不行,试试把工具定义的system prompt改成更接近自然语言指令的风格,比如“当用户提到库存时,必须调用get_stock接口,参数为商品ID”,而不是纯JSON schema,模型有时对“白话约束”的遵从度反而更高。你现在的数据量大概多少条?如果少于两千条,可能还是得靠构造更多边界case来逼模型学会区分“该调用”和“不该调用”。
工具描述确实得写细点,尤其参数类型和必填项,不然模型容易猜。另外试试在训练数据里多塞几轮工具调用后的结果反馈,比单纯堆负样本管用。
我之前也踩过类似的坑,特别是LoRA微调做工具调用的时候,模型特别容易“飘”。你提到ChatML模板包工具定义,这个方向没问题,但我觉得关键可能不在格式本身,而在于工具描述和对话历史的组织方式。比如,Qwen的官方function calling教程里其实强调过,工具描述要尽量具体,最好带上参数类型、枚举值、示例值,甚至边界条件,不然模型对“什么时候该调、什么时候不该调”的理解会很模糊。另外,你混合负样本的思路是对的,但负样本的比例和构造方式也很有讲究——如果只是简单拒绝,模型可能学会的是“回避工具”,而不是“判断何时不调”。我自己的经验是,多轮带工具结果的样本特别重要,尤其是那些“工具返回错误后模型要修正参数”的样本,这能让模型真正理解工具调用的闭环逻辑。还有个细节,你有没有检查过tokenizer对工具定义的截断?如果system太长被截断,模型其实根本没看到完整工具信息,那输出自然不稳定。建议你把工具描述精简到核心字段,同时把训练数据里的JSON格式统一成严格模式,比如所有键都用双引号、别留空格,这样能减少模型生成时的格式漂移。最后,如果还是不稳定,可以试试在推理时强制加一个“输出前先判断是否需要工具”的prompt前缀,有时候比纯靠微调更管用。
我之前也踩过类似的坑,后来发现数据里工具描述的语序和关键词权重影响特别大,你得把必填参数和可选参数分开写,最好每个参数都加个示例值。另外别光靠ChatML,试试把工具调用结果也作为user消息塞回去,模拟真实的多轮反馈,模型会更容易学会“先调用再回答”的节奏。还有你那个JSON少逗号的问题,我怀疑是分词器把数字和引号切碎了,可以在训练时加几个故意漏标点的负样本让它学会纠错。