Featured image of post SpringAI实战指南_大模型集成_流式输出

SpringAI实战指南_大模型集成_流式输出

前言

Spring AI 是 Spring 官方打造的 AI 应用开发框架,旨在将 AI 能力无缝集成到 Spring 生态系统中。它侧重于提供构建 AI 应用所需的底层原子能力抽象,让 Java 开发者可以像使用其他 Spring 组件一样,轻松地与大语言模型进行交互。

一、Spring AI 核心能力

能力 说明 应用场景
模型通信 (ChatClient) 统一接口与不同 LLM 对话 简历评分、问答生成
提示词 (Prompt) 结构化管理发送给模型的提示词 .st 模板文件管理
RAG (检索增强生成) 通过 VectorStore 实现 RAG 模式 知识库问答
工具调用 (Function Calling) 模型调用 Java 应用中定义的方法 Agent
记忆 (ChatMemory) 管理多轮对话的上下文历史 会话管理

二、框架对比与选型

2.1 Java AI 框架对比

框架 特点 JDK 基线 适用场景
Spring AI Spring 官方,原生集成 JDK 17+(建议 JDK 21) Spring 项目首选
LangChain4j 功能全面,更新快 JDK 8+ 老项目,兼容性需求
Solon-AI 轻量化,性能优秀 JDK 8+ Serverless、GraalVM
Agent-Flex 专注于 Agent 编排 JDK 11/17+ Agent 编排需求

2.2 为什么选择 Spring AI

  1. 无缝集成:与现有 Spring Boot 技术栈完美融合,学习成本低
  2. 高度抽象:统一的、与具体模型无关的 API,轻松切换模型
  3. 生态整合:整合向量数据库、ETL 框架等 AI 应用开发工具链
  4. 虚拟线程支持:全面拥抱 Java 21 虚拟线程,高并发处理能力强

三、项目依赖配置

3.1 添加依赖

1
2
3
4
5
6
7
// build.gradle
dependencies {
    // Spring AI 2.0 - OpenAI兼容模式 (阿里云DashScope)
    implementation "org.springframework.ai:spring-ai-starter-model-openai:${spring-ai.version}"
    // Spring AI 2.0 - PostgreSQL Vector Store (pgvector)
    implementation "org.springframework.ai:spring-ai-starter-vector-store-pgvector:${spring-ai.version}"
}

3.2 配置属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# application.yml
spring:
  ai:
    openai:
      base-url: https://dashscope.aliyuncs.com/compatible-mode
      api-key: ${AI_BAILIAN_API_KEY}
      chat:
        options:
          model: ${AI_MODEL:qwen-plus}
          temperature: 0.2
      # Embedding模型配置
      embedding:
        options:
          model: text-embedding-v3
    # 禁用自动重试机制,让异常立即返回
    retry:
      max-attempts: 1
      on-client-errors: false
    # PostgreSQL Vector Store配置
    vectorstore:
      pgvector:
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1024
        initialize-schema: true
        remove-existing-vector-store-table: false

3.3 关键配置说明

配置项 说明
base-url 阿里云 DashScope 的 OpenAI 兼容端点
api-key 通过环境变量注入,严禁硬编码
temperature 控制输出随机性(0-1),结构化输出使用 0.2 更稳定
dimensions text-embedding-v3 的向量维度为 1024
initialize-schema 开发环境设为 true 自动创建表

四、ChatClient 大模型调用

4.1 服务类设计

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Service
public class ResumeGradingService {

    private final ChatClient chatClient;
    private final PromptTemplate systemPromptTemplate;
    private final PromptTemplate userPromptTemplate;
    private final BeanOutputConverter<ResumeAnalysisResponseDTO> outputConverter;

    public ResumeGradingService(
            ChatClient.Builder chatClientBuilder,
            @Value("classpath:prompts/resume-analysis-system.st") Resource systemPromptResource,
            @Value("classpath:prompts/resume-analysis-user.st") Resource userPromptResource) {
        this.chatClient = chatClientBuilder.build();
        this.systemPromptTemplate = new PromptTemplate(systemPromptResource.getContentAsString(StandardCharsets.UTF_8));
        this.userPromptTemplate = new PromptTemplate(userPromptResource.getContentAsString(StandardCharsets.UTF_8));
        this.outputConverter = new BeanOutputConverter<>(ResumeAnalysisResponseDTO.class);
    }
}

