Spring Boot + AI 企业应用开发教程

教程简介

零基础Spring Boot + AI企业应用开发教程,涵盖Spring AI框架、多模型适配、Function Calling、RAG集成、流式输出、对话记忆、向量数据库集成、安全认证、生产部署等核心技能,配有企业级AI客服系统实战项目,适合Java开发者系统学习。

Spring Boot + AI 企业应用开发教程

前言

随着大语言模型技术的快速发展,将 AI 能力集成到企业级 Java 应用中已成为行业趋势。Spring Boot 作为 Java 生态中最流行的微服务框架,通过 Spring AI 项目提供了优雅的 AI 集成方案。

本教程将系统性地讲解如何使用 Spring Boot + Spring AI 构建企业级 AI 应用,从框架核心概念到多模型适配,从 Function Calling 到 RAG 集成,从流式输出到生产部署,最终通过一个完整的企业级 AI 客服系统实战项目,帮助 Java 开发者快速掌握 AI 应用开发的全栈技能。


第一章:Spring AI 框架概述 — Java 生态的 AI 集成方案

1.1 为什么选择 Spring AI

在 Java 生态中集成 AI 能力,开发者面临多个选择:直接调用 OpenAI API、使用 LangChain4j、或者选择 Spring AI。Spring AI 具有以下核心优势:

与 Spring 生态无缝集成:Spring AI 遵循 Spring 的设计哲学,提供了统一的抽象层,使得切换不同的 AI 模型提供商(OpenAI、Ollama、Azure、通义千问等)只需要修改配置,不需要改动业务代码。

企业级特性:与 Spring Security、Spring Data、Spring Cloud 等组件天然兼容,可以轻松实现认证授权、数据持久化、微服务通信等企业级需求。

统一抽象:Spring AI 提供了 ChatClientEmbeddingClientImageClient 等核心抽象,屏蔽了底层模型 API 的差异。

1.2 项目初始化

使用 Spring Initializr 创建项目:

<!-- pom.xml 核心依赖 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.0</version>
</parent>

<dependencies>
    <!-- Spring AI BOM -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-bom</artifactId>
        <version>1.0.0</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>

    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI OpenAI Starter -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>

    <!-- Spring AI Ollama Starter(本地模型) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
    </dependency>

    <!-- Spring AI PgVector Store -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
    </dependency>

    <!-- Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- WebFlux(用于流式输出) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
</dependencies>

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
    </repository>
</repositories>

1.3 基础配置

# application.yml
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      base-url: https://api.openai.com
      chat:
        options:
          model: gpt-4o
          temperature: 0.7
          max-tokens: 2000
      embedding:
        options:
          model: text-embedding-3-small
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen2.5:7b
  datasource:
    url: jdbc:postgresql://localhost:5432/ai_app
    username: postgres
    password: ${DB_PASSWORD}

第二章:Spring AI 核心抽象

2.1 ChatClient — 对话客户端

ChatClient 是 Spring AI 中最核心的接口,用于与大语言模型进行对话:

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.stereotype.Service;

@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder
            .defaultSystem("你是一个专业的AI助手,善于用简洁清晰的中文回答问题。")
            .build();
    }

    /**
     * 简单对话
     */
    public String chat(String userMessage) {
        return chatClient.prompt()
            .user(userMessage)
            .call()
            .content();
    }

    /**
     * 带系统提示的对话
     */
    public String chatWithSystem(String systemPrompt, String userMessage) {
        return chatClient.prompt()
            .system(systemPrompt)
            .user(userMessage)
            .call()
            .content();
    }

    /**
     * 获取完整的 ChatResponse(包含元数据)
     */
    public ChatResponse chatWithMetadata(String userMessage) {
        return chatClient.prompt()
            .user(userMessage)
            .call()
            .chatResponse();
    }

    /**
     * 多轮对话
     */
    public String multiTurnChat(List<Message> conversationHistory, String newMessage) {
        conversationHistory.add(new org.springframework.ai.chat.messages.UserMessage(newMessage));

        Prompt prompt = new Prompt(conversationHistory);
        ChatResponse response = chatClient.prompt(prompt).call().chatResponse();

        String assistantReply = response.getResult().getOutput().getContent();
        conversationHistory.add(new org.springframework.ai.chat.messages.AssistantMessage(assistantReply));

        return assistantReply;
    }
}

2.2 EmbeddingClient — 向量化客户端

EmbeddingClient 用于将文本转化为向量表示,是 RAG 系统的基础:

import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.stereotype.Service;

@Service
public class EmbeddingService {

    private final EmbeddingClient embeddingClient;

    public EmbeddingService(EmbeddingClient embeddingClient) {
        this.embeddingClient = embeddingClient;
    }

    /**
     * 单文本向量化
     */
    public float[] embed(String text) {
        List<float[]> embeddings = embeddingClient.embed(List.of(text));
        return embeddings.get(0);
    }

    /**
     * 批量文本向量化
     */
    public List<float[]> batchEmbed(List<String> texts) {
        return embeddingClient.embed(texts);
    }

    /**
     * 计算两个文本的相似度
     */
    public double similarity(String text1, String text2) {
        float[] vec1 = embed(text1);
        float[] vec2 = embed(text2);

        double dotProduct = 0, norm1 = 0, norm2 = 0;
        for (int i = 0; i < vec1.length; i++) {
            dotProduct += vec1[i] * vec2[i];
            norm1 += vec1[i] * vec1[i];
            norm2 += vec2[i] * vec2[i];
        }

        return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
    }
}

