摘要

很多 Java 开发者都听过 Spring AI,但真正动手跑过 Demo 的并不多。这篇文章不空聊概念,直接用 Spring Boot + Spring AI + DeepSeek 从 0 跑通一个最简单的 AI 对话接口,包含项目创建、依赖配置、API Key 配置、Controller 编写、接口测试和常见坑处理。

前言

最近我准备系统研究一下 Spring AI。

说实话,一开始看到 Spring AI、RAG、Agent、Tools、向量库这些词的时候,很容易有点懵。

但我后来想了一下,学这种新东西,第一步最好别上来就看太多概念。

最稳的方式其实很简单:

先跑起来。

你先把第一个 Demo 跑通,再回过头去理解它为什么这么写,会舒服很多。

这篇文章就不讲太多高大上的东西了,目标只有一个:

用 Spring Boot + Spring AI + DeepSeek 跑通第一个对话接口。

为什么这里用 DeepSeek?

主要是因为国内很多开发者接 OpenAI 不太方便,而 DeepSeek 对国内用户更友好一些。Spring AI 官方也提供了 DeepSeek Chat 的接入支持,配置项里可以直接使用 spring.ai.deepseek.api-key,模型可以使用 deepseek-chatdeepseek-reasoner

这篇跑通以后,后面再继续学:

  • Prompt 怎么写
  • 结构化输出怎么做
  • Tools 怎么接
  • RAG 怎么玩

就不会那么虚了。


一、这篇文章最终要实现什么

我们这篇不做复杂功能,只做一个最小闭环。

最终效果是:

用户访问接口:

http://localhost:8080/ai/chat?message=用大白话介绍一下Spring AI

后端调用 DeepSeek 模型,然后返回一段回答。

类似这样:

Spring AI 可以理解成 Spring 生态里接入大模型能力的一套工具。对于 Java 开发者来说,它可以让你用熟悉的 Spring Boot 方式,把 AI 对话、提示词、工具调用等能力接进项目里。

这就够了。


二、准备工作

开始之前,先准备这些东西。

1. JDK

建议使用:

JDK 17+

Spring Boot 3.x 本身就要求 Java 17 起步,所以这里直接用 JDK 17 比较稳。

如果你的机器上有多个 jdk 版本的话那就切换到对应的 jdk 版本

查看当前所使用的 jdk 版本

2. Maven

本篇使用 Maven 示例。

你可以先确认一下本地 Maven 是否正常:

mvn -v

3. Spring Boot

这里使用 Spring Boot 3.x。

我下面示例用的是:

Spring Boot 3.5.0
Spring AI 1.0.0

Spring AI 官方文档里也提到,Spring AI 1.0.0 支持 Spring Boot 3.4.x 和 3.5.x。

4. DeepSeek API Key

你需要先去 DeepSeek 平台申请一个 API Key。

拿到之后,后面会配置成环境变量:

DEEPSEEK_API_KEY


三、创建一个 Spring Boot 项目

你可以用 Spring Initializr 创建项目,也可以自己手动建。

如果用 Spring Initializr,建议先选这些:

  • Spring Boot 3.x
  • Java 17
  • Maven
  • Spring Web

项目名称可以叫:

spring-ai-deepseek-demo

包名示例:

com.itlay.demo

先不加数据库、不加 Redis、不加复杂依赖。

越简单越好。


四、配置 pom.xml

下面是一个完整的 pom.xml 示例。

小伙伴们可以直接参考。

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.0</version>
        <relativePath/>
    </parent>

    <groupId>com.itlay</groupId>
    <artifactId>spring-ai-deepseek-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-ai-deepseek-demo</name>
    <description>Spring AI DeepSeek Demo</description>

    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.0.0</spring-ai.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- Web 接口依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring AI DeepSeek 支持 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-deepseek</artifactId>
        </dependency>

        <!-- 测试依赖,可选 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Spring Boot 打包插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

这里重点看两个地方。

第一个是 Spring AI BOM:

<artifactId>spring-ai-bom</artifactId>

它主要是用来统一 Spring AI 相关依赖版本。

第二个是 DeepSeek starter:

<artifactId>spring-ai-starter-model-deepseek</artifactId>

这个就是我们本篇接入 DeepSeek 的核心依赖。Spring AI 官方文档里 DeepSeek Chat 也是通过 DeepSeek 相关配置接入的。


五、配置 application.yml

接下来配置 application.yml

server:
  port: 8080

spring:
  ai:
    deepseek:
      api-key: ${DEEPSEEK_API_KEY}
      chat:
        options:
          model: deepseek-chat

这里先保持最小配置。

重点有两个:

api-key: ${DEEPSEEK_API_KEY}

表示从环境变量里读取 DeepSeek API Key。

model: deepseek-chat

表示使用 DeepSeek 的 deepseek-chat 模型。

