Java生态低代码工作流+智能体平台架构实践:LangChain4j与LangGraph4j落地

发布时间:2026/9/28 7:55:13
Java生态低代码工作流+智能体平台架构实践:LangChain4j与LangGraph4j落地 开头先说一个背景。我们团队常年在 Java 技术栈上做业务系统今年以来连续接了三个和“智能体”相关的内部项目一个是客服工单自动分派一个是合同要素抽取加合规校验还有一个是让业务人员在页面拖拽搭一个“AI 处理流程”。三个项目做下来有个共同感受——把模型接入、工具调用、RAG 这些都搞定其实不难真正复杂的是把流程的流转、分支、人工审批和智能体能力揉在一起还得让业务人员看得懂、改得动。这就是我们决定不做一堆独立机器人、而是做一个“低代码工作流 智能体”统一平台的原因。技术选型最后落在 LangChain4j 和 LangGraph4j 上理由后面会细说。这篇文章就是平台从零到一落地过程中的架构笔记。适合正在 Java 生态里做智能体平台、想复刻类似 Coze/n8n/Dify 形态但又不想引入 Python 技术栈的团队。我会把分层设计、工作流引擎建模、节点运行时、人工介入、持久化和踩坑经验都展开讲尽量给到可直接参考的结论。1. 为什么要把智能体工作流搬到 Java 生态项目背景与选型逻辑1.1 我们真正要解决的需求不是“聊天机器人”需求方最初提得很简单“给我们做个智能体平台”。但做需求拆解的时候发现大家想要的不是网页上能聊天的机器人而是一套能承接真实业务的自动化流程。比如客服工单进来之后先由大模型判断意图再自动查订单库超时需要转人工特殊投诉要推到审批流每一步都要留痕。这类需求天然是“工作流”形态。工作流引擎负责把流程跑起来LLM 只是流程里面的一个计算节点。把这两件事混在一起是会出问题的如果只靠智能体自行规划业务人员完全没法控制边界如果只靠传统流程引擎又没法利用模型的语义理解能力。所以一开始的需求定义就清楚低代码画布上拖节点节点里可以有 LLM 调用节点之间的流转由平台掌控。1.2 选 Java 生态最核心的原因是长期维护成本当然也认真考虑过 Python 生态毕竟 LangChain、LangGraph 的原始版本都在 Python 那边资料多社区案例丰富。我们在这个阶段做了一个比较理性的判断这不是性能对比问题而是“谁会长期维护这套系统”的问题。团队主力是 Java 工程师业务系统全部跑在 Spring Boot 和微服务体系里。引入 Python 服务意味着要多留一套技术栈、多招一类人、多维护一套部署链路。为了用上几个 AI 框架推翻现有的工程体系不划算。更何况 LangChain4j 已经覆盖了 Java 生态里绝大多数模型接入需求LangGraph4j 虽然是相对年轻的项目但已经把 LangGraph 的状态图核心建模思想移植过来了。Java 生态并不缺能力缺的是有人愿意把它工程化。1.3 Spring AI、LangChain4j、LangGraph4j 之间怎么分工这是很多团队都会纠结的问题尤其是“到底用 Spring AI 还是 LangGraph4j”这种问题在社区里反复出现。我的结论是它们根本不在一个抽象层次上。Spring AI 和 LangChain4j 解决的是“模型怎么接入、工具怎么暴露、对话记忆怎么管理”这类 AI 基础设施问题。LangGraph4j 解决的是“多个步骤之间的状态流转和编排”问题。工作流平台同时需要这两层能力所以不是二选一而是要组合。我们最终选了 LangChain4j 而不是 Spring AI主要考虑的是 RAG 组件、工具调用协议、多模型适配这些能力更接近 AI 应用的实际需求如果团队已经深度绑定 Spring 生态选 Spring AI 也完全合理这和 LangGraph4j 不冲突。我们内部的架构原则是LangChain4j 只管 AI 能力下沉LangGraph4j 只管工作流执行谁也不要试图替对方做决策。2. 平台的五层架构从可视化画布到可执行图的完整链路2.1 整体分层平台整体的横向分层我们参考了低代码引擎和流程引擎的常见做法同时在中间插入了一层专门承接“智能体能力”。从下往上看是这样的层级职责核心组件接入层对外暴露 API、Webhook、消息触发REST API、MQSender、定时调度设计层可视化画布、DSL 解析、版本管理流程设计器、Schema Validator、DSL Compiler编排层状态机执行、分支路由、中断恢复LangGraph4j、Checkpoint、事件总线能力层模型接入、工具调用、RAG、记忆管理LangChain4j、EmbeddingStore、Tool Registry资源层基础模型、数据库、向量库、外部系统各类 SaaS/内部服务、Mysql、Vector DB这里要特别说明的是“设计层”和“编排层”之间的边界。设计层负责把画布上的图翻译成一种结构化的 DSL编排层只负责执行 DSL 编译之后的状态机。这样的好处是前端换了、画布换成别的厂商后端执行引擎不需要动反过来执行引擎升级画布也不用等。2.2 低代码不是“零代码”边界要划清楚低代码平台最容易犯的错误是把“低代码”理解成业务人员什么都能拖。实际落地过程中我们发现业务人员适合拖的是“流程顺序”和“参数映射”绝对不能拖的是“一段任意的 Java 代码逻辑”。所以我们把节点分成两类内置基础节点和扩展能力节点。内置节点包括触发节点、LLM 节点、HTTP 请求节点、条件分支节点、人工审批节点、消息通知节点。扩展能力节点则通过自定义插件方式提供业务人员看不到、也改不了插件内部代码只能看到暴露出来的输入输出字段。这样既保证了灵活性也避免了把整个平台的稳定性寄托在业务方写的代码上。2.3 为什么不能允许前端直接生成可执行代码第一版我们想过一种“更简单”的方案前端画布直接生成 LangGraph4j 的 Java 代码然后动态编译执行。试了几天就发现不可行。问题有三个安全风险太高。业务方输入的内容会进入代码生成流程等于把 RCE 的路子开了一部分。版本管理困难。Java 代码不是好的存储格式没法做字段级别的 diff review。流程引擎变更后存量流程无法平滑升级因为已经生成出去的代码我们控制不了。所以现在我们的链路是画布 JSON → 平台 DSL → 编译校验 → 可执行图。每一个环节都有产物、有版本号、有校验出问题能回滚。3. 低代码画布与 LangGraph4j 的桥接流程图如何变成可执行状态机3.1 第一步定义一套稳定的 DSL 结构我们在画布上看到的是图但后端存下来的是一份 JSON DSL。节点之间不直接连线传参数全部通过全局状态读写。DSL 大概是这样的结构{ version: 1.0, nodes: [ { id: start, type: trigger, name: 工单触发, outputs: { query: string, userId: string } }, { id: classify, type: llm, name: 意图分类, model: qwen-plus, promptTemplate: 你是客服分类助手根据用户输入判断工单类型只输出一个分类。, inputs: [query], outputs: { category: string } }, { id: search, type: http, name: 查订单, url: http://order-service/api/orders, method: POST, inputs: [userId], outputs: { orderDetail: object } } ], edges: [ { from: start, to: classify }, { from: classify, to: search, condition: outputs.category after_sales } ] }这份 DSL 是平台的地基。字段名、类型、条件表达式、输入输出的引用关系全部要在这个环节做校验。我们后来专门做了一套 Schema Validator就是为了防止两个节点之间字段名拼写不一致导致运行期才炸。3.2 第二步把 DSL 编译成 LangGraph4j 的状态图LangGraph4j 的核心概念和 Python 版 LangGraph 一致StateGraph 定义节点和边State 保存整个流程的上下文节点执行后返回一批字段更新到 State 里。我们用 DSL Compiler 把平台 DSL 翻译成 StateGraphWorkflowDefinition definition WorkflowDslParser.parse(dslString); StateGraphWorkflowState builder new StateGraph(WorkflowState.SCHEMA); for (WorkflowNode node : definition.nodes()) { builder.addNode(node.id(), new NodeExecutorFactory().create(node)); } for (WorkflowEdge edge : definition.edges()) { if (edge.hasCondition()) { builder.addConditionalEdge(edge.from(), state - evaluateCondition(edge.condition(), state)); } else { builder.addEdge(edge.from(), edge.to()); } } StateGraphWorkflowState graph builder.compile();上面的代码是我压缩过的核心逻辑实际工程里还要处理节点超时、重试策略、日志埋点等包装逻辑。这个桥接层是整个平台最值得投入的地方它把面向业务的 DSL 和面向引擎的实现彻底解耦了。3.3 为什么状态必须共享、节点之间不能自己传数据这是一个非常关键的设计决策。第一版我们把每个节点都设计成独立的小服务输入输出通过内部消息传后来发现调试困难流程跑到一半根本看不清是哪个节点写坏了数据。改成全局 State 之后所有节点的输入都是从 State 里按 key 读取输出也是往 State 里按 key 写。这样做有四个直接好处画布上可以实时展示当前 State 快照业务人员能直观看到“当前这一步拿到了什么数据”。LangGraph4j 的 checkpoint 机制可以很容易把整个运行状态保存下来实现中断恢复。条件分支可以引用任意上游字段不需要边的设计再去定义“传递哪些字段”。排查问题的时候回放一条 Trace 就等于回放整个流程的数据变化不用同时追踪几十个节点间的消息。当然代价也很明显State 会越来越大字段命名规范必须抽成团队约定否则过一个月就没人分得清 state 里面哪个 key 是订单号。3.4 分支条件的表达不能过度自由条件分支的 DSL 我们故意限制成很简单的表达式没有引入完整脚本语言。目前支持的是等值比较、包含判断、数值大小比较以及它们之间的 and/or 组合。为什么不做 Groovy 之类的脚本因为条件表达式会被业务人员直接写在配置里一旦支持完整脚本语言就会有人写循环、写系统调用、写一些执行引擎完全无法预料的逻辑。现在的做法是条件表达式中只允许引用 State 里的已有字段和节点输出字段由 Schema Validator 做严格的编译期检查。执行时如果字段不存在直接报错并触发兜底分支而不是静默通过。4. 节点运行时的设计LLM 调用、工具编排与记忆管理怎么下沉4.1 每个节点都遵守同一个执行契约状态图看起来简单但真正要支持多种类型节点时统一契约非常重要。我们定义了一个节点执行器接口所有内置节点和插件节点都必须实现它。契约只有三个步骤解析输入、执行逻辑、写入输出。resolveInput(state) - 从 State 中取出 declaring inputs execute(context) - 执行实际逻辑 writeOutput(state) - 更新结果字段到 State这样一个 HTTP 节点或 LLM 节点在 LangGraph4j 眼里只是一个执行函数。业务人员新配置一个节点就是告诉平台“节点的输入输出字段是什么”而不会直接触碰执行器代码。4.2 LLM 节点最常用的节点也最容易失控LLM 节点是我们平台上用得最多的节点承担意图分类、信息抽取、摘要生成、内容改写等任务。它的配置项除了模型、提示词模板之外必须锁定超时时间、最大 Token 数、温度参数。实践中非常关键的细节是所有 LLM 节点返回的字段都要经过输出 Schema 校验再写回 State。大模型的输出不稳定可能多出来一个字段也可能漏掉一个字段。我们的做法是定义一个“结构化输出”配置例如让模型按 JSON 返回一段摘要和一段分类然后接入层做 JSON 解析和字段校验不满足直接触发重试或走兜底分支。4.3 Agent 节点的价值在可控流程里保留自主空间不是所有步骤都能固化成单一 LLM 调用。例如“售后纠纷处理”可能需要模型先判断要不要查订单、要不要查物流、要不要调用退款接口。如果把这些可能路径全部用前置节点画出来流程会膨胀到不可维护。解决方案是 Agent 节点。它本质上是一个小型 ReAct 循环由 LangGraph4j 外包里面的子流程或者由 LangChain4j 的 AiServices 直接运行。每个 Agent 节点挂载一批允许使用的工具和一个系统提示词模型可以自主选择调用哪些工具但工具范围是平台锁定的。这正是“工作流 智能体”的组合价值所在外层是业务人员看得见的流程骨架内层是模型自适应的执行空间。4.4 对话记忆不能一股脑放进全局 State带有多轮对话的工作流都会遇到记忆问题。我们把记忆分成两级流程级记忆和节点级记忆。流程级记忆保存用户输入、历史消息列表、业务上下文主要给长链路流程使用节点级记忆只存在于单个 LLM 节点或 Agent 节点内部节点执行完就释放。这样做是因为如果所有节点共享同一块 ChatMemory会让后续节点的输入越来越长、Token 成本直线上升。我们的 LangChain4j 集成里每个节点都有独立的 ChatMemoryProvider流程级上下文则通过 State 里的固定字段传递。5. 把“人”放进工作流审批、中断恢复与人工介入的实现思路5.1 人工节点不是暂停线程而是保存现场一开始我们天真地想人工审批节点就是“运行到这个节点时线程 wait等审批回调后线程 resume”。这样做首先就把线程池打爆了而且进程重启后所有等待中的流程全部丢失。正确做法是基于 checkpoing 的“挂起-恢复”。当流程执行到 HumanTask 节点时先把当前 State 快照持久化然后发布一个待办任务到任务中心。任务中心处理完人审动作后把审批结果写回事件总线平台收到事件后从快照中恢复状态机继续往下执行。LangGraph4j 本身在 Python 版里有 interrupt 概念Java 版目前我们更多是自己实现这一层。具体的做法是执行 HumanTask 节点前调用 checkpoint 保存 State 快照向消息队列发送待办消息包含流程实例 ID 和当前节点 ID队列消费端将任务入库业务系统或审批系统展示审批完成后回调平台 API带上审批意见平台根据流程实例 ID 恢复快照写入审批结果用 LangGraph4j 的 invoke 重新进入流程继续执行后续边。5.2 超时和自动驳回不能省人工介入最大的坑不是功能实现不了而是流程永远挂在那等人。我们给每个 HumanTask 节点都配置了超时时间和超时策略。超时之后可以自动驳回、自动通过、或者转交给另一个审批组。记得有一次做工单流程没有配超时第二天发现积压了三千多个流程实例全部停在“待人工确认”阶段。从那以后我们在 DSL 校验规则里直接加了一条人工节点必须配置超时策略否则不允许发布。5.3 对账所有挂起流程都要有巡检任务事件驱动的好处是异步解耦但代价是消息丢失后没有反馈。我们加了一个定时对账任务每五分钟扫描一次数据库里的挂起状态流程如果状态显示 HumanTask 等待中但任务中心已经没有对应待办就把它重新推送一次或者标记为异常。对账逻辑听起来很朴素但真正救过我们很多次。只要涉及“状态和外部系统不同步”的场景最后兜底的永远是这种简单可靠的扫描任务而不是更复杂的分布式事务。6. 上线前必须解决的工程问题状态持久化、并行度与可观测性6.1 状态持久化是平台的中枢神经由于平台天然是一个有状态系统State 快照和恢复就不能只是“够用”。我们为每个流程实例维护了完整的历史快照每次节点执行前后都会做一次 Checkpoint记录这次执行的输入、输出、耗时、节点版本、DSL 版本。这样做的成本不小但收益非常大任何一次执行出问题可以直接回放 Checkpoint 复现发布新版 DSL 之后可以对比老实例的执行路径人工介入恢复不需要担心状态丢失。我们存储上用的是数据库 JSON 字段并没有一开始就上专门的流式记录系统。简单、可查、好扩容先满足业务闭环后续再考虑更重的存储。6.2 并行度可以靠虚拟线程但要控制资源上限Java 21 的虚拟线程让并发执行成本下降很多我们所有的节点执行器都跑在虚拟线程里省去了一大堆异步回调代码。但这里有个隐藏问题LLM 调用和 HTTP 调用都是外部 IO如果流程实例大量并发启动虚拟线程只是让线程数量不再成为瓶颈真正的瓶颈会跑到外部系统的限流上。所以我们在平台入口做了两层控制一是全局并发上限限制同一时刻运行的事务数二是每个外部目标服务配了独立的信号量避免某个流程发起的批量请求直接把下游系统打挂。6.3 一条 Trace 贯穿画布、引擎和模型调用工作流平台排查问题最怕的是“不知道哪个环节出错”。我们的做法是把 Trace ID 从入口请求一路贯穿到 StateGraph 的每次节点执行、每一次 LLM 调用、每一条工具调用。LangChain4j 的调用链路和 LangGraph4j 的节点执行信息都打到同一个 Trace 上下文里前端画布上可以直接按时间轴查看每个节点的输入输出。体验上的效果是业务人员截图给研发的时候已经不是“这个流程挂了”而是直接告诉你“这个 Trace 在第三个节点上模型输出超时了”。这个投入很值得。6.4 模型限流和异常退避LLM 节点必须有熔断策略大模型服务的限流是平台稳定性的最大变量。不同的模型供应商返回限流的状态码可能不完全相同所以我们在模型调用层做了一个统一适配把限流、超时、服务不可用分别映射成平台内部异常再由节点执行器统一决定重试还是走兜底分支。重试策略我们遵循一个原则幂等操作可以自动重试非幂等操作最多重试一次。比如查询类节点可以重试三次但“创建工单”这类写操作默认不自动重试防止重复创建。7. 落地过程中的几个反直觉经验7.1 低代码平台的瓶颈不在引擎在 DSL 校验我们项目最花时间的其实不是 LangGraph4j 的集成而是把 DSL 校验做完整。之前有一版流程发布上线之后所有“查询订单”节点的字段名是 orderId但“生成回复”节点的输入字段引用的是 orderID大小写差一个字母运行期间全部返回空值。后来我们把 Schema Validator 做成独立模块每次 DSL 发布前做全量符号解析所有引用的字段必须存在所有输出字段必须有明确类型所有条件表达式必须能通过静态分析。这套校验规则已经累计了三十多条可以说每一次崩溃都在这里补一条规则。7.2 别让智能体替流程做所有决策产品经理一开始希望智能体自动处理尽可能多的场景后来我们发现自主性越高越难保证结果稳定。凡是业务有明确定义的路径就画成普通工作流节点只有分支路径不确定的场景才开放给 Agent 节点。用大白话说能画清楚的不要交给模型猜。这个理念做进产品之后流程稳定性提高很多。业务方也逐渐理解了平台的定位不再要求“智能体全自动搞定一切”。7.3 缓存模型输出要非常谨慎为了省钱我们曾经做过一层模型结果缓存输入提示词完全相同的请求直接返回上一次的结果。表面上命中率很高但后来出现一个诡异问题政策规则变了模型提示词没变缓存里还是旧结果业务方坚持说系统没更新。从那以后我们对缓存策略做了限制只有明确声明为“可缓存”的节点才启用缓存并且缓存 key 必须包含 DSL 版本号和节点配置版本号。发布新版本后对应旧缓存自动失效。7.4 最后一点做平台级工具一定要预留“演示 demo”给业务方做平台最难的往往不是技术而是让他们理解“拖一个节点”意味着什么。我们最后总结出一个比较有效的方式每一类内置节点都预置了一批“示例流程”业务方进去直接复制一个模板改改关键词就能跑通。这些精心设计过的模板比写二十页文档都有用。从工程角度看LangChain4j 和 LangGraph4j 的配合已经能在 Java 生态里撑起一个足够完整的工作流智能体平台。真正决定这套系统能不能长期活下去的反而是 DSL 设计、状态恢复机制、节点契约和那些看起来不性感的校验逻辑。希望这份架构笔记能帮打算走这条路线的团队少踩一些我们踩过的坑。