Spring AI + Ollama 实战:如何用本地DeepSeek模型实现智能家居控制(附代码)
Spring AI 与 Ollama 实战:构建基于本地大模型的智能家居控制中枢
最近在折腾家里的智能设备时,我一直在想,能不能让 AI 不只是简单地回答天气,而是真正理解“把客厅的灯调暗一点”或者“半小时后关闭空调”这样的自然指令,并自动执行。市面上的智能音箱虽然方便,但隐私和定制化程度总让人心里不踏实。直到我把 Spring AI、Ollama 和本地运行的 DeepSeek 模型组合在一起,才发现这个方案不仅解决了隐私顾虑,更打开了一扇通往高度个性化、可编程智能家居的大门。这篇文章,就是记录我如何一步步搭建这个“家庭AI大脑”的完整过程,面向那些已经玩过本地模型,但想让模型真正“动手做事”的开发者。
1. 环境搭建与核心组件解析
在开始写代码之前,我们需要理清整个技术栈的角色和它们之间的协作关系。这绝不是简单的 API 调用,而是一个微服务架构下的智能体(Agent)系统。
Ollama 扮演的是模型运行时的角色。你可以把它理解为一个轻量级的模型容器,它负责加载、运行我们本地的 DeepSeek 或其他大语言模型,并通过一个标准的 HTTP 接口提供服务。它的优势在于简化了模型部署的复杂性,一条命令就能让模型跑起来。
Spring AI 是 Spring 生态为 AI 应用提供的一整套抽象和工具集。它最核心的价值在于“标准化”。无论是调用 OpenAI 的 GPT,还是通过 Ollama 调用本地模型,亦或是未来接入新的模型提供商,Spring AI 都试图用一套统一的 API 和编程模型来屏蔽底层差异。这对于我们构建稳定、可迁移的应用至关重要。
而我们的智能家居控制应用,则是一个标准的 Spring Boot 服务。它利用 Spring AI 的能力与 Ollama 中的模型对话,并在此基础上,增加了“工具调用”(Tool Calling)的逻辑。所谓工具,就是一个个具体的 Java 方法,比如 turnOnLight(String room)、setThermostatTemperature(int degrees)。我们的目标是让模型学会在恰当的时机,选择并调用这些工具。
注意:本文假设你已在本地安装并运行 Ollama,且已拉取 DeepSeek 模型(如
deepseek-r1:8b)。如果尚未完成,请先执行ollama run deepseek-r1:8b进行准备。
1.1 初始化 Spring Boot 项目
我习惯使用 start.spring.io 来快速初始化项目。依赖选择上,除了基础的 Spring Web,核心就是 Spring AI 的相关模块。
<!-- pom.xml 关键依赖 -->
<dependencies>
<!- Web 框架 ->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!- Spring AI 核心,提供与模型交互的通用接口 ->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-starter</artifactId>
</dependency>
<!- Spring AI 对 Ollama 后端的支持 ->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
<!- 可选,用于更便捷的工具调用支持 ->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tool-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<!-- 必须引入 Spring AI 的 BOM 以管理版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M5</version> <!-- 请使用最新稳定版 -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
对应的 application.yml 配置如下。这里明确指定了 Ollama 服务的地址和我们要使用的模型。
# application.yml
spring:
ai:
ollama:
base-url: http://localhost:11434 # Ollama 默认端口
chat:
model: deepseek-r1:8b # 使用的模型名称
tool:
enabled: true # 启用工具调用功能
2. 定义智能家居的“工具集”
这是整个项目的灵魂所在。模型本身并不知道如何关灯,它需要我们去定义这些能力,并以一种模型能理解的方式“告诉”它。在 Spring AI 中,我们可以通过 @Tool 注解将普通的 Java 方法暴露为 AI 可调用的工具。
我首先创建了一个 HomeAutomationService 类,它模拟了智能家居中常见的控制功能。在实际生产中,这些方法内部会是调用 Philips Hue、米家或 Home Assistant 等平台的真实 HTTP API 或 SDK。
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
@Service
public class HomeAutomationService {
// 工具1:控制灯光
@Tool(description = "根据房间名称和指令控制灯光。指令可以是 'on', 'off', 'dim', 'bright'。")
public String controlLight(String room, String command) {
// 模拟控制逻辑
String action = switch (command.toLowerCase()) {
case "on" -> "打开";
case "off" -> "关闭";
case "dim" -> "调暗";
case "bright" -> "调亮";
default -> "执行未知操作于";
};
String result = String.format("已%s%s的灯光。", action, room);
System.out.println("[智能家居执行] " + result);
// 此处应替换为真实的硬件API调用
return result;
}
// 工具2:调整恒温器温度
@Tool(description = "设置指定房间恒温器的目标温度,单位为摄氏度。")
public String setThermostat(String room, double temperatureCelsius) {
if (temperatureCelsius < 16 || temperatureCelsius > 30) {
return "错误:温度设置超出舒适范围 (16°C - 30°C)。";
}
String result = String.format("已将%s的空调温度设置为 %.1f°C。", room, temperatureCelsius);
System.out.println("[智能家居执行] " + result);
return result;
}
// 工具3:控制窗帘
@Tool(description = "控制指定房间的窗帘。状态可选 'open'(打开), 'close'(关闭), 'half'(半开)。")
public String controlCurtain(String room, String state) {
String stateZh = switch (state.toLowerCase()) {
case "open" -> "打开";
case "close" -> "关闭";
case "half" -> "半开";
default -> state;
};
String result = String.format("已将%s的窗帘设置为%s状态。", room, stateZh);
System.out.println("[智能家居执行] " + result);
return result;
}
// 工具4:获取设备状态(查询类工具)
@Tool(description = "获取指定房间所有智能设备的当前状态概览。")
public String getRoomStatus(String room) {
// 模拟返回状态
return String.format("%s状态:灯光-关闭,空调-26°C,窗帘-关闭。", room);
}
}
关键点在于 @Tool 注解中的 description。这个描述至关重要,因为模型主要依靠这段文本来理解工具的用途、输入参数的意义。描述要清晰、准确,使用模型易于理解的自然语言。
3. 构建AI智能体并处理对话逻辑
有了工具,下一步就是创建一个“智能体”(Agent),它负责接收用户的自然语言请求,协调模型进行思考、决定是否调用工具以及如何调用。我创建了一个 HomeAssistantController 来处理用户交互。
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/assistant")
public class HomeAssistantController {
private final ChatClient chatClient;
// 通过构造器注入 ChatClient,Spring AI 会自动配置其连接 Ollama
@Autowired
public HomeAssistantController(ChatClient.Builder chatClientBuilder) {
// 构建 ChatClient 时,注入系统提示词(System Prompt),这是指导模型行为的关键
String systemPrompt = """
你是一个专业的智能家居控制助手。
你的核心能力是理解用户对家居设备的控制意图,并调用相应的工具函数来执行操作。
用户可能会用模糊或口语化的方式表达,你需要准确解析其意图。
例如:
- “我有点热” -> 可能意图是调低空调温度。
- “客厅太亮了” -> 可能意图是调暗或关闭客厅灯光。
- “我想看看窗外” -> 可能意图是打开窗帘。
在调用工具前,请确保你理解了房间和设备信息。
如果用户意图不明确,请礼貌地询问澄清。
所有工具调用结果将自动返回给你,你需要将其整合成对用户友好的回复。
""";
this.chatClient = chatClientBuilder
.system(s -> s.text(systemPrompt))
.defaultTools("homeAutomationService") // 启用我们定义的所有工具
.build();
}
@PostMapping("/chat")
public String handleUserCommand(@RequestBody Map<String, String> request) {
String userMessage = request.get("message");
if (userMessage == null || userMessage.isBlank()) {
return "请发送有效的指令。";
}
// 使用 ChatClient 发起对话。模型会根据系统提示和用户输入,自动判断是否需要调用工具。
Generation generation = chatClient.prompt()
.user(userMessage)
.call()
.content();
return generation.getText();
}
}
这里有一个非常重要的概念:系统提示词(System Prompt)。它就像是给模型的一份岗位说明书,定义了它的角色、行为规范和能力范围。一份好的系统提示词能极大提升模型在特定任务上的表现。我的提示词明确了其作为“智能家居控制助手”的身份,并给出了一些意图解析的例子。
3.1 工具调用的内部流程解析
当用户发送“把卧室的灯关上”时,背后发生了什么?理解这个过程对调试和优化至关重要。
- 请求发送:用户消息通过 Controller 传递给
ChatClient。 - 模型推理:ChatClient 将系统提示词和用户消息组合,发送给 Ollama 中的 DeepSeek 模型。
- 工具识别:模型分析消息后,判断需要调用
controlLight工具。它不会直接执行代码,而是返回一个结构化的“工具调用请求”,包含工具名和参数({“room”: “卧室”, “command”: “off”})。 - 工具执行:Spring AI 框架拦截到这个请求,通过反射找到
homeAutomationService.controlLight(“卧室”, “off”)方法并执行。 - 结果整合:工具执行的结果(“已关闭卧室的灯光。”)被自动送回到模型上下文中。
- 最终回复生成:模型收到工具执行结果,生成一段面向用户的自然语言回复,例如:“好的,已经为您关闭了卧室的灯光。”
整个过程是自动的,我们只需要定义好工具和提示词。你可以通过开启调试日志来观察这个流程:
# application.yml 追加日志配置
logging:
level:
org.springframework.ai: DEBUG
4. 进阶:实现复杂意图与场景联动
基础的控制实现后,我们可以追求更智能的体验。比如,用户说“我睡觉了”,系统应该能执行一系列操作:关闭所有灯光、将空调设为睡眠模式、关闭窗帘。这需要模型具备规划能力。
4.1 实现场景化工具
我们可以创建一个高级工具,直接对应一个场景。
@Service
public class AdvancedSceneService {
@Tool(description = “执行‘睡眠’场景。这将关闭指定房间的灯光,将空调设置为26°C的舒适睡眠温度,并关闭窗帘。”)
public String activateSleepScene(String room) {
StringBuilder result = new StringBuilder();
// 顺序执行一系列操作
result.append(controlLight(room, “off”)).append(“\n”);
result.append(setThermostat(room, 26.0)).append(“\n”);
result.append(controlCurtain(room, “close”));
return result.toString();
}
// ... 假设这里可以调用前面定义的 HomeAutomationService 中的方法
}
然后,在系统提示词中补充:“当用户表达‘睡觉’、‘晚安’、‘就寝’等意图时,优先考虑调用‘睡眠场景’工具。”
4.2 处理模糊意图与上下文记忆
现实中的指令往往是模糊的。“调高一点”这样的指令,需要模型记住上下文(上次设置的温度是多少?)。Spring AI 提供了 ChatMemory 组件来支持对话历史管理。
我们可以改造 Controller,为每个会话引入记忆:
import org.springframework.ai.chat.memory.InMemoryChatMemory;
// 在构造器中或每次请求时关联记忆
public String handleUserCommand(@RequestBody Map<String, String> request, @RequestHeader(“Session-Id”) String sessionId) {
// 为每个 sessionId 创建或获取一个独立的记忆体
ChatMemory chatMemory = getOrCreateMemory(sessionId);
Generation generation = chatClient.prompt()
.user(userMessage)
.chatMemory(chatMemory) // 关联记忆
.call()
.content();
return generation.getText();
}
这样,当用户先说“把客厅设为25度”,再说“太冷了,调高一点”时,模型能从记忆里知道之前的温度是25,从而推断出“调高一点”意味着调到26或27度。
4.3 安全与权限考量
让 AI 控制物理设备,安全是第一位的。我们必须在工具执行前加入权限校验。
@Tool(description = “控制灯光...”)
public String controlLight(String room, String command, @Header(“User-Id”) String userId) {
// 1. 验证用户是否有权限控制这个房间的设备
if (!permissionService.canControlRoom(userId, room)) {
return “权限拒绝:您无权控制” + room + “的设备。”;
}
// 2. 校验指令是否安全(例如,防止频繁开关)
if (!safetyCheckService.isSafeOperation(“light”, room, command)) {
return “安全限制:此操作被阻止。”;
}
// 3. 执行实际操作
// ...
// 4. 记录审计日志
auditService.logOperation(userId, “controlLight”, room, command);
return result;
}
我们可以通过自定义 ChatClient 的请求拦截器,将用户身份信息(如从 JWT 令牌解析)作为 Header 注入到每一次工具调用中。
5. 项目优化与部署实践
一个原型跑起来后,要让它变得健壮、可用,还需要很多工程化工作。
5.1 配置管理
将设备与房间的映射关系、模型参数等外部化到配置文件中。
home:
automation:
rooms:
- name: 客厅
light-entity-id: light.living_room
thermostat-entity-id: climate.living_room
- name: 卧室
light-entity-id: light.bedroom
spring:
ai:
ollama:
chat:
options:
temperature: 0.2 # 降低随机性,使工具调用更稳定
top-p: 0.9
5.2 异常处理与模型降级
网络可能不稳定,Ollama 服务可能重启。我们需要优雅地处理这些情况。
@RestControllerAdvice
public class AiAssistantExceptionHandler {
@ExceptionHandler(ResourceAccessException.class)
public ResponseEntity<String> handleOllamaConnectionFailed(ResourceAccessException e) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(“智能家居大脑暂时离线,请检查本地模型服务。”);
}
// 当工具调用因模型理解错误而失败时,提供更友好的信息
@ExceptionHandler(ToolExecutionException.class)
public ResponseEntity<String> handleToolExecutionError(ToolExecutionException e) {
log.warn(“模型尝试调用工具时出错:”, e);
return ResponseEntity.badRequest()
.body(“抱歉,我未能正确处理您的指令。请尝试换一种说法。”);
}
}
5.3 性能监控与评估
我们需要知道系统的表现如何。可以添加指标收集:
- 工具调用准确率:模型在应该调用工具时,是否成功调用了正确的工具?
- 意图识别准确率:对于模糊指令,模型的解析是否符合用户真实意图?
- 响应延迟:从用户发送消息到收到回复的总耗时,主要瓶颈在模型推理还是网络?
可以使用 Micrometer 集成 Prometheus 和 Grafana 来可视化这些指标。例如,记录每次对话的耗时:
@Around(“@within(org.springframework.web.bind.annotation.RestController)”)
public Object monitorAiRequest(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long duration = System.currentTimeMillis() - start;
Metrics.timer(“ai.assistant.response.time”).record(duration, TimeUnit.MILLISECONDS);
}
}
最后,在部署时,可以考虑将 Spring Boot 应用和 Ollama 打包到同一个 Docker Compose 环境中,实现一键部署。同时,为 Ollama 配置 GPU 加速(如果硬件支持),能显著提升模型响应速度。
整个项目搭建下来,最深的体会是,技术组合本身只是基础,真正的智能来自于对场景的深度理解和对细节的精心打磨。比如,那个描述清晰的 @Tool 注解,那段精心编写的系统提示词,往往比调参更能提升效果。我的书房现在运行着这套系统,从最初的简单开关灯,到现在能理解“我要看电影了”这种复杂场景并自动调整灯光、关闭窗帘,这个过程充满了探索的乐趣。如果你也在尝试,不妨从一个小房间、一两个设备开始,逐步迭代,最重要的是,享受这个让你家真正“变聪明”的过程。
更多推荐


所有评论(0)