尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Java低代码AI智能体平台:LangChain4j与LangGraph4j架构实践

发布时间:2026/9/28 15:39:36

资讯中心
01
ARTICLE

Java低代码AI智能体平台:LangChain4j与LangGraph4j架构实践

Java低代码AI智能体平台:LangChain4j与LangGraph4j架构实践
做 Java 技术栈的 AI 智能体平台LangChain4j 和 LangGraph4j 是绕不开的两个名字。前者把 LLM 调用、工具调用、RAG 这些碎片能力封装成统一的 Java API后者把智能体最常见的状态机、分支、循环落实成一张可执行的图。我最近用这两个库做了一版低代码工作流通用智能体平台的架构设计目标很直接让业务人员能在画布上拖出流程让开发人员能往里接自己的工具和模型而不是每个项目都从零手搓一套 Agent 框架。这篇文章不聊 PPT 架构就聊我在分层、DSL 设计、图执行和 RAG 工程化上踩过的一些坑以及最后沉淀下来的实现路径。这个平台不是要再造一个 Coze 或者 Dify虽然它们的产品形态值得参考。我们的场景是 Java 技术栈的存量业务系统已经有用户体系、权限、数据库、消息队列缺的是一个能把 LLM 编排进业务流程的“中间层”。低代码工作流是这个中间层的外壳LangChain4j 和 LangGraph4j 是内核。如果你也在做类似的事情这篇文章应该能帮你少走不少弯路。1. 为什么要在 Java 生态里做低代码智能体平台1.1 通用智能体平台到底解决什么问题先说个反直觉的结论智能体平台最大的难点不是让大模型“聪明”而是让大模型“听话地接入业务系统”。我见过太多项目把 Agent 写在业务代码里某个 Service 里调一次 LLM写死一个 prompt再用 JsonPath 扒结果。单个需求这么做没问题一旦需求多起来每个接口都变成“隐形的 AI 烟囱”没法复用、没法观测、没法给业务人员调整。低代码工作流平台要解决的就是把这段隐形的胶水代码变成可视化画布上一张可保存、可版本化、可审计的图。这里的“通用”有两层含义第一模型无关可以随时切换 OpenAI、通义、DeepSeek 或者本地部署的模型第二工具无关平台提供一个工具注册中心业务系统的接口通过标准方式挂进来LLM 可以调用人工节点也可以调用。有了这两个通用性工作流才能真正脱离某个具体项目变成公司内部的通用能力。在这个框架下LangChain4j 负责“模型和工具的底座”LangGraph4j 负责“图的运行”低代码层负责“把 JSON 变成图”。三者各管一段边界很清楚。1.2 选型对比LangChain4j、Spring AI、LangGraph4j 怎么各司其职我在选型时专门纠结过一个问题到底用 Spring AI 还是 LangGraph4j这也是网上被问得很多的问题。简单说Spring AI 是 Spring 官方出的 LLM 集成库和 Spring Boot 生态融合得非常好做简单的 Chat 调用、结构化输出、RAG 都够用代码也比 LangChain4j 更“Spring 味”。但它目前的重心还是偏向模型接入和 AI 应用脚手架复杂的图编排、条件分支、人工暂停恢复这类能力要么没有要么需要自己写很多胶水。LangGraph4j 则是把 LangGraph 的设计搬到了 Java。它有明确的 StateGraph 模型状态、节点、边、条件边、循环、子图、checkpoint。这些恰恰是工作流引擎需要的底层原语。比如“如果用户意图是退款走退款工具节点否则走人工客服节点”这种逻辑用 LangGraph4j 的条件边写起来非常自然。所以我的结论不是二选一而是组合LangChain4j 做模型抽象、工具调用、RAG、记忆LangGraph4j 做图执行、状态管理、断点恢复Spring AI 不参与核心运行时只在不依赖图编排的简单场景里用。三者没有谁替代谁各有分工。维度LangChain4jSpring AILangGraph4j定位LLM 应用通用 SDKSpring 官方 AI 脚手架图编排与智能体运行时模型接入多种模型 Provider多种模型 Provider本身不关心配合上层使用工具调用Tool 注解成熟支持较简洁不做授权给 LangChain4j复杂编排弱需要自己写弱核心能力断点/恢复无无checkpoint 原生支持与 Spring 集成手动装配开箱即用手动装配这里有一个很实际的建议如果团队 Java 经验一般只想快速做一个带对话和 RAG 的内部工具Spring AI 就够了如果要做低代码工作流平台LangGraph4j 几乎是绕不开的。两者差异不在“谁更强”而在“你需不需要一张可执行的状态图”。2. 平台整体架构四层模型与核心模块2.1 接入、编排、运行时、智能体的四层怎么切我最终把平台拆成了四层每一层只依赖下一层暴露的接口禁止跨层调用。这不是为了架构图好看而是为了后续能分别替换模型、替换工具、替换前端。接入层提供低代码画布的编辑器服务、OpenAPI 开放接口、定时触发和 Webhook 入口。这一层只做“接收事件”和“返回状态”不碰业务逻辑。编排层负责工作流的定义、DSL 解析与校验、节点 Schema 管理、版本发布。数据都存在数据库中画布前端只和这一层对话。运行时把编排层产生的 DSL 编译成可执行的图执行节点、维护状态、派发事件、处理人工任务。这一层是 LangGraph4j 的主场。智能体层封装模型网关、工具注册中心、记忆服务、RAG 服务同时定义 Agent 的调用协议。运行时通过标准接口调用它不必关心底层是哪个模型。举个例子前端拖好一个节点保存时调用编排层接口编排层把节点 JSON 存库并生成一个 DSL 快照点击发布后运行时把 DSL 加载进来编译成 StateGraph任务触发后图中某个节点调用智能体层请求模型做一次意图分类。整个过程里接入层不知道 LangGraph4j 的存在智能体层不知道 DSL 长什么样数据边界非常干净。2.2 每一层的关键职责与数据边界分层容易边界难。我在实际设计里给每一层定了几条硬规矩编排层不允许发起模型调用。哪怕只是“用 LLM 帮用户校验一下 DSL 配置是否合理”也不能在编排层调必须显式创建一个“辅助节点”走运行时。为什么因为一旦编排层开始隐式调模型你很难统计整个工作流到底消耗了多少 token也容易在保存时出现意外的外部依赖。运行时不允许直接访问业务数据库。节点要查业务数据必须通过工具节点调用业务系统提供的接口。这样做的原因是权限控制可以统一收敛在工具注册中心图引擎只做编排不做数据访问。很多自研工作流平台栽在这里图引擎写得自由节点里塞了一堆 SQL最后权限、审计全失控。智能体层的工具注册中心是唯一允许挂接业务逻辑的地方。每个工具声明自己的入参、出参、权限标签、是否写操作、超时时间。运行时执行工具节点时只要按 ID 找到工具定义填充参数调用返回结果。这个设计让“低代码”真正落到“可配置”而不是“改代码”。2.3 低代码编辑器和运行时如何解耦很多低代码平台一上来就做画布结果被拖拽、连线、撤销重做这些前端细节拖死。我的建议是编辑器只是一个“DSL 生成器”它不感知运行时。画布产生的数据分两块。第一块是“运行时元数据”包括节点 id、节点类型、配置参数、连线、条件分支第二块是“编辑器元数据”包括节点坐标、颜色、分组、折叠状态。保存时编辑器元数据单独放一个字段运行时永远不读它。这样做的好处是你用任意前端框架重写画布只要还能输出相同的 DSL平台就不受影响。解耦之后DSL 的版本管理变得非常重要。每次发布生成一个不可变的版本快照运行中的实例绑定发布时的版本不允许热更新。否则业务人员改一下流程正在跑的工单流程就会跳到新逻辑责任完全分不清。这个坑我踩得很深后来所有执行上下文都存了version字段排查问题时才真正有据可依。3. 工作流引擎核心从 DSL 到 StateGraph 的落地3.1 DSL 设计一份可保存、可校验、可版本化的图描述工作流的 DSL 我选的是 JSON不用 YAML。原因很简单前端画布天然操作 JSONJSON Schema 可以做强校验而且 LangChain4j 和 Spring 生态对 JSON 的处理都最顺。一份最简 DSL 大概是这样的结构{ id: wf_ticket, version: 1, entryNodeId: start, nodes: [ { id: start, type: start, config: {} }, { id: llm_ticket_category, type: llm, config: { model: default, promptTemplate: 判断工单类别只返回 refund/query/complaint 之一, outputKey: category } }, { id: condition_category, type: condition, config: { expression: state.category refund } } ], edges: [ { from: start, to: llm_ticket_category }, { from: llm_ticket_category, to: condition_category }, { from: condition_category, to: refund_tool, condition: refund }, { from: condition_category, to: human_review, condition: default } ] }这个 DSL 看起来简单但背后要设计好三个点。第一节点配置里的promptTemplate是模板而不是最终 prompt运行时需要用状态变量渲染它这样同一个节点可以复用到不同工作流。第二所有的表达式中引用的状态字段必须在 DSL 校验阶段就能在状态 Schema 中找到否则直接判为非法。第三画布坐标字段不要放在节点里单独放在editorData字段运行时解析时直接丢弃保持图结构干净。3.2 节点类型定义与执行语义工作流节点的类型决定了平台能力的上限。我第一版只需要 LLM 节点和工具节点后来逐步补全最终沉淀了一套可扩展的节点协议。节点类型执行语义关键配置start / end流程入口和出口入参定义、超时llm调用模型执行 prompt 模板输出结构化内容model、promptTemplate、temperature、outputSchematool调用工具注册中心里的某个工具toolId、入参映射、重试策略condition根据状态值决定下一跳走向expression、目标边映射http请求外部 HTTP 接口url、method、headers、bodyTemplatescript执行一段隔离的脚本做数据转换language、code、输入输出声明human暂停流程等待人工审批/录入assignee、待办表单定义、超时处理subgraph调用另一个已发布的工作流workflowId、version、入参映射每个节点类型都必须实现相同的接口输入是当前状态快照输出是一个局部变更集合由运行时合并到全量状态中。注意这里的关键设计节点不直接改状态而是返回变更集合。这样同一个节点可以并发执行也能在失败时准确重放。工具节点尤其要小心不是所有业务接口都适合让 LLM 直接调用。我要求工具注册中心在注册时显式声明“幂等性”和“危险等级”写操作默认需要一个确认开关。比如“删除客户订单”这类工具如果没有人工确认绝对不能放进自动分支里让模型决定。3.3 把 DSL 翻译成 StateGraph 执行器拿到 DSL 之后运行时要做一次“编译”先校验图结构包括节点引用完整性、边的目标是否存在、是否有环、是否有死路然后再把节点 JSON 翻译成 LangGraph4j 的 StateGraph 节点。示意代码如下StateGraphWorkflowState graph new StateGraph(WorkflowState::new); for (NodeDef nodeDef : dsl.getNodes()) { switch (nodeDef.getType()) { case LLM - graph.addNode(nodeDef.getId(), ctx - { WorkflowState state ctx.state(); String output llmService.complete(nodeDef.getPrompt(), state); return Map.of(lastLlmOutput, output); }); case TOOL - graph.addNode(nodeDef.getId(), ctx - { ToolResponse resp toolCenter.invoke(nodeDef.getToolId(), resolveInputs(nodeDef.getInputMapping(), ctx.state())); return Map.of(lastToolResult, resp.getData()); }); case CONDITION - graph.addNode(nodeDef.getId(), ctx - { // 条件节点的“判断”实际发生在边选择上这里只做状态透传 return Map.of(); }); } } for (EdgeDef edgeDef : dsl.getEdges()) { if (edgeDef.getCondition() null) { graph.addEdge(edgeDef.getFrom(), edgeDef.getTo()); } else { graph.addConditionalEdge(edgeDef.getFrom(), state - evaluateCondition(edgeDef.getCondition(), state), Map.of(true, edgeDef.getTo())); } } CompiledGraphWorkflowState compiled graph.compile();这个翻译层是整个平台里最容易出 bug 的地方尤其是条件边。我踩过的坑是条件表达式基于状态值求值时如果状态里字段还不存在必须返回一个显式的“走默认分支”结果而不是抛异常。否则模型节点输出格式稍微变一点点整个图就卡在条件边上排障时非常难查。3.4 状态传递、分支与循环的细节LangGraph4j 的核心是状态图但状态怎么定义直接决定工作流能跑得多复杂。我的经验是状态对象必须区分为input、output、intermediate三类字段中间结果默认不进入最终上下文。比如工单分类流程模型输出的category是中间结果后续条件边要用它但最终对外暴露的响应只需要result。如果不做区分中间结果会全塞进响应里既不安全token 浪费也严重。循环是另一个容易失控的地方。低代码工作流里“让模型反复优化直到满足条件”是刚需但如果没有循环上限模型一次抽风就可能跑几十轮。我在 DSL 编译阶段就给所有回边自动附加MAX_LOOPS限制运行时在状态里维护每个节点的循环计数超过阈值就强制走兜底分支。这个兜底分支必须是人工节点或者预设结果不能直接让流程失败。状态对象我采用“不可变快照 变更合并”的模式。每个节点执行前拿到的是一个只读状态执行后返回局部变更集合由运行时统一合并成新状态。这样设计的好处是图引擎能精确回答“某个节点执行后到底改了什么”便于审计也便于 checkpoint 持久化。3.5 checkpoint 是实现断点续跑的基础LangGraph4j 支持 checkpoint也就是在节点执行前后保存状态快照。低代码平台里的人工审批、失败重试、版本回滚全依赖这个能力。我实现的 checkpoint 保存到数据库而不是只放内存。原因是平台节点可能执行很久服务一重启状态就丢了会很难交代。保存内容包括工作流实例 ID、当前节点 ID、状态快照、循环计数、已执行节点列表。每次节点执行完成后写一条 checkpoints 记录。这样做的代价是每次节点切换都多一次数据库写入。我的优化方案是只在“条件节点前、人工节点前、工具节点前”强制写入 checkpoint普通 LLM 节点之间不落库靠运行时内存保存。如果发生崩溃从最近一个 checkpoint 恢复最多重复执行一小段可接受。4. 智能体层工具调用、记忆与 RAG 的工程化4.1 让 LLM 安全调用平台工具LangChain4j 的 Tool 注解用起来很爽直接在方法上标注描述和参数说明模型就能自动生成 JSON 参数调用。但低代码平台不能把工具写死在代码里否则业务方加一个工具还要发版本。我的做法是做一个动态工具注册中心。数据库里一张工具定义表字段大致是toolId、name、description、methodRef、参数 Schema、超时、幂等标记、权限标签。运行时启动时根据 methodRef 用反射包装成 LangChain4j 需要的 ToolSpecification再放到一个并发安全的 Map 里。ToolSpecification spec ToolSpecification.builder() .name(tool.getToolId()) .description(tool.getDescription()) .parameters(...) .build();动态注册真正难的不是反射包装而是“给模型提供哪些工具”的过滤逻辑。我见过一个事故模型在工具列表里看到一个批量删除接口参数都生成好了要不是权限校验拦下来差点把测试数据清空。后来我强制要求每个工作流实例在创建时显式声明可用工具集合LLM 节点只能在这个集合内选择工具。宁可配置麻烦不能把全量工具暴露给模型。4.2 记忆管理会话记忆与长期记忆的分层工作流平台里的记忆比单轮 Chatbot 复杂得多。我把记忆拆成三层第一层是节点内的局部上下文。一个 LLM 节点的 prompt 模板里引用的变量只随当前状态注入节点结束即释放。第二层是工作流实例的会话记忆。比如多轮对话式工作流用户和模型连续交互每次模型节点执行后把消息追加到上下文中但只保留最近 N 轮防止 token 膨胀。第三层是长期记忆。比如客户的历史偏好、历史工单记录通过 RAG 查询注入。关键坑是不要把整个会话记忆塞进 LangGraph4j 的 State 然后一路带着跑。State 是用来做图流转的里面应该只放结构化的业务字段人机对话的上下文消息列表应该单独放记忆服务里。我一开始把 messages 字段放进 State结果每次条件判断都要序列化一大段文本性能奇差排查问题也费劲。4.3 RAG 多路召回和 LangChain4j 默认 RRF 的坑RAG 做多了会发现单路向量检索在真实业务里不够用。用户搜“上个月订单退款没到账”向量库可能召回语义相近但错误的文档而关键词检索能命中“退款”“到账”这些字面词。正确的做法是多路召回再融合。LangChain4j 提供了 RRFReciprocal Rank Fusion融合器思路是把多路召回结果按排名加权合并公式是每个文档得分累加1 / (k rank)k 通常取 60。但网上讨论很多的一个坑是默认 RRF 实现在去重上存在缺陷。如果同一个文档出现在多路召回结果里它可能被当成不同条目导致融合时同一个文档被重复计分排名虚高甚至重复返回给大模型。这在工程上是不能接受的——大模型拿到两段一模一样的上下文容易产生错误结论。我修复的方式是重写融合逻辑先按文档唯一键去重再算分。示意代码如下MapString, ScoredDocument dedup new HashMap(); MapString, Double scoreMap new HashMap(); for (ListScoredDocument recallList : reranker.getRecallLists()) { for (int rank 0; rank recallList.size(); rank) { ScoredDocument doc recallList.get(rank); String key doc.documentId(); dedup.putIfAbsent(key, doc); scoreMap.merge(key, 1.0 / (60 rank 1), Double::sum); } } ListScoredDocument merged scoreMap.entrySet().stream() .sorted(Map.Entry.String, DoublecomparingByValue().reversed()) .limit(topK) .map(e - dedup.get(e.getKey())) .toList();这段代码看起来不复杂但实际排查花了我两天。所以提醒所有做 Java RAG 的朋友接 LangChain4j 的默认融合器之前先看一眼它的去重实现尤其是你们做多路召回时同一批文档源之间重叠率很高的话这个坑必踩。5. 运行时的稳定性和可观测性这些坑必须提前填5.1 幂等、超时与重试工作流编排看着像写业务代码但运行时的稳定性要求比普通接口高得多。一个工作流实例从触发到结束中间可能经过模型节点、外部 HTTP 节点、人工审批节点任何一个环节出问题都要能定位、重试、甚至整体回滚。第一件事是幂等。每个工作流实例有全局唯一的 executionId所有节点执行都带上这个 ID。工具节点调用业务系统时把 executionId 透传给下游下游可以按它做去重重复重试同一个工具调用时不能重复扣款、重复发通知。如果工具本身不支持幂等我宁愿在工具定义里标记为“禁止自动重试”留给人工处理。第二件事是超时。LLM 节点默认超时设置成 30 秒工具节点根据业务接口情况单独配置绝对不能统一超时。我踩过的坑是某个报表接口平均要跑 90 秒被统一超时策略卡在 30 秒重试两次之后系统直接堆积了几千条告警。后来每个节点都独立配置 timeout甚至允许覆盖全局默认值。第三件事是降级。平台要支持“模型不存在、模型超时、模型返回格式非法”三类降级策略。我在模型网关里做了配置LLM 节点失败后可以选择走人工节点、走规则节点、或者用上一次成功结果兜底。低代码的价值就在这里运维人员能在画布上直接改流程而不是半夜找开发改代码。5.2 人工审批节点如何不阻塞线程池这是低代码工作流平台最容易犯的架构错误。很多人第一次实现人工审批节点时很自然地想到“线程 sleep 到审批完成再继续”。这在 Demo 阶段没问题一上生产就完蛋——线程池被卡死工作流实例一多整个运行时直接瘫痪。我的做法是事件驱动暂停与恢复。图执行到 human 节点时不执行任何阻塞逻辑而是把当前节点和状态快照保存到数据库把执行状态置为WAITING_HUMAN然后直接结束这次图执行。前端待办列表展示审批任务审批人提交后审批接口校验权限更新任务状态再向运行时队列发送一个恢复事件。运行时代码收到事件后从 checkpoint 恢复状态构建一个新的图执行上下文从 human 节点的后续边继续跑。这个设计还有额外的好处人工审批期间工作流实例不占线程、不占内存服务可以随便重启审批记录天然留存审计也方便。代价是实现复杂度上了一个台阶但这是生产级平台绕不开的。5.3 图执行的可观测性低代码平台的排障和普通接口排障不一样问题往往不在某一行代码而在“图里某个节点为什么走到了不是预期的那条边”。所以节点级可观测性是刚需。我在运行时给每一次节点执行写 trace 记录字段包括executionId、workflowId、version、nodeId、nodeType、开始时间、结束时间、耗时、输入关键字段、输出关键字段、模型调用 token 数、错误信息。这些记录单独存一张表并提供按照 executionId 聚合查看的查询接口。有了 trace 表之后很多模型问题一眼就能定位。比如“客户说退款流程却走了投诉”点开 trace 看 model 节点输出发现模型返回了category:refund_request而条件表达式写的却是category refund。这种字符串枚举不一致的问题比任何中间件 bug 都常见。后来我把 LLM 节点输出强制要求为结构化 JSON并且用 JSON Schema 校验这类问题才大幅减少。6. 从架构图到能跑通的最小实现路径6.1 最小闭环怎么搭架构图画得再完整不如先跑通一条最小链路。我的建议是严格执行这个顺序不要一上来做前端画布。第一步先定义 DSL 的 JSON Schema用最简单的 start - llm - end 三条边走通校验流程。第二步把 DSL 翻译成 LangGraph4j 的 StateGraph用写死的 LLM 调用跑通一次执行。第三步接入工具注册中心和 LangChain4j 的 Tool 注解让模型能调一个真实接口。第四步加 checkpoint 和数据库持久化实现失败节点重试。第五步再做可视化编辑器前端只负责输出 JSON不做任何运行时逻辑。这个顺序的核心逻辑是先解决“能不能跑”再解决“好不好用”。画布是很重要的产品功能但它对架构设计没有决定性影响反而会消耗大量精力。如果你先画画布再补运行时很容易被前端细节带偏把 DSL 设计得迎合编辑器而不是迎合图引擎。Maven 依赖方面LangChain4j 的坐标很稳定LangGraph4j 的坐标需要单独确认。我建议以官方 GitHub 仓库和 Maven Central 为准不要轻易抄网上某篇文章的版本号因为这两个库的 API 演进速度都很快不同版本之间节点定义方式可能有差异。我自己就遇到过按照老版本 API 写的图编译代码升级后发现addConditionalEdge的签名变了被迫重写一部分翻译层。6.2 后续扩展方向最小闭环跑通之后有几个方向值得继续做。第一是多租户隔离不同部门的工作流定义、工具权限、模型配额要分开。第二是流程模板市场把高频流程沉淀成可复用的模板比如“客服工单分类”“简历筛选初筛”“广告文案生成”。第三是子流程能力LangGraph4j 支持嵌套图可以在一个节点里调用另一个已发布工作流实现流程复用。还有一个很多人容易忽略的点平台里如果出现会签、多人审批、限时办理这些强 BPM 需求不要试图在 LangGraph4j 里硬实现。AI 工作流引擎擅长的是“节点快速流转 智能决策”而 Camunda、Flowable 这类 BPM 引擎擅长的是“长周期流程 复杂人工任务 组织模型”。我的建议是二者共存AI 工作流负责调模型、降本增效BPM 引擎负责正式的流程审批。两者通过外部任务接口对接AI 工作流跑完后把结果写入 BPM 流程变量触发下一步审批而不是互相替代。整个项目做下来我最深的体会是LangChain4j 和 LangGraph4j 本身不难难的是在它们之上设计好边界。DSL 的校验和版本管理要做得足够稳工具安全和权限模型要一开始就立住人工审批和 checkpoint 要做到生产级至于画布反而是最不急的部分。如果你也是 Java 团队想自建低代码智能体平台我建议先拿一个最小闭环跑起来在真实业务里磨上一个月回头再看这份架构设计你会知道哪些部分可以砍掉哪些部分必须加固。架构图谁都会画能在生产环境把一次失败节点的断点续跑和人工审批恢复做好才算真正落地。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。