Graph-Based AI Agent Workflow Orchestration with Spring AI Alibaba

This tutorial demonstrates building a graph-based workflow for AI agents using Spring AI Alibaba, covering state management, node implementation, conditional edges, and a practical excuse-upgrade example with LLM-driven nodes and dynamic routing based on believability scores.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Graph-Based AI Agent Workflow Orchestration with Spring AI Alibaba

1. Introduction

In real-world AI agent development, a single LLM prompt rarely covers complex tasks. These tasks require multiple execution steps where each step's output determines the next path. Manually coding, debugging, and maintaining such multi-step orchestration logic incurs high development and maintenance costs.

This article uses Spring AI Alibaba to model agent workflows as self-navigating step graphs that dynamically decide execution paths at runtime.

Core Concepts of Graph-Based Workflows

Graph-based workflows decompose complex tasks into smaller work units connected as a graph. Instead of hardcoding execution order, you describe possible paths; the workflow follows appropriate paths based on its data. Three main concepts:

State : Shared data flowing through the workflow

Nodes : Work units that read state, execute business logic, and update state

Edges : Connections between nodes that control execution order by deciding which node to move to next

2. Practical Example: Excuse Upgrade Workflow

We build a workflow where an employee node invents an excuse for a given work scenario, then a manager node rates the excuse's believability. The workflow loops until the manager believes the excuse (score ≥ 0.8) or max attempts (3) are reached.

2.1 Environment Setup

Environment: Spring Boot 3.5.0

Dependencies

<dependency>
  <groupId>com.alibaba.cloud.ai</groupId>
  <artifactId>spring-ai-alibaba-agent-framework</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

Configuration

spring:
  ai:
    openai:
      base-url: https://dashscope.aliyuncs.com/compatible-mode
      api-key: ${AI_DASHSCOPE_API_KEY}
      chat:
        options:
          model: qwen3.8-max

2.2 Defining State Keys

Workflow state is a collection of key-value pairs. Before nodes can read/write them, we declare keys and their update strategies via a KeyStrategyFactory bean:

@Bean
KeyStrategyFactory excuseKeyStrategyFactory() {
  return () -> {
    Map<String, KeyStrategy> strategies = new HashMap<>();
    // Scenario (specific work context preset in the workflow)
    strategies.put("situation", new ReplaceStrategy());
    // Excuse (employee node's generated explanation)
    strategies.put("excuse", new ReplaceStrategy());
    // Manager replies (manager node's feedback content)
    strategies.put("managerReplies", new AppendStrategy());
    // Believability (manager's quantified credibility score)
    strategies.put("believability", new ReplaceStrategy());
    // Attempts (cumulative rounds of excuse generation/optimization)
    strategies.put("attempts", new ReplaceStrategy());
    return strategies;
  };
}
ReplaceStrategy

overwrites existing values; AppendStrategy adds each new value to a list. The situation key is the required initial input.

2.3 Employee Node: Inventing Excuses

Implement NodeAction interface. The apply method receives OverAllState, executes logic, and returns a map of state updates.

