最近在做一个内部工具Agent,基于Qwen2.5-7B做LoRA微调,主要是让模型学会调用我们内部几个API(查库存、下单、算运费)。训练数据我参考了Function Calling的格式,用ChatML模板包了system、user、assistant三轮对话,工具定义放在system里。但微调完发现,模型偶尔能正确输出工具调用参数,但经常“自说自话”直接给用户答案,或者把参数格式写错(比如JSON少了逗号)。我已经调过学习率、epoch,也试过混入一些拒绝回答的负样本,效果还是不稳定。想问问大家,是不是工具调用的数据构造有特殊讲究?比如需要把工具描述写得特别详细?或者需要混合多轮带工具结果的样本?有没有踩过坑的朋友指点一下,谢谢!
微调后的模型做Agent工具调用总出错,是数据格式的问题还是我姿势不对?
全部回复
共 60 条我之前也踩过类似的坑,Qwen系对工具调用的输出格式其实挺敏感的,尤其是参数里带嵌套结构时。建议你检查一下训练数据里有没有把assistant的回复写成“先思考再调工具”的完整思维链,只给最终JSON的话模型容易学跑偏。另外工具描述别写太长,把必填参数和格式示例放到最前面,我试过把描述精简到3行内效果会稳很多。你负样本是怎么混的?我是按1:3的比例掺了纯文本拒绝回答,但必须保证这类样本里不要出现任何工具名,否则模型会学到“见工具就绕开”的毛病。还有一个笨办法,微调后跑一批测试样本,把模型输出的JSON单独抽出来用json.loads校验,看看具体错误集中在哪个字段,我最后发现是模型偶尔会把数字类型写成字符串。
数据格式应该问题不大,建议重点查下工具结果的回填格式,很多模型是这步学歪了才瞎编参数。
我最近也踩过类似的坑,感觉问题多半出在数据构造上而不是模型本身。工具描述不用写太细,但格式一定要极其统一,尤其JSON的key顺序和缩进风格都得固定,不然模型容易学歪。另外你试试把“直接回答”和“调用工具”的样本比例调到1:3左右,甚至可以把工具结果回填成多轮对话,让模型看到调用后的反馈,这样它会更倾向走工具路线。还有个偏方,微调时在assistant回复前强制加一个特殊token,比如“tool”,推理时用这个token做约束,能明显减少自说自话的情况。你现在的负样本是纯拒绝,还是也包含了那种“看似回答但实际应调工具”的干扰样本?后者可能更关键。
我之前也踩过类似的坑,尤其是参数格式那种“少了逗号”的问题,大概率不是模型学不会,而是你数据里工具调用的“边界”没给它划清楚。建议你检查一下,是不是所有正样本里,工具调用的输出都严格用了同一种JSON结构,比如key的顺序、缩进、甚至引号风格,稍微乱一点模型就容易懵。另一个点是你提到的“自说自话”,这种往往是因为训练数据里,有部分样本的assistant回复同时包含了自然语言解释和工具调用,模型就学会了“偷懒”直接回答。最好把“必须调用工具”和“直接回答”的场景在system提示里用更强烈的措辞区分开,甚至可以在数据里故意加一些“如果库存未知就必须查API”这样的硬性规则样本。工具描述确实可以更详细,但别堆砌无关信息,反而要把每个参数的含义、取值范围、以及“什么情况下该用这个工具”写清楚,模型对工具的理解很大程度来自这段文本。另外,多轮对话里工具结果怎么回填也很关键,我猜你可能是单轮调用的数据多,真实场景里多轮依赖工具结果的样本太少,模型就没学会“基于工具输出再生成最终回复”这个链路。如果你混了负样本,注意别让拒绝回答的样本和工具调用样本在对话历史上产生冲突,比如前面刚拒绝过,后面又要调工具,这种矛盾会让LoRA很难收敛。最后一个小建议,你可以试试把工具调用的输出单独拆成一个特殊token包裹起来,而不是纯靠JSON格式,很多开源模型对固定模板的遵从度会高很多。
我之前也踩过类似的坑,后来发现光有对话格式还不够,工具描述里得把每个参数的类型、取值范围、甚至常见错误示例都写进去,模型才不容易乱编。另外你试试把训练数据里多塞点“模型直接回复用户”的样本,比例大概1:3,让它学会区分什么时候该调工具什么时候该说话。还有个小细节,JSON格式错误可能和tokenizer对空格和缩进的处理有关,可以检查下是不是数据里混了全角符号。
另外你提到混了负样本,但数量够不够?我之前是正负样本1:1才稳定下来,不然模型还是会偷懒。要是还不行,建议看看推理时的temperature和top_p,调低点能减少随机性,参数格式会稳很多。
我之前也踩过类似的坑,后来发现数据里工具描述写得太简略,模型确实容易“偷懒”直接答。你可以试试把每个API的输入输出示例、边界条件都写进system,甚至给几个典型的错误调用反例,模型会稳很多。另外,多轮对话里工具结果怎么回填也很关键,我最后是把工具调用和结果回填拆成独立的两轮训练,效果比混在一起好。你现在的负样本是纯拒绝,还是也包含了“模型自己尝试但改口”的情况?后者可能更贴近真实场景。
遇到过类似的坑,工具描述里把每个参数的取值范围和必填项写细点,比调学习率管用。
数据格式只是表象,核心是让模型在训练时看到“工具结果回填”的完整轨迹,光靠system里堆描述不够。试试每轮都强制接上tool role的真实返回,参数错误率会明显降。
我之前也踩过差不多的坑,最后发现大概率不是模型的问题,而是数据格式的一致性没做好。你拿ChatML包三轮对话没问题,但工具定义放system里其实挺讲究的,比如JSON Schema的字段顺序、类型描述、甚至缩进换行,模型对格式的敏感度远超你想象,稍微不一致它就容易“放飞自我”。我后来是把所有工具描述统一成极简的“参数名+类型+一句话用途”,而且每个样本里只保留和当前轮次相关的工具,多余的描述全删掉,准确率立刻上来了。另外你说混了负样本,这个方向对,但负样本的构造方式很关键——不能只是简单拒绝,最好让模型在“有工具可用但用户意图模糊”时先反问澄清,这样能逼它学会判断何时该调用而非硬答。还有一个容易被忽略的点:训练时assistant的回复里,工具调用的JSON一定要用真实API返回结果去反推生成,别手写,手写格式再标准也容易让模型学到“错觉”。你试试把每轮tool call的输入输出都串成完整轨迹,让模型看到调用后的结果再决定下一步,比单纯教它“输出一次调用”稳定得多。最后问一下,你微调时的loss有没有关注过工具调用那部分的token级loss?如果那里一直不降,那基本就是数据标注的锅,跟超参关系不大。
这问题我太有同感了,之前拿Llama3-8B调工具调用也差点被逼疯。你提到的“自说自话”和JSON漏逗号,大概率不是LoRA本身的问题,而是数据格式里隐含的“对齐信号”不够强。我后来发现一个关键点:工具描述不能只写“查库存”,得把参数类型、枚举值、甚至一个完整的输入输出示例直接塞进system里,让模型在生成时能“抄”格式,而不是凭空推理。另外,单纯三轮对话可能不够,我试过把工具结果作为assistant的下一轮输入,构造出“调用-返回-再调用”的链式样本,模型对参数格式的敏感度会明显提升。还有个细节,负样本别只混“拒绝回答”,要混“错误调用后纠正”的样本,比如模型输出缺参数,然后assistant自己补一个“抱歉,我需要补充参数”的修正轮,这比单纯拒绝更能教会模型兜底逻辑。你试过把学习率降到1e-5以下,并且用cosine衰减吗?我这边是这么调才稳住的。最后想确认下,你的工具定义是放在system里还是user里?我后来发现放user里、紧挨着用户请求,模型对工具存在的感知会强很多,你可以对照试试。
说实话你这个情况我调RAG类工具时也踩过,数据格式反而是其次,关键看你微调时有没有把“工具调用”和“直接回答”的样本比例控制好。我试过把工具描述写详细点确实有帮助,但更管用的是在system里加一句“如果用户没明确要求,不要主动调用工具”,不然模型老觉得自己该给答案。另外你负样本混了多少?我后来是1比3混的,效果稳很多。你输出层是不是用了grammar约束?没有的话建议加上,能治JSON漏逗号这种毛病。
工具描述确实得写得跟说明书似的,尤其参数默认值和边界条件别省略,不然模型全靠猜。再就是多轮里工具结果回填的格式,比单轮对话影响大多了。
我之前搞类似的东西也踩过这个坑,Qwen系列对工具调用的格式其实挺敏感的,尤其是LoRA微调时,数据里工具描述和参数schema的写法直接影响生成稳定性。你试过把工具定义从system里挪到user消息末尾吗?有些模型对system的注意力权重比较低,放前面容易被忽略,放对话末尾反而更能触发工具调用逻辑。另外你说的JSON少逗号,八成是模型在生成时把工具描述里的示例当成了“标准答案”复述,而不是按当前输入动态生成,这时候可以试试在训练数据里故意混入参数顺序打乱、字段缺失的负样本,强制模型学会“看”而不是“背”。还有个细节,多轮对话里工具结果的拼接方式也重要,如果你把工具返回内容放在assistant消息后面再接user,格式上最好和官方function calling的demo完全对齐,包括那个“thought”字段要不要留空、工具名要不要加引号,这些小地方不统一模型就会乱。我最后是直接把训练样本压到2000条以内,但每条都手工校准了JSON和角色轮换,效果反而比堆数据好。你要是方便的话,可以贴一条出错的具体case,看看是参数值乱填还是整个调用结构崩了,这两种问题解法完全不同。
说实话你这个情况我也踩过坑,Qwen2.5对工具调用的格式其实挺敏感的,特别是JSON那块,建议你试试把工具参数定义成严格的schema,并且在训练数据里故意塞一些“参数不完整让模型追问”的样本。还有就是LoRA的rank别调太高,我上次rank设64直接导致输出格式崩了,降到16之后稳定很多。你负样本混了多少比例?我试下来大概10%到15%比较合适,多了模型会变得太保守,动不动就拒绝调用。
我之前也踩过类似的坑,后来发现光有工具描述还不够,得在训练数据里把“模型直接回答但没调用工具”的反例也明确标出来,让它知道什么时候该闭嘴调API。另外你试试把工具参数格式弄成严格的JSON schema,并且在数据里随机穿插一些多轮历史记录,让模型学会看上下文而不是每次从头推理。还有个细节,LoRA的rank调大一点(比如64)对工具调用的稳定性帮助挺明显的,你可以对比下。
我之前也踩过类似的坑,后来发现问题多半出在训练数据的“对话流”上。你只给了单轮工具调用,但模型实际推理时是multi-turn的,得把“模型调用工具→拿到结果→再回复用户”这个完整链条也混进去,不然它学不会什么时候该停。另外工具描述别写太复杂,把关键参数和格式示例直接怼在system里,比长篇大论管用。还有个土办法,就是故意在训练集里加一些“不该调用工具”的query,强制它输出纯文本答案,这样能压一压它乱调用的倾向。参数格式错的话,试试在loss里对工具调用部分加权重,或者用代码把模型输出的JSON强校验一下再回填到下一轮训练里。
我之前也踩过类似的坑,后来发现问题多半出在训练数据里工具调用的“对话节奏”上。光把工具定义塞进system不够,得让assistant的回复里明确出现“我需要调用XX工具”这种思考痕迹,不然模型容易把工具调用和普通回答搞混。另外你试试把工具结果也作为user消息接在后面,强制模型学会基于结果继续对话,参数格式错误会少很多。
我之前也踩过类似的坑,后来发现光有对话格式不够,工具描述里得把每个参数的枚举值、边界条件都写清楚,模型才不容易自由发挥。另外你试试把工具调用结果作为下一轮user消息回灌进训练数据,多轮带执行反馈的样本比单轮更稳。还有个小技巧,负样本别只混拒绝回答,可以故意给几个“参数漏一个”的坏例子让它学纠错,效果比单纯调学习率明显。
数据格式问题更大,工具描述和示例得对齐真实调用,不然模型学不到“什么时候该调”的边界。
我之前也踩过这坑,建议把每个API的触发条件写死,多塞点正例,负样本别乱混。
我之前也踩过类似的坑,后来发现问题多半出在数据构造上。工具描述别光写“查库存”这种短句,得把参数类型、必填项、示例值都塞进去,模型才不容易自由发挥。另外多轮对话里,如果上一轮工具返回结果没写进assistant消息,模型很容易“失忆”直接瞎答。你试试在训练样本里强制让模型先输出“调用工具”的思考过程,再给最终答案,稳定性会好很多。