LangGraph4j工作流框架

目录

[一. LangGraph4j介绍](#一. LangGraph4j介绍)

[二. LangGraph4j核心特性](#二. LangGraph4j核心特性)

[StateGraph 工作流图](#StateGraph 工作流图)

AgentState状态

Nodes工作节点

Edges边

Checkpoints检查点

[三. LangGraph4j高级特性](#三. LangGraph4j高级特性)

Streaming异步和流式处理

可视化

[Parallel Branch 并发](#Parallel Branch 并发)

[Subgraphs 子图](#Subgraphs 子图)

[Breakpoints 断点](#Breakpoints 断点)

[四. 工作流开发](#四. 工作流开发)

定义状态

定义工作节点

开发工作节点

图片收集节点

内容图片收集工具

插画图片收集工具

架构图绘制工具

Logo图片生成工具

图片收集节点开发

提示词增强节点

智能路由节点

代码生成节点

项目构建节点

工作流图使用工作节点

[五、LangGraph4j 工作流特性实战](#五、LangGraph4j 工作流特性实战)

条件边

循环边

并发

[CompletableFuture 并发实现](#CompletableFuture 并发实现)

[LangGraph4j 并发实现](#LangGraph4j 并发实现)

SSE流式输出


一. LangGraph4j介绍

LangGraph4j 是 Java 生态的状态化多智能体工作流编排框架 ,灵感来自 Python 的 LangGraph,用于构建有状态、可循环、支持 LLM 的 Agent 应用,无缝集成 LangChain4j / Spring AI ,虽然对比py中的LangGraph框架来说,LangGraph4j还不够成熟,但是已经能够满足大多数工作流开发的需求

二. LangGraph4j核心特性

根据官方特性文档,LangGraph4j提供了几个核心特性:stateGraph 工作流图、AgentState 状态、Nodes 工作节点、Edges边、Checkpoints检查点,接下来我们一个一个来学习

StateGraph 工作流图

StateGraph带全局状态的有向图工作流引擎 ,源自 LangGraph,以状态 为核心、节点 为执行单元、为流程跳转规则,搭建可分支、可循环、可回退的 AI / 业务流程。它和传统的有向无环⁢⁢⁢图(DAG)不同,LangGra‍‍‍ph4j 支持循环,比如一个智能体可能需要根据结果回到之前的步骤‏‏‏进行重试,或者需要在某个条件满足‎‎‎之前持续循环执行某个逻辑。

经典的几个工作流图:

AgentState状态

AgentState 是 LangGraph4j 里 StateGraph 工作流唯一全局共享状态容器 ,整个流程图所有节点共用同一份状态数据,实现节点间数据传递、上下文留存、流程记忆

核心作用:

  • 存储流程全局数据:输入文本、LLM 结果、工具返回值、历史对话、标记字段

  • 节点只读获取状态,只返回增量数据,不直接修改原状态

  • 作为条件边判断依据,决定流程走分支、循环还是结束

  • 支持持久化存档,实现断点续跑

状态有关的概念:

  1. Schema:状态的结构通过Schema来定义,这是一个Map<String,channel.Reducer>的映射,Schema中的每一个键对应状态中的一个属性,而值则是一个 Channel.Reducer,用于定义如何处理对该属性的更新。

  2. Channel.Reducer是状态更新的核心机制,定义了如何将新值和旧值合并

总结:

  • Schema = 状态的「结构定义」(规定每个字段叫什么、怎么存、怎么合并)

  • Channel.Reducer = 字段的「合并规则」(当多个节点同时更新同一个字段时,怎么算最终值)

没有 Schema,就没有 StateGraph;没有 Reducer,状态就会互相覆盖。

Nodes工作节点

Node(工作节点) 是 StateGraph 工作流里最小独立执行单元 ,承载具体业务 / AI 逻辑,接收全局AgentState状态作为入参,执行完毕产出状态增量数据,交由引擎合并更新全局状态。

一个节点通常是一个函数或者一个实现了NodeAction<S>AsyncNodeAction<S> 接口的类,在其中编写具体操作的代码,节点可以是⁢⁢⁢同步的,也可以‍是‍异‍步的,甚至可以多个节点同时‏执行‏。

节点执行流程:

  1. 工作流流转到达当前节点

  2. 节点接收最新全局 AgentState

  3. 读取状态内字段数据,执行业务逻辑

  4. 生成增量状态数据(只写变动字段)

  5. 引擎根据Schema绑定的Reducer合并新旧状态

  6. 合并完成后,按照边规则跳转至下一节

LangGraph4j中还定义了两个特殊的节点,START 和 END。START ‏‏‏节点表示图的入口点,EN‎‎‎D 节点表示执行路径的结束点,这让工作流的起始和结束变得明确可控

Edges边

Edge(边) 是连接各个Node 节点流程流向规则 ,作用是定义节点执行先后顺序、控制工作流走向,决定跑完当前节点后下一步走哪个节点。

LangGraph4j 支持几种不同类型的边:

  • 普通边:最简单的边类型,提供从一个节点到另一个节点的无条件转换。可以通过 addEdge(sourceNodeName, destinationNodeName) 来定义普通边。
  • 条件边:下一个节点是根据当前 AgentState 动态确定的,更加灵活。当源节点完成后,会执行一个判断函数,这个函数接收当前状态并返回下一个要执行的节点名称。相当于实现了 if else 分支逻辑。比如智能体决定使用工具,就跳转到 "执行工具" 节点,否则跳转到 "回复用户" 节点。

Checkpoints检查点

Checkpoint(检查点) 是 StateGraph 提供的状态持久化快照机制 ,流程运行中自动保存每一阶段的 AgentState 全局状态 ,实现暂停、重启、断点续跑、历史回溯

检查点的核心作用体现在两个方面:

  • 支持人类检查、中断和批准⁢⁢⁢工作步骤。在某些场景下,人类必须能够在任何时间点查看图‍‍‍的状态,并且图必须能够在人类对状态进行任何更新后恢复执行。这种 "人在环路"(human-in-the-lo‏‏‏op)的设计在许多实际应用中都是必需的,比如 Curs‎‎‎or 在使用文件删除工具时会找用户确认。
  • 允许通过线程隔离不同⁢⁢⁢用户的交互,并且相同用户可以恢复之前的记忆。这对‍‍‍于构建多用户的 AI 应用来说特别重要,每个用户都可以有自己独立的执行历史。跟我们之前学习的 C‏‏‏hat Memory 对话记忆类似

在实际使用中,要根据具体需求来选择合适的持久化方案。内存检查点器适合开发和测试,但对于生产环境,需要考虑更持久的存储方案,比如 保存到 Postgre SQL

三. LangGraph4j高级特性

Streaming异步和流式处理

通过CompletableFutrue,LangGraph4j允许非阻塞线程的异步操作,当某个节点等待LLM返回时,整个应用不会被阻塞,可以继续处理其他任务

CompletableFutrue和Thread的区别:

Thread = 执行任务的 "工人"

  • 真正干活的执行单元

  • 分为:平台线程(重量级)、虚拟线程(轻量级)

  • 只负责:执行代码,不负责传递结果、异常、编排

CompletableFuture = 任务的 "包装 + 调度 + 结果容器"

  • 把任务包成异步任务

  • 自动管理:结果返回、异常、回调、链式执行、多任务组合

  • 底层还是要用线程去执行(平台线程 / 虚拟线程)

CompletableFuture 底层就是基于 Thread 实现,只是在上层做了超强封装

核心真相:

  • 本质没变任何异步任务最终都离不开线程执行,CompletableFuture 本身不创建线程,只是任务调度、结果托管、流程编排的工具壳子。

默认线程来源:

  • runAsync() / supplyAsync() 不传线程池:默认使用 ForkJoinPool.commonPool(),池子里全是平台线程

根据 流式处理文档,LangGraph4j 支持通过 Java 异步生成器来处理来自 LLM 和其他源的流式响应,可以实现 SSE 流式输出,提升用户体验。

可视化

LangGraph4j 通过 graph.getGraph 方法提供了多种内置的工作流可视化方式。可以使用Mermaid文本绘图语法来生成工作流结构图:

Parallel Branch 并发

Parallel Branch = 工作流里的「多任务同时跑」= 一个节点 → 同时分叉成多个节点并行执行 → 全部跑完再汇总

但是目前并发处理存在一些局限性,比如不支持条件边,并且并发执行的分支中只能有一个工作节点,不过对于这种局限性我们可以通过子图来解决:

Subgraphs 子图

如果工作流需要用到上面说的那些功能,就可以使用子图功能,相当于把一个工作流拆分成多个子模块,多个子图可以同时并发:

Breakpoints 断点

假设我们在某个节点需要暂停执行,等待人工审核或确认,LangGraph4j的断点功能就是为这种场景设计的,断点可以在编译时静态指定也可以让节点操作实现特定接口来在运行时动态设置,当执行到设置了断点的节点时,会自动暂停等待外部信号再继续执行:

比如在代码生成系统中,可能希望在生成代码后暂停执行‏‏‏,让人工审核代码质量,确认无误‎‎‎后再继续后续的构建和部署流程。使用断点功能时必须配合检查点器使用 。因为图需要能够保存当前状态并在稍后恢复执行。要恢复执行,只需要调用 GraphInput.resume() 即可。

四. 工作流开发

根据前几期Langchain4j实战做的AI应用生成平台,这期我们用工作流实现一下核心流程

根据我们的需求梳理出工作流程:

  1. 输入原始 Prompt

  2. 获取图片素材 Agent:通过工具调用从不同的渠道获取图片

  3. 内容图片:pexels 网页搜索

  4. 插画图片:undraw 抓取

  5. 画架构图:文本绘图 + 上传到 COS

  6. Logo 等设计图片:AI 生成或者 MCP

  7. 提示词增强:关联图片内容到原始提示词

  8. 智能路由 Agent:选用哪种模式生成网站

  9. 原生 HTML

  10. 原生多文件

  11. Vue 工程

  12. 网站生成 Agent:利用搜集到的图片,根据上一步确认的生成模式来生成网站

  13. 项目构建器:文件保存 / 打包构建

那么我们先来写一个简化版的工作流结构代码模板。模拟跑一边流程:

复制代码
/**
 * 简化版网站生成工作流应用 - 使用 MessagesState
 */
@Slf4j
public class SimpleWorkflowApp {

    /**
     * 创建工作节点的通用方法
     */
    static AsyncNodeAction<MessagesState<String>> makeNode(String message) {
        return node_async(state -> {
            log.info("执行节点: {}", message);
            return Map.of("messages", message);
        });
    }

    public static void main(String[] args) throws GraphStateException {
        // 创建工作流图
        CompiledGraph<MessagesState<String>> workflow = new MessagesStateGraph<String>()
                // 添加节点
                .addNode("image_collector", makeNode("获取图片素材"))
                .addNode("prompt_enhancer", makeNode("增强提示词"))
                .addNode("router", makeNode("智能路由选择"))
                .addNode("code_generator", makeNode("网站代码生成"))
                .addNode("project_builder", makeNode("项目构建"))

                // 添加边
                .addEdge(START, "image_collector")                // 开始 -> 图片收集
                .addEdge("image_collector", "prompt_enhancer")    // 图片收集 -> 提示词增强
                .addEdge("prompt_enhancer", "router")             // 提示词增强 -> 智能路由
                .addEdge("router", "code_generator")              // 智能路由 -> 代码生成
                .addEdge("code_generator", "project_builder")     // 代码生成 -> 项目构建
                .addEdge("project_builder", END)                  // 项目构建 -> 结束

                // 编译工作流
                .compile();

        log.info("开始执行工作流");

        GraphRepresentation graph = workflow.getGraph(GraphRepresentation.Type.MERMAID);
        log.info("工作流图: \n{}", graph.content());

        // 执行工作流
        int stepCounter = 1;
        for (NodeOutput<MessagesState<String>> step : workflow.stream(Map.of())) {
            log.info("--- 第 {} 步完成 ---", stepCounter);
            log.info("步骤输出: {}", step);
            stepCounter++;
        }

        log.info("工作流执行完成!");
    }
}

MessagesState 其实就是专门装对话消息的上下文盒子,执行工作流程序

复制代码
13:52:23.425 [main] INFO com.sunny.sunnyaicodebackend.langGraph4j.SimpleStatefulworkflowApp -- 初始输入: 创建一个爱编程的小新的个人博客网站
13:52:23.435 [main] INFO com.sunny.sunnyaicodebackend.langGraph4j.SimpleStatefulworkflowApp -- 开始执行工作流
13:52:23.446 [main] INFO com.sunny.sunnyaicodebackend.langGraph4j.SimpleStatefulworkflowApp -- 工作流图:
---
title: Graph Diagram
---
flowchart TD
	__START__((start))
	__END__((stop))
	image_collector("image_collector")
	prompt_enhancer("prompt_enhancer")
	router("router")
	code_generator("code_generator")
	project_builder("project_builder")
	__START__:::__START__ --> image_collector:::image_collector
	image_collector:::image_collector --> prompt_enhancer:::prompt_enhancer
	prompt_enhancer:::prompt_enhancer --> router:::router
	router:::router --> code_generator:::code_generator
	code_generator:::code_generator --> project_builder:::project_builder
	project_builder:::project_builder --> __END__:::__END__

	classDef ___START__ fill:black,stroke-width:1px,font-size:xx-small;
	classDef ___END__ fill:black,stroke-width:1px,font-size:xx-small;

上面输出结果就是工作流图结构,我们可以用Merm‏aid‏ 语法解析器解析:

接下来的就是工作流每一步节点执行信息和状态信息:

那么工作流结构代码模板现在已经写好了,我们现在只需要一点一点进行完善即可

定义状态

官方提供的默认状态类时消息列表结构:

但是现在,我们需要维护的状态包含多个字段,所以我们需要定义一个类的结构,统一维护所有需要的字段,为了和AgentState兼容,我们可以将WorkflowContext(自定义类)对象作为一个key/value存放在MessageState中,需要使用时同过state.data().getkey获得

创建WorkflowContext上下文类:

复制代码
/**
 * 工作流上下文 - 存储所有状态信息
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WorkflowContext implements Serializable {

    /**
     * WorkflowContext 在 MessagesState 中的存储key
     */
    public static final String WORKFLOW_CONTEXT_KEY = "workflowContext";

    /**
     * 当前执行步骤
     */
    private String currentStep;

    /**
     * 用户原始输入的提示词
     */
    private String originalPrompt;

    /**
     * 图片资源字符串
     */
    private String imageListStr;

    /**
     * 图片资源列表
     */
    private List<ImageResource> imageList;

    /**
     * 增强后的提示词
     */
    private String enhancedPrompt;

    /**
     * 代码生成类型
     */
    private CodeGenTypeEnum generationType;

    /**
     * 生成的代码目录
     */
    private String generatedCodeDir;

    /**
     * 构建成功的目录
     */
    private String buildResultDir;

    /**
     * 错误信息
     */
    private String errorMessage;

    @Serial
    private static final long serialVersionUID = 1L;

    // ========== 上下文操作方法 ==========

    /**
     * 从 MessagesState 中获取 WorkflowContext
     */
    public static WorkflowContext getContext(MessagesState<String> state) {
        return (WorkflowContext) state.data().get(WORKFLOW_CONTEXT_KEY);
    }

    /**
     * 将 WorkflowContext 保存到 MessagesState 中
     */
    public static Map<String, Object> saveContext(WorkflowContext context) {
        return Map.of(WORKFLOW_CONTEXT_KEY, context);
    }
}

创建ImageResource类:存放图片信息

复制代码
/**
 * 图片资源对象
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ImageResource implements Serializable {

    /**
     * 图片类别
     */
    private ImageCategoryEnum category;

    /**
     * 图片描述
     */
    private String description;

    /**
     * 图片地址
     */
    private String url;

    @Serial
    private static final long serialVersionUID = 1L;
}

创建图片类型枚举类:

复制代码
@Getter
public enum ImageCategoryEnum {

    CONTENT("内容图片", "CONTENT"),
    LOGO("LOGO图片", "LOGO"),
    ILLUSTRATION("插画图片", "ILLUSTRATION"),
    ARCHITECTURE("架构图片", "ARCHITECTURE");


    private final String text;

    private final String value;

    ImageCategoryEnum(String text, String value) {
        this.text = text;
        this.value = value;
    }

    /**
     * 根据 value 获取枚举
     *
     * @param value 枚举值的value
     * @return 枚举值
     */
    public static ImageCategoryEnum getEnumByValue(String value) {
        if (ObjUtil.isEmpty(value)) {
            return null;
        }
        for (ImageCategoryEnum anEnum : ImageCategoryEnum.values()) {
            if (anEnum.value.equals(value)) {
                return anEnum;
            }
        }
        return null;
    }
}

修改工作流结构代码模板(工作流图):创建带状态感知的工作节点,执行工作流传入初始上下文对象

复制代码
/**
 * 简化版带状态定义的工作流 - 只定义状态结构,不实现具体流转
 */
@Slf4j
public class SimpleStatefulWorkflowApp {

    /**
     * 创建带状态感知的工作节点
     */
    static AsyncNodeAction<MessagesState<String>> makeStatefulNode(String nodeName, String message) {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            log.info("执行节点: {} - {}", nodeName, message);
            // 只记录当前步骤,不做具体的状态流转
            if (context != null) {
                context.setCurrentStep(nodeName);
            }
            return WorkflowContext.saveContext(context);
        });
    }

    public static void main(String[] args) throws GraphStateException {
        // 创建工作流图
        CompiledGraph<MessagesState<String>> workflow = new MessagesStateGraph<String>()
                // 添加节点 - 使用带状态感知的节点
                .addNode("image_collector", makeStatefulNode("image_collector", "获取图片素材"))
                .addNode("prompt_enhancer", makeStatefulNode("prompt_enhancer", "增强提示词"))
                .addNode("router", makeStatefulNode("router", "智能路由选择"))
                .addNode("code_generator", makeStatefulNode("code_generator", "网站代码生成"))
                .addNode("project_builder", makeStatefulNode("project_builder", "项目构建"))

                // 添加边
                .addEdge(START, "image_collector")
                .addEdge("image_collector", "prompt_enhancer")
                .addEdge("prompt_enhancer", "router")
                .addEdge("router", "code_generator")
                .addEdge("code_generator", "project_builder")
                .addEdge("project_builder", END)

                // 编译工作流
                .compile();

        // 初始化 WorkflowContext - 只设置基本信息
        WorkflowContext initialContext = WorkflowContext.builder()
                .originalPrompt("创建一个爱编程的小新的个人博客网站")
                .currentStep("初始化")
                .build();

        log.info("初始输入: {}", initialContext.getOriginalPrompt());
        log.info("开始执行工作流");

        // 显示工作流图
        GraphRepresentation graph = workflow.getGraph(GraphRepresentation.Type.MERMAID);
        log.info("工作流图:\n{}", graph.content());

        // 执行工作流
        int stepCounter = 1;
        for (NodeOutput<MessagesState<String>> step : workflow.stream(Map.of(WorkflowContext.WORKFLOW_CONTEXT_KEY, initialContext))) {
            log.info("--- 第 {} 步完成 ---", stepCounter);
            // 显示当前状态
            WorkflowContext currentContext = WorkflowContext.getContext(step.state());
            if (currentContext != null) {
                log.info("当前步骤上下文: {}", currentContext);
            }
            stepCounter++;
        }
        log.info("工作流执行完成!");
    }
}

定义工作节点

上面我们定义了状态信息存放的类,现在我们来定义工作节点,在工作节点中就可以执行对应的逻辑了,这里我们先进行模式状态流转,先不实现真实业务逻辑

创建图片收集节点:

复制代码
@Slf4j
public class ImageCollectorNode {
    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            log.info("执行节点: 图片收集");
            
            // TODO: 实际执行图片收集逻辑
            
            // 简单的假数据
            List<ImageResource> imageList = Arrays.asList(
                ImageResource.builder()
                    .category(ImageCategoryEnum.CONTENT)
                    .description("假数据图片1")
                    .url("https://www.codefather.cn/logo.png")
                    .build(),
                ImageResource.builder()
                    .category(ImageCategoryEnum.LOGO)
                    .description("假数据图片2")
                    .url("https://www.codefather.cn/logo.png")
                    .build()
            );
            
            // 更新状态
            context.setCurrentStep("图片收集");
            context.setImageList(imageList);
            log.info("图片收集完成,共收集 {} 张图片", imageList.size());
            return WorkflowContext.saveContext(context);
        });
    }
}

创建提示词增强节点:

复制代码
@Slf4j
public class PromptEnhancerNode {
    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            log.info("执行节点: 提示词增强");
            
            // TODO: 实际执行提示词增强逻辑
            
            // 简单的假数据
            String enhancedPrompt = "这是增强后的假数据提示词";
            
            // 更新状态
            context.setCurrentStep("提示词增强");
            context.setEnhancedPrompt(enhancedPrompt);
            log.info("提示词增强完成");
            return WorkflowContext.saveContext(context);
        });
    }
}

创建只能路由节点:

复制代码
@Slf4j
public class RouterNode {
    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            log.info("执行节点: 智能路由");
            
            // TODO: 实际执行智能路由逻辑
            
            // 简单的假数据
            CodeGenTypeEnum generationType = CodeGenTypeEnum.HTML;
            // 更新状态
            context.setCurrentStep("智能路由");
            context.setGenerationType(generationType);
            log.info("路由决策完成,选择类型: {}", generationType.getText());
            return WorkflowContext.saveContext(context);
        });
    }
}

