ARTICLE DETAIL

资讯详情

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

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

基于LangChain4j与LangGraph4j的低代码智能体工作流平台架构设计与实践 1. 为什么要在 LangChain4j 和 LangGraph4j 上搭一层低代码工作流第一次接触这个组合是在一个内部工具项目里当时的需求很直接业务侧想自己拖拽配置一个“合同初审”流程技术侧又不想为每个新流程重写一遍 Java 代码。试过纯 LangChain4j 的 Chain 写法也试过把流程逻辑硬编码在 Service 里最后都卡在同一个问题上——流程一变代码就得跟着改改完还要重新发版。后来把 LangGraph4j 引进来做状态编排再在上面套一层低代码的配置层才算把“业务能改流程”和“代码能复用”这两件事同时兜住。这套架构的核心价值说白了就是把智能体的能力拆成可配置的节点让工作流的走向由配置决定而不是由代码决定。LangChain4j 负责模型调用、工具绑定、RAG 检索这些原子能力LangGraph4j 负责把这些原子能力按有向图串起来低代码层则负责把图的结构、节点的参数、边的条件暴露成可视化配置。三者各管一段边界清晰后面维护起来才不会互相打架。适合谁来参考这篇内容如果你正在做企业内部智能体平台、想把 AI 能力沉淀成可复用的流程资产或者你已经在用 LangChain4j 但发现流程编排越来越乱那这套思路应该能帮到你。下面我会从整体设计、核心细节、实操落地、问题排查四个角度把踩过的坑和验证过的方案都摊开讲。2. 整体架构设计与技术选型拆解2.1 三层架构的职责边界怎么划这套平台我最终落成了三层能力层、编排层、配置层。能力层基于 LangChain4j封装模型对话、Embedding、工具调用、RAG 检索这些原子操作每个能力都做成独立的 Bean对外只暴露统一接口。编排层基于 LangGraph4j把能力层的 Bean 包装成图节点用 StateGraph 定义节点之间的流转关系状态对象贯穿整个执行过程。配置层是自研的低代码部分把图的节点列表、边条件、节点参数序列化成 JSON前端拖拽生成的就是这份 JSON后端解析后动态构建图。为什么这么分因为这三层的变更频率完全不同。能力层最稳定模型换了、工具加了才动编排层中等流程逻辑调整时动配置层最频繁业务侧天天改。如果混在一起改一个流程条件就要动到模型调用的代码风险太大。分层之后配置层改坏了最多是流程跑不通不会影响底层能力。这里有个容易踩的坑很多人会把 LangGraph4j 的 StateGraph 直接暴露给配置层让前端去拼图结构。我试过不行。LangGraph4j 的图是有类型约束的节点入参出参都是强类型的 State前端拼出来的 JSON 很难保证类型正确。正确做法是在配置层和编排层之间加一个流程定义模型用扁平的节点数组和边数组描述流程编排层再把这个模型翻译成 StateGraph。这样前端只需要关心“有哪些节点、怎么连”不需要关心 State 的类型。2.2 为什么选 LangChain4j 而不是 Spring AI这个问题被问过很多次。Spring AI 的抽象确实更贴合 Spring 生态但在这个项目里我最终选了 LangChain4j原因有三个。第一LangChain4j 的工具调用Tool Calling抽象更成熟Tool注解加ToolSpecification的组合把 Java 方法直接暴露成模型可调用的工具配置成本极低。第二LangChain4j 的 RAG 模块更完整EmbeddingStore、ContentRetriever、EmbeddingStoreContentRetriever这套组合开箱即用不用自己拼检索链路。第三LangGraph4j 本身就是 LangChain4j 生态的产物两者在状态传递和消息类型上天然兼容混用 Spring AI 反而要做一层适配。当然 Spring AI 也有优势比如和 Spring Boot 的自动配置集成更顺。但在这个项目里智能体的核心是编排和工具调用LangChain4j 在这两块的成熟度更高。选型这事没有绝对对错关键看你的核心诉求在哪。2.3 低代码层的配置模型设计配置层的核心是一份流程定义 JSON结构大概是这样的{ flowId: contract_review, nodes: [ { id: start, type: START, next: extract }, { id: extract, type: LLM, config: { model: qwen-plus, prompt: 从以下合同文本中提取甲方、乙方、金额、期限{{input}}, outputKey: contractInfo }, next: check }, { id: check, type: CONDITION, config: { expression: contractInfo.amount 100000, trueNext: manual_review, falseNext: auto_approve } } ] }节点类型我定义了六种START、END、LLM、TOOL、CONDITION、RAG。每种类型对应编排层的一个节点构建器构建器负责把配置翻译成 LangGraph4j 的节点函数。LLM节点调用 LangChain4j 的 ChatModelTOOL节点调用注册好的工具 BeanRAG节点走检索链路CONDITION节点做条件分支。注意条件表达式的解析不要用脚本引擎我一开始用 Groovy 做表达式求值后来发现安全审计过不了。改成自己写一个简单的表达式解析器只支持属性访问、比较运算和逻辑运算够用且安全。3. 核心细节解析与实操要点3.1 LangGraph4j 的状态设计LangGraph4j 的 StateGraph 需要一个状态对象贯穿始终。我一开始用MapString, Object做状态灵活是灵活但类型不安全节点里取值经常要强转运行时才报错。后来改成定义一个WorkflowState类用Data加字段的方式管理状态Data public class WorkflowState { private String input; private MapString, Object variables new HashMap(); private ListMessage messages new ArrayList(); private String currentNode; private MapString, Object nodeOutputs new HashMap(); }variables存流程中产生的中间变量nodeOutputs存每个节点的输出messages存对话历史。节点函数从 State 里取输入处理完把输出写回 State。这样类型安全调试时也能直接看到每个节点的产出。但这里有个细节LangGraph4j 的状态更新是通过Channel做的默认的Channels.base()是覆盖式更新多个节点同时写同一个字段会互相覆盖。如果流程里有并行分支需要用Channels.appending()或者自定义 Reducer。我在合同审查流程里没用到并行所以用的默认覆盖但如果你要做并行检索这个点一定要注意。3.2 节点构建器的实现套路每个节点类型对应一个构建器统一实现一个接口public interface NodeBuilder { String getType(); NodeActionWorkflowState build(NodeConfig config, WorkflowContext context); }NodeConfig是配置层传来的节点配置WorkflowContext持有 ChatModel、ToolRegistry、EmbeddingStore 这些运行时依赖。build方法返回一个NodeAction也就是 LangGraph4j 的节点函数。以LLM节点为例构建逻辑是从 config 里取 prompt 模板和 outputKey把 State 里的变量填充进模板调用 ChatModel 拿到结果把结果写到 State 的nodeOutputs里。这里有个优化点prompt 模板的变量填充不要用字符串替换用 LangChain4j 的PromptTemplate它支持{{var}}语法且能处理转义。TOOL节点的构建稍微复杂一点。工具在平台启动时注册到ToolRegistry每个工具有一个名字和对应的ToolSpecification。构建时根据 config 里的工具名找到对应的工具把 State 里的参数映射成工具入参调用后把结果写回 State。工具调用的异常要捕获不能让一个工具失败导致整个流程崩掉我的做法是捕获后把异常信息写到 State 的nodeOutputs里流程继续走由后续的条件节点决定怎么处理。3.3 条件分支的实现细节条件节点是低代码平台里最容易被低估的部分。表面上看就是 if-else但实际做起来有几个坑。第一个坑是表达式的上下文条件表达式里引用的变量必须能从 State 里取到我一开始只支持nodeOutputs.xxx的写法后来发现业务侧更习惯直接写变量名于是加了一层变量解析先查variables再查nodeOutputs。第二个坑是条件节点的出边。LangGraph4j 的addConditionalEdges需要一个路由函数返回下一个节点的名字。我的实现是把 config 里的trueNext和falseNext传给路由函数路由函数求值表达式后返回对应的节点名。这里要注意路由函数返回的节点名必须在图里存在否则运行时会报错。我在配置保存时加了一层校验检查所有边的目标节点是否存在避免运行时才发现问题。第三个坑是嵌套条件。业务侧有时候需要“如果 A 且 B 则走 X否则走 Y”这种复合条件用简单的 true/false 分支表达不了。我的做法是允许条件节点嵌套一个条件节点的trueNext指向另一个条件节点形成条件链。虽然配置上麻烦一点但逻辑清晰调试也方便。3.4 RAG 节点的检索链路RAG 节点在合同审查、知识问答这类场景里用得很多。LangChain4j 的检索链路是EmbeddingStoreEmbeddingStoreContentRetrieverContentInjector。我的 RAG 节点构建逻辑是从 config 里取知识库 ID 和查询模板用查询模板填充 State 变量得到查询语句调用 Retriever 拿到相关文档把文档内容拼进 prompt再调用 ChatModel 生成回答。这里有个性能优化点EmbeddingStoreContentRetriever每次检索都会做 Embedding如果同一个查询在流程里被多次用到可以加一层缓存。我用 Caffeine 做了一个简单的查询缓存key 是查询语句的哈希value 是检索结果过期时间设了 10 分钟。实测下来在批量审查场景里能省不少时间。提示RAG 节点的maxResults和minScore这两个参数很关键。maxResults控制返回文档数太多会撑爆 prompt太少可能漏掉关键信息。minScore控制相似度阈值设太低会引入无关文档。我的经验值是maxResults5、minScore0.7具体要看你的 Embedding 模型和文档质量。4. 实操过程与核心环节实现4.1 环境准备与依赖配置先说一下依赖。LangChain4j 和 LangGraph4j 的版本要匹配我用的是 LangChain4j 0.35.0 和 LangGraph4j 1.0.0。Maven 依赖大概是这样dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.35.0/version /dependency dependency groupIdorg.bsc.langgraph4j/groupId artifactIdlanggraph4j-core/artifactId version1.0.0/version /dependency如果你用的是国产模型比如通义千问或者 DeepSeekLangChain4j 有对应的适配模块把langchain4j-open-ai换成对应的依赖就行。LangGraph4j 本身不绑定模型它只负责图编排模型调用还是走 LangChain4j。配置方面ChatModel 的 Bean 配置要注意超时和重试。我一开始没设超时模型响应慢的时候整个流程卡死。后来加了timeout(Duration.ofSeconds(30))和maxRetries(2)稳定性好了很多。EmbeddingModel 的配置类似但超时可以设短一点因为 Embedding 通常比对话快。4.2 流程定义的解析与图构建配置层的 JSON 传到后端后第一步是解析成FlowDefinition对象第二步是校验第三步是构建 StateGraph。校验包括节点 ID 唯一性、边目标存在性、条件表达式语法、必填参数完整性。校验不通过直接返回错误不要让有问题的配置进入构建阶段。构建 StateGraph 的代码大概是这样public CompiledGraphWorkflowState buildGraph(FlowDefinition definition) { StateGraphWorkflowState graph new StateGraph(WorkflowState::new); for (NodeDef node : definition.getNodes()) { NodeBuilder builder nodeBuilderFactory.getBuilder(node.getType()); NodeActionWorkflowState action builder.build(node.getConfig(), context); graph.addNode(node.getId(), action); } for (NodeDef node : definition.getNodes()) { if (CONDITION.equals(node.getType())) { graph.addConditionalEdges(node.getId(), state - routeCondition(node, state), Map.of(true, node.getConfig().getTrueNext(), false, node.getConfig().getFalseNext())); } else if (node.getNext() ! null) { graph.addEdge(node.getId(), node.getNext()); } } graph.addEdge(END, StateGraph.END); return graph.compile(); }这里有个细节StateGraph的入口节点需要显式设置我用的是START类型的节点作为入口。compile()之后得到CompiledGraph每次执行流程时调用invoke传入初始 State 即可。4.3 工具注册与动态调用工具注册是平台能力扩展的关键。我定义了一个ToolRegistry启动时扫描所有带Tool注解的 Bean把工具名和ToolSpecification注册进去。工具名默认用方法名也支持通过注解参数自定义。Component public class ContractTools { Tool(查询企业工商信息) public String queryCompanyInfo(P(企业名称) String companyName) { // 调用外部 API return companyInfoService.query(companyName); } Tool(计算合同风险分) public int calculateRiskScore(P(合同金额) double amount, P(合同期限) int months) { // 风险计算逻辑 return riskCalculator.calculate(amount, months); } }TOOL节点在构建时从 config 里取工具名从ToolRegistry里找到对应的ToolSpecification然后用 LangChain4j 的ToolExecutor执行。参数映射的规则是config 里的paramMapping定义 State 变量到工具参数的映射关系比如{companyName: variables.companyName}。注意工具执行一定要加超时和异常捕获。外部 API 不可控一个工具卡住不能影响整个流程。我的做法是用CompletableFuture包一层超时时间设 10 秒超时后返回错误信息流程继续走。4.4 流程执行与状态追踪流程执行时CompiledGraph.invoke会按图的结构依次执行节点每个节点的输出写回 State。为了追踪执行过程我在每个节点函数里加了一个埋点记录节点 ID、开始时间、结束时间、输入输出摘要。这些埋点数据写到日志和数据库前端可以拉取展示执行链路。执行日志的表结构大概是flow_instance_id、node_id、node_type、status、input_summary、output_summary、start_time、end_time、error_msg。有了这份日志排查问题时能直接看到哪个节点出了错、输入输出是什么比翻代码快得多。这里有个经验输入输出摘要不要存全量存前 500 个字符就行。全量数据太大查询慢而且大部分时候看摘要就够了。需要看全量的时候再根据flow_instance_id去查原始记录。5. 常见问题与排查技巧实录5.1 流程执行卡住不动怎么排查这是最常见的问题。流程执行卡住通常有三个原因模型调用超时、工具调用阻塞、条件路由死循环。排查顺序是先看执行日志找到最后一个有记录的节点如果这个节点是LLM或TOOL类型大概率是调用超时如果日志显示节点执行完了但流程没继续检查条件路由的返回值是否匹配到了下一个节点。死循环的排查稍微麻烦一点。LangGraph4j 本身没有循环检测如果条件路由把流程导回了之前的节点就会无限循环。我的做法是在 State 里加一个visitedNodes列表每次进入节点时检查是否超过最大访问次数我设的是 10 次超过就抛异常终止流程。这个检查放在节点函数的入口成本很低但能救命。5.2 条件表达式求值报错怎么处理条件表达式报错通常是变量取不到或者类型不匹配。变量取不到的原因可能是上游节点没有输出这个变量或者变量名写错了。我的做法是在表达式求值前先做一次变量存在性检查缺失的变量给出明确的错误提示而不是直接抛 NPE。类型不匹配常见于数字比较。JSON 里的数字解析出来可能是Integer也可能是Double直接比较会出问题。我在表达式解析器里做了统一的数字类型转换比较前都转成BigDecimal避免精度问题。5.3 工具调用返回结果解析失败工具返回的结果格式不确定有时候是 JSON有时候是纯文本有时候是空。如果后续节点依赖工具的输出解析失败就会导致流程中断。我的处理方式是工具调用统一返回ToolResult对象包含success、data、errorMsg三个字段。节点函数根据success决定后续逻辑失败时把errorMsg写到 State由条件节点决定是重试还是走异常分支。5.4 常见问题速查表问题现象可能原因排查方法解决方案流程卡住不动模型/工具调用超时查看执行日志最后节点加超时配置检查外部服务流程无限循环条件路由指回上游检查 visitedNodes加最大访问次数限制条件表达式报错变量缺失或类型不匹配打印 State 内容加变量检查统一数字类型工具结果解析失败返回格式不确定查看工具原始返回统一 ToolResult 封装图构建失败节点 ID 重复或边目标不存在查看校验日志配置保存时加校验RAG 检索结果差maxResults/minScore 不合理调整参数对比根据场景调优5.5 几个踩过的坑和实操心得第一个坑LangGraph4j 的StateGraph在compile()之后不能再修改如果流程需要动态调整必须重新构建图。我一开始想复用图对象后来发现不行改成每次流程执行前根据最新配置构建图构建成本很低实测几百个节点的图构建也就几十毫秒。第二个坑LangChain4j 的ChatModel是有状态的多线程共用同一个实例会有问题。我的做法是用ThreadLocal或者每次调用创建新实例。后来发现 LangChain4j 的OpenAiChatModel本身是线程安全的但如果你用了带记忆的ChatMemory那就要注意隔离。第三个坑低代码配置的版本管理。业务侧改了流程配置如果直接覆盖出问题就回不去了。我的做法是每次保存生成一个新版本流程执行时绑定版本号出问题可以快速回滚到旧版本。这个功能看起来简单但在实际运维中救过好几次命。第四个坑前端拖拽生成的 JSON 和后端解析的模型要对齐。我一开始前后端各定义了一套模型结果字段名不一致调试了半天。后来统一用同一份 JSON Schema前端根据 Schema 生成表单后端根据 Schema 做校验问题就没了。6. 平台扩展与后续演进方向这套平台跑起来之后扩展点主要在三个方向。第一个是节点类型的扩展目前有六种后面可以加HTTP节点直接调外部接口、CODE节点执行一段脚本、SUBFLOW节点调用子流程。每加一种节点类型只需要实现一个NodeBuilder并注册到工厂里不用动核心代码。第二个是执行引擎的扩展。目前是同步执行适合短流程。如果流程很长比如涉及人工审批就需要异步执行加状态持久化。LangGraph4j 支持checkpoint可以把 State 存到数据库流程暂停后从 checkpoint 恢复。这个能力在做审批流的时候很有用。第三个是多智能体协作。LangGraph4j 的图可以嵌套一个节点可以是一个子图。把每个子图封装成一个智能体主图负责协调多个智能体之间的消息传递就能实现多智能体协作。这个方向我还在探索目前的想法是用Supervisor模式一个主智能体负责任务分解和结果汇总多个子智能体负责具体执行。最后分享一个实际使用中的体会低代码平台的价值不在于“不用写代码”而在于“把变化的部分和稳定的部分分开”。稳定的部分沉淀成节点和工具变化的部分交给配置。这样业务侧改流程不用等技术排期技术侧也不用为每个流程写重复代码。但前提是边界要划清楚什么该配置化、什么该代码化这个判断需要在实际项目中慢慢磨。我见过太多低代码平台因为边界没划好最后变成“低代码写代码”比直接写代码还麻烦。
返回列表