Spring AI 框架实战:Java 后端集成大模型的架构设计与工程落地

一、API 调用之外的工程挑战:Java 应用集成 LLM 的真实痛点

当团队决定在 Java 后端系统中集成大语言模型时,最常见的起点是直接用 RestTemplateWebClient 调用 OpenAI 的 HTTP API。这种做法在 Demo 阶段看似可行,但一旦进入生产环境,问题便接踵而至:不同模型供应商的 API 格式各异(OpenAI、Azure OpenAI、Anthropic、国内模型),切换供应商需要重写调用逻辑;流式响应(SSE)的处理缺乏统一抽象;Prompt 模板散落在业务代码中,难以版本管理和 A/B 测试;调用失败时的重试与降级策略缺失,导致级联故障。

Spring AI 正是为了解决这些工程痛点而诞生的框架。它提供了一套统一的模型调用抽象、Prompt 管理机制、向量存储接口和 RAG(检索增强生成)管道,让 Java 开发者能够以 Spring 风格的方式集成大模型,而非在业务代码中堆砌 HTTP 调用。

本文将从架构视角出发,完整演示 Spring AI 在生产环境中的集成方案,覆盖模型调用、Prompt 管理、RAG 管道和可观测性四个核心环节。

二、从 ChatModel 到 VectorStore:Spring AI 的核心抽象与协作机制

Spring AI 的架构设计遵循 Spring 的一贯哲学:通过接口抽象屏蔽实现差异,通过自动装配降低集成成本。

flowchart TB
    subgraph 应用层
        A[业务服务] --> B[ChatClient]
        A --> C[PromptTemplate]
        A --> D[RAG 管道]
    end

    subgraph Spring AI 核心抽象
        B --> E[ChatModel 接口]
        C --> F[Prompt 管理]
        D --> G[DocumentReader]
        D --> H[DocumentTransformer]
        D --> I[VectorStore 接口]
    end

    subgraph 模型实现
        E --> J[OpenAI ChatModel]
        E --> K[Azure OpenAI]
        E --> L[Ollama ChatModel]
        E --> M[其他供应商]
    end

    subgraph 向量存储实现
        I --> N[PgVector]
        I --> O[Chroma]
        I --> P[Milvus]
        I --> Q[Redis]
    end

    subgraph 可观测性
        B --> R[Micrometer 指标]
        E --> S[Token 用量统计]
        B --> T[OpenTelemetry Tracing]
    end

ChatModel 接口是 Spring AI 的核心抽象,定义了与 LLM 交互的统一契约。无论是 OpenAI、Azure OpenAI 还是本地部署的 Ollama,都通过实现 ChatModel 接口来适配。应用代码只依赖接口,切换模型供应商只需修改配置,无需改动业务逻辑。

PromptTemplate 提供了参数化的 Prompt 管理能力。Prompt 不再硬编码在 Java 代码中,而是以模板文件的形式外部化管理,支持变量替换和条件逻辑。这使得 Prompt 的版本管理、A/B 测试和跨团队协作成为可能。

VectorStore 接口统一了向量存储的访问方式。无论是 PgVector、Chroma 还是 Milvus,应用代码都通过 VectorStore 接口进行向量写入和相似度检索,底层实现可随时替换。

三、生产级 Spring AI 集成:从模型调用到 RAG 管道

下面给出一个完整的生产级 Spring AI 集成方案,包含模型调用、Prompt 管理、RAG 管道和异常处理。

Maven 依赖配置:

<dependencies>
    <!-- Spring AI BOM 统一版本管理 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- OpenAI 模型适配器 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>

    <!-- PgVector 向量存储适配器 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
    </dependency>
</dependencies>

应用配置:

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
          max-tokens: 2048
    vectorstore:
      pgvector:
        index-type: HNSW
        distance-type: COSINE
        dimensions: 1536

Prompt 模板管理(外部化文件):