创建代码生成节点:

复制代码
@Slf4j
public class CodeGeneratorNode {
    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            log.info("执行节点: 代码生成");
            
            // TODO: 实际执行代码生成逻辑
            
            // 简单的假数据
            String generatedCodeDir = "/tmp/generated/fake-code";
            // 更新状态
            context.setCurrentStep("代码生成");
            context.setGeneratedCodeDir(generatedCodeDir);
            log.info("代码生成完成,目录: {}", generatedCodeDir);
            return WorkflowContext.saveContext(context);
        });
    }
}

创建项目构建节点:

复制代码
@Slf4j
public class ProjectBuilderNode {
    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            log.info("执行节点: 项目构建");
            
            // TODO: 实际执行项目构建逻辑
            
            // 简单的假数据
            String buildResultDir = "/tmp/build/fake-build";
            
            // 更新状态
            context.setCurrentStep("项目构建");
            context.setBuildResultDir(buildResultDir);
            log.info("项目构建完成,结果目录: {}", buildResultDir);
            return WorkflowContext.saveContext(context);
        });
    }
}

创建工作流图应用工作节点(就是修改模板啦):

复制代码
@Slf4j
public class WorkflowApp {

    public static void main(String[] args) throws GraphStateException {
        // 创建工作流图
        CompiledGraph<MessagesState<String>> workflow = new MessagesStateGraph<String>()
                // 添加节点 - 使用真实的工作节点
                .addNode("image_collector", ImageCollectorNode.create())
                .addNode("prompt_enhancer", PromptEnhancerNode.create())
                .addNode("router", RouterNode.create())
                .addNode("code_generator", CodeGeneratorNode.create())
                .addNode("project_builder", ProjectBuilderNode.create())
                // 添加边
                .addEdge(START, "image_collector")
                .addEdge("image_collector", "prompt_enhancer")
                .addEdge("prompt_enhancer", "router")
                .addEdge("router", "code_generator")
                .addEdge("code_generator", "project_builder")
                .addEdge("project_builder", END)
                // 编译工作流
                .compile();

        // 初始化 WorkflowContext - 只设置基本信息
        WorkflowContext initialContext = WorkflowContext.builder()
                .originalPrompt("创建一个爱编程的小新的个人博客网站")
                .currentStep("初始化")
                .build();
        log.info("初始输入: {}", initialContext.getOriginalPrompt());
        log.info("开始执行工作流");

        // 显示工作流图
        GraphRepresentation graph = workflow.getGraph(GraphRepresentation.Type.MERMAID);
        log.info("工作流图:\n{}", graph.content());

        // 执行工作流
        int stepCounter = 1;
        for (NodeOutput<MessagesState<String>> step : workflow.stream(Map.of(WorkflowContext.WORKFLOW_CONTEXT_KEY, initialContext))) {
            log.info("--- 第 {} 步完成 ---", stepCounter);
            // 显示当前状态
            WorkflowContext currentContext = WorkflowContext.getContext(step.state());
            if (currentContext != null) {
                log.info("当前步骤上下文: {}", currentContext);
            }
            stepCounter++;
        }
        log.info("工作流执行完成!");
    }
}

