最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Spring?AI?Alibaba?+?Ollama?實戰(zhàn)教程(基于本地?Qwen3?的?Spring?Boot?大模型應(yīng)用)

 更新時間:2025年12月23日 14:35:19   作者:中國胖子風(fēng)清揚  
本文介紹了如何使用SpringAI和SpringAIAlibaba將Java/SpringBoot應(yīng)用與阿里云通義(Qwen)和靈積平臺(DashScope)等模型服務(wù)無縫對接,實現(xiàn)了在本地開發(fā)調(diào)試和云端生產(chǎn)環(huán)境的無縫切換,本文介紹的非常詳細,感興趣的朋友一起看看吧

在大模型快速演進的今天,Java 開發(fā)者同樣希望“開箱即用”地接入各類模型服務(wù)。Spring 官方推出的 Spring AI,已經(jīng)為 Java / Spring Boot 應(yīng)用提供了一套統(tǒng)一、優(yōu)雅的 AI 抽象;而在國內(nèi)模型生態(tài)中,如何更好地對接阿里云通義(Qwen)與靈積平臺(DashScope),則是 Spring AI Alibaba 重點解決的問題。

本文基于倉庫中的 spring_ai_alibaba-demo 子項目,從真實代碼出發(fā),帶你一起拆解:如何用 Spring AI + Spring AI Alibaba 的生態(tài),在本地通過 Ollama 跑 Qwen3 模型,并逐步擴展到 RAG、工具調(diào)用和 Graph 工作流。

GitHub 項目地址:https://github.com/zhouByte-hub/java-ai/tree/main/spring_ai_alibaba-demo
歡迎 Star、Fork 和關(guān)注!文中所有代碼都可以在該子項目中找到,更適合邊讀邊跑。

面向讀者:

  • 已有 Spring Boot 基礎(chǔ),希望快速接入大模型的后端開發(fā);
  • 計劃在本地或內(nèi)網(wǎng)環(huán)境使用 Qwen3 等模型(通過 Ollama),但又希望未來平滑切到阿里云 DashScope;
  • 想了解 Spring AI Alibaba 在 Graph、RAG、工具調(diào)用等場景中的作用和優(yōu)勢。

一、項目概覽:Spring AI + Spring AI Alibaba 在這個 Demo 里的分工

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

  • 根模塊 spring_ai_alibaba-demo
    • 使用 Spring AIspring-ai-starter-model-ollama 接入本地 Ollama 服務(wù);
    • 使用 spring-ai-starter-vector-store-pgvector 集成 PostgreSQL + PgVector 做向量檢索;
    • 通過 ChatModel / ChatClient 演示基礎(chǔ)對話、RAG、工具調(diào)用和記憶;
    • 通過依賴管理引入 spring-ai-alibaba-bom,為后續(xù)接入阿里云生態(tài)(包括 DashScope、Graph 等)奠定基礎(chǔ)。
  • 子模塊 alibaba-graph
    • 使用 spring-ai-alibaba-graph-core 演示基于大模型的有狀態(tài)流程(StateGraph),依然以 Ollama 的 Qwen3 作為底層模型;
  • 子模塊 alibaba-mcp-server / alibaba-mcp-client
    • 使用 Spring AI 的 MCP 能力演示模型調(diào)用外部工具 / 資源的模式。

換句話說:

當(dāng)前 Demo 沒有直接連阿里云 DashScope,而是選擇在本地通過 Ollama 運行 Qwen3 模型;
但項目在依賴管理和結(jié)構(gòu)設(shè)計上,已經(jīng)完全站在 Spring AI Alibaba 生態(tài) 之上,隨時可以切換到阿里云在線服務(wù)。

接下來,我們按“從簡單到復(fù)雜”的順序,依次看看各個模塊是怎么搭建的。

二、依賴與環(huán)境:本地 Qwen3 + PgVector

