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: 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();
	}

}

测试地址 

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

测试结果如下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

非ban必选

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值