java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring AI Alibaba  Ollama 实战

Spring AI Alibaba + Ollama 实战教程(基于本地 Qwen3 的 Spring Boot 大模型应用)

作者:中国胖子风清扬

本文介绍了如何使用SpringAI和SpringAIAlibaba将Java/SpringBoot应用与阿里云通义(Qwen)和灵积平台(DashScope)等模型服务无缝对接,实现了在本地开发调试和云端生产环境的无缝切换,本文介绍的非常详细,感兴趣的朋友一起看看吧

在大模型快速演进的今天,Java 开发者同样希望“开箱即用”地接入各类模型服务。Spring 官方推出的 Spring AI,已经为 Java / Spring Boot 应用提供了一套统一、优雅的 AI 抽象;而在国内模型生态中,如何更好地对接阿里云通义(Qwen)与灵积平台(DashScope),则是 Spring AI Alibaba 重点解决的问题。

本文基于仓库中的 spring_ai_alibaba-demo 子项目,从真实代码出发,带你一起拆解:如何用 Spring AI + Spring AI Alibaba 的生态,在本地通过 Ollama 跑 Qwen3 模型,并逐步扩展到 RAG、工具调用和 Graph 工作流。

GitHub 项目地址:https://github.com/zhouByte-hub/java-ai/tree/main/spring_ai_alibaba-demo
欢迎 Star、Fork 和关注!文中所有代码都可以在该子项目中找到,更适合边读边跑。

面向读者:

一、项目概览:Spring AI + Spring AI Alibaba 在这个 Demo 里的分工

spring_ai_alibaba-demo 是一个多模块示例工程,核心模块包括:

换句话说:

当前 Demo 没有直接连阿里云 DashScope,而是选择在本地通过 Ollama 运行 Qwen3 模型
但项目在依赖管理和结构设计上,已经完全站在 Spring AI Alibaba 生态 之上,随时可以切换到阿里云在线服务。

接下来,我们按“从简单到复杂”的顺序,依次看看各个模块是怎么搭建的。

二、依赖与环境:本地 Qwen3 + PgVector

先看根模块 spring_ai_alibaba-demo/pom.xml 中的关键部分:

<properties>
    <!-- 项目使用的 JDK 版本 -->
    <java.version>17</java.version>
    <!-- Spring AI Alibaba 相关依赖统一使用的版本 -->
    <spring-ai-alibaba.version>1.1.0.0-M5</spring-ai-alibaba.version>
    <!-- Spring AI 核心依赖统一使用的版本 -->
    <spring-ai.version>1.1.0</spring-ai.version>