2.3 ImageClient — 图像生成客户端

import org.springframework.ai.image.ImageClient;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.stereotype.Service;

@Service
public class ImageService {

    private final ImageClient imageClient;

    public ImageService(ImageClient imageClient) {
        this.imageClient = imageClient;
    }

    /**
     * 生成图片
     */
    public String generateImage(String description) {
        ImageResponse response = imageClient.call(
            new ImagePrompt(description)
        );
        return response.getResult().getOutput().getUrl();
    }
}

第三章:多模型适配 — OpenAI、Ollama、Azure、通义千问

3.1 多模型配置策略

在企业应用中,通常需要支持多个模型提供商,以实现成本优化、故障切换和功能差异化:

# application.yml - 多模型配置
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      base-url: ${OPENAI_BASE_URL:https://api.openai.com}
      chat:
        options:
          model: gpt-4o
          temperature: 0.7
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen2.5:7b

# 自定义模型配置
app:
  ai:
    models:
      fast:
        provider: ollama
        model: qwen2.5:7b
      smart:
        provider: openai
        model: gpt-4o
      balanced:
        provider: openai
        model: gpt-4o-mini

3.2 模型路由器

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

@Component
public class ModelRouter {

    private final Map<String, ChatClient> chatClients;

    public ModelRouter(List<ChatClient.Builder> builders) {
        this.chatClients = new HashMap<>();
        // 根据配置初始化不同的 ChatClient
    }

    /**
     * 根据任务类型选择模型
     */
    public ChatClient route(TaskType taskType) {
        return switch (taskType) {
            case SIMPLE_QA -> chatClients.get("fast");      // 简单问答用小模型
            case COMPLEX_ANALYSIS -> chatClients.get("smart"); // 复杂分析用大模型
            case CODE_GENERATION -> chatClients.get("smart");
            case TRANSLATION -> chatClients.get("balanced");
            default -> chatClients.get("balanced");
        };
    }

    public enum TaskType {
        SIMPLE_QA, COMPLEX_ANALYSIS, CODE_GENERATION, TRANSLATION, SUMMARIZATION
    }
}

3.3 通义千问集成

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

@Configuration
public class QwenConfig {

    @Bean
    public ChatClient qwenChatClient() {
        // 通义千问兼容 OpenAI API 格式
        RestClient.Builder restClientBuilder = RestClient.builder()
            .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
            .defaultHeader("Authorization", "Bearer " + System.getenv("DASHSCOPE_API_KEY"));

        // 使用 OpenAI 兼容模式
        return ChatClient.builder(
            new org.springframework.ai.openai.OpenAiChatModel(
                new org.springframework.ai.openai.OpenAiApi(
                    "https://dashscope.aliyuncs.com/compatible-mode/v1",
                    System.getenv("DASHSCOPE_API_KEY")
                ),
                org.springframework.ai.openai.OpenAiChatOptions.builder()
                    .model("qwen-plus")
                    .temperature(0.7)
                    .build()
            )
        ).build();
    }
}

第四章:Function Calling 在 Spring Boot 中的实现

4.1 Function Calling 概述

Function Calling 允许大模型调用外部函数,获取实时数据或执行特定操作。这是构建 AI Agent 的核心能力。

4.2 定义 Function

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Description;
import org.springframework.stereotype.Service;

import java.util.function.Function;

@Service
public class AIFunctions {

    /**
     * 查询天气函数
     */
    @Bean
    @Description("获取指定城市的当前天气信息")
    public Function<WeatherRequest, WeatherResponse> getWeather() {
        return request -> {
            // 调用天气 API
            String weather = callWeatherAPI(request.city());
            return new WeatherResponse(
                request.city(),
                weather,
                "25°C",
                "晴天"
            );
        };
    }

    /**
     * 查询订单函数
     */
    @Bean
    @Description("根据订单号查询订单状态和详情")
    public Function<OrderRequest, OrderResponse> getOrderStatus() {
        return request -> {
            // 查询数据库
            Order order = orderRepository.findByOrderNo(request.orderNo());
            return new OrderResponse(
                order.getOrderNo(),
                order.getStatus().name(),
                order.getCreateTime(),
                order.getTrackingNo()
            );
        };
    }

    /**
     * 发送通知函数
     */
    @Bean
    @Description("向指定用户发送通知消息")
    public Function<NotificationRequest, NotificationResponse> sendNotification() {
        return request -> {
            notificationService.send(request.userId(), request.message());
            return new NotificationResponse(true, "通知发送成功");
        };
    }

    // 请求/响应记录
    public record WeatherRequest(String city) {}
    public record WeatherResponse(String city, String description, String temperature, String condition) {}
    public record OrderRequest(String orderNo) {}
    public record OrderResponse(String orderNo, String status, String createTime, String trackingNo) {}
    public record NotificationRequest(String userId, String message) {}
    public record NotificationResponse(boolean success, String message) {}
}

4.3 使用 Function Calling

@Service
public class FunctionCallingService {

    private final ChatClient chatClient;

    public FunctionCallingService(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("你是一个智能助手,可以查询天气、订单状态,并发送通知。")
            .defaultFunctions("getWeather", "getOrderStatus", "sendNotification")
            .build();
    }

    /**
     * 带 Function Calling 的对话
     */
    public String chatWithFunctions(String userMessage) {
        return chatClient.prompt()
            .user(userMessage)
            .call()
            .content();
    }

    /**
     * 示例:
     * 用户:"北京今天天气怎么样?"
     * AI 会自动调用 getWeather("北京") 函数,然后基于返回结果生成回答
     */
}

4.4 动态 Function 注册

@Service
public class DynamicFunctionService {

    private final ChatClient.Builder chatClientBuilder;

    public DynamicFunctionService(ChatClient.Builder chatClientBuilder) {
        this.chatClientBuilder = chatClientBuilder;
    }

    /**
     * 根据用户权限动态注册可用函数
     */
    public String chatWithDynamicFunctions(String userMessage, Set<String> permissions) {
        ChatClient.Builder builder = chatClientBuilder.copy();

        // 根据权限动态添加函数
        if (permissions.contains("weather")) {
            builder.defaultFunctions("getWeather");
        }
        if (permissions.contains("order:read")) {
            builder.defaultFunctions("getOrderStatus");
        }
        if (permissions.contains("notification:send")) {
            builder.defaultFunctions("sendNotification");
        }

        return builder.build()
            .prompt()
            .user(userMessage)
            .call()
            .content();
    }
}

第五章:RAG 集成 — VectorStore、DocumentReader、Advisor

5.1 RAG 架构概览

Spring AI 提供了完整的 RAG 支持,核心组件包括:

  • DocumentReader:文档读取器,支持 PDF、Word、HTML 等格式
  • DocumentTransformer:文档转换器,包括分块、清洗等
  • DocumentWriter:文档写入器,将文档向量化后存入 VectorStore
  • VectorStore:向量存储,支持 PgVector、Milvus、Chroma 等
  • Advisor:顾问模式,用于在对话流程中注入 RAG 检索结果

5.2 文档加载与处理

import org.springframework.ai.document.Document;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.stereotype.Service;

import java.io.InputStream;
import java.util.List;

@Service
public class DocumentService {

    private final VectorStore vectorStore;

    public DocumentService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    /**
     * 加载 PDF 文档
     */
    public List<Document> loadPdf(InputStream pdfStream) {
        PagePdfDocumentReader reader = new PagePdfDocumentReader(pdfStream);
        return reader.get();
    }

    /**
     * 使用 Tika 加载多种格式文档
     */
    public List<Document> loadWithTika(InputStream stream, String mediaType) {
        TikaDocumentReader reader = new TikaDocumentReader(stream, mediaType);
        return reader.get();
    }

    /**
     * 文档分块
     */
    public List<Document> splitDocuments(List<Document> documents) {
        TokenTextSplitter splitter = new TokenTextSplitter(
            800,    // 默认 chunk size
            200,    // min chunk size
            10,     // min chunk length tokens
            5000,   // max num chunks
            true    // keep separator
        );
        return splitter.apply(documents);
    }

    /**
     * 完整的文档入库流程
     */
    public int ingestDocument(InputStream stream, String mediaType, Map<String, Object> metadata) {
        // 1. 加载文档
        List<Document> documents = loadWithTika(stream, mediaType);

        // 2. 添加元数据
        documents.forEach(doc -> {
            doc.getMetadata().putAll(metadata);
            doc.getMetadata().put("ingest_time", Instant.now().toString());
        });

        // 3. 分块
        List<Document> chunks = splitDocuments(documents);

        // 4. 写入向量数据库
        vectorStore.add(chunks);

        return chunks.size();
    }
}

5.3 VectorStore 集成

import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.stereotype.Service;

@Service
public class VectorStoreService {

    private final VectorStore vectorStore;

    public VectorStoreService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    /**
     * 基本相似度搜索
     */
    public List<Document> search(String query, int topK) {
        SearchRequest request = SearchRequest.query(query)
            .withTopK(topK);
        return vectorStore.similaritySearch(request);
    }

    /**
     * 带过滤条件的搜索
     */
    public List<Document> searchWithFilter(String query, int topK, String category) {
        FilterExpressionBuilder b = new FilterExpressionBuilder();

        SearchRequest request = SearchRequest.query(query)
            .withTopK(topK)
            .withFilterExpression(b.eq("category", category).build());

        return vectorStore.similaritySearch(request);
    }

    /**
     * 带相似度阈值的搜索
     */
    public List<Document> searchWithThreshold(String query, int topK, double threshold) {
        SearchRequest request = SearchRequest.query(query)
            .withTopK(topK)
            .withSimilarityThreshold(threshold);

        return vectorStore.similaritySearch(request);
    }
}

5.4 Advisor 模式实现 RAG

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

@Service
public class RAGService {

    private final ChatClient chatClient;

    public RAGService(ChatClient.Builder builder, VectorStore vectorStore) {
        // 创建 RAG Advisor
        QuestionAnswerAdvisor ragAdvisor = new QuestionAnswerAdvisor(
            vectorStore,
            SearchRequest.defaults().withTopK(5)
        );

        this.chatClient = builder
            .defaultSystem("你是一个企业知识库助手,请基于提供的参考资料回答问题。如果资料中没有相关信息,请明确告知。")
            .defaultAdvisors(ragAdvisor)
            .build();
    }

    /**
     * RAG 问答
     */
    public String ask(String question) {
        return chatClient.prompt()
            .user(question)
            .call()
            .content();
    }

    /**
     * 带对话记忆的 RAG 问答
     */
    public String askWithMemory(String question, String sessionId) {
        return chatClient.prompt()
            .user(question)
            .advisors(new MessageChatMemoryAdvisor(chatMemory, sessionId))
            .call()
            .content();
    }
}

第六章:流式输出与 SSE 实现

6.1 流式输出的价值

在 AI 应用中,流式输出(Streaming)能显著提升用户体验。用户不必等待完整回答生成,而是可以像打字一样逐字看到回答。

6.2 Spring AI 流式 API

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

@Service
public class StreamingChatService {

    private final ChatClient chatClient;

    public StreamingChatService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    /**
     * 流式对话
     */
    public Flux<String> streamChat(String userMessage) {
        return chatClient.prompt()
            .user(userMessage)
            .stream()
            .content();
    }

    /**
     * 流式对话(包含元数据)
     */
    public Flux<ChatResponse> streamChatWithMetadata(String userMessage) {
        return chatClient.prompt()
            .user(userMessage)
            .stream()
            .chatResponse();
    }
}

6.3 SSE(Server-Sent Events)控制器

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final StreamingChatService streamingChatService;
    private final ChatService chatService;

    public ChatController(StreamingChatService streamingChatService, ChatService chatService) {
        this.streamingChatService = streamingChatService;
        this.chatService = chatService;
    }

    /**
     * 流式聊天接口(SSE)
     */
    @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamChat(@RequestBody ChatRequest request) {
        return streamingChatService.streamChat(request.message())
            .map(chunk -> "data: " + chunk + "\n\n");
    }

    /**
     * 非流式聊天接口
     */
    @PostMapping("/call")
    public ChatResponse callChat(@RequestBody ChatRequest request) {
        String response = chatService.chat(request.message());
        return new ChatResponse(response);
    }

    /**
     * 带 RAG 的流式聊天
     */
    @PostMapping(value = "/rag/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamRAGChat(@RequestBody ChatRequest request) {
        return ragService.streamAsk(request.message())
            .map(chunk -> "data: " + chunk + "\n\n");
    }

    public record ChatRequest(String message, String sessionId) {}
    public record ChatResponse(String content) {}
}

