返回顶部
热门问答 更多热门问答
技术文章 更多技术文章

Qwen Agent | Function Call兼容OpenAI工具调用改造

[复制链接]
链载Ai 显示全部楼层 发表于 半小时前 |阅读模式 打印 上一主题 下一主题

上回给大家分享了: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后直接卡住,未正常调用工具,无法继续运行。

qwen agent调用qwen3/qwq-32B等支持tools传参模型效果,运行卡住

我们查看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也未必是一种很好的设计。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

链载AI是专业的生成式人工智能教程平台。提供Stable Diffusion、Midjourney AI绘画教程,Suno AI音乐生成指南,以及Runway、Pika等AI视频制作与动画生成实战案例。从提示词编写到参数调整,手把手助您从入门到精通。
  • 官方手机版

  • 微信公众号

  • 商务合作

  • Powered by Discuz! X3.5 | Copyright © 2025-2025. | 链载Ai
  • 桂ICP备2024021734号 | 营业执照 | |广西笔趣文化传媒有限公司|| QQ