1. 概述

Spring Framework 通过 Spring AI 项目正式引入了生成式 AI 的强大能力。本教程将深入探讨在 Spring Boot 应用中集成生成式 AI 的核心方法,并帮助开发者掌握关键 AI 概念。

我们将解析 Spring AI 与 AI 模型的交互机制,并通过实际应用案例展示其功能特性。

2. Spring AI 核心概念

在开始前,先梳理几个关键领域术语和概念。

Spring AI 最初专注于处理语言输入并生成语言输出的模型。项目的核心思想是为开发者提供抽象接口,将生成式 API 作为独立组件集成到应用中。

典型抽象是 AiClient 接口,包含两个基础实现:OpenAI 和 Azure OpenAI:

public interface AiClient {
    default String generate(String message);
    AiResponse generate(Prompt prompt);
}

AiClient 提供两种生成方式:

  • 简化版 generate(String message):直接使用字符串输入输出,避免 PromptAiResponse 的复杂性
  • 高级版 generate(Prompt prompt):提供更精细的控制能力

2.1. 高级 PromptAiResponse

在 AI 领域,Prompt 指提供给 AI 的文本消息,包含上下文和问题,模型据此生成答案。

从 Spring AI 视角看,Prompt 是参数化 Message 的集合:

public class Prompt {
    private final List<Message> messages;
    // 构造方法和工具方法
}

public interface Message {
    String getContent();
    Map<String, Object> getProperties();
    MessageType getMessageType();
}

Prompt 让开发者能更精细控制文本输入。典型场景是提示模板(Prompt Templates),通过预定义文本和占位符构建,再用 Map<String, Object> 填充:

Tell me a {adjective} joke about {content}.

Message 接口还承载 AI 模型可处理的消息类型信息。例如 OpenAI 实现区分对话角色,通过 MessageType 映射。其他模型可能反映消息格式或自定义属性。官方文档 提供更多细节:

public class AiResponse {
    private final List<Generation> generations;
    // getter/setter
}

public class Generation {
    private final String text;
    private Map<String, Object> info;
}

AiResponseGeneration 对象列表组成,每个对象包含对应提示的输出。Generation 对象还提供 AI 响应的元数据信息。

⚠️ 注意:Spring AI 项目仍处于 beta 阶段,部分功能尚未完善。可通过 GitHub 仓库 跟踪进展。

3. Spring AI 快速上手

首先,AiClient 需要 OpenAI 平台的 API 密钥进行所有通信。需在 API Keys 页面 创建令牌。

Spring AI 定义了配置属性 spring.ai.openai.api-key,在 application.yml 中配置:

spring:
  ai:
    openai.api-key: ${OPEN_AI_KEY}

下一步是配置依赖仓库。Spring AI 项目在 Spring Milestone Repository 提供构件。

需添加仓库定义:

<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/snapshot</url>
        <releases>
            <enabled>false</enabled>
        </releases>
    </repository>
    <repository>
        <name>Central Portal Snapshots</name>
        <id>central-portal-snapshots</id>
        <url>https://central.sonatype.com/repository/maven-snapshots/</url>
        <releases>
            <enabled>false</enabled>
        </releases>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
</repositories>

然后导入 open-ai-spring-boot-starter

<dependency>
    <groupId>org.springframework.experimental.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0-M6</version>
</dependency>

✅ 提示:Spring AI 项目迭代迅速,建议定期查看 官方 GitHub 页面 获取最新版本。

现在开始实践应用。

4. Spring AI 实战演示

构建一个简单的 REST API 演示功能,包含两个接口:

  • /ai/cathaiku:实现基础 generate() 方法,返回关于猫的俳句(纯文本)
  • /ai/poetry?theme={{theme}}&genre={{genre}}:展示 PromptTemplateAiResponse 的高级能力

4.1. 在 Spring Boot 中注入 AiClient

从猫俳句接口开始。用 @RestController 注解创建 PoetryController

@RestController
@RequestMapping("ai")
public class PoetryController {
    private final PoetryService poetryService;

    // 构造器

    @GetMapping("/cathaiku")
    public ResponseEntity<String> generateHaiku(){
        return ResponseEntity.ok(poetryService.getCatHaiku());
    }
}

按 DDD 思想,服务层处理领域逻辑。只需将 AiClient 注入 PoetryService 即可调用 generate()。定义字符串提示词:

@Service
public class PoetryServiceImpl implements PoetryService {
    public static final String WRITE_ME_HAIKU_ABOUT_CAT = """
        Write me Haiku about cat,
        haiku should start with the word cat obligatory""";

    private final AiClient aiClient;

    // 构造器

    @Override
    public String getCatHaiku() {
        return aiClient.generate(WRITE_ME_HAIKU_ABOUT_CAT);
    }
}

接口已就绪,响应为纯文本:

Cat prowls in the night,
Whiskers twitch with keen delight,
Silent hunter's might.