6.4 前端 SSE 接收(Vue3)

// Vue3 组合式 API
import { ref } from 'vue'

export function useStreamingChat() {
  const messages = ref([])
  const isStreaming = ref(false)

  async function sendMessage(content) {
    // 添加用户消息
    messages.value.push({ role: 'user', content })

    // 创建 AI 消息占位
    const aiMessage = { role: 'assistant', content: '' }
    messages.value.push(aiMessage)
    isStreaming.value = true

    try {
      const response = await fetch('/api/chat/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: content })
      })

      const reader = response.body.getReader()
      const decoder = new TextDecoder()

      while (true) {
        const { done, value } = await reader.read()
        if (done) break

        const text = decoder.decode(value)
        const lines = text.split('\n')

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const chunk = line.slice(6)
            aiMessage.content += chunk
            // 触发响应式更新
            messages.value = [...messages.value]
          }
        }
      }
    } finally {
      isStreaming.value = false
    }
  }

  return { messages, isStreaming, sendMessage }
}

第七章:对话记忆管理

7.1 ChatMemory 架构

Spring AI 通过 ChatMemoryMessageChatMemoryAdvisor 实现对话记忆管理:

import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatMemoryConfig {

    /**
     * 基于窗口的对话记忆(保留最近 N 条消息)
     */
    @Bean
    public ChatMemory chatMemory() {
        return MessageWindowChatMemory.builder()
            .maxMessages(20)  // 保留最近 20 条消息
            .build();
    }

    /**
     * 基于 Token 的对话记忆(适合长对话)
     */
    @Bean
    public ChatMemory tokenBasedChatMemory() {
        return TokenWindowChatMemory.builder()
            .maxTokens(4000)
            .build();
    }
}

