欢迎光临
我们一直在努力

构建高效AI工作流:Java生态的LangGraph4j框架详解

AI工作流的概念及其在Java生态中的实现框架LangGraph4j

什么是工作流?

AI工作流是将多个AI任务、数据处理步骤和业务逻辑按照特定顺序组织起来的自动化流程。有了工作流就不需要让AI来判断是否该执行某个规定的任务。

在Java生态中,AI工作流类似于传统的工作流引擎,但专门为AI任务优化。是将AI能力工程化、工业化的重要手段。通过工作流编排,可以实现:

  • 复杂AI任务的自动化执行
  • 多模型协同工作
  • 与现有业务系统的无缝集成
  • 完整的监控和治理
  • 为什么需要工作流?

    在AI应用开发中,我们经常需要串联多个步骤,例如:获取用户输入、调用多个工具、条件判断、循环重试等。传统的方式是将这些步骤写在业务代码中,导致代码冗长、难以维护和扩展。工作流引擎通过将流程可视化、模块化,使得复杂的业务流程变得清晰、易于编排和调试。

    传统模式:业务流程分散在各个Service方法中,逻辑耦合

    工作流模式:将业务流程抽象为有向图,节点代表操作单元,边控制流程走向

    前言

    目前比较流行的几种AI工作流平台,像Dify、Coze、阿里云百炼,它们只要通过拖拉拽的方式就能快速搭建工作流,上手简单、能够快速看到效果。这些不需要写什么代码的工作流平台比较适合于从头开始的新项目,各个节点之间相对没那么复杂的场景。而当涉及到一些复杂的业务时,这些平台就无法满足我们的需求,尤其是需要与现有逻辑业务深度集成时。

    因此需要用到一款专门为java生态设计的工作流框架LangGraph4j,它不仅与SpringBoot深度集成,而且也与开发AI应用的框架LangChain4j兼容性很好。能够无缝融入现有的业务逻辑技术,可以直接复用项目已经开发的一些AI工具、逻辑、配置呀等,不需要因为引入工作流而从头开始重新开发。LangGraph4j还提供了各种特性,如循环执行、并发处理等让我们能够构建出智能化工作流程。LangGraph4j实现工作流需要通过编写java代码来实现,相比于像Dify这些工作流平台,虽然会有一定的学习成本,但是它的扩展性和可维护性好,哪怕业务逻辑变的越来越复杂,要新加入很多复杂业务,也能够很好的进行迭代。因此LangGraph4j适合于满足复杂的业务需求,也能很好支持已有的项目进行扩展。

    学习LangGraph4j步骤建议:

  • 先掌握核心概念(图、节点、状态、边)
  • 跑通简单Demo,理解工作流执行过程
  • 实现一个完整节点,理解状态流转
  • 学习高级特性(并发、条件边、流式输出)
  • 根据业务需求选择合适特性
  • 本文下面会介绍LangGraph4j进行入门,学习并掌握实践LangGraph4j的高级特性。

    LangGraph4j入门

    1. 图(Graph)

    LangGraph4j 使用有向图来定义工作流,图由节点(Nodes)和边(Edges)组成。与传统的DAG(有向无环图)不同,LangGraph4j 的图支持循环,这意味着可以很容易地实现重试、循环等逻辑。

    2. 节点(Nodes)

    节点是工作流中的执行单元,每个节点可以执行一段业务逻辑。节点可以是同步的,也可以是异步的。节点通过实现 NodeAction 或 AsyncNodeAction 接口来定义。

    节点设计的三种开发模式

    模式一:同步简单节点

    // 适合快速操作,无IO阻塞
    static NodeAction<MessagesState<String>> createSimpleNode() {
    return node(state -> {
    WorkflowContext ctx = WorkflowContext.getContext(state);
    ctx.setCurrentStep("simple_node");
    return WorkflowContext.saveContext(ctx);
    });
    }

    模式二:异步复杂节点(推荐)

    // 适合网络请求、AI调用等IO操作
    public static AsyncNodeAction<MessagesState<String>> create() {
    return node_async(state -> {
    WorkflowContext context = WorkflowContext.getContext(state);

    // 关键:异常处理策略
    try {
    // 业务逻辑
    ImageCollectionService service = SpringContextUtil.getBean(ImageCollectionService.class);
    String imageListStr = service.collectImages(context.getOriginalPrompt());

    // 更新状态
    context.setImageListStr(imageListStr);
    context.setCurrentStep("图片收集");

    } catch (Exception e) {
    // 错误处理:记录但不中断流程
    log.error("图片收集失败,但继续执行: {}", e.getMessage());
    context.setErrorMessage("图片收集失败: " + e.getMessage());
    }

    return WorkflowContext.saveContext(context);
    });
    }

    模式三:带参数配置的节点

    // 工厂方法模式,支持配置
    public class ImageCollectorNode {
    private final int maxImages;
    private final boolean enableConcurrent;

    private ImageCollectorNode(int maxImages, boolean enableConcurrent) {
    this.maxImages = maxImages;
    this.enableConcurrent = enableConcurrent;
    }

    public static AsyncNodeAction<MessagesState<String>> create(
    int maxImages,
    boolean enableConcurrent) {
    ImageCollectorNode instance = new ImageCollectorNode(maxImages, enableConcurrent);
    return instance.createNode();
    }

    private AsyncNodeAction<MessagesState<String>> createNode() {
    return node_async(state -> {
    WorkflowContext context = WorkflowContext.getContext(state);
    if (enableConcurrent) {
    // 并发收集逻辑
    } else {
    // 串行收集逻辑
    }
    return WorkflowContext.saveContext(context);
    });
    }
    }

    节点开发的核心原则

  • 单一职责:一个节点只做一件事
  • 幂等性:相同输入应产生相同输出(便于重试)
  • 状态透明:节点只修改自己负责的状态字段
  • 异常可恢复:错误应该记录在状态中,而不是抛出异常中断流程
  • 3. 状态(State)

    状态是工作流中节点之间共享的数据。LangGraph4j 提供了两种状态管理方式:

    • 内置的AgentState:本质上是一个Map,通过Schema定义结构,使用Reducer来合并更新。
    • 自定义上下文类:更推荐的方式,自己定义一个类来管理状态,然后将这个类的实例作为状态的一部分。

    自定义状态类示例:

    @Data
    public class WorkflowContext {
    private String currentStep;
    private String originalPrompt;
    private List<ImageResource> imageList;
    // … 其他字段

    // 提供从状态中获取和保存的方法
    public static WorkflowContext getContext(MessagesState<String> state) {
    return (WorkflowContext) state.data().get("workflowContext");
    }

    public static Map<String, Object> saveContext(WorkflowContext context) {
    return Map.of("workflowContext", context);
    }
    }

     4. 边(Edges)

    边定义了节点之间的流转逻辑。分为两种:

    • 普通边:无条件跳转到下一个节点。
    • 条件边:根据当前状态动态决定下一个节点。

    普通边示例:

    graph.addEdge("nodeA", "nodeB"); // A完成后一定执行B

    条件边示例:

    // 核心:条件边函数
    private String routeAfterQualityCheck(MessagesState<String> state) {
    WorkflowContext context = WorkflowContext.getContext(state);
    QualityResult qualityResult = context.getQualityResult();

    // 决策逻辑
    if (qualityResult == null) {
    return "error"; // 质检未执行
    } else if (!qualityResult.getIsValid()) {
    // 关键:实现循环重试机制
    int retryCount = context.getRetryCount() != null ? context.getRetryCount() : 0;
    if (retryCount < 3) {
    context.setRetryCount(retryCount + 1);
    return "retry"; // 重新生成
    } else {
    return "fail"; // 超过重试次数
    }
    } else {
    // 质检通过,根据类型决定下一步
    CodeGenTypeEnum type = context.getGenerationType();
    return type == CodeGenTypeEnum.VUE_PROJECT ? "build" : "skip_build";
    }
    }

    // 在图中使用条件边
    graph.addConditionalEdges(
    "code_quality_check", // 源节点
    edge_async(this::routeAfterQualityCheck), // 路由函数
    Map.of( // 目标节点映射
    "retry", "code_generator", // 重试:回到代码生成
    "build", "project_builder", // 构建:进入构建节点
    "skip_build", END, // 跳过构建:直接结束
    "error", "error_handler", // 错误:进入错误处理
    "fail", "fail_handler" // 失败:进入失败处理
    )
    );

    边的四种设计:

    1、线性流程(最简单)

    start → A → B → C → end

    2、条件分支(if-else)

    3、循环重试(while)

    4、并行聚合(fork-join)

    工作流开发步骤

    1、定义工作流结构

    先梳理业务流程,确定需要哪些节点,节点之间的顺序和条件分支。可以画一个流程图。

    2、定义状态

    根据业务流程,确定需要在节点之间共享哪些数据,设计状态类(WorkflowContext)。

    3、开发节点

    按照节点开发模式,逐个实现节点。可以先使用假数据模拟,确保流程跑通。

    4、整合服务

    在节点中注入需要的Spring Bean,实现真实的业务逻辑。

    5、测试和调试

    通过单元测试和工作流可视化,验证工作流的正确性。

    LangGraph4j高级特性

    Streaming 异步和流式处理。首先,通过 CompletableFuture,LangGraph4j允许非阻塞的异步操作。也就是说,当某个节点在等待LLM响应时,整个应用不会被阻塞,可以继续处理其他任务。LangGraph4j支持通过 Java 异步生成器来处理来自LLM和其他源的流式响应,可以实现SSE流式输出,提升用户体验。此外,流式处理不仅支持主流的输出,还能处理子流,并且在所有流输出完成后获取到最终结果。此外LangGraph4j还提供了并发、子图、断点、可视化等高级特性。

     子图

    如果一个工作流图特别复杂,或者其中的一些流程需要复用,就可以考虑子图功能。相当于把一个工作流拆分成

    多个子模块,而且多个子图可以同时并发执行。子图功能不仅支持可视化,还支持流式处理。不过在操作节点方式下,需要自己处理流的传递。

    有3种不同的使用方式,适合不同的场景:

    1、直接添加:这种方式会将子图完全合并到父图中,无缝集成。在这种模式下,父图和子图共享所有状态,子图就像是父图的一个扩展部分。

    2、编译添加:这种方式让子图和父图保持一定的独立性,只共享部分状态。适合需要一定独立性但又需要与主流程交互的场景。

    3、操作节点:可以自己定义子图和父图之间的交互逻辑,包括状态的转换、数据的传递等,最灵活。适合需要复杂交互逻辑的场景。

     断点

    断点就是有时我们需要在某个关键节点暂停执行,等待人工判断确认。这个特性在实现“人在环路”(human-in-the-loop)的 AI系统时特别有用。比如在代码生成系统中,可能希望在生成代码后暂停执行,让人工审核代码质量,确认无误后再继续后续的构建和部署流程。相信大家都用过AI写代码,如Cursor的有些操作就会停下来,让用户来决定是否继续执行,这就是断点的使用,LangGraph4j的断点功能正是为这种场景设计的。

    注意:使用断点功能时必须配合检查点器使用。因为图需要能够保存当前状态并在稍后恢复执行,要恢复执行只需要调用 GraphInput.resume()就可以了。

    并发

    为什么需要并发?

    如果图片收集的4个工具(内容图片、插画、架构图、Logo)相互独立,要是使用串行执行,会非常浪费时间,而用并发就能大大提高执行效率,减少执行耗时。

    1、并发处理的三种方案,推荐CompletableFuture(简单场景)或 LangGraph4j并发(工作流原生):

    节点内部并发示例代码:

    public static AsyncNodeAction<MessagesState<String>> create() {
    return node_async(state -> {
    WorkflowContext context = WorkflowContext.getContext(state);

    // 1. 获取收集计划
    ImageCollectionPlan plan = getPlan(context.getOriginalPrompt());

    // 2. 创建并发任务
    List<CompletableFuture<List<ImageResource>>> futures = new ArrayList<>();

    // 内容图片
    futures.add(CompletableFuture.supplyAsync(() ->
    imageSearchTool.searchContentImages(plan.getContentQuery())
    ));

    // 插画图片
    futures.add(CompletableFuture.supplyAsync(() ->
    illustrationTool.searchIllustrations(plan.getIllustrationQuery())
    ));

    // 3. 等待所有完成
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

    // 4. 聚合结果
    List<ImageResource> allImages = futures.stream()
    .map(CompletableFuture::join)
    .flatMap(List::stream)
    .collect(Collectors.toList());

    context.setImageList(allImages);
    return WorkflowContext.saveContext(context);
    });
    }

    LangGraph4j原生并发示例代码:

    // 关键:配置线程池
    ExecutorService pool = Executors.newFixedThreadPool(
    4, // 并发数 = 图片类型数
    new ThreadFactoryBuilder()
    .setNamePrefix("image-collect-")
    .setDaemon(true)
    .build()
    );

    // 关键:运行时配置
    RunnableConfig config = RunnableConfig.builder()
    .addParallelNodeExecutor("image_plan", pool) // 从plan节点开始并发
    .build();

    // 图结构:多个节点从同一节点出发
    graph.addEdge("image_plan", "content_collector");
    graph.addEdge("image_plan", "illustration_collector");
    graph.addEdge("image_plan", "diagram_collector");
    graph.addEdge("image_plan", "logo_collector");

    // 所有收集节点汇聚到聚合器
    graph.addEdge("content_collector", "image_aggregator");
    graph.addEdge("illustration_collector", "image_aggregator");
    // …

    并发配置的重点:

    // 必须配置线程池才能生效
    ExecutorService pool = ExecutorBuilder.create()
    .setCorePoolSize(10)
    .setMaxPoolSize(20)
    .build();

    RunnableConfig runnableConfig = RunnableConfig.builder()
    .addParallelNodeExecutor("image_plan", pool)
    .build();

    并发设计的核心问题:

    1、资源竞争:多个线程访问共享状态 → 使用线程安全集合

    2、错误处理:一个任务失败不影响其他 → 独立try-catch

    3、超时控制:防止某个任务卡死 → 设置超时时间

    4、结果顺序:并发结果顺序不确定 → 按类型分类聚合

    流式处理

    为什么需要流式输出?因为工作流步骤多,如果等AI输出完才一次性返回结果,那用户的体验感是不是很差,不知道的还以为卡住了,因此用户需要实时反馈,这时就可以使用流式输出。关键就是将工作流执行步骤作为事件流输出。

    流式输出的实现方案

    • Flux方案(推荐):兼容性好,易整合

    // 工作流执行方法(流式版)
    public Flux<WorkflowEvent> executeWorkflowStream(String prompt) {
    return Flux.create(sink -> {
    Thread.startVirtualThread(() -> {
    try {
    // 1. 开始事件
    sink.next(WorkflowEvent.start(prompt));

    // 2. 执行工作流,监听每一步
    CompiledGraph<MessagesState<String>> workflow = createWorkflow();

    for (NodeOutput<MessagesState<String>> step : workflow.stream(…)) {
    WorkflowContext context = WorkflowContext.getContext(step.state());

    // 根据不同节点类型发送不同事件
    String nodeName = step.nodeName();
    switch (nodeName) {
    case "image_collector":
    sink.next(WorkflowEvent.imageCollect(
    context.getImageList().size()
    ));
    break;
    case "code_generator":
    // 这里可以更进一步:流式输出代码生成过程
    sink.next(WorkflowEvent.codeGenerateStart());
    // 需要修改代码生成节点,使其能推送进度
    break;
    // … 其他节点
    }
    }

    // 3. 完成事件
    sink.next(WorkflowEvent.complete(context));
    sink.complete();

    } catch (Exception e) {
    sink.next(WorkflowEvent.error(e));
    sink.complete();
    }
    });
    });
    }

    然后前端实时进行展示输出效果,示例代码如下:

    // 前端EventSource监听
    const eventSource = new EventSource('/api/workflow/stream?prompt=…');

    eventSource.addEventListener('step_start', (e) => {
    const data = JSON.parse(e.data);
    updateProgress(data.stepName, data.progress);
    });

    eventSource.addEventListener('image_collected', (e) => {
    const data = JSON.parse(e.data);
    showImages(data.images); // 实时显示收集到的图片
    });

    eventSource.addEventListener('code_chunk', (e) => {
    const data = JSON.parse(e.data);
    appendCode(data.chunk); // 实时显示生成的代码
    });

    • SseEmitter方案:标准SSE实现(可以参考看我的大模型应用开发需要掌握的技术-AI智能体(Agent)构建文章https://blog.csdn.net/TheChosenOnev/article/details/155917880?fromshare=blogdetail&sharetype=blogdetail&sharerId=155917880&sharerefer=PC&sharesource=TheChosenOnev&sharefrom=from_link)

    扩展

    一、如何让AI学会从错误中改进?

    1、传统重试:使用循环来完成简单的重复调用,示例代码如下:

    for (int i = 0; i < 3; i++) {
    String code = ai.generateCode(prompt);
    if (checkQuality(code)) break;
    }

    这会有个问题,AI不知道错在哪里,只是盲目重试。

    2、智能重试:提供反馈指导,示例代码如下:

    // 第一次生成
    String codeV1 = ai.generateCode(prompt);
    QualityResult check1 = qualityChecker.check(codeV1);

    if (!check1.isValid()) {
    // 构建改进提示词
    String improvedPrompt = buildFixPrompt(prompt, check1.getErrors(), check1.getSuggestions());

    // 第二次生成:带着问题和建议
    String codeV2 = ai.generateCode(improvedPrompt);
    QualityResult check2 = qualityChecker.check(codeV2);

    if (!check2.isValid()) {
    // 还可以继续,但通常2次就够了
    }
    }

    3、工作流实现:质检→反馈→重试循环,示例代码如下:

    // 质检节点
    public static AsyncNodeAction<MessagesState<String>> createQualityCheckNode() {
    return node_async(state -> {
    WorkflowContext context = WorkflowContext.getContext(state);

    // 读取生成的代码
    String codeContent = readCodeFiles(context.getGeneratedCodeDir());

    // AI质检
    QualityResult result = qualityCheckService.checkCodeQuality(codeContent);

    // 关键:记录质检历史
    List<QualityResult> history = context.getQualityHistory();
    if (history == null) history = new ArrayList<>();
    history.add(result);
    context.setQualityHistory(history);

    // 当前质检结果
    context.setQualityResult(result);

    return WorkflowContext.saveContext(context);
    });
    }

    // 代码生成节点(支持重试)
    public static AsyncNodeAction<MessagesState<String>> createCodeGenNode() {
    return node_async(state -> {
    WorkflowContext context = WorkflowContext.getContext(state);

    // 关键:判断是否为重试
    List<QualityResult> history = context.getQualityHistory();
    String userMessage = context.getEnhancedPrompt();

    if (history != null && !history.isEmpty()) {
    // 最后一次质检结果
    QualityResult lastResult = history.get(history.size() – 1);
    if (!lastResult.getIsValid()) {
    // 构建修复提示词
    userMessage = buildFixPrompt(
    userMessage,
    lastResult.getErrors(),
    lastResult.getSuggestions()
    );
    log.info("第{}次重试生成代码", history.size());
    }
    }

    // 调用AI生成代码
    Flux<String> codeStream = codeGeneratorFacade.generateAndSaveCodeStream(
    userMessage,
    context.getGenerationType(),
    context.getAppId()
    );

    // 等待完成
    codeStream.blockLast(Duration.ofMinutes(10));

    return WorkflowContext.saveContext(context);
    });
    }

    二、循环失控(条件边导致无限循环)

    解决示例代码如下:

    private String routeWithLimit(MessagesState<String> state) {
    WorkflowContext context = WorkflowContext.getContext(state);

    // 添加循环计数器
    Integer loopCount = context.getLoopCount();
    if (loopCount == null) loopCount = 0;

    if (loopCount > MAX_LOOPS) {
    log.warn("超过最大循环次数: {}", MAX_LOOPS);
    return "fail";
    }

    context.setLoopCount(loopCount + 1);

    // 正常路由逻辑
    if (needRetry(context)) {
    return "retry";
    } else {
    return "next";
    }
    }

    三、状态污染(节点A修改了节点B需要的字段,导致B出错)

    解决示例代码如下:

    // 使用不可变状态副本
    public static AsyncNodeAction<MessagesState<String>> createSafeNode() {
    return node_async(state -> {
    WorkflowContext context = WorkflowContext.getContext(state);

    // 创建副本,只修改需要的字段
    WorkflowContext update = WorkflowContext.builder()
    .originalPrompt(context.getOriginalPrompt()) // 保留原字段
    .imageList(collectImages(context)) // 只修改本节点负责的字段
    .currentStep("图片收集")
    .build();

    // 其他字段保持不变
    return WorkflowContext.saveContext(update);
    });
    }

    四、并发死锁(多个节点等待彼此释放资源)

    解决示例代码如下:

    // 1. 使用超时
    ExecutorService pool = Executors.newFixedThreadPool(4);
    RunnableConfig config = RunnableConfig.builder()
    .addParallelNodeExecutor("start", pool)
    .timeout(Duration.ofSeconds(30)) // 设置超时
    .build();

    // 2. 避免共享可变状态
    // 并发节点只读取共享状态,写入各自独立字段
    graph.addNode("content_images", node_async(state -> {
    WorkflowContext ctx = WorkflowContext.getContext(state);
    List<ImageResource> content = collectContentImages(ctx.getOriginalPrompt());
    ctx.setContentImages(content); // 独立字段,不冲突
    return WorkflowContext.saveContext(ctx);
    }));

    graph.addNode("illustrations", node_async(state -> {
    WorkflowContext ctx = WorkflowContext.getContext(state);
    List<ImageResource> illus = collectIllustrations(ctx.getOriginalPrompt());
    ctx.setIllustrations(illus); // 独立字段,不冲突
    return WorkflowContext.saveContext(ctx);
    }));

    赞(0)
    未经允许不得转载:171主机测评 » 构建高效AI工作流:Java生态的LangGraph4j框架详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址