LangChain4j实战七:函数调用
该函数的作用是调用接口盒子的HTTP服务获取实时天气,其中使用LangChain4j注解介绍如下:@Tool- LangChain4j 注解标记此方法为可被 AI 模型调用的工具函数注解内的字符串是工具的描述,帮助 AI 理解何时以及如何使用此工具这使得 AI 能够在需要获取天气信息时自动调用此方法@P- LangChain4j 参数注解为工具方法的参数提供描述帮助 AI 理解每个参数的含义和用途
·
1. 关于函数调用
- 可以按照以下六步来理解函数调用到底是什么:
- 开发者写一个本地方法A
- 第一次和LLM对话,把提示词和方法A的签名都给到LLM
- LLM返回信息中,会说明用什么参数去调用A
- 按照LLM返回的参数去调用A
- 把调用A的返回值返回给LLM
- 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注解介绍如下:
-
@Tool- LangChain4j 注解- 标记此方法为可被 AI 模型调用的工具函数
- 注解内的字符串是工具的描述,帮助 AI 理解何时以及如何使用此工具
- 这使得 AI 能够在需要获取天气信息时自动调用此方法
-
@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]";
}
}
-
上述代码的重点如下:
-
整个功能由getWeather方法实现,该方法会被controller中的接口实现调用,入参就是用户的提示词
-
请求LLM的办法是执行:openAiChatModel.chat
-
解析第一次LLM响应再根据响应调用本地函数,这些都被封装在executeTool方法中
-
UserMessage.from(prompt),也就是说第二次请求必须要带上提示词,否则难以得到理想结果 -
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());
}
}
更多推荐

所有评论(0)