7.2 持久化对话记忆

import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.repository.ChatMemoryRepository;
import org.springframework.ai.chat.messages.Message;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class JdbcChatMemoryRepository implements ChatMemoryRepository {

    private final JdbcTemplate jdbcTemplate;

    public JdbcChatMemoryRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        initTable();
    }

    private void initTable() {
        jdbcTemplate.execute("""
            CREATE TABLE IF NOT EXISTS chat_memory (
                id BIGSERIAL PRIMARY KEY,
                conversation_id VARCHAR(255) NOT NULL,
                message_type VARCHAR(50) NOT NULL,
                content TEXT NOT NULL,
                metadata JSONB,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """);
    }

    @Override
    public List<Message> findByConversationId(String conversationId) {
        return jdbcTemplate.query(
            "SELECT message_type, content FROM chat_memory WHERE conversation_id = ? ORDER BY created_at",
            (rs, rowNum) -> deserializeMessage(
                rs.getString("message_type"),
                rs.getString("content")
            ),
            conversationId
        );
    }

    @Override
    public void saveAll(String conversationId, List<Message> messages) {
        // 先删除旧消息
        jdbcTemplate.update("DELETE FROM chat_memory WHERE conversation_id = ?", conversationId);

        // 批量插入
        jdbcTemplate.batchUpdate(
            "INSERT INTO chat_memory (conversation_id, message_type, content) VALUES (?, ?, ?)",
            messages.stream().map(msg -> new Object[]{
                conversationId,
                msg.getMessageType().name(),
                msg.getContent()
            }).toList()
        );
    }

    @Override
    public void deleteByConversationId(String conversationId) {
        jdbcTemplate.update("DELETE FROM chat_memory WHERE conversation_id = ?", conversationId);
    }
}

7.3 对话记忆服务

@Service
public class ConversationService {

