Spring?AI?Alibaba?+?Ollama?實(shí)戰(zhàn)教程(基于本地?Qwen3?的?Spring?Boot?大模型應(yīng)用)
在大模型快速演進(jìn)的今天,Java 開發(fā)者同樣希望“開箱即用”地接入各類模型服務(wù)。Spring 官方推出的 Spring AI,已經(jīng)為 Java / Spring Boot 應(yīng)用提供了一套統(tǒng)一、優(yōu)雅的 AI 抽象;而在國(guó)內(nèi)模型生態(tài)中,如何更好地對(duì)接阿里云通義(Qwen)與靈積平臺(tái)(DashScope),則是 Spring AI Alibaba 重點(diǎn)解決的問(wèn)題。
本文基于倉(cāng)庫(kù)中的 spring_ai_alibaba-demo 子項(xiàng)目,從真實(shí)代碼出發(fā),帶你一起拆解:如何用 Spring AI + Spring AI Alibaba 的生態(tài),在本地通過(guò) Ollama 跑 Qwen3 模型,并逐步擴(kuò)展到 RAG、工具調(diào)用和 Graph 工作流。
GitHub 項(xiàng)目地址:https://github.com/zhouByte-hub/java-ai/tree/main/spring_ai_alibaba-demo
歡迎 Star、Fork 和關(guān)注!文中所有代碼都可以在該子項(xiàng)目中找到,更適合邊讀邊跑。
面向讀者:
- 已有 Spring Boot 基礎(chǔ),希望快速接入大模型的后端開發(fā);
- 計(jì)劃在本地或內(nèi)網(wǎng)環(huán)境使用 Qwen3 等模型(通過(guò) Ollama),但又希望未來(lái)平滑切到阿里云 DashScope;
- 想了解 Spring AI Alibaba 在 Graph、RAG、工具調(diào)用等場(chǎng)景中的作用和優(yōu)勢(shì)。
一、項(xiàng)目概覽:Spring AI + Spring AI Alibaba 在這個(gè) Demo 里的分工
spring_ai_alibaba-demo 是一個(gè)多模塊示例工程,核心模塊包括:
- 根模塊
spring_ai_alibaba-demo:- 使用 Spring AI 的
spring-ai-starter-model-ollama接入本地 Ollama 服務(wù); - 使用
spring-ai-starter-vector-store-pgvector集成 PostgreSQL + PgVector 做向量檢索; - 通過(guò)
ChatModel/ChatClient演示基礎(chǔ)對(duì)話、RAG、工具調(diào)用和記憶; - 通過(guò)依賴管理引入
spring-ai-alibaba-bom,為后續(xù)接入阿里云生態(tài)(包括 DashScope、Graph 等)奠定基礎(chǔ)。
- 使用 Spring AI 的
- 子模塊
alibaba-graph:- 使用
spring-ai-alibaba-graph-core演示基于大模型的有狀態(tài)流程(StateGraph),依然以 Ollama 的 Qwen3 作為底層模型;
- 使用
- 子模塊
alibaba-mcp-server/alibaba-mcp-client:- 使用 Spring AI 的 MCP 能力演示模型調(diào)用外部工具 / 資源的模式。
換句話說(shuō):
當(dāng)前 Demo 沒(méi)有直接連阿里云 DashScope,而是選擇在本地通過(guò) Ollama 運(yùn)行 Qwen3 模型;
但項(xiàng)目在依賴管理和結(jié)構(gòu)設(shè)計(jì)上,已經(jīng)完全站在 Spring AI Alibaba 生態(tài) 之上,隨時(shí)可以切換到阿里云在線服務(wù)。
接下來(lái),我們按“從簡(jiǎn)單到復(fù)雜”的順序,依次看看各個(gè)模塊是怎么搭建的。
二、依賴與環(huán)境:本地 Qwen3 + PgVector
先看根模塊 spring_ai_alibaba-demo/pom.xml 中的關(guān)鍵部分:
<properties>
<!-- 項(xiàng)目使用的 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>
<!-- 通過(guò) Spring AI 訪問(wèn)本地 Ollama 大模型服務(wù) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<!-- 向量數(shù)據(jù)庫(kù):Spring AI 對(duì) PgVector 的封裝 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
<!-- PostgreSQL JDBC 驅(qū)動(dòng),用于訪問(wèn)數(shù)據(jù)庫(kù) -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- Spring AI Alibaba 統(tǒng)一版本管理(國(guó)內(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 的版本對(duì)齊) -->
<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)了幾個(gè)核心設(shè)計(jì)理念:
- 通過(guò) BOM(
spring-ai-alibaba-bom+spring-ai-bom)統(tǒng)一版本管理,避免各個(gè) Starter 之間的版本地獄; - 實(shí)際運(yùn)行時(shí)模型選擇 Ollama,既方便本地開發(fā)調(diào)試,又可以在網(wǎng)絡(luò)受限場(chǎng)景下順暢運(yùn)行;
- 未來(lái)如果要切到阿里云 DashScope,只需要:
- 打開已經(jīng)寫好的(但當(dāng)前被注釋掉的)
spring-ai-alibaba-starter-dashscope依賴; - 在配置文件里增加
spring.ai.dashscope.*對(duì)應(yīng)配置,不需要改業(yè)務(wù)代碼。
- 打開已經(jīng)寫好的(但當(dāng)前被注釋掉的)
環(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 # 啟動(dòng)時(shí)自動(dòng)創(chuàng)建 PgVector 表結(jié)構(gòu)
datasource:
url: jdbc:postgresql://<your-host>:5432/postgres?serverTimezone=Asia/Shanghai # PostgreSQL 連接串
username: postgres
password: **** # 建議通過(guò)環(huán)境變量或配置中心注入- Ollama 在本機(jī) 11434 端口提供服務(wù),加載的是
qwen3:0.6b模型(本質(zhì)上仍然是阿里云通義家族的模型,只是以本地方式運(yùn)行); - Embedding 使用
qwen3-embedding:0.6b; - PgVector 存儲(chǔ)維度設(shè)置為 1024,采用余弦相似度;
- 數(shù)據(jù)源配置指向 PostgreSQL,用于向量存儲(chǔ)和(可選)對(duì)話記憶持久化。
三、基礎(chǔ)對(duì)話:從 ChatModel 到 ChatClient
Demo 中提供了兩種對(duì)話方式:直接使用 ChatModel,以及通過(guò) ChatClient 封裝后的高級(jí)用法。
3.1 使用 ChatModel 流式返回
ChatModelController:
@RestController
@RequestMapping("/chatModel")
public class ChatModelController {
// 注入由 Spring AI 自動(dòng)裝配的 Ollama ChatModel
private final ChatModel ollamaChatModel;
public ChatModelController(ChatModel ollamaChatModel) {
this.ollamaChatModel = ollamaChatModel;
}
@GetMapping("/chat")
public Flux<String> chat(@RequestParam("message") String message) {
// message:用戶輸入的自然語(yǔ)言問(wèn)題
return ollamaChatModel.stream(new Prompt(message)) // 以流式方式調(diào)用大模型
.map(ChatResponse::getResult) // 提取每個(gè)增量響應(yīng)的結(jié)果對(duì)象
.mapNotNull(result -> result.getOutput().getText()); // 只保留最終輸出的文本內(nèi)容
}
}ChatModel由spring-ai-starter-model-ollama自動(dòng)裝配,底層指向本地 Qwen3 模型;.stream(...)返回的是一個(gè) 響應(yīng)式 Flux,可以在前端按 token/片段逐步渲染;- 控制器本身和普通 Spring Web 控制器沒(méi)有本質(zhì)差別,學(xué)習(xí)成本非常低。
3.2 使用 ChatClient 提升可用性
ChatClientController:
@RestController
@RequestMapping("/chatClient")
public class ChatClientController {
// 基于 ChatModel 封裝的高級(jí)客戶端,后續(xù)可以掛接 Adviser、工具等能力
private final ChatClient ollamaChatClient;
public ChatClientController(ChatClient ollamaChatClient) {
this.ollamaChatClient = ollamaChatClient;
}
@GetMapping("/chat")
public Flux<String> stream(@RequestParam("message") String message) {
// 使用最簡(jiǎn)單的 Prompt,直接將用戶輸入交給大模型,并以流式方式返回結(jié)果
return ollamaChatClient
.prompt(new Prompt(message)) // 構(gòu)造 Prompt 對(duì)象
.stream() // 流式調(diào)用
.content(); // 提取文本內(nèi)容
}
@GetMapping("/prompt")
public Flux<String> prompt() {
PromptTemplate template = PromptTemplate.builder()
.template("請(qǐng)用簡(jiǎn)短中文回答:{question}") // 模板中定義占位符 {question}
.variables(Map.of()) // 這里可以預(yù)先聲明變量,也可以在 create 時(shí)傳入
.build();
// 使用實(shí)際問(wèn)題填充模板變量
Prompt prompt = template.create(Map.of("question", "Spring AI Alibaba 有什么特點(diǎn)?"));
return ollamaChatClient.prompt(prompt).stream().content();
}
}和 ChatModel 相比,ChatClient 的優(yōu)勢(shì)在于:
- 提供鏈?zhǔn)?API:
.prompt().call()/stream(),更易讀; - 更容易掛接 Adviser(記憶、RAG、工具等),形成統(tǒng)一調(diào)用入口;
- 在需要多輪交互、上下文管理時(shí)更易擴(kuò)展。
在 OllamaConfig 中,Demo 還展示了如何為 ChatClient 掛接記憶 Adviser,后面章節(jié)會(huì)展開。
四、對(duì)話記憶:內(nèi)存版與可擴(kuò)展版
實(shí)際業(yè)務(wù)中,一個(gè)“傻傻忘記前文”的大模型體驗(yàn)非常差。Demo 中給出了兩種記憶實(shí)現(xiàn)方式。
4.1 簡(jiǎn)單內(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:會(huì)話標(biāo)識(shí);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ù)會(huì)話 ID 取出該會(huì)話的歷史消息
return MEMORIES_CACHE.getOrDefault(conversationId, new ArrayList<>());
}
@Override
public void clear(String conversationId) {
// 清空某個(gè)會(huì)話的記憶
List<Message> messages = MEMORIES_CACHE.get(conversationId);
if (messages != null) {
messages.clear();
}
}
}- 通過(guò)
conversationId區(qū)分不同會(huì)話; - 適合 Demo、PoC 或?qū)煽啃砸蟛桓叩膱?chǎng)景;
- 結(jié)合
MessageChatMemoryAdvisor可以自動(dòng)把歷史消息注入到當(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)識(shí)當(dāng)前會(huì)話 ID 的 key
private static final String CHAT_MEMORIES_SESSION_ID = "chat_memories_session_id";
@Override
public ChatClientRequest before(ChatClientRequest request, AdvisorChain chain) {
// 從上下文中讀取會(huì)話 ID,并取出其歷史消息
String sessionId = request.context().get(CHAT_MEMORIES_SESSION_ID).toString();
List<Message> messages = MEMORIES.getOrDefault(sessionId, new ArrayList<>());
// 當(dāng)前請(qǐ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ù)寫回到對(duì)應(yīng)會(huì)話的記憶中
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)前消息拼成一個(gè)新的 Prompt; - 在
after中把模型回復(fù)寫回內(nèi)存; - 通過(guò)在
ChatClient構(gòu)建時(shí)添加defaultAdvisors(memoriesAdvisor),即可對(duì)所有請(qǐng)求啟用記憶能力。
進(jìn)一步,你可以把 DataBaseChatMemoryRepository 補(bǔ)充完整,將消息寫入數(shù)據(jù)庫(kù),實(shí)現(xiàn)持久化對(duì)話記憶。
五、RAG:Qwen3 + PgVector 的檢索增強(qiáng)
RAG(Retrieval Augmented Generation)是典型的企業(yè)級(jí)能力,本 Demo 通過(guò) RagChatClientController 進(jìn)行演示。
5.1 向量入庫(kù):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) // 每個(gè)分片的最大 token 數(shù)
.withKeepSeparator(true) // 是否保留分隔符(如換行符)
.withMaxNumChunks(1024) // 單次允許生成的最大分片數(shù)
.withMinChunkLengthToEmbed(20) // 小于該長(zhǎng)度的分片不入庫(kù),避免噪聲
.withMinChunkSizeChars(10) // 切分時(shí)的最小字符數(shù),避免切得過(guò)碎
.build();
List<Document> docs = splitter.split(Document.builder().text(message).build()); // 將文本切分為多個(gè) Document
pgVectorStore.add(docs); // 寫入 PgVector 向量庫(kù)
}
}TokenTextSplitter基于 token 切分文檔,避免切得過(guò)碎或過(guò)長(zhǎng);PgVectorStore.add將切分后的文檔寫入 PostgreSQL + PgVector;- 真實(shí)項(xiàng)目中可把
/embedding換成異步批處理任務(wù)。
5.2 RAG 對(duì)話:RetrievalAugmentationAdvisor
@Configuration
public class VectorChatClientConfig {
@Bean("ragChatClient")
public ChatClient ragChatClient(ChatModel chatModel, VectorStore vectorStore) {
VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore) // 具體使用的向量庫(kù)實(shí)現(xiàn),這里是 PgVector
.topK(3) // 每次檢索返回相似度最高的前 3 條文檔
.similarityThreshold(0.5) // 相似度閾值,小于該值的文檔會(huì)被過(guò)濾掉
.build();
RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(retriever) // 指定文檔檢索器
.order(0) // Adviser 執(zhí)行順序,越小越先執(zhí)行
.build();
return ChatClient.builder(chatModel)
.defaultAdvisors(advisor) // 默認(rèn)啟用 RAG 能力
.build();
}
}RetrievalAugmentationAdvisor會(huì)在每次請(qǐng)求前,先到向量庫(kù)檢索相關(guān)文檔;- 然后把檢索結(jié)果作為“系統(tǒng)提示詞”或“上下文”塞給 Qwen3 模型;
- 對(duì)你來(lái)說(shuō),只需調(diào)用
ragChatClient.prompt().user(question).call(),就能得到“帶知識(shí)庫(kù)”的回答。
六、工具調(diào)用:用 @Tool 讓模型調(diào)用你的 Java 方法
在很多場(chǎng)景中,大模型需要調(diào)用業(yè)務(wù)系統(tǒng)的 API 才能完成任務(wù)。Spring AI 提供了 @Tool 注解,Demo 中的 ZoomTool 便是一個(gè)簡(jiǎn)單示例。
6.1 定義工具:ZoomTool
@Component
public class ZoomTool {
@Tool(description = "通過(guò)時(shí)區(qū) ID 獲取當(dāng)前時(shí)間")
public String getTimeByZone(@ToolParam(description = "時(shí)區(qū) ID,比如 Asia/Shanghai") String zone) {
// zone:時(shí)區(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); // 返回格式化后的時(shí)間字符串
}
}6.2 將工具掛到 ChatClient 上
@Configuration
public class ToolChatClientConfig {
@Bean("toolChatClient")
public ChatClient toolChatClient(ChatModel ollamaChatModel, ZoomTool zoomTool) {
// ollamaChatModel:底層使用的 Qwen3 模型;zoomTool:提供獲取時(shí)間的業(yè)務(wù)工具
return ChatClient.builder(ollamaChatModel)
.defaultSystem(this.systemPrompt()) // 設(shè)置默認(rèn)的系統(tǒng)提示詞,統(tǒng)一咖啡館背景
.defaultTools(zoomTool) // 將 ZoomTool 注冊(cè)為可調(diào)用的工具
.build();
}
private String systemPrompt() {
Map<String, Object> vars = new HashMap<>();
vars.put("AMERICAN", "1-3"); // 美式咖啡制作時(shí)間(分鐘)
vars.put("LATTE", "2"); // 拿鐵咖啡制作時(shí)間(分鐘)
vars.put("TIME_ZONE", "Asia/Shanghai"); // 默認(rèn)時(shí)區(qū) ID
SystemPromptTemplate tpl = SystemPromptTemplate.builder()
.template("歡迎光臨 ZhouByte咖啡館,... 默認(rèn)時(shí)區(qū):{TIME_ZONE}") // 系統(tǒng)提示詞模板
.variables(vars) // 綁定上面的變量
.build();
return tpl.render(); // 渲染出包含具體變量值的系統(tǒng)提示詞
}
}對(duì)應(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)建一次新的對(duì)話請(qǐng)求
.user(message) // 添加一條用戶消息
.stream() // 流式調(diào)用大模型
.content(); // 只提取文本內(nèi)容返回
}
}- 模型可以在需要時(shí)自動(dòng)調(diào)用
getTimeByZone,返回指定時(shí)區(qū)時(shí)間; - 你只需要編寫普通的 Java 方法,剩下的交給 Spring AI 的工具調(diào)用機(jī)制。
七、Alibaba Graph 子項(xiàng)目:有狀態(tài)工作流編排
spring_ai_alibaba-demo/alibaba-graph 子項(xiàng)目使用 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(), // 多次寫入時(shí),后寫入的值覆蓋之前的值
"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é)點(diǎn)、邊和狀態(tài)合并策略;AsyncNodeAction封裝每個(gè)節(jié)點(diǎn)的執(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); // 將每個(gè)節(jié)點(diǎn)的輸出對(duì)象轉(zhuǎn)換為字符串返回
}
}- 使用 WebFlux +
Flux<NodeOutput>將節(jié)點(diǎn)執(zhí)行結(jié)果流式返回; - 通過(guò)
RunnableConfig.builder().threadId(conversationId)還可以實(shí)現(xiàn)“帶會(huì)話 ID 的工作流”,類似有狀態(tài) Agent。
7.3 quickStartGraph 執(zhí)行流程圖
結(jié)合上面的 GraphConfig 和 GraphController,/v1/graph 接口整體執(zhí)行流程可以用下面這張流程圖來(lái)表示(以 GitHub 為例,可以直接渲染 Mermaid):



