1.SAA Graph基础介绍
1.1基础介绍
Spring AI Alibaba Graph理解为一个专为Java开发者设计的智能体工作流编排框架。它的设计理念很大程度上借鉴了著名的LangGraph,可以看作是LangGraph在Java生态中的实现 。它允许你将一个复杂的AI任务拆解成一系列由节点(Node)和边(Edge)组成的图(Graph),并通过一个共享的状态(State)来驱动整个流程的执行。
1.2 核心概念
状态(State):状态是整个图执行过程中的"共享记忆" 。它本质上是一个 Map<String, Object>,负责在节点之间传递数据 。例如,用户输入、模型生成的中间结果、最终答案等,都存储在State中。状态的管理策略是理解它的关键。State中的每个字段都可以定义自己的更新策略,这决定了当多个节点都想修改同一个字段时,最终结果是什么 。最常用的3种策略是:
ReplaceStrategy(覆盖策略):这是默认策略。新值会直接覆盖旧值 。适用于只需保留最终结果的场景,比如"最终答案"。
AppendStrategy(追加策略):新值会被追加到旧值的列表中 。这对于需要记录完整过程的场景至关重要,比如保存对话历史(messages),或者记录一个任务的多个中间思考步骤。
MergeStrategy (聚合策略):对数据进行聚合,如Map类型的数据聚合
节点(Node ):节点是图里的基本执行单元,负责完成一项具体的工作 。一个节点可以是任何逻辑,比如调用一次大模型、执行一个本地函数、或者调用一个外部工具 。每个节点接收当前的State作为输入,执行自己的任务,然后返回对State的更新(一个Map增量)。Spring AI Alibaba提供了很多开箱即用的节点,比如:
LlmNode:用于调用大语言模型。
QuestionClassifierNode:用于对输入进行分类。
ToolNode:用于执行工具调用。
你也可以通过实现 NodeAction 接口来创建完全自定义的节点 。
边(Edge):边定义了节点之间的控制流,即一个节点执行完后,接下来该去哪个节点 。边有两种主要类型:
直接边(Direct Edge):从一个节点无条件地连接到下一个节点,形成顺序执行流 。
条件边(Conditional Edge):根据当前State中的信息,动态地决定下一个执行的节点,从而实现分支逻辑 。例如,一个"分类器"节点执行后,可以通过条件边判断:如果结果是"positive",则走"结束"节点;如果结果是"negative",则走"售后处理"节点 。
1.3 核心构建块
**StateGraph:**你可以把它看作一个"图构建器"。它提供了一系列流畅的API(如.addNode(), .addEdge(), .addConditionalEdges())来帮助你组装节点和边,从而定义整个工作流的蓝图 。
CompiledGraph:当你用StateGraph定义好蓝图后,需要调用.compile()方法来编译它 。编译过程会进行一些基本的检查(比如确保没有孤立节点),并生成一个可执行的、不可变的图 。之后,你就可以通过调用CompiledGraph的invoke()或stream()方法来启动并执行这个工作流了 。
2.项目实操
2.1.创建Springboot项目,pom添加核心依赖
xml
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.4.0</spring-boot.version>
<spring-ai.version>1.0.0</spring-ai.version>
<spring-ai-alibaba.version>1.0.0.4</spring-ai-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring AI -->
<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>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-graph-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
2.2 添加核心配置 application.yml
yaml
server:
port: 8085
servlet:
encoding:
charset: UTF-8
force: true
enabled: true
spring:
application:
name: saa-graph-demo-project
ai:
zhipuai:
api-key: your-token-key
base-url: "https://open.bigmodel.cn/api/paas"
chat:
options:
model: GLM-4.7
2.3 核心java代码
2.3.1 GraphCofig 配置类
java
package com.java.ai.graph.config;
import com.alibaba.cloud.ai.graph.*;
import com.alibaba.cloud.ai.graph.exception.GraphStateException;
import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy;
import com.java.ai.graph.edge.ReJokeConditionEdgeAction;
import com.java.ai.graph.node.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async;
@Slf4j
@Configuration
public class GraphCofig {
/**
* keyStrategyFactory :3个策略 替换,合并 map类型,追加 List类型
* state
*
* @return
* @throws GraphStateException
*/
@Bean
public CompiledGraph jokeGraph(ChatClient.Builder chatClientBuilder) throws GraphStateException {
KeyStrategyFactory keyStrategyFactory = new KeyStrategyFactory() {
@Override
public Map<String, KeyStrategy> apply() {
return Map.of("topic", new ReplaceStrategy());
}
};
//定义状态图state
StateGraph startGraph = new StateGraph("jokeGraph", keyStrategyFactory);
//定义节点node nodeAction 定义节点异步执行逻辑
startGraph.addNode("GenerateJokeNode", node_async(new GenerateJokeNode(chatClientBuilder)));
startGraph.addNode("LoopEvaluateJokeNode", node_async(new LoopEvaluateJokeNode(chatClientBuilder,10,5)));
//定义边edge
//1.生成笑话->2.评估笑话->3.重新生成笑话
startGraph.addEdge(StateGraph.START, "GenerateJokeNode");
startGraph.addEdge("GenerateJokeNode", "LoopEvaluateJokeNode");
//loop条件边判断
ReJokeConditionEdgeAction conditionEdgeAction = new ReJokeConditionEdgeAction();
startGraph.addConditionalEdges("LoopEvaluateJokeNode",
conditionEdgeAction,
Map.of("break", StateGraph.END, "loop", "GenerateJokeNode"));
//graph 可视化图
GraphRepresentation graphRepresentation = startGraph.getGraph(GraphRepresentation.Type.PLANTUML, "JokeGraph");
log.info("graphRepresentation:{}", graphRepresentation.content());
//编译状态图
return startGraph.compile();
}
}
3.2 自定义node节点
生成笑话节点node
java
package com.java.ai.graph.node;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.action.NodeAction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import java.util.Map;
/**
* 生成笑话节点node
*/
@Slf4j
public class GenerateJokeNode implements NodeAction {
private final ChatClient chatClient;
public GenerateJokeNode(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@Override
public Map<String, Object> apply(OverAllState state) throws Exception {
//从state中获取要翻译的句子
String topic = state.value("topic", "");
//模型调用
PromptTemplate promptTemplate = new PromptTemplate("你是一个讲笑话大师,能够基于给定的主题{topic}进行讲笑话。要求只返回最终讲好的笑话,不要返回其他信息。");
Prompt prompt = promptTemplate.create(Map.of("topic", topic));
String content = chatClient
.prompt(prompt)
.call().content();
log.info("joke:{}", content);
//结果保存在state中
return Map.of("topic", topic, "joke", content);
}
}
循环评估笑话的节点
java
package com.java.ai.graph.node;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.action.NodeAction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import java.util.Map;
/**
* 评估笑话节点node
*/
@Slf4j
public class LoopEvaluateJokeNode implements NodeAction {
private final ChatClient chatClient;
private final Integer limitLoopCount;
private final Integer targetScore;
public LoopEvaluateJokeNode(ChatClient.Builder builder, Integer limitLoopCount, Integer targetScore) {
this.chatClient = builder.build();
this.limitLoopCount = limitLoopCount;
this.targetScore = targetScore;
}
@Override
public Map<String, Object> apply(OverAllState state) throws Exception {
//从state中获取joke
String joke = state.value("joke", "");
Integer loopCount = state.value("loopCount", 1);
//模型调用
PromptTemplate promptTemplate = new PromptTemplate("你是一个讲笑话评估师,能够基于给定的笑话搞笑层度对{joke}进行评分。要求只返回最终评估结果为 0-10的整数,分数越高越优秀,不要返回其他信息。");
Prompt prompt = promptTemplate.create(Map.of("joke", joke));
String scoreStrResult= chatClient
.prompt(prompt)
.call().content();
Integer score = Integer.parseInt(scoreStrResult.trim());
log.info("score:{}",score);
String action="loop";
//分数大于7或者循环次数大于10则结束循环
if (score > targetScore || loopCount > limitLoopCount){
action="break";
}
//结果保存在state中
return Map.of("joke", joke, "result", action, "loopCount", loopCount+1);
}
}
自定义一个条件edge
java
package com.java.ai.graph.edge;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.action.AsyncEdgeAction;
import java.util.concurrent.CompletableFuture;
/***
* 自定义条件边
*/
public class ReJokeConditionEdgeAction implements AsyncEdgeAction {
@Override
public CompletableFuture<String> apply(OverAllState state) {
CompletableFuture<String> result = new CompletableFuture<>();
try {
// Get the condition result from the state
String conditionResult = state.value("result", "break");
result.complete(conditionResult);
} catch (Exception e) {
result.completeExceptionally(e);
}
return result;
}
}
测试调用
java
package com.java.ai.graph.controller;
import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/graph")
public class SpringAIGraphController {
private final CompiledGraph jokeGraph;
public SpringAIGraphController(@Qualifier("jokeGraph") CompiledGraph jokeGraph) {
this.jokeGraph = jokeGraph;
}
@GetMapping("/joke")
public Map joke(@RequestParam("topic") String topic) {
try {
return jokeGraph.call(Map.of("topic", topic)).get().data();
} catch (Exception e) {
e.printStackTrace();
}
return new HashMap<>();
}
}
接口响应结果
bash
{
"result": "break",
"topic": "爱情",
"loopCount": 2,
"joke": "女朋友问男朋友:"亲爱的,你爱我吗?"\n男朋友深情地说:"爱。"\n女朋友又问:"那你愿意为了我去死吗?"\n男朋友坚定地说:"不愿意。"\n女朋友生气了:"为什么?"\n男朋友说:"因为我的爱是至死不渝的。""
}