1. 关于函数调用

  • 可以按照以下六步来理解函数调用到底是什么:
    1. 开发者写一个本地方法A
    2. 第一次和LLM对话,把提示词和方法A的签名都给到LLM
    3. LLM返回信息中,会说明用什么参数去调用A
    4. 按照LLM返回的参数去调用A
    5. 把调用A的返回值返回给LLM
    6. LLM根据这个返回值来生成最终的结果,并返回给用户
  • 如果是低级API方案,步骤2到5由开发者自己写代码实现,如果是高级API方案,步骤2到5由LangChain4j代为执行
  • 注意重点:用户最终拿到的不是本地方法的结果,而是LLM根据本地方法的结果生成的内容
    在这里插入图片描述

2. 准备工作

2.1. 获取实时天气数据

这里选择了接口盒子网站查询到天气信息,自行注册获取KEY

2.2. 构建项目,添加pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.0</version>
        <relativePath/>
    </parent>

    <groupId>cn.cjc</groupId>
    <artifactId>springboot-ai</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


        <!-- https://mvnrepository.com/artifact/dev.langchain4j/langchain4j -->
        <!--  LangChain4j 核心库 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
            <version>1.9.1</version>
        </dependency>
        <!-- AiService依赖 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-core</artifactId>
            <version>1.9.1</version>
        </dependency>
        <!-- LangChain4j OpenAI支持(可用于通义千问的OpenAI兼容接口) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
            <version>1.9.1-beta17</version>
        </dependency>
        <!--  导入响应式编程依赖包-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-reactor</artifactId>
            <version>1.9.1-beta17</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
    </dependencies>
</project>

2.3. 配置文件

spring:
  application:
    name: springboot-ai
  main:
    allow-bean-definition-overriding: true

langchain4j:
  open-ai:
    chat-model:
      api-key: ******
      model-name: qwen-plus
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1


# 调用接口盒子的接口查询天气,这里是地址
weather:
  tools:
    url: https://cn.apihz.cn/api/tianqi/tqyb.php?id=%s&key=%s&sheng=%s&place=%s
  # 调用接口盒子的接口查询天气,这里是ID,请改成您自己的注册ID
    id: ******
  # 调用接口盒子的接口查询天气,这里是通讯KEY,请改成您自己的通讯KEY
    key: ******

2.4. 提示词请求类

@Data
public class PromptRequest {
    private String prompt;
}

3. 接口盒子实体类

  • 接口盒子返回的json数据,要准备数据结构来保存,由于有嵌套对象,所以需要两个pojo,第一个是NowInfo,存的是实时数据,如温度湿度
@Data
public class WeatherInfo implements Serializable {
    private int code;
    private String guo;
    private String sheng;
    private String shi;
    private String name;
    private String weather1;
    private String weather2;
    private int wd1;
    private int wd2;
    private String winddirection1;
    private String winddirection2;
    private String windleve1;
    private String windleve2;
    private String weather1img;
    private String weather2img;
    private double lon;
    private double lat;
    private String uptime;
    private NowInfo nowinfo;
    private Object alarm;
}


@Data
public class NowInfo implements Serializable {
    private double precipitation;
    private double temperature;
    private int pressure;
    private int humidity;
    private String windDirection;
    private int windDirectionDegree;
    private int windSpeed;
    private String windScale;
    private double feelst;
    private String uptime;
}

4. 自定义函数

  • 该函数的作用是调用接口盒子的HTTP服务获取实时天气,其中使用LangChain4j注解介绍如下:
    1. @Tool - LangChain4j 注解

      • 标记此方法为可被 AI 模型调用的工具函数
      • 注解内的字符串是工具的描述,帮助 AI 理解何时以及如何使用此工具
      • 这使得 AI 能够在需要获取天气信息时自动调用此方法
    2. @P - LangChain4j 参数注解

      • 为工具方法的参数提供描述
      • 帮助 AI 理解每个参数的含义和用途
      • 例如 @P("应返回天气预报的省份") 告诉 AI 第一个参数是省份名称
import cn.cjc.ai.entity.WeatherInfo;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.client.RestTemplate;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Data
@Slf4j
public class WeatherTools {

    private String weatherToolsUrl;
    private String weatherToolsId;
    private String weatherToolsKey;


    @SuppressWarnings("null")
    @Tool("返回给定省份和城市的天气预报综合信息")
    public WeatherInfo getWeather(@P("应返回天气预报的省份") String province, @P("应返回天气预报的城市") String city) throws IllegalArgumentException {
        String encodedProvince = URLEncoder.encode(province, StandardCharsets.UTF_8);
        String encodedCity = URLEncoder.encode(city, StandardCharsets.UTF_8);
        String url = String.format(weatherToolsUrl, weatherToolsId, weatherToolsKey, encodedProvince, encodedCity);
        log.info("调用天气接口:{}", url);
        return new RestTemplate().getForObject(url, WeatherInfo.class);
    }
}