先看根模塊 spring_ai_alibaba-demo/pom.xml 中的關(guān)鍵部分:

<properties>
    <!-- 項目使用的 JDK 版本 -->
    <java.version>17</java.version>
    <!-- Spring AI Alibaba 相關(guān)依賴統(tǒng)一使用的版本 -->
    <spring-ai-alibaba.version>1.1.0.0-M5</spring-ai-alibaba.version>
    <!-- Spring AI 核心依賴統(tǒng)一使用的版本 -->
    <spring-ai.version>1.1.0</spring-ai.version>
</properties>
<dependencies>
    <!-- 基礎(chǔ) Web 能力:提供 Spring MVC / 內(nèi)嵌容器等 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 通過 Spring AI 訪問本地 Ollama 大模型服務(wù) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-ollama</artifactId>
    </dependency>
    <!-- 向量數(shù)據(jù)庫:Spring AI 對 PgVector 的封裝 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
    </dependency>
    <!-- PostgreSQL JDBC 驅(qū)動,用于訪問數(shù)據(jù)庫 -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <!-- Spring AI Alibaba 統(tǒng)一版本管理(國內(nèi)生態(tài)相關(guān)依賴) -->
        <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>

這里體現(xiàn)了幾個核心設(shè)計理念:

  • 通過 BOMspring-ai-alibaba-bom + spring-ai-bom)統(tǒng)一版本管理,避免各個 Starter 之間的版本地獄;
  • 實際運行時模型選擇 Ollama,既方便本地開發(fā)調(diào)試,又可以在網(wǎng)絡(luò)受限場景下順暢運行;
  • 未來如果要切到阿里云 DashScope,只需要:
    • 打開已經(jīng)寫好的(但當(dāng)前被注釋掉的) spring-ai-alibaba-starter-dashscope 依賴;
    • 在配置文件里增加 spring.ai.dashscope.* 對應(yīng)配置,不需要改業(yè)務(wù)代碼。

環(huán)境配置:Ollama + Qwen3 + PgVector

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

server:
  port: 8081                 # 應(yīng)用監(jiān)聽端口
  servlet:
    context-path: /alibaba-ai  # 統(tǒng)一的服務(wù)前綴
spring:
  ai:
    ollama:
      base-url: http://localhost:11434  # 本地 Ollama 服務(wù)地址
      chat:
        options:
          model: qwen3:0.6b             # 聊天用的 Qwen3 模型名稱
          temperature: 0.8              # 采樣溫度,越高回答越發(fā)散
      embedding:
        options:
          model: qwen3-embedding:0.6b   # 用于向量化的 embedding 模型
    vectorstore:
      pgvector:
        dimensions: 1024                # 向量維度,需要與 embedding 模型輸出一致
        distance-type: cosine_distance  # 相似度度量方式
        initialize-schema: true         # 啟動時自動創(chuàng)建 PgVector 表結(jié)構(gòu)
  datasource:
    url: jdbc:postgresql://<your-host>:5432/postgres?serverTimezone=Asia/Shanghai  # PostgreSQL 連接串
    username: postgres
    password: ****                      # 建議通過環(huán)境變量或配置中心注入
  • Ollama 在本機 11434 端口提供服務(wù),加載的是 qwen3:0.6b 模型(本質(zhì)上仍然是阿里云通義家族的模型,只是以本地方式運行);
  • Embedding 使用 qwen3-embedding:0.6b;
  • PgVector 存儲維度設(shè)置為 1024,采用余弦相似度;
  • 數(shù)據(jù)源配置指向 PostgreSQL,用于向量存儲和(可選)對話記憶持久化。

