
1. 为什么要在 LangChain4j 之上再叠一层 LangGraph4j1.1 单链式 Agent 的天花板在哪里用 LangChain4j 写过 Agent 的人大概都有这个体会一开始用AiServices加几个Tool方法跑个问答、查个数据库、调个接口确实很爽几十行代码就能跑起来。但只要业务稍微复杂一点比如先判断用户意图再决定走知识库检索还是走工单创建创建失败要回滚并通知人工单链式的Chain就开始力不从心了。问题出在控制流上。LangChain4j 的核心抽象是链——输入经过一串处理变成输出本质是线性的。而真实业务里的智能体往往是有环的、有分支的、有状态的。用户说帮我查一下上个月的订单Agent 查完发现没有得回到询问用户是不是记错了时间这个节点这就是一个环。用链式结构硬写只能靠 if-else 层层嵌套代码很快就变成意大利面条。LangGraph4j 解决的正是这个问题。它把智能体的执行过程建模成一张有向图节点是动作调 LLM、调工具、做判断边是流转条件。环、分支、并行、中断恢复这些在链式结构里很难表达的东西在图结构里都是原生能力。所以LangChain4j LangGraph4j这个组合的定位很清晰LangChain4j 负责能力模型调用、工具、RAG、记忆LangGraph4j 负责编排状态流转、分支决策、循环控制。1.2 低代码平台为什么必须要有图编排引擎如果只是自己写代码用不用图引擎其实两可。但一旦要做低代码平台图编排就从可选变成了必需。低代码的核心诉求是让非专业开发者业务人员、产品经理也能搭出可用的智能体。他们不会写 Java但他们能看懂流程图。一张开始 → 意图识别 → 分支A/B → 结束的图比一段 200 行的 Java 代码直观得多。前端画布上拖拽出来的节点和连线本质上就是一张图的数据结构后端要有一个引擎能把这坨 JSON 跑起来——LangGraph4j 就是这个引擎。更关键的是可观测性和可干预性。低代码平台上的流程业务方需要能看到现在跑到哪一步了为什么走了这个分支哪一步卡住了。图结构天然支持这些每个节点是一个可观测的执行单元每条边是一次可追溯的决策。这是链式结构给不了的。1.3 这套架构到底适合谁说句实在话这套架构不是给我就想做个问答机器人的人准备的。如果你只需要一个客服问答直接用 LangChain4j 的AiServices就够了上 LangGraph4j 是杀鸡用牛刀。它真正适合的是这几类场景多步骤业务流程自动化比如简历筛选解析 → 匹配 → 打分 → 人工复核 → 通知每一步都可能失败、都可能需要回退。需要人工介入的审批流Agent 跑到某一步需要人确认确认后继续这要求图能暂停和恢复。多智能体协作一个主管 Agent调度多个专业 Agent本质是一张图里有子图。需要给业务方开放编排能力的平台这是低代码的典型场景业务方自己画流程平台负责执行。我见过不少团队一上来就想搞通用智能体平台结果发现 80% 的需求其实就是个固定流程用图引擎反而增加了复杂度。所以我的建议是先把图引擎用在一两个真正需要分支和循环的场景上跑通了再谈平台化。2. 把 LangChain4j 的能力封装成图节点设计取舍2.1 节点粒度一个节点该干多少事这是设计图编排时第一个要拍板的问题。节点太细图会变得巨大无比画布上几百个节点没人看得懂节点太粗又失去了图编排的意义等于把一大段代码塞进一个节点里。我的经验是按业务语义切分而不是按技术步骤切分。举个例子调用大模型生成回复和解析大模型的 JSON 输出这两个技术步骤应该合成一个节点叫生成结构化回复因为对业务方来说这是一件事。而检索知识库和生成回复应该拆成两个节点因为业务方可能想在这两步之间插入一个人工审核检索结果的环节。具体到实现上LangGraph4j 的节点就是一个函数输入是共享状态State输出是状态更新。用 LangChain4j 封装一个典型的 LLM 节点大概长这样public class LlmNode implements NodeActionAgentState { private final ChatLanguageModel model; private final String systemPrompt; public LlmNode(ChatLanguageModel model, String systemPrompt) { this.model model; this.systemPrompt systemPrompt; } Override public MapString, Object apply(AgentState state) { ListChatMessage messages new ArrayList(); messages.add(SystemMessage.from(systemPrompt)); messages.addAll(state.getHistory()); messages.add(UserMessage.from(state.getCurrentInput())); AiMessage reply model.generate(messages).content(); return Map.of(lastReply, reply.text()); } }这里有个坑要提醒节点函数必须是纯函数式的不要在里面直接改 state。LangGraph4j 的状态更新是通过返回 Map 合并的如果你在节点里直接state.setXxx()在并行分支下会出现竞态。我一开始就踩过这个坑两个并行节点同时改一个字段结果谁后写谁生效排查了半天。2.2 状态设计共享状态是图的血液State 是整张图里所有节点共享的数据容器设计得好不好直接决定了图能不能跑通。我的原则是状态里只放跨节点需要传递的数据节点内部的临时变量不要塞进去。一个典型的 AgentState 大概包含这几类字段字段类别示例说明输入输出currentInput, finalOutput用户输入和最终结果对话历史history多轮对话的消息列表中间结果retrievedDocs, toolResults节点间传递的临时数据控制标志needHumanReview, retryCount影响分支决策的标志位元信息traceId, startTime用于可观测性这里有个设计上的取舍状态用强类型 POJO 还是 Map。LangGraph4j 官方示例多用MapString, Object灵活但容易写错 key。我在生产项目里更倾向于定义一个AgentState类用 Lombok 的Builder和Data配合自定义的Channel来做状态合并。强类型的好处是编译期就能发现字段名写错坏处是加字段要改类。低代码平台场景下因为字段是动态的反而更适合用 Map Schema 校验的方式。2.3 工具调用怎么让 LLM 决定走哪条边图编排里最微妙的一点是分支决策往往需要 LLM 参与。比如用户这句话是咨询还是投诉这个判断得让模型来做。但模型输出的是自然语言怎么把它变成图上的边标准做法是让模型输出结构化的路由指令。LangChain4j 支持把模型输出映射成 Java 对象配合 LangGraph4j 的条件边就能实现LLM 决策分支public class IntentRouter implements EdgeActionAgentState { Override public String apply(AgentState state) { // 用轻量模型做意图分类别用大模型成本和延迟都扛不住 String intent classifyIntent(state.getCurrentInput()); return switch (intent) { case consult - consult_node; case complaint - complaint_node; default - fallback_node; }; } }提示意图分类这种高频、简单的判断千万别用主力大模型。我实测下来用一个小模型比如 7B 级别的做分类准确率能到 90% 以上延迟从 2 秒降到 200 毫秒成本降一个数量级。主力模型留给真正需要推理的节点。3. 低代码画布与后端图引擎的对接细节3.1 前端画布 JSON 到 LangGraph4j 图的映射低代码平台的前端画布本质上是在编辑一张图的数据结构。主流画布库比如阿里低代码引擎、X6、ReactFlow导出的 JSON 大概长这样{ nodes: [ {id: start, type: start, data: {}}, {id: intent, type: llm, data: {prompt: ..., model: qwen}}, {id: end, type: end, data: {}} ], edges: [ {source: start, target: intent}, {source: intent, target: end, condition: intent consult} ] }后端要做的就是把这个 JSON 翻译成 LangGraph4j 的StateGraph。核心映射关系是画布节点 →graph.addNode(id, nodeAction)画布连线 →graph.addEdge(source, target)或graph.addConditionalEdges(source, edgeAction, mappings)节点配置prompt、模型、工具→ 构造 NodeAction 时的参数这里最容易出问题的是条件边的映射。LangGraph4j 的条件边需要你提供一个路由函数和一个路由结果到节点 ID 的映射表。前端画布上的连线条件是一段表达式比如intent consult后端需要把它解析成可执行的逻辑。我的做法是用 SpELSpring Expression Language或者 Aviator 这类表达式引擎把表达式字符串编译成可复用的Expression对象避免每次执行都重新解析。3.2 节点配置的动态化别把 prompt 写死在代码里低代码平台的精髓在于配置驱动。如果每个节点的 prompt、模型、温度这些参数都写死在 Java 代码里那还叫什么低代码所以节点必须是配置化的。我的做法是定义一个NodeConfig结构从画布 JSON 里读出来运行时动态构造 NodeActionpublic class ConfigurableLlmNode implements NodeActionAgentState { private final NodeConfig config; private final ChatLanguageModel model; private final PromptTemplate template; public ConfigurableLlmNode(NodeConfig config, ModelFactory factory) { this.config config; this.model factory.get(config.getModelName()); this.template PromptTemplate.from(config.getPromptTemplate()); } Override public MapString, Object apply(AgentState state) { String prompt template.apply(state.toVariableMap()); AiMessage reply model.generate(prompt).content(); return Map.of(config.getOutputKey(), reply.text()); } }这样业务方在画布上改 prompt、换模型后端不用重新部署。但要注意模型实例要缓存不能每次执行都 new 一个否则连接池会被打爆。我一般用一个ConcurrentHashMapString, ChatLanguageModel做模型工厂的缓存。3.3 人工介入节点图的暂停与恢复审批流场景里人工审核节点是刚需。这个节点不能像普通节点那样执行完就往下走它得暂停整张图等外部事件触发后再恢复。LangGraph4j 支持通过interruptBefore或interruptAfter在指定节点前后中断。实现思路是图执行到人工节点前中断把当前 State 持久化存数据库或 Redis。平台前端展示待办人工操作后写入审核结果。用同一个threadId重新调用图的执行引擎会从断点恢复把人工结果合并进 State继续往下走。// 第一次执行会在 human_review 前中断 CompiledGraphAgentState graph StateGraph.build() .addNode(human_review, new HumanReviewNode()) .interruptBefore(human_review) // ... 其他节点和边 .compile(); // 恢复执行 RunnableConfig config RunnableConfig.builder() .threadId(task-123) .build(); graph.stream(Map.of(reviewResult, approved), config);注意State 的持久化必须用支持序列化的结构。我见过有人把ChatLanguageModel这种不可序列化的对象塞进 State结果恢复时报NotSerializableException。记住State 里只放数据不放服务对象。4. 多智能体协作子图与主管模式4.1 什么时候该拆成多个智能体单智能体加一堆工具和多智能体协作这两条路怎么选我的判断标准很简单当工具数量超过 10 个或者不同工具需要不同的系统提示词时就该拆了。原因在于 LLM 的注意力是有限的。你给它 20 个工具让它选它选错的概率会显著上升。而且不同领域的工具往往需要不同的角色设定——客服 Agent和财务 Agent的系统提示词完全不一样硬塞进一个 Agent 里模型会精神分裂。拆成多智能体后每个 Agent 只负责自己领域的事工具少、提示词专一准确率明显提升。代价是需要一个主管来调度这就引出了主管模式。4.2 主管模式一个调度 Agent 加若干执行 Agent主管模式的结构是一个 Supervisor 节点负责理解用户意图决定把任务派给哪个子 Agent子 Agent 执行完把结果交回 SupervisorSupervisor 决定是继续派活还是结束。在 LangGraph4j 里这可以用子图来实现。每个子 Agent 是一张独立的StateGraphSupervisor 是主图里的一个节点通过条件边路由到不同的子图StateGraphAgentState mainGraph new StateGraph(AgentState.SCHEMA) .addNode(supervisor, new SupervisorNode()) .addNode(customer_service, customerServiceSubgraph) .addNode(finance, financeSubgraph) .addConditionalEdges(supervisor, state - state.getNextAgent(), Map.of( customer_service, customer_service, finance, finance, FINISH, StateGraph.END )) .addEdge(customer_service, supervisor) .addEdge(finance, supervisor);注意子图执行完要回到 Supervisor形成一个环。这个环的终止条件是 Supervisor 判断任务完成返回FINISH。这里有个坑一定要设置最大循环次数否则 Supervisor 可能陷入派活 → 子 Agent 说做不了 → 再派活的死循环。我一般设 10 次上限超了就强制结束并返回当前结果。4.3 子图之间的状态隔离与共享子图和主图共享同一个 State 类型但子图往往只关心 State 的一部分。比如财务子图只关心金额发票号这些字段不关心用户情绪。我的做法是在子图入口做一次状态投影把主图 State 里子图需要的字段提取出来子图执行完再把结果合并回主图。这样做的另一个好处是子图可以独立测试——喂给它一个精简的 State 就能跑不用构造完整的主图状态。5. 踩过的坑从 Demo 到生产要跨过的坎5.1 流式输出与图执行的冲突Demo 阶段最爽的是流式输出用户能看到字一个个蹦出来。但上了图编排之后流式就变得复杂了图执行到某个节点时怎么把该节点的 LLM 输出流式推给前端LangChain4j 的StreamingChatLanguageModel支持 token 级别的回调但 LangGraph4j 的节点执行是一次性返回结果的模型。两者结合需要一点技巧在节点内部用StreamingChatLanguageModel通过一个StreamingResponseHandler把 token 推到一个 SSE 通道同时累积完整结果作为节点返回值。public MapString, Object apply(AgentState state) { StringBuilder full new StringBuilder(); CountDownLatch latch new CountDownLatch(1); streamingModel.generate(messages, new StreamingResponseHandler() { Override public void onNext(String token) { full.append(token); sseEmitter.send(token); // 推给前端 } Override public void onComplete(ResponseAiMessage response) { latch.countDown(); } Override public void onError(Throwable error) { latch.countDown(); } }); latch.await(60, TimeUnit.SECONDS); return Map.of(lastReply, full.toString()); }提示latch.await一定要设超时否则模型卡住时整个图就挂死了。我吃过这个亏线上一个请求卡了 5 分钟把线程池占满了。5.2 状态持久化的序列化陷阱前面提过 State 不能放服务对象这里再展开说。生产环境里 State 要存 Redis 或数据库序列化方式的选择很关键。用 Java 原生序列化要求所有字段都实现Serializable而且版本兼容性差——改个字段类型老数据就反序列化失败。我推荐用JSON 序列化Jackson配合JsonIgnoreProperties(ignoreUnknown true)这样加字段不会影响老数据。但 JSON 序列化对ChatMessage这类多态类型不友好需要自定义序列化器。还有个隐蔽的坑State 里的时间字段。如果用LocalDateTimeJackson 默认序列化成数组反序列化容易出错。统一用Instant或者配置JavaTimeModule并禁用时间戳格式能省很多事。5.3 循环与重试别让图跑飞图里的环是双刃剑。用得好实现重试直到成功用不好就是死循环把 CPU 跑满。我的经验是每个环都必须有明确的退出条件且退出条件要能覆盖所有异常路径。比如检索 → 判断相关性 → 不相关则改写查询重试这个环退出条件不能只是相关性达标还得有重试次数超过 3 次和总耗时超过 30 秒这两个兜底。另外重试节点要记录重试次数到 State否则每次重试都是从零开始模型可能反复犯同样的错。把失败原因也记进 State下次重试时作为上下文喂给模型能显著提高重试成功率。5.4 可观测性图跑起来之后怎么排查图编排最大的运维挑战是排查。链式结构出问题看日志一行行往下读就行图结构出问题你得知道当前在哪个节点为什么走了这条边State 长什么样。我的做法是在每个节点执行前后打结构化日志包含traceId、nodeId、stateSnapshot脱敏后、耗时。配合一个可视化的执行轨迹页面把每次执行的节点序列和状态变化画出来排查效率能提升一个数量级。LangGraph4j 本身支持通过Checkpoint机制记录每个节点的状态快照把这个能力和日志系统结合就能实现任意一次历史执行都能回放。这在排查偶发分支走错这类问题时特别有用。6. 关于选型的一点个人看法经常有人问现在到底用 Spring AI 还是 LangGraph4j。我的看法是这俩不是竞品是不同层次的东西。Spring AI 更偏向把模型能力接入 Spring 生态它的抽象是ChatClient、EmbeddingModel这些LangGraph4j 是编排引擎解决的是控制流问题。你完全可以用 Spring AI 做模型调用用 LangGraph4j 做编排。至于 LangChain4j 和 LangGraph4j 的关系前者是能力库后者是编排库两者是互补的。LangChain4j 的AiServices适合快速搭单 AgentLangGraph4j 适合搭复杂流程。低代码平台这种场景两者都得用。最后说个务实的建议别一上来就追求通用。我见过太多团队想做一个什么流程都能跑的平台结果做了一年还在改架构。正确的路径是先做一两个具体场景比如简历筛选、工单处理把图引擎、状态管理、人工介入这些核心能力打磨扎实再抽象成平台。通用性是长出来的不是设计出来的。