本文手把手教你用Spring AI快速对接智谱GLM-4,构建一个支持多轮对话、上下文记忆的智能天气Agent!
📖 前言
随着大语言模型(LLM)的快速发展,将AI能力集成到企业应用已成为趋势。但对于Java开发者而言,如何优雅地在Spring生态中集成LLM一直是个难题。Spring AI 的出现彻底改变了这一局面------它提供了统一抽象的AI编程模型,让Java开发者能够像使用JDBC一样简单地对接各种AI模型。
今天,我将手把手带你用Spring AI对接智谱AI(ZhipuAI) ,实现一个有记忆功能的智能天气助手Agent。这个Agent不仅能查询天气,还能记住对话上下文,支持多轮对话!
🚀 一、项目概述
1.1 技术栈
| 技术 | 说明 |
|------|------|
| Spring Boot 3.x | 基础框架 |
| Spring AI 1.1.2 | AI集成框架 |
| 智谱AI GLM-4 | 大语言模型 |
| Java 17+ | 开发语言 |
1.2 功能特性
┌─────────────────────────────────────────────────────────┐
│ 智能天气助手 Agent │
├─────────────────────────────────────────────────────────┤
│ ✓ 多轮对话记忆 - 记住之前的对话上下文 │
│ ✓ 工具调用 - 自动调用天气API获取实时数据 │
│ ✓ 智能推断 - 根据上下文推断用户意图 │
│ ✓ 穿衣建议 - 根据天气自动推荐穿衣 │
│ ✓ 多城市对比 - 同时查询多个城市天气 │
└─────────────────────────────────────────────────────────┘
1.3 效果演示
用户: 北京今天天气怎么样?
Agent: 【北京】天气信息
🌡️ 当前温度: 25°C
☁️ 天气状况: 晴
💧 湿度: 45%
👔 穿衣建议: 天气暖和,建议穿T恤...
用户: 那明天呢?
Agent: 根据您之前查询的北京,明天天气如下:
🌡️ 温度: 23-28°C
☁️ 天气状况: 多云转晴
👔 穿衣建议: 建议带件薄外套,早晚温差较大...
用户: 需要带伞吗?
Agent: 不需要带伞!明天北京天气以晴为主,降水概率很低。
🛠️ 二、环境准备
2.1 获取智谱AI API Key
-
访问 智谱AI开放平台
-
注册账号并完成认证
-
在控制台创建应用,获取 API Key
2.2 添加Maven依赖
xml
<!-- Spring AI 智谱模型支持 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
<version>1.1.2</version>
</dependency>
2.3 配置文件
在 bootstrap-dev.yml 中配置:
yaml
spring:
ai:
zhipuai:
api-key: your-zhipuai-api-key-here # 替换为你的API Key
model: glm-4-flash # 模型:glm-4 或 glm-4-flash
🏗️ 三、核心架构设计
3.1 项目结构
com.easthope.modules.weather/
├── WeatherAgentTest.java # 测试类
├── api/
│ └── controller/
│ └── WeatherAgentController.java # REST API控制器
└── domain/
├── agent/
│ ├── WeatherAgent.java # 🌟 核心Agent类
│ └── WeatherAgentConfig.java # Agent配置
├── memory/
│ └── ConversationMemory.java # 🌟 对话记忆管理
├── model/
│ ├── AgentResponse.java # 响应DTO
│ ├── WeatherInfo.java # 天气数据模型
│ └── ZhiPuMessage.java # 消息格式
├── service/
│ ├── WeatherService.java # 天气服务接口
│ └── impl/
│ └── WeatherServiceImpl.java # 天气服务实现
└── tool/
└── WeatherTool.java # 🌟 天气查询工具
3.2 核心组件关系
┌──────────────────────────────────────────────────────────────────┐
│ WeatherAgent │
│ (核心调度器) │
└────────────────────────┬───────────────────────────────────────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌──────────────┐ ┌───────────────────┐
│ Conversation │ │ WeatherTool │ │ WeatherAgentConfig │
│ Memory │ │ (工具) │ │ (配置) │
│ (记忆) │ │ │ │ │
└───────────────┘ └──────────────┘ └───────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────────────────────────────────────────────────────┐
│ ZhiPuAiChatModel │
│ (智谱GLM-4) │
└───────────────────────────────────────────────────────────────┘
🔑 四、关键技术实现
4.1 System Prompt - 行为定义的核心
System Prompt 是定义Agent行为的灵魂!一个好的Prompt能让AI准确理解自己的角色和工具使用方法。
java
/**
* 获取天气Agent的System Prompt
*/
public String getSystemPrompt() {
return """
你是一个专业、友好的天气预报助手,名为"小天气"。
【核心能力】
1. 查询任意城市的天气信息(温度、湿度、天气状况、穿衣建议等)
2. 提供穿衣、出行、健康等方面的建议
3. 支持多城市天气对比
4. 能够记住用户的偏好和之前的对话上下文
【对话风格】
- 使用友好、亲切的语气
- 回复简洁有条理,适当使用emoji增加可读性
- 如需查询天气,必须调用天气工具获取准确数据
- 结合工具返回的数据和日常知识给出建议
【工具使用规则】
- 当用户询问天气时,必须调用 get_weather 工具
- 当用户需要穿衣建议时,可以调用 get_dressing_advice 工具
- 当用户需要对比多个城市天气时,调用 get_multiple_cities_weather 工具
- 如果用户没有指定城市,先询问用户想查询哪个城市
【上下文记忆】
- 记住用户之前查询过的城市
- 在多轮对话中保持连贯性
""";
}
💡 提示:System Prompt 应该包含:
- 角色定义 - AI扮演什么角色
- 能力边界 - 能做什么,不能做什么
- 工具说明 - 什么时候该调用工具
- 输出格式 - 如何格式化回复
- 安全边界 - 什么不该说
4.2 工具定义 - 与外部世界交互
Spring AI 的 @Tool 注解让工具定义变得极其简单:
java
@Component
public class WeatherTool {
@Autowired
private WeatherService weatherService;
/**
* 查询指定城市的天气信息
*
* @param city 城市名称(必填),例如:北京、上海、广州、深圳
* @return 天气信息JSON字符串
*/
@Tool(name = "get_weather",
description = "查询指定城市的天气信息,返回温度、湿度、天气状况、穿衣建议等详细数据。")
public String getWeather(
@ToolParam(description = "城市名称,必填", required = true)
String city) {
// 调用天气服务获取数据
WeatherInfo weatherInfo = weatherService.queryCurrentWeather(city);
// 格式化返回结果
return String.format("""
【%s】天气信息
🌡️ 当前温度: %d°C
📈 最高温度: %d°C
📉 最低温度: %d°C
☁️ 天气状况: %s
💧 湿度: %d%%
🌬️ 风向风力: %s%s
👔 穿衣建议: %s
""",
weatherInfo.getCity(),
weatherInfo.getTemperature(),
weatherInfo.getTempHigh(),
weatherInfo.getTempLow(),
weatherInfo.getWeather(),
weatherInfo.getHumidity(),
weatherInfo.getWindDirection(),
weatherInfo.getWindPower(),
weatherInfo.getDressingAdvice()
);
}
/**
* 批量查询多个城市的天气
*/
@Tool(name = "get_multiple_cities_weather",
description = "批量查询多个城市的天气信息,用于对比不同城市的天气状况。")
public String getMultipleCitiesWeather(
@ToolParam(description = "城市列表,逗号分隔", required = true)
String cities) {
// ... 批量查询逻辑
}
}
🎯 工具定义要点:
- 使用
@Tool注解标记工具方法@ToolParam详细描述参数用途- 返回值要易读,便于AI理解和展示给用户
4.3 对话记忆 - 实现多轮对话的关键
这是实现"有记忆"功能的核心!我们让 ConversationMemory 实现 Spring AI 的 ChatMemory 接口:
java
/**
* 对话记忆管理器
* 实现Spring AI的ChatMemory接口,支持多会话
*/
@Slf4j
@Component
public class ConversationMemory implements ChatMemory {
private static final int MAX_MESSAGES = 20;
// 会话存储:sessionId -> 消息列表
private final Map<String, List<Message>> sessions = new ConcurrentHashMap<>();
/**
* 添加消息到会话
*/
@Override
public void add(String sessionId, List<Message> messages) {
List<Message> sessionMessages = sessions.computeIfAbsent(sessionId, k -> new ArrayList<>());
// 限制消息数量,防止内存溢出
int maxToAdd = MAX_MESSAGES - sessionMessages.size();
if (maxToAdd > 0) {
sessionMessages.addAll(messages.subList(
Math.max(0, messages.size() - maxToAdd),
messages.size()
));
}
}
/**
* 获取指定会话的消息
*/
@Override
public List<Message> get(String sessionId, int lastN) {
List<Message> allMessages = sessions.getOrDefault(sessionId, new ArrayList<>());
if (lastN <= 0 || lastN >= allMessages.size()) {
return new ArrayList<>(allMessages);
}
return new ArrayList<>(allMessages.subList(
allMessages.size() - lastN,
allMessages.size()
));
}
/**
* 清除指定会话
*/
@Override
public void clear(String sessionId) {
sessions.remove(sessionId);
}
/**
* 获取或创建会话ID
*/
public String getOrCreateSessionId(String sessionId) {
if (sessionId == null || sessionId.trim().isEmpty()) {
return UUID.randomUUID().toString().replace("-", "");
}
sessions.computeIfAbsent(sessionId, k -> new ArrayList<>());
return sessionId;
}
}
🔑 记忆管理要点:
- 实现
ChatMemory接口,与Spring AI无缝集成- 会话隔离:每个sessionId独立存储
- 容量限制:防止内存溢出
- 自动创建:会话不存在时自动创建
4.4 核心Agent - 调度一切
java
@Slf4j
@Component
public class WeatherAgent {
@Value("${spring.ai.zhipuai.api-key:}")
private String apiKey;
@Value("${spring.ai.zhipuai.model:glm-4-flash}")
private String model;
private final WeatherTool weatherTool;
private final ConversationMemory conversationMemory;
private final WeatherAgentConfig agentConfig;
/**
* 对话(带记忆)
*/
public AgentResponse chat(String message, String sessionId) {
// 1. 确保有有效的会话ID
String validSessionId = conversationMemory.getOrCreateSessionId(sessionId);
try {
// 2. 获取System Prompt
String systemPrompt = agentConfig.getSystemPrompt();
// 3. 创建ChatClient(带工具和记忆)
ChatClient chatClient = createToolEnabledChatClient(validSessionId);
// 4. 调用AI
String response = chatClient.prompt()
.system(systemPrompt)
.user(message)
.call()
.content();
// 5. 返回结果
return AgentResponse.builder()
.content(response)
.sessionId(validSessionId)
.turnCount(conversationMemory.getTurnCount(validSessionId))
.build();
} catch (Exception e) {
log.error("Agent调用失败", e);
return AgentResponse.builder()
.content("抱歉,服务暂时不可用。")
.error(e.getMessage())
.build();
}
}
/**
* 创建带工具和记忆的ChatClient
*/
private ChatClient createToolEnabledChatClient(String sessionId) {
// 创建智谱API实例
ZhiPuAiApi zhiPuAiApi = ZhiPuAiApi.builder()
.apiKey(apiKey)
.build();
// 配置模型参数
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()
.model(model)
.temperature(0.7) // 控制随机性
.maxTokens(2048) // 最大token数
.build();
ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(zhiPuAiApi, options);
// 创建ChatClient,配置工具和记忆
return ChatClient.builder(chatModel)
.defaultTools(weatherTool) // 注册工具
.defaultAdvisors(
new MessageChatMemoryAdvisor(conversationMemory, sessionId)
)
.build();
}
/**
* 流式对话
*/
public Flux<String> chatStream(String message, String sessionId) {
String validSessionId = conversationMemory.getOrCreateSessionId(sessionId);
ChatClient chatClient = createToolEnabledChatClient(validSessionId);
return chatClient.prompt()
.system(agentConfig.getSystemPrompt())
.user(message)
.stream()
.content();
}
}
📡 五、REST API 接口
5.1 控制器实现
java
@RestController
@RequestMapping("/weather/agent")
@Tag(name = "天气Agent服务", description = "有记忆功能的智能天气问答助手")
public class WeatherAgentController {
@Autowired
private WeatherAgent weatherAgent;
/**
* 对话接口(带记忆)
*/
@PostMapping("/chat")
public Map<String, Object> chat(
@RequestParam String message,
@RequestParam(required = false) String sessionId) {
AgentResponse response = weatherAgent.chat(message, sessionId);
return Map.of(
"code", 200,
"data", Map.of(
"content", response.getContent(),
"sessionId", response.getSessionId(),
"turnCount", response.getTurnCount()
)
);
}
/**
* 流式对话接口
*/
@PostMapping(value = "/chat/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(
@RequestParam String message,
@RequestParam(required = false) String sessionId) {
return weatherAgent.chatStream(message, sessionId);
}
/**
* 获取对话历史
*/
@GetMapping("/history/{sessionId}")
public Map<String, Object> getHistory(@PathVariable String sessionId) {
return Map.of(
"code", 200,
"data", weatherAgent.getHistory(sessionId)
);
}
/**
* 清除对话历史
*/
@DeleteMapping("/history/{sessionId}")
public Map<String, Object> clearHistory(@PathVariable String sessionId) {
weatherAgent.clearHistory(sessionId);
return Map.of("code", 200, "msg", "历史已清除");
}
}
5.2 接口说明
| 接口 | 方法 | 说明 | 示例 |
|------|------|------|------|
| /chat | POST | 带记忆的对话 | 传入sessionId继续对话 |
| /chat/stream | POST | 流式对话 | SSE实时输出 |
| /weather | GET | 直接查询天气 | 工具直接调用 |
| /history/{id} | GET | 获取对话历史 | 查看上下文 |
| /history/{id} | DELETE | 清除历史 | 重置会话 |
5.3 调用示例
bash
# 第一次对话(自动创建sessionId)
curl -X POST "http://localhost:8080/weather/agent/chat?message=北京天气怎么样"
# 返回
{
"code": 200,
"data": {
"content": "【北京】天气信息...\n🌡️ 当前温度: 25°C...",
"sessionId": "a1b2c3d4e5f6",
"turnCount": 1
}
}
# 第二次对话(传入sessionId,继续上下文)
curl -X POST "http://localhost:8080/weather/agent/chat?message=那明天呢&sessionId=a1b2c3d4e5f6"
# 返回(Agent记得是北京)
{
"code": 200,
"data": {
"content": "根据您查询的北京,明天天气如下...\n🌡️ 温度: 23-28°C...",
"sessionId": "a1b2c3d4e5f6",
"turnCount": 2
}
}
🧪 六、测试验证
6.1 单元测试
java
@ExtendWith(MockitoExtension.class)
public class WeatherAgentTest {
@Mock
private WeatherTool weatherTool;
@Mock
private WeatherAgentConfig agentConfig;
private ConversationMemory conversationMemory;
private WeatherAgent weatherAgent;
@BeforeEach
public void setUp() {
conversationMemory = new ConversationMemory();
weatherAgent = new WeatherAgent(weatherTool, conversationMemory, agentConfig);
ReflectionTestUtils.setField(weatherAgent, "apiKey", "test-key");
when(agentConfig.getSystemPrompt()).thenReturn("你是一个天气预报助手。");
}
@Test
public void testSessionIsolation() {
// 会话1
String sessionId1 = conversationMemory.getOrCreateSessionId(null);
conversationMemory.addUserMessage(sessionId1, "北京");
// 会话2
String sessionId2 = conversationMemory.getOrCreateSessionId(null);
conversationMemory.addUserMessage(sessionId2, "上海");
// 验证隔离
assert conversationMemory.getHistory(sessionId1).size() == 1;
assert conversationMemory.getHistory(sessionId1).get(0).getText().equals("北京");
}
@Test
public void testMemoryCapacityLimit() {
String sessionId = conversationMemory.getOrCreateSessionId(null);
// 添加50条消息
for (int i = 0; i < 25; i++) {
conversationMemory.addUserMessage(sessionId, "用户" + i);
conversationMemory.addAssistantMessage(sessionId, "助手" + i);
}
// 验证容量限制
assert conversationMemory.getHistory(sessionId).size() <= 40;
}
}
6.2 集成测试场景
测试1: 多轮对话上下文记忆
├── 第一轮: 用户问"北京天气"
├── 第二轮: 用户问"明天呢" → Agent记得是北京
└── 第三轮: 用户问"需要带伞吗" → Agent记得还是北京
测试2: 多会话隔离
├── 会话A: 查询北京
└── 会话B: 查询上海 → 两个会话互不影响
测试3: 工具自动调用
├── 用户: "广州热吗"
└── Agent: 自动调用 get_weather(广州) → 返回温度信息
📚 七、深入理解 Spring AI
7.1 Spring AI 核心概念
┌─────────────────────────────────────────────────────────────┐
│ Spring AI 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Prompt │────▶│ ChatModel │────▶│ Output │ │
│ │ (输入) │ │ (模型) │ │ (结果) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ ▲ │ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Advisor │────▶│ Memory │ │ Parser │ │
│ │ (增强器) │ │ (记忆) │ │ (解析器) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────┐│
│ │ Tools │ │ Formats ││
│ │ (工具) │ │(格式转换)││
│ └─────────────┘ └─────────┘│
│ │
└─────────────────────────────────────────────────────────────┘
7.2 关键组件说明
| 组件 | 说明 | 我们的实现 |
|------|------|-----------|
| ChatModel | 模型抽象,支持多种LLM | ZhiPuAiChatModel |
| Prompt | 提示词模板 | System + User Message |
| Memory | 对话记忆 | ConversationMemory |
| Advisor | 拦截器/增强器 | MessageChatMemoryAdvisor |
| Tool | 外部工具 | WeatherTool |
| OutputParser | 输出解析 | 字符串直接返回 |
💡 八、最佳实践
8.1 System Prompt 设计技巧
java
// ❌ 不好的Prompt - 太模糊
"你是AI助手,回答问题。"
// ✅ 好的Prompt - 具体明确
"""
你是一个专业的[角色]。
你的专长是[领域]。
当用户询问[场景]时,你应该[行为]。
禁止:[限制项]
"""
8.2 工具设计原则
-
单一职责 - 每个工具只做一件事
-
参数明确 - 必填/可选参数要标注清楚
-
返回易读 - 返回结果要方便用户理解
-
错误处理 - 异常情况要返回友好提示
8.3 记忆管理策略
java
// 容量限制
private static final int MAX_MESSAGES = 20;
// 自动过期(定时清理)
@Scheduled(fixedRate = 3600000) // 每小时
public void cleanExpiredSessions() {
// 清理超过1小时的会话
}
// 分层记忆
public class HybridMemory {
private final Map<String, List<Message>> shortTerm = new ConcurrentHashMap<>();
private final Map<String, Summary> longTerm = new ConcurrentHashMap<>();
}
8.4 安全考虑
java
// 1. API Key 保护
// ❌ 不要硬编码
private String apiKey = "sk-xxx";
// ✅ 使用配置中心或环境变量
@Value("${spring.ai.zhipuai.api-key:}")
private String apiKey;
// 2. 敏感信息过滤
public String filterSensitive(String input) {
return input.replaceAll("\\d{11}", "***") // 手机号
.replaceAll("\\d{18}", "***"); // 身份证
}
// 3. 内容安全检查
public boolean isSafe(String content) {
// 调用内容安全API或使用本地规则
}
🔮 九、扩展方向
9.1 RAG 知识增强
java
// 结合向量数据库,实现知识检索增强
@Autowired
private VectorStore vectorStore;
public String chatWithKnowledge(String question) {
// 1. 检索相关知识
List<Document> docs = vectorStore.similaritySearch(question);
// 2. 构建增强Prompt
String context = docs.stream()
.map(Document::getText)
.collect(Collectors.joining("\n"));
String prompt = """
基于以下知识回答问题:
%s
问题:%s
""".formatted(context, question);
// 3. 调用模型
return chatClient.prompt().user(prompt).call().content();
}
9.2 多Agent协作
┌─────────────────────────────────────────────────────────────┐
│ Multi-Agent System │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ │
│ │ Router │ ← 用户请求入口 │
│ │ Agent │ │
│ └────┬────┘ │
│ │ │
│ ┌─────┴─────┬──────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────┐ ┌────────┐ ┌────────┐ │
│ │Weather│ │ Stock │ │ News │ │
│ │Agent │ │ Agent │ │ Agent │ │
│ └──────┘ └────────┘ └────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
9.3 流式输出优化
java
// 前端SSE接收
const eventSource = new EventSource('/chat/stream?message=...');
eventSource.onmessage = (event) => {
document.getElementById('response').innerText += event.data;
};
eventSource.onerror = () => {
eventSource.close();
};
📝 总结
通过本文,我们完整实现了一个基于 Spring AI + 智谱GLM-4 的智能天气助手Agent。核心要点回顾:
| 知识点 | 关键实现 |
|--------|----------|
| System Prompt | 定义Agent角色、工具、行为规则 |
| 工具调用 | @Tool 注解,自动函数调用 |
| 对话记忆 | ChatMemory 接口,多会话支持 |
| Agent调度 | ChatClient 整合所有组件 |
| 流式输出 | Flux<String> + SSE |
核心技术收获
-
✅ Spring AI 统一抽象,切换模型零改动
-
✅ 智谱AI 中文支持优秀,集成简单
-
✅ 对话记忆实现多轮上下文
-
✅ 工具调用扩展Agent能力边界
📎 参考资源
💬 写在最后:AI能力正在快速改变软件开发方式。作为Java开发者,掌握Spring AI将使你在AI时代保持竞争力。希望本文能帮助你在项目中成功落地AI能力!
如果觉得有帮助,欢迎 Star ⭐ 支持!有任何问题欢迎留言讨论!