LangChain4j+LangGraph4j构建低代码智能体工作流平台实战
标题起得很工程化但背后其实是一个很实在的问题在 Java 技术栈里能不能像搭积木一样把 AI 能力编排成可运行的业务系统。我最近用 LangChain4j 和 LangGraph4j 把一套低代码工作流通用智能体平台的架构从想法落成了代码踩了不少坑也理清了不少设计决策。这篇就把整个平台的架构设计思路、核心实现细节、以及实操中的教训一次性讲透。开门见山说结论LangChain4j 负责“模型能力接入”LangGraph4j 负责“流程状态编排”中间加一层自研 DSL 做低代码桥接这个组合在 Java 生态里是目前最务实的一条路。全文围绕这个核心结论展开适合正在做 Java AI 应用、或者正在选型智能体开发框架的团队参考。1. 为什么选 LangChain4j LangGraph4j先解决选型纠结1.1 当前 Java AI 框架的“三足鼎立”Java 生态做 AI 应用现在绕不开三个选项Spring AISpring 官方出的 AI 抽象层封装了 OpenAI、Azure、Ollama 等模型 API主打“Spring 开发者熟悉”的体验。LangChain4j社区驱动、由原作者维护的 Java 版 LangChain目标非常明确——把 Python 生态里成熟的 Agent/RAG 模式用 Java 重写。LangGraph4jLangChain4j 官方的图编排扩展对标 Python 的 LangGraph核心解决“多节点、带状态、可循环”的复杂 Agent 工作流问题。很多人一上来就纠结“到底用 Spring AI 还是 LangGraph4j”。我的答案是先分清你要解决的是“模型调用问题”还是“流程编排问题”。1.2 LangGraph4j 和 Spring AI 的定位对比Spring AI 解决的问题是“让 Java 开发者能用最少的代码调用大模型”它做得很好但它没有真正意义上的图状态编排引擎。你要实现“先检索、再生成、然后人工审核、不满意就循环重写”这种流程用 Spring AI 写出来是一堆 Service 方法互相调用状态管理全靠自己。你可能需要维护一个状态机的复杂逻辑这种场景代码量一大就失控。LangGraph4j 解决的恰好就是这个层次的问题。它的核心抽象是StateGraph把整个 AI 任务建模成一张有向图节点是处理逻辑边是流转条件图内共享一个可序列化的状态对象。它天生支持循环、条件分支、异步执行、断点恢复这正是工作流引擎需要的底层能力。这是对比表格。维度Spring AILangChain4j LangGraph4j模型调用封装好好对话记忆管理基础好支持多轮、持久化多节点流程编排弱强图模型循环/条件分支需手写原生支持状态持久化与断点需手写LangGraph4j 提供 Checkpoint 机制团队技术栈匹配度高Spring 系高Java 系学习曲线平缓中等需要理解图模型1.3 低代码平台为什么必须选“图编排”而不是“链编排”做低代码工作流平台最关键的需求是用户拖出的流程是动态的。用户今天拖一个“A 节点 → B 节点 → 结束”明天可能拖一个“A 节点 → 条件判断 → B/C 节点 → 循环回 A”。这种动态拓扑如果靠硬编码编排代码几乎不可能实现。LangChain4j 早期的 AiServices 和 AiStreamingServices 主要面向“链路Chain”模式适合顺序调用但一旦出现条件分支和循环链式抽象会很吃力。LangGraph4j 的 StateGraph 天然就是图节点之间的关系自己控制结构灵活且这套思路和低代码可视化画布的目标架构是一致的——用户在画布上画图平台把图翻译成 StateGraph。另外一个重要原因是执行引擎的通用性。低代码平台需要承接大量不同业务方的工作流图引擎可以统一处理这些动态变化。如果每个流程都写一套定制代码就不是低代码平台了。2. 平台整体架构从“画布拖拽”到“图引擎执行”的完整链路2.1 五层架构设计整个平台我按五层来拆每一层只做一件事边界尽量清楚。层级职责关键技术选型展示层可视化画布、节点配置表单、运行日志面板Vue3 LogicFlow或类似流程设计器组件接入层REST API / WebSocket 提供设计器读写、任务触发、运行状态推送Spring Boot 3 WebFlux领域层工作流定义管理、版本控制、DSL 解析器Java 17 Jackson自研 DSL Schema执行层把 DSL 翻译成 LangGraph4j StateGraph 并运行LangGraph4j LangChain4j基础设施层模型 API、向量库、任务队列、持久化存储OpenAI/Ollama 等RedisPostgreSQL/MySQL这套分层最简单也最可靠。低代码平台最容易犯的错误是把所有逻辑都塞进“画布后端接口”里接口越写越胖到最后加一个节点类型要改十几个地方。2.2 DSL 是低代码平台的核心枢纽低代码平台无法回避一个关键设计问题前端画布的数据结构怎么变成后端能跑的东西。很多团队走的是“前端直接传流程图 JSON 给后端后端每收到一个 JSON 就解析执行”。这个方案在前几个版本用着还行但项目一复杂就出问题前端数据结构稳定不下来、后端要兼容前端所有历史版本、没法做语义校验也缺少独立的版本管理。更好的方式是引入DSL领域特定语言即一套平台自定义的工作流描述规范。前端画布产出的是“图形数据”包含节点位置、连线信息等展示属性后端把它翻译成 DSL只包含执行语义执行引擎再消费 DSL。这中间多了一层翻译看着多一道工序但上游和下游都能独立演进。这是设计好的简化 DSL 结构。{ id: wf_001, version: 3, nodes: [ { id: start, type: start, name: 开始, config: {} }, { id: subjective, type: llm_chat, name: 简历筛选Agent, config: { modelName: qwen-plus, promptTemplate: 你是 HR 助手请筛选以下简历……, temperature: 0.3 } }, { id: review, type: human_review, name: 人工复核, config: { assigneeType: role, assigneeValue: hr_manager } }, { id: end, type: end, name: 结束, config: {} } ], edges: [ { id: e1, source: start, target: subjective }, { id: e2, source: subjective, target: review, condition: result.confidence 0.6 }, { id: e3, source: subjective, target: end, condition: result.confidence 0.6 } ] }字段含义nodes定义图里的所有节点核心是type和configtype决定这个节点映射到哪个执行策略config是节点参数。edges定义节点之间的连接source/target是节点 IDcondition是可选的流转条件表达式。每个工作流有version修改后生成新版本执行时按版本号使用保证线上流程稳定性。引入 DSL 之后平台能力边界就清晰了画布只负责生成图形数据。DSL 只负责描述执行语义。执行引擎只负责把 DSL 翻译成 LangGraph4j 图并运行。2.3 三个层的中间件设计实际编码过程中我加了一道服务DSL 翻译器。严格来说它不是独立一层而是领域层的一个核心模块。它的作用是把 DSL JSON 解析成 LangGraph4j 的各种内部对象——节点函数、边、条件判断、状态类型定义。你可以类比成“编译器前端”和“编译器后端”——DSL 是源代码StateGraph 是生成的目标代码。这个翻译器要处理三类映射节点映射DSL 的type字段 → Java 里的节点执行类。边映射DSL 的edgescondition→ LangGraph4j 的addEdge和addConditionalEdges。状态映射DSL 声明的全局状态字段 → LangGraph4j 的Config和状态对象类。做好这个翻译器整个平台的扩展方式就变成每新增一种节点类型加一个节点执行类 注册一个 type 映射不需要改执行引擎主体。3. 核心实现把 DSL 翻译成 LangGraph4j 图3.1 状态对象设计工作流的数据总线LangGraph4j 里所有节点共享同一个状态对象节点之间通过它传数据。低代码平台的节点类型五花八门所以状态对象不能写死字段。我的做法是定义一个“Map 容器型”状态每个节点往里放东西。这是简化后的实现。public class WorkflowState implements Serializable { private MapString, Object data; public WorkflowState() { this.data new HashMap(); } public WorkflowState(MapString, Object data) { this.data data; } public Object get(String key) { return data.get(key); } public void set(String key, Object value) { data.put(key, value); } public MapString, Object data() { return this.data; } public static WorkflowState of(MapString, Object data) { return new WorkflowState(data); } /*********** 以下是 LangGraph4j StateGraph 要求的接口实现 ***********/ public static WorkflowState from(MapString, Object initData) { return new WorkflowState(initData); } Override public MapString, Object toMap() { return data; } }在 LangGraph4j 中状态接口一般要求能从一个MapString, Object构建实例并能把自己的数据转回成 Map。上面这个类正好满足这两个要求。如果你要让状态支持类似“并发更新哪部分字段”的红ucer 机制可以在toMap()和构造方法里做增量合并。低代码平台初期可以不做那么细但要留好这个扩展点因为很多工作流是需要多个分支并发执行的并发写状态时必须要有合并策略。3.2 节点抽象让“拖拽的节点”变成“可执行的代码”工作流平台最核心的扩展点是节点。我先定义了一个通用节点基类public abstract class WorkflowNode { protected final String nodeId; protected final MapString, Object config; protected WorkflowNode(String nodeId, MapString, Object config) { this.nodeId nodeId; this.config config; } public String nodeId() { return nodeId; } public MapString, Object config() { return config; } public abstract WorkflowState execute(WorkflowState state, WorkflowContext context); }WorkflowContext里装着模型客户端、向量库客户端、调用链 Trace 等运行时设施节点执行时从里面取依赖不自己 new 客户端。平台预设了几类基础节点每类一个执行类。节点类型执行策略说明llm_chat调用大模型生成文本写入状态支持 prompt 模板、temperature、maxTokens 等rag_retrieve从向量库检索文档片段支持 topK、相似度阈值、过滤器http_request调用外部 API写入状态支持 GET/POST、鉴权、头/体映射human_review暂停流程等待人工输入通过 WebSocket/轮询把待审任务推给前端code_exec执行一段脚本用于扩展性极强但风险较高的场景condition按条件路由到不同下游节点支持表达式、多重条件end结束流程返回最终状态可聚合结果、记录日志每个节点执行类的核心逻辑遵循一个约定从 state 取输入做处理把结果写回 state。这样节点与节点之间是“数据耦合”而非“代码耦合”符合低代码平台松耦合的设计要求。3.3 用 LangGraph4j 构建 StateGraph在 LangGraph4j 里构建图核心 API 是StateGraph。流程通常是定义状态类 → 创建 StateGraph → 添加节点 → 添加边 → 编译成可执行图 → 传入初始状态运行。把 DSL 翻译成 StateGraph 的逻辑大致如下。public StateGraphWorkflowState build(WorkflowDefinition dsl) { StateGraphWorkflowState graph new StateGraph(WorkflowState::from); // 1. 添加所有节点 for (NodeDef nodeDef : dsl.nodes()) { WorkflowNode executor nodeFactory.create(nodeDef); String nodeKey nodeDef.id(); graph.addNode(nodeKey, state - executor.execute(state, context)); } // 2. 添加普通边 for (EdgeDef edgeDef : dsl.ordinaryEdges()) { graph.addEdge(edgeDef.source(), edgeDef.target()); } // 3. 添加条件边一个 source 对多个 target for (BranchDef branch : dsl.branches()) { graph.addConditionalEdges( branch.source(), state - decideNextNode(branch, state), branch.targets() ); } // 4. 设置入口 graph.setEntryPoint(dsl.startNodeId()); // 5. 设置结束节点 graph.setFinishPoint(dsl.endNodeId()); return graph; }这里的关键点是addConditionalEdges的用法第一个参数是“从哪个节点出发”第二个参数是一个函数——接收当前状态返回下一个节点的 key第三个参数是候选节点 key 列表。我根据 DSL 里的 condition 表达式封装了一个decideNextNode方法private String decideNextNode(BranchDef branch, WorkflowState state) { if (branch.conditions() null || branch.conditions().isEmpty()) { return branch.defaultTargetId(); } // 这里可以是简单的 map 匹配也可以是 Aviator/SpEL 表达式求值 return evalCondition(branch.conditions(), state) ? branch.trueTargetId() : branch.falseTargetId(); }各节点之间流转的数据就是通过WorkflowState传递的。比如简历筛选场景llm_chat节点把分析结果写在state.data[resumeAnalysis]上后面的条件边读取这个字段判断是否进入人工复核。这样把 DSL 中声明的字段名和节点执行类里写入的 key 对齐即可很直观。3.4 三个典型的图模式实际搭平台时我发现最常用的三种图模式是直链、条件分支、循环回绕。写出来给大家直接照抄参考。直链模式是最简单的顺序执行比如“开始 → 一问一答 → 结束”。对应代码就是graph.addNode(start, ...); graph.addNode(llm_1, ...); graph.addNode(end, ...); graph.addEdge(start, llm_1); graph.addEdge(llm_1, end);直链模式适合简单的知识问答、内容分类、格式转换。LLM 只需要一次调用的情况用 LangChain4j 的ChatLanguageModel直接调用就行不需要进图。条件分支模式用于需要判断的业务流比如简历筛选“高置信度走人工复审低置信度直接结束”。核心是addConditionalEdges。LangGraph4j 的实现里条件边的返回值是目标节点 key这个 key 必须在候选节点列表里。循环回绕模式是最体现 LangGraph4j 价值的地方。常见场景是“Agent 生成的内容自检不合格就重新生成最多重试 N 次”。在 LangGraph4j 里实现循环的本质是让图中出现一个环。graph.addNode(generate, ...); graph.addNode(quality_check, ...); graph.addEdge(generate, quality_check); graph.addConditionalEdges( quality_check, state - { int retryCount state.get(retryCount) null ? 0 : (int) state.get(retryCount); boolean pass Boolean.TRUE.equals(state.get(qualityPass)); if (pass || retryCount 3) { return end; } // 没通过就回到 generate 再来一次 state.set(retryCount, retryCount 1); return generate; }, List.of(generate, end) );每次重试把计数器累加写回状态直到条件满足或者次数耗尽。这种做法如果用普通 Java 代码写会陷入“哪里保存状态”“什么时候跳出循环”“会不会死循环”的泥潭在 LangGraph4j 里因为循环本身就是图的一部分每次经过quality_check节点都会重新进入generate节点执行逻辑是天然可持续的。这里有个很隐蔽的坑也是我实际踩过的条件边返回的 key 必须和候选节点列表里的 key 一致大小写、前后空格一个都不能差。LangGraph4j 内部是用 Map 查找节点找不到会直接抛异常而且异常信息并不直观第一次遇到时排查了很久。4. 低代码平台的核心细节可视化编排、DSL 校验与状态快照4.1 可视化画布背后要做的事很多人以为低代码就是“拖个框、连根线、填几个参数”就完了实际要处理的事情比这多得多。设计器后端至少要做三件事第一节点参数的校验。不同节点类型有各自的必填项和校验规则。比如http_request节点必须填 URL、必须有请求方式llm_chat节点必须填 prompt 模板模型名有枚举限制。这些校验如果在后端做一遍在前端就要做一遍同样的。最好的办法是把节点参数 Schema 定义成一份公共 JSON Schema前端根据 Schema 动态渲染表单后端用同一份 Schema 做校验。前后端共用一套定义能避免大量“前端能保存、后端执行报错”的尴尬情况。第二图的合法性校验。前端画布允许用户自由连线但后端必须校验几个基本规则每个图必须有且仅有一个start节点和一个end节点。不允许存在孤立节点既无入边也无出边的节点除了 start/end。不允许出现不可达节点从 start 出发无法到达。condition节点必须有至少两条出边且所有出边的条件不能同时为 true这里可以做静态分析但大多是运行时判断。建议画布上加“校验错误”标记不用等保存后才看到问题。这些规则如果完全依赖画布前端去限制会被用户的各种奇怪操作打破。后端在解析 DSL 时做严格校验是最后一道防线不能省。第三版本管理。工作流和代码一样要支持修改、回滚、灰度发布。我设计的工作流定义表里有三个关键字段workflow_id逻辑 ID、version版本号、status草稿/已发布/已下线。每次编辑保存生成新版本草稿发布后执行引擎固定引用某个版本。这样线上已经在跑的老流程不会被新的编辑影响出问题可以瞬间回滚到上一个稳定版本。4.2 DSL 校验语义校验比结构校验更重要普通 JSON Schema 只能做结构校验例如“字段类型对不对、必填项有没有”。但工作流 DSL 更需要语义校验这些是 JSON Schema 做不到的边引用的source或target是否在nodes里存在。条件表达式引用的字段是否在状态数据里存在这个很难做到完全静态校验因为字段是动态写入的但可以约定节点输出字段必须声明在节点的 outputs 配置里。循环路径上是否至少有节一次“会修改状态”的节点防止死循环实际运行中还需配合超时控制。我在 DSL 解析器里加了一层WorkflowValidator在翻译成 StateGraph 之前先做一次遍历校验。发现了问题直接返回错误信息而不是等执行到一半才报错。这层校验对用户体验的改善非常明显。4.3 执行状态快照与 Checkpoint不只要“能跑”还要“看得见”低代码平台和普通代码项目最大的区别是使用者不需要理解代码但是使用者需要看见执行过程和结果。DL 执行过程的可观测性、可干预性比代码项目的日志要求高得多。LangGraph4j 提供了 Checkpoint 机制可以保存每次节点执行后的状态快照。我在执行层包了一层ExecutionRecord每条工作流运行记录保存工作流 ID 和版本号。执行开始时间、结束时间、整体状态。每个节点的入参、出参、耗时。状态快照序列化的 JSON。关于状态快照序列化有个需要特别提醒的实战点WorkflowState 对象里不能直接放不可序列化的对象。LLM 返回的Response、向量库返回的Document这类 Java 对象你可以在节点执行过程中使用但在写入状态之前必须转成纯数据String/Map/List否则 Checkpoint 保存的时候直接序列化异常。我在写rag_retrieve节点时一开始直接把ListDocument放进了状态执行时很顺畅保存快照时一序列化就炸了。解决方案是在写入状态前做一个 DTO 转换只保留文档内容、来源、得分等纯字段。4.4 前端画布的执行过程可视化运行状态可视化这块比较好的方案是通过 WebSocket 把执行记录实时推给前端前端在每个节点上高亮当前状态节点执行中黄色边框 旋转标识。节点执行成功绿色边框 通过。节点执行失败红色边框 错误信息弹窗。等待人工输入蓝色边框 提示文案。实测下来这套交互对“让产品经理理解 Agent 在做什么”特别有效。工作流已经不只是后端工具而是变成整个团队的协作界面。5. 实操中的常见问题与排查技巧5.1 问题速查表下面是这个项目里踩过的一些比较典型的问题做了个速查表。问题现象排查思路解决方案addConditionalEdges报节点找不到运行时抛出“Node not found”异常检查条件边返回的 key 是否和候选节点列表完全一致统一用常量管理节点 key避免硬编码字符串Checkpoint 序列化失败执行到保存快照时报 JsonMappingException状态里放入了不可序列化的 Java 对象所有写入状态的数据先转成纯 Map/List/String模型调用超时导致整个工作流阻塞页面上流程一直停在某个节点模型客户端没有配超时时间在 LangChain4j 配置里设置timeout建议 60s 上限多个分支并发写状态互相覆盖出现了不符合预期的数据缺少状态合并策略用 LangGraph4j 的状态 reducer 机制或者规范化写状态 keyDSL 版本混乱线上流程被新编辑影响老流程行为发生变化执行引擎没有按版本号取 DSL工作流发布后生成不可变版本快照执行时只读快照某一类节点扩展成本高加一个节点类型要改前端、后端、校验三层没有统一的节点注册机制用“type 配置项”驱动新增节点只需实现执行类并注册循环路径不退出流程一直重试浪费模型额度循环次数没有限制在条件函数里维护 retryCount 并设置上限同时加全局超时5.2 长期维护的低代码平台必须做好的三个底线第一节点执行必须有超时和熔断。大模型调用的不确定性决定了节点可能很慢甚至挂起。每个节点执行时都提供maxExecutionTime参数超时后标记失败并走错误分支而不是无限等待。这个参数在设计器里就是节点配置项用户可调平台兜底一个默认值比如 120 秒。第二条件表达式要限制能力边界。DSL 里的 condition 字段会透传到执行引擎如果允许任意代码表达式等于给用户开了一个云函数后门。安全上必须做白名单校验只允许访问状态里已有的 key、只允许调用预设的比较函数不允许自由执行任意 Java 代码。可以用 Aviator 这类轻量表达式引擎做白名单解析也可以只做一个简化的jsonPath 比较符表达式的自定义解析器。第三工作流编排必须考虑费用估算。低代码平台上会有很多非技术人员编排 AI 流程很容易在循环里烧掉大量 token。建议在设计器上显示“本流程预计消耗 token 数”和“当前已执行次数”运行记录里展示每次调用的 token 明细。我在执行记录表里专门存了每节点调用的 model 和 token 数月底一拉报表就能看到哪条工作流最烧钱。这个设计很多人初期不做等账单出来再补会很被动。6. 架构之外这套平台还能怎么长LangChain4j LangGraph4j DSL 这套架构的扩展潜力其实不止于“给业务人员拖拽 AI 流程”。个人觉得后续有几个方向可以自然演进多智能体协作。LangGraph4j 天然支持多 Agent 节点互联可以把“需求拆解 Agent”“代码生成 Agent”“测试执行 Agent”编排成一张协作图每个 Agent 一个节点通过状态传递上下文。与现有业务系统集成。低代码工作流本质是一条可编程的业务链路DSL 里加http_request节点就已经能对接 ERP、CRM 等系统。再往上可以让节点触发表单、消息、审批等业务动作形成完整的企业自动化平台。表单联动。工作流里的human_review节点可以扩展成“动态表单引擎”——人工审核时根据流程上下文渲染不同表单审核结果再写回状态供后续节点使用。这样就从“AI 流程编排”自然延伸成“AI 业务流一体化”平台。流程市场与模板复用。DSL 是纯文本 JSON天然适合做模板分享。做一个工作流模板市场用户可以一键导入别人发布的 DSL 模板再改改配置就能跑这也是复用性很好的方向。架构上这一套实现思路跟 Python 生态的 LangGraph 几乎一一对应如果团队里有人熟悉 Python 版迁移起来会很快。我个人在实际搭建过程中的体会是技术选型的第一性原则是想清楚你到底在解决什么层次的问题。如果你只是想让 Java 应用能“调通大模型”Spring AI 足够但如果你要建的是一个能让业务人员自由编排 AI 能力的平台那图编排引擎就势在必行。LangChain4j LangGraph4j 的组合是在 Java 生态里把这件事做得最规整的一套方案。最后再分享一个小技巧把 DSL 校验器做成一个独立的单元测试模块每个新 DSL 用例都跑一遍。平台上线后业务方总会设计出你没想到的拓扑结构比如三个条件分支嵌套、循环里套循环。与其靠人肉测试不如维护一批 DSL 测试 fixture每次修改解析器或节点执行类全量跑一遍回归能挡住绝大多数低级错误。这套测试体系花了我两天时间搭建后期维护省下的精力远远超过当时投入。