AI项目博客
记录 AI 工程实践与大模型应用

Spring AI Alibaba 66|Graph Core · Graph 工作流编排指南 · 步骤 5:组装Graph

现在我们将节点连接成一个可工作的图。由于我们的节点处理自己的路由决策,我们只需要一些基本的边。

要启用人工介入(human-in-the-loop)功能,我们需要使用 checkpointer 编译以在运行之间保存状态:

import com.alibaba.cloud.ai.graph.StateGraph;
import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
import com.alibaba.cloud.ai.graph.CompileConfig;
import com.alibaba.cloud.ai.graph.exception.GraphStateException;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.client.ChatClient;
import static com.alibaba.cloud.ai.graph.StateGraph.END;
import static com.alibaba.cloud.ai.graph.StateGraph.START;
import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async;
import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async;
import java.util.Map;

/**
* 创建邮件处理 Graph
*/
public static CompiledGraph createEmailAgentGraph(ChatModel chatModel) throws GraphStateException {
  // 配置 ChatClient
  ChatClient.Builder chatClientBuilder = ChatClient.builder(chatModel);

  // 创建节点
  var readEmail = node_async(new ReadEmailNode());
  var classifyIntent = node_async(new ClassifyIntentNode(chatClientBuilder));
  var searchDocumentation = node_async(new SearchDocumentationNode());
  var bugTracking = node_async(new BugTrackingNode());
  var draftResponse = node_async(new DraftResponseNode(chatClientBuilder));
  var humanReview = node_async(new HumanReviewNode());
  var sendReply = node_async(new SendReplyNode());

  // 创建图
  StateGraph workflow = new StateGraph(createKeyStrategyFactory())
          .addNode("read_email", readEmail)
          .addNode("classify_intent", classifyIntent)
          .addNode("search_documentation", searchDocumentation)
          .addNode("bug_tracking", bugTracking)
          .addNode("draft_response", draftResponse)
          .addNode("human_review", humanReview)
          .addNode("send_reply", sendReply);

  // 添加基本边
  workflow.addEdge(START, "read_email");
  workflow.addEdge("read_email", "classify_intent");
  workflow.addEdge("send_reply", END);

  // 添加条件边(基于节点返回的 next_node)
  workflow.addConditionalEdges("classify_intent",
          edge_async(state -> {
              return (String) state.value("next_node").orElse("draft_response");
          }),
          Map.of(
                  "search_documentation", "search_documentation",
                  "bug_tracking", "bug_tracking",
                  "human_review", "human_review",
                  "draft_response", "draft_response"
          ));

  workflow.addConditionalEdges("draft_response",
          edge_async(state -> {
              return (String) state.value("next_node").orElse("send_reply");
          }),
          Map.of(
                  "human_review", "human_review",
                  "send_reply", "send_reply"
          ));

  workflow.addConditionalEdges("human_review",
          edge_async(state -> {
              return (String) state.value("next_node").orElse("send_reply");
          }),
          Map.of(
                  "send_reply", "send_reply"
          ));

  workflow.addEdge("search_documentation", "draft_response");
  workflow.addEdge("bug_tracking", "draft_response");

  // 配置持久化
  var memory = new MemorySaver();
  var compileConfig = CompileConfig.builder()
          .saverConfig(SaverConfig.builder()
                  .register(memory)
                  .build())
          .interruptBefore("human_review")  // 在人工审核前中断
          .build();

  return workflow.compile(compileConfig);
}

图结构是最小的,因为路由通过节点内部的返回值发生。每个节点通过返回 next_node 键来声明它可以去哪里,使流程显式和可追溯。

测试您的代理

让我们用一个需要人工审核的紧急账单问题来运行我们的代理:

import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.NodeOutput;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.Map;

/**
* 测试紧急账单问题
*/
public static void testBillingIssue(CompiledGraph app) throws Exception {
  log.info("=== 测试紧急账单问题 ===");

  // 测试紧急账单问题
  Map<String, Object> initialState = Map.of(
          "email_content", "我的订阅被收费两次了!这很紧急!",
          "sender_email", "customer@example.com",
          "email_id", "email_123",
          "messages", new ArrayList<String>()
  );

  // 使用 thread_id 运行以实现持久化
  var config = RunnableConfig.builder()
          .threadId("customer_123")
          .build();

  // 使用 stream 执行,直到中断点(human_review)
  // 图将在 human_review 处暂停(因为配置了 interruptBefore)
  Flux<NodeOutput> stream = app.stream(initialState, config);
  stream
          .doOnNext(output -> log.info("节点输出: {}", output))
          .doOnError(error -> log.error("执行错误: {}", error.getMessage()))
          .doOnComplete(() -> log.info("流完成"))
          .blockLast();

  // 获取当前状态,检查是否有草稿回复
  var currentState = app.getState(config);
  Map<String, Object> stateData = currentState.state().data();
  String draftResponse = (String) stateData.get("draft_response");
  if (draftResponse != null) {
      log.info("Draft ready for review: {}...", 
              draftResponse.length() > 100 
                      ? draftResponse.substring(0, 100) 
                      : draftResponse);
  }

  // 准备好后,提供人工输入以恢复
  // 使用 updateState 更新状态(interruptBefore 模式下,传入 null 作为节点 ID)
  var updatedConfig = app.updateState(config, Map.of(
          "approved", true,
          "edited_response", "我们对重复收费深表歉意。我已经立即启动了退款..."
  ), null);

  // 继续执行(input 为 null,使用之前的状态)
  app.stream(null, updatedConfig)
          .doOnNext(output -> log.info("节点输出: {}", output))
          .doOnError(error -> log.error("执行错误: {}", error.getMessage()))
          .doOnComplete(() -> log.info("流完成"))
          .blockLast();

  // 获取最终状态
  var finalState = app.getState(updatedConfig);
  String status = (String) finalState.state().data().get("status");
  log.info("Email sent successfully! Status: {}", status);
}

图在遇到 interruptBefore 时暂停,将所有内容保存到 checkpointer,然后等待。它可以在几天后恢复,准确地从它停止的地方继续。thread_id 确保这个对话的所有状态都保存在一起。

注意:在 interruptBefore 模式下,使用 app.stream() 方法执行图,它会流式返回每个节点的输出。当执行到配置的中断点(human_review)时,图会自动暂停。使用 app.getState() 可以获取当前状态,使用 app.updateState() 可以更新状态并提供人工输入,然后再次调用 app.stream() 继续执行。

本文整理自 Spring AI Alibaba 官方文档《Graph 工作流编排指南》:https://java2ai.com/docs/frameworks/graph-core/quick-start

赞(0)
未经允许不得转载:AI项目博客 » Spring AI Alibaba 66|Graph Core · Graph 工作流编排指南 · 步骤 5:组装Graph

评论 抢沙发

AI项目博客

专注 AI 工程化落地 —— 覆盖智能体、大模型、Java AI 与 Python 的学习笔记,以及 AI 平台的项目实战记录。

关于本站文章归档

登录

找回密码

注册