</properties>
<dependencies>
    <!-- 基础 Web 能力:提供 Spring MVC / 内嵌容器等 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 通过 Spring AI 访问本地 Ollama 大模型服务 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-ollama</artifactId>
    </dependency>
    <!-- 向量数据库:Spring AI 对 PgVector 的封装 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
    </dependency>
    <!-- PostgreSQL JDBC 驱动,用于访问数据库 -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <!-- Spring AI Alibaba 统一版本管理(国内生态相关依赖) -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-bom</artifactId>
            <version>${spring-ai-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- Spring AI 官方 BOM(核心抽象与 Starter 的版本对齐) -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

这里体现了几个核心设计理念:

环境配置:Ollama + Qwen3 + PgVector

spring_ai_alibaba-demo/src/main/resources/application.yaml 中:

server:
  port: 8081                 # 应用监听端口
  servlet:
    context-path: /alibaba-ai  # 统一的服务前缀
spring:
  ai:
    ollama:
      base-url: http://localhost:11434  # 本地 Ollama 服务地址
      chat:
        options:
          model: qwen3:0.6b             # 聊天用的 Qwen3 模型名称
          temperature: 0.8              # 采样温度,越高回答越发散
      embedding:
        options:
          model: qwen3-embedding:0.6b   # 用于向量化的 embedding 模型
    vectorstore:
      pgvector:
        dimensions: 1024                # 向量维度,需要与 embedding 模型输出一致
        distance-type: cosine_distance  # 相似度度量方式
        initialize-schema: true         # 启动时自动创建 PgVector 表结构
  datasource:
    url: jdbc:postgresql://<your-host>:5432/postgres?serverTimezone=Asia/Shanghai  # PostgreSQL 连接串
    username: postgres
    password: ****                      # 建议通过环境变量或配置中心注入

三、基础对话:从 ChatModel 到 ChatClient

Demo 中提供了两种对话方式:直接使用 ChatModel,以及通过 ChatClient 封装后的高级用法。

3.1 使用 ChatModel 流式返回

ChatModelController

@RestController
@RequestMapping("/chatModel")
public class ChatModelController {
    // 注入由 Spring AI 自动装配的 Ollama ChatModel
    private final ChatModel ollamaChatModel;
    public ChatModelController(ChatModel ollamaChatModel) {
        this.ollamaChatModel = ollamaChatModel;
    }
    @GetMapping("/chat")
    public Flux<String> chat(@RequestParam("message") String message) {
        // message:用户输入的自然语言问题
        return ollamaChatModel.stream(new Prompt(message))     // 以流式方式调用大模型
                .map(ChatResponse::getResult)                  // 提取每个增量响应的结果对象
                .mapNotNull(result -> result.getOutput().getText()); // 只保留最终输出的文本内容
    }
}

3.2 使用 ChatClient 提升可用性

ChatClientController

@RestController
@RequestMapping("/chatClient")
public class ChatClientController {
    // 基于 ChatModel 封装的高级客户端,后续可以挂接 Adviser、工具等能力
    private final ChatClient ollamaChatClient;
    public ChatClientController(ChatClient ollamaChatClient) {
        this.ollamaChatClient = ollamaChatClient;
    }
    @GetMapping("/chat")
    public Flux<String> stream(@RequestParam("message") String message) {
        // 使用最简单的 Prompt,直接将用户输入交给大模型,并以流式方式返回结果
        return ollamaChatClient
                .prompt(new Prompt(message))  // 构造 Prompt 对象
                .stream()                     // 流式调用
                .content();                   // 提取文本内容
    }
    @GetMapping("/prompt")
    public Flux<String> prompt() {
        PromptTemplate template = PromptTemplate.builder()
                .template("请用简短中文回答:{question}") // 模板中定义占位符 {question}
                .variables(Map.of())                   // 这里可以预先声明变量,也可以在 create 时传入
                .build();
        // 使用实际问题填充模板变量
        Prompt prompt = template.create(Map.of("question", "Spring AI Alibaba 有什么特点?"));
        return ollamaChatClient.prompt(prompt).stream().content();
    }
}

ChatModel 相比,ChatClient 的优势在于:

OllamaConfig 中,Demo 还展示了如何为 ChatClient 挂接记忆 Adviser,后面章节会展开。

四、对话记忆:内存版与可扩展版

实际业务中,一个“傻傻忘记前文”的大模型体验非常差。Demo 中给出了两种记忆实现方式。

4.1 简单内存记忆:SimpleMemories

@Component
public class SimpleMemories implements ChatMemory {
    private static final Map<String, List<Message>> MEMORIES_CACHE = new HashMap<>();
    @Override
    public void add(String conversationId, List<Message> messages) {
        // conversationId:会话标识;messages:本轮新增的消息列表
        List<Message> memories = MEMORIES_CACHE.getOrDefault(conversationId, new ArrayList<>());
        if (messages != null && !messages.isEmpty()) {
            memories.addAll(messages);
        }
        MEMORIES_CACHE.put(conversationId, memories);
    }
    @Override
    public List<Message> get(String conversationId) {
        // 根据会话 ID 取出该会话的历史消息
        return MEMORIES_CACHE.getOrDefault(conversationId, new ArrayList<>());
    }
    @Override
    public void clear(String conversationId) {
        // 清空某个会话的记忆
        List<Message> messages = MEMORIES_CACHE.get(conversationId);
        if (messages != null) {
            messages.clear();
        }
    }
}

4.2 Adviser 方式:MemoriesAdviser

@Component
public class MemoriesAdviser implements BaseAdvisor {
    private static final Map<String, List<Message>> MEMORIES = new HashMap<>();
    // 用于在 ChatClient 的上下文中标识当前会话 ID 的 key
    private static final String CHAT_MEMORIES_SESSION_ID = "chat_memories_session_id";
    @Override
    public ChatClientRequest before(ChatClientRequest request, AdvisorChain chain) {
        // 从上下文中读取会话 ID,并取出其历史消息
        String sessionId = request.context().get(CHAT_MEMORIES_SESSION_ID).toString();
        List<Message> messages = MEMORIES.getOrDefault(sessionId, new ArrayList<>());
        // 当前请求的消息放到历史消息后面,一起交给大模型
        messages.addAll(request.prompt().getInstructions());
        Prompt prompt = request.prompt().mutate().messages(messages).build();
        return request.mutate().prompt(prompt).build();
    }
    @Override
    public ChatClientResponse after(ChatClientResponse response, AdvisorChain chain) {
        // 把本次大模型回复写回到对应会话的记忆中
        AssistantMessage output = response.chatResponse().getResult().getOutput();
        String sessionId = response.context().get(CHAT_MEMORIES_SESSION_ID).toString();
        List<Message> messages = MEMORIES.getOrDefault(sessionId, new ArrayList<>());
        messages.add(output);
        MEMORIES.put(sessionId, messages);
        return response;
    }
}

进一步,你可以把 DataBaseChatMemoryRepository 补充完整,将消息写入数据库,实现持久化对话记忆。

五、RAG:Qwen3 + PgVector 的检索增强

RAG(Retrieval Augmented Generation)是典型的企业级能力,本 Demo 通过 RagChatClientController 进行演示。

5.1 向量入库:TokenTextSplitter + PgVectorStore

@RestController
@RequestMapping("/rag")
public class RagChatClientController {
    private final ChatClient ragChatClient;
    private final PgVectorStore pgVectorStore;
    public RagChatClientController(ChatClient ragChatClient, PgVectorStore pgVectorStore) {
        this.ragChatClient = ragChatClient;
        this.pgVectorStore = pgVectorStore;
    }
    @GetMapping("/embedding")
    public void embeddingContent(@RequestParam("message") String message) {
        // message:待向量化的原始文本内容
        TokenTextSplitter splitter = TokenTextSplitter.builder()
                .withChunkSize(50)                 // 每个分片的最大 token 数
                .withKeepSeparator(true)           // 是否保留分隔符(如换行符)
                .withMaxNumChunks(1024)            // 单次允许生成的最大分片数
                .withMinChunkLengthToEmbed(20)     // 小于该长度的分片不入库,避免噪声
                .withMinChunkSizeChars(10)         // 切分时的最小字符数,避免切得过碎
                .build();
        List<Document> docs = splitter.split(Document.builder().text(message).build()); // 将文本切分为多个 Document
        pgVectorStore.add(docs); // 写入 PgVector 向量库
    }
}

5.2 RAG 对话:RetrievalAugmentationAdvisor

@Configuration
public class VectorChatClientConfig {
    @Bean("ragChatClient")
    public ChatClient ragChatClient(ChatModel chatModel, VectorStore vectorStore) {
        VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
                .vectorStore(vectorStore)     // 具体使用的向量库实现,这里是 PgVector
                .topK(3)                      // 每次检索返回相似度最高的前 3 条文档
                .similarityThreshold(0.5)     // 相似度阈值,小于该值的文档会被过滤掉
                .build();
        RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
                .documentRetriever(retriever) // 指定文档检索器
                .order(0)                     // Adviser 执行顺序,越小越先执行
                .build();
        return ChatClient.builder(chatModel)
                .defaultAdvisors(advisor)     // 默认启用 RAG 能力
                .build();
    }
}

六、工具调用:用 @Tool 让模型调用你的 Java 方法

在很多场景中,大模型需要调用业务系统的 API 才能完成任务。Spring AI 提供了 @Tool 注解,Demo 中的 ZoomTool 便是一个简单示例。

6.1 定义工具:ZoomTool

@Component
public class ZoomTool {
    @Tool(description = "通过时区 ID 获取当前时间")
    public String getTimeByZone(@ToolParam(description = "时区 ID,比如 Asia/Shanghai") String zone) {
        // zone:时区 ID,示例:Asia/Shanghai、Europe/Berlin
        ZoneId zoneId = ZoneId.of(zone);
        ZonedDateTime now = ZonedDateTime.now(zoneId);
        return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(now); // 返回格式化后的时间字符串
    }
}

6.2 将工具挂到 ChatClient 上

@Configuration
public class ToolChatClientConfig {
    @Bean("toolChatClient")
    public ChatClient toolChatClient(ChatModel ollamaChatModel, ZoomTool zoomTool) {
        // ollamaChatModel:底层使用的 Qwen3 模型;zoomTool:提供获取时间的业务工具
        return ChatClient.builder(ollamaChatModel)
                .defaultSystem(this.systemPrompt()) // 设置默认的系统提示词,统一咖啡馆背景
                .defaultTools(zoomTool)             // 将 ZoomTool 注册为可调用的工具
                .build();
    }
    private String systemPrompt() {
        Map<String, Object> vars = new HashMap<>();
        vars.put("AMERICAN", "1-3");          // 美式咖啡制作时间(分钟)
        vars.put("LATTE", "2");               // 拿铁咖啡制作时间(分钟)
        vars.put("TIME_ZONE", "Asia/Shanghai"); // 默认时区 ID
        SystemPromptTemplate tpl = SystemPromptTemplate.builder()
                .template("欢迎光临 ZhouByte咖啡馆,... 默认时区:{TIME_ZONE}") // 系统提示词模板
                .variables(vars)                                          // 绑定上面的变量
                .build();
        return tpl.render(); // 渲染出包含具体变量值的系统提示词
    }
}

对应的 Controller:

@RestController
@RequestMapping("/tool")
public class ToolChatController {
    private final ChatClient toolChatClient;
    public ToolChatController(ChatClient toolChatClient) {
        this.toolChatClient = toolChatClient;
    }
    @GetMapping("/chat")
    public Flux<String> chat(@RequestParam("message") String message) {
        return toolChatClient
                .prompt()          // 创建一次新的对话请求
                .user(message)     // 添加一条用户消息
                .stream()          // 流式调用大模型
                .content();        // 只提取文本内容返回
    }
}

七、Alibaba Graph 子项目:有状态工作流编排

spring_ai_alibaba-demo/alibaba-graph 子项目使用 spring-ai-alibaba-graph-core 演示了如何构建大模型工作流。

7.1 定义 Graph:StateGraph + CompiledGraph

GraphConfig 中:

@Configuration
public class GraphConfig {
    @Bean("quickStartGraph")
    public CompiledGraph quickStartGraph() throws GraphStateException {
        // "quickStartGraph":图名称;后面的 Map 用于定义状态 key 的合并策略
        StateGraph graph = new StateGraph("quickStartGraph", () -> Map.of(
                "input", new ReplaceStrategy(),  // 多次写入时,后写入的值覆盖之前的值
                "output", new ReplaceStrategy()
        ));
        graph.addNode("node1", AsyncNodeAction.node_async(state -> {
            // node1:设置初始 input 和 output
            return Map.of("input", "graphConfig_addNode", "output", "graphConfig_output");
        }));
        graph.addNode("node2", AsyncNodeAction.node_async(state -> {
            // node2:模拟业务处理,将 input 改为 ZhouByte
            return Map.of("input", "ZhouByte", "output", "EMPTY");
        }));
        // 定义执行顺序:START -> node1 -> node2 -> END
        graph.addEdge(StateGraph.START, "node1")
             .addEdge("node1", "node2")
             .addEdge("node2", StateGraph.END);
        return graph.compile();
    }
}

7.2 调用 Graph:WebFlux + 流式输出

GraphController

@RestController
@RequestMapping("/v1")
public class GraphController {
    @Resource
    private CompiledGraph quickStartGraph;
    @GetMapping("/graph")
    public Flux<String> startGraph() {
        // 这里传入空的初始状态 Map,按定义好的 StateGraph 顺序执行
        return quickStartGraph.stream(Map.of())
                .map(NodeOutput::toString); // 将每个节点的输出对象转换为字符串返回
    }
}

7.3 quickStartGraph 执行流程图

结合上面的 GraphConfigGraphController/v1/graph 接口整体执行流程可以用下面这张流程图来表示(以 GitHub 为例,可以直接渲染 Mermaid):

7.4 多条件分支 Graph 示例(addConditionalEdges)

在实际业务中,Graph 往往不只是线性顺序,还会根据状态进行分支判断。spring-ai-alibaba-graph-core 提供了 addConditionalEdges,可以基于当前 OverAllState 计算「条件标签」,再根据标签跳转到不同节点。

下面是一个简化的「评分决策」示例,根据 score 分数分别走向通过 / 复核 / 拒绝三条路径:

@Configuration
public class ConditionalGraphConfig {
    @Bean("scoreDecisionGraph")
    public CompiledGraph scoreDecisionGraph() throws GraphStateException {
        StateGraph graph = new StateGraph("scoreDecisionGraph", () -> Map.of(
                "score", new ReplaceStrategy(),   // 保存当前评分
                "result", new ReplaceStrategy()   // 保存决策结果
        ));
        // 读取或设置评分(示例中从 state 中读取,实际可由外部请求传入)
        graph.addNode("checkScore", AsyncNodeAction.node_async(state -> {
            Integer score = (Integer) state.value("score").orElse(75); // 默认 75 分
            return Map.of("score", score);
        }));
        // 三个业务分支节点:通过 / 复核 / 拒绝
        graph.addNode("pass", AsyncNodeAction.node_async(state ->
                Map.of("result", "PASS")));
        graph.addNode("review", AsyncNodeAction.node_async(state ->
                Map.of("result", "REVIEW")));
        graph.addNode("reject", AsyncNodeAction.node_async(state ->
                Map.of("result", "REJECT")));
        // 起点先进入评分检查节点
        graph.addEdge(StateGraph.START, "checkScore");
        // 多条件边:根据 score 返回不同的“标签”,再由 mappings 决定下一跳节点
        graph.addConditionalEdges("checkScore",
                AsyncEdgeAction.edge_async(state -> {
                    int score = (Integer) state.value("score").orElse(0);
                    if (score >= 80) {
                        return "PASS";
                    }
                    if (score >= 60) {
                        return "REVIEW";
                    }
                    return "REJECT";
                }),
                Map.of(
                        "PASS", "pass",
                        "REVIEW", "review",
                        "REJECT", "reject"
                )
        );
        // 三个结果节点最终都指向 END
        graph.addEdge("pass", StateGraph.END);
        graph.addEdge("review", StateGraph.END);
        graph.addEdge("reject", StateGraph.END);
        return graph.compile();
    }
}

这段代码中,addConditionalEdges 的三个参数含义是:

对应的执行流程,可以画成如下多分支流程图:

在真实项目中,你可以把 score 换成「风控评分」「召回结果命中情况」「用户画像标签」等任意业务信号,通过 addConditionalEdges 把复杂分支逻辑从代码 if/else 中抽离出来,统一放在 Graph 层管理。

在这个子项目中,Graph 本身是“流程层”,可以在节点里调用 Spring AI / Spring AI Alibaba 的各种模型与工具,实现复杂的多步推理与业务编排。

八、如何从本地 Ollama 平滑切到阿里云 DashScope

虽然当前 Demo 主要跑在本地 Ollama 上,但由于使用了 Spring AI + Spring AI Alibaba 的统一抽象,切换到阿里云 DashScope 十分简单:

  1. pom.xml 中启用 DashScope Starter(示例中已给出注释代码):
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    <version>1.1.0.0-M5</version>
</dependency>
  1. 在配置文件中增加 DashScope 的配置(示例):
spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}      # 从环境变量或配置中心读取 DashScope 的 API Key
      endpoint: https://dashscope.aliyuncs.com
      chat:
        options:
          model: qwen-plus              # 使用的通义千问在线模型
          temperature: 0.8              # 采样温度
          max-tokens: 2048              # 单次回答的最大 token 数
  1. 将原来的 ChatModel 注入点从 Ollama 替换为 DashScope 对应的 Bean(通常只需要调整配置,不改业务代码)。

凭借 Spring AI 的抽象层,你可以:

九、实践建议与最佳实践

十、总结与展望

基于 spring_ai_alibaba-demo 子项目,我们实际体验了一次:

对 Spring 开发者来说,这套体系最大的价值在于:

再次附上示例子项目 GitHub 地址,欢迎你亲手跑一跑代码、提 Issue、点 Star:

GitHub 项目地址:https://github.com/zhouByte-hub/java-ai/tree/main/spring_ai_alibaba-demo

到此这篇关于Spring AI Alibaba + Ollama 实战教程(基于本地 Qwen3 的 Spring Boot 大模型应用)的文章就介绍到这了,更多相关Spring AI Alibaba Ollama 实战内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文