❌ 踩坑点:当前方案存在两个问题:

  1. 纯文本响应不符合 REST API 最佳实践
  2. 固定提示词缺乏灵活性

下一步添加参数化支持:主题(theme)和体裁(genre),这正是 PromptTemplate 的用武之地。

4.2. 使用 PromptTemplate 实现动态查询

PromptTemplate 本质类似 StringBuilder 和字典的组合。与 /cathaiku 类似,先定义提示词模板,但这次使用占位符:

String promptString = """
    Write me {genre} poetry about {theme}
    """;
PromptTemplate promptTemplate = new PromptTemplate(promptString);
promptTemplate.add("genre", genre);
promptTemplate.add("theme", theme);

为标准化接口输出,创建 简单记录类 PoetryDto

public record PoetryDto (String title, String poetry, String genre, String theme){}

PoetryDto 注册到 BeanOutputParser,用于序列化/反序列化 OpenAI API 输出。将解析器提供给 promptTemplate 后,消息将自动序列化为 DTO 对象。

最终生成函数如下:

@Override
public PoetryDto getPoetryByGenreAndTheme(String genre, String theme) {
    BeanOutputParser<PoetryDto> poetryDtoBeanOutputParser = new BeanOutputParser<>(PoetryDto.class);

    String promptString = """
        Write me {genre} poetry about {theme}
        {format}
    """;

    PromptTemplate promptTemplate = new PromptTemplate(promptString);
    promptTemplate.add("genre", genre);
    promptTemplate.add("theme", theme);
    promptTemplate.add("format", poetryDtoBeanOutputParser.getFormat());
    promptTemplate.setOutputParser(poetryDtoBeanOutputParser);

    AiResponse response = aiClient.generate(promptTemplate.create());

    return poetryDtoBeanOutputParser.parse(response.getGeneration().getText());
}

现在客户端响应更规范,符合 REST API 标准:

{
    "title": "Dancing Flames",
    "poetry": "In the depths of night, flames dance with grace,\n       Their golden tongues lick the air with fiery embrace.\n       A symphony of warmth, a mesmerizing sight,\n       In their flickering glow, shadows take flight.\n       Oh, flames so vibrant, so full of life,\n       Burning with passion, banishing all strife.\n       They consume with ardor, yet do not destroy,\n       A paradox of power, a delicate ploy.\n       They whisper secrets, untold and untamed,\n       Their radiant hues, a kaleidoscope unnamed.\n       In their gentle crackling, stories unfold,\n       Of ancient tales and legends untold.\n       Flames ignite the heart, awakening desire,\n       They fuel the soul, setting it on fire.\n       With every flicker, they kindle a spark,\n       Guiding us through the darkness, lighting up the dark.\n       So let us gather 'round, bask in their warm embrace,\n       For in the realm of flames, magic finds its place.\n       In their ethereal dance, we find solace and release,\n       And in their eternal glow, our spirits find peace.",
    "genre": "Liric",
    "theme": "Flames"
}

5. 错误处理

Spring AI 通过 OpenAiHttpException 类提供 OpenAPI 错误的抽象。遗憾的是,它未按错误类型提供单独的异常类映射。但借助该抽象,可用 RestControllerAdvice 统一处理所有异常。

以下代码使用 Spring 6 的 ProblemDetail 标准:

@RestControllerAdvice
public class ExceptionTranslator extends ResponseEntityExceptionHandler {
    public static final String OPEN_AI_CLIENT_RAISED_EXCEPTION = "Open AI client raised exception";

    @ExceptionHandler(OpenAiHttpException.class)
    ProblemDetail handleOpenAiHttpException(OpenAiHttpException ex) {
        HttpStatus status = Optional
          .ofNullable(HttpStatus.resolve(ex.statusCode))
          .orElse(HttpStatus.BAD_REQUEST);
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, ex.getMessage());
        problemDetail.setTitle(OPEN_AI_CLIENT_RAISED_EXCEPTION);
        return problemDetail;
    }
}

当 OpenAPI 返回错误时,将得到结构化响应:

{
    "type": "about:blank",
    "title": "Open AI client raised exception",
    "status": 401,
    "detail": "Incorrect API key provided: sk-XG6GW***************************************wlmi. \n       You can find your API key at https://platform.openai.com/account/api-keys.",
    "instance": "/ai/cathaiku"
}

完整异常状态列表见 官方文档

6. 总结

本文介绍了 Spring AI 项目及其在 REST API 中的应用能力。尽管撰写时 spring-ai-starter 仍处于快照版本的开发阶段,但它已为 Spring Boot 应用集成生成式 AI 提供了可靠接口。

我们覆盖了基础和高级集成方案,包括:

  • AiClient 底层工作原理
  • 基础生成式接口实现
  • 高级特性应用:PromptTemplateAiResponseBeanOutputParser
  • 错误处理机制

作为概念验证,我们实现了一个诗歌生成 REST 应用,展示了 Spring AI 的实用价值。


原始标题:Introduction to Spring AI | Baeldung