[控场AI]
· 14 分钟阅读· 7,340 字

Spring AI 入门实战:5步构建Java AI应用

Spring AI 入门实战:5步构建Java AI应用

Spring AI 五步入门:从本地 Ollama 对话到结构化输出、工具调用与流式响应。

本文面向 Java 开发者,介绍如何用 Spring AI 在 Spring Boot 应用中快速集成大语言模型。全文以本地 Ollama 运行 qwen2.5:3b 为起点,依次演示五个递进步骤:通过 ChatClient 实现最基础的对话接口;借助 `.entity()` 把模型输出直接反序列化为 Java Record,实现结构化输出;使用 `@Tool` 与 `@ToolParam` 注解让模型调用本地 Java 方法获取实时数据、规避幻觉;用 PromptTemplate 实现提示词的动态渲染与集中管理;最后通过 Flux<String> 与 SSE 实现打字机式流式响应。文章强调 Spring AI 以熟悉的依赖注入和流式构建器 API 降低了 Java 团队的 AI 集成门槛,并指出 RAG、多模态与工具编排是后续进阶方向。

为什么 Java 开发者需要关注 Spring AI

在生成式 AI 席卷软件开发的时代,Java 生态的开发者不必再羡慕 Python 的便利。Spring AI 为 Spring Boot 应用提供了与大型语言模型(LLM)交互的一站式抽象,让你能用熟悉的注解和依赖注入,快速构建 AI 功能。无论是接入本地 Ollama 模型,还是调用云端 API,Spring AI 都大幅降低了集成门槛。

据技术博客 Spring AI: First Steps 的实践分享,从零开始只需五个步骤,即可实现从“Hello World”到实时流式响应的完整能力。本文基于该教程,结合 Spring AI 的核心设计理念,为你梳理一条清晰的学习路径。

Spring AI 入门示意图

准备工作:运行本地模型

Spring AI 支持多种模型提供商,但入门阶段最便捷的方式是使用 Ollama 在本地运行开源模型。首先确保 Ollama 服务已启动,并下载所需模型(例如 qwen2.5:3b):

ollama pull qwen2.5:3b

然后在 Spring Boot 的 application.properties 中配置连接信息:

spring.ai.ollama.chat.options.model=qwen2.5:3b
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.temperature=0.7

temperature 控制输出的随机性:0.0 趋向确定性,1.0 更具创造性。你可以替换为 ollama list 中出现的任何模型,名称必须完全一致。

Ollama 是一个开源的本地大模型运行工具,它把模型权重下载、推理引擎和 HTTP API 封装在一起,让开发者无需 GPU 集群或云服务账号即可在笔记本上运行开源模型。其架构分为两层:Ollama 守护进程负责加载模型并暴露 REST 接口(默认端口 11434),CLI 则用于拉取、列出和运行模型。Spring AI 的 Ollama Starter 本质上是把这个 REST 接口封装成 Spring 的自动配置 Bean。需要留意的是,本地模型的推理速度取决于显存大小和量化精度,qwen2.5:3b 这类 30 亿参数模型在消费级硬件上通常能获得可接受的响应速度,但与云端大模型在推理能力上有明显差距,入门验证流程足够,生产场景需评估模型能力是否匹配。

第一步:AI Hello World——最简单的对话接口

创建一个 REST 控制器,注入 ChatClient.Builder 并构建 ChatClient。以下代码实现了向模型发送固定提示并返回文本响应的端点:

@RestController
public class AiController {
    private final ChatClient chatClient;

    public AiController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/ai/hello")
    public String aiHello() {
        String prompt = "Say 'Hello, World!' and add one short, interesting fact about the Java language.";
        return chatClient.prompt()
                .user(prompt)
                .call()
                .content();
    }
}

访问 http://localhost:8080/ai/hello 即可看到模型回复。首次请求可能耗时 2–5 秒,因为模型需要加载到内存。如果遇到 404 模型未找到错误,请核对 ollama list 中的模型名称。.call().content() 是同步调用;若需打字机效果,后续会介绍 .stream()。