三、基礎(chǔ)對話:從 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))     // 以流式方式調(diào)用大模型
                .map(ChatResponse::getResult)                  // 提取每個增量響應(yīng)的結(jié)果對象
                .mapNotNull(result -> result.getOutput().getText()); // 只保留最終輸出的文本內(nèi)容
    }
}
  • ChatModelspring-ai-starter-model-ollama 自動裝配,底層指向本地 Qwen3 模型;
  • .stream(...) 返回的是一個 響應(yīng)式 Flux,可以在前端按 token/片段逐步渲染;
  • 控制器本身和普通 Spring Web 控制器沒有本質(zhì)差別,學(xué)習(xí)成本非常低。

3.2 使用 ChatClient 提升可用性

ChatClientController

@RestController
@RequestMapping("/chatClient")
public class ChatClientController {
    // 基于 ChatModel 封裝的高級客戶端,后續(xù)可以掛接 Adviser、工具等能力
    private final ChatClient ollamaChatClient;
    public ChatClientController(ChatClient ollamaChatClient) {
        this.ollamaChatClient = ollamaChatClient;
    }
    @GetMapping("/chat")
    public Flux<String> stream(@RequestParam("message") String message) {
        // 使用最簡單的 Prompt,直接將用戶輸入交給大模型,并以流式方式返回結(jié)果
        return ollamaChatClient
                .prompt(new Prompt(message))  // 構(gòu)造 Prompt 對象
                .stream()                     // 流式調(diào)用
                .content();                   // 提取文本內(nèi)容
    }
    @GetMapping("/prompt")
    public Flux<String> prompt() {
        PromptTemplate template = PromptTemplate.builder()
                .template("請用簡短中文回答:{question}") // 模板中定義占位符 {question}
                .variables(Map.of())                   // 這里可以預(yù)先聲明變量,也可以在 create 時傳入
                .build();
        // 使用實際問題填充模板變量
        Prompt prompt = template.create(Map.of("question", "Spring AI Alibaba 有什么特點?"));
        return ollamaChatClient.prompt(prompt).stream().content();
    }
}

ChatModel 相比,ChatClient 的優(yōu)勢在于:

  • 提供鏈?zhǔn)?API:.prompt().call()/stream(),更易讀;
  • 更容易掛接 Adviser(記憶、RAG、工具等),形成統(tǒng)一調(diào)用入口;
  • 在需要多輪交互、上下文管理時更易擴展。

OllamaConfig 中,Demo 還展示了如何為 ChatClient 掛接記憶 Adviser,后面章節(jié)會展開。

四、對話記憶:內(nèi)存版與可擴展版

實際業(yè)務(wù)中,一個“傻傻忘記前文”的大模型體驗非常差。Demo 中給出了兩種記憶實現(xiàn)方式。

4.1 簡單內(nèi)存記憶: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:會話標(biāo)識;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) {
        // 根據(jù)會話 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();
        }
    }
}
  • 通過 conversationId 區(qū)分不同會話;
  • 適合 Demo、PoC 或?qū)煽啃砸蟛桓叩膱鼍埃?/li>
  • 結(jié)合 MessageChatMemoryAdvisor 可以自動把歷史消息注入到當(dāng)前 Prompt 中。

4.2 Adviser 方式:MemoriesAdviser

@Component
public class MemoriesAdviser implements BaseAdvisor {
    private static final Map<String, List<Message>> MEMORIES = new HashMap<>();
    // 用于在 ChatClient 的上下文中標(biāo)識當(dāng)前會話 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<>());
        // 當(dāng)前請求的消息放到歷史消息后面,一起交給大模型
        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) {
        // 把本次大模型回復(fù)寫回到對應(yīng)會話的記憶中
        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;
    }
}
  • before 中把歷史消息 + 當(dāng)前消息拼成一個新的 Prompt;
  • after 中把模型回復(fù)寫回內(nèi)存;
  • 通過在 ChatClient 構(gòu)建時添加 defaultAdvisors(memoriesAdvisor),即可對所有請求啟用記憶能力。

進一步,你可以把 DataBaseChatMemoryRepository 補充完整,將消息寫入數(shù)據(jù)庫,實現(xiàn)持久化對話記憶。