    private final ChatClient chatClient;
    private final ChatMemory chatMemory;

    public ConversationService(ChatClient.Builder builder, ChatMemory chatMemory) {
        this.chatMemory = chatMemory;
        this.chatClient = builder
            .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
            .build();
    }

    /**
     * 带记忆的对话
     */
    public String chat(String sessionId, String message) {
        return chatClient.prompt()
            .user(message)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
            .call()
            .content();
    }

    /**
     * 获取对话历史
     */
    public List<Message> getHistory(String sessionId) {
        return chatMemory.get(sessionId);
    }

    /**
     * 清除对话历史
     */
    public void clearHistory(String sessionId) {
        chatMemory.clear(sessionId);
    }
}

第八章:向量数据库集成

8.1 PgVector 集成

PgVector 是基于 PostgreSQL 的向量数据库,适合已有 PostgreSQL 基础设施的企业:

# application.yml
spring:
  ai:
    vectorstore:
      pgvector:
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1536
  datasource:
    url: jdbc:postgresql://localhost:5432/ai_app
@Configuration
public class PgVectorConfig {

    @Bean
    public VectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingClient embeddingClient) {
        return PgVectorStore.builder(jdbcTemplate, embeddingClient)
            .dimensions(1536)
            .indexType(IndexType.HNSW)
            .distanceType(COSINE_DISTANCE)
            .initializeSchema(true)
            .build();
    }
}

8.2 Milvus 集成

Milvus 是专业级分布式向量数据库,适合大规模向量检索场景:

@Configuration
public class MilvusConfig {

    @Bean
    public VectorStore milvusVectorStore(EmbeddingClient embeddingClient) {
        return MilvusVectorStore.builder(
                new MilvusServiceClient(
                    Param.newBuilder()
                        .withHost("localhost")
                        .withPort(19530)
                        .build()
                ),
                embeddingClient
            )
            .collectionName("documents")
            .indexType(IndexType.HNSW)
            .metricType(MetricType.COSINE)
            .build();
    }
}

8.3 Chroma 集成

Chroma 是轻量级向量数据库,适合开发和测试环境:

@Configuration
public class ChromaConfig {

    @Bean
    public VectorStore chromaVectorStore(EmbeddingClient embeddingClient) {
        return new ChromaVectorStore(
            new ChromaApi("http://localhost:8000"),
            embeddingClient,
            "documents"
        );
    }
}

8.4 多向量存储策略

@Service
public class MultiVectorStoreService {

    private final Map<String, VectorStore> vectorStores;

    public MultiVectorStoreService(
            @Qualifier("pgVectorStore") VectorStore pgStore,
            @Qualifier("milvusVectorStore") VectorStore milvusStore) {
        this.vectorStores = Map.of(
            "documents", pgStore,
            "knowledge", milvusStore
        );
    }

    /**
     * 根据数据类型选择向量存储
     */
    public List<Document> search(String query, String storeType, int topK) {
        VectorStore store = vectorStores.get(storeType);
        if (store == null) {
            throw new IllegalArgumentException("Unknown store type: " + storeType);
        }
        return store.similaritySearch(
            SearchRequest.query(query).withTopK(topK)
        );
    }

    /**
     * 跨存储联合搜索
     */
    public List<Document> federatedSearch(String query, int topK) {
        List<Document> allResults = new ArrayList<>();
        for (VectorStore store : vectorStores.values()) {
            allResults.addAll(
                store.similaritySearch(SearchRequest.query(query).withTopK(topK))
            );
        }
        // 按相似度排序并去重
        return allResults.stream()
            .sorted(Comparator.comparingDouble(Document::getScore).reversed())
            .limit(topK)
            .toList();
    }
}

第九章:安全与认证 — Spring Security + AI

9.1 安全架构设计

企业级 AI 应用需要完善的认证授权机制:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/chat/**").authenticated()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

9.2 API Key 认证

@Component
public class ApiKeyAuthFilter extends OncePerRequestFilter {

    private final ApiKeyRepository apiKeyRepository;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain chain) throws ServletException, IOException {

        String apiKey = request.getHeader("X-API-Key");
        if (apiKey == null) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        Optional<ApiKeyEntity> keyEntity = apiKeyRepository.findByKeyAndActive(apiKey, true);
        if (keyEntity.isEmpty()) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        ApiKeyEntity entity = keyEntity.get();

        // 检查速率限制
        if (isRateLimited(entity.getUserId())) {
            response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
            return;
        }

        // 设置认证信息
        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
            entity.getUserId(), null, List.of(new SimpleGrantedAuthority("ROLE_USER"))
        );
        SecurityContextHolder.getContext().setAuthentication(auth);

        chain.doFilter(request, response);
    }
}

9.3 内容安全过滤

@Service
public class ContentSafetyService {

    private final ChatClient moderationClient;

    public ContentSafetyService(ChatClient.Builder builder) {
        this.moderationClient = builder
            .defaultSystem("你是一个内容安全审核员。判断以下内容是否安全,返回 SAFE 或 UNSAFE。")
            .build();
    }

    /**
     * 检查用户输入是否安全
     */
    public boolean isInputSafe(String input) {
        // 简单的关键词过滤
        List<String> blockedKeywords = List.of("暴力", "色情", "违法");
        for (String keyword : blockedKeywords) {
            if (input.contains(keyword)) {
                return false;
            }
        }

        // 使用 AI 进行深度审核(可选)
        String result = moderationClient.prompt()
            .user("请审核以下内容是否安全:" + input)
            .call()
            .content();

        return result.contains("SAFE");
    }

    /**
     * 过滤 AI 输出中的敏感信息
     */
    public String filterOutput(String output) {
        // 脱敏处理:手机号、身份证号、邮箱等
        output = output.replaceAll("\\b1[3-9]\\d{9}\\b", "1**********");
        output = output.replaceAll("\\b\\d{17}[\\dXx]\\b", "*******************");
        output = output.replaceAll("\\b[\\w.-]+@[\\w.-]+\\.\\w+\\b", "***@***.***");

        return output;
    }
}