4.2 调用大模型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public ResumeAnalysisResponse analyzeResume(String resumeText) {
    // 1. 加载系统提示词
    String systemPrompt = systemPromptTemplate.render();

    // 2. 加载用户提示词并填充变量
    Map<String, Object> variables = new HashMap<>();
    variables.put("resumeText", resumeText);
    String userPrompt = userPromptTemplate.render(variables);

    // 3. 添加格式指令到系统提示词
    String systemPromptWithFormat = systemPrompt + "\n\n" + outputConverter.getFormat();

    // 4. 调用AI
    ResumeAnalysisResponseDTO dto = chatClient.prompt()
        .system(systemPromptWithFormat)
        .user(userPrompt)
        .call()
        .entity(outputConverter);

    // 5. 转换为业务对象
    return convertToResponse(dto, resumeText);
}

4.3 调用流程图

1
2
3
4
5
6
7
8
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ 加载提示词   │────▶│ 填充变量    │────▶│ 调用 ChatClient│
└─────────────┘     └─────────────┘     └─────────────┘
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ 转换业务对象 │◀────│ 解析响应    │◀────│ 获取 AI 响应 │
└─────────────┘     └─────────────┘     └─────────────┘

五、结构化输出

5.1 Java Record 定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 中间DTO用于接收AI响应
private record ResumeAnalysisResponseDTO(
    int overallScore,
    ScoreDetailDTO scoreDetail,
    String summary,
    List<String> strengths,
    List<SuggestionDTO> suggestions
) {}

private record ScoreDetailDTO(
    int contentScore,
    int structureScore,
    int skillMatchScore,
    int expressionScore,
    int projectScore
) {}

private record SuggestionDTO(
    String category,
    String priority,
    String issue,
    String recommendation
) {}

5.2 结构化输出重试策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public <T> T invokeWithRetry(ChatClient chatClient, String systemPrompt,
                              String userPrompt, BeanOutputConverter<T> converter) {
    int maxAttempts = 2;
    Exception lastError = null;

    for (int i = 0; i < maxAttempts; i++) {
        try {
            return chatClient.prompt()
                .system(systemPrompt + "\n\n" + converter.getFormat())
                .user(userPrompt)
                .call()
                .entity(converter);
        } catch (Exception e) {
            lastError = e;
            // 重试时注入上次错误信息
            userPrompt += "\n\n注意:上次输出解析失败,错误原因:" + e.getMessage();
        }
    }
    throw new BusinessException(ErrorCode.AI_RESPONSE_PARSING_FAILED, lastError);
}

5.3 处理流水线

1
生成 → 解析 → 修复(可选)→ 校验
阶段 说明 失败处理
生成 LLM 输出 JSON -
解析 Jackson 反序列化 进入修复阶段
修复 将错误信息发回 LLM 要求修正 达最大次数则返回错误
校验 JSR-380 Bean Validation 进入修复阶段

六、流式输出 SSE

6.1 SSE 简介

SSE(Server-Sent Events) 是一种基于 HTTP 的服务器推送技术,允许服务器向客户端单向发送事件流。

6.2 SSE vs WebSocket

维度 SSE WebSocket
通信方式 单向(服务端→客户端) 全双工
协议 基于 HTTP 独立协议
实现复杂度
自动重连 支持 需自己实现
二进制数据 需编码 原生支持
适用场景 AI 流式生成、实时通知 聊天室、协作编辑

6.3 SSE 协议格式

1
2
3
4
5
6
7
data: 第一行内容
data: 第二行内容
data: 第三行内容

id: 事件ID
event: 自定义事件类型
retry: 3000
字段 说明
data 具体的业务数据
event 自定义事件类型
id 事件 ID,断线重连时使用
retry 重连间隔(毫秒)

6.4 Spring Boot SSE 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamData() {
    return Flux.interval(Duration.ofMillis(500))
        .map(seq -> ServerSentEvent.<String>builder()
            .id(String.valueOf(seq))
            .event("message")
            .data("消息 " + seq)
            .retry(3000L)
            .build());
}

