【Spring AI 实战】二、5 分钟上手 ChatClient,对话模型调用就这么简单

作者:Spring AI 系列专题 | 更新时间:2025-04

所属阶段:第一阶段·核心基础

前置知识:建议先阅读第一篇,了解 Spring AI 的整体定位与模型抽象。

适用版本:本文保留 Spring AI 1.0.0-M4 的入门写法;如果你使用 1.0 正式版,优先采用 spring-ai-openai-spring-boot-starter 等 starter 依赖。

本文导航


1. 前置条件

1.1 环境要求

  • JDK 17+(Spring AI 要求)
  • Spring Boot 3.2+
  • 可用的 OpenAI API Key(或任何支持的模型 API Key)

1.2 项目创建

推荐使用 Spring Initializr 创建项目,勾选:


✅ Spring Web

✅ Spring Boot DevTools

然后在 pom.xml 中加入:


<dependencyManagement>

    <dependencies>

        <dependency>

            <groupId>org.springframework.ai</groupId>

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

            <version>1.0.0-M4</version>

            <type>pom</type>

            <scope>import</scope>

        </dependency>

    </dependencies>

</dependencyManagement>



<dependencies>

    <dependency>

        <groupId>org.springframework.ai</groupId>

        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>

    </dependency>

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-web</artifactId>

    </dependency>

</dependencies>

1.3 配置 API Key

方式一:环境变量(推荐)


export OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx

方式二:application.yml


spring:

  ai:

    openai:

      api-key: ${OPENAI_API_KEY:sk-your-key-here}

      base-url: https://api.openai.com

      chat:

        options:

          model: gpt-4o-mini

          temperature: 0.7

⚠️ 生产环境务必使用环境变量或 Vault,不要将 API Key 硬编码在配置文件中。


2. ChatClient 核心 API 详解

2.1 ChatClient 是什么

ChatClient 是 Spring AI 1.0 推出的全新对话 API,设计灵感来自 RestClientJdbcTemplate,特点是:流式 API + Builder 模式 + 类型安全


// 使用方式极其简洁

chatClient.prompt()

          .user("你好")

          .call()

          .content();

2.2 核心组件

![ChatClient API 四种调用方式](…/blog/images/02-chatclient-invocation-modes.png null)

| 组件 | 说明 |

|------|------|

| ChatClient | 主入口,通过 prompt().user().call() 链式调用 |

| Prompt | 对话提示词,包含 user message / system message |

| UserPrompt | 用户消息,构建 .user() |

| SystemPrompt | 系统提示词,构建 .system() |

| ChatResponse | 响应对象,包含 content、metadata 等 |

2.3 工作流程


Application Code

       │

       ▼

chatClient.prompt()

       │

       ▼

Prompt(user message, system message, ...)

       │

       ▼

[OpenAiChatModel / AnthropicChatModel / ...]  ← 自动注入的模型实现

       │

       ▼

HTTP POST to AI Provider API

       │

       ▼

ChatResponse

       │

       ▼

.content() / .entity() / .document()


3. 四种调用方式实战

在这里插入图片描述

3.1 同步调用(最常用)


@Service

public class AiService {



    private final ChatClient chatClient;



    public AiService(ChatClient.Builder builder) {

        this.chatClient = builder.build();

    }



    /**

     * 最简单的同步调用

     */

    public String ask(String question) {

        return chatClient.prompt()

                .user(question)

                .call()

                .content();

    }



    /**

     * 带系统提示词的同步调用

     */

    public String askWithContext(String question) {

        return chatClient.prompt()

                .system("你是一位资深的 Java 后端工程师,用简洁专业的语言回答问题。")

                .user(question)

                .call()

                .content();

    }

}

3.2 流式调用(适合实时输出)

流式调用的核心优势:首 token 延迟低,用户体验好,适合 AI 助手类应用


/**

 * 流式调用 - 返回 Flux<分段内容>

 */

public Flux<String> askStream(String question) {

    return chatClient.prompt()

            .user(question)

            .stream()

            .content();

}