这个例子展示了 Spring AI 最基础的 API 风格:通过流式构建器(fluent API)组合提示词、调用模型并提取内容。

第二步:结构化输出——让 LLM 返回 Java 对象

实际业务中,我们往往需要模型输出结构化的数据,而非纯文本。Spring AI 支持直接将 JSON 响应反序列化为 Java Record。定义一个 Record 表示路由意图:

public record RoutingIntent(
    String targetService,
    String action,
    String userId,
    int priority
) {}

然后在控制器中添加端点,使用 .entity() 方法指定目标类型:

@GetMapping("/ai/route")
public RoutingIntent routeRequest(@RequestParam String text) {
    String systemPrompt = """
        You are a smart API gateway. Analyze the user's request 
        and extract data for routing. 
        If a field is unknown, leave it empty (null or 0).
        """;

    return chatClient.prompt()
            .system(systemPrompt)
            .user(text)
            .call()
            .entity(RoutingIntent.class);
}

请求 http://localhost:8080/ai/route?text=Disable notifications for user user-999,Spring 会自动将模型返回的 JSON 序列化为 RoutingIntent 对象。这消除了手动解析 JSON 的繁琐工作,让 LLM 的输出直接融入业务逻辑。

结构化输出背后依赖的是 LLM 的 JSON 模式或函数调用式约束解码能力。Spring AI 在调用 .entity(Class) 时,会把目标 Java 类型转换成 JSON Schema,并通过提示词或模型提供商的原生结构化输出参数(如 OpenAI 的 response_format)约束模型只能产出符合该 Schema 的 JSON,再由 Jackson 反序列化为对象。这意味着两点工程约束:其一,模型必须支持结构化输出或具备足够的指令遵循能力,小参数模型可能返回格式错误的 JSON 导致反序列化异常;其二,Spring AI 默认会进行重试与格式修复,但对复杂嵌套类型仍建议在提示词中显式列出字段名和取值范围。对于字段可选性,Java Record 的包装类型(如 Integer 而非 int)能正确表达 null 语义,避免模型缺省字段时被强制赋零值。

第三步:函数调用——让 LLM 调用 Java 方法

函数调用(Function Calling)是 AI 应用的关键能力:模型可以主动请求执行本地代码,从而获取实时数据或执行操作。Spring AI 通过 @Tool 注解简化了这一过程。

创建一个 SystemTools 服务,其中方法用 @Tool 标注,参数用 @ToolParam 描述:

@Service
public class SystemTools {
    @Tool(description = "Checks the current status and latency (in ms) of the specified microservice")
    public String getServiceStatus(@ToolParam(description = "Name of the service, e.g., 'payment', 'auth', 'database'") String serviceName) {
        // 模拟调用监控系统
        return switch (serviceName.toLowerCase()) {
            case "payment" -> "Status: OK, average latency 45ms.";
            case "auth" -> "Status: DEGRADED, latency 300ms, experiencing timeouts.";
            default -> "Status: Unknown service or no data available.";
        };
    }
}

在控制器中,通过 ChatClient.Builder.defaultTools(systemTools) 注册工具:

public AiController(ChatClient.Builder chatClientBuilder, SystemTools systemTools) {
    this.chatClient = chatClientBuilder
            .defaultTools(systemTools)
            .build();
}

之后,当用户提问“payment 和 auth 服务当前状态如何?”,模型会识别出需要调用 getServiceStatus 方法,Spring AI 执行后返回结果,模型再基于真实数据生成最终回答。这避免了模型“幻觉”,确保了信息的准确性。