五、RAG:Qwen3 + PgVector 的檢索增強

RAG(Retrieval Augmented Generation)是典型的企業(yè)級能力,本 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:待向量化的原始文本內(nèi)容
        TokenTextSplitter splitter = TokenTextSplitter.builder()
                .withChunkSize(50)                 // 每個分片的最大 token 數(shù)
                .withKeepSeparator(true)           // 是否保留分隔符(如換行符)
                .withMaxNumChunks(1024)            // 單次允許生成的最大分片數(shù)
                .withMinChunkLengthToEmbed(20)     // 小于該長度的分片不入庫,避免噪聲
                .withMinChunkSizeChars(10)         // 切分時的最小字符數(shù),避免切得過碎
                .build();
        List<Document> docs = splitter.split(Document.builder().text(message).build()); // 將文本切分為多個 Document
        pgVectorStore.add(docs); // 寫入 PgVector 向量庫
    }
}
  • TokenTextSplitter 基于 token 切分文檔,避免切得過碎或過長;
  • PgVectorStore.add 將切分后的文檔寫入 PostgreSQL + PgVector;
  • 真實項目中可把 /embedding 換成異步批處理任務(wù)。

5.2 RAG 對話:RetrievalAugmentationAdvisor

@Configuration
public class VectorChatClientConfig {
    @Bean("ragChatClient")
    public ChatClient ragChatClient(ChatModel chatModel, VectorStore vectorStore) {
        VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
                .vectorStore(vectorStore)     // 具體使用的向量庫實現(xiàn),這里是 PgVector
                .topK(3)                      // 每次檢索返回相似度最高的前 3 條文檔
                .similarityThreshold(0.5)     // 相似度閾值,小于該值的文檔會被過濾掉
                .build();
        RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
                .documentRetriever(retriever) // 指定文檔檢索器
                .order(0)                     // Adviser 執(zhí)行順序,越小越先執(zhí)行
                .build();
        return ChatClient.builder(chatModel)
                .defaultAdvisors(advisor)     // 默認(rèn)啟用 RAG 能力
                .build();
    }
}
  • RetrievalAugmentationAdvisor 會在每次請求前,先到向量庫檢索相關(guān)文檔;
  • 然后把檢索結(jié)果作為“系統(tǒng)提示詞”或“上下文”塞給 Qwen3 模型;
  • 對你來說,只需調(diào)用 ragChatClient.prompt().user(question).call(),就能得到“帶知識庫”的回答。

六、工具調(diào)用:用 @Tool 讓模型調(diào)用你的 Java 方法

在很多場景中,大模型需要調(diào)用業(yè)務(wù)系統(tǒng)的 API 才能完成任務(wù)。Spring AI 提供了 @Tool 注解,Demo 中的 ZoomTool 便是一個簡單示例。

6.1 定義工具:ZoomTool

@Component
public class ZoomTool {
    @Tool(description = "通過時區(qū) ID 獲取當(dāng)前時間")
    public String getTimeByZone(@ToolParam(description = "時區(qū) ID,比如 Asia/Shanghai") String zone) {
        // zone:時區(qū) 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:提供獲取時間的業(yè)務(wù)工具
        return ChatClient.builder(ollamaChatModel)
                .defaultSystem(this.systemPrompt()) // 設(shè)置默認(rèn)的系統(tǒng)提示詞,統(tǒng)一咖啡館背景
                .defaultTools(zoomTool)             // 將 ZoomTool 注冊為可調(diào)用的工具
                .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"); // 默認(rèn)時區(qū) ID
        SystemPromptTemplate tpl = SystemPromptTemplate.builder()
                .template("歡迎光臨 ZhouByte咖啡館,... 默認(rèn)時區(qū):{TIME_ZONE}") // 系統(tǒng)提示詞模板
                .variables(vars)                                          // 綁定上面的變量
                .build();
        return tpl.render(); // 渲染出包含具體變量值的系統(tǒng)提示詞
    }
}