执行查看输出结果,应该就能看到和之前一样的步骤,并且有输出日志,接下来我们就要在每个结点中实现真实的业务啦

开发工作节点

图片收集节点

图片收集节点的作用是让AI根据用户提示词获取到网站所需的图片,我们只需要给AI提供各种不同类型的图片收集工具,让AI调用工具来获取不同类型的图片,并且利用结构化输出特性直接获取到最终的图片列表

内容图片收集工具

这里我们使用Pexels来免费获取图片资源

复制代码
@Slf4j
@Component
public class ImageSearchTool {

    private static final String PEXELS_API_URL = "https://api.pexels.com/v1/search";

    @Value("${pexels.api-key}")
    private String pexelsApiKey;

    @Tool("搜索内容相关的图片,用于网站内容展示")
    public List<ImageResource> searchContentImages(@P("搜索关键词") String query) {
        List<ImageResource> imageList = new ArrayList<>();
        int searchCount = 12;
        // 调用 API,注意释放资源
        try (HttpResponse response = HttpRequest.get(PEXELS_API_URL)
                .header("Authorization", pexelsApiKey)
                .form("query", query)
                .form("per_page", searchCount)
                .form("page", 1)
                .execute()) {
            if (response.isOk()) {
                JSONObject result = JSONUtil.parseObj(response.body());
                JSONArray photos = result.getJSONArray("photos");
                for (int i = 0; i < photos.size(); i++) {
                    JSONObject photo = photos.getJSONObject(i);
                    JSONObject src = photo.getJSONObject("src");
                    imageList.add(ImageResource.builder()
                            .category(ImageCategoryEnum.CONTENT)
                            .description(photo.getStr("alt", query))
                            .url(src.getStr("medium"))
                            .build());
                }
            }
        } catch (Exception e) {
            log.error("Pexels API 调用失败: {}", e.getMessage(), e);
        }
        return imageList;
    }
}
插画图片收集工具

这里我们可以通过Pixabay获取插画图片资源

复制代码
package com.sunny.sunnyaicodebackend.langGraph4j.tools;

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.sunny.sunnyaicodebackend.langGraph4j.model.ImageCategoryEnum;
import com.sunny.sunnyaicodebackend.langGraph4j.model.ImageResource;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Component
public class PixabayIllustrationTool {

    // Pixabay API 基础地址。使用常量可以避免魔法值,提高代码可维护性
    private static final String PIXABAY_API_URL = "https://pixabay.com/api/";

    // 从配置文件读取 API 密钥。使用 @Value 注解可以轻松从外部配置获取敏感信息
    @Value("${pixabay.api-key}")
    private String apiKey;

    // 用于验证配置是否加载成功的日志方法
    @PostConstruct
    public void init() {
        log.info("PixabayIllustrationTool 初始化完成,API Key: {}",
                apiKey != null && !apiKey.isEmpty() ? "已配置" : "未配置");
    }

    /**
     * 搜索插画图片,用于网站美化和装饰。
     * 该方法会被 LangGraph4j 或 LangChain4j 的 AI Agent 自动调用。
     *
     * @param query 搜索关键词
     * @return 图片资源列表
     */
    @Tool("搜索插画图片,用于网站美化和装饰")
    public List<ImageResource> searchIllustrations(@P("搜索关键词") String query) {
        List<ImageResource> imageList = new ArrayList<>();

        // 校验 API Key 是否配置,避免不必要的 API 调用
        if (apiKey == null || apiKey.isEmpty()) {
            log.error("Pixabay API Key 未配置,请检查 application.yml 文件");
            return imageList;
        }

        // 使用 try-with-resources 语法,HttpResponse 会自动关闭底层连接[reference:2]
        try (HttpResponse response = HttpRequest.get(PIXABAY_API_URL)
                .form("key", apiKey)           // API 密钥
                .form("q", query)              // 搜索关键词
                .form("image_type", "illustration") // 筛选插画类型
                .form("per_page", 5)          // 每页返回数量 (官方范围 3-200)
                .form("safesearch", "true")    // 开启安全搜索,过滤不适合的内容【11†L4-L10】
                .timeout(15000)                // 设置超时时间,单位为毫秒
                .execute()) {

            // 检查 HTTP 响应状态码是否为 2xx
            if (!response.isOk()) {
                log.warn("Pixabay API 请求失败,状态码: {}", response.getStatus());
                return imageList;
            }

            // 解析返回的 JSON 字符串
            JSONObject result = JSONUtil.parseObj(response.body());
            JSONArray hits = result.getJSONArray("hits");

            // 检查请求配额,避免在调试时因配额用尽而困惑(可选)
            checkRateLimit(result);

            // 如果没有找到任何结果,直接返回空列表
            if (hits == null || hits.isEmpty()) {
                log.info("未找到关键词为 '{}' 的插画", query);
                return imageList;
            }

            // 遍历所有命中结果,将其转换为业务模型
            for (int i = 0; i < hits.size(); i++) {
                JSONObject hit = hits.getJSONObject(i);

                // 提取图片的标签和 URL,处理好空值情况
                String description = hit.getStr("tags", query);
                // 优先使用预览图 URL,格式为 'https://..._640.jpg'
                String previewURL = hit.getStr("webformatURL");

                // 只有当 URL 不为空时才添加到结果列表【11†L4-L10】
                if (StrUtil.isNotBlank(previewURL)) {
                    imageList.add(ImageResource.builder()
                            .category(ImageCategoryEnum.ILLUSTRATION) // 插画类型
                            .description(description)                 // 图片描述
                            .url(previewURL)                          // 图片 URL
                            .build());
                }
            }

            log.info("成功搜索到 {} 张相关插画", imageList.size());

        } catch (Exception e) {
            log.error("调用 Pixabay API 搜索插画时发生异常", e);
        }

        return imageList;
    }