函数调用(Function Calling) 的机制是:应用把可用工具的名称、描述和参数 Schema 一并发给模型,模型并不直接执行代码,而是返回一个结构化的“调用意图”,由 Spring AI 在本地反射执行对应 Java 方法,再把执行结果作为一条工具消息回传给模型进行二次推理,最终生成自然语言回答。因此 @Tool 的 description 和 @ToolParam 的描述质量直接决定模型能否在正确时机选中正确工具,这本质上是一种提示词工程。安全层面需特别注意:工具方法的入参来自模型生成,必须做白名单校验和权限控制,避免模型被诱导调用高危操作;同时应为工具执行设置超时,防止外部依赖阻塞整个对话链路。

第四步:提示词模板——动态生成提示

硬编码提示词不利于维护和复用。Spring AI 提供了 PromptTemplate,支持类似 Mustache 的变量占位符。创建一个端点,接收用户输入并渲染模板:

@GetMapping("/ai/template")
public String generateWithTemplate(
        @RequestParam String userName,
        @RequestParam String serviceName,
        @RequestParam String action
) {
    String templateText = """
        You are a system administrator. User {userName} wants to perform the action '{action}' 
        on the {serviceName} service. 
        Provide a brief instruction on how to do this safely.
        """;

    PromptTemplate promptTemplate = new PromptTemplate(templateText);
    String finalPrompt = promptTemplate.render(Map.of(
            "userName", userName,
            "serviceName", serviceName,
            "action", action
    ));

    return chatClient.prompt()
            .user(finalPrompt)
            .call()
            .content();
}

模板可以存放在单独的文件(.txt 或 .prompt)中,实现与代码分离和版本管理。这种方式让提示词工程更加系统化。

第五步:流式响应——实现打字机效果

对于聊天界面或语音助手,等待完整响应会损害用户体验。流式传输允许 token 逐个返回,前端可以实时渲染。Spring AI 返回 Flux<String>,Spring Boot 自动将其转换为 Server-Sent Events (SSE)。

首先添加 WebFlux 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

然后创建流式端点:

@GetMapping(value = "/ai/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE + ";charset=UTF-8")
public Flux<String> streamResponse(@RequestParam String question) {
    return chatClient.prompt()
            .user(question)
            .stream()
            .content();
}

配套的前端页面(stream-test.html)使用 fetch 读取流并逐块解析 SSE 数据,动态更新界面。完整代码见原文,核心逻辑是 reader.read() 循环解码字节流,提取 data: 行内容并追加到 DOM。打开 http://localhost:8080/stream-test.html 即可体验逐词输出的效果。

Server-Sent Events(SSE) 是一种基于 HTTP 的单向服务器推送协议,响应头为 text/event-stream,服务端以 data: 开头的文本行持续推送事件,浏览器端通过 EventSource 或 fetch 的流式读取接口消费。相比 WebSocket,SSE 天然支持断线重连、走标准 HTTP 端口、无需额外协议升级,非常适合 LLM 这种“一问一答、服务端持续吐字”的场景。Spring AI 的 .stream() 返回 Reactor 的 Flux<String>,Spring MVC 或 WebFlux 会将其适配为 SSE 帧;引入 spring-boot-starter-webflux 是为了获得 Reactor 与响应式编解码支持。实践中有两个常见坑:一是需要在 produces 中显式声明 charset=UTF-8,否则中文可能乱码;二是反向代理(如 Nginx)默认会缓冲响应,需关闭缓冲才能真正看到逐字输出。

总结与进阶方向

通过这五个步骤,你已掌握 Spring AI 的核心用法:基础对话、结构化输出、函数调用、提示模板和流式响应。这些能力足以构建许多实用的 AI 集成场景,例如智能客服、动态路由、系统监控问答等。

Spring AI 仍在快速发展,后续可以探索向量数据库集成(RAG)、多模态输入、以及更复杂的工具编排。对于 Java 团队而言,Spring AI 提供了一条低摩擦的 AI 落地路径,值得深入实践。

本文实践步骤基于技术博客 Spring AI: First Steps,代码示例已按原文逻辑整理。

#Spring AI#Java AI 开发#Ollama#函数调用#流式响应#Spring Boot#提示词模板#结构化输出
分享:

相关推荐