ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于LangChain4j与LangGraph4j的低代码智能体工作流平台架构设计

基于LangChain4j与LangGraph4j的低代码智能体工作流平台架构设计 企业里做 AI 应用落地最难受的不是模型效果差而是改流程比训练模型还慢。业务方说这里要加一个人工确认节点那个分支要换一套 Prompt你一看又得改代码、发版本、等测试一个需求排两周业务早就不想等了。所以我看到 LangChain4j 和 LangGraph4j 在 Java 生态里逐步成熟时第一反应就是这两个东西结合起来非常合适做一套低代码工作流通用智能体平台。这套平台的定位很明确把大模型调用、知识库检索、外部 API、人工审批这些能力封装成可拖拽的节点让业务人员通过配置而不是敲代码来搭建一个智能体流程。这篇文章聊聊我基于这个组合做的平台架构设计包括为什么这么选型、工作流模型怎么定义、底层执行引擎怎么和 LangGraph4j 结合、以及实测踩过的坑。适合准备在 Java 项目里引入智能体编排、或者正在对比 Spring AI 和 LangGraph4j 做技术取舍的团队参考。全文不涉及具体公司信息只讲思路和落地细节。1. 平台定位与整体设计思路1.1 为什么还要自己做一套低代码智能体平台市面上的低代码工作流平台已经很多了n8n、Dify、Coze 都做得不错界面漂亮、节点丰富但落到企业内部 Java 技术栈时问题立刻暴露出来。第一是数据安全。流程节点要访问内网数据库、ERP 系统、企业微信审批接口外部 SaaS 平台很难打通。第二是扩展成本。你确实可以调用它们的 API但每对接一个内部系统都要写一堆 glue 代码最终变成另一个需要维护的中间层。第三是部署形态。很多企业要求私有化部署甚至要在离线环境运行这就把所有依赖云服务的编排平台直接排除了。所以我们需要在 Java 技术栈里做一套自己的平台。LangChain4j 解决了模型接入、Prompt 管理、工具调用、RAG 这些基础能力LangGraph4j 恰好弥补了 LangChain4j 在复杂流程编排上的短板——条件分支、循环、多轮对话状态、人工介入。这两个库组合起来正好覆盖一个工作流智能体平台所需要的全部底层能力。相比直接用现成的智能体框架自研平台最终省下的是流程变更的时间成本。业务方自己拖拽、测试、发布我们只需要维护好节点能力和平台稳定性这才是低代码的核心价值。1.2 选型分析为什么是 LangChain4j LangGraph4j而不是 Spring AI这里要先说一个背景我做这个架构之前也认真对比过 Spring AI。Spring AI 的好处是生态统一、自动配置方便尤其是快速调用大模型写个 Chat 接口十分钟就能跑起来。但做工作流时问题就来了工作流天然是图结构需要节点间共享状态、支持循环、支持阻塞等待人工审批而 Spring AI 在这方面目前还没有成熟的图执行引擎。LangGraph4j 的核心思想是把智能体执行过程定义成一张有向图。每个节点是一个计算步骤边决定了下一步怎么走一个状态对象贯穿整个图。这个模型和我们低代码流程设计器里的节点 连线 变量几乎一一对应。所以用 LangGraph4j 做执行内核LangChain4j 做模型和工具层是一个很自然的组合。另一个现实原因是团队熟悉度。LangChain4j 的 API 风格贴近原版 LangChain中文社区资料多普通 Java 开发看几天就能上手。LangGraph4j 虽然是社区移植版但基本概念和 Python 版一致文档里的例子也足够多。我们在架构设计阶段做了 5 个 PoC 验证包括构建图、条件跳转、状态持久化、工具调用确认这条路走得通才定下来的。1.3 整体架构分层与模块边界整个平台从上层往下分为五层每层职责单一依赖关系清晰。表现层Web 可视化流程设计器基于 React Ant Design 开发使用拖拽库提供节点面板、画布、连线、配置表单。API 层提供流程定义的 CRUD、发布、执行、实例查询、运行日志等 REST API用 Spring Boot 3 Spring Security 做权限控制。编排层流程解析器把 JSON 定义翻译成 LangGraph4j 的 StateGraph执行引擎负责启动、暂停、继续、终止流程实例调度器处理定时触发和并发控制。能力层封装模型调用、向量检索、工具执行、人工任务、脚本执行等节点能力这一层是平台的可扩展点。持久层MySQL 存流程定义、工作流实例、运行日志Redis 存分布式锁和缓存对象存储存流程运行中产生的文件。需要特别说明的是编排层是核心但也是最容易踩坑的地方。LangGraph4j 提供的 StateGraph 是单机内存执行模型我们必须在它外面包一层持久化和调度能力否则流程一重启就全部丢失。后面第 3 节我会详细讲状态管理与持久化方案。2. 核心模块与工作流模型设计2.1 工作流图的数据结构一份可落地的 JSON 设计低代码平台的第一步是把工作流定义数据结构化。我们使用 JSON Schema 来定义流程定义简化后的结构是这样的{ id: lead_qualification, name: 销售线索质检, version: 3, startNodeId: start, nodes: [ { id: start, type: start, next: classify }, { id: classify, type: llm, config: { model: qwen-plus, promptTemplate: 你是销售线索质检助手。用户提交线索如下{{input}}请判断线索是否有效并输出JSON{\valid\: boolean, \reason\: string}, outputVariable: classifyResult }, next: branch }, { id: branch, type: condition, config: { expression: classifyResult.valid true, nextIfTrue: score, nextIfFalse: end } }, { id: score, type: api, config: { url: https://internal-scoring/score, method: POST, timeout: 5000, body: {\company\: \{{classifyResult.company}}\, \contact\: \{{classifyResult.contact}}\}, outputVariable: scoreResult }, next: human_review }, { id: human_review, type: human, config: { assignee: sales_admin, title: 请确认高潜力线索, contentTemplate: 线索{{scoreResult.company}}评分{{scoreResult.score}} }, next: end }, { id: end, type: end } ], variables: { input: string, classifyResult: object, scoreResult: object } }这个 JSON 结构参考了 Camunda BPMN 的思路但为了智能体场景做了大幅精简。每个节点都有一个全局唯一的 id、类型、配置、出边。条件节点用显式的nextIfTrue/nextIfFalse避免执行引擎去解析复杂的布尔表达式提高可读性和可靠性。在设计时我们定了一个约定所有节点输出都写入同一个顶层状态对象节点之间通过变量名引用。条件表达式使用 SpELSpring Expression Language来取值因为 Java 团队熟悉、可控而且可以通过StandardEvaluationContext限制只读变量避免表达式注入。实际实现里我们会把流程定义中所有变量先做一轮静态校验确保引用的变量名在前面某个节点中被声明过否则直接拒绝发布。2.2 低代码设计器到 LangGraph4j 的映射流程设计器里画出来的节点 箭头最终要变成可执行的图。LangGraph4j 的构建方式是addNodeaddEdgeaddConditionalEdge关键代码大致如下StateGraphAppState graph new StateGraph(AppState::new); graph.addNode(classify, ctx - classifierNode.invoke(ctx)); graph.addNode(score, ctx - scoringNode.invoke(ctx)); graph.addNode(human_review, ctx - humanTaskNode.invoke(ctx)); graph.addNode(end, ctx - null); graph.setEntryPoint(start); graph.addEdge(start, classify); graph.addConditionalEdge(classify, ctx - isTrue(ctx.state().getClassifyResult()) ? score : end, Map.of(score, score, end, end)); graph.addEdge(score, human_review); graph.addEdge(human_review, end);但这里有个关键的工程问题低代码平台的工作流是配置驱动的不可能为每个流程写一个静态图。所以我们不会在代码里硬编码节点类而是维护一个NodeRegistry。每种节点类型对应一个实现了WorkflowNode接口的 Spring Beanpublic interface WorkflowNode { void invoke(NodeExecutionContext context); }流程解析器读 JSON 定义时先遍历节点数组根据type从NodeRegistry拿到对应的 Bean然后用graph.addNode(nodeId, ctx - node.invoke(ctx))动态注册再遍历边关系调用addEdge或addConditionalEdge。这样一来新增一种节点类型只需要写实现类和配置表单引擎代码完全不用动。这才是低代码在技术层面真正站得住脚的地方。2.3 智能体节点能力池平台真正要沉淀的东西平台好不好用最终看节点能力够不够。我们一开始预设了以下几类节点节点类型作用关键配置项start流程入口输入参数定义、触发方式end流程结束输出结果定义llm调用大模型模型、Base URL、API Key、Prompt 模板、输出解析rag知识库检索Embedding 模型、向量库、TopK、重排策略api调用外部 HTTP 接口URL、Method、Headers、参数映射、超时、重试condition条件分支SpEL 表达式、目标节点human人工审批/输入审批人、任务标题、任务内容模板、超时提醒script数据转换Groovy 或 Java 表达式loop循环处理循环变量、内部子流程parallel并行分支分支列表、汇合策略以 LLM 节点为例它并不是简单调用一次chatModel.generate()就结束。我们支持两种输出模式一种是将 LLM 的回复作为整体写入变量另一种是要求模型返回 JSON并自动做 JSON 解析和字段映射。后者在实际业务中更常用所以我们在 LLM 节点配置里增加了一个outputSchema字段运行时会把它拼接进 Prompt要求模型严格按 Schema 输出再用 Jackson 反序列化。如果解析失败节点可以走一条专门的解析失败分支让流程设计者决定是重试还是转人工。RAG 节点这里多说一句LangChain4j 本身提供了EmbeddingStoreIngestor和检索工具可以直接在 Java 里做知识库入库与召回。但我们的平台里更常见的设计是把 RAG 封装成一个检索服务并通过 API 节点调用而不是每个流程都直接引入向量库连接。原因很简单核心知识库通常由专门团队维护权限和版本控制都在那边工作流只需要拿到检索结果。当然如果平台本身就是知识库的所有者那直接用 LangChain4j 的 RAG 组件会更省事。3. 关键机制实现3.1 状态管理与工作流实例持久化LangGraph4j 的核心是状态对象。我们定义的状态类大致是这样的Data public class AppState { private String workflowInstanceId; private String sessionId; private MapString, Object data; private String currentNodeId; private Integer attempt; private Boolean finished; }每个节点执行前从data读取输入执行后把结果写回data。这个设计本身不复杂复杂在于低代码平台必须支持暂停和恢复。比如人工审批节点流程发起后要等待审批人点击通过或驳回可能一等就是几小时。如果进程重启流程必须能从上次停留的节点继续跑。LangGraph4j 自带的持久化机制在 Java 版里还不够成熟所以我们做了两层持久化。第一层是状态快照每执行完一个节点就把AppState整个序列化成 JSON保存到wf_instance_state表同时记录当前节点 ID。序列化只支持基础类型、List、Map禁止自定义业务对象直接放进去——这是为了避免反序列化时的类版本问题。第二层是人工任务表把待审批事项单独存到wf_task表绑定流程实例 ID、节点 ID、审批状态。审批回调时通过流程实例 ID 恢复AppState然后找到继续执行的边重新进入 LangGraph4j 图。这里有一个优化细节状态快照不要每个节点都全量写否则高并发下数据库压力会很大。我们的做法是配置一个checkpointInterval默认 3 个节点写一次但对于有人工节点的分支路径人工节点之前一定强制做一次快照因为那里是最可能发生长等待的位置。3.2 工具调用与函数注册机制在实际工作流里大模型经常需要调用外部工具比如查订单、算折扣、发邮件。LangChain4j 对工具调用的支持相当完善我们可以把 Java 方法直接暴露出给模型但工程上不能把任何方法都给模型要有明确的注册边界。我们的做法是定义一套平台工具注解PlatformTool( name queryOrder, description 根据订单号查询订单信息, params { Param(name orderId, type string, description 订单号必填) } ) public Order queryOrder(String orderId) { // ... }平台在启动时通过反射扫描带注解的 Bean生成ToolSpecification列表。LLM 节点运行时会从上下文拿到当前流程允许使用的工具 ID 列表从平台工具注册表里筛选出对应的ToolSpecification传给模型。模型输出工具调用请求后由 LangChain4j 的ToolExecutor执行并把结果写回状态。这个设计要特别注意一点工具的入参一定要做白名单校验。比如一个发送邮件工具参数的to字段必须符合邮箱格式content要经过模板渲染不能让模型自由拼接任意文本。我们曾经遇到过大模型把业务参数和一些额外字符拼接在一起导致下游接口报错的问题后来所有工具入参统一过一层ParameterValidator按声明类型和约束强制校验错误信息再返回给模型让它修正效果好了很多。3.3 执行引擎的并行、超时与重试策略工作流引擎如果只是顺序调用实现很简单但真实场景里并行分支、API 超时、多实例并发这些问题躲不掉。并行节点我们这样实现把并行子分支放进ExecutorService用CompletableFuture聚合结果。为了不占用太多线程我们在线程池参数上做过测算。节点执行以 IO 等待为主不是 CPU 密集所以线程池核心线程数设置为2 * CPU 核数最大线程数设置为8 * CPU 核数队列大小控制在 200拒绝策略是CallerRunsPolicy——如果任务实在太多就由调用线程执行避免直接丢弃业务请求。并行分支全部完成后可以配置两种汇合策略一种是全部成功才继续一种是至少一个成功就继续。第二种适合多个模型结果投票的场景实现时只需要对CompletableFuture的anyOf和allOf做选择。超时与重试是所有 API 类节点的标配。重试策略采用指数退避加随机抖动delay base * 2^attempt random(0, 500ms)。基础值默认 1 秒最多重试 3 次。为什么加随机抖动因为很多下游系统在故障恢复时同时收到大量重试请求没有抖动会把系统打挂。对于 LLM 节点我们一般不会自动重试整个 Prompt 调用因为大模型调用成本较高且响应时间长通常是节点配置里让用户选择失败后进入人工处理还是重试当前节点。分布式锁也是必要的一环。同一个流程实例如果被重复触发必须先拿到实例级锁。我们用 Redis Redisson 实现锁粒度是wf:instance:{id}锁的超时时间根据流程预估耗时设置一般 10 分钟到期自动释放。这样能避免定时触发和人工重试同时把同一个实例跑成两份。4. 实操过程从一个销售线索质检工作流看落地4.1 场景梳理把业务痛点翻译成流程图销售团队每天会收到大量线上线索需要判断是否有效、是否值得跟进。原来全靠商务手动一条条看人均每天处理 200 条就饱和了而且判断标准不统一有人只看公司名有人还要查官网。我们和业务一起梳理了一个流程先用大模型从线索文本里抽取结构化信息包括公司名、行业、规模、联系人再调用企业内部评分服务根据行业权重和关键词匹配打一个 0-100 的分数分数大于 60 的进入人工确认小于等于 60 的直接淘汰。整个过程可以用平台里的 LLM 节点、API 节点、条件分支节点、人工审批节点串起来。4.2 在低代码设计器里配置流程配置流程的过程业务人员自己就能完成大半。第一步新建流程输入流程名称和描述提交后系统自动生成流程 ID。第二步从左侧节点面板拖入一个 LLM 节点给它命名信息抽取在配置表单里选择模型供应商填写 Prompt 模板模板里我们用{{input}}引用流程入参。第三步拖入 API 节点配置内部评分服务的 URL把 LLM 节点输出的字段映射到请求体里。第四步拖入条件节点填表达式scoreResult.score 60把是的方向连到人工审批节点否的方向连到结束节点。第五步人工审批节点配置审批人为销售主管审批通过后走通知销售的脚本节点驳回则直接结束。整个配置过程大概 20 分钟。配完之后点击试运行平台会针对当前流程定义生成一个临时实例填入测试数据跑一遍并把每个节点的输入输出都记录下来。试运行通过后再点击发布流程定义生成一个新的版本号后续新的流程实例都基于版本 3 执行。4.3 LangGraph4j 执行器代码骨架为了让读者更清楚动态构建图的过程我贴一段简化后的执行器代码Service public class WorkflowExecutor { private final NodeRegistry nodeRegistry; public WorkflowExecutionResult execute(String definitionId, MapString, Object inputData) { WorkflowDefinition def workflowDefinitionRepository.load(definitionId); StateGraphAppState graph buildGraph(def); AppState initialState new AppState(); initialState.setWorkflowInstanceId(UUID.randomUUID().toString()); initialState.setData(new HashMap(inputData)); CompiledGraphAppState compiledGraph graph.compile(); AppState finalState compiledGraph.invoke(initialState); saveInstance(finalState); return mapResult(finalState); } private StateGraphAppState buildGraph(WorkflowDefinition def) { StateGraphAppState graph new StateGraph(AppState::new); for (WorkflowNodeDef nodeDef : def.getNodes()) { WorkflowNode node nodeRegistry.getNode(nodeDef.getType()); graph.addNode(nodeDef.getId(), ctx - node.invoke(ctx)); } for (WorkflowEdgeDef edge : def.getEdges()) { graph.addEdge(edge.getSource(), edge.getTarget()); } for (WorkflowConditionDef condition : def.getConditions()) { graph.addConditionalEdge(condition.getSource(), ctx - evaluate(condition, ctx.state()), condition.getTargetMap()); } graph.setEntryPoint(def.getStartNodeId()); return graph; } }这段代码的核心是buildGraph工作流定义 JSON 被解析成WorkflowDefinition对象然后循环注册节点、注册边和条件边。节点执行时统一接收NodeExecutionContext里面包含了当前AppState、流程定义、节点配置等具体某个节点做什么由实现类决定。实际项目中我们还会在compiledGraph.invoke前后做状态快照、日志采集、异常捕获。注意invoke是同步阻塞的如果某个节点是异步任务比如发消息等待回调会在节点内部实现为挂起直接返回一个等待中状态而不是真的阻塞线程。这部分设计比较绕简单说就是只有像人工审批这样的长等待节点才需要挂起普通 API 节点还是同步等待结果。4.4 上线效果与优化过程这个流程上线两周实际效果超过预期。以前商务手动处理线索人均每天 200 条封顶现在只需要处理系统筛出来的高潜力线索日均 40 条左右判断标准也统一了。流程处理耗时中位数从原来的 18 分钟因为要排队等人工降到了 1.2 分钟因为大部分线索在到达人工节点前已经被自动淘汰。但中间也暴露了一个问题大模型抽取字段的准确率一开始只有 85%主要是部分线索文本很简短比如只有一行某某科技有限公司 张经理 138xxxx模型会漏掉行业信息。我们做了两个优化一是在 Prompt 里加了 few-shot 示例模型能参考完整样例理解抽取模式二是在信息抽取节点后面加了一个字段完整性检查的脚本节点如果发现必填字段为空就进入一个再次抽取的 LLM 节点用更直白的追问式 Prompt 重新抽一次。这个重试子流程在低代码平台里配置非常方便最终准确率提到了 94%。5. 常见问题与排查技巧实录5.1 工作流引擎高频问题速查表把团队在开发和压测阶段遇到的高频问题整理成一个速查表方便读者排查问题现象可能原因解决方案LangGraph4j 条件边不生效总是走默认分支SpEL 表达式解析结果不是布尔值比如拿到了字符串true在表达式前加#并确保类型为 boolean或在脚本节点里强制转换LLM 返回的 JSON 解析失败Prompt 约束不够模型输出了额外解释文字使用outputSchema并在 Prompt 中明确“只输出 JSON 代码块”解析时先提取代码块内容流程暂停后恢复状态数据丢失只保存了部分变量或者自定义对象序列化后反序列化失败状态快照里只存基础类型和 JSON 结构业务对象不直接放入状态并行节点偶尔结果缺失某个分支异常时CompletableFuture未做异常捕获每个分支捕获异常写入该分支结果字段汇合时先检查所有分支状态API 节点重试导致下游重复提交接口不是幂等的在节点配置中增加“幂等键”比如根据流程实例 ID 节点 ID 生成request_id下游用这个做去重动态构建图时报节点 ID 冲突用户在设计器里复制粘贴节点时未重新生成 ID节点 ID 用 UUID 作为内部标识用户看到的名称单独存储5.2 独家避坑心得动态节点注册的类型安全我们最初把所有节点统一设计成NodeR想通过泛型来约束输入输出结果在动态构建图时频繁出现ClassCastException。因为 LangGraph4j 的节点执行接口拿到的是同一个AppState泛型参数在运行时被擦除了类型判断根本不可靠。后来我们调整了设计每个节点实现一个无泛型的WorkflowNode接口节点自己的配置类独立定义并在注册时绑定一个配置解析器。执行时流程引擎从 JSON 节点定义里把config字段读出来用配置解析器转成具体的配置对象。节点内部自行从AppState.data读取变量并写入新变量。这样避免了网关处的类型判断同时也让节点逻辑更内聚。5.3 流程解析器不要用递归实现还有一个经验低代码流程引擎的执行器不要试图自己用递归深度优先来遍历图。我们的流程里存在循环边比如抽取失败再抽一次递归遍历很容易栈溢出。即便没有循环深路径也会浪费大量栈帧。正确做法是直接用 LangGraph4j 的图执行器或者自己通过队列实现状态机遍历。我们选择前者因为 LangGraph4j 已经处理了环、自环、多入口等复杂情况只要把节点注册正确执行逻辑是可信的。6. 这套架构的边界与后续演进最后聊一点边界认识。这套低代码智能体平台并不是万能的。如果团队只需要一个聊天机器人知识库问答那直接用 LangChain4j 原生写几个类就够了完全不需要工作流引擎。低代码平台的价值在多条路径、多类节点、跨部门协作的场景里才会被放大。后续我们规划了几个方向一是把更多常用模型接入做成开箱即用的插件二是把流程版本回滚和灰度发布能力做完整三是将节点执行日志结构化方便业务方自己在后台查看。这些方向都依赖当前这套图定义 节点注册中心 LangGraph4j 执行引擎的底座所以底座稳定扩展才可能。从个人实际体会看这类平台最忌讳一上来就想着覆盖所有节点。先固化两三类高频路径跑通端到端看业务方真实使用情况再一点点增加节点类型。平台的价值不在于功能花哨而在于让业务方形成改流程不用求人的习惯。一旦这种习惯建立后续的需求沟通就会顺畅很多。
返回列表