最近在做一个内部工具Agent,基于Qwen2.5-7B做LoRA微调,主要是让模型学会调用我们内部几个API(查库存、下单、算运费)。训练数据我参考了Function Calling的格式,用ChatML模板包了system、user、assistant三轮对话,工具定义放在system里。但微调完发现,模型偶尔能正确输出工具调用参数,但经常“自说自话”直接给用户答案,或者把参数格式写错(比如JSON少了逗号)。我已经调过学习率、epoch,也试过混入一些拒绝回答的负样本,效果还是不稳定。想问问大家,是不是工具调用的数据构造有特殊讲究?比如需要把工具描述写得特别详细?或者需要混合多轮带工具结果的样本?有没有踩过坑的朋友指点一下,谢谢!
微调后的模型做Agent工具调用总出错,是数据格式的问题还是我姿势不对?
全部回复
共 76 条我之前也踩过类似的坑,最后发现问题大概率出在数据构造的“对话流”上。你光把工具定义丢在system里是不够的,模型需要看到“工具结果返回后,assistant再根据结果生成最终回复”这个完整闭环,不然它学不会“先调用再回答”的节奏,自然就容易跳步。另外,你那些负样本是不是都放在最后一轮?我试过把“拒绝调用”的场景穿插在中间轮次,效果比全堆在结尾要好,模型会更清楚什么时候该收手。至于参数格式,我怀疑是你训练时JSON的序列化方式跟推理时不统一,比如键的顺序、缩进、甚至引号转义,LoRA对这类细节特别敏感,建议你在数据里故意混入几种不同风格但都合法的JSON写法,让模型学会“宽容”解析。还有个偏方:把工具描述里的必填参数用“必须包含”这种强约束词,比单纯列schema管用,模型对自然语言指令的服从性比对结构化定义高得多。最后,如果推理时还用贪心解码,建议换成采样加top_p,有时候输出崩坏纯粹是解码策略太死板。你试过把工具调用结果作为单独一条user消息回填到历史里再训练吗?这步没做的话,模型永远只学会“发起调用”,学不会“消化结果”。
工具描述确实得写细,但更关键的是得在数据里混入工具结果反馈,单靠一轮对话学不会调用时序。
我试过把工具调用和结果观察拆成多轮,再配合随机扰动参数,稳定性明显上来了。
你这情况我太熟了,之前调类似模型也栽过跟头。个人感觉数据格式占比很大,特别是工具描述别写太抽象,把每个参数含义和取值示例都塞进去,模型就有抓手了。另外多轮对话里,如果上一轮工具结果返回了,下一轮模型输出格式也得跟着变,这块容易忽略。还有个小技巧,把错误参数格式的样本也放进训练集,让模型见过“错”的,反而更知道怎么输出“对”的。你试试把工具调用和普通回复的样本比例调到7比3,我这边效果稳了不少。
我最近也踩过类似的坑,后来发现数据格式里最关键的其实是“工具调用结果”那轮得单独拆出来,别跟assistant的最终回复混在一起写。还有你试过把工具描述里加一些具体示例参数吗?模型对空泛的schema理解很差,给几个“正确调用示例”进去,准确率能上来不少。另外生成时记得关掉采样或者调低temperature,LoRA微调后模型对随机性特别敏感。
我之前也踩过类似的坑,后来发现问题多半出在训练数据里工具调用的“对话流”不够完整。你光给单轮工具调用不行,得把模型从判断“该调工具”到“调完工具怎么把结果自然说给用户”的整个链路都喂进去,不然它根本学不会什么时候该闭嘴。
另外你试试把工具描述里每个参数的边界值、可选值都写死,甚至给几个JSON正例和错例放在system里,模型对格式的感知会强很多。负样本别只混拒绝回答,多加点“用户问题含糊时先反问”的样本,比单纯防幻觉有用。
我怀疑你现在的数据里“必须调工具”和“可以不调工具”的比例太失衡了,模型学成了只要能糊弄就糊弄,你检查下是不是所有user query都对应了工具调用?如果全是强绑定,它当然会乱来。
工具调用的数据里,工具描述和参数示例的格式一致性比你想的重要,试试把每个API的样例输出都固定成完整JSON再看看。
我之前也踩过这坑,后来发现多轮历史里工具结果得紧跟助手调用,别让模型自己脑补格式,你检查下数据里有没有这类断层。
数据格式问题概率大,试试把system里的工具描述改成JSON Schema,再给几个带错误修正的few-shot样本。
我之前也卡这,后来发现拒绝样本比例别超10%,多轮工具结果回填比想象中重要。
我之前也踩过类似的坑,后来发现问题多半出在数据构造上。工具描述不用太啰嗦,但参数格式和必填字段一定要在system里写死,最好每个参数都给个示例值。另外多轮对话里,如果上一轮工具结果返回后模型需要继续调用,这种衔接样本得多加,不然它容易“跳戏”直接答用户。还有个土办法,把错误输出当成负样本混进去微调,比纯靠调参管用。
数据格式肯定有坑,但更可能是工具描述和真实调用逻辑没对齐,试试把API参数示例直接塞进few-shot里。
工具描述确实得写细点,但更关键的是得在数据里混入工具结果回填的多轮样本,纯单轮对话模型学不会状态跟踪。
我之前也踩过类似的坑,尤其是Qwen这种本身指令遵循能力很强的底座,LoRA微调时如果数据里“拒绝调用”或“直接回答”的样本比例不对,模型很容易学会偷懒。你提到混了负样本,但关键要看负样本是不是跟正样本在对话结构上完全一致——如果负样本里的user query跟正样本太像,模型反而会困惑到底该调工具还是该聊天。
工具描述确实有讲究,但不是越长越好,我试过把每个API的参数约束、必填项、返回值的典型示例都写清楚,效果比单纯堆功能描述强很多。另外你的训练数据里有没有包含“工具调用结果反馈给模型后再生成下一轮”的样本?如果只有单轮工具调用,模型学不到“看到工具输出后要总结给用户”这个闭环,它就容易在生成参数时半途而废。
还有个小细节,你说JSON偶尔少逗号,这往往是数据里工具调用的输出格式不一致导致的——比如有的样本用```json代码块包裹,有的直接裸JSON,模型就学乱了。我后来把所有工具调用结果都统一成纯文本的“Action: xxx\nAction Input: {json}”,不再用代码块,错误率降了不少。
另外可以试试在训练时把system里的工具定义顺序打乱,让模型学会按需匹配而不是死记硬背位置,我之前固定顺序时模型偶尔会串参数。最后想问下你用的LoRA rank和target modules是什么?我有次rank设太高导致灾难性遗忘,调回16之后稳定性好了很多。
工具描述确实得写细点,但更关键的是得让模型见过工具结果回填的样本,纯对话式微调它当然爱瞎答。
说实话我之前也踩过类似的坑,后来发现多半是数据里工具调用的“对话轮次”结构没对齐。光把工具定义塞system里不够,最好在每轮assistant输出后紧跟一个tool结果,再让模型基于结果继续,这种闭环样本至少要占一半。另外你试试把工具描述里的参数约束写得更死板一点,比如直接给出JSON schema示例,而不是自然语言解释,模型对格式的记忆会强很多。负样本混入比例也别太高,我试过超过20%反而会让模型变得过于保守,宁可乱答也不调用工具。
工具描述得写细点,参数类型和必填项都列清楚,另外负样本别光混拒绝,得混点格式错的让它学会纠错。
这个问题我踩过类似的坑,感觉大概率不是数据格式本身,而是训练目标的粒度没对齐。你现在的样本里,assistant那一轮同时承担了“判断要不要调工具”和“生成参数”两件事,模型很容易学成直接回答更省事,因为直接给答案的loss下降更快。我后来把任务拆成两步,先专门训一个判断是否调用的分类头或者短输出,再单独训参数生成,稳定性好了不少。另外JSON少逗号这种事,光靠SFT很难根治,可以考虑在推理时加constrained decoding,或者用语法约束的采样,别指望模型自己学会严格语法。还有一点,工具描述真的别写太长太抽象,字段名和类型要跟你实际校验逻辑完全一致,否则模型学到的映射是飘的。负样本也别只加“拒绝回答”,得加那种“看起来该调但实际不该调”的hard negative,不然模型学不到边界。
工具调用得混多轮样本,光靠system描述不够,模型容易偷懒直接答。