對應(yīng)的 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()          // 創(chuàng)建一次新的對話請求
                .user(message)     // 添加一條用戶消息
                .stream()          // 流式調(diào)用大模型
                .content();        // 只提取文本內(nèi)容返回
    }
}
  • 模型可以在需要時自動調(diào)用 getTimeByZone,返回指定時區(qū)時間;
  • 你只需要編寫普通的 Java 方法,剩下的交給 Spring AI 的工具調(diào)用機制。

七、Alibaba Graph 子項目:有狀態(tài)工作流編排

spring_ai_alibaba-demo/alibaba-graph 子項目使用 spring-ai-alibaba-graph-core 演示了如何構(gòu)建大模型工作流。

7.1 定義 Graph:StateGraph + CompiledGraph

GraphConfig 中:

@Configuration
public class GraphConfig {
    @Bean("quickStartGraph")
    public CompiledGraph quickStartGraph() throws GraphStateException {
        // "quickStartGraph":圖名稱;后面的 Map 用于定義狀態(tài) key 的合并策略
        StateGraph graph = new StateGraph("quickStartGraph", () -> Map.of(
                "input", new ReplaceStrategy(),  // 多次寫入時,后寫入的值覆蓋之前的值
                "output", new ReplaceStrategy()
        ));
        graph.addNode("node1", AsyncNodeAction.node_async(state -> {
            // node1:設(shè)置初始 input 和 output
            return Map.of("input", "graphConfig_addNode", "output", "graphConfig_output");
        }));
        graph.addNode("node2", AsyncNodeAction.node_async(state -> {
            // node2:模擬業(yè)務(wù)處理,將 input 改為 ZhouByte
            return Map.of("input", "ZhouByte", "output", "EMPTY");
        }));
        // 定義執(zhí)行順序:START -> node1 -> node2 -> END
        graph.addEdge(StateGraph.START, "node1")
             .addEdge("node1", "node2")
             .addEdge("node2", StateGraph.END);
        return graph.compile();
    }
}
  • StateGraph 描述節(jié)點、邊和狀態(tài)合并策略;
  • AsyncNodeAction 封裝每個節(jié)點的執(zhí)行邏輯;
  • compile() 得到可執(zhí)行的 CompiledGraph。

7.2 調(diào)用 Graph:WebFlux + 流式輸出

GraphController

@RestController
@RequestMapping("/v1")
public class GraphController {
    @Resource
    private CompiledGraph quickStartGraph;
    @GetMapping("/graph")
    public Flux<String> startGraph() {
        // 這里傳入空的初始狀態(tài) Map,按定義好的 StateGraph 順序執(zhí)行
        return quickStartGraph.stream(Map.of())
                .map(NodeOutput::toString); // 將每個節(jié)點的輸出對象轉(zhuǎn)換為字符串返回
    }
}
  • 使用 WebFlux + Flux<NodeOutput> 將節(jié)點執(zhí)行結(jié)果流式返回;
  • 通過 RunnableConfig.builder().threadId(conversationId) 還可以實現(xiàn)“帶會話 ID 的工作流”,類似有狀態(tài) Agent。

7.3 quickStartGraph 執(zhí)行流程圖

結(jié)合上面的 GraphConfigGraphController/v1/graph 接口整體執(zhí)行流程可以用下面這張流程圖來表示(以 GitHub 為例,可以直接渲染 Mermaid):

  • GraphController.startGraph() 開始,調(diào)用 CompiledGraph.stream(Map.of()) 啟動圖的執(zhí)行;
  • 圖從 StateGraph.START 出發(fā),依次流經(jīng) node1、node2,最終到達 StateGraph.END;
  • 每個節(jié)點都會向全局狀態(tài)寫入 input / output 等字段,并以 Flux<NodeOutput> 的形式逐步返回給調(diào)用方。

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