    /**
     * 检查并记录API的速率限制信息【11†L4-L10】。
     * 这是一个辅助方法,可以帮助监控API使用情况。
     */
    private void checkRateLimit(JSONObject result) {
        // 注意:根据官方文档,限流信息在响应头中。
        // 如果使用 Hutool,可以通过 use method 获取响应头。
        // 这里提供一个占位实现,实际获取逻辑可能需要调整。
        // 可从响应头 'X-RateLimit-Limit', 'X-RateLimit-Remaining',
        // 'X-RateLimit-Reset' 中获取相关信息,但需评估性能影响。
        if (log.isDebugEnabled()) {
            // 解析限流信息暂略;官方文档指出响应头包含 X-RateLimit-* 字段。
            log.debug("API 限流检查(完整实现需要解析响应头)");
        }
    }
}
架构图绘制工具

由于架构图是需⁢⁢⁢要根据特定的描述来定制的,‍‍‍不能直接上网搜索,因此我们的思路是将 AI 调用工具‏‏‏时传入的 Mermaid ‎‎‎文本绘图代码转换成图片。

mermaid-cli + COS:先利用 Mermaid CLI 工具将文本绘图代码转换为图片,之后上传到 COS 对象存储拿到对应的 URL 地址方便后续使用。

安装必备的mermaid-cli工具:

复制代码
npm install -g @mermaid-js/mermaid-cli

package com.sunny.sunnyaicodebackend.langGraph4j.tools;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.RuntimeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.system.SystemUtil;
import com.sunny.sunnyaicodebackend.exception.BusinessException;
import com.sunny.sunnyaicodebackend.exception.ErrorCode;
import com.sunny.sunnyaicodebackend.langGraph4j.model.ImageCategoryEnum;
import com.sunny.sunnyaicodebackend.langGraph4j.model.ImageResource;
import com.sunny.sunnyaicodebackend.manager.CosManager;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Slf4j
@Component
public class MermaidDiagramTool {

    //cos管理对象
    @Resource
    private CosManager cosManager;
    
    @Tool("将 Mermaid 代码转换为架构图图片,用于展示系统结构和技术关系")
    public List<ImageResource> generateMermaidDiagram(@P("Mermaid 图表代码") String mermaidCode,
                                                      @P("架构图描述") String description) {
        if (StrUtil.isBlank(mermaidCode)) {
            return new ArrayList<>();
        }
        try {
            // 转换为SVG图片
            File diagramFile = convertMermaidToSvg(mermaidCode);
            // 上传到COS
            String keyName = String.format("/mermaid/%s/%s",
                    RandomUtil.randomString(5), diagramFile.getName());
            String cosUrl = cosManager.uploadFile(keyName, diagramFile);
            // 清理临时文件
            FileUtil.del(diagramFile);
            if (StrUtil.isNotBlank(cosUrl)) {
                return Collections.singletonList(ImageResource.builder()
                        .category(ImageCategoryEnum.ARCHITECTURE)
                        .description(description)
                        .url(cosUrl)
                        .build());
            }
        } catch (Exception e) {
            log.error("生成架构图失败: {}", e.getMessage(), e);
        }
        return new ArrayList<>();
    }

    /**
     * 将Mermaid代码转换为SVG图片
     */
    private File convertMermaidToSvg(String mermaidCode) {
        // 创建临时输入文件
        File tempInputFile = FileUtil.createTempFile("mermaid_input_", ".mmd", true);
        FileUtil.writeUtf8String(mermaidCode, tempInputFile);
        // 创建临时输出文件
        File tempOutputFile = FileUtil.createTempFile("mermaid_output_", ".svg", true);
        // 根据操作系统选择命令
        String command = SystemUtil.getOsInfo().isWindows() ? "mmdc.cmd" : "mmdc";
        // 构建命令
        String cmdLine = String.format("%s -i %s -o %s -b transparent",
                command,
                tempInputFile.getAbsolutePath(),
                tempOutputFile.getAbsolutePath()
        );
        // 执行命令
        RuntimeUtil.execForStr(cmdLine);
        // 检查输出文件
        if (!tempOutputFile.exists() || tempOutputFile.length() == 0) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "Mermaid CLI 执行失败");
        }
        // 清理输入文件,保留输出文件供上传使用
        FileUtil.del(tempInputFile);
        return tempOutputFile;
    }
}
Logo图片生成工具

Log⁢⁢⁢o 图片也是需要根据文字‍‍‍定制生成的,因此需要用到 AI 文生图模型,此处选择接⁢⁢⁢入阿里云百炼平‍台‍的‍ AI 大模型

引入依赖:

复制代码
<!-- 阿里云 DashScope SDK -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dashscope-sdk-java</artifactId>
    <version>2.21.1</version>
</dependency>

添加配置:

复制代码
# 阿里云 DashScope 配置
dashscope:
  api-key: <Your API Key>
  image-model: wan2.2-t2i-flash

@Slf4j
@Component
public class LogoGeneratorTool {

    @Value("${dashscope.api-key:}")
    private String dashScopeApiKey;

    @Value("${dashscope.image-model:wan2.2-t2i-flash}")
    private String imageModel;

    @Tool("根据描述生成 Logo 设计图片,用于网站品牌标识")
    public List<ImageResource> generateLogos(@P("Logo 设计描述,如名称、行业、风格等,尽量详细") String description) {
        List<ImageResource> logoList = new ArrayList<>();
        try {
            // 构建 Logo 设计提示词
            String logoPrompt = String.format("生成 Logo,Logo 中禁止包含任何文字!Logo 介绍:%s", description);
            ImageSynthesisParam param = ImageSynthesisParam.builder()
                    .apiKey(dashScopeApiKey)
                    .model(imageModel)
                    .prompt(logoPrompt)
                    .size("512*512")
                    .n(1) // 生成 1 张足够,因为 AI 不知道哪张最好
                    .build();
            ImageSynthesis imageSynthesis = new ImageSynthesis();
            ImageSynthesisResult result = imageSynthesis.call(param);
            if (result != null && result.getOutput() != null && result.getOutput().getResults() != null) {
                List<Map<String, String>> results = result.getOutput().getResults();
                for (Map<String, String> imageResult : results) {
                    String imageUrl = imageResult.get("url");
                    if (StrUtil.isNotBlank(imageUrl)) {
                        logoList.add(ImageResource.builder()
                                .category(ImageCategoryEnum.LOGO)
                                .description(description)
                                .url(imageUrl)
                                .build());
                    }
                }
            }
        } catch (Exception e) {
            log.error("生成 Logo 失败: {}", e.getMessage(), e);
        }
        return logoList;
    }
}

创建一个专门收集图片的AI服务:

复制代码
你是一个专业的图片收集助手。根据用户的网站需求,智能选择并调用相应的工具收集不同类型的图片资源。

你可以根据需要调用下面多个工具,收集全面的图片资源:
1. searchContentImages - 搜索内容相关图片,用于网站内容展示
2. searchIllustrations - 搜索插画图片,用于网站美化和装饰  
3. generateArchitectureDiagram - 根据技术主题生成架构图,用于展示系统结构和技术关系
4. generateLogos - 根据描述生成Logo设计图片,用于网站品牌标识

请根据用户的需求分析,优先选择与用户需求最相关的图片类型:
- 如果涉及技术、系统、架构等内容,调用 generateArchitectureDiagram 生成架构图
- 如果需要品牌标识、Logo设计,调用 generateLogos 生成Logo
- 如果需要内容相关图片,调用 searchContentImages 搜索图片
- 如果需要装饰性插画,调用 searchIllustrations 搜索插画

你必须按照 JSON 格式输出!

public interface ImageCollectionService {

    /**
     * 根据用户提示词收集所需的图片资源
     * AI 会根据需求自主选择调用相应的工具
     */
    @SystemMessage(fromResource = "prompt/image-collection-system-prompt.txt")
    String collectImages(@UserMessage String userPrompt);
}

创建AI服务创建工厂,注入指定的模型和各种图片收集参数

复制代码
@Slf4j
@Configuration
public class ImageCollectionServiceFactory {

    @Resource
    private ChatModel chatModel;

    @Resource
    private ImageSearchTool imageSearchTool;

    @Resource
    private UndrawIllustrationTool undrawIllustrationTool;

    @Resource
    private MermaidDiagramTool mermaidDiagramTool;

    @Resource
    private LogoGeneratorTool logoGeneratorTool;

    /**
     * 创建图片收集 AI 服务
     */
    @Bean
    public ImageCollectionService createImageCollectionService() {
        return AiServices.builder(ImageCollectionService.class)
                .chatModel(chatModel)
                .tools(
                        imageSearchTool,
                        undrawIllustrationTool,
                        mermaidDiagramTool,
                        logoGeneratorTool
                )
                .build();
    }
}

在WorkflowContext状态新增图片资源字符串字段,用户接收AI输出的图片信息:

复制代码
/**
 * 图片资源字符串
 */
private String imageListStr;
图片收集节点开发

工作节点需⁢⁢⁢要调用 AI ‍服‍务‍收集图片,并更新状态的 ‏ima‏geL‏i‎stStr‎ 字段‎。由于目前我们的工作节点类都是通过静态方法提供工作节点的,可以写一个获取 Spring Bean 的静态工具类,就可以在静态方法中获取到 ImageCollectionService

复制代码
/**
 * Spring上下文工具类
 * 用于在静态方法中获取Spring Bean
 */
@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    /**
     * 获取Spring Bean
     */
    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    /**
     * 获取Spring Bean
     */
    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }

    /**
     * 根据名称和类型获取Spring Bean
     */
    public static <T> T getBean(String name, Class<T> clazz) {
        return applicationContext.getBean(name, clazz);
    }
}

图片收集节点开发

复制代码
/**
 * 图片收集节点
 * 使用AI进行工具调用,收集不同类型的图片
 */
@Slf4j
public class ImageCollectorNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            String originalPrompt = context.getOriginalPrompt();
            String imageListStr = "";
            try {
                // 获取AI图片收集服务
                ImageCollectionService imageCollectionService = SpringContextUtil.getBean(ImageCollectionService.class);
                // 使用 AI 服务进行智能图片收集
                imageListStr = imageCollectionService.collectImages(originalPrompt);
            } catch (Exception e) {
                log.error("图片收集失败: {}", e.getMessage(), e);
            }
            // 更新状态
            context.setCurrentStep("图片收集");
            context.setImageListStr(imageListStr);
            return WorkflowContext.saveContext(context);
        });
    }
}