- 從
GraphController.startGraph()開始,調(diào)用CompiledGraph.stream(Map.of())啟動(dòng)圖的執(zhí)行; - 圖從
StateGraph.START出發(fā),依次流經(jīng)node1、node2,最終到達(dá)StateGraph.END; - 每個(gè)節(jié)點(diǎn)都會(huì)向全局狀態(tài)寫入
input/output等字段,并以Flux<NodeOutput>的形式逐步返回給調(diào)用方。
7.4 多條件分支 Graph 示例(addConditionalEdges)
在實(shí)際業(yè)務(wù)中,Graph 往往不只是線性順序,還會(huì)根據(jù)狀態(tài)進(jìn)行分支判斷。spring-ai-alibaba-graph-core 提供了 addConditionalEdges,可以基于當(dāng)前 OverAllState 計(jì)算「條件標(biāo)簽」,再根據(jù)標(biāo)簽跳轉(zhuǎn)到不同節(jié)點(diǎn)。
下面是一個(gè)簡(jiǎn)化的「評(píng)分決策」示例,根據(jù) score 分?jǐn)?shù)分別走向通過(guò) / 復(fù)核 / 拒絕三條路徑:
@Configuration
public class ConditionalGraphConfig {
@Bean("scoreDecisionGraph")
public CompiledGraph scoreDecisionGraph() throws GraphStateException {
StateGraph graph = new StateGraph("scoreDecisionGraph", () -> Map.of(
"score", new ReplaceStrategy(), // 保存當(dāng)前評(píng)分
"result", new ReplaceStrategy() // 保存決策結(jié)果
));
// 讀取或設(shè)置評(píng)分(示例中從 state 中讀取,實(shí)際可由外部請(qǐng)求傳入)
graph.addNode("checkScore", AsyncNodeAction.node_async(state -> {
Integer score = (Integer) state.value("score").orElse(75); // 默認(rèn) 75 分
return Map.of("score", score);
}));
// 三個(gè)業(yè)務(wù)分支節(jié)點(diǎn):通過(guò) / 復(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")));
// 起點(diǎn)先進(jìn)入評(píng)分檢查節(jié)點(diǎn)
graph.addEdge(StateGraph.START, "checkScore");
// 多條件邊:根據(jù) score 返回不同的“標(biāo)簽”,再由 mappings 決定下一跳節(jié)點(diǎn)
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"
)
);
// 三個(gè)結(jié)果節(jié)點(diǎn)最終都指向 END
graph.addEdge("pass", StateGraph.END);
graph.addEdge("review", StateGraph.END);
graph.addEdge("reject", StateGraph.END);
return graph.compile();
}
}這段代碼中,addConditionalEdges 的三個(gè)參數(shù)含義是:
sourceId:條件邊的源節(jié)點(diǎn) ID,這里是"checkScore";AsyncEdgeAction:根據(jù)當(dāng)前OverAllState計(jì)算條件標(biāo)簽,這里返回"PASS"/"REVIEW"/"REJECT";mappings:標(biāo)簽與目標(biāo)節(jié)點(diǎn) ID 的映射,例如"PASS" -> "pass",即當(dāng)標(biāo)簽為"PASS"時(shí)跳到pass節(jié)點(diǎn)。
對(duì)應(yīng)的執(zhí)行流程,可以畫成如下多分支流程圖:

在真實(shí)項(xiàng)目中,你可以把 score 換成「風(fēng)控評(píng)分」「召回結(jié)果命中情況」「用戶畫像標(biāo)簽」等任意業(yè)務(wù)信號(hào),通過(guò) addConditionalEdges 把復(fù)雜分支邏輯從代碼 if/else 中抽離出來(lái),統(tǒng)一放在 Graph 層管理。
在這個(gè)子項(xiàng)目中,Graph 本身是“流程層”,可以在節(jié)點(diǎn)里調(diào)用 Spring AI / Spring AI Alibaba 的各種模型與工具,實(shí)現(xiàn)復(fù)雜的多步推理與業(yè)務(wù)編排。
八、如何從本地 Ollama 平滑切到阿里云 DashScope
雖然當(dāng)前 Demo 主要跑在本地 Ollama 上,但由于使用了 Spring AI + Spring AI Alibaba 的統(tǒng)一抽象,切換到阿里云 DashScope 十分簡(jiǎn)單:
- 在
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>- 在配置文件中增加 DashScope 的配置(示例):
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY} # 從環(huán)境變量或配置中心讀取 DashScope 的 API Key
endpoint: https://dashscope.aliyuncs.com
chat:
options:
model: qwen-plus # 使用的通義千問(wèn)在線模型
temperature: 0.8 # 采樣溫度
max-tokens: 2048 # 單次回答的最大 token 數(shù)- 將原來(lái)的
ChatModel注入點(diǎn)從 Ollama 替換為 DashScope 對(duì)應(yīng)的 Bean(通常只需要調(diào)整配置,不改業(yè)務(wù)代碼)。
憑借 Spring AI 的抽象層,你可以:
- 開發(fā)階段:本地跑 Qwen3(Ollama),成本低、調(diào)試快;
- 生產(chǎn)階段:切到云上 DashScope(Qwen-Max / Qwen-Plus 等),享受更強(qiáng)算力和更高可用性;
- 中長(zhǎng)期:在 Spring AI Alibaba 的生態(tài)內(nèi)同時(shí)兼容多家國(guó)內(nèi)模型廠商。
九、實(shí)踐建議與最佳實(shí)踐
- 配置管理
- API Key 使用環(huán)境變量或配置中心(Nacos、KMS 等),避免硬編碼;
- Ollama、DashScope 的模型名稱、溫度等參數(shù)盡量抽到配置文件中。
- 錯(cuò)誤處理與重試
- 針對(duì)網(wǎng)絡(luò)異常、超時(shí)、限流等場(chǎng)景做兜底和重試策略;
- 對(duì)外暴露的接口統(tǒng)一封裝錯(cuò)誤返回,避免直接把底層錯(cuò)誤拋給前端。
- 性能與成本
- 在高并發(fā)場(chǎng)景建議優(yōu)先使用流式輸出 + 前端增量渲染;
- RAG 中控制 TopK、相似度閾值和切分策略,避免向量庫(kù)“爆炸”。
- 代碼結(jié)構(gòu)
- 將
ChatClient配置、Graph 配置等放在獨(dú)立的config包中,業(yè)務(wù)層只關(guān)心接口調(diào)用; - 工具方法使用
@Tool暴露,便于模型統(tǒng)一管理和調(diào)用。
- 將
- 版本與升級(jí)
- Spring AI Alibaba 當(dāng)前仍以 Milestone 版本為主(如
1.1.0.0-M5),升級(jí)前建議閱讀 release notes; - 保持對(duì)
spring-ai-bom/spring-ai-alibaba-bom的依賴,讓升級(jí)盡量在 BOM 層完成。
- Spring AI Alibaba 當(dāng)前仍以 Milestone 版本為主(如
十、總結(jié)與展望
基于 spring_ai_alibaba-demo 子項(xiàng)目,我們實(shí)際體驗(yàn)了一次:
- 如何用 Spring AI + Spring AI Alibaba BOM 快速接入本地 Qwen3(Ollama);
- 如何在同一套抽象下串聯(lián)起對(duì)話、記憶、RAG、工具調(diào)用;
- 如何通過(guò)
spring-ai-alibaba-graph-core構(gòu)建基于大模型的有狀態(tài)工作流; - 以及如何在不改業(yè)務(wù)代碼的前提下,為未來(lái)切換到阿里云 DashScope 留出空間。
對(duì) Spring 開發(fā)者來(lái)說(shuō),這套體系最大的價(jià)值在于:
- 統(tǒng)一抽象:不同模型供應(yīng)商之間切換成本極低;
- 生態(tài)完善:兼容 Spring Boot、WebFlux、向量庫(kù)、MCP、Graph 等豐富組件;
- 本地 + 云端雙模:既能在本地快速迭代,又能無(wú)縫遷移到云上生產(chǎn)環(huán)境。
再次附上示例子項(xiàng)目 GitHub 地址,歡迎你親手跑一跑代碼、提 Issue、點(diǎn) Star:
GitHub 項(xiàng)目地址:https://github.com/zhouByte-hub/java-ai/tree/main/spring_ai_alibaba-demo
到此這篇關(guān)于Spring AI Alibaba + Ollama 實(shí)戰(zhàn)教程(基于本地 Qwen3 的 Spring Boot 大模型應(yīng)用)的文章就介紹到這了,更多相關(guān)Spring AI Alibaba Ollama 實(shí)戰(zhàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mybatis基于注解形式的sql語(yǔ)句生成實(shí)例代碼
這篇文章主要介紹了 Mybatis基于注解形式的sql語(yǔ)句生成實(shí)例代碼,需要的朋友可以參考下2017-09-09
基于Eclipse 的JSP/Servlet的開發(fā)環(huán)境的搭建(圖文)
本文將會(huì)詳細(xì)地展示如何搭建JSP的開發(fā)環(huán)境。本次教程使用的是最新版的Eclipse 2018-09編輯器和最新版的Apache Tomcat v9.0,步驟詳細(xì),內(nèi)容詳盡,適合零基礎(chǔ)學(xué)者作為學(xué)習(xí)參考2018-12-12
Spring Boot項(xiàng)目@RestController使用重定向redirect方式
這篇文章主要介紹了Spring Boot項(xiàng)目@RestController使用重定向redirect方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
Mybatis把返回結(jié)果封裝成map類型的實(shí)現(xiàn)
本文主要介紹了Mybatis把返回結(jié)果封裝成map類型的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03
java實(shí)現(xiàn)航空用戶管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)航空用戶管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07
RestTemplate自定義請(qǐng)求失敗異常處理示例解析
這篇文章主要為大家介紹了RestTemplate自定義請(qǐng)求失敗異常處理的示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-03-03
Idea跑的項(xiàng)目沒(méi)問(wèn)題將程序install成jar包運(yùn)行報(bào)錯(cuò)空指針的問(wèn)題
這篇文章主要介紹了Idea跑的項(xiàng)目沒(méi)問(wèn)題,將程序install成jar包運(yùn)行報(bào)錯(cuò)空指針的問(wèn)題,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06
SpringBoot中注解@ConfigurationProperties與@Value的區(qū)別與使用詳解
本文主要介紹了SpringBoot中注解@ConfigurationProperties與@Value的區(qū)別與使用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09