Spring AI DeepSeek 文档中也说明,spring.ai.deepseek.chat.options.model 可以使用 deepseek-chatdeepseek-reasoner,默认模型为 deepseek-chat。 


六、配置 DeepSeek API Key

这里建议不要直接把 Key 写死在配置文件里。

本地开发可以先临时配置环境变量。

Mac / Linux

export DEEPSEEK_API_KEY=你的DeepSeekKey

然后在同一个终端里启动项目:

mvn spring-boot:run

Windows PowerShell

$env:DEEPSEEK_API_KEY="你的DeepSeekKey"

然后启动项目:

mvn spring-boot:run

如果你只是本地测试,临时写在 application.yml 里也能跑。

比如:

spring:
  ai:
    deepseek:
      api-key: sk-xxxxxx

Tips:但这种方式不建议提交到 Git,API Key 属于敏感信息,别直接放到公开仓库里。


七、创建启动类

新建启动类:

package com.itlay.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringAiDeepseekDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringAiDeepseekDemoApplication.class, args);
    }
}

这部分跟普通 Spring Boot 项目没区别。


八、写第一个对话接口

接下来写最关键的 Controller。

新建:

com.itlay.demo.controller.ChatController

代码如下:

package com.itlay.demo.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return this.chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

这段代码很短,但已经完成了一个完整的 AI 对话接口。

我们拆开看一下。

1. 注入 ChatClient.Builder

public ChatController(ChatClient.Builder chatClientBuilder) {
    this.chatClient = chatClientBuilder.build();
}

ChatClient 是 Spring AI 里比较常用的对话客户端,官方文档也给了通过 ChatClient.Builder 构建客户端的示例。 

你可以先把它理解成:

用来帮我们向模型发起对话请求的客户端。

2. 构造 Prompt

this.chatClient.prompt()

这里表示开始构造一次对话请求。

3. 传入用户问题

.user(message)

这里把接口接收到的 message 传给模型。

4. 发起调用

.call()

这里才是真正发起请求。

5. 获取文本结果

.content()

这里拿到模型返回的文本内容。

整个链路就是:


九、启动项目测试

启动项目:

mvn spring-boot:run

控制台正常启动后,浏览器访问:

http://localhost:8080/ai/chat?message=用大白话介绍一下Spring AI

也可以用 curl:

curl "http://localhost:8080/ai/chat?message=用大白话介绍一下Spring AI"

如果一切正常,你会看到类似这样的回答:

Spring AI 可以理解成 Spring 生态里接入大模型能力的一套工具。对于 Java 开发者来说,它可以让你用熟悉的 Spring Boot 方式,把 AI 对话、提示词、工具调用等能力接进项目里。

到这一步,第一个 Spring AI + DeepSeek 对话接口就跑通了。

这一步特别重要。

因为很多人学新东西,卡的不是后面多高级的部分,而是第一步就没跑起来。


十、加一个 System Prompt,让回答更稳定一点

刚才那个接口已经能用了。

但它的问题是,模型回答风格不太受控。

比如同样的问题,它可能有时候回答很长,有时候回答很散。

我们可以加一个 system 提示词,告诉模型它应该怎么回答。

新增一个接口:

@GetMapping("/chat2")
public String chat2(@RequestParam String message) {
    return this.chatClient.prompt()
            .system("你是一个面向 Java 开发者的 Spring AI 入门助手。回答要求:简洁、清楚、少说空话,尽量用大白话解释。")
            .user(message)
            .call()
            .content();
}

完整 Controller 变成这样:

package com.itlay.demo.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return this.chatClient.prompt()
                .user(message)
                .call()
                .content();
    }

    @GetMapping("/chat2")
    public String chat2(@RequestParam String message) {
        return this.chatClient.prompt()
                .system("你是一个面向 Java 开发者的 Spring AI 入门助手。回答要求:简洁、清楚、少说空话,尽量用大白话解释。")
                .user(message)
                .call()
                .content();
    }
}

访问:

http://localhost:8080/ai/chat2?message=Spring AI适合什么人学

这个接口相比第一个接口,多了一个系统提示词。

你可以先简单理解成:

system 用来约束模型角色和回答风格。
user 是用户真正提出的问题。

这一步虽然简单,但已经开始接近实际项目里的 Prompt 设计了。


十一、再写一个 POST 接口,更像真实项目

GET 接口适合演示。

但真实项目里,一般更常用 POST 请求。

比如前端传一个 JSON:

{
  "message": "请用三句话解释一下 Spring AI"
}

我们先创建一个请求对象。

ChatRequest.java

package com.itlay.demo.dto;

