
本案例将引导您一步步构建一个 Spring Boot 应用,演示如何利用 Spring AI Alibaba 的 React Agent 功能,实现基于 Graph 的智能问答,并集成天气查询工具。
1. 案例目标
我们将创建一个包含核心功能的 Web 应用:
- React Agent 问答 (
/react/chat):通过 Graph 架构,让 AI 能够自主决定是否需要调用工具来回答用户的问题,实现更智能的交互体验。 - 天气查询工具集成:集成天气查询功能,当用户询问天气相关问题时,AI 能够自动调用天气工具获取实时数据。
2. 技术栈与核心依赖
- Spring Boot 3.x
- Spring AI Alibaba (用于对接阿里云 DashScope 通义大模型)
- Spring AI Alibaba Graph Core (用于构建 Graph 架构的 Agent)
- Maven (项目构建工具)
在 pom.xml 中,你需要引入以下核心依赖:
<dependencies>
<!-- Spring AI Alibaba 核心启动器,集成 DashScope -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<!-- Spring AI Chat Client 自动配置 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
</dependency>
<!-- Spring AI Alibaba Graph Core -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-graph-core</artifactId>
<version>1.0.0.3</version>
</dependency>
<!-- Spring Web 用于构建 RESTful API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.4.1</version>
</dependency>
<!-- OpenAI 兼容模式 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
</dependency>
<!-- Hutool 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>5.8.38</version>
</dependency>
</dependencies>
3. 项目配置
在 src/main/resources/application.yml 文件中,配置你的 DashScope API Key 和天气 API Key。
server:
port: 8080
spring:
application:
name: react
ai:
alibaba:
toolcalling:
weather:
enabled: true
api-key: ${WEATHER_API_KEY}
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY}
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: ${AI_DASHSCOPE_API_KEY}
chat:
options:
model: qwen-max-latest
embedding:
options:
model: text-embedding-v1
重要提示 :请将 AI_DASHSCOPE_API_KEY 和 WEATHER_API_KEY 环境变量设置为你从阿里云获取的有效 API Key。你也可以直接将其写在配置文件中,但这不推荐用于生产环境。
4. 项目结构
React Agent 项目的结构如下:
src/main/java/com/alibaba/cloud/ai/graph/react/
├── ReactApplication.java # 应用程序主类
├── ReactAutoconfiguration.java # React Agent 自动配置
├── ReactController.java # REST API 控制器
└── function/ # 工具函数目录
├── WeatherAutoConfiguration.java # 天气服务自动配置
├── WeatherProperties.java # 天气服务配置属性
├── WeatherService.java # 天气服务实现
└── WeatherUtils.java # 天气工具类
5. 核心组件实现
5.1 应用程序主类 ReactApplication.java
package com.alibaba.cloud.ai.graph.react;
import com.alibaba.cloud.ai.graph.react.function.WeatherProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableConfigurationProperties(WeatherProperties.class)
public class ReactApplication {
public static void main(String[] args) {
SpringApplication.run(ReactApplication.class, args);
}
}
5.2 React Agent 自动配置 ReactAutoconfiguration.java
package com.alibaba.cloud.ai.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.exception.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)
.defaultToolNames("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() {
// 配置 HTTP 客户端超时
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.of(10, TimeUnit.MINUTES))
.setResponseTimeout(Timeout.of(10, TimeUnit.MINUTES))
.setConnectionRequestTimeout(Timeout.of(10, TimeUnit.MINUTES))
.build();
HttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
return RestClient.builder().requestFactory(requestFactory);
}
}
5.3 REST API 控制器 ReactController.java
package com.alibaba.cloud.ai.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 com.alibaba.cloud.ai.graph.exception.GraphRunnerException;
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) throws GraphRunnerException {
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();
}
}
5.4 天气服务实现
5.4.1 天气服务配置属性 WeatherProperties.java
package com.alibaba.cloud.ai.graph.react.function;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.ai.alibaba.toolcalling.weather")
public class WeatherProperties {
private String apiKey;
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
}
5.4.2 天气服务实现类 WeatherService.java
package com.alibaba.cloud.ai.graph.react.function;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.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 reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
public class WeatherService implements Function<WeatherService.Request, WeatherService.Response> {
private static final Logger logger = LoggerFactory.getLogger(WeatherService.class);
private static final String WEATHER_API_URL = "https://api.weatherapi.com/v1/forecast.json";
private final WebClient webClient;
private final ObjectMapper objectMapper = new ObjectMapper();
public WeatherService(WeatherProperties properties) {
this.webClient = WebClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.defaultHeader("key", properties.getApiKey())
.build();
}
public static Response fromJson(Map<String, Object> json) {
Map<String, Object> location = (Map<String, Object>) json.get("location");
Map<String, Object> current = (Map<String, Object>) json.get("current");
Map<String, Object> forecast = (Map<String, Object>) json.get("forecast");
List<Map<String, Object>> forecastDays = (List<Map<String, Object>>) forecast.get("forecastday");
String city = (String) location.get("name");
return new Response(city, current, forecastDays);
}
@Override
public Response apply(Request request) {
if (request == null || !StringUtils.hasText(request.city())) {
logger.error("Invalid request: city is required.");
return null;
}
String location = WeatherUtils.preprocessLocation(request.city());
String url = UriComponentsBuilder.fromHttpUrl(WEATHER_API_URL)
.queryParam("q", location)
.queryParam("days", request.days())
.toUriString();
logger.info("url : {}", url);
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 {
if (Objects.equals("杭州", request.city())) {
return new Response(request.city(), Map.of("temp", 25, "condition", "Sunny"),
List.of(Map.of("date", "2025-05-27", "high", 28, "low", 20)));
}
else if (Objects.equals("上海", request.city())) {
return new Response(request.city(), Map.of("temp", 26, "condition", "Sunny"),
List.of(Map.of("date", "2025-05-27", "high", 29, "low", 21)));
}
else if (Objects.equals("南京", request.city())) {
return new Response(request.city(), Map.of("temp", 18, "condition", "cloudy"),
List.of(Map.of("date", "2025-05-27", "high", 18, "low", 10)));
}
else {
return new Response(request.city(), Map.of("temp", -20, "condition", "Snowy"),
List.of(Map.of("date", "2025-05-27", "high", -10, "low", -30)));
}
}
@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) {
}
}
5.4.3 天气工具类 WeatherUtils.java
package com.alibaba.cloud.ai.graph.react.function;
import cn.hutool.extra.pinyin.PinyinUtil;
public class WeatherUtils {
public static String preprocessLocation(String location) {
if (containsChinese(location)) {
return PinyinUtil.getPinyin(location, "");
}
return location;
}
public static boolean containsChinese(String str) {
return str.matches(".*[\u4e00-\u9fa5].*");
}
}
5.4.4 天气服务自动配置 WeatherAutoConfiguration.java
package com.alibaba.cloud.ai.graph.react.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(WeatherService.class)
@ConditionalOnProperty(prefix = "spring.ai.alibaba.toolcalling.weather", name = "enabled", havingValue = "true")
public class WeatherAutoConfiguration {
@Bean(name = "getWeatherFunction")
@ConditionalOnMissingBean
@Description("Use api.weather to get weather information.")
public WeatherService getWeatherServiceFunction(WeatherProperties properties) {
return new WeatherService(properties);
}
}
6. 运行与测试
- 启动应用:运行你的 Spring Boot 主程序。
- 使用浏览器或 API 工具(如 Postman, curl)进行测试。
测试 1:普通问答
访问以下 URL,询问一个不需要调用工具的问题。
http://localhost:8080/react/chat?query=你好,请介绍一下你自己
预期响应:
你好!我是一个基于 React Agent 架构的 AI 助手,可以帮助你回答各种问题。我还可以使用工具来获取实时信息,比如天气数据。有什么我可以帮助你的吗?
测试 2:天气查询
访问以下 URL,询问天气相关问题。
http://localhost:8080/react/chat?query=杭州今天的天气怎么样?
预期响应:
根据查询结果,杭州今天的天气情况是:温度25摄氏度,天气晴朗(Sunny)。今天最高温度28度,最低温度20度。
测试 3:其他城市天气查询
访问以下 URL,查询其他城市的天气。
http://localhost:8080/react/chat?query=南京今天天气如何?
预期响应:
根据查询结果,南京今天的天气情况是:温度18摄氏度,天气多云(cloudy)。今天最高温度18度,最低温度10度。
7. 实现思路与扩展建议
实现思路
本案例的核心思想是**"基于 Graph 的 Agent 架构"**。通过 React Agent,我们实现了:
- 自主决策:AI 能够自主决定是否需要调用工具来回答用户的问题,无需预先定义调用规则。
- 工具集成:通过 Spring AI 的工具调用机制,轻松集成外部 API,如天气查询服务。
- 状态管理:通过 Graph 架构,管理 Agent 的状态和决策流程,使整个交互过程更加可控和可观测。
扩展建议
- 集成更多工具:可以集成更多外部 API,如股票查询、新闻获取、地图服务等,扩展 Agent 的能力范围。
- 多轮对话优化:通过添加记忆机制,使 Agent 能够记住之前的对话内容,提供更连贯的交互体验。
- 错误处理增强:增强工具调用的错误处理机制,当工具调用失败时,提供友好的错误提示和替代方案。
- 可视化监控:集成可视化工具,展示 Agent 的决策过程和工具调用情况,便于调试和优化。
- 多 Agent 协作:构建多个专业 Agent,每个 Agent 专注于特定领域,通过协作机制解决复杂问题。