在實際業(yè)務(wù)中,Graph 往往不只是線性順序,還會根據(jù)狀態(tài)進行分支判斷。spring-ai-alibaba-graph-core 提供了 addConditionalEdges,可以基于當(dāng)前 OverAllState 計算「條件標(biāo)簽」,再根據(jù)標(biāo)簽跳轉(zhuǎn)到不同節(jié)點。

下面是一個簡化的「評分決策」示例,根據(jù) score 分?jǐn)?shù)分別走向通過 / 復(fù)核 / 拒絕三條路徑:

@Configuration
public class ConditionalGraphConfig {
    @Bean("scoreDecisionGraph")
    public CompiledGraph scoreDecisionGraph() throws GraphStateException {
        StateGraph graph = new StateGraph("scoreDecisionGraph", () -> Map.of(
                "score", new ReplaceStrategy(),   // 保存當(dāng)前評分
                "result", new ReplaceStrategy()   // 保存決策結(jié)果
        ));
        // 讀取或設(shè)置評分(示例中從 state 中讀取,實際可由外部請求傳入)
        graph.addNode("checkScore", AsyncNodeAction.node_async(state -> {
            Integer score = (Integer) state.value("score").orElse(75); // 默認(rèn) 75 分
            return Map.of("score", score);
        }));
        // 三個業(yè)務(wù)分支節(jié)點:通過 / 復(fù)核 / 拒絕
        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")));
        // 起點先進入評分檢查節(jié)點
        graph.addEdge(StateGraph.START, "checkScore");
        // 多條件邊:根據(jù) score 返回不同的“標(biāo)簽”,再由 mappings 決定下一跳節(jié)點
        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"
                )
        );
        // 三個結(jié)果節(jié)點最終都指向 END
        graph.addEdge("pass", StateGraph.END);
        graph.addEdge("review", StateGraph.END);
        graph.addEdge("reject", StateGraph.END);
        return graph.compile();
    }
}

這段代碼中,addConditionalEdges 的三個參數(shù)含義是:

  • sourceId:條件邊的源節(jié)點 ID,這里是 "checkScore";
  • AsyncEdgeAction:根據(jù)當(dāng)前 OverAllState 計算條件標(biāo)簽,這里返回 "PASS" / "REVIEW" / "REJECT";
  • mappings:標(biāo)簽與目標(biāo)節(jié)點 ID 的映射,例如 "PASS" -> "pass",即當(dāng)標(biāo)簽為 "PASS" 時跳到 pass 節(jié)點。

對應(yīng)的執(zhí)行流程,可以畫成如下多分支流程圖:

在真實項目中,你可以把 score 換成「風(fēng)控評分」「召回結(jié)果命中情況」「用戶畫像標(biāo)簽」等任意業(yè)務(wù)信號,通過 addConditionalEdges 把復(fù)雜分支邏輯從代碼 if/else 中抽離出來,統(tǒng)一放在 Graph 層管理。

在這個子項目中,Graph 本身是“流程層”,可以在節(jié)點里調(diào)用 Spring AI / Spring AI Alibaba 的各種模型與工具,實現(xiàn)復(fù)雜的多步推理與業(yè)務(wù)編排。

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

雖然當(dāng)前 Demo 主要跑在本地 Ollama 上,但由于使用了 Spring AI + Spring AI Alibaba 的統(tǒng)一抽象,切換到阿里云 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}      # 從環(huán)境變量或配置中心讀取 DashScope 的 API Key
      endpoint: https://dashscope.aliyuncs.com
      chat:
        options:
          model: qwen-plus              # 使用的通義千問在線模型
          temperature: 0.8              # 采樣溫度
          max-tokens: 2048              # 單次回答的最大 token 數(shù)
  1. 將原來的 ChatModel 注入點從 Ollama 替換為 DashScope 對應(yīng)的 Bean(通常只需要調(diào)整配置,不改業(yè)務(wù)代碼)。

