【第46篇】Graph - react Agent
第一部分:概述
一、 ReAct
ReAct 是 Reasoning(推理)和 Acting(行动)的缩写,是一种让大语言模型(LLM)像人类一样"边想边做"的架构模式。
1.1 为什么需要 ReAct?
想象你问朋友"明天去杭州西湖穿什么衣服合适?",他不会直接瞎猜,而是会:
- 思考(Reasoning):“需要知道杭州明天的天气”
- 行动(Acting):打开手机查天气预报
- 观察(Observation):看到明天杭州 15-22°C,小雨
- 再思考:“气温适中但有雨,需要带伞,穿薄外套”
- 回答:“建议穿薄外套,记得带伞”
ReAct 就是让 AI 复现这个**“思考 → 行动 → 观察 → 再思考”**的循环过程,直到得出最终答案。
1.2 ReAct 的核心循环
关键洞察:ReAct 不是一次性生成答案,而是通过多轮交互让 LLM 逐步逼近正确答案。每轮 LLM 都会输出一个"想法"(Thought),决定是继续调用工具还是给出最终回答。
二、项目定位与能力
本项目是 Spring AI Alibaba Graph 框架的官方示例,演示如何构建一个具备工具调用能力的 AI 智能体。
2.1 它能做什么?
| 场景 | 示例 | AI 行为 |
|---|---|---|
| 天气查询 | “杭州天气怎么样?” | 调用天气工具 → 获取数据 → 组织回答 |
| 多城市对比 | “北京和上海哪个更热?” | 分别查询两地 → 对比温度 → 给出结论 |
| 闲聊 | “你好呀” | 直接友好回复,不调用工具 |
2.2 技术栈全景
| 层级 | 组件 | 作用说明 |
|---|---|---|
| 应用层 | Spring Boot 3.x | 提供 Web 容器和依赖注入 |
| AI 框架 | Spring AI Alibaba | 统一封装 LLM 调用、工具注册、提示词管理 |
| 大模型 | Qwen-Max | 负责推理决策和文本生成 |
| 图编排 | spring-ai-alibaba-graph-core | 用"图"的方式编排 ReAct 循环逻辑 |
| 工具层 | WeatherService | 封装天气查询能力,供 AI 调用 |
三、核心组件深度解析
3.1 启动入口:ReactApplication
@SpringBootApplication
@EnableConfigurationProperties(WeatherProperties.class)
public class ReactApplication {
public static void main(String[] args) {
SpringApplication.run(ReactApplication.class, args);
}
}
作用拆解:
@SpringBootApplication:开启自动配置、组件扫描@EnableConfigurationProperties:将application.yml中以spring.ai.alibaba.toolcalling.weather开头的配置自动绑定到WeatherProperties对象
为什么需要配置绑定? 因为天气服务的 API Key 需要从外部注入,而不是硬编码在代码里。
3.2 核心配置:ReactAutoconfiguration
这是整个项目的"心脏",负责把各个零件组装成能工作的 ReAct Agent。
@Configuration
public class ReactAutoconfiguration {
@Bean
public ReactAgent normalReactAgent(ChatModel chatModel, ToolCallbackResolver resolver) {
// 步骤 1:构建 ChatClient(LLM 调用客户端)
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultToolNames("getWeatherFunction") // 注册可用工具
.defaultAdvisors(new SimpleLoggerAdvisor()) // 添加日志切面
.defaultOptions(OpenAiChatOptions.builder()
.internalToolExecutionEnabled(false) // ⚠️ 关键:关闭内部工具执行
.build())
.build();
// 步骤 2:构建 ReactAgent(ReAct 逻辑封装)
return ReactAgent.builder()
.name("React Agent Demo")
.chatClient(chatClient)
.resolver(resolver) // 工具解析器:根据名称找到具体工具
.build();
}
@Bean
public CompiledGraph reactAgentGraph(@Qualifier("normalReactAgent") ReactAgent reactAgent) {
// 步骤 3:编译图(将 ReAct 逻辑编译成可执行的状态机)
CompiledGraph compiledGraph = reactAgent.getAndCompileGraph();
// 步骤 4:打印 PlantUML 图(调试用,可视化图结构)
GraphRepresentation graphRepresentation = compiledGraph.getGraph(GraphRepresentation.Type.PLANTUML);
System.out.println(graphRepresentation.content());
return compiledGraph;
}
}
关键配置解析:
| 配置项 | 值 | 原因 |
|---|---|---|
internalToolExecutionEnabled(false) |
关闭 | 如果开启,Spring AI 会在内部自动执行工具调用,但我们希望由 Graph 框架来编排整个 ReAct 循环,所以需要关闭 |
defaultToolNames("getWeatherFunction") |
工具名 | 告诉 LLM 有哪些工具可用,LLM 会根据用户问题决定是否调用 |
SimpleLoggerAdvisor |
日志切面 | 记录每次 LLM 调用的输入输出,方便调试 |
⚠️ 潜在问题 1:超时配置不合理
// 原代码(问题)
RequestConfig.custom()
.setConnectTimeout(Timeout.of(10, TimeUnit.MINUTES)) // ❌ 10分钟太长
.setResponseTimeout(Timeout.of(10, TimeUnit.MINUTES))
问题分析:天气查询是轻量级调用,10 分钟超时会导致故障时长时间挂起,浪费资源。
修复建议:
RequestConfig.custom()
.setConnectTimeout(Timeout.of(30, TimeUnit.SECONDS)) // 连接超时 30 秒
.setResponseTimeout(Timeout.of(60, TimeUnit.SECONDS)) // 响应超时 60 秒
.build();
3.3 HTTP 接口:ReactController
@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) {
// 1. 构建用户消息
Optional<OverAllState> result = compiledGraph.invoke(
Map.of("messages", new UserMessage(query))
);
// 2. 提取 AI 回复(⚠️ 有风险,见下方)
List<Message> messages = (List<Message>) result.get().value("messages").get();
AssistantMessage assistantMessage = (AssistantMessage) messages.get(messages.size() - 1);
return assistantMessage.getText();
}
}
⚠️ 潜在问题 2:空指针风险
// 原代码(问题)
List<Message> messages = (List<Message>) result.get().value("messages").get();
// 如果 result 是 empty,.get() 抛出 NoSuchElementException
修复建议:
@GetMapping("/chat")
public String simpleChat(String query) {
// 参数校验
if (query == null || query.trim().isEmpty()) {
return "请提供查询内容";
}
// 调用 ReAct 图
Optional<OverAllState> result = compiledGraph.invoke(
Map.of("messages", new UserMessage(query))
);
// 安全取值
OverAllState state = result.orElseThrow(() ->
new RuntimeException("ReAct 图执行失败,未返回结果"));
List<Message> messages = (List<Message>) state.value("messages")
.orElse(Collections.emptyList());
if (messages.isEmpty()) {
return "未获取到有效回复";
}
AssistantMessage assistantMessage = (AssistantMessage) messages.get(messages.size() - 1);
return assistantMessage.getText();
}
⚠️ 潜在问题 3:GET 方法限制
// 原代码:GET 请求,参数通过 URL 传递
@GetMapping("/chat")
public String simpleChat(String query) // query 暴露在 URL 中
问题:URL 长度有限制(通常 2KB-8KB),且中文需要编码,不适合长文本。
修复建议:
@PostMapping("/chat")
public String simpleChat(@RequestBody ChatRequest request) {
// 使用 POST + JSON Body,支持更复杂的内容
}
public record ChatRequest(String query) {}
四、天气工具组件
4.1 WeatherService:AI 的"手"
WeatherService 实现了 Java 的 Function 接口,这是 Spring AI 定义工具的标准方式。
public class WeatherService implements Function<WeatherService.Request, WeatherService.Response> {
// 输入参数:LLM 调用时会自动填充
public record Request(
@JsonProperty(required = true, value = "city")
@JsonPropertyDescription("城市名称,如:杭州、Beijing")
String city,
@JsonProperty(required = true, value = "days")
@JsonPropertyDescription("预报天数,范围 1-14 天")
int days
) {}
// 输出参数:工具执行后返回给 LLM
public record Response(
@JsonProperty(required = true, value = "city") String city,
@JsonProperty(required = true, value = "current") Map<String, Object> current,
@JsonProperty(required = true, value = "forecastDays") List<Map<String, Object>> forecastDays
) {}
@Override
public Response apply(Request request) {
// 实际调用逻辑...
}
}
注解解析:
| 注解 | 作用 | 给谁看 |
|---|---|---|
@JsonProperty |
定义 JSON 字段名和是否必填 | 序列化框架 |
@JsonPropertyDescription |
描述字段含义 | LLM(这是关键!LLM 根据这个描述理解参数怎么用) |
为什么需要描述? LLM 看到工具定义时,并不知道 “city” 是城市名,“days” 是天数。@JsonPropertyDescription 就是给 LLM 看的"说明书"。
4.2 自动配置:WeatherAutoConfiguration
@Configuration
@ConditionalOnClass(WeatherService.class) // 只有当 WeatherService 类存在时才生效
@ConditionalOnProperty( // 只有当配置中 enabled=true 时才生效
prefix = "spring.ai.alibaba.toolcalling.weather",
name = "enabled",
havingValue = "true"
)
public class WeatherAutoConfiguration {
@Bean(name = "getWeatherFunction") // ⚠️ 名称必须与 Agent 注册的一致
@ConditionalOnMissingBean // 只有当用户没有自定义时才创建
@Description("查询指定城市的天气信息,支持未来 1-14 天预报")
public WeatherService getWeatherServiceFunction(WeatherProperties properties) {
return new WeatherService(properties);
}
}
⚠️ 潜在问题 4:Mock 数据陷阱
// 当前代码实际走的是这个方法(假数据)
private Response doGetWeatherMock(Request request) {
if (Objects.equals("杭州", request.city())) {
return new Response(request.city(),
Map.of("temp", 25, "condition", "Sunny"),
List.of(...));
}
// ... 其他城市也是硬编码
}
问题:虽然代码里有 doGetWeather() 方法(真实 API 调用),但实际执行的是 doGetWeatherMock()。这意味着无论问哪个城市,返回的都是预设的假数据。
修复建议(带降级策略):
@Override
public Response apply(Request request) {
try {
// 优先调用真实 API
return doGetWeather(request);
} catch (Exception e) {
logger.error("天气 API 调用失败,降级到 Mock 数据: {}", e.getMessage());
// 降级返回 Mock 数据,保证服务可用性
return doGetWeatherMock(request);
}
}
private Response doGetWeather(Request request) {
String location = WeatherUtils.preprocessLocation(request.city());
String url = UriComponentsBuilder.fromHttpUrl(WEATHER_API_URL)
.queryParam("q", location)
.queryParam("days", request.days())
.queryParam("key", properties.getApiKey())
.toUriString();
return restTemplate.getForObject(url, Response.class);
}
⚠️ 潜在问题 5:Bean 名称硬编码
// Agent 中硬编码了工具名
.defaultToolNames("getWeatherFunction")
// 配置类中定义了 Bean 名
@Bean(name = "getWeatherFunction")
问题:两边名称必须完全一致,改名时容易遗漏,导致工具找不到。
修复建议:使用常量或配置中心管理:
public class ToolNames {
public static final String WEATHER = "getWeatherFunction";
}
// 两边都引用常量
.defaultToolNames(ToolNames.WEATHER)
@Bean(name = ToolNames.WEATHER)
4.3 WeatherUtils:城市名转换
public class WeatherUtils {
/**
* 将中文城市名转换为拼音
* 因为 WeatherAPI 需要英文城市名
*/
public static String preprocessLocation(String location) {
if (containsChinese(location)) {
return PinyinUtil.getPinyin(location, ""); // 杭州 -> hangzhou
}
return location; // Beijing -> Beijing
}
}
为什么需要转换? 国外的天气 API 通常不支持中文城市名,所以需要先把"杭州"转成"hangzhou"。
五、配置详解
5.1 application.yml 完整配置
server:
port: 8080
spring:
application:
name: react-agent
ai:
# ========== 天气工具配置 ==========
alibaba:
toolcalling:
weather:
enabled: true # 启用天气工具
api-key: ${WEATHER_API_KEY} # 从环境变量读取
# ========== 大模型配置 ==========
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY} # DashScope API Key
# ========== OpenAI 兼容配置 ==========
# Spring AI 使用 OpenAI 协议与 DashScope 通信
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: ${AI_DASHSCOPE_API_KEY}
chat:
options:
model: qwen-max-latest # 使用最新版 Qwen-Max
5.2 环境变量设置
# Linux / Mac
export AI_DASHSCOPE_API_KEY="sk-xxxxxxxxxxxxxxxx"
export WEATHER_API_KEY="xxxxxxxxxxxxxxxx"
# Windows PowerShell
$env:AI_DASHSCOPE_API_KEY="sk-xxxxxxxxxxxxxxxx"
$env:WEATHER_API_KEY="xxxxxxxxxxxxxxxx"
# 验证
echo $AI_DASHSCOPE_API_KEY
六、ReAct 工作流程深度剖析
6.1 一次完整的请求旅程
以用户提问"杭州明天天气怎么样?"为例:
6.2 图结构详解
关键理解:
- 节点(Node):图中的每个步骤,如 LLM Node、Tool Node
- 边(Edge):节点之间的流转关系,可以是条件分支
- 状态(State):每次循环都会更新共享状态,包含消息历史、工具结果等
- 循环:ReAct 的核心就是"LLM → Tool → LLM"的循环,直到 LLM 决定结束
七、接口规范
7.1 接口详情
| 属性 | 值 |
|---|---|
| 路径 | /react/chat |
| 方法 | GET(建议生产环境改为 POST) |
| 参数 | query: String,用户问题 |
| 返回值 | String,AI 回复文本 |
7.2 请求示例
# 天气查询
curl "http://localhost:8080/react/chat?query=杭州天气怎么样"
# 多轮意图(当前不支持,需要扩展)
curl "http://localhost:8080/react/chat?query=北京呢?"
# 闲聊
curl "http://localhost:8080/react/chat?query=你好,请自我介绍"
7.3 响应示例
杭州今天天气晴朗,气温 25°C,体感舒适。未来 3 天预报:
• 明天:多云,18°C ~ 26°C
• 后天:小雨,16°C ~ 22°C
• 大后天:晴,17°C ~ 25°C
建议:后天有雨,出门记得带伞 🌂
八、问题汇总与改进建议
| 序号 | 问题 | 严重程度 | 影响 | 建议方案 |
|---|---|---|---|---|
| 1 | HTTP 超时 10 分钟 | ⭐⭐⭐ | 故障时资源挂起 | 改为 30s 连接 / 60s 响应 |
| 2 | 空指针风险(双重 .get()) | ⭐⭐⭐⭐ | 直接抛异常,用户体验差 | 使用 orElseThrow + 空值检查 |
| 3 | GET 请求限制 | ⭐⭐⭐ | URL 长度受限,中文编码问题 | 改为 POST + JSON Body |
| 4 | 使用 Mock 数据 | ⭐⭐⭐⭐ | 返回假数据,误导用户 | 接入真实 API,Mock 作为降级 |
| 5 | Bean 名称硬编码 | ⭐⭐ | 维护困难,易遗漏 | 使用常量类或配置中心 |
九、设计总结
本项目展示了:
- ✅ 如何基于 Spring AI Alibaba 构建 ReAct Agent
- ✅ 如何注册和调用自定义工具
- ✅ 如何使用 Graph 框架编排复杂 AI 流程
- ⚠️ 生产环境需解决超时、容错、真实数据等问题
第二部分:部署实操指南
手把手教你把 ReAct 智能问答从代码跑起来,直到上线 运行🚀
一、前置准备
1.1 环境检查清单
在开始前,请确认你的环境满足以下要求:
检查 Java:
java -version
预期输出(版本号可能不同,但必须 ≥ 17):
openjdk version "21.0.2" 2024-01-16
OpenJDK Runtime Environment (build 21.0.2+13-58)
如果没有 Java 21:
- Ubuntu/Debian:
sudo apt install openjdk-21-jdk - Mac:
brew install openjdk@21 - Windows:下载 Eclipse Temurin 安装包
检查 Maven:
mvn -version
预期输出:
Apache Maven 3.9.6
如果没有 Maven:
- Ubuntu:
sudo apt install maven - Mac:
brew install maven - Windows:下载 Maven 官方包
1.2 获取阿里云 DashScope API Key(必须!)
这是整个项目的"钥匙",没有它 AI 无法工作。
申请步骤:
- 打开 阿里云 DashScope 控制台
- 登录阿里云账号(没有就注册一个)
- 左侧菜单找到 “API-KEY 管理”
- 点击 “创建新的 API Key”
- 给 Key 起个名字(如"react-demo")
- 立即复制保存! Key 只显示一次
Key 长这样:sk-abc123def456ghi789jkl012mn345op
💡 小贴士:建议创建后先测试一下 Key 是否有效:
curl https://dashscope.aliyuncs.com/api/v1/models > -H "Authorization: Bearer sk-你的Key"
1.3 获取天气 API Key(可选但建议)
项目内置了天气查询功能,需要 WeatherAPI.com 的 Key。
申请步骤:
- 访问 WeatherAPI.com
- 点击 “Sign Up Free”(免费版足够用)
- 填写邮箱注册
- 登录后进入 Dashboard
- 找到 API Key,复制保存
⚠️ 重要说明:如果不配置天气 Key,项目也能跑,但返回的是假数据(Mock)。详见第一部分"潜在问题 4"。
二、本地开发环境运行
2.1 进入项目目录
# 假设你已经在 examples 仓库目录
cd spring-ai-alibaba-graph-example/react
# 查看项目结构
ls -la
预期看到:
pom.xml
src/
main/
java/.../ReactApplication.java
resources/
application.yml
test/
2.2 配置 API Key
方式一:环境变量(推荐,安全)
# Linux / Mac(当前终端会话有效)
export AI_DASHSCOPE_API_KEY="sk-你的DashScopeKey"
export WEATHER_API_KEY="你的天气Key"
# 验证是否设置成功
echo $AI_DASHSCOPE_API_KEY
echo $WEATHER_API_KEY
# Windows PowerShell(当前会话有效)
$env:AI_DASHSCOPE_API_KEY="sk-你的DashScopeKey"
$env:WEATHER_API_KEY="你的天气Key"
方式二:写入配置文件(仅本地开发,切勿提交到 Git!)
编辑 src/main/resources/application.yml:
spring:
ai:
alibaba:
toolcalling:
weather:
api-key: "你的天气Key" # ⚠️ 生产环境不要这样写
dashscope:
api-key: "sk-你的DashScopeKey" # ⚠️ 生产环境不要这样写
🔒 安全警告:将 API Key 写入配置文件并提交到代码仓库是严重安全隐患!可能导致 Key 泄露被盗用,产生高额费用。
2.3 编译并启动服务
# 编译(第一次需要下载依赖,约 1-3 分钟,取决于网络)
mvn clean compile
# 启动服务
mvn spring-boot:run
启动过程解析:
启动成功的标志:
. ____ _ __ _ _
/\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.x)
Started ReactApplication in 8.456 seconds
Tomcat started on port(s): 8080 (http)
💡 观察 PlantUML 图:启动时控制台会打印 ReAct Agent 的图结构,可以复制到 PlantUML 在线编辑器 查看可视化流程。
2.4 接口测试
打开新的终端窗口(保持服务运行),执行:
# 测试 1:天气查询(会触发工具调用)
curl "http://localhost:8080/react/chat?query=杭州天气怎么样"
# 测试 2:另一个城市
curl "http://localhost:8080/react/chat?query=上海今天多少度"
# 测试 3:闲聊(不会触发工具调用)
curl "http://localhost:8080/react/chat?query=你好呀"
# 测试 4:空参数(观察错误处理)
curl "http://localhost:8080/react/chat"
预期响应:
- 天气查询:返回天气描述(注意:当前可能是 Mock 数据)
- 闲聊:友好的问候回复
- 空参数:根据代码实现,可能返回空或报错
三、生产环境部署
3.1 打包应用
# 清理并打包(跳过测试加速)
mvn clean package -DskipTests
# 查看生成的 JAR 包
ls -lh target/
预期生成:
target/react-1.0.0.jar (约 30-50MB,包含所有依赖)
3.2 服务器准备
最低配置要求:
| 资源 | 最低要求 | 推荐配置 |
|---|---|---|
| CPU | 1 核 | 2 核 |
| 内存 | 2 GB | 4 GB |
| 磁盘 | 10 GB | 20 GB |
| 系统 | Ubuntu 20.04+ / CentOS 8+ | Ubuntu 22.04 LTS |
| 网络 | 能访问 dashscope.aliyuncs.com | 带宽 ≥ 5Mbps |
云服务器推荐:
- 阿里云 ECS(同地域访问 DashScope 更快)
- 腾讯云 CVM
- AWS EC2
3.3 上传 JAR 包到服务器
# 使用 scp 命令(本地终端执行)
scp target/react-1.0.0.jar root@你的服务器IP:/opt/react/
# 如果目录不存在,先创建
ssh root@你的服务器IP "mkdir -p /opt/react"
或者使用图形化工具:
- Windows:WinSCP、FileZilla
- Mac:Cyberduck、Transmit
3.4 启动方式对比
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 直接运行 | 临时测试 | 简单 | 关闭终端即停止 |
| nohup | 简单后台 | 脱离终端 | 重启后需手动启动 |
| Systemd | 生产环境 | 开机自启、自动重启、状态管理 | 配置稍复杂 |
| Docker | 容器化部署 | 环境隔离、一次构建多处运行 | 需要 Docker 知识 |
3.5 Systemd 部署(推荐用于生产)
步骤 1:创建服务文件
sudo vim /etc/systemd/system/react-agent.service
写入以下内容(根据你的实际路径修改):
[Unit]
Description=Spring AI Alibaba ReAct Agent
After=network.target
[Service]
Type=simple
User=ubuntu # 运行服务的用户(不要用 root)
WorkingDirectory=/opt/react # JAR 包所在目录
ExecStart=/usr/bin/java -jar /opt/react/react-1.0.0.jar
# 环境变量(重要!)
Environment="AI_DASHSCOPE_API_KEY=sk-你的DashScopeKey"
Environment="WEATHER_API_KEY=你的天气Key"
Environment="SERVER_PORT=8080"
# JVM 参数(根据服务器内存调整)
Environment="JAVA_OPTS=-Xms512m -Xmx1024m"
# 自动重启策略
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
步骤 2:启动并启用服务
# 重新加载 Systemd 配置
sudo systemctl daemon-reload
# 启动服务
sudo systemctl start react-agent
# 设置开机自启
sudo systemctl enable react-agent
# 查看状态
sudo systemctl status react-agent
# 查看实时日志
sudo journalctl -u react-agent -f
常用命令速查:
sudo systemctl start react-agent # 启动
sudo systemctl stop react-agent # 停止
sudo systemctl restart react-agent # 重启
sudo systemctl status react-agent # 查看状态
四、Docker 部署(最简单)
如果你熟悉 Docker,这是最快的部署方式。
4.1 编写 Dockerfile
在项目根目录创建 Dockerfile:
# ========== 构建阶段 ==========
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
# 安装 Maven 并构建
RUN apk add --no-cache maven && mvn clean package -DskipTests
# ========== 运行阶段 ==========
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# 从构建阶段复制 JAR 包
COPY --from=builder /app/target/*.jar app.jar
# 创建非 root 用户(安全最佳实践)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
# 启动命令(环境变量从外部传入)
ENTRYPOINT ["java", "-jar", "app.jar"]
Dockerfile 解析:
| 指令 | 作用 |
|---|---|
multi-stage build |
分阶段构建,最终镜像只包含 JRE 和 JAR,体积更小 |
non-root user |
安全最佳实践,防止容器逃逸攻击 |
HEALTHCHECK |
Docker 自动检测服务是否健康 |
4.2 构建镜像
# 构建镜像(注意最后的点)
docker build -t react-agent:latest .
# 查看构建好的镜像
docker images | grep react-agent
4.3 运行容器
docker run -d --name react-agent -p 8080:8080 -e AI_DASHSCOPE_API_KEY="sk-你的DashScopeKey" -e WEATHER_API_KEY="你的天气Key" --restart unless-stopped react-agent:latest
参数说明:
| 参数 | 含义 |
|---|---|
-d |
后台运行(detached) |
--name |
容器名称 |
-p 8080:8080 |
端口映射(主机端口:容器端口) |
-e |
设置环境变量 |
--restart unless-stopped |
除非手动停止,否则自动重启 |
验证运行:
# 查看容器状态
docker ps
# 查看日志
docker logs -f react-agent
# 测试接口
curl "http://localhost:8080/react/chat?query=杭州天气"
4.4 Docker Compose 部署(推荐)
创建 docker-compose.yml:
version: '3.8'
services:
react-agent:
build: .
container_name: react-agent
ports:
- "8080:8080"
environment:
- AI_DASHSCOPE_API_KEY=${AI_DASHSCOPE_API_KEY}
- WEATHER_API_KEY=${WEATHER_API_KEY}
- JAVA_OPTS=-Xms512m -Xmx1024m
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/react/chat?query=ping"]
interval: 30s
timeout: 10s
retries: 3
使用:
# 启动
docker-compose up -d
# 查看日志
docker-compose logs -f
# 停止
docker-compose down
五、Nginx 反向代理与 HTTPS
5.1 为什么需要 Nginx?
Nginx 的作用:
- 反向代理:隐藏后端服务,统一入口
- 负载均衡:多台服务时分发请求
- HTTPS:SSL 证书管理
- 静态资源:缓存、压缩
5.2 安装 Nginx
# Ubuntu / Debian
sudo apt update
sudo apt install nginx
# 启动
sudo systemctl start nginx
sudo systemctl enable nginx
# 验证
sudo systemctl status nginx
5.3 配置反向代理
创建配置文件:
sudo vim /etc/nginx/sites-available/react-agent
写入:
server {
listen 80;
server_name your-domain.com; # 改成你的域名或 IP
# 日志
access_log /var/log/nginx/react-access.log;
error_log /var/log/nginx/react-error.log;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
# 转发真实 IP 和 Host
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
启用配置:
# 创建软链接
sudo ln -s /etc/nginx/sites-available/react-agent /etc/nginx/sites-enabled/
# 检查配置语法
sudo nginx -t
# 重载配置
sudo systemctl reload nginx
5.4 配置 HTTPS(强烈推荐)
使用 Let’s Encrypt 免费证书:
# 安装 certbot
sudo apt install certbot python3-certbot-nginx
# 获取并自动配置证书
sudo certbot --nginx -d your-domain.com
# 按照提示操作,选择 redirect HTTP to HTTPS
# 测试自动续期
sudo certbot renew --dry-run
Certbot 会自动:
- 生成 SSL 证书
- 修改 Nginx 配置添加 443 端口
- 设置 HTTP 自动跳转 HTTPS
- 配置证书自动续期(每 90 天)
六、部署验证清单
部署完成后,逐项检查:
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 服务运行状态 | sudo systemctl status react-agent |
Active: active (running) |
| 端口监听 | sudo ss -tlnp | grep 8080 |
LISTEN |
| 本地接口 | curl "http://localhost:8080/react/chat?query=测试" |
返回 AI 回复 |
| 外网接口 | curl "http://你的IP:8080/react/chat?query=测试" |
返回 AI 回复 |
| Nginx 代理 | curl "http://你的域名/react/chat?query=测试" |
返回 AI 回复 |
| HTTPS | curl -k "https://你的域名/react/chat?query=测试" |
返回 AI 回复 |
| 日志 | sudo tail -f /var/log/react-agent.log |
无 ERROR |
| 资源使用 | htop 或 free -h |
CPU < 50%, 内存 < 80% |
七、常见问题排查
问题 1:启动报错 “API Key 为空”
现象:
IllegalArgumentException: API Key must not be empty
排查步骤:
# 1. 检查环境变量
echo $AI_DASHSCOPE_API_KEY
# 2. 如果为空,重新设置
export AI_DASHSCOPE_API_KEY="sk-xxx"
# 3. 如果使用 Systemd,检查服务文件中的 Environment 配置
sudo cat /etc/systemd/system/react-agent.service | grep Environment
# 4. 修改后重载并重启
sudo systemctl daemon-reload
sudo systemctl restart react-agent
问题 2:端口 8080 被占用
现象:
Web server failed to start. Port 8080 was already in use.
解决:
# 查看谁占用了 8080
sudo lsof -i:8080
# 或
sudo ss -tlnp | grep 8080
# 方式 1:杀掉占用进程
kill -9 <PID>
# 方式 2:修改服务端口
# 在 application.yml 或环境变量中设置:
export SERVER_PORT=8081
问题 3:外网无法访问
排查流程:
命令:
# 检查防火墙
sudo ufw status
sudo ufw allow 8080/tcp
# 检查云服务器安全组(阿里云/腾讯云/AWS 控制台)
# 确保入站规则允许 8080 或 80/443 端口
问题 4:LLM 调用报错
| 错误码 | 含义 | 解决 |
|---|---|---|
| 401 | API Key 无效 | 检查 Key 是否正确,是否过期 |
| 429 | 请求太频繁 | 降低调用频率,或升级 DashScope 套餐 |
| 503 | 服务暂时不可用 | 等待几分钟后重试,或查看 DashScope 状态页 |
| 连接超时 | 网络问题 | 检查服务器能否访问 dashscope.aliyuncs.com |
测试网络连通性:
curl -v https://dashscope.aliyuncs.com/compatible-mode/v1/models -H "Authorization: Bearer sk-你的Key"
问题 5:返回空回复或异常
可能原因:
- API Key 没有权限(新 Key 可能需要等待几分钟生效)
- 网络不通(服务器需要能访问外网)
- 请求格式错误
查看详细日志:
# Systemd 方式
sudo journalctl -u react-agent -n 100 --no-pager
# Docker 方式
docker logs react-agent --tail 100
问题 6:天气数据是假的
确认方法:
curl "http://localhost:8080/react/chat?query=杭州天气"
# 如果返回的温度总是 25°C,无论实际天气如何,就是 Mock 数据
解决:修改代码,让 apply() 方法调用 doGetWeather() 而不是 doGetWeatherMock()。详见第一部分"潜在问题 4"的修复建议。
八、与 chatflow 示例的对比
| 对比维度 | chatflow | react(本项目) |
|---|---|---|
| 功能定位 | 智能待办助手 | 天气问答助手 |
| 难度等级 | ⭐⭐ 中级 | ⭐ 入门级 |
| 核心流程 | 多意图识别 + 子图路由 | 单 ReAct 循环 |
| 交互方式 | POST + Session 保持多轮 | GET + 单轮问答 |
| 状态管理 | 复杂状态机 | 简单状态传递 |
| 工具数量 | 多个(增删改查) | 单个(天气查询) |
| 适用学习阶段 | 进阶 | 入门 |
学习建议:先掌握 react(简单直观),再挑战 chatflow(复杂但功能更强)。
九、一键部署脚本
为了方便快速部署,提供以下脚本:
#!/bin/bash
set -e
# ========== 配置区(修改为你的实际值)==========
JAR_FILE="react-1.0.0.jar"
SERVICE_NAME="react-agent"
PORT=8080
DASHSCOPE_KEY="sk-你的DashScopeKey"
WEATHER_KEY="你的天气Key"
INSTALL_DIR="/opt/react"
JAVA_VERSION="21"
# ========== 颜色输出 ==========
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ========== 检查 root 权限 ==========
if [ "$EUID" -ne 0 ]; then
log_error "请使用 sudo 运行此脚本"
exit 1
fi
# ========== 安装 Java ==========
if ! command -v java &> /dev/null; then
log_info "安装 OpenJDK ${JAVA_VERSION}..."
apt update
apt install -y openjdk-${JAVA_VERSION}-jdk
else
JAVA_CURRENT=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | cut -d'.' -f1)
if [ "$JAVA_CURRENT" -lt "$JAVA_VERSION" ]; then
log_warn "当前 Java 版本过低,建议升级到 ${JAVA_VERSION}"
fi
fi
# ========== 创建目录 ==========
log_info "创建应用目录..."
mkdir -p ${INSTALL_DIR}
# ========== 复制 JAR 包 ==========
if [ ! -f "${JAR_FILE}" ]; then
log_error "找不到 JAR 文件: ${JAR_FILE}"
log_info "请先运行: mvn clean package -DskipTests"
exit 1
fi
cp ${JAR_FILE} ${INSTALL_DIR}/
log_info "JAR 包已复制到 ${INSTALL_DIR}"
# ========== 创建 Systemd 服务 ==========
log_info "创建 Systemd 服务..."
cat > /etc/systemd/system/${SERVICE_NAME}.service <<EOF
[Unit]
Description=Spring AI Alibaba ReAct Agent
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=${INSTALL_DIR}
ExecStart=/usr/bin/java -Xms512m -Xmx1024m -jar ${INSTALL_DIR}/${JAR_FILE}
Environment="AI_DASHSCOPE_API_KEY=${DASHSCOPE_KEY}"
Environment="WEATHER_API_KEY=${WEATHER_KEY}"
Environment="SERVER_PORT=${PORT}"
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
# ========== 启动服务 ==========
log_info "启动服务..."
systemctl daemon-reload
systemctl start ${SERVICE_NAME}
systemctl enable ${SERVICE_NAME}
# ========== 验证 ==========
sleep 3
if systemctl is-active --quiet ${SERVICE_NAME}; then
log_info "✅ 服务启动成功!"
log_info "📍 本地访问: http://localhost:${PORT}/react/chat?query=测试"
log_info "📊 查看状态: systemctl status ${SERVICE_NAME}"
log_info "📖 查看日志: journalctl -u ${SERVICE_NAME} -f"
else
log_error "❌ 服务启动失败,请检查日志"
journalctl -u ${SERVICE_NAME} -n 50 --no-pager
exit 1
fi
使用方法:
# 1. 保存为 deploy.sh
# 2. 修改配置区的变量
# 3. 执行
chmod +x deploy.sh
sudo ./deploy.sh
十、部署总结
部署方式选择建议:
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 本地开发测试 | mvn spring-boot:run |
最快,热更新 |
| 内部演示 | nohup java -jar |
简单,临时使用 |
| 生产环境 | Systemd | 稳定,自动恢复 |
| 微服务架构 | Docker + K8s | 弹性伸缩,环境一致 |
| 快速验证 | Docker Compose | 一键启动,易于清理 |
下一步可以做的事:
- 🔧 修改代码接入真实天气 API(替换 Mock 数据)
- 🔒 添加接口鉴权(Spring Security + JWT)
- 📊 接入监控(Prometheus + Grafana)
- 🚀 学习更复杂的 chatflow 示例
- 🌐 配置域名和 CDN,开放给更多人使用
更多推荐


所有评论(0)