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

Spring AI Alibaba 65|Graph Core · Graph 工作流编排指南 · 步骤 4:构建您的节点:实现我们的邮件代理节点

我们将每个节点实现为一个简单的函数。记住:节点接受状态,执行工作,并返回更新。

读取和分类节点

import com.alibaba.cloud.ai.graph.action.NodeAction;
import com.alibaba.cloud.ai.graph.OverAllState;
import org.springframework.ai.chat.client.ChatClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

private static final Logger log = LoggerFactory.getLogger(QuickStartExample.class);

// 读取邮件节点
public static class ReadEmailNode implements NodeAction {

  @Override
  public Map<String, Object> apply(OverAllState state) throws Exception {
      // 在生产环境中,这将连接到您的邮件服务
      String emailContent = state.value("email_content")
              .map(v -> (String) v)
              .orElse("");

      log.info("Processing email: {}", emailContent);

      List<String> messages = new ArrayList<>();
      messages.add("Processing email: " + emailContent);

      return Map.of("messages", messages);
  }
}

// 分类意图节点
public static class ClassifyIntentNode implements NodeAction {

  private final ChatClient chatClient;

  public ClassifyIntentNode(ChatClient.Builder chatClientBuilder) {
      this.chatClient = chatClientBuilder.build();
  }

  @Override
  public Map<String, Object> apply(OverAllState state) throws Exception {
      String emailContent = state.value("email_content")
              .map(v -> (String) v)
              .orElseThrow(() -> new IllegalStateException("No email content"));
      String senderEmail = state.value("sender_email")
              .map(v -> (String) v)
              .orElse("unknown");

      // 按需格式化提示,不存储在状态中
      String classificationPrompt = String.format("""
              分析这封客户邮件并进行分类:

              邮件: %s
              发件人: %s

              提供分类,包括意图、紧急程度、主题和摘要。

              意图应该是以下之一: question, bug, billing, feature, complex
              紧急程度应该是以下之一: low, medium, high, critical

              以JSON格式返回: {"intent": "...", "urgency": "...", "topic": "...", "summary": "..."}
              """, emailContent, senderEmail);

      // 获取结构化响应
      String response = chatClient.prompt()
              .user(classificationPrompt)
              .call()
              .content();

      // 解析为 EmailClassification 对象
      EmailClassification classification = parseClassification(response);

      // 根据分类确定下一个节点
      String nextNode;
      if ("billing".equals(classification.getIntent()) ||
              "critical".equals(classification.getUrgency())) {
          nextNode = "human_review";
      } else if (List.of("question", "feature").contains(classification.getIntent())) {
          nextNode = "search_documentation";
      } else if ("bug".equals(classification.getIntent())) {
          nextNode = "bug_tracking";
      } else {
          nextNode = "draft_response";
      }

      // 将分类作为单个对象存储在状态中
      return Map.of(
              "classification", classification,
              "next_node", nextNode
      );
  }