public class ChatRequest {

    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

然后 Controller 里加一个 POST 接口。

@PostMapping("/chat")
public String chatPost(@RequestBody ChatRequest request) {
    return this.chatClient.prompt()
            .system("你是一个 Java 后端技术助手,回答尽量简洁、清楚。")
            .user(request.getMessage())
            .call()
            .content();
}

完整 Controller:

package com.itlay.demo.controller;

import com.itlay.demo.dto.ChatRequest;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return this.chatClient.prompt()
                .user(message)
                .call()
                .content();
    }

    @GetMapping("/chat2")
    public String chat2(@RequestParam String message) {
        return this.chatClient.prompt()
                .system("你是一个面向 Java 开发者的 Spring AI 入门助手。回答要求:简洁、清楚、少说空话,尽量用大白话解释。")
                .user(message)
                .call()
                .content();
    }

    @PostMapping("/chat")
    public String chatPost(@RequestBody ChatRequest request) {
        return this.chatClient.prompt()
                .system("你是一个 Java 后端技术助手,回答尽量简洁、清楚。")
                .user(request.getMessage())
                .call()
                .content();
    }
}

这个版本就更像真实项目里会用的接口了。


十二、给接口返回一个统一结构

上面的接口直接返回字符串,演示可以。

但真实项目里,我们一般不会直接返回一段文本,而是会包一层统一响应。

比如:

{
  "success": true,
  "data": "模型回答内容"
}

先建一个通用返回类。

ApiResponse.java

package com.itlay.demo.dto;

public class ApiResponse<T> {

    private boolean success;

    private T data;

    private String message;

    public ApiResponse() {
    }

    public ApiResponse(boolean success, T data, String message) {
        this.success = success;
        this.data = data;
        this.message = message;
    }

    public static <T> ApiResponse<T> ok(T data) {
        return new ApiResponse<>(true, data, "success");
    }

    public static <T> ApiResponse<T> fail(String message) {
        return new ApiResponse<>(false, null, message);
    }

    public boolean isSuccess() {
        return success;
    }

    public T getData() {
        return data;
    }

