1、在本示例中,我们仅为 Agent 绑定了一个天气查询服务,接收到用户的天气查询服务后,流程会在 AgentNode 和 ToolNode 之间循环执行,直到完成用户指令。示例中判断指令完成的条件(即 ReAct 结束条件)也很简单,模型 AssistantMessage 无 tool_call 指令则结束(采用默认行为)。
2、pom文件
<properties>
<gson.version>2.10.1</gson.version>
</properties>
<dependencies>
<dependency>
<groupId>net.sourceforge.plantuml</groupId>
<artifactId>plantuml-mit</artifactId>
<version>1.2024.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.alibaba.cloud.ai</groupId>-->
<!-- <artifactId>spring-ai-alibaba-starter</artifactId>-->
<!-- <version>${project.parent.version}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-graph-core</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-studio</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>document-parser-tika</artifactId>
<version>1.0.0-M6.1</version>
</dependency>
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>5.8.20</version>
<scope>compile</scope>
</dependency>
<!-- HttpClient 核心库 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.3</version>
</dependency>
<!-- 如果需要使用 HttpClient 连接池管理 -->
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>5.2.3</version>
</dependency>
<!-- This dependency automatically matches the appropriate version of Chrome Driver, -->
<!-- causing it to be slower on first boot -->
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.7.0</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.25.0</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-chrome-driver</artifactId>
<version>4.25.0</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-api</artifactId>
<version>4.25.0</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-remote-driver</artifactId>
<version>4.25.0</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-manager</artifactId>
<version>4.25.0</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-http</artifactId>
<version>4.25.0</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
</dependencies>
3、配置文件
#
# Copyright 2024-2025 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
server:
port: 18080
spring:
application:
name: spring-ai-alibaba-helloworld
ai:
alibaba:
toolcalling:
weather:
enabled: true
api-key: aaaaa
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: sk-xxxxoooooyyyyyyxxxxooooo
chat:
options:
model: qwen-max-latest
4、天气接口 tool
/*
* Copyright 2024-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.example.graph.react.tool.weather.function;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
/**
* @author yingzi
* @since 2025/3/27:11:07
*/
public class FreeWeatherService implements Function<FreeWeatherService.Request, FreeWeatherService.Response> {
private static final Logger logger = LoggerFactory.getLogger(FreeWeatherService.class);
private static final String WEATHER_API_URL = "http://t.weather.sojson.com/api/weather/city/";
private final WebClient webClient;
private final ObjectMapper objectMapper = new ObjectMapper();
private final static Map<String, String> CITY_NAME_CODE_MAP = Map.of("杭州", "101210101",
"上海", "101020100", "南京", "101190101", "合肥", "101220101");
public FreeWeatherService() {
this.webClient = WebClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.build();
}
public static Response fromJson(Map<String, Object> json) {
Map<String, Object> location = (Map<String, Object>) json.get("cityInfo");
Map<String, Object> data = (Map<String, Object>) json.get("data");
List<Map<String, Object>> forecastDays = (List<Map<String, Object>>) data.get("forecast");
Map<String, Object> current = forecastDays.get(0);
String city = (String) location.get("city");
return new Response(city, current, forecastDays.subList(1, forecastDays.size()));
}
@Override
public Response apply(Request request) {
if (request == null || !StringUtils.hasText(request.city())) {
logger.error("Invalid request: city is required.");
return null;
}
try {
return doGetWeatherMock(request);
} catch (Exception e) {
logger.error("Failed to fetch weather data: {}", e.getMessage());
return null;
}
}
@NotNull
private Response doGetWeatherMock(Request request) throws JsonProcessingException {
return doGetWeather(WEATHER_API_URL, request);
}
@NotNull
private Response doGetWeather(String url, Request request) throws JsonProcessingException {
String city = request.city();
String cityCode = CITY_NAME_CODE_MAP.get(city);
if (org.apache.commons.lang3.StringUtils.isBlank(cityCode)) {
return null;
}
Mono<String> responseMono = webClient.get().uri(url + cityCode).retrieve().bodyToMono(String.class);
String jsonResponse = responseMono.block();
assert jsonResponse != null;
Response response = fromJson(objectMapper.readValue(jsonResponse, new TypeReference<Map<String, Object>>() {
}));
logger.info("Weather data fetched successfully for city: {}", response.city());
return response;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonClassDescription("Weather Service API request")
public record Request(
@JsonProperty(required = true, value = "city") @JsonPropertyDescription("city name") String city,
@JsonProperty(required = true,
value = "days") @JsonPropertyDescription("Number of days of weather forecast. Value ranges from 1 to 14") int days) {
}
@JsonClassDescription("Weather Service API response")
public record Response(
@JsonProperty(required = true, value = "city") @JsonPropertyDescription("city name") String city,
@JsonProperty(required = true,
value = "current") @JsonPropertyDescription("Current weather info") Map<String, Object> current,
@JsonProperty(required = true,
value = "forecastDays") @JsonPropertyDescription("Forecast weather info") List<Map<String, Object>> forecastDays) {
}
}
/*
* Copyright 2024-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.example.graph.react.tool.weather.function;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
@Configuration
@ConditionalOnClass(FreeWeatherService.class)
@ConditionalOnProperty(prefix = "spring.ai.alibaba.toolcalling.weather", name = "enabled", havingValue = "true")
public class FreeWeatherAutoConfiguration {
@Bean(name = "getWeatherFunction")
@ConditionalOnMissingBean
@Description("Use api.weather to get weather information.")
public FreeWeatherService getWeatherFunction() {
return new FreeWeatherService();
}
}
5、核心代码
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.example.graph.react;
import java.util.concurrent.TimeUnit;
import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.alibaba.cloud.ai.graph.GraphRepresentation;
import com.alibaba.cloud.ai.graph.GraphStateException;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.util.Timeout;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
@Configuration
public class ReactAutoconfiguration {
@Bean
public ReactAgent normalReactAgent(ChatModel chatModel, ToolCallbackResolver resolver) throws GraphStateException {
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools("getWeatherFunction")
.defaultAdvisors(new SimpleLoggerAdvisor())
.defaultOptions(OpenAiChatOptions.builder().internalToolExecutionEnabled(false).build())
.build();
return ReactAgent.builder()
.name("React Agent Demo")
.chatClient(chatClient)
.resolver(resolver)
.maxIterations(10)
.build();
}
@Bean
public CompiledGraph reactAgentGraph(@Qualifier("normalReactAgent") ReactAgent reactAgent)
throws GraphStateException {
GraphRepresentation graphRepresentation = reactAgent.getStateGraph()
.getGraph(GraphRepresentation.Type.PLANTUML);
System.out.println("\n\n");
System.out.println(graphRepresentation.content());
System.out.println("\n\n");
return reactAgent.getAndCompileGraph();
}
@Bean
public RestClient.Builder createRestClient() {
// 2. 创建 RequestConfig 并设置超时
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.of(10, TimeUnit.MINUTES)) // 设置连接超时
.setResponseTimeout(Timeout.of(10, TimeUnit.MINUTES))
.setConnectionRequestTimeout(Timeout.of(10, TimeUnit.MINUTES))
.build();
// 3. 创建 CloseableHttpClient 并应用配置
HttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
// 4. 使用 HttpComponentsClientHttpRequestFactory 包装 HttpClient
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
// 5. 创建 RestClient 并设置请求工厂
return RestClient.builder().requestFactory(requestFactory);
}
}
/*
* Copyright 2024-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.example.graph.react;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.alibaba.cloud.ai.graph.OverAllState;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.messaging.Message;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/react")
public class ReactController {
private final CompiledGraph compiledGraph;
ReactController(@Qualifier("reactAgentGraph") CompiledGraph compiledGraph) {
this.compiledGraph = compiledGraph;
}
@GetMapping("/chat")
public String simpleChat(String query) {
Optional<OverAllState> result = compiledGraph.invoke(Map.of("messages", new UserMessage(query)));
List<Message> messages = (List<Message>) result.get().value("messages").get();
AssistantMessage assistantMessage = (AssistantMessage) messages.get(messages.size() - 1);
return assistantMessage.getText();
}
}
测试地址
测试结果如下