<!-- src/main/resources/prompts/customer-support.st -->
你是一个专业的客户支持助手。请根据以下知识库内容回答用户问题。

知识库内容:
{{knowledge}}

用户问题:{{question}}

回答要求:
1. 仅基于知识库内容回答,不要编造信息
2. 如果知识库中没有相关信息,明确告知用户
3. 回答需简洁、准确、专业

RAG 管道服务实现:

/**
 * RAG 增强的客户支持服务
 * 将用户问题与知识库向量检索结合,生成有依据的回答
 */
@Service
@Slf4j
public class CustomerSupportRagService {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;
    private final PromptTemplate promptTemplate;

    public CustomerSupportRagService(ChatClient.Builder chatClientBuilder,
                                     VectorStore vectorStore) {
        this.chatClient = chatClientBuilder.build();
        this.vectorStore = vectorStore;

        // 加载外部化 Prompt 模板
        var promptResource = new ClassPathResource("prompts/customer-support.st");
        this.promptTemplate = new PromptTemplate(promptResource);
    }

    /**
     * RAG 查询主流程:检索 -> 构建 Prompt -> 调用模型
     *
     * @param question 用户问题
     * @return 基于知识库的回答
     */
    public String query(String question) {
        // 1. 向量检索:从知识库中找到与问题最相关的文档片段
        List<Document> relevantDocs = retrieveRelevantDocuments(question);

        // 2. 拼接知识库上下文
        String knowledge = relevantDocs.stream()
                .map(Document::getContent)
                .collect(Collectors.joining("\n\n"));

        // 3. 构建 Prompt
        Prompt prompt = promptTemplate.create(Map.of(
                "knowledge", knowledge,
                "question", question
        ));

        // 4. 调用模型,带重试与降级
        try {
            ChatResponse response = chatClient.prompt(prompt)
                    .call()
                    .chatResponse();

            // 记录 Token 用量,用于成本监控
            logTokenUsage(response);

            return response.getResult().getOutput().getContent();

        } catch (ApiException e) {
            log.error("LLM API 调用失败,状态码: {}, 原因: {}",
                      e.getStatusCode(), e.getMessage());
            return generateFallbackAnswer(question, relevantDocs);
        }
    }

    /**
     * 向量检索:基于问题语义查找最相关的知识库片段
     * 限制 Top-K 和相似度阈值,避免无关内容干扰
     */
    private List<Document> retrieveRelevantDocuments(String question) {
        SearchRequest searchRequest = SearchRequest.builder()
                .query(question)
                .topK(5)                    // 返回最相关的 5 个片段
                .similarityThreshold(0.7)   // 相似度阈值,过滤低质量结果
                .build();

        return vectorStore.similaritySearch(searchRequest);
    }

    /**
     * 降级策略:当 LLM 调用失败时,返回检索到的原始文档片段
     * 保证服务可用性,避免因模型不可用导致完全无响应
     */
    private String generateFallbackAnswer(String question,
                                          List<Document> relevantDocs) {
        if (relevantDocs.isEmpty()) {
            return "抱歉,当前无法获取回答,请稍后重试。";
        }
        StringBuilder sb = new StringBuilder("以下是与您问题相关的知识库内容:\n\n");
        for (int i = 0; i < relevantDocs.size(); i++) {
            sb.append("[").append(i + 1).append("] ")
              .append(relevantDocs.get(i).getContent()).append("\n\n");
        }
        sb.append("(系统暂时无法生成总结,以上为原始检索结果)");
        return sb.toString();
    }

    /**
     * 记录 Token 用量,用于成本监控与预算控制
     */
    private void logTokenUsage(ChatResponse response) {
        Usage usage = response.getMetadata().getUsage();
        log.info("Token 用量 - Prompt: {}, Completion: {}, Total: {}",
                 usage.getPromptTokens(),
                 usage.getGenerationTokens(),
                 usage.getTotalTokens());
    }
}

知识库文档导入服务:

