当前位置:   article > 正文

Qwen2在Java项目中如何实现优雅的Function_Call工具调用_qwen2 function call

qwen2 function call

在当今AI技术飞速发展的背景下,大语言模型如Qwen2和GLM-4凭借其强大的语言处理能力,在诸多领域展现出了巨大的潜力。然而,大模型并非全知全能,它们在处理特定任务时,尤其是在需要与外部系统交互或执行具体功能时,会遇到一定的局限性。这主要是因为大模型通常被设计为封闭的文本生成系统,缺乏直接调用外部工具或API的能力。这种局限性凸显了工具调用在实际应用中的必要性,它能够扩展模型的功能边界,使其能够在真实世界场景中执行更加复杂和具体的操作。

工具调用的必要性

尽管大模型在自然语言理解和生成上取得了显著进步,但它们往往受限于训练数据的内容,无法直接访问网络资源、执行代码或操作数据库等。这意味着在解决实际问题时,模型可能无法提供直接、即时且准确的解决方案,尤其是那些需要实时数据处理或特定功能执行的任务。因此,通过工具调用来增强大模型的功能,成为提升其实用性和灵活性的关键。

在此背景下,ChatGLM3以及最近的GLM-4原生就已经支持了工具调用,这就非常方便,通过直接与外部工具交互,减少了中间环节,提高了响应速度和效率。

tools = [
    {
        "name": "track",
        "description": "追踪指定股票的实时价格",
        "parameters": {
            "type": "object",
            "properties": {
                "symbol": {
                    "description": "需要追踪的股票代码"
                }
            },
            "required": ['symbol']
        }
    },
    {
        "name": "text-to-speech",
        "description": "将文本转换为语音",
        "parameters": {
            "type": "object",
            "properties": {
                "text": {
                    "description": "需要转换成语音的文本"
                },
                "voice": {
                    "description": "要使用的语音类型(男声、女声等)"
                },
                "speed": {
                    "description": "语音的速度(快、中等、慢等)"
                }
            },
            "required": ['text']
        }
    }
]
system_info = {"role": "system", "content": "Answer the following questions as best as you can. You have access to the following tools:", "tools": tools}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

但是Qwen1.5以及Qwen2并不具备原生的工具调用功能,得借助于其Qwen-Agent框架或者langChain框架。那不借助Python框架,我就要使用Java实现该怎么做呢?

使用Java实现Qwen2工具调用

