上回给大家分享了:Agent | MCP & Function Calling流程解读" data-itemshowtype="0" linktype="text" data-linktype="2" style="letter-spacing: 0em;text-align: left;text-indent: 0em;background-color: transparent;caret-color: var(--weui-BRAND);">Qwen Agent | MCP & Function Calling流程解读,以数据库操作为例详细解读了mcp & function calling调用的流程,了解输入输出如何适配工具调用,市面上大部分主流模型都能支持,不管是否带思维链。整体原理回顾,大模型以Assistant身份返回工具调用指令 -> Agent框架触发实际调用获取工具返回结果并以User身份追加到会话上下文->大模型以Assistant身份读取带工具结果的上下文信息,如果需要更多工具调用则重复上述过程,否则直接回答并输出。文章受众还是很广的(~6.5k阅读),qwen团队的运营同学都联系上我了。因此,决定继续沿着这个方向,给大家分享更多相关内容。
用户反馈过程中收到一些同学的反馈,认为qwen3等模型原生支持openai格式的tool调用,直接传tools即可,返回结果中的tool_calls字段里有调用工具的指令。说的也没错。不过,不管是qwen3还是GPT-4o等支持传tools的大模型,每次的工具调用本质都是通过prompt+多轮交互驱动(第一轮返回工具调用指令+第二轮插入调用结果并回答)。openai格式封装的底层tools传参以及返回值中的tool_calls,本质和Qwen Agent类似,只是在接口内部定义了一套工具解析器,是一种工程化实现手段。所谓的原生支持,更多的是指"返回工具调用指令"的能力。 基于这个反馈,我考虑在Qwen Agent上尝试使用支持传tools的大模型。发现Qwen Agent的工具解析引擎并未适配这种情况。如下图所示,给Qwen Agent配置了一个image_gen工具,image_gen是个外部api,能基于prompt绘图并返回图像链接。正常情况下,模型能构造输入prompt参数并调用工具。此处模型在thinking后直接卡住,未正常调用工具,无法继续运行。  我们查看oai接口的返回数据,可以看到指令调用在tool_calls字段中正常返回,但qwen agent未正确解析,导致运行卡住。  问题定位究其原因,是因为Qwen Agent支持的大模型response中的tool_calls相关指令是在'content'字段返回的,通过Agent框架封装的单独工具解析器来适配。而qwen3等已经做了一层工程化封装tool_calls的模型,其参考openai的标准格式,返回的调用指令单独存放在“tool_calls”字段,造成Qwen Agent的解析失效。 从代码中也能发现,仅处理了content和reasoning_content相关字段,未处理tool_calls相关字段。  兼容oai的tool_calls工具调用一种思路是如果存在tool_calls字段,则将相应工具调用指令提取出来,和content内容拼接在一起,适配Qwen Agent的解析引擎,这样后续Qwen Agent即可正常解析content字段中的工具调用指令,触发实际调用和多轮交互。代码如下所示,在oai.py代码中修改: content = response.choices[0].message.contentor""
def_has_tool_calls(self, obj, attribute='tool_calls')-> bool: returnhasattr(obj, attribute)andgetattr(obj, attribute)
# 解析tool_calls字段的调用指令,构造成qwen agent的格式:<tool_call>\n{tool_call_json}\n</tool_call> def_format_tool_call(self, tool_call = None, tool_call_data: Dict = None)-> Tuple[str, bool]: function_name =None arguments =None # 从tool_call对象中提取函数名和参数 iftool_callis not None andhasattr(tool_call,'function')andtool_call.function: function_name = getattr(tool_call.function,'name','') arguments = getattr(tool_call.function,'arguments','') # 从tool_call_data字典中提取函数名和参数 eliftool_call_datais not None: function_name = tool_call_data.get('name','') arguments = tool_call_data.get('arguments','')
iffunction_nameis None orargumentsis None: return "",False function_name = function_nameor "" arguments_str = argumentsifargumentselse'{}'
try: # 尝试解析arguments为JSON对象 args_obj = json.loads(arguments_str) tool_call_obj = {"name": function_name,"arguments": args_obj} tool_call_json = json.dumps(tool_call_obj, ensure_ascii=False) returnf"\n<tool_call>\n{tool_call_json}\n</tool_call>",True exceptjson.JSONDecodeError: # 如果解析失败,使用空对象 tool_call_obj = {"name": function_name,"arguments": {}} tool_call_json = json.dumps(tool_call_obj, ensure_ascii=False) returnf"\n<tool_call>\n{tool_call_json}\n</tool_call>",True
# 实际使用时,拼接到content中 ifself._has_tool_calls(response.choices[0].message): fortool_callinresponse.choices[0].message.tool_calls: tool_call_str, success = self._format_tool_call(tool_call=tool_call) ifsuccess: content += tool_call_str# 以固定格式拼接到content中
修改后的代码示例,新增tool_calls处理。  更新后的效果如下是适配解析器格式后,qwq-32b的效果:   第三次调用时候报错了,模型反思后自我纠正了:   总结不管是openai底层封装单独的tool_calls字段、还是Qwen Agent基于content字段封装tool_calls,都是一种工具调用和解析方法,可以有不同的实现,也可以统一抽象成1种方法。本文演示了如何统一到Qwen Agent的解析器:单独获取openai格式的tool_calls字段然后拼接到content字段中,再运行qwen agent统一的工具解析器即可。不过这种方法只是一种workaround,相当于叠加了2次工具解析,理想情况下有更通用和顶层设计,兼容所有类型的模型,openai的tools传参和返回tool_calls也未必是一种很好的设计。 |