6.5 Spring AI 流式调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamChat(@RequestParam String question) {
    return chatClient.prompt()
        .system(systemPrompt)
        .user(question)
        .stream()
        .content()
        .map(chunk -> ServerSentEvent.<String>builder()
            .data(chunk.replace("\n", "\\n").replace("\r", "\\r"))
            .build());
}

6.6 SSE 格式转义处理

1
2
3
4
5
6
7
// 后端转义
.map(chunk -> ServerSentEvent.<String>builder()
    .data(chunk.replace("\n", "\\n").replace("\r", "\\r"))
    .build())

// 前端反向转义
const text = chunk.replace(/\\n/g, '\n').replace(/\\r/g, '\r');

6.7 错误处理与超时控制

1
2
3
4
5
6
return responseFlux
    .timeout(Duration.ofSeconds(30))
    .onErrorResume(TimeoutException.class, e -> {
        log.error("流式输出超时", e);
        return Flux.just("【错误】回答生成超时,请缩短问题或稍后重试。");
    });

6.8 虚拟线程配置

1
2
3
4
spring:
  threads:
    virtual:
      enabled: true  # 启用虚拟线程

虚拟线程 vs 平台线程

场景 平台线程 虚拟线程
200 并发 SSE 线程池满,排队 轻松处理
AI 调用等待 3 秒 线程阻塞,占用资源 自动挂起,让出资源
10000 并发请求 拒绝服务 正常处理

七、Function Calling

7.1 Function Calling 原理

Function Calling 允许 LLM 调用我们定义的 Java 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
    return builder
        .defaultFunction("getWeather", new GetWeatherFunction())
        .build();
}

public class GetWeatherFunction implements Function<WeatherRequest, WeatherResponse> {
    @Override
    public WeatherResponse apply(WeatherRequest request) {
        // 调用天气 API
        return weatherService.getWeather(request.getCity());
    }
}

7.2 Prompt 模板定义

1
2
3
4
5
6
# System Prompt
你是一个专业的助手。当用户询问天气时,
请调用 getWeather 函数获取实时天气信息。

# User Prompt
{userQuestion}

7.3 处理流程

1
用户输入 → LLM 判断需要调用函数 → 执行函数 → 返回结果 → LLM 生成最终回答

八、提示词模板管理

8.1 .st 模板文件

1
2
3
4
5
6
7
8
# interview-question-system.st
你是一个专业的面试官。请根据候选人的简历内容,
生成针对性的面试问题。

## 要求
1. 问题要基于简历中的具体经历
2. 每个主问题需要生成追问
3. 问题难度要适中,考察真实能力

8.2 变量注入

1
2
3
4
5
6
Map<String, Object> variables = new HashMap<>();
variables.put("resumeText", resumeText);
variables.put("questionCount", 10);
variables.put("followUpCount", 2);

String userPrompt = userPromptTemplate.render(variables);

8.3 安全注入

用户输入注入模板前需清洗特殊符号,防止 Prompt Injection:

1
2
3
4
5
6
public String sanitizeUserInput(String input) {
    // 移除可能破坏 Prompt 结构的符号
    return input.replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;");
}

九、常见问题与解决方案

问题 原因 解决方案
JSON 解析失败 低温导致格式不稳定 升温至 0.3 + Retry 闭环
流式输出中断 网络波动 / 超时 添加超时控制 + 前端重连
响应太慢 上下文太长 减少输入 Token 或分批处理
429 限流 请求频率过高 添加限流器 + 指数退避
线程池耗尽 同步调用阻塞 启用虚拟线程 + 异步处理

十、总结

Spring AI 提供了企业级 AI 应用开发所需的核心能力:

  1. 统一抽象:一套代码支持多种 LLM,切换模型只需改配置
  2. 结构化输出:BeanOutputConverter 实现 LLM 输出到 Java 对象的自动映射
  3. 流式响应:SSE 实现"边生成边展示"的打字机效果
  4. 提示词管理:.st 模板文件实现提示词的外置化管理
  5. 高并发支持:Java 21 虚拟线程轻松应对高并发场景
使用 Hugo 构建
主题 StackJimmy 设计

发布了 35 篇文章 | 共 90422 字