第十章:生产部署

10.1 Docker 化部署

# Dockerfile
FROM eclipse-temurin:21-jre-alpine

WORKDIR /app

# 复制构建产物
COPY target/ai-app.jar app.jar

# 环境变量
ENV JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC"

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

EXPOSE 8080

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
# docker-compose.yml
version: '3.8'

services:
  ai-app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - DB_URL=jdbc:postgresql://postgres:5432/ai_app
      - DB_USER=postgres
      - DB_PASSWORD=${DB_PASSWORD}
    depends_on:
      - postgres
      - redis

  postgres:
    image: pgvector/pgvector:pg16
    environment:
      - POSTGRES_DB=ai_app
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - ai-app

volumes:
  pgdata:
  redisdata:

10.2 监控与指标

@Configuration
public class MetricsConfig {

    @Bean
    public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
        return registry -> registry.config().commonTags("application", "ai-app");
    }
}

@Service
public class AIMetricsService {

    private final Counter chatRequestCounter;
    private final Timer chatLatencyTimer;
    private final Counter tokenUsageCounter;

    public AIMetricsService(MeterRegistry registry) {
        this.chatRequestCounter = Counter.builder("ai.chat.requests.total")
            .description("Total chat requests")
            .register(registry);

        this.chatLatencyTimer = Timer.builder("ai.chat.latency")
            .description("Chat request latency")
            .register(registry);

        this.tokenUsageCounter = Counter.builder("ai.tokens.usage")
            .description("Token usage")
            .register(registry);
    }

    public void recordChatRequest(String model, long latencyMs, int tokensUsed) {
        chatRequestCounter.increment();
        chatLatencyTimer.record(latencyMs, TimeUnit.MILLISECONDS);
        tokenUsageCounter.increment(tokensUsed);
    }
}

10.3 性能优化

@Configuration
public class PerformanceConfig {

    /**
     * 连接池配置
     */
    @Bean
    public RestClientCustomizer restClientCustomizer() {
        return restClient -> {
            HttpClient httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                .responseTimeout(Duration.ofSeconds(30))
                .doOnConnected(conn ->
                    conn.addHandlerLast(new ReadTimeoutHandler(30, TimeUnit.SECONDS))
                );

            restClient.requestFactory(new JdkClientHttpRequestFactory(
                httpClient
            ));
        };
    }