/**

 * 在 Controller 中返回流式响应(SSE)

 */

@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)

public Flux<String> chatStream(@RequestParam String question) {

    return aiService.askStream(question);

}

实际效果:AI 是一边"思考"一边输出字符,而不是等全部生成完再一次性返回。


// 后端推送到前端的 SSE 格式

/*

data: 你好

data: ,

data: 我

data: 是

data: Spring

data: AI

data: 。



data: [DONE]

*/

3.3 带结构化参数调用


/**

 * 控制模型的温度(创造性)和最大 token 数

 */

public String askWithOptions(String question) {

    return chatClient.prompt()

            .user(question)

            .options(

                ChatOptionsBuilder.builder()

                    .withTemperature(0.3)       // 降低随机性,更确定性

                    .withMaxTokens(500)          // 限制输出长度

                    .withTopP(0.9)               // 控制采样范围

                    .build()

            )

            .call()

            .content();

}

常见参数说明:

| 参数 | 范围 | 说明 |

|------|------|------|

| temperature | 0.0~2.0 | 随机性,越低越确定性,默认 0.7 |

| maxTokens | 1~4096 | 最大生成 token 数,限制响应长度 |

| topP | 0.0~1.0 | 核采样,控制输出多样性 |

| frequencyPenalty | -2.0~2.0 | 频率惩罚,减少重复 |

| presencePenalty | -2.0~2.0 | 在场惩罚,鼓励话题扩展 |

3.4 返回结构化对象(强类型响应)

Spring AI 支持让模型直接返回 Java 对象,而不只是字符串:


// 定义响应结构

public record WeatherInfo(String city, String condition, double temperature) {}



// 服务层

public WeatherInfo getWeather(String city) {

    return chatClient.prompt()

            .user("请查询" + city + "今天的天气,用 JSON 格式返回,包含 city、condition、temperature 字段")

            .call()

            .entity(WeatherInfo.class);  // 自动解析为 Java 对象

}



// 输出

// WeatherInfo[city=北京, condition=晴, temperature=23.5]

配合 @JsonProperty 可以映射更复杂的结构:


public record ApiResponse<T>(

    int code,

    String message,

    T data

) {}



public ApiResponse<UserProfile> getUserProfile(Long userId) {

    return chatClient.prompt()

            .user("查询用户 ID=" + userId + " 的个人信息")

            .call()

            .entity(new ParameterizedTypeReference<ApiResponse<UserProfile>>() {});

}


4. 多轮对话实现

4.1 简单的内存级对话


@Service

public class ChatSessionService {



    private final Map<String, List<Message>> sessions = new ConcurrentHashMap<>();



    private final ChatClient chatClient;



    public ChatSessionService(ChatClient.Builder builder) {

        this.chatClient = builder.build();

    }



    /**

     * 多轮对话:每次携带历史消息

     */

    public String chat(String sessionId, String userMessage) {

        // 获取或创建会话历史

        List<Message> history = sessions.computeIfAbsent(

            sessionId, k -> new ArrayList<>()

        );



        // 构建 Prompt(携带历史)

        Prompt prompt = new Prompt(

            MessageBuilder.createMessage(

                MessageType.USER,

                userMessage,

                new MediaContent(MediaType.TEXT_PLAIN, userMessage)

            )

        );



        // 如果有系统提示词,先加进去

        if (history.isEmpty()) {

            prompt.getOptions().setSystem("你是智能助手小 Spring。");

        }



        // 实际使用 Advisors 更优雅(见下节),这里演示手动方式

        return chatClient.prompt(prompt).call().content();

    }

}

4.2 使用 Advisors 实现对话记忆(推荐)

Advisors 是 Spring AI 的拦截器链,用法类似 Spring AOP,是实现对话记忆的标准方式


@Service

public class AdvisorChatService {



    private final ChatClient chatClient;



    // 默认开启 20 轮对话记忆

    public AdvisorChatService(ChatClient.Builder builder) {

        this.chatClient = builder

                .defaultSystem("你是一位乐于助人的 Java 技术顾问")

                .build();

    }