提示词增强节点

将获取到的⁢⁢⁢图片信息拼接到原‍‍始‍提示词下,并在拼接提示词时引导‏‏ AI‏ 利用这些‎‎图片信息作为‎网站素材。

复制代码
@Slf4j
public class PromptEnhancerNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            log.info("执行节点: 提示词增强");
            // 获取原始提示词和图片列表
            String originalPrompt = context.getOriginalPrompt();
            String imageListStr = context.getImageListStr();
            List<ImageResource> imageList = context.getImageList();
            // 构建增强后的提示词
            StringBuilder enhancedPromptBuilder = new StringBuilder();
            enhancedPromptBuilder.append(originalPrompt);
            // 如果有图片资源,则添加图片信息
            if (CollUtil.isNotEmpty(imageList) || StrUtil.isNotBlank(imageListStr)) {
                enhancedPromptBuilder.append("\n\n## 可用素材资源\n");
                enhancedPromptBuilder.append("请在生成网站使用以下图片资源,将这些图片合理地嵌入到网站的相应位置中。\n");
                if (CollUtil.isNotEmpty(imageList)) {
                    for (ImageResource image : imageList) {
                        enhancedPromptBuilder.append("- ")
                                .append(image.getCategory().getText())
                                .append(":")
                                .append(image.getDescription())
                                .append("(")
                                .append(image.getUrl())
                                .append(")\n");
                    }
                } else {
                    enhancedPromptBuilder.append(imageListStr);
                }
            }
            String enhancedPrompt = enhancedPromptBuilder.toString();
            // 更新状态
            context.setCurrentStep("提示词增强");
            context.setEnhancedPrompt(enhancedPrompt);
            log.info("提示词增强完成,增强后长度: {} 字符", enhancedPrompt.length());
            return WorkflowContext.saveContext(context);
        });
    }
}

智能路由节点

根据用户的原始提示词选择对应的网站生成方式

复制代码
@Slf4j
public class RouterNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            log.info("执行节点: 智能路由");

            CodeGenTypeEnum generationType;
            try {
                // 获取AI路由服务
                AiCodeGenTypeRoutingService routingService = SpringContextUtil.getBean(AiCodeGenTypeRoutingService.class);
                // 根据原始提示词进行智能路由
                generationType = routingService.routeCodeGenType(context.getOriginalPrompt());
                log.info("AI智能路由完成,选择类型: {} ({})", generationType.getValue(), generationType.getText());
            } catch (Exception e) {
                log.error("AI智能路由失败,使用默认HTML类型: {}", e.getMessage());
                generationType = CodeGenTypeEnum.HTML;
            }

            // 更新状态
            context.setCurrentStep("智能路由");
            context.setGenerationType(generationType);
            return WorkflowContext.saveContext(context);
        });
    }
}

代码生成节点

首先根据代码生成类型,调用 AiCodeGeneratorFacade 生成代码,获得流式输出。然后还需要同步等待流式输出完成,将代码生成目录保存到状态中,交给下一个工作节点去处理。

复制代码
@Slf4j
public class CodeGeneratorNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            log.info("执行节点: 代码生成");

            // 使用增强提示词作为发给 AI 的用户消息
            String userMessage = context.getEnhancedPrompt();
            CodeGenTypeEnum generationType = context.getGenerationType();
            // 获取 AI 代码生成外观服务
            AiCodeGeneratorFacade codeGeneratorFacade = SpringContextUtil.getBean(AiCodeGeneratorFacade.class);
            log.info("开始生成代码,类型: {} ({})", generationType.getValue(), generationType.getText());
            // 先使用固定的 appId (后续再整合到业务中)
            Long appId = 0L;
            // 调用流式代码生成
            Flux<String> codeStream = codeGeneratorFacade.generateAndSaveCodeStream(userMessage, generationType, appId);
            // 同步等待流式输出完成
            codeStream.blockLast(Duration.ofMinutes(10)); // 最多等待 10 分钟
            // 根据类型设置生成目录
            String generatedCodeDir = String.format("%s/%s_%s", AppConstant.CODE_OUTPUT_ROOT_DIR, generationType.getValue(), appId);
            log.info("AI 代码生成完成,生成目录: {}", generatedCodeDir);

            // 更新状态
            context.setCurrentStep("代码生成");
            context.setGeneratedCodeDir(generatedCodeDir);
            return WorkflowContext.saveContext(context);
        });
    }
}

项目构建节点

复制代码
@Slf4j
public class ProjectBuilderNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            log.info("执行节点: 项目构建");

            // 获取必要的参数
            String generatedCodeDir = context.getGeneratedCodeDir();
            CodeGenTypeEnum generationType = context.getGenerationType();
            String buildResultDir;
            // Vue 项目类型:使用 VueProjectBuilder 进行构建
            if (generationType == CodeGenTypeEnum.VUE_PROJECT) {
                try {
                    VueProjectBuilder vueBuilder = SpringContextUtil.getBean(VueProjectBuilder.class);
                    // 执行 Vue 项目构建(npm install + npm run build)
                    boolean buildSuccess = vueBuilder.buildProject(generatedCodeDir);
                    if (buildSuccess) {
                        // 构建成功,返回 dist 目录路径
                        buildResultDir = generatedCodeDir + File.separator + "dist";
                        log.info("Vue 项目构建成功,dist 目录: {}", buildResultDir);
                    } else {
                        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "Vue 项目构建失败");
                    }
                } catch (Exception e) {
                    log.error("Vue 项目构建异常: {}", e.getMessage(), e);
                    buildResultDir = generatedCodeDir; // 异常时返回原路径
                }
            } else {
                // HTML 和 MULTI_FILE 代码生成时已经保存了,直接使用生成的代码目录
                buildResultDir = generatedCodeDir;
            }

            // 更新状态
            context.setCurrentStep("项目构建");
            context.setBuildResultDir(buildResultDir);
            log.info("项目构建节点完成,最终目录: {}", buildResultDir);
            return WorkflowContext.saveContext(context);
        });
    }
}

工作流图使用工作节点

复制代码
@Slf4j
public class CodeGenWorkflow {

    /**
     * 创建完整的工作流
     */
    public CompiledGraph<MessagesState<String>> createWorkflow() {
        try {
            return new MessagesStateGraph<String>()
                    // 添加节点 - 使用完整实现的节点
                    .addNode("image_collector", ImageCollectorNode.create())
                    .addNode("prompt_enhancer", PromptEnhancerNode.create())
                    .addNode("router", RouterNode.create())
                    .addNode("code_generator", CodeGeneratorNode.create())
                    .addNode("project_builder", ProjectBuilderNode.create())

                    // 添加边
                    .addEdge(START, "image_collector")
                    .addEdge("image_collector", "prompt_enhancer")
                    .addEdge("prompt_enhancer", "router")
                    .addEdge("router", "code_generator")
                    .addEdge("code_generator", "project_builder")
                    .addEdge("project_builder", END)

                    // 编译工作流
                    .compile();
        } catch (GraphStateException e) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "工作流创建失败");
        }
    }

    /**
     * 执行工作流
     */
    public WorkflowContext executeWorkflow(String originalPrompt) {
        CompiledGraph<MessagesState<String>> workflow = createWorkflow();

        // 初始化 WorkflowContext
        WorkflowContext initialContext = WorkflowContext.builder()
                .originalPrompt(originalPrompt)
                .currentStep("初始化")
                .build();

        GraphRepresentation graph = workflow.getGraph(GraphRepresentation.Type.MERMAID);
        log.info("工作流图:\n{}", graph.content());
        log.info("开始执行代码生成工作流");

        WorkflowContext finalContext = null;
        int stepCounter = 1;
        for (NodeOutput<MessagesState<String>> step : workflow.stream(
                Map.of(WorkflowContext.WORKFLOW_CONTEXT_KEY, initialContext))) {
            log.info("--- 第 {} 步完成 ---", stepCounter);
            // 显示当前状态
            WorkflowContext currentContext = WorkflowContext.getContext(step.state());
            if (currentContext != null) {
                finalContext = currentContext;
                log.info("当前步骤上下文: {}", currentContext);
            }
            stepCounter++;
        }
        log.info("代码生成工作流执行完成!");
        return finalContext;
    }
}

测试结果:

显然,比之前用随机占位图片的效果要好很多~

五、LangGraph4j 工作流特性实战

上面我们已经正式跑通了工作流,但是好像没有体现出工作流的优势,只是利用工作流将逻辑串起来了而已,接下里我们来一起学习一下更多的特性

条件边

对于 HTM⁢⁢⁢L 和 MULTI_F‍‍‍ILE 网站生成类型,网站生成工作节点中已经‏‏‏会自动保存网站文件,不‎‎‎需要进入项目构建节点。除了在项目构建工作节点中写 if-else 外,还可以使用LangGraph4j条件边

在工作流新增路由函数和条件边配置:

复制代码
.addEdge("router", "code_generator")
// 使用条件边:根据代码生成类型决定是否需要构建
.addConditionalEdges("code_generator",
        edge_async(this::routeBuildOrSkip),
        Map.of(
                "build", "project_builder",  // 需要构建的情况
                "skip_build", END             // 跳过构建直接结束
        ))
.addEdge("project_builder", END)



private String routeBuildOrSkip(MessagesState<String> state) {
    WorkflowContext context = WorkflowContext.getContext(state);
    CodeGenTypeEnum generationType = context.getGenerationType();
    // HTML 和 MULTI_FILE 类型不需要构建,直接结束
    if (generationType == CodeGenTypeEnum.HTML || generationType == CodeGenTypeEnum.MULTI_FILE) {
        return "skip_build";
    }
    // VUE_PROJECT 需要构建
    return "build";
}

项目构建工作节点中移除 if-else 逻辑:

复制代码
String buildResultDir;
// 一定是 Vue 项目类型:使用 VueProjectBuilder 进行构建
try {
    VueProjectBuilder vueBuilder = SpringContextUtil.getBean(VueProjectBuilder.class);
    // 执行 Vue 项目构建(npm install + npm run build)
    boolean buildSuccess = vueBuilder.buildProject(generatedCodeDir);
    if (buildSuccess) {
        // 构建成功,返回 dist 目录路径
        buildResultDir = generatedCodeDir + File.separator + "dist";
        log.info("Vue 项目构建成功,dist 目录: {}", buildResultDir);
    } else {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "Vue 项目构建失败");
    }
} catch (Exception e) {
    log.error("Vue 项目构建异常: {}", e.getMessage(), e);
    buildResultDir = generatedCodeDir; // 异常时返回原路径
}

用条件边的优势:

  1. 可视化更清晰:工作流图能直观显示不同路径
  2. 性能更好:直接跳过不需要的节点,避免无用的 Bean 加载
  3. 关注点分离:节点专注业务逻辑,边专注流程控制

循环边

我们可以新增一个代码质检的工作节点,生成代码后进行检查是否达到要求,如果没达到则跳转到代码生成节点重新生成

创建数据模型:

复制代码
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class QualityResult implements Serializable {
    
    @Serial
    private static final long serialVersionUID = 1L;
    
    /**
     * 是否通过质检
     */
    private Boolean isValid;
    
    /**
     * 错误列表
     */
    private List<String> errors;
    
    /**
     * 改进建议
     */
    private List<String> suggestions;
}

WorkflowContext补充字段:

复制代码
/**
 * 质量检查结果
 */
private QualityResult qualityResult;

创建代码质检AI服务(记得让ai生成一份质检提示词):

复制代码
public interface CodeQualityCheckService {

    /**
     * 检查代码质量
     * AI 会分析代码并返回质量检查结果
     */
    @SystemMessage(fromResource = "prompt/code-quality-check-system-prompt.txt")
    QualityResult checkCodeQuality(@UserMessage String codeContent);
}

创建工厂类:

复制代码
@Slf4j
@Configuration
public class CodeQualityCheckServiceFactory {

    @Resource
    private ChatModel chatModel;

    /**
     * 创建代码质量检查 AI 服务
     */
    @Bean
    public CodeQualityCheckService createCodeQualityCheckService() {
        return AiServices.builder(CodeQualityCheckService.class)
                .chatModel(chatModel)
                .build();
    }
}

开发代码检查节点:

复制代码
/**
 * 需要检查的文件扩展名
 */
private static final List<String> CODE_EXTENSIONS = Arrays.asList(
        ".html", ".htm", ".css", ".js", ".json", ".vue", ".ts", ".jsx", ".tsx"
);

/**
 * 读取并拼接代码目录下的所有代码文件
 */
private static String readAndConcatenateCodeFiles(String codeDir) {
    if (StrUtil.isBlank(codeDir)) {
        return "";
    }
    File directory = new File(codeDir);
    if (!directory.exists() || !directory.isDirectory()) {
        log.error("代码目录不存在或不是目录: {}", codeDir);
        return "";
    }
    StringBuilder codeContent = new StringBuilder();
    codeContent.append("# 项目文件结构和代码内容\n\n");
    // 使用 Hutool 的 walkFiles 方法遍历所有文件
    FileUtil.walkFiles(directory, file -> {
        // 过滤条件:跳过隐藏文件、特定目录下的文件、非代码文件
        if (shouldSkipFile(file, directory)) {
            return;
        }
        if (isCodeFile(file)) {
            String relativePath = FileUtil.subPath(directory.getAbsolutePath(), file.getAbsolutePath());
            codeContent.append("## 文件: ").append(relativePath).append("\n\n");
            String fileContent = FileUtil.readUtf8String(file);
            codeContent.append(fileContent).append("\n\n");
        }
    });
    return codeContent.toString();
}

/**
 * 判断是否应该跳过此文件
 */
private static boolean shouldSkipFile(File file, File rootDir) {
    String relativePath = FileUtil.subPath(rootDir.getAbsolutePath(), file.getAbsolutePath());
    // 跳过隐藏文件
    if (file.getName().startsWith(".")) {
        return true;
    }
    // 跳过特定目录下的文件
    return relativePath.contains("node_modules" + File.separator) ||
            relativePath.contains("dist" + File.separator) ||
            relativePath.contains("target" + File.separator) ||
            relativePath.contains(".git" + File.separator);
}

/**
 * 判断是否是需要检查的代码文件
 */
private static boolean isCodeFile(File file) {
    String fileName = file.getName().toLowerCase();
    return CODE_EXTENSIONS.stream().anyMatch(fileName::endsWith);
}

/**
 * 代码质量检查节点
 */
@Slf4j
public class CodeQualityCheckNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            log.info("执行节点: 代码质量检查");
            String generatedCodeDir = context.getGeneratedCodeDir();
            QualityResult qualityResult;
            try {
                // 1. 读取并拼接代码文件内容
                String codeContent = readAndConcatenateCodeFiles(generatedCodeDir);
                if (StrUtil.isBlank(codeContent)) {
                    log.warn("未找到可检查的代码文件");
                    qualityResult = QualityResult.builder()
                            .isValid(false)
                            .errors(List.of("未找到可检查的代码文件"))
                            .suggestions(List.of("请确保代码生成成功"))
                            .build();
                } else {
                    // 2. 调用 AI 进行代码质量检查
                    CodeQualityCheckService qualityCheckService = SpringContextUtil.getBean(CodeQualityCheckService.class);
                    qualityResult = qualityCheckService.checkCodeQuality(codeContent);
                    log.info("代码质量检查完成 - 是否通过: {}", qualityResult.getIsValid());
                }
            } catch (Exception e) {
                log.error("代码质量检查异常: {}", e.getMessage(), e);
                qualityResult = QualityResult.builder()
                        .isValid(true) // 异常直接跳到下一个步骤
                        .build();
            }
            // 3. 更新状态
            context.setCurrentStep("代码质量检查");
            context.setQualityResult(qualityResult);
            return WorkflowContext.saveContext(context);
        });
    }
}

修改代码生成节点:

复制代码
/**
 * 构造用户消息,如果存在质检失败结果则添加错误修复信息
 */
private static String buildUserMessage(WorkflowContext context) {
    String userMessage = context.getEnhancedPrompt();
    // 检查是否存在质检失败结果
    QualityResult qualityResult = context.getQualityResult();
    if (isQualityCheckFailed(qualityResult)) {
        // 直接将错误修复信息作为新的提示词(起到了修改的作用)
        userMessage = buildErrorFixPrompt(qualityResult);
    }
    return userMessage;
}

/**
 * 判断质检是否失败
 */
private static boolean isQualityCheckFailed(QualityResult qualityResult) {
    return qualityResult != null && 
           !qualityResult.getIsValid() && 
           qualityResult.getErrors() != null && 
           !qualityResult.getErrors().isEmpty();
}

/**
 * 构造错误修复提示词
 */
private static String buildErrorFixPrompt(QualityResult qualityResult) {
    StringBuilder errorInfo = new StringBuilder();
    errorInfo.append("\n\n## 上次生成的代码存在以下问题,请修复:\n");
    // 添加错误列表
    qualityResult.getErrors().forEach(error -> 
        errorInfo.append("- ").append(error).append("\n"));
    // 添加修复建议(如果有)
    if (qualityResult.getSuggestions() != null && !qualityResult.getSuggestions().isEmpty()) {
        errorInfo.append("\n## 修复建议:\n");
        qualityResult.getSuggestions().forEach(suggestion -> 
            errorInfo.append("- ").append(suggestion).append("\n"));
    }
    errorInfo.append("\n请根据上述问题和建议重新生成代码,确保修复所有提到的问题。");
    return errorInfo.toString();
}

修改工作流图:

复制代码
.addNode("code_quality_check", CodeQualityCheckNode.create())
// ...
.addEdge("code_generator", "code_quality_check")
// 新增质检条件边:根据质检结果决定下一步
.addConditionalEdges("code_quality_check",
        edge_async(this::routeAfterQualityCheck),
        Map.of(
                "build", "project_builder",   // 质检通过且需要构建
                "skip_build", END,            // 质检通过但跳过构建
                "fail", "code_generator"      // 质检失败,重新生成
        ))



private String routeAfterQualityCheck(MessagesState<String> state) {
    WorkflowContext context = WorkflowContext.getContext(state);
    QualityResult qualityResult = context.getQualityResult();
    // 如果质检失败,重新生成代码
    if (qualityResult == null || !qualityResult.getIsValid()) {
        log.error("代码质检失败,需要重新生成代码");
        return "fail";
    }
    // 质检通过,使用原有的构建路由逻辑
    log.info("代码质检通过,继续后续流程");
    return routeBuildOrSkip(state);
}

这个时候通过调试就可以看到代码质检信息啦

并发

目前通过工⁢⁢⁢具调用获取图片需要‍‍‍和 AI 进行多轮交互,不仅耗费时间‏‏‏长、性能低、还会消‎‎‎耗大量 token。此时我们可以先通过AI获取要收集的图片类型和参数,通过这些信息然后分别并发调用对应的图片收集工具执行,那么就可以在同一时间进行不同类型图片的收集,并且单独凭借提示词减少token的消耗

CompletableFuture 并发实现

图片收集规划服务(记得找ai生成AI收集图片的提示词)

1.创建数据模型,用于保存图片收集计划:

复制代码
package com.sunny.sunnyaicodebackend.langGraph4j.model;

import lombok.Data;

import java.io.Serializable;
import java.util.List;

@Data
public class ImageCollectionPlan implements Serializable {



    /**
     * 内容图片搜索任务列表
     */
    private List<ImageSearchTask> contentImageTasks;

    /**
     * 插画图片搜索任务列表
     */
    private List<IllustrationTask> illustrationTasks;

    /**
     * 架构图生成任务列表
     */
    private List<DiagramTask> diagramTasks;

    /**
     * Logo生成任务列表
     */
    private List<LogoTask> logoTasks;

    /**
     * 内容图片搜索任务
     * 对应 ImageSearchTool.searchContentImages(String query)
     */
    public record ImageSearchTask(String query) implements Serializable {}

    /**
     * 插画图片搜索任务
     * 对应 UndrawIllustrationTool.searchIllustrations(String query)
     */
    public record IllustrationTask(String query) implements Serializable {}