    /**
     * 异步线程池配置
     */
    @Bean("aiTaskExecutor")
    public Executor aiTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("ai-task-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

第十一章:实战项目 — 企业级 AI 客服系统

11.1 系统架构

┌─────────────────────────────────────────────┐
│              前端(Vue3)                     │
│  ┌─────────┐  ┌──────────┐  ┌────────────┐ │
│  │ 对话界面  │  │ 知识库管理 │  │ 数据统计    │ │
│  └─────────┘  └──────────┘  └────────────┘ │
├─────────────────────────────────────────────┤
│              API 网关(Nginx)                │
├─────────────────────────────────────────────┤
│              Spring Boot 后端                │
│  ┌─────────┐  ┌──────────┐  ┌────────────┐ │
│  │ 对话服务  │  │ RAG 服务  │  │ 管理服务    │ │
│  └─────────┘  └──────────┘  └────────────┘ │
│  ┌─────────┐  ┌──────────┐  ┌────────────┐ │
│  │ 记忆管理  │  │ 内容安全  │  │ Function   │ │
│  │         │  │          │  │ Calling    │ │
│  └─────────┘  └──────────┘  └────────────┘ │
├─────────────────────────────────────────────┤
│              数据层                          │
│  ┌─────────┐  ┌──────────┐  ┌────────────┐ │
│  │ PostgreSQL│  │  Redis   │  │  MinIO     │ │
│  │ +PgVector│  │  缓存     │  │  文件存储   │ │
│  └─────────┘  └──────────┘  └────────────┘ │
└─────────────────────────────────────────────┘

11.2 核心数据模型

// 对话实体
@Entity
@Table(name = "conversations")
public class Conversation {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String sessionId;

    private Long userId;
    private String title;

    @Enumerated(EnumType.STRING)
    private ConversationStatus status;

    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// 消息实体
@Entity
@Table(name = "messages")
public class Message {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long conversationId;

    @Enumerated(EnumType.STRING)
    private MessageRole role;  // USER, ASSISTANT, SYSTEM

    @Column(columnDefinition = "TEXT")
    private String content;

    private String model;
    private Integer promptTokens;
    private Integer completionTokens;

    @Column(columnDefinition = "jsonb")
    private String metadata;

    private LocalDateTime createdAt;
}

// 知识库文档实体
@Entity
@Table(name = "knowledge_documents")
public class KnowledgeDocument {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String category;

    @Column(columnDefinition = "TEXT")
    private String content;

    private String filePath;
    private Long fileSize;

    @Enumerated(EnumType.STRING)
    private DocumentStatus status;

    private Integer chunkCount;
    private LocalDateTime createdAt;
    private LocalDateTime processedAt;
}

11.3 客服对话服务

@Service
@Slf4j
public class CustomerServiceChat {

    private final ChatClient chatClient;
    private final ChatMemory chatMemory;
    private final VectorStore vectorStore;
    private final ContentSafetyService safetyService;
    private final OrderFunction orderFunction;
    private final ConversationRepository conversationRepo;
    private final MessageRepository messageRepo;

    public CustomerServiceChat(
            ChatClient.Builder builder,
            ChatMemory chatMemory,
            VectorStore vectorStore,
            ContentSafetyService safetyService,
            OrderFunction orderFunction,
            ConversationRepository conversationRepo,
            MessageRepository messageRepo) {

        this.chatMemory = chatMemory;
        this.vectorStore = vectorStore;
        this.safetyService = safetyService;
        this.orderFunction = orderFunction;
        this.conversationRepo = conversationRepo;
        this.messageRepo = messageRepo;

        // 构建带 RAG 和记忆的 ChatClient
        this.chatClient = builder
            .defaultSystem("""
                你是一个专业的客服助手。请遵守以下规则:
                1. 基于知识库中的信息回答问题,不要编造信息
                2. 如果知识库中没有相关信息,引导用户联系人工客服
                3. 回答要简洁、专业、友好
                4. 涉及订单问题时,使用订单查询功能获取实时信息
                5. 如果用户情绪激动,保持耐心和同理心
                """)
            .defaultAdvisors(
                new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults().withTopK(5)),
                new MessageChatMemoryAdvisor(chatMemory)
            )
            .defaultFunctions("getOrderStatus", "createTicket")
            .build();
    }

    /**
     * 处理用户消息
     */
    @Transactional
    public ChatResult handleUserMessage(String sessionId, String userMessage) {
        // 1. 内容安全检查
        if (!safetyService.isInputSafe(userMessage)) {
            return new ChatResult("抱歉,您的消息包含不当内容,请重新表述。", false);
        }

        // 2. 保存用户消息
        saveMessage(sessionId, MessageRole.USER, userMessage);

        // 3. 获取 AI 回答
        String aiResponse = chatClient.prompt()
            .user(userMessage)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
            .call()
            .content();

        // 4. 输出安全过滤
        aiResponse = safetyService.filterOutput(aiResponse);

        // 5. 保存 AI 回答
        saveMessage(sessionId, MessageRole.ASSISTANT, aiResponse);

        return new ChatResult(aiResponse, true);
    }

    /**
     * 流式处理用户消息
     */
    public Flux<String> streamUserMessage(String sessionId, String userMessage) {
        if (!safetyService.isInputSafe(userMessage)) {
            return Flux.just("抱歉,您的消息包含不当内容,请重新表述。");
        }

        saveMessage(sessionId, MessageRole.USER, userMessage);

        return chatClient.prompt()
            .user(userMessage)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
            .stream()
            .content()
            .doOnComplete(() -> {
                // 流完成后保存完整回答(可选)
            });
    }

    private void saveMessage(String sessionId, MessageRole role, String content) {
        Conversation conv = conversationRepo.findBySessionId(sessionId)
            .orElseGet(() -> {
                Conversation c = new Conversation();
                c.setSessionId(sessionId);
                c.setStatus(ConversationStatus.ACTIVE);
                c.setCreatedAt(LocalDateTime.now());
                return conversationRepo.save(c);
            });

        Message msg = new Message();
        msg.setConversationId(conv.getId());
        msg.setRole(role);
        msg.setContent(content);
        msg.setCreatedAt(LocalDateTime.now());
        messageRepo.save(msg);
    }

    public record ChatResult(String content, boolean success) {}
}

11.4 客服 REST API

@RestController
@RequestMapping("/api/customer-service")
public class CustomerServiceController {

    private final CustomerServiceChat chatService;

    public CustomerServiceController(CustomerServiceChat chatService) {
        this.chatService = chatService;
    }

    /**
     * 普通对话接口
     */
    @PostMapping("/chat")
    public ResponseEntity<ChatResponse> chat(@RequestBody @Valid ChatRequest request,
                                              @AuthenticationPrincipal Long userId) {
        var result = chatService.handleUserMessage(request.sessionId(), request.message());
        return ResponseEntity.ok(new ChatResponse(result.content(), result.success()));
    }

    /**
     * 流式对话接口(SSE)
     */
    @PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamChat(@RequestBody @Valid ChatRequest request) {
        return chatService.streamUserMessage(request.sessionId(), request.message())
            .map(chunk -> "data: " + chunk + "\n\n");
    }

    /**
     * 获取对话历史
     */
    @GetMapping("/conversations/{sessionId}/messages")
    public ResponseEntity<List<Message>> getHistory(@PathVariable String sessionId) {
        return ResponseEntity.ok(chatService.getConversationHistory(sessionId));
    }

    /**
     * 结束对话
     */
    @PostMapping("/conversations/{sessionId}/close")
    public ResponseEntity<Void> closeConversation(@PathVariable String sessionId) {
        chatService.closeConversation(sessionId);
        return ResponseEntity.ok().build();
    }

    public record ChatRequest(
        @NotBlank String sessionId,
        @NotBlank String message
    ) {}

    public record ChatResponse(String content, boolean success) {}
}

11.5 Vue3 前端实现

<!-- ChatView.vue -->
<template>
  <div class="chat-container">
    <div class="chat-header">
      <h2>AI 客服</h2>
      <span class="status" :class="{ online: isConnected }">
        {{ isConnected ? '在线' : '离线' }}
      </span>
    </div>

    <div class="messages" ref="messagesRef">
      <div
        v-for="(msg, index) in messages"
        :key="index"
        class="message"
        :class="msg.role"
      >
        <div class="avatar">
          {{ msg.role === 'user' ? '👤' : '🤖' }}
        </div>
        <div class="content">
          <div class="text" v-html="formatMessage(msg.content)"></div>
          <div class="time">{{ formatTime(msg.timestamp) }}</div>
        </div>
      </div>

      <div v-if="isStreaming" class="message assistant">
        <div class="avatar">🤖</div>
        <div class="content">
          <div class="typing-indicator">
            <span></span><span></span><span></span>
          </div>
        </div>
      </div>
    </div>

    <div class="input-area">
      <textarea
        v-model="inputText"
        @keydown.enter.exact.prevent="sendMessage"
        placeholder="输入您的问题..."
        :disabled="isStreaming"
      ></textarea>
      <button @click="sendMessage" :disabled="!inputText.trim() || isStreaming">
        发送
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, nextTick, onMounted } from 'vue'

const messages = ref([])
const inputText = ref('')
const isStreaming = ref(false)
const isConnected = ref(true)
const messagesRef = ref(null)
const sessionId = ref(crypto.randomUUID())

async function sendMessage() {
  const text = inputText.value.trim()
  if (!text || isStreaming.value) return

  inputText.value = ''

  messages.value.push({
    role: 'user',
    content: text,
    timestamp: new Date()
  })

  const aiMessage = {
    role: 'assistant',
    content: '',
    timestamp: new Date()
  }
  messages.value.push(aiMessage)
  isStreaming.value = true

  await scrollToBottom()

  try {
    const response = await fetch('/api/customer-service/chat/stream', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        sessionId: sessionId.value,
        message: text
      })
    })

    const reader = response.body.getReader()
    const decoder = new TextDecoder()

    while (true) {
      const { done, value } = await reader.read()
      if (done) break

      const text = decoder.decode(value)
      const lines = text.split('\n')

      for (const line of lines) {
        if (line.startsWith('data: ')) {
          aiMessage.content += line.slice(6)
          messages.value = [...messages.value]
          await scrollToBottom()
        }
      }
    }
  } catch (error) {
    aiMessage.content = '抱歉,服务暂时不可用,请稍后重试。'
  } finally {
    isStreaming.value = false
  }
}