5. 配置类

  • 创建了天气服务类和LLM模型服务类的bean,要注意的是在创建模型服务类的时候设置了监听类ChatModelListener,这样服务和LLM的每一次请求响应详情都会打印到日志
@Configuration
@Slf4j
public class WeatherChatModelConfig {

    @Value("${langchain4j.open-ai.chat-model.api-key}")
    private String apiKey;

    @Value("${langchain4j.open-ai.chat-model.model-name:qwen-turbo}")
    private String modelName;

    @Value("${langchain4j.open-ai.chat-model.base-url}")
    private String baseUrl;

    /**
     * 创建并配置OpenAiChatModel实例(使用通义千问的OpenAI兼容接口)
     *
     * @return OpenAiChatModel实例
     */
    @Bean("weatherOpenAiChatModel")
    public OpenAiChatModel weatherOpenAiChatModel() {
        ChatModelListener listener = new ChatModelListener() {
            @Override
            public void onRequest(ChatModelRequestContext reqCtx) {
                // 1. 拿到 List<ChatMessage>
                List<ChatMessage> messages = reqCtx.chatRequest().messages();
                log.info("发到LLM的请求: {}", messages);
            }

            @Override
            public void onResponse(ChatModelResponseContext respCtx) {
                // 2. 先取 ChatModelResponse
                ChatResponse response = respCtx.chatResponse();
                // 3. 再取 AiMessage
                AiMessage aiMessage = response.aiMessage();

                // 4. 工具调用
                List<ToolExecutionRequest> tools = aiMessage.toolExecutionRequests();
                for (ToolExecutionRequest t : tools) {
                    log.info("LLM响应, 执行函数[{}], 函数入参 : {}", t.name(), t.arguments());
                }

                // 5. 纯文本
                if (aiMessage.text() != null) {
                    log.info("LLM响应, 纯文本 : {}", aiMessage.text());
                }
            }

            @Override
            public void onError(ChatModelErrorContext errorCtx) {
                errorCtx.error().printStackTrace();
            }
        };

        return OpenAiChatModel.builder()
                .apiKey(apiKey)
                .modelName(modelName)
                .baseUrl(baseUrl)
                .listeners(List.of(listener))
                .build();
    }

    @Value("${weather.tools.url}")
    private String weatherToolsUrl;

    @Value("${weather.tools.id}")
    private String weatherToolsId;

    @Value("${weather.tools.key}")
    private String weatherToolsKey;

    @Bean
    public WeatherTools weatherTools() {
        WeatherTools tools = new WeatherTools();
        tools.setWeatherToolsUrl(weatherToolsUrl);
        tools.setWeatherToolsId(weatherToolsId);
        tools.setWeatherToolsKey(weatherToolsKey);
        return tools;
    }
}

6. 实现类

6.1. 低级API版本

  • 需自行实现和LLM的两次通信,并且在第一次收到响应时,要根据LLM给定的参数去调用查询天气的服务类
package cn.cjc.ai.service.impl;

import cn.cjc.ai.config.WeatherTools;
import cn.cjc.ai.entity.WeatherInfo;
import cn.cjc.ai.service.Weather;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.agent.tool.ToolSpecifications;
import dev.langchain4j.data.message.ToolExecutionResultMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.openai.OpenAiChatModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service
public class WeatherService {

    @Autowired
    private OpenAiChatModel weatherOpenAiChatModel;

    @Autowired
    private WeatherTools weatherTools;

    private List<ToolSpecification> prepareToolSpecifications() {
        return ToolSpecifications.toolSpecificationsFrom(WeatherTools.class);
    }

    /**
     * 处理工具执行请求
     */
    private String executeTool(ToolExecutionRequest request) {
        try {
            if ("getWeather".equals(request.name())) {
                String arguments = request.arguments();
                log.info("执行工具调用:getWeather,参数:{}", arguments);

                // 简单解析 JSON 参数
                String province = null;
                String city = null;

                // 检查参数格式
                if (arguments.contains("arg0") && arguments.contains("arg1")) {
                    // 格式:{"arg0": "广东", "arg1": "深圳"}
                    province = extractValue(arguments, "arg0");
                    city = extractValue(arguments, "arg1");
                } else if (arguments.contains("province") && arguments.contains("city")) {
                    // 格式:{"province": "广东", "city": "深圳"}
                    province = extractValue(arguments, "province");
                    city = extractValue(arguments, "city");
                }

                log.info("解析后的参数:province={}, city={}", province, city);

                if (province == null || city == null) {
                    throw new IllegalArgumentException("无法解析参数:" + arguments);
                }

                WeatherInfo weatherInfo = weatherTools.getWeather(province, city);
                return weatherInfo.toString();
            } else {
                return "Unknown tool: " + request.name();
            }
        } catch (Exception e) {
            log.error("工具执行失败", e);
            return "Tool execution failed: " + e.getMessage();
        }
    }