    /**
     * 架构图生成任务
     * 对应 MermaidDiagramTool.generateMermaidDiagram(String mermaidCode, String description)
     */
    public record DiagramTask(String mermaidCode, String description) implements Serializable {}

    /**
     * Logo生成任务
     * 对应 LogoGeneratorTool.generateLogos(String description)
     */
    public record LogoTask(String description) implements Serializable {}
}

创建图片搜集计划AI服务:

复制代码
public interface ImageCollectionPlanService {

    /**
     * 根据用户提示词分析需要收集的图片类型和参数
     */
    @SystemMessage(fromResource = "prompt/image-collection-plan-system-prompt.txt")
    ImageCollectionPlan planImageCollection(@UserMessage String userPrompt);
}

创建工厂类:

复制代码
@Configuration
public class ImageCollectionPlanServiceFactory {

    @Resource
    private ChatModel chatModel;

    @Bean
    public ImageCollectionPlanService createImageCollectionPlanService() {
        return AiServices.builder(ImageCollectionPlanService.class)
                .chatModel(chatModel)
                .build();
    }
}

修改图片收集节点,先利用AI进行规划,然后并发收集图片并汇总:

复制代码
@Slf4j
public class ImageCollectorNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            String originalPrompt = context.getOriginalPrompt();
            List<ImageResource> collectedImages = new ArrayList<>();
            
            try {
                // 第一步:获取图片收集计划
                ImageCollectionPlanService planService = SpringContextUtil.getBean(ImageCollectionPlanService.class);
                ImageCollectionPlan plan = planService.planImageCollection(originalPrompt);
                log.info("获取到图片收集计划,开始并发执行");
                
                // 第二步:并发执行各种图片收集任务
                List<CompletableFuture<List<ImageResource>>> futures = new ArrayList<>();
                // 并发执行内容图片搜索
                if (plan.getContentImageTasks() != null) {
                    ImageSearchTool imageSearchTool = SpringContextUtil.getBean(ImageSearchTool.class);
                    for (ImageCollectionPlan.ImageSearchTask task : plan.getContentImageTasks()) {
                        futures.add(CompletableFuture.supplyAsync(() -> 
                            imageSearchTool.searchContentImages(task.query())));
                    }
                }
                // 并发执行插画图片搜索
                if (plan.getIllustrationTasks() != null) {
                    UndrawIllustrationTool illustrationTool = SpringContextUtil.getBean(UndrawIllustrationTool.class);
                    for (ImageCollectionPlan.IllustrationTask task : plan.getIllustrationTasks()) {
                        futures.add(CompletableFuture.supplyAsync(() -> 
                            illustrationTool.searchIllustrations(task.query())));
                    }
                }
                // 并发执行架构图生成
                if (plan.getDiagramTasks() != null) {
                    MermaidDiagramTool diagramTool = SpringContextUtil.getBean(MermaidDiagramTool.class);
                    for (ImageCollectionPlan.DiagramTask task : plan.getDiagramTasks()) {
                        futures.add(CompletableFuture.supplyAsync(() -> 
                            diagramTool.generateMermaidDiagram(task.mermaidCode(), task.description())));
                    }
                }
                // 并发执行Logo生成
                if (plan.getLogoTasks() != null) {
                    LogoGeneratorTool logoTool = SpringContextUtil.getBean(LogoGeneratorTool.class);
                    for (ImageCollectionPlan.LogoTask task : plan.getLogoTasks()) {
                        futures.add(CompletableFuture.supplyAsync(() -> 
                            logoTool.generateLogos(task.description())));
                    }
                }
                
                // 等待所有任务完成并收集结果
                CompletableFuture<Void> allTasks = CompletableFuture.allOf(
                    futures.toArray(new CompletableFuture[0]));
                allTasks.join();
                // 收集所有结果
                for (CompletableFuture<List<ImageResource>> future : futures) {
                    List<ImageResource> images = future.get();
                    if (images != null) {
                        collectedImages.addAll(images);
                    }
                }
                log.info("并发图片收集完成,共收集到 {} 张图片", collectedImages.size());
            } catch (Exception e) {
                log.error("图片收集失败: {}", e.getMessage(), e);
            }
            // 更新状态
            context.setCurrentStep("图片收集");
            context.setImageList(collectedImages);
            return WorkflowContext.saveContext(context);
        });
    }
}

LangGraph4j 并发实现

这种方式就是利用我们上面说过的Parallel Branch 并发特性,将每个图片收集工具都定义成一个工作节点,然后这些工作节点并发执行

WorkflowContext新增字段:

复制代码
/**
 * 图片收集计划
 */
private ImageCollectionPlan imageCollectionPlan;


/**
 * 并发图片收集的中间结果字段
 */
private List<ImageResource> contentImages;
private List<ImageResource> illustrations;
private List<ImageResource> diagrams;
private List<ImageResource> logos;

将之前的工具分别定义成节点:

复制代码
@Slf4j
public class ImagePlanNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            String originalPrompt = context.getOriginalPrompt();
            try {
                // 获取图片收集计划服务
                ImageCollectionPlanService planService = SpringContextUtil.getBean(ImageCollectionPlanService.class);
                ImageCollectionPlan plan = planService.planImageCollection(originalPrompt);
                log.info("生成图片收集计划,准备启动并发分支");
                // 将计划存储到上下文中
                context.setImageCollectionPlan(plan);
                context.setCurrentStep("图片计划");
            } catch (Exception e) {
                log.error("图片计划生成失败: {}", e.getMessage(), e);
            }
            return WorkflowContext.saveContext(context);
        });
    }
}

@Slf4j
public class ContentImageCollectorNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            List<ImageResource> contentImages = new ArrayList<>();
            try {
                ImageCollectionPlan plan = context.getImageCollectionPlan();
                if (plan != null && plan.getContentImageTasks() != null) {
                    ImageSearchTool imageSearchTool = SpringContextUtil.getBean(ImageSearchTool.class);
                    log.info("开始并发收集内容图片,任务数: {}", plan.getContentImageTasks().size());
                    for (ImageCollectionPlan.ImageSearchTask task : plan.getContentImageTasks()) {
                        List<ImageResource> images = imageSearchTool.searchContentImages(task.query());
                        if (images != null) {
                            contentImages.addAll(images);
                        }
                    }
                    log.info("内容图片收集完成,共收集到 {} 张图片", contentImages.size());
                }
            } catch (Exception e) {
                log.error("内容图片收集失败: {}", e.getMessage(), e);
            }
            // 将收集到的图片存储到上下文的中间字段中
            context.setContentImages(contentImages);
            context.setCurrentStep("内容图片收集");
            return WorkflowContext.saveContext(context);
        });
    }
}

/**
 * 插画图片收集节点
 */
@Slf4j
public class IllustrationCollectorNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            List<ImageResource> illustrations = new ArrayList<>();
            try {
                ImageCollectionPlan plan = context.getImageCollectionPlan();
                if (plan != null && plan.getIllustrationTasks() != null) {
                    PixabayIllustrationTool illustrationTool = SpringContextUtil.getBean(PixabayIllustrationTool.class);
                    log.info("开始并发收集插画图片,任务数: {}", plan.getIllustrationTasks().size());
                    for (ImageCollectionPlan.IllustrationTask task : plan.getIllustrationTasks()) {
                        List<ImageResource> images = illustrationTool.searchIllustrations(task.query());
                        if (images != null) {
                            illustrations.addAll(images);
                        }
                    }
                    log.info("插画图片收集完成,共收集到 {} 张图片", illustrations.size());
                }
            } catch (Exception e) {
                log.error("插画图片收集失败: {}", e.getMessage(), e);
            }
            context.setIllustrations(illustrations);
            context.setCurrentStep("插画图片收集");
            return WorkflowContext.saveContext(context);
        });
    }
}

@Slf4j
public class DiagramCollectorNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            List<ImageResource> diagrams = new ArrayList<>();
            try {
                ImageCollectionPlan plan = context.getImageCollectionPlan();
                if (plan != null && plan.getDiagramTasks() != null) {
                    MermaidDiagramTool diagramTool = SpringContextUtil.getBean(MermaidDiagramTool.class);
                    log.info("开始并发生成架构图,任务数: {}", plan.getDiagramTasks().size());
                    for (ImageCollectionPlan.DiagramTask task : plan.getDiagramTasks()) {
                        List<ImageResource> images = diagramTool.generateMermaidDiagram(
                                task.mermaidCode(), task.description());
                        if (images != null) {
                            diagrams.addAll(images);
                        }
                    }
                    log.info("架构图生成完成,共生成 {} 张图片", diagrams.size());
                }
            } catch (Exception e) {
                log.error("架构图生成失败: {}", e.getMessage(), e);
            }
            context.setDiagrams(diagrams);
            context.setCurrentStep("架构图生成");
            return WorkflowContext.saveContext(context);
        });
    }
}

@Slf4j
public class LogoCollectorNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            List<ImageResource> logos = new ArrayList<>();
            try {
                ImageCollectionPlan plan = context.getImageCollectionPlan();
                if (plan != null && plan.getLogoTasks() != null) {
                    LogoGeneratorTool logoTool = SpringContextUtil.getBean(LogoGeneratorTool.class);
                    log.info("开始并发生成Logo,任务数: {}", plan.getLogoTasks().size());
                    for (ImageCollectionPlan.LogoTask task : plan.getLogoTasks()) {
                        List<ImageResource> images = logoTool.generateLogos(task.description());
                        if (images != null) {
                            logos.addAll(images);
                        }
                    }
                    log.info("Logo生成完成,共生成 {} 张图片", logos.size());
                }
            } catch (Exception e) {
                log.error("Logo生成失败: {}", e.getMessage(), e);
            }
            context.setLogos(logos);
            context.setCurrentStep("Logo生成");
            return WorkflowContext.saveContext(context);
        });
    }
}

这里需要一个图片聚合节点,汇聚所有并发分支收集到的图片:

复制代码
@Slf4j
public class ImageAggregatorNode {