  /**
   * 简化的JSON解析(实际应用中使用Jackson或Gson)
   */
  private EmailClassification parseClassification(String jsonResponse) {
      EmailClassification classification = new EmailClassification();

      // 简单的正则表达式解析
      Pattern intentPattern = Pattern.compile(""intent"s*:s*"([^"]+)"");
      Pattern urgencyPattern = Pattern.compile(""urgency"s*:s*"([^"]+)"");
      Pattern topicPattern = Pattern.compile(""topic"s*:s*"([^"]+)"");
      Pattern summaryPattern = Pattern.compile(""summary"s*:s*"([^"]+)"");

      Matcher matcher = intentPattern.matcher(jsonResponse);
      if (matcher.find()) {
          classification.setIntent(matcher.group(1));
      }

      matcher = urgencyPattern.matcher(jsonResponse);
      if (matcher.find()) {
          classification.setUrgency(matcher.group(1));
      }

      matcher = topicPattern.matcher(jsonResponse);
      if (matcher.find()) {
          classification.setTopic(matcher.group(1));
      }

      matcher = summaryPattern.matcher(jsonResponse);
      if (matcher.find()) {
          classification.setSummary(matcher.group(1));
      }

      // 如果解析失败,设置默认值
      if (classification.getIntent() == null) {
          classification.setIntent("question");
      }
      if (classification.getUrgency() == null) {
          classification.setUrgency("medium");
      }
      if (classification.getTopic() == null) {
          classification.setTopic("general");
      }
      if (classification.getSummary() == null) {
          classification.setSummary("需要处理的客户邮件");
      }

      return classification;
  }
}

搜索和跟踪节点

import com.alibaba.cloud.ai.graph.action.NodeAction;
import com.alibaba.cloud.ai.graph.OverAllState;
import java.util.Map;
import java.util.List;

// 文档搜索节点
public static class SearchDocumentationNode implements NodeAction {

  @Override
  public Map<String, Object> apply(OverAllState state) throws Exception {
      // 从分类构建搜索查询
      EmailClassification classification = state.value("classification")
              .map(v -> (EmailClassification) v)
              .orElse(new EmailClassification());
      String query = classification.getIntent() + " " + classification.getTopic();

      try {
          // 实现您的搜索逻辑
          // 存储原始搜索结果,而不是格式化的文本
          List<String> searchResults = List.of(
                  "通过设置 > 安全 > 更改密码重置密码",
                  "密码必须至少12个字符",
                  "包含大写字母、小写字母、数字和符号"
          );

          log.info("Searching documentation for: {}", query);

          return Map.of(
                  "search_results", searchResults,
                  "next_node", "draft_response"
          );
      } catch (Exception e) {
          // 对于可恢复的搜索错误,存储错误并继续
          log.warn("Search error: {}", e.getMessage());
          List<String> errorResult = List.of("搜索暂时不可用: " + e.getMessage());
          return Map.of(
                  "search_results", errorResult,
                  "next_node", "draft_response"
          );
      }
  }
}

// Bug跟踪节点
public static class BugTrackingNode implements NodeAction {

  @Override
  public Map<String, Object> apply(OverAllState state) throws Exception {
      // 在您的bug跟踪系统中创建票据
      String ticketId = "BUG-12345";  // 将通过API创建

      log.info("Created bug ticket: {}", ticketId);

      return Map.of(
              "search_results", List.of("已创建Bug票据 " + ticketId),
              "current_step", "bug_tracked",
              "next_node", "draft_response"
      );
  }
}

响应节点

import com.alibaba.cloud.ai.graph.action.NodeAction;
import com.alibaba.cloud.ai.graph.OverAllState;
import org.springframework.ai.chat.client.ChatClient;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
import java.util.stream.Collectors;

// 起草回复节点
public static class DraftResponseNode implements NodeAction {

  private final ChatClient chatClient;

  public DraftResponseNode(ChatClient.Builder chatClientBuilder) {
      this.chatClient = chatClientBuilder.build();
  }

  @Override
  public Map<String, Object> apply(OverAllState state) throws Exception {
      EmailClassification classification = state.value("classification")
              .map(v -> (EmailClassification) v)
              .orElse(new EmailClassification());
      String emailContent = state.value("email_content")
              .map(v -> (String) v)
              .orElse("");

      // 从原始状态数据按需格式化上下文
      List<String> contextSections = new ArrayList<>();

      Optional<List<String>> searchResults = state.value("search_results")
              .map(v -> (List<String>) v);
      if (searchResults.isPresent()) {
          // 为提示格式化搜索结果
          List<String> docs = searchResults.get();
          String formattedDocs = docs.stream()
                  .map(doc -> "- " + doc)
                  .collect(Collectors.joining("
"));
          contextSections.add("相关文档:
" + formattedDocs);
      }

      Optional<Map<String, Object>> customerHistory = state.value("customer_history")
              .map(v -> (Map<String, Object>) v);
      if (customerHistory.isPresent()) {
          // 为提示格式化客户数据
          Map<String, Object> history = customerHistory.get();
          contextSections.add("客户等级: " + history.getOrDefault("tier", "standard"));
      }

      // 使用格式化的上下文构建提示
      String draftPrompt = String.format("""
              为这封客户邮件起草回复:
              %s

              邮件意图: %s
              紧急程度: %s

              %s

              指南:
              - 专业且有帮助
              - 解决他们的具体问题
              - 在相关时使用提供的文档
              """,
              emailContent,
              classification.getIntent(),
              classification.getUrgency(),
              String.join("
", contextSections)
      );

      String response = chatClient.prompt()
              .user(draftPrompt)
              .call()
              .content();

      // 根据紧急程度和意图确定是否需要人工审核
      boolean needsReview =
              List.of("high", "critical").contains(classification.getUrgency()) ||
                      "complex".equals(classification.getIntent());

      // 路由到适当的下一个节点
      String nextNode = needsReview ? "human_review" : "send_reply";

      return Map.of(
              "draft_response", response,  // 仅存储原始响应
              "next_node", nextNode
      );
  }
}

// 人工审核节点
// 注意:在 interruptBefore 模式下,中断是在编译配置中设置的(见 createEmailAgentGraph 方法)。
// 节点本身不需要做任何特殊处理,只需要正常返回状态即可。
// 当执行到此节点前时,Graph 会自动中断,等待人工输入。
public static class HumanReviewNode implements NodeAction {

  @Override
  public Map<String, Object> apply(OverAllState state) throws Exception {
      EmailClassification classification = state.value("classification")
              .map(v -> (EmailClassification) v)
              .orElse(new EmailClassification());

      // 准备审核数据
      Map<String, Object> reviewData = Map.of(
              "email_id", state.value("email_id").map(v -> (String) v).orElse(""),
              "original_email", state.value("email_content").map(v -> (String) v).orElse(""),
              "draft_response", state.value("draft_response").map(v -> (String) v).orElse(""),
              "urgency", classification.getUrgency(),
              "intent", classification.getIntent(),
              "action", "请审核并批准/编辑此响应"
      );

      log.info("Waiting for human review: {}", reviewData);

      // 返回审核数据和下一个节点
      // 注意:在 interruptBefore 模式下,此节点在人工输入后才会执行
      return Map.of(
              "review_data", reviewData,
              "status", "waiting_for_review",
              "next_node", "send_reply"
      );
  }
}

// 发送回复节点
public static class SendReplyNode implements NodeAction {

  @Override
  public Map<String, Object> apply(OverAllState state) throws Exception {
      String draftResponse = state.value("draft_response")
              .map(v -> (String) v)
              .orElse("");

      // 与邮件服务集成
      log.info("Sending reply: {}...", 
              draftResponse.length() > 100 
                      ? draftResponse.substring(0, 100) 
                      : draftResponse);

      return Map.of("status", "sent");
  }
}

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

赞(0)
未经允许不得转载:AI项目博客 » Spring AI Alibaba 65|Graph Core · Graph 工作流编排指南 · 步骤 4:构建您的节点:实现我们的邮件代理节点

评论 抢沙发

AI项目博客

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

关于本站文章归档

登录

找回密码

注册