@Component
public class InventExcuseNode implements NodeAction {
  private static final String PROMPT_TEMPLATE = """
    You are an employee explaining why you delayed a task.
    You are bad at lying but have a wild imagination and creative ideas.
    You never take responsibility and never admit you are lying.
    Scenario: {situation}
    Your previous excuse: {previousExcuse}
    Your manager's replies: {managerReplies}
    Give a new excuse in no more than two sentences. If you already gave an excuse, do not overturn it. Keep the original story and add a new layer of detail to flesh it out.
    Return only the excuse content.
    """;
  private final ChatClient chatClient;
  public InventExcuseNode(ChatClient.Builder chatClientBuilder) {
    this.chatClient = chatClientBuilder
      .defaultAdvisors(new SimpleLoggerAdvisor())
      .build();
  }
  @Override
  public Map<String, Object> apply(OverAllState state) throws Exception {
    String situation = state.value("situation", String.class)
      .orElseThrow(IllegalStateException::new);
    String previousExcuse = state.value("excuse", "None yet");
    List<String> managerReplies = state.value("managerReplies", List.of());
    Integer attempts = state.value("attempts", 0);
    String excuse = chatClient.prompt()
      .user(user -> user.text(PROMPT_TEMPLATE)
        .param("situation", situation)
        .param("previousExcuse", previousExcuse)
        .param("managerReplies", managerReplies.isEmpty() ? "None yet" : String.join("
", managerReplies)))
      .call().content();
    return Map.of("excuse", excuse, "attempts", attempts + 1);
  }
}

Reads state via value() with defaults (except situation which is required). Updates state with new excuse and incremented attempt count.

2.4 Manager Node: Rating Excuses

Another NodeAction implementation that scores believability 0.0–1.0.

@Component
public class ManagerReactsNode implements NodeAction {
  private static final String PROMPT_TEMPLATE = """
    You are a physically and mentally exhausted tech manager listening to an employee explain a mistake.
    Scenario: {situation}
    Excuse given: {excuse}
    Reply in one sentence and rate the excuse's believability from 0.0 to 1.0.
    """;
  private final ChatClient chatClient;
  public ManagerReactsNode(ChatClient.Builder chatClientBuilder) {
    this.chatClient = chatClientBuilder.build();
  }
  @Override
  public Map<String, Object> apply(OverAllState state) throws Exception {
    String situation = state.value("situation", String.class)
      .orElseThrow(IllegalStateException::new);
    String excuse = state.value("excuse", String.class)
      .orElseThrow(IllegalStateException::new);
    System.out.println("Excuse: %s".formatted(excuse));
    ManagerReaction managerReaction = chatClient
      .prompt()
      .user(user -> user.text(PROMPT_TEMPLATE)
        .param("situation", situation)
        .param("excuse", excuse))
      .call()
      .entity(ManagerReaction.class);
    System.err.println("Reply: %s, Believability: %s".formatted(managerReaction.reply, managerReaction.believability));
    return Map.of(
      "managerReplies", managerReaction.reply(),
      "believability", managerReaction.believability()
    );
  }
  record ManagerReaction(String reply, Double believability) {}
}

Reads state, fills prompt, calls LLM with structured output ( ManagerReaction record), returns reply and score.

2.5 Conditional Edge: Routing Logic

Implement EdgeAction to decide whether to stop or escalate (loop back).

@Component
public class ExcuseDispatcher implements EdgeAction {
  private static final double BELIEVABILITY_THRESHOLD = 0.8;
  private static final int MAX_ATTEMPTS = 3;
  @Override
  public String apply(OverAllState state) {
    Double believability = state.value("believability", 0.0);
    Integer attempts = state.value("attempts", 0);
    if (isBelieved(believability) || attempts >= MAX_ATTEMPTS) {
      return "stop";
    }
    return "escalate";
  }
  public static boolean isBelieved(Double believability) {
    return believability >= BELIEVABILITY_THRESHOLD;
  }
}

Reads believability and attempts. Returns stop if score ≥ 0.8 or attempts ≥ 3; otherwise escalate to loop back.

2.6 Assembling the Graph

Expose a CompiledGraph bean wiring nodes and edges.

@Bean
CompiledGraph excuseGraph(KeyStrategyFactory keyStrategyFactory,
    InventExcuseNode inventExcuseNode,
    ManagerReactsNode managerReactsNode,
    ExcuseDispatcher excuseDispatcher) throws GraphStateException {
  StateGraph excuseGraph = new StateGraph(keyStrategyFactory)
    .addNode("invent_excuse", AsyncNodeAction.node_async(inventExcuseNode))
    .addNode("manager_reacts", AsyncNodeAction.node_async(managerReactsNode))
    .addEdge(StateGraph.START, "invent_excuse")
    .addEdge("invent_excuse", "manager_reacts")
    .addConditionalEdges("manager_reacts", AsyncEdgeAction.edge_async(excuseDispatcher),
      Map.of("escalate", "invent_excuse", "stop", StateGraph.END));
  return excuseGraph.compile();
}

Steps: (1) Create StateGraph with key strategy factory. (2) Register nodes with unique names via addNode(). (3) Define flow: START → invent_excuse → manager_reacts. (4) Attach conditional edge from manager_reacts using dispatcher; map escalate back to invent_excuse, stop to END.

2.7 Testing

REST endpoint invokes the compiled graph with a situation input.

private final CompiledGraph excuseGraph;
@GetMapping("/excuse")
ResponseEntity<ExcuseResponse> generateExcuse(ExcuseRequest request) {
  OverAllState finalState = excuseGraph
    .invoke(
      Map.of("situation", request.situation()),
      RunnableConfig.builder()
        .threadId(UUID.randomUUID().toString())
        .build())
    .orElseThrow();
  ExcuseResponse response = new ExcuseResponse(
    finalState.value("excuse", ""),
    finalState.value("managerReplies", List.of()),
    finalState.value("attempts", 0),
    ExcuseDispatcher.isBelieved(finalState.value("believability", 0.0)));
  return ResponseEntity.ok(response);
}
public static record ExcuseRequest(String situation) {}
public static record ExcuseResponse(String finalExcuse, List<String> managerReplies,
  Integer attempts, boolean believed) {}

The article includes screenshots of test runs showing the workflow iterating through multiple excuse attempts until the manager's believability threshold is met or max attempts reached. It also demonstrates modifying the employee node's prompt to change excuse style and re-running.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

State ManagementLLM IntegrationWorkflow OrchestrationAgent DevelopmentSpring Boot 3Spring AI AlibabaGraph WorkflowConditional Edges
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.