    public String getMessage() {
        return message;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public void setData(T data) {
        this.data = data;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

然后把 POST 接口改一下:

@PostMapping("/chat-result")
public ApiResponse<String> chatResult(@RequestBody ChatRequest request) {
    String content = this.chatClient.prompt()
            .system("你是一个 Java 后端技术助手,回答尽量简洁、清楚。")
            .user(request.getMessage())
            .call()
            .content();

    return ApiResponse.ok(content);
}

这样返回格式就更像一个正常后端接口了。

返回示例:

{
  "success": true,
  "data": "Spring AI 更像是 Spring 生态里的统一接入层,可以让你用更熟悉的 Spring Boot 方式接入不同模型,而不是自己手写 HTTP 调用。",
  "message": "success"
}

十三、简单加一下参数校验

上面的代码还有个问题:

如果用户传空字符串,接口还是会去调用模型。

这就没必要了。

我们简单加一个校验。

@PostMapping("/chat-result")
public ApiResponse<String> chatResult(@RequestBody ChatRequest request) {
    if (request == null || request.getMessage() == null || request.getMessage().trim().isEmpty()) {
        return ApiResponse.fail("message 不能为空");
    }

    String content = this.chatClient.prompt()
            .system("你是一个 Java 后端技术助手,回答尽量简洁、清楚。")
            .user(request.getMessage())
            .call()
            .content();

    return ApiResponse.ok(content);
}

这点很小,但挺重要。

因为真实接口里,别让无效请求白白消耗模型调用。

后面如果再深入,还可以继续加:

  • 字符长度限制
  • 频率限制
  • 用户鉴权
  • 调用日志
  • 异常处理

但第一篇先不用展开太多。


十四、加一个简单异常处理

AI 接口调用并不是百分百成功。

可能会遇到:

  • API Key 错误
  • 网络超时
  • 模型不可用
  • 请求频率限制
  • 服务端异常

所以我们简单包一层异常处理。

@PostMapping("/chat-result")
public ApiResponse<String> chatResult(@RequestBody ChatRequest request) {
    if (request == null || request.getMessage() == null || request.getMessage().trim().isEmpty()) {
        return ApiResponse.fail("message 不能为空");
    }

    try {
        String content = this.chatClient.prompt()
                .system("你是一个 Java 后端技术助手,回答尽量简洁、清楚。")
                .user(request.getMessage())
                .call()
                .content();

        return ApiResponse.ok(content);
    } catch (Exception e) {
        return ApiResponse.fail("AI 服务调用失败:" + e.getMessage());
    }
}

这里为了演示简单,直接在 Controller 里 catch 了。

如果是正式项目,我更建议放到:

  • service 层
  • 全局异常处理器
  • 调用日志里

别让 Controller 越写越厚。


十五、稍微整理一下代码结构

如果你只是演示,上面写法够了。

但如果你想让代码更像项目一点,可以拆成 Service。

AiChatService.java

package com.itlay.demo.service;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class AiChatService {

    private final ChatClient chatClient;

    public AiChatService(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    public String chat(String message) {
        return this.chatClient.prompt()
                .system("你是一个 Java 后端技术助手,回答尽量简洁、清楚。")
                .user(message)
                .call()
                .content();
    }
}

ChatController.java

package com.itlay.demo.controller;

import com.itlay.demo.dto.ApiResponse;
import com.itlay.demo.dto.ChatRequest;
import com.itlay.demo.service.AiChatService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/ai")
public class ChatController {

    private final AiChatService aiChatService;

    public ChatController(AiChatService aiChatService) {
        this.aiChatService = aiChatService;
    }

    @PostMapping("/chat-result")
    public ApiResponse<String> chatResult(@RequestBody ChatRequest request) {
        if (request == null || request.getMessage() == null || request.getMessage().trim().isEmpty()) {
            return ApiResponse.fail("message 不能为空");
        }

        try {
            String content = aiChatService.chat(request.getMessage());
            return ApiResponse.ok(content);
        } catch (Exception e) {
            return ApiResponse.fail("AI 服务调用失败:" + e.getMessage());
        }
    }
}

这样结构会清楚一些:

controller:负责接收请求
service:负责调用 AI
dto:负责请求和响应对象

虽然只是一个小 Demo,但这个拆法更适合后面继续扩展。

十六、项目目录结构参考

最后目录大概是这样:

spring-ai-deepseek-demo
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── itlay
        │           └── demo
        │               ├── SpringAiDeepseekDemoApplication.java
        │               ├── controller
        │               │   └── ChatController.java
        │               ├── dto
        │               │   ├── ApiResponse.java
        │               │   └── ChatRequest.java
        │               └── service
        │                   └── AiChatService.java
        └── resources
            └── application.yml

这套结构不复杂,但已经比“所有代码写在 Controller 里”要舒服一点。


十七、第一次最容易踩的几个坑

1. API Key 没配成功

最常见。

如果报鉴权失败,先检查:

echo $DEEPSEEK_API_KEY

看看环境变量到底有没有生效。

如果你是 IDEA 启动项目,还要注意:

终端里配置的环境变量,不一定自动带到 IDEA 的运行配置里。

这种情况可以在 IDEA 的 Run Configuration 里手动配置环境变量。


2. 配置前缀写错

DeepSeek 原生接法用的是:

spring:
  ai:
    deepseek:
      api-key: xxx

不要写成:

spring:
  ai:
    openai:
      api-key: xxx

这是两套不同配置。

当然,DeepSeek 本身也提供 OpenAI 兼容接口,但这篇我们走的是 Spring AI 原生 DeepSeek 接法。


3. 模型名写错

本篇使用的是:

model: deepseek-chat

别一上来乱填模型名。

Spring AI DeepSeek 文档里也明确提到,模型 ID 可以使用 deepseek-chatdeepseek-reasoner


4. 网络问题

如果接口一直超时,别只盯着代码看。

先确认:

  • 当前机器能不能访问 DeepSeek API
  • 公司网络有没有限制
  • 本地代理有没有影响
  • API Key 是否有效

很多时候,新手第一次接模型服务,卡住的不是代码,而是网络和权限。


5. 版本不匹配

Spring AI 和 Spring Boot 版本最好不要乱搭。

我这篇示例用的是:

Spring Boot 3.5.0
Spring AI 1.0.0
JDK 17

你如果换版本,依赖名、配置项、API 都可能有差异。

新手第一篇建议先按一套版本跑通,再考虑升级。


十八、先不要搞太多东西

这篇我们只做了一件事:

跑通第一个 Spring AI + DeepSeek 对话接口。

不要一上来就搞:

  • RAG
  • 向量库
  • Agent
  • 多轮记忆
  • Tools
  • 流式输出

这些都可以做,但不是第一步。

第一步最重要的是建立信心:

原来 Spring Boot 接 AI 模型,也就是这么一步步配起来。

你跑通了这个 Demo,后面再看 Prompt、结构化输出、Tools,就不会那么虚。


总结

本篇文章我们用 Spring Boot + Spring AI + DeepSeek 跑通了一个最小对话接口。

主要做了这些事:

  • 创建 Spring Boot 项目
  • 引入 Spring AI DeepSeek 依赖
  • 配置 DeepSeek API Key
  • 使用 ChatClient 调用模型
  • 写了 GET 和 POST 两种接口
  • 做了简单的统一返回和参数校验
  • 补了一些常见坑

其实整体看下来,你会发现第一步并没有想象中那么复杂。

对 Java 开发者来说,Spring AI 的好处就在于:

它让你可以继续用熟悉的 Spring Boot 方式,把 AI 能力接进项目里。

这篇先把门打开,大家加油:)

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