先聊个现象。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-engine:LangGraph4j 的集成层,负责图构建、状态管理、编译执行。workflow-toolkit:内置工具集合,比如 HTTP、SQL、RAG、邮箱等连接器。workflow-admin:管理端服务,负责设计器 CRUD、发布、权限,不参与运行时执行。
模块之间的依赖是单向的:workflow-admin依赖workflow-api和workflow-core,workflow-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> <groupId>org.bsc.langgraph4j</groupId> <artifactId>langgraph4j-core</artifactId> <version>1.7.0</version> </dependency> <dependency> <groupId>org.bsc.langgraph4j</groupId> <artifactId>langgraph4j-langchain4j</artifactId> <version>1.7.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>1.0.0</version> </dependency>值得说明的是langgraph4j-langchain4j这个模块。它提供了 LangChain4j 到 LangGraph4j 的适配层,核心是把ChatLanguageModel封装成可用的 Agent 节点组件,省去很多胶水代码。
5.2 图构建与节点注册机制
LangGraph4j 的核心 API 是StateGraph。它有一个泛型 State,表示整个工作流的共享状态。在我们的低代码平台里,State 必须是一个通用的 Map 结构,因为低代码编排的节点类型是动态的,运行时无法知道节点会往状态里写什么字段。
我先定义一个WorkflowState:
public class WorkflowState { private Map<String, Object> data = new HashMap<>(); public Map<String, Object> getData() { return data; } public void setData(Map<String, Object> data) { this.data = data; } }然后让每个低代码节点的执行逻辑变成一个NodeAction:
public interface NodeAction { Map<String, Object> execute(NodeContext context, Map<String, Object> input); }NodeContext包含工作流 ID、执行 ID、节点配置、模型实例、工具注册表等运行时信息。input就是经过outputSelector映射后的输入字段。返回的Map<String, Object>会被引擎自动写入全局状态,完成一次节点执行的数据流转。
图构建的核心方法如下:
StateGraph<WorkflowState> 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, NodeAction<WorkflowState> action)里,NodeAction只有一个方法apply(WorkflowState state)。这意味着在真正的 action 里,我需要从 state 的数据 Map 里取出该节点的输入字段序列。这就是序列化设置的关键设计。
5.3 条件边与 Agent 循环的实现
LangGraph4j 的条件边是用addConditionalEdges(String sourceId, ConditionalEdges<WorkflowState> conditionalEdges)来完成的。我需要把低代码节点的路由配置转换成条件边的判断逻辑:
// 注册条件路由 for (RouteConfig route : conditionNode.getRoutes()) { // routes 里的每个分支生成一个条件判断 } graph.addConditionalEdges("node_condition", state -> { Map<String, 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, List<PropertySchema> properties, List<OutputSchema> outputs ) {} public record PropertySchema( String key, String label, String type, // string | number | boolean | enum | expression | prompt Map<String, 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里返回一个范型Map<String, Object>,然后引擎那边的Jackson反序列化又收到了ArrayList之类的具体类型。
这个问题的根因是 Java 泛型在运行时的类型擦除。解决方案是显式定义WorkflowState里 data 字段的元素类型,并且在执行引擎入口统一清理类型。
8.2 Agent 循环失控
给业务人员用的 Agent 节点,工具调用经常出现循环不收敛的情况。模型总认为“我还能再调一个工具”。我发现这个问题的概率不低,尤其在开放工具集比较宽泛的场景。
方案有两个:第一,引擎强制最大步数,这属于兜底;第二,Agent 节点里给模型塞“可用工具描述”时,把工具的调用前提写得更加严格,减少多轮递归。
8.3 工具执行超时与幂等处理
工作流里如果接上 HTTP 工具,必须处理外部接口的慢调用和宕机。我为工具节点加了三种模式:同步阻塞、超时降级、重试补偿。
同步容易做,超时降级需要节点 Schema 支持 fallback。比如 HTTP 工具失败后,可以跳到兜底节点,也可以返回固定的降级文本,这取决于业务方配置。幂等性则要求在工具节点设计时,把请求 ID 作为业务参数传入,外部服务要能识别并去重,否则重试操作会把一件业务事件执行成两遍。
8.4 多线程执行上下文丢失
LangGraph4j 是单线程模型,但节点里的业务代码经常需要并发调用外部 API。线程池里的代码想读取工作流的上下文(比如租户 ID、调用方信息),容易拿不到。解法是把 WorkflowState 设计成ThreadLocal+copy语义:工作流开始时设置上下文,节点内部需要并发时先拷贝一份上下文再分发到子线程。
9. 从实践中得到的几点体会
这个项目做到后期,我最大的体会是:低代码平台真正的门槛不在画布交互,而在运行时设计。画布做得再炫,如果引擎层把工作流一次跑不活、调不稳,一切等于零。
LangChain4j 和 LangGraph4j 的组合是很合适的 Java 技术底座。LangChain4j 的模型抽象和工具体系足够成熟,LangGraph4j 的图执行能力撑起了循环、分支、状态流转这些核心语义。两者配合起来,刚好覆盖了低代码 Agent 平台从“节点能力”到“编排能力”的完整链路。
还有一点想分享:不要一上来就追求大而全的节点类型。我们平台第一个版本只有五个节点类型——开始、结束、LLM、HTTP、条件。先把这五类打通,跑通一个完整的“请求→调用大模型→调外部接口→条件判断→输出”链路之后,再去加 RAG、代码执行、循环这些进阶节点,节奏会稳很多。
这个方向后续还可以继续扩展:工作流版本对比与 A/B 测试、Agent 自我反思的图模式库、更细粒度的 Token 成本审计报表。每一步都是在现有架构上增量生长,这也正是这类平台设计的价值所在。