
先聊个现象。Java 生态做大模型应用过去一年最热闹的话题已经从“怎么调 OpenAI API”变成了“怎么把 Agent 编排成一套能落地、能运维、能复用的东西”。我自己在团队里带队做 AI 平台最早是用 LangChain4j 写各种零散的 Chain后来发现业务方真正想要的不是一段代码而是一条可视化的工作流拖一个“意图识别”节点、接一个“查数据库”节点、再连一个“生成回复”节点中间还要能根据结果决定走哪条分支。于是就有了这个项目的雏形——基于 LangChain4j 和 LangGraph4j 搭一个低代码工作流通用智能体平台。这篇文章会把这套平台的架构设计思路完整拆开从选型、分层、数据结构、跟 LangGraph4j 的整合细节、可视化编排器怎么做一直到企业级能力和排坑记录。适合正在做 AI 平台、Agent 编排或者低代码产品的后端同学参考。1. 项目背景与核心设计思路1.1 解决什么问题先说清楚痛点。市面上不是没有 Agent 编排产品像 Dify、Coze、n8n 都很成熟但落到我们自己的场景里有几个绕不过去的问题第一技术栈要统一到 Java。团队主力是 Spring Boot 背景不可能为了一个 AI 平台引入一套 Python 服务运维、监控、交付全都得重新搭。第二要私有化部署。客户数据不能出内网Dify 虽然可以私有化但定制接入企业内部的审批流、ERP 接口、统一登录时成本和灵活性都不够理想。第三业务人员需要“低代码”地搭 Agent而不是由开发去写链式调用代码。理想的形态是一个可视化画布拖拽节点、配置参数、预览测试、发布上线。那为什么要自己做而不是选现成的真实答案是既要 Agent 能力又要跟企业内部系统深度融合还要兼容已有的统一权限、审计、消息中心。自己基于 LangChain4j 和 LangGraph4j 搭一套底座反而比改造外部产品更快。1.2 为什么是 LangChain4j LangGraph4j 的组合LangChain4j 在 Java 圈子里基本是事实标准了模型抽象、Prompt 模板、自带工具调用、RAG 生态、输出解析器这些东西都齐了。LangGraph4j 是 LangGraph 的 Java 移植版核心价值是把工作流建模成一张有向图支持循环、条件分支、全局状态管理和多步 Agent 编排。这两个库的组合天然匹配低代码平台的需求LangChain4j 解决“每个节点里 AI 能力怎么封装”LangGraph4j 解决“多个节点之间怎么连接、怎么流转、怎么维护状态”。传统工作流引擎如 Flowable、Camunda 也不是不能用但它们擅长的是审批流、任务流节点是人和系统任务对 LLM 调用、流式输出、Token 成本控制这些事没有原生支持。如果硬用 BPMN 表达一个 Agent 循环会发现既笨重又别扭。LangGraph4j 的图模型要轻量得多节点就是一个个函数边决定流转方向状态就是整个图的共享内存。1.3 低代码平台的核心能力边界低代码不等于“什么都能拖出来”。我们明确边界平台面向的是工作流编排和 Agent 组装而不是通用业务系统开发。目标用户包括两类人一类是懂业务但不懂代码的运营同学他们编排的是“用户提问 → 查询知识库 → 生成答案”这类流程另一类是后端工程师他们用平台快速搭建内部 AI 接口但需要的时候仍能写脚本节点扩展。还要想清楚平台的形态是纯 SaaS 套壳还是提供 SDK 的嵌入式中台我们的选择是做“平台 OpenAPI”双开放模式。对内设计器、运行时、监控中心一体对外支持把工作流发布成 HTTP 接口也支持通过事件回调把执行结果推回业务系统。2. 技术选型的关键决策过程2.1 LangChain4j 与 Spring AI 的取舍Spring AI 在 2023 年刚出来时很受关注毕竟 Spring 生态官方加持。我特意用它们各做了几个 PoC最后选了 LangChain4j原因有三条一是兼容范围。LangChain4j 对模型供应商的适配更全除了 OpenAI、Azure、本地 Ollama对国内各家模型的接入案例也更多。Spring AI 的模型支持列表更新略慢某些国产模型只能自己写 Client比较费劲。二是工具调用的成熟度。Agent 场景里模型能不能稳定地按 Schema 输出工具调用参数是关键。LangChain4j 的ToolSpecification和Tool注解体系非常成熟跟 Jackson 的兼容性也更好Spring AI 在这块的函数调用机制发展得稍晚。三是社区密度。LangChain4j 从 0.x 到 1.x 的演进非常快文档、示例、博客密度在 Java 的 LLM 框架里最高。遇到问题几乎能搜到社区方案。当然 Spring AI 也有可取之处比如它对 Spring 生态的自动装配更友好、MCP 客户端标准化很积极。但考虑到团队已有 LangChain4j 经验这局 LangChain4j 更稳。2.2 LangGraph4j 与自研状态机的取舍低代码工作流引擎的核心是图执行。最开始我脑子里冒出的方案是自己写一个状态机节点就存 Map循环用递归条件分支用 if-else。写了几百行 demo 之后果断放弃了。原因不复杂。自研状态机的复杂度是失控的要么不支持循环导致 Agent 场景根本做不了要么加上循环之后状态回溯、断点恢复、调试可视化都要从零实现。而 LangGraph4j 把这些基础能力吃掉了有向图建模、条件边、固定步数循环、全局状态、编译后的图可以复用。LangGraph4j 另一层价值是“状态一旦跨节点流转天然就是可序列化的”这让工作流的断点续跑、人工审批介入、日志追踪都变得顺理成章。相比之下自研状态机做到这一步要付出的工时实在太大。2.3 低代码平台实测对比Dify / Coze / n8n 的启发即使决定自研也必须充分研究成熟产品尤其是 Coze 和 Dify 的节点设计。它们给我的启发很直接Coze 的节点分区很清晰大模型节点、知识库节点、代码节点、插件节点、逻辑节点条件/循环/变量。这启发我们把节点类型划分为“AI 类”“数据类”“集成类”“控制流类”而不是混在一个大类里。Dify 的工作流在“对话流”和“工作流”两种模式间做了明确区分。对话流适合 chatbot带多轮记忆工作流适合一次性的任务处理。参考这个我们的平台也把 Agent 应用拆成两种模式会话型 Agent 和任务型作业。前者挂载更多的交互节点后者更强调确定性执行。n8n 对我们的启发主要在产品层面错误重试机制、节点执行日志、Webhook 触发方式这些都要在低代码引擎里原生支持而不是等业务出问题再去补。3. 平台总体架构设计3.1 分层架构与模块职责整个平台我把它分成四层接入层、编排层、运行层、集成层。接入层负责各种触发方式。前端 SDK、OpenAPI、Webhook、定时任务、消息队列都能成为工作流的入口。一个工作流可以配置多触发执行时把外部输入统一解析为标准WorkflowRequest。编排层是低代码的核心。它包含可视化设计器、节点 Schema 注册中心、版本管理和发布管理。设计器产生的画布数据是 JSON编排层负责把这些 JSON 校验、编译成可执行的工作流定义对象再交给运行层。运行层基于 LangGraph4j 内核。工作流定义被翻译成 StateGraph节点映射到编译后的图节点条件分支映射为条件边。运行层还要处理并发控制、状态持久化、日志和指标采集。集成层解决“跟外部世界握手”的问题。内置一批 Tool 类型比如 HTTP 调用、数据库查询、对象存储、RAG 检索、消息推送。同时支持 external tool registry企业内部系统的 SDK 可以注册成标准工具让画布上的节点去调用。核心设计原则是“编排与运行分离”。设计、发布、运行三个状态互相独立一个正在运行的工作流不能因为画布编辑而中断。版本号就是这条链的粘合剂。3.2 前端可视化编排器的技术选型前端画布我选了 React Flow现名 xyflow。原因很简单开箱即用的拖拽、连线、缩放、小地图、自定义节点社区插件也丰富。架构上分三层组件展示层做各种业务节点比如 LLM 节点卡片、知识库节点卡片。每个节点组件只负责展示 Schema 上的配置项不存业务逻辑。状态管理层维护画布上的节点集合和边集。我用 Zustand 管理画布状态节点增删、连线拖拽、选中状态都是 O(1) 更新。转换层是最关键的。它负责把画布对象转换成WorkflowDefinition也就是后端可执行的 JSON。这里面要做 ID 映射、边校验、孤儿节点清理。前端画布中最容易忽视的是“模型不能直接消费 UI 状态”。用户拖出来一个节点UI 里的数据里有x、y坐标、宽度、颜色这些展示属性但后端运行时根本不在乎它们。所以转换层要把数据模型和视图模型分离画布存的是 ViewModel编译后只保留业务配置。3.3 后端服务模块的拆分后端按模块拆分每个模块清楚自己的边界workflow-api对外提供 REST API承担发布、触发、查询的入口。workflow-core定义 WorkflowDefinition、节点接口、运行时上下文、事件总线。workflow-engineLangGraph4j 的集成层负责图构建、状态管理、编译执行。workflow-toolkit内置工具集合比如 HTTP、SQL、RAG、邮箱等连接器。workflow-admin管理端服务负责设计器 CRUD、发布、权限不参与运行时执行。模块之间的依赖是单向的workflow-admin依赖workflow-api和workflow-coreworkflow-engine也依赖workflow-core。这样设计和执行互不污染。4. 工作流编排模型与数据格式4.1 节点类型体系节点类型是整个平台的地基。设计之初我就列了三类AI 类节点LLM 节点、Prompt 模板、RAG 节点、意图分类节点。这类节点的特点是依赖模型推理输出不可完全预期所以要有容错设计。逻辑类节点条件判断、循环、变量赋值、代码执行。这类节点是低代码工作流里的“粘合剂”让流程能干起来。集成类节点HTTP 请求、数据库操作、消息推送、AI 工具调用。这类节点负责跟外部系统打交道。每个节点由四部分组成唯一 ID、类型标识、输入参数 Schema、输出参数 Schema。节点定义写成 Jackson 友好的 POJO便于存库和前后端传递。4.2 WorkflowDefinition 数据结构工作流定义是整个平台的“源程序”。我用 JSON 形式存储整个核心对象如下{ workflowId: wf_rag_question, version: 12, name: 文档问答工作流, description: 基于知识库的问答流程, trigger: { type: api, path: /v1/workflows/wf_rag_question/execute }, nodes: [ { id: node_start, type: START, name: 开始, next: node_rag }, { id: node_rag, type: RAG, name: 知识库检索, config: { collection: product-docs, topK: 5 }, outputSelector: { query: input.query, documents: output.documents }, next: node_llm }, { id: node_llm, type: LLM, name: 生成回答, config: { model: qwen-max, temperature: 0.3, promptTemplateId: pt_doc_answer }, next: node_end }, { id: node_end, type: END, name: 结束 } ] }这个 JSON 有两点很讲究。一是每个节点输出的字段必须显式声明outputSelector也就是告诉引擎“这个节点的输出字段要写入全局状态里的哪个位置”。这避免了节点之间隐式依赖让任意节点可以读取任意前置节点写入的字段。二是节点之间用next字段线性连接但遇到 CONDITIONS 节点时next被替换成routes数组引擎根据路由规则选择下一条边。4.3 条件路由与循环语义条件路由是低代码平台的灵魂。我的设计参考了 n8n 的规则表达式方式每个路由分支带着一个表达式表达式运行在安全沙箱里只允许访问状态中的字段和少量工具函数。{ id: node_condition, type: CONDITION, name: 判断是否有相关知识, config: { conditions: [ { rule: {{documents.size 0}}, target: node_llm }, { rule: default, target: node_fallback } ] } }循环用 LangGraph4j 的条件边原生支持。一个“多轮工具调用循环”的语义是这样的Agent 节点执行一次推理如果返回结果里包含工具调用则路由到工具节点工具节点执行结束再回到 Agent 节点如果推理结果不含工具调用则走向 END。在 LangGraph4j 里这就是简单但强大的 conditional edge。这里我给个警告无界循环必须控制。Low-code 平台很容易让业务人员配出死循环我的做法是在引擎层加上最大执行步数限制默认 50 步超过直接中止并标记为失败。必要的时候工作流定义里也可以单独配置步数上限。5. LangGraph4j 内核集成实战5.1 引入依赖与 Maven 配置LangGraph4j 目前是独立于 LangChain4j 的 Maven 坐标我用的版本是 1.x支持 JDK 17 以上底层依赖 LangChain4j 的 ChatLanguageModel。dependency groupIdorg.bsc.langgraph4j/groupId artifactIdlanggraph4j-core/artifactId version1.7.0/version /dependency dependency groupIdorg.bsc.langgraph4j/groupId artifactIdlanggraph4j-langchain4j/artifactId version1.7.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version1.0.0/version /dependency值得说明的是langgraph4j-langchain4j这个模块。它提供了 LangChain4j 到 LangGraph4j 的适配层核心是把ChatLanguageModel封装成可用的 Agent 节点组件省去很多胶水代码。5.2 图构建与节点注册机制LangGraph4j 的核心 API 是StateGraph。它有一个泛型 State表示整个工作流的共享状态。在我们的低代码平台里State 必须是一个通用的 Map 结构因为低代码编排的节点类型是动态的运行时无法知道节点会往状态里写什么字段。我先定义一个WorkflowStatepublic class WorkflowState { private MapString, Object data new HashMap(); public MapString, Object getData() { return data; } public void setData(MapString, Object data) { this.data data; } }然后让每个低代码节点的执行逻辑变成一个NodeActionpublic interface NodeAction { MapString, Object execute(NodeContext context, MapString, Object input); }NodeContext包含工作流 ID、执行 ID、节点配置、模型实例、工具注册表等运行时信息。input就是经过outputSelector映射后的输入字段。返回的MapString, Object会被引擎自动写入全局状态完成一次节点执行的数据流转。图构建的核心方法如下StateGraphWorkflowState graph new StateGraph(WorkflowState.STATE_SCHEMA); // 为每个低代码节点注册一个 LangGraph4j 节点 for (WorkflowNode node : definition.getNodes()) { switch (node.getType()) { case LLM - graph.addNode(node.getId(), new WrappedNodeAction(llmNodeAction(node))); case RAG - graph.addNode(node.getId(), new WrappedNodeAction(ragNodeAction(node))); case HTTP - graph.addNode(node.getId(), new WrappedNodeAction(httpNodeAction(node))); case CONDITION - graph.addNode(node.getId(), new WrappedNodeAction(conditionNodeAction(node))); // ... } }Mapper 机制也很重要。LangGraph4j 的addNode(String id, NodeActionWorkflowState action)里NodeAction只有一个方法apply(WorkflowState state)。这意味着在真正的 action 里我需要从 state 的数据 Map 里取出该节点的输入字段序列。这就是序列化设置的关键设计。5.3 条件边与 Agent 循环的实现LangGraph4j 的条件边是用addConditionalEdges(String sourceId, ConditionalEdgesWorkflowState conditionalEdges)来完成的。我需要把低代码节点的路由配置转换成条件边的判断逻辑// 注册条件路由 for (RouteConfig route : conditionNode.getRoutes()) { // routes 里的每个分支生成一个条件判断 } graph.addConditionalEdges(node_condition, state - { MapString, Object data state.getData(); for (RouteConfig route : conditionNode.getRoutes()) { if (default.equals(route.getRule())) { // 记录默认分支 } else if (MvelEvaluator.eval(route.getRule(), data)) { return route.getTarget(); } } return defaultTarget; });这里我踩过一个坑条件表达式的执行不能放在 LangGraph4j 的边判断逻辑里直接用SpelExpressionParser解析。原因是 Java 的 Spring EL 表达式在解析 Map 字段访问时容易出错而且安全沙箱难做。最后我选了 MVEL 作为表达式引擎它可以无反射地访问 Map 字段速度也快得多。Agent 循环的实现在 LangGraph4j 里非常典雅。用一个isThereMoreTools条件边控制graph.addNode(agent, new ReActAgentNode(model, tools)); graph.addNode(tools, new ToolExecutionNode(toolRegistry)); graph.addConditionalEdges(agent, state - { if (stateHasToolCalls(state)) { return tools; // 回到工具节点继续执行 } return end; // 没有工具调用则结束 }); graph.addEdge(tools, agent); // 工具执行完回到 agent 节点再做一轮推理这样得到的图天然支持多轮工具调用而且不需要自己维护 while 循环。这是 LangGraph4j 对比自制状态机最大的优势循环是图结构的一等公民。6. 可视化编排器与低代码设计6.1 节点注册表机制低代码平台的“低”体现在业务人员能通过配置完成搭建但这背后需要一套强类型的元数据体系。我的设计是给每个节点类型搞一个节点注册表NodeSchemaRegistry。public record NodeSchema( String type, String displayName, String category, ListPropertySchema properties, ListOutputSchema outputs ) {} public record PropertySchema( String key, String label, String type, // string | number | boolean | enum | expression | prompt MapString, Object options, boolean required, String defaultValue ) {}前端设计器打开画布时会拿到一份完整的节点 Schema 列表。每拖一个节点到画布上自动生成一个表单卡片。用户填写的字段直接绑定到节点的config属性里。这个注册表机制让平台扩展节点类型变成一件很干净的事后端新增一个NodeAction实现注册一个新的NodeSchema前端立刻就能用不需要改一个前端包。6.2 属性面板与合法性校验属性面板是用户配置节点的唯一入口。它最核心的体验点在于“表达式提示”。比如 LLM 节点的 Prompt 模板里用户可以引用上游节点的输出我们会提供一个字段选择器实际上是从outputs元数据里读取字段列表。合法性的校验纬度有三个字段必填校验LLM 节点的模型字段没选扔出可读的错误信息。类型校验有些配置项是枚举比如 RAG 检索策略必须是固定值里的一项。连通性校验从 START 到 END 之间不能有不可达节点或者悬空边。如果用户把一条边连到了已删除的节点编译时直接报错并高亮画布中的问题节点。6.3 画布到可执行图的转换管线设计器产生的画布数据是 ViewModel后端要执行的是 WorkflowDefinition。中间的转换管线是这样的第一步前端把画布上的节点提取成CanvasNode[]边提取成CanvasEdge[]。第二步对每个 CanvasNode 执行 Schema 解析。我们不用前端逐个字段翻译而是用一个通用转换器读取节点组件上绑定的schemaType属性然后把表单上产生的整个配置对象作为config字段塞进节点定义里。第三步边的转换。一条画布边代表的含义取决于源节点的类型。如果源节点是条件节点边会被收集进 routes 数组其他类型的边直接映射为next字段。第四步在编译执行前做图完整性检查。这块我在后端做了一次因为依赖可信的 Service 层校验不能只看前端。整个管线最抽象的部分是第三步。条件节点有多条出边每条件分支都有对应的 target。普通节点理论上只有一条出边但为了用户操作方便画布上也可以允许抛出多条边编译时如果发现一条普通节点出现多条出边直接报错。7. 企业级能力与运营支撑7.1 多租户与权限隔离低代码平台一旦让人人都能编排工作流隔离就成了第一优先级。我的做法是四层隔离租户级租户只能看到自己创建工作流和工具。项目级同租户下多个项目资源可以跨项目共享也可以隔离通过资源 ID 加作用域标记。角色级区分查看者、编辑者、发布者、管理员操作权限各有边界。调用级运行时的工作流调用需要校验调用方凭证防止越权触发别人的工作流。7.2 模型路由与成本控制平台天然支持多模型配置。比如 RAG 生成节点可以用 qwen-max代码生成节点可以用 gpt-4o。每个模型实例在配置中心里可以预设一个成本告警阈值超出后自动熔断。LangChain4j 提供了ChatLanguageModel接口我们在实现里包了一层带计数功能的CostTrackingChatModel每个工作流执行完毕后日志里会记录 Token 消耗和预估成本。7.3 运行追踪与可观测性传统 API 的日志一条一条但工作流的追踪需要一条链路串起来。我设计了一个ExecutionTrace模型每个工作流执行有唯一的executionId每个节点的执行产生一条 span。span 里带着父节点 ID、输入摘要、输出摘要、耗时、错误信息。前端监控面板通过 WebSocket 实时推送执行状态。用户在画布上可以直接看到每个节点的执行状态变成绿色、黄色或者红色并阅读具体的报错信息。这个体验远比查日志舒服。实现上LangGraph4j 有StateGraph的执行监听器可以在节点完成时回调换回我们的追踪模型。8. 踩坑记录与排查方案8.1 类型擦除引发的节点执行结果丢失LangGraph4j 在内部会维护 State 的序列化但我第一次在业务代码里写节点时直接在NodeAction里返回一个范型MapString, Object然后引擎那边的Jackson反序列化又收到了ArrayList之类的具体类型。这个问题的根因是 Java 泛型在运行时的类型擦除。解决方案是显式定义WorkflowState里 data 字段的元素类型并且在执行引擎入口统一清理类型。8.2 Agent 循环失控给业务人员用的 Agent 节点工具调用经常出现循环不收敛的情况。模型总认为“我还能再调一个工具”。我发现这个问题的概率不低尤其在开放工具集比较宽泛的场景。方案有两个第一引擎强制最大步数这属于兜底第二Agent 节点里给模型塞“可用工具描述”时把工具的调用前提写得更加严格减少多轮递归。8.3 工具执行超时与幂等处理工作流里如果接上 HTTP 工具必须处理外部接口的慢调用和宕机。我为工具节点加了三种模式同步阻塞、超时降级、重试补偿。同步容易做超时降级需要节点 Schema 支持 fallback。比如 HTTP 工具失败后可以跳到兜底节点也可以返回固定的降级文本这取决于业务方配置。幂等性则要求在工具节点设计时把请求 ID 作为业务参数传入外部服务要能识别并去重否则重试操作会把一件业务事件执行成两遍。8.4 多线程执行上下文丢失LangGraph4j 是单线程模型但节点里的业务代码经常需要并发调用外部 API。线程池里的代码想读取工作流的上下文比如租户 ID、调用方信息容易拿不到。解法是把 WorkflowState 设计成ThreadLocalcopy语义工作流开始时设置上下文节点内部需要并发时先拷贝一份上下文再分发到子线程。9. 从实践中得到的几点体会这个项目做到后期我最大的体会是低代码平台真正的门槛不在画布交互而在运行时设计。画布做得再炫如果引擎层把工作流一次跑不活、调不稳一切等于零。LangChain4j 和 LangGraph4j 的组合是很合适的 Java 技术底座。LangChain4j 的模型抽象和工具体系足够成熟LangGraph4j 的图执行能力撑起了循环、分支、状态流转这些核心语义。两者配合起来刚好覆盖了低代码 Agent 平台从“节点能力”到“编排能力”的完整链路。还有一点想分享不要一上来就追求大而全的节点类型。我们平台第一个版本只有五个节点类型——开始、结束、LLM、HTTP、条件。先把这五类打通跑通一个完整的“请求→调用大模型→调外部接口→条件判断→输出”链路之后再去加 RAG、代码执行、循环这些进阶节点节奏会稳很多。这个方向后续还可以继续扩展工作流版本对比与 A/B 测试、Agent 自我反思的图模式库、更细粒度的 Token 成本审计报表。每一步都是在现有架构上增量生长这也正是这类平台设计的价值所在。