憑借 Spring AI 的抽象層,你可以:

  • 開發(fā)階段:本地跑 Qwen3(Ollama),成本低、調(diào)試快;
  • 生產(chǎn)階段:切到云上 DashScope(Qwen-Max / Qwen-Plus 等),享受更強算力和更高可用性;
  • 中長期:在 Spring AI Alibaba 的生態(tài)內(nèi)同時兼容多家國內(nèi)模型廠商。

九、實踐建議與最佳實踐

  • 配置管理
    • API Key 使用環(huán)境變量或配置中心(Nacos、KMS 等),避免硬編碼;
    • Ollama、DashScope 的模型名稱、溫度等參數(shù)盡量抽到配置文件中。
  • 錯誤處理與重試
    • 針對網(wǎng)絡(luò)異常、超時、限流等場景做兜底和重試策略;
    • 對外暴露的接口統(tǒng)一封裝錯誤返回,避免直接把底層錯誤拋給前端。
  • 性能與成本
    • 在高并發(fā)場景建議優(yōu)先使用流式輸出 + 前端增量渲染;
    • RAG 中控制 TopK、相似度閾值和切分策略,避免向量庫“爆炸”。
  • 代碼結(jié)構(gòu)
    • ChatClient 配置、Graph 配置等放在獨立的 config 包中,業(yè)務(wù)層只關(guān)心接口調(diào)用;
    • 工具方法使用 @Tool 暴露,便于模型統(tǒng)一管理和調(diào)用。
  • 版本與升級
    • Spring AI Alibaba 當(dāng)前仍以 Milestone 版本為主(如 1.1.0.0-M5),升級前建議閱讀 release notes;
    • 保持對 spring-ai-bom / spring-ai-alibaba-bom 的依賴,讓升級盡量在 BOM 層完成。

十、總結(jié)與展望

基于 spring_ai_alibaba-demo 子項目,我們實際體驗了一次:

  • 如何用 Spring AI + Spring AI Alibaba BOM 快速接入本地 Qwen3(Ollama);
  • 如何在同一套抽象下串聯(lián)起對話、記憶、RAG、工具調(diào)用;
  • 如何通過 spring-ai-alibaba-graph-core 構(gòu)建基于大模型的有狀態(tài)工作流;
  • 以及如何在不改業(yè)務(wù)代碼的前提下,為未來切換到阿里云 DashScope 留出空間。

對 Spring 開發(fā)者來說,這套體系最大的價值在于:

  • 統(tǒng)一抽象:不同模型供應(yīng)商之間切換成本極低;
  • 生態(tài)完善:兼容 Spring Boot、WebFlux、向量庫、MCP、Graph 等豐富組件;
  • 本地 + 云端雙模:既能在本地快速迭代,又能無縫遷移到云上生產(chǎn)環(huán)境。

再次附上示例子項目 GitHub 地址,歡迎你親手跑一跑代碼、提 Issue、點 Star:

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

到此這篇關(guān)于Spring AI Alibaba + Ollama 實戰(zhàn)教程(基于本地 Qwen3 的 Spring Boot 大模型應(yīng)用)的文章就介紹到這了,更多相關(guān)Spring AI Alibaba Ollama 實戰(zhàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

湘潭县| 招远市| 辽源市| 安陆市| 武清区| 丘北县| 竹溪县| 龙泉市| 延庆县| 忻城县| 老河口市| 阿勒泰市| 邻水| 东阿县| 社旗县| 恩施市| 屯留县| 巴中市| 临江市| 梁平县| 承德市| 翁源县| 江华| 托里县| 钟山县| 株洲市| 当雄县| 宝应县| 阳朔县| 当涂县| 潼南县| 咸阳市| 澳门| 杭州市| 贵州省| 灵丘县| 青州市| 屯昌县| 米林县| 台南市| 灵宝市|