    public static AsyncNodeAction<MessagesState<String>> create() {
        return node_async(state -> {
            WorkflowContext context = WorkflowContext.getContext(state);
            List<ImageResource> allImages = new ArrayList<>();
            log.info("开始聚合并发收集的图片");
            // 从各个中间字段聚合图片
            if (context.getContentImages() != null) {
                allImages.addAll(context.getContentImages());
            }
            if (context.getIllustrations() != null) {
                allImages.addAll(context.getIllustrations());
            }
            if (context.getDiagrams() != null) {
                allImages.addAll(context.getDiagrams());
            }
            if (context.getLogos() != null) {
                allImages.addAll(context.getLogos());
            }
            log.info("图片聚合完成,总共 {} 张图片", allImages.size());
            // 更新最终的图片列表
            context.setImageList(allImages);
            context.setCurrentStep("图片聚合");
            return WorkflowContext.saveContext(context);
        });
    }
}

重新编写工作流:

复制代码
package com.sunny.sunnyaicodebackend.langGraph4j;

import cn.hutool.core.thread.ExecutorBuilder;
import cn.hutool.core.thread.ThreadFactoryBuilder;
import com.sunny.sunnyaicodebackend.exception.BusinessException;
import com.sunny.sunnyaicodebackend.exception.ErrorCode;
import com.sunny.sunnyaicodebackend.langGraph4j.model.QualityResult;
import com.sunny.sunnyaicodebackend.langGraph4j.model.WorkflowContext;
import com.sunny.sunnyaicodebackend.langGraph4j.node.*;
import com.sunny.sunnyaicodebackend.langGraph4j.node.concurrent.*;
import com.sunny.sunnyaicodebackend.model.enums.CodeGenTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.bsc.langgraph4j.*;
import org.bsc.langgraph4j.prebuilt.MessagesState;
import org.bsc.langgraph4j.prebuilt.MessagesStateGraph;

import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;

import static org.bsc.langgraph4j.StateGraph.END;
import static org.bsc.langgraph4j.StateGraph.START;
import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async;

@Slf4j
public class CodeGenConcurrentWorkflow {

    /**
     * 创建并发工作流
     */
    public CompiledGraph<MessagesState<String>> createWorkflow() {
        try {
            return new MessagesStateGraph<String>()
                    // 添加节点
                    .addNode("image_plan", ImagePlanNode.create())
                    .addNode("prompt_enhancer", PromptEnhancerNode.create())
                    .addNode("router", RouterNode.create())
                    .addNode("code_generator", CodeGeneratorNode.create())
                    .addNode("code_quality_check", CodeQualityCheckNode.create())
                    .addNode("project_builder", ProjectBuilderNode.create())

                    // 添加并发图片收集节点
                    .addNode("content_image_collector", ContentImageCollectorNode.create())
                    .addNode("illustration_collector", IllustrationCollectorNode.create())
                    .addNode("diagram_collector", DiagramCollectorNode.create())
                    .addNode("logo_collector", LogoCollectorNode.create())
                    .addNode("image_aggregator", ImageAggregatorNode.create())

                    // 添加边
                    .addEdge(START, "image_plan")

                    // 并发分支:从计划节点分发到各个收集节点
                    .addEdge("image_plan", "content_image_collector")
                    .addEdge("image_plan", "illustration_collector")
                    .addEdge("image_plan", "diagram_collector")
                    .addEdge("image_plan", "logo_collector")

                    // 汇聚:所有收集节点都汇聚到聚合器
                    .addEdge("content_image_collector", "image_aggregator")
                    .addEdge("illustration_collector", "image_aggregator")
                    .addEdge("diagram_collector", "image_aggregator")
                    .addEdge("logo_collector", "image_aggregator")

                    // 继续串行流程
                    .addEdge("image_aggregator", "prompt_enhancer")
                    .addEdge("prompt_enhancer", "router")
                    .addEdge("router", "code_generator")
                    .addEdge("code_generator", "code_quality_check")

                    // 质检条件边
                    .addConditionalEdges("code_quality_check",
                            edge_async(this::routeAfterQualityCheck),
                            Map.of(
                                    "build", "project_builder",
                                    "skip_build", END,
                                    "fail", "code_generator"
                            ))
                    .addEdge("project_builder", END)
                    .compile();
        } catch (GraphStateException e) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "并发工作流创建失败");
        }
    }

    /**
     * 执行并发工作流
     */
    public WorkflowContext executeWorkflow(String originalPrompt) {
        CompiledGraph<MessagesState<String>> workflow = createWorkflow();
        WorkflowContext initialContext = WorkflowContext.builder()
                .originalPrompt(originalPrompt)
                .currentStep("初始化")
                .build();
        GraphRepresentation graph = workflow.getGraph(GraphRepresentation.Type.MERMAID);
        log.info("并发工作流图:\n{}", graph.content());
        log.info("开始执行并发代码生成工作流");
        WorkflowContext finalContext = null;
        int stepCounter = 1;
        // 配置并发执行
        ExecutorService pool = ExecutorBuilder.create()
                .setCorePoolSize(10)
                .setMaxPoolSize(20)
                .setWorkQueue(new LinkedBlockingQueue<>(100))
                .setThreadFactory(ThreadFactoryBuilder.create().setNamePrefix("Parallel-Image-Collect").build())
                .build();
        RunnableConfig runnableConfig = RunnableConfig.builder()
                .addParallelNodeExecutor("image_plan", pool)
                .build();
        for (NodeOutput<MessagesState<String>> step : workflow.stream(
                Map.of(WorkflowContext.WORKFLOW_CONTEXT_KEY, initialContext),
                runnableConfig)) {
            log.info("--- 第 {} 步完成 ---", stepCounter);
            WorkflowContext currentContext = WorkflowContext.getContext(step.state());
            if (currentContext != null) {
                finalContext = currentContext;
                log.info("当前步骤上下文: {}", currentContext);
            }
            stepCounter++;
        }
        log.info("并发代码生成工作流执行完成!");
        return finalContext;
    }

    /**
     * 路由函数:根据质检结果决定下一步
     */
    private String routeAfterQualityCheck(MessagesState<String> state) {
        WorkflowContext context = WorkflowContext.getContext(state);
        QualityResult qualityResult = context.getQualityResult();

        if (qualityResult == null || !qualityResult.getIsValid()) {
            log.error("代码质检失败,需要重新生成代码");
            return "fail";
        }
        log.info("代码质检通过,继续后续流程");
        CodeGenTypeEnum generationType = context.getGenerationType();
        if (generationType == CodeGenTypeEnum.VUE_PROJECT) {
            return "build";
        } else {
            return "skip_build";
        }
    }
}

再次执行单元测试,这次并发生效:

SSE流式输出

Cod⁢⁢⁢eGenWorkfl‍‍‍ow 工作流新增 SSE 输出版本的执行‏‏‏工作流方法,自己构造‎‎‎ Flux 响应流:

复制代码
/**
 * 执行工作流(Flux 流式输出版本)
 */
public Flux<String> executeWorkflowWithFlux(String originalPrompt) {
    return Flux.create(sink -> {
        Thread.startVirtualThread(() -> {
            try {
                CompiledGraph<MessagesState<String>> workflow = createWorkflow();
                WorkflowContext initialContext = WorkflowContext.builder()
                        .originalPrompt(originalPrompt)
                        .currentStep("初始化")
                        .build();
                sink.next(formatSseEvent("workflow_start", Map.of(
                        "message", "开始执行代码生成工作流",
                        "originalPrompt", originalPrompt
                )));
                GraphRepresentation graph = workflow.getGraph(GraphRepresentation.Type.MERMAID);
                log.info("工作流图:\n{}", graph.content());

                int stepCounter = 1;
                for (NodeOutput<MessagesState<String>> step : workflow.stream(
                        Map.of(WorkflowContext.WORKFLOW_CONTEXT_KEY, initialContext))) {
                    log.info("--- 第 {} 步完成 ---", stepCounter);
                    WorkflowContext currentContext = WorkflowContext.getContext(step.state());
                    if (currentContext != null) {
                        sink.next(formatSseEvent("step_completed", Map.of(
                                "stepNumber", stepCounter,
                                "currentStep", currentContext.getCurrentStep()
                        )));
                        log.info("当前步骤上下文: {}", currentContext);
                    }
                    stepCounter++;
                }
                sink.next(formatSseEvent("workflow_completed", Map.of(
                        "message", "代码生成工作流执行完成!"
                )));
                log.info("代码生成工作流执行完成!");
                sink.complete();
            } catch (Exception e) {
                log.error("工作流执行失败: {}", e.getMessage(), e);
                sink.next(formatSseEvent("workflow_error", Map.of(
                        "error", e.getMessage(),
                        "message", "工作流执行失败"
                )));
                sink.error(e);
            }
        });
    });
}

/**
 * 格式化 SSE 事件的辅助方法
 */
private String formatSseEvent(String eventType, Object data) {
    try {
        String jsonData = JSONUtil.toJsonStr(data);
        return "event: " + eventType + "\ndata: " + jsonData + "\n\n";
    } catch (Exception e) {
        log.error("格式化 SSE 事件失败: {}", e.getMessage(), e);
        return "event: error\ndata: {\"error\":\"格式化失败\"}\n\n";
    }
}

通过上面的Flux流就可以实现流式输出的,在需要推送给前端信息的地方加上sink.next()即可,以上内容就是LangGraph4j工作流的特性和实战使用了感谢大家的观看!

相关推荐
@蔓蔓喜欢你8 小时前
Web Components:构建可复用组件的未来
人工智能·ai
庚昀◟8 小时前
ClaudeCode安装教程,基础使用、进阶推荐
人工智能·python·ai
programhelp_8 小时前
Google 2026 New Grad SDE VO 三轮面试详解 | 含Behavioral、Coding、Design
java·服务器·数据库
@PHARAOH8 小时前
HOW - 构建一个轻量前后端一体服务
前端·微服务·服务端
Mr_sst8 小时前
Codex 部署、使用教程 & Vibe Coding 实战指南
java·ai·语言模型·chatgpt·ai编程
czhc11400756638 小时前
数据库520 HALCONAN安装
数据库
无限进步_8 小时前
【C++】C++11的类功能增强与STL变化
java·前端·数据结构·c++·后端·算法
一只小小Java8 小时前
Echarts单表多图实现
前端·javascript·echarts
阿坤带你走近大数据8 小时前
Oracle中的OGG介绍
数据库·oracle