    /**

     * 基于 Advisors 的多轮对话(自动管理历史)

     */

    public String chatWithMemory(String sessionId, String question) {

        return chatClient.prompt()

                .user(question)

                .advisors(

                    // 对话记忆 Advisors:自动保存并注入历史消息

                    MessageChatAdvisor.builder()

                        .chatMemory(

                            new InMemoryChatMemory()  // 内存存储,生产环境换 Redis

                        )

                        .sessionId(sessionId)       // 按会话 ID 隔离历史

                        .build()

                )

                .call()

                .content();

    }

}

MessageChatAdvisor 的工作原理:


用户: 第一个问题

       ↓

[MessageChatAdvisor] ← InMemoryChatMemory.getHistory(sessionId) → []

       ↓

调用模型,携带历史 []

       ↓

模型回复: 第一个回答

       ↓

[MessageChatAdvisor] ← InMemoryChatMemory.add(sessionId, user+assistant) 

       ↓

用户: 第二个问题(上下文相关)

       ↓

[MessageChatAdvisor] → InMemoryChatMemory.getHistory(sessionId) → [Q1, A1]

       ↓

调用模型,携带历史 [Q1, A1, Q2]

5. 系统提示词与参数控制

![ChatClient 参数控制一览](…/blog/images/02-chatclient-parameter-control.png null)

5.1 全局系统提示词


// 在构建 ChatClient 时设置全局系统提示词

@Bean

public ChatClient chatClient(ChatClient.Builder builder) {

    return builder

            .defaultSystem("你是一位专业的 Java 后端开发专家," +

                    "擅长 Spring Boot、Spring Cloud、Spring AI," +

                    "回答问题时优先给出代码示例," +

                    "用【技术要点】【代码示例】【运行说明】三段式结构回答。")

            .build();

}

5.2 每次调用的局部系统提示词


// 覆盖默认系统提示词

String reply = chatClient.prompt()

        .system(

            SystemPromptTemplate.from(

                "你是{role},用{style}风格回答用户问题。用户所在地:{location}"

            )

            .render(Map.of(

                "role", "宠物医生",

                "style", "专业但通俗易懂",

                "location", "北京"

            ))

        )

        .user("我的猫最近食欲不振怎么办?")

        .call()

        .content();

5.3 PromptTemplate 模板


// 定义一个 Prompt 模板

PromptTemplate promptTemplate = PromptTemplate.from(

    "请为以下{language}代码添加中文注释,并说明每段逻辑的作用:\n```\n{code}\n```"

);



// 渲染模板

Prompt prompt = promptTemplate.render(

    Map.of(

        "language", "Java",

        "code", "public class Demo { public static void main(String[] args) { System.out.println(\"Hello\"); } }"

    )

);



String result = chatClient.prompt(prompt).call().content();

6. 异常处理

6.1 Spring AI 异常体系


// 异常继承结构

SpringAIException (Root)

 ├── RateLimitException         // 速率限制

 ├── BadRequestException       // 参数错误(400)

 ├── AuthenticationException   // 认证失败(401)

 ├── AuthorizationException    // 权限不足(403)

 ├── ResourceNotFoundException  // 资源不存在(404)

 └── AIException               // AI 模型通用错误

6.2 全局异常处理


@RestControllerAdvice

public class GlobalExceptionHandler {



    @ExceptionHandler(RateLimitException.class)

    public ResponseEntity<Map<String, Object>> handleRateLimit(RateLimitException ex) {

        return ResponseEntity

                .status(HttpStatus.TOO_MANY_REQUESTS)

                .body(Map.of(

                    "error", "请求过于频繁,请稍后再试",

                    "retryAfter", 30

                ));

    }



    @ExceptionHandler(AuthenticationException.class)

    public ResponseEntity<Map<String, String>> handleAuth(AuthenticationException ex) {

        return ResponseEntity

                .status(HttpStatus.UNAUTHORIZED)

                .body(Map.of("error", "API Key 无效或已过期"));

    }



    @ExceptionHandler(AIException.class)