/**
 * 知识库文档导入服务
 * 将文档分块、生成向量嵌入并写入 VectorStore
 */
@Service
public class KnowledgeBaseIngestionService {

    private final VectorStore vectorStore;

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

    /**
     * 导入文本文档到知识库
     * 分块策略:按段落分割,每块不超过 500 Token,重叠 50 Token
     */
    public void ingestDocument(String documentId, String content) {
        // 文档分块:控制块大小以平衡检索精度与上下文完整性
        TextSplitter splitter = new TokenTextSplitter(
                500,   // 每块最大 Token 数
                50,    // 块间重叠 Token 数
                5,     // 最小块大小
                10000, // 最大块大小
                true   // 保留分隔符
        );

        Document originalDoc = new Document(documentId, content,
                Map.of("source", documentId, "ingestedAt",
                       Instant.now().toString()));

        List<Document> chunks = splitter.split(originalDoc);

        // 写入向量存储,Spring AI 自动调用 Embedding 模型生成向量
        vectorStore.add(chunks);
    }
}

四、Token 成本与延迟抖动:Spring AI 集成的架构权衡

Spring AI 显著降低了 Java 应用集成 LLM 的工程复杂度,但引入大模型调用链路后,系统面临的新约束必须被正视。

第一,Token 成本的不可预测性。 LLM 的计费基于 Token 数量,而用户输入的长度和模型的输出长度在请求前无法精确预估。一个看似简单的 RAG 查询,可能因为检索到大量知识库片段而导致 Prompt Token 数飙升。在 Spring AI 的配置中,max-tokens 参数可以限制输出长度,但无法限制输入长度。必须在业务层实现 Token 预算控制:在调用模型前估算 Prompt Token 数,超出预算时截断知识库片段或拒绝请求。

第二,模型调用的延迟不确定性。 LLM 的推理延迟从数百毫秒到数十秒不等,取决于输入长度、输出长度和模型负载。Spring AI 默认使用同步调用模式,一个慢请求会阻塞业务线程。对于延迟敏感的接口,必须使用 stream() 方法进行流式调用,或设置合理的超时时间。Spring AI 的 ChatClient 支持通过 RestClient.Builder 自定义超时配置。

第三,向量检索的精度与召回率权衡。 RAG 管道的质量高度依赖向量检索的效果。相似度阈值设置过高会导致召回不足(知识库中有答案但未被检索到),设置过低则引入噪声(检索到不相关的内容,干扰模型回答)。similarityThreshold 参数需要根据业务场景反复调优,没有通用的最优值。

适用边界: Spring AI 适合需要集成多种模型供应商、管理复杂 Prompt、构建 RAG 管道的企业级 Java 应用。对于只需要调用单一模型 API 的简单场景,直接使用 HTTP 客户端可能更轻量。Spring AI 的抽象层引入了一定的学习成本和框架约束,团队需要评估其收益是否大于引入成本。

五、总结

Spring AI 为 Java 后端集成大模型提供了一套完整的工程化方案:ChatModel 接口统一了模型调用抽象,PromptTemplate 实现了 Prompt 的外部化管理,VectorStore 接口屏蔽了向量存储的实现差异,RAG 管道将检索与生成串联为端到端流程。降级策略和 Token 用量监控则是生产环境不可或缺的保障机制。

然而,Token 成本的不可预测性、推理延迟的不确定性、向量检索的精度与召回率权衡,都是架构师在设计 LLM 集成方案时必须纳入考量的约束条件。Spring AI 提供的是工具和抽象,而非银弹。

落地路线建议:第一步,以单一模型供应商(如 OpenAI)为起点,验证 Spring AI 的基本调用链路;第二步,将硬编码的 Prompt 迁移为外部化模板,建立 Prompt 版本管理流程;第三步,引入 VectorStore 和 RAG 管道,构建知识库增强的问答能力;第四步,实现 Token 预算控制和延迟监控,将 LLM 调用纳入系统的可观测性体系。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