    /**
     * 从 JSON 字符串中提取值
     */
    private String extractValue(String json, String key) {
        int start = json.indexOf('"' + key + '"');
        if (start == -1)
            return null;

        int colon = json.indexOf(':', start);
        int valueStart = json.indexOf('"', colon);
        int valueEnd = json.indexOf('"', valueStart + 1);

        return valueStart != -1 && valueEnd != -1 ? json.substring(valueStart + 1, valueEnd) : null;
    }

    /**
     * 通过提示词range大模型返回JSON格式的内容
     */
    public String getLowWeather(String prompt) {
        List<ToolSpecification> toolSpecifications = prepareToolSpecifications();

        ChatRequest req = ChatRequest.builder()
                .messages(UserMessage.from(prompt))
                .toolSpecifications(toolSpecifications)
                .build();

        ChatResponse resp = weatherOpenAiChatModel.chat(req);

        log.info("初始响应:" + resp);

        // 检查是否需要执行工具调用
        if (resp.aiMessage().toolExecutionRequests() != null && !resp.aiMessage().toolExecutionRequests().isEmpty()) {
            log.info("需要执行工具调用");

            // 执行所有工具调用
            for (ToolExecutionRequest toolRequest : resp.aiMessage().toolExecutionRequests()) {
                String toolResult = executeTool(toolRequest);

                log.info("工具执行结果:" + toolResult);

                // 将工具执行结果发送回模型
                ChatRequest toolResultRequest = ChatRequest.builder()
                        .messages(
                                UserMessage.from(prompt),
                                resp.aiMessage(),
                                ToolExecutionResultMessage.from(toolRequest, "工具执行结果:" + toolResult))
                        .toolSpecifications(toolSpecifications)
                        .build();

                resp = weatherOpenAiChatModel.chat(toolResultRequest);
                log.info("工具执行后的响应:" + resp);
            }
        }

        return resp.aiMessage().text() + "[from low level getWeather]";
    }
}
  • 上述代码的重点如下:

    1. 整个功能由getWeather方法实现,该方法会被controller中的接口实现调用,入参就是用户的提示词

    2. 请求LLM的办法是执行:openAiChatModel.chat

    3. 解析第一次LLM响应再根据响应调用本地函数,这些都被封装在executeTool方法中

    4. UserMessage.from(prompt),也就是说第二次请求必须要带上提示词,否则难以得到理想结果

    5. executeTool方法中还有个细节,就是LLM返回的参数信息,其参数名可能不是咱们函数的入参名称,例如这里就是arg0和arg1,所以不能只用province和city去解析

6.2. 高级API版本

  • 自定义获取天气接口,该接口的实现就是和LLM的两次交互以及函数调用,这些都是LangChain4j在背后实现的
  public interface WeatherAssistant {
  
      /**
       * 通过提示词查询最新的天气情况
       * @param userMessage 用户消息
       * @return 助手生成的回答
       */
      String getHighWeather(String userMessage);
  }
  • 配置类
    • 和前文基本一致,但是需要新增WeatherAssistant实例,创建必须执行tools方法,这样LangChain4j才知道有哪些函数可用
  @Configuration
  @Slf4j
  public class WeatherChatModelConfig {
  
    @Bean
    public WeatherAssistant weatherAssistant(@Qualifier("weatherOpenAiChatModel") OpenAiChatModel chatModel, WeatherTools weatherTools) {
        return AiServices.builder(WeatherAssistant.class)
                .chatModel(chatModel)
                .tools(weatherTools)
                .build();
    }
  }
  • 服务类,只需要调用bean的方法
  @Slf4j
  @Service
  public class WeatherService {
  
      @Autowired
      private WeatherAssistant weatherAssistant;
  
      public String getHighWeather(String prompt) {
          return weatherAssistant.getHighWeather(prompt)+ "[from high level getWeather]";
      }
  }

7. controller

  @RestController
  @RequestMapping("/api/weather")
  public class WeatherController {
  
      @Autowired
      private WeatherService weatherService;
  
      @PostMapping("/getLowWeather")
      public ResponseEntity<String> getLowWeather(@RequestBody PromptRequest request) {
          Object response = weatherService.getLowWeather(request.getPrompt());
          return ResponseEntity.ok(response.toString());
      }
  
      @PostMapping("/getHighWeather")
      public ResponseEntity<String> getHighWeather(@RequestBody PromptRequest request) {
          Object response = weatherService.getHighWeather(request.getPrompt());
          return ResponseEntity.ok(response.toString());
      }
  }
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