Spring AI Alibaba Graph基于 ReAct Agent 的天气预报查询系统
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: 18080spring:application:name: spring-ai-alibaba-helloworldai:alibaba:toolcalling:weather:enabled: trueapi-key: aaaaaopenai:base-url: https://dashscope.aliyuncs.com/compatible-modeapi-key: sk-xxxxoooooyyyyyyxxxxooooochat: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()));}@Overridepublic 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;}}@NotNullprivate Response doGetWeatherMock(Request request) throws JsonProcessingException {return doGetWeather(WEATHER_API_URL, request);}@NotNullprivate 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 {@Beanpublic 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();}@Beanpublic 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();}@Beanpublic 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 包装 HttpClientHttpComponentsClientHttpRequestFactory 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();}}
测试地址
http://localhost:18080/react/chat?query=%E5%88%86%E5%88%AB%E5%B8%AE%E6%88%91%E6%9F%A5%E8%AF%A2%E6%9D%AD%E5%B7%9E%E3%80%81%E4%B8%8A%E6%B5%B7%E5%92%8C%E5%8D%97%E4%BA%AC%E7%9A%84%E5%A4%A9%E6%B0%94
测试结果如下