首先,我们需要自定义两个注解FunctionDef​和FunctionParam

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface FunctionDef {
    /**
     * 函数名称
     * @return 函数名称
     */
    String name() default "";

    /**
     * 函数描述
     * @return 函数描述
     */
    String description();
}

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface FunctionParam {
    /**
     * 参数名称
     * @return 参数名称
     */
    String name();

    /**
     * 参数描述
     * @return 参数描述
     */
    String description();

    /**
     * 参数枚举
     * @return 参数枚举
     */
    String[] enums() default {};

    /**
     * 是否必填
     * @return 必填
     */
    boolean required() default false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

然后,我们可以根据自己的需求,创建几个工具插件。下面是我创建的一个查询天气的插件:

public class WeatherTool {

    /**
     * 查询天气
     * @param city 城市
     * @return 天气信息
     */
    @FunctionDef(name = "getWeatherInfo", description = "get the weather info")
    public static String getWeatherInfo(
            @FunctionParam(name = "city", description = "the city name") String city
    ) {
        if (city == null || city.isEmpty()) {
            throw new IllegalArgumentException("City name must not be null or empty");
        }

        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(60, TimeUnit.SECONDS)
                .writeTimeout(60, TimeUnit.SECONDS)
                .readTimeout(60, TimeUnit.SECONDS)
                .build();

        try {
            Map<String, String> headers = new HashMap<>(16);
            headers.put("Content-Type", "application/json");
            Request.Builder builder = new Request.Builder()
                    .url("https://query.asilu.com/weather/baidu/?city="+city);
            builder.headers(Headers.of(headers));
            builder.method("GET", null);
            Request request = builder.build();
            Response response = client.newCall(request).execute();
            if (response.isSuccessful()) {
                ResponseBody responseBody = response.body();
                JSONObject jsonObject = JSONObject.parseObject(responseBody.string());
                return jsonObject.toString();
            } else {
                throw new OpenAIChatException("Failed with status code %d. messages: %s", response.code(), response.message());
            }
        } catch (IOException e) {
            e.printStackTrace();
            return "Error encountered while fetching weather data!";
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

再然后,我们把所有的工具插件都交给大模型,让它判断要满足用户的提问,应该选择哪个工具插件:

public String getToolResult(String sessionId,String prompt, List<Function> baseTools){
		String class2Json = buildClass2Json(new BaseFunction());
        String finalPrompt = String.format("你是一个AI助手,我会给你一个工具对象集合,工具对象包括name(工具名)、description(工具描述)、clazz(工具类名)、parameters(工具参数)。" +
                        "你可以结合工具对象,从用户的问句中提取到关键词,确定要实现用户的任务应该选择哪个工具对象和工具的参数。" +
                        "【工具集合】:%s。" +
                        "【用户提问】:%s?" +
                        "您的响应结果必须为JSON格式,并且不要返回任何不必要的解释,只提供遵循此格式的符合RFC8259的JSON响应。以下是输出必须遵守的JSON Schema实例:‍```%s‍```",
                JSON.toJSONString(baseTools),prompt,class2Json);
        String funcParams = chat(sessionId,finalPrompt);
        funcParams = JSON.parseObject(funcParams, OpenAIChatResponse.class).getChoices().get(0).getMessage().getContent();
        funcParams = funcParams.substring(funcParams.indexOf("{"), funcParams.lastIndexOf("}")+1);
        return LoadFunctions.load(JSON.parseObject(funcParams, BaseFunction.class));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

确定哪个工具插件后,再使用LoadFunctions.load加载执行这个工具插件:

public static String load(BaseFunction baseFunction){
        String className = baseFunction.getClazz();
        String methodName = baseFunction.getFunctionName();
        Map<String,String> arg = baseFunction.getParams();
        List<String> params = new ArrayList<>();
        String result = "";
        try {
            // 加载类
            Class<?> clazz = Class.forName(className);
			//可以使用arg.size确定几个参数,我为了演示方便,这里就默认只有一个参数了
            //int size = arg.size();
            Method method = clazz.getMethod(methodName,String.class);
            Parameter[] parameters = method.getParameters();
            // 如果方法有参数,并且参数类型已知(例如只有一个String类型的参数)
            for (int i = 0; i < parameters.length; i++){
                params.add(arg.values().stream().skip(i).findFirst().orElse(null));
            }
            // 创建类的实例,如果CarBean有一个无参构造函数
            Object instance = clazz.newInstance();
            result = method.invoke(instance,params.toArray()).toString();
        } catch (ClassNotFoundException e) {
            LOG.error("类未找到: {}" , className);
        } catch (NoSuchMethodException e) {
            LOG.error("找不到方法: {}" , methodName);
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
            LOG.error("无法调用方法: {}" , e.getMessage());
        }
        return result;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

最后,我们就可以拿到工具执行的结果,然后把工具执行结果直接给到大模型,让它组织语言回答用户提问就可以了

public Flux<String> streamChatWithTools(String sessionId, String prompt, List<Function> baseTools) {
        //获取工具结果
        String toolResult = getToolResult(null,prompt, baseTools);
        LOG.info("工具调用结果为:{}",toolResult);
        String promptFormat = String.format("基于工具查询的结果:{%s}。请回答:%s?", toolResult, prompt);
        return streamChat(sessionId, promptFormat);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

到这里,我们就完成了像Qwen2这种没有原生支持Function_call的大模型的工具调用的功能了。

改进优化

在最初的版本中,我们是把普通问答和工具调用的问答分开设计的,这样的设计虽然能实现各种不同的功能,但是对于用户并不友好,“我怎么知道什么时候该使用工具模式呢?”。
在这里插入图片描述

因此,我们打算将普通问答模式和工具调用问答模式进行合并。这样,用户只需要专注于自己的问题即可,不用在纠结该选择哪个模式。

首先,我们定义一个返回空字符串的工具插件:

/**
     * 返回一个空字符串
     * @return 归属地
     */
    @FunctionDef(name = "getEmptyResult", description = "get a empty result")
    public static String getEmptyResult() {
        return "";
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

然后,也需要修改一下大模型选择工具插件的提示词,“如果用户提问内容与除了getEmptyResult之外的其他所有的工具都不相关,就返回getEmptyResult”:

public String getToolResult(String sessionId,String prompt, List<Function> baseTools){
		String class2Json = buildClass2Json(new BaseFunction());
        String finalPrompt = String.format("你是一个AI助手,我会给你一个工具对象集合,工具对象包括name(工具名)、description(工具描述)、clazz(工具类名)、parameters(工具参数)。" +
                        "你可以结合工具对象,从用户的问句中提取到关键词,确定要实现用户的任务应该选择哪个工具对象和工具的参数。" +
                        "【工具集合】:%s。" +
                        "【用户提问】:%s?" +
                        "如果用户提问内容与除了getEmptyResult之外的其他所有的工具都不相关,则你需要响应getEmptyResult工具即可。"+
                        "您的响应结果必须为JSON格式,并且不要返回任何不必要的解释,只提供遵循此格式的符合RFC8259的JSON响应。以下是输出必须遵守的JSON Schema实例:‍```%s‍```",
                JSON.toJSONString(baseTools),prompt,class2Json);
        String funcParams = chat(sessionId,finalPrompt);
        funcParams = JSON.parseObject(funcParams, OpenAIChatResponse.class).getChoices().get(0).getMessage().getContent();
        funcParams = funcParams.substring(funcParams.indexOf("{"), funcParams.lastIndexOf("}")+1);
        return LoadFunctions.load(JSON.parseObject(funcParams, BaseFunction.class));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这样,如果我如果输入一个问题,如地球的直径是多少。大模型识别这个问题与所有的工具插件都不相关,它就返回一个空字符串,也就是不用基于查询的知识进行回答。

public Flux<String> streamChatWithTools(String sessionId, String prompt, List<Function> baseTools) {
        //获取工具结果
        String toolResult = getToolResult(null,prompt, baseTools);
        LOG.info("工具调用结果为:{}",toolResult);
        String promptFormat = StringUtils.isEmpty(toolResult) ? String.format("请回答:%s?", prompt):String.format("基于工具查询的结果:{%s}。请回答:%s?", toolResult, prompt);
        return streamChat(sessionId, promptFormat);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这样,我们就实现了使用一个接口,同时处理用户的通识问答和需要进行工具调用的问答。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/815860
推荐阅读
相关标签
  

闽ICP备14008679号