    public ResponseEntity<Map<String, String>> handleAI(AIException ex) {

        log.error("AI 调用异常", ex);

        return ResponseEntity

                .status(HttpStatus.BAD_GATEWAY)

                .body(Map.of("error", "AI 服务暂时不可用,请稍后重试"));

    }

}

7. 完整示例:从配置到 Controller

7.1 配置类


@Configuration

public class ChatConfig {



    @Value("${spring.ai.openai.api-key}")

    private String apiKey;



    @Bean

    public ChatClient chatClient(ChatClient.Builder builder) {

        return builder

                .defaultSystem("你是 AI 助手,请专业、简洁地回答用户问题。")

                .build();

    }

}

7.2 服务类


@Service

@RequiredArgsConstructor

public class AiChatService {



    private final ChatClient chatClient;



    public String chat(String message) {

        return chatClient.prompt()

                .user(message)

                .call()

                .content();

    }



    public Flux<String> chatStream(String message) {

        return chatClient.prompt()

                .user(message)

                .stream()

                .content();

    }

}

7.3 Controller


@RestController

@RequestMapping("/api/ai")

@RequiredArgsConstructor

public class AiController {



    private final AiChatService aiChatService;



    /**

     * 同步对话

     */

    @GetMapping("/chat")

    public ResponseEntity<Map<String, String>> chat(@RequestParam String message) {

        if (message == null || message.isBlank()) {

            return ResponseEntity.badRequest()

                    .body(Map.of("error", "消息不能为空"));

        }

        String reply = aiChatService.chat(message);

        return ResponseEntity.ok(Map.of(

            "reply", reply,

            "timestamp", String.valueOf(System.currentTimeMillis())

        ));

    }



    /**

     * 流式对话(SSE)

     */

    @GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)

    public Flux<String> chatStream(@RequestParam String message) {

        return aiChatService.chatStream(message);

    }

}

7.4 application.yml


spring:

  application:

    name: spring-ai-demo

  ai:

    openai:

      api-key: ${OPENAI_API_KEY}

      base-url: https://api.openai.com

      chat:

        options:

          model: gpt-4o-mini

          temperature: 0.7



server:

  port: 8080

7.5 启动测试


# 测试同步接口

curl "http://localhost:8080/api/ai/chat?message=Spring+AI+是什么"



# 测试流式接口

curl -N "http://localhost:8080/api/ai/chat/stream?message=用三句话介绍Java"

8. 常见问题排查

| 问题 | 原因 | 解决方案 |

|------|------|---------|

| 401 Unauthorized | API Key 错误或过期 | 检查 OPENAI_API_KEY 环境变量 |

| 429 Too Many Requests | 请求频率超限 | 添加限流或等待重试 |

| SocketTimeoutException | 网络超时 | 增大超时时间或检查网络 |

| 返回空字符串 | 模型未正确配置 | 检查 base-url 和 model |

| 流式无响应 | 前端未正确解析 SSE | 检查 Accept: text/event-stream |

| 响应乱码 | 编码问题 | 确保 UTF-8 配置正确 |


9. 小结

本文围绕 ChatClient 做了全面深入的实战讲解:

  1. 四种调用方式:同步调用、流式调用、带参数调用、结构化对象返回
  2. 多轮对话:手动管理历史 vs Advisors 自动管理(推荐)
  3. 系统提示词:全局默认 + 局部覆盖 + 模板渲染
  4. 异常处理:Spring AI 完整异常体系与全局处理
  5. 完整示例:从配置 → 服务类 → Controller → 测试

下一篇预告:【Spring AI 实战】三、Prompt 工程:模板化、结构化输出与 Advisors 顾问模式——我们将深入 Prompt 工程的核心技巧,包括 Few-Shot 提示、Chain of Thought 推理、Advisors 拦截器链的高级用法,以及如何引导模型输出严格符合预期的 JSON 结构。


📌 系列导航

📎 示例说明:本文以入门链路为主,若你准备做生产级接入,建议继续阅读第三篇和第四篇。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