function formatMessage(content) {
  return content
    .replace(/\n/g, '<br>')
    .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
}

function formatTime(date) {
  return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}

async function scrollToBottom() {
  await nextTick()
  if (messagesRef.value) {
    messagesRef.value.scrollTop = messagesRef.value.scrollHeight
  }
}
</script>

<style scoped>
.chat-container {
  max-width: 800px;
  margin: 0 auto;
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: #f5f5f5;
}

.chat-header {
  padding: 16px 24px;
  background: #1a73e8;
  color: white;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.messages {
  flex: 1;
  overflow-y: auto;
  padding: 24px;
}

.message {
  display: flex;
  gap: 12px;
  margin-bottom: 16px;
}

.message.user {
  flex-direction: row-reverse;
}

.message .avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #e0e0e0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  flex-shrink: 0;
}

.message .content {
  max-width: 70%;
}

.message .text {
  padding: 12px 16px;
  border-radius: 12px;
  line-height: 1.6;
  font-size: 15px;
}

.message.assistant .text {
  background: white;
  border: 1px solid #e0e0e0;
}

.message.user .text {
  background: #1a73e8;
  color: white;
}

.input-area {
  padding: 16px 24px;
  background: white;
  border-top: 1px solid #e0e0e0;
  display: flex;
  gap: 12px;
}

.input-area textarea {
  flex: 1;
  padding: 12px;
  border: 1px solid #ddd;
  border-radius: 8px;
  resize: none;
  height: 48px;
  font-size: 15px;
}

.input-area button {
  padding: 12px 24px;
  background: #1a73e8;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 15px;
}

.input-area button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.typing-indicator span {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #999;
  margin: 0 2px;
  animation: bounce 1.4s infinite ease-in-out;
}

.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }

@keyframes bounce {
  0%, 80%, 100% { transform: scale(0); }
  40% { transform: scale(1); }
}
</style>

总结

本教程系统性地讲解了 Spring Boot + Spring AI 构建企业级 AI 应用的完整技术栈。核心要点回顾:

  1. Spring AI 统一抽象:ChatClient、EmbeddingClient、ImageClient 屏蔽了底层模型差异,切换模型只需改配置
  2. Function Calling 是 AI Agent 的基础:让大模型能够调用外部工具,获取实时数据
  3. RAG 是企业知识库的核心:通过 VectorStore + Advisor 模式实现文档检索增强
  4. 流式输出提升体验:SSE 实现逐字输出,减少用户等待焦虑
  5. 对话记忆是多轮对话的关键:支持窗口记忆、Token 限制和持久化
  6. 安全是企业应用的生命线:认证授权、内容安全过滤、数据脱敏缺一不可
  7. 生产部署要关注性能和可观测性:连接池、异步处理、指标监控

通过本教程的学习,你将能够独立构建一个完整的企业级 AI 应用系统。Spring Boot + Spring AI 的组合,让 Java 开发者也能轻松驾驭大模型应用开发。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录