1. 项目概述为什么需要一个“低代码工作流通用智能体平台”我做AI工程落地快五年了从最早手写Prompt模板、硬编码调用大模型API到后来用Spring AI封装基础能力再到去年开始大规模接入RAG和Agent模式——踩过的坑比写的代码还多。直到上个月我们团队在给一家制造业客户做智能工单系统时被彻底卡住了业务方提了17个不同场景的流程需求——设备报修自动分派知识库检索维修记录生成短信通知工单闭环确认多级审批驳回重填……每个流程背后都涉及异构系统对接MES/ERP/钉钉/微信、非结构化文档解析PDF维修手册、动态决策逻辑是否触发备件采购、以及人工干预节点工程师确认方案。如果按传统方式逐个开发光接口联调就要三周更别说后续维护和业务变更。这时候“基于 LangChain4j LangGraph4j 的低代码工作流通用智能体平台架构设计”这个方向就不是技术选型问题而是生存问题。它要解决的是让业务分析师能拖拽定义“谁在什么条件下做什么”而开发者只需提供可复用的原子能力模块比如‘查知识库’‘发钉钉消息’‘解析PDF表格’中间的编排、状态管理、错误重试、人机协同、可观测性全部由平台兜底。LangChain4j 不是简单翻译 Java 版 LangChain它把 LLM 调用、工具绑定、提示词管理、输出解析这些底层能力做了强类型封装LangGraph4j 更不是图计算库它是专为 Agent 工作流设计的状态机引擎——支持条件分支、循环、并行、中断恢复、状态快照且所有节点都是纯 Java 方法没有魔法字符串或 YAML 魔法配置。这两个库组合起来恰好填补了 Spring 生态里缺失的“可编程智能体编排层”。你不需要懂 React 或 Vue 就能搭出前端配置面板也不需要研究 Camunda 的 BPMN XML 规范就能定义复杂流程——因为它的核心抽象是State → Node → Edge → State而 State 是 POJONode 是 Bean 方法Edge 是 Predicate 函数。这才是真正面向 Java 工程师的低代码。关键词“LangChain4j”“LangGraph4j”“低代码”“工作流”“智能体”不是堆砌它们各自承担不可替代的角色LangChain4j 提供原子能力底盘模型调用、RAG 检索、工具执行LangGraph4j 提供流程骨架状态流转、节点调度、异常处理低代码是交付形态可视化编排配置即代码工作流是业务表达顺序/分支/循环/等待智能体是最终产物能感知、决策、行动、反馈的软件实体。这五者缺一不可少了任何一个都会退化成“半自动脚本”或“高门槛定制开发”。2. 整体架构设计与核心思路拆解2.1 四层架构从能力沉淀到业务交付的完整链路我们最终落地的架构不是单体应用也不是微服务套壳而是清晰划分的四层能力层 → 编排层 → 配置层 → 交付层。每一层都有明确边界、独立演进路径和可观测入口避免出现“改一个按钮导致整个工作流引擎崩溃”的灾难。能力层Capability Layer这是整个平台的地基。它不包含任何业务逻辑只提供标准化、可插拔、带元数据描述的原子能力。比如KnowledgeBaseSearch能力它封装了向 Milvus 向量库发起语义检索的全过程但对外暴露的是search(String query, String collectionName)方法再比如DingTalkNotifier它封装了钉钉机器人签名、消息格式、重试策略对外只暴露send(String title, String content, ListString userIds)。关键点在于每个能力必须实现Capability接口该接口强制定义id()唯一标识、name()中文名、description()功能说明、inputSchema()JSON Schema 描述输入参数、outputSchema()输出结构。这样上层才能自动生成表单、校验参数、渲染文档。我们拒绝“能力即 Service Bean”的粗放做法——Service Bean 是运行时对象而 Capability 是契约对象前者关注怎么执行后者关注怎么被使用。编排层Orchestration Layer这是 LangGraph4j 发挥威力的地方。它不负责能力实现只负责“如何把能力串起来”。我们定义了WorkflowDefinition类它是一个 POJO包含nodes节点列表、edges边列表、entryPoint入口节点ID、stateType状态类Class。每个Node对应一个 Capability 的调用但不是直接调用而是通过NodeBuilder构建NodeBuilder.of(search_kb).capability(knowledge_base_search).inputMapping(Map.of(query, state.userQuery, collection, manuals))。这里inputMapping是核心创新——它支持 SpEL 表达式允许将当前 State 中的字段、常量、甚至其他节点的输出结果映射为该节点的输入参数。比如state.lastResult.content可以作为下一个节点的 prompt 输入。Edge则定义了流转条件Edge.from(search_kb).to(generate_report).condition(state.searchResults.size() 0)。这种设计让流程逻辑完全脱离硬编码变成可序列化、可版本化、可灰度发布的配置。配置层Configuration Layer这是低代码的体现。我们没自己造前端而是基于开源低代码引擎如 Appsmith二次开发了一个“智能体工作流设计器”。它渲染的不是 HTML 表单而是WorkflowDefinition的 JSON Schema。用户拖拽一个“知识库搜索”节点设计器自动根据knowledge_base_search能力的inputSchema渲染出查询字段、知识库下拉框、相似度阈值滑块连接两个节点时自动根据目标节点的inputSchema和源节点的outputSchema做字段映射建议。所有操作最终生成标准 JSON存入 PostgreSQL 的workflow_definitions表。关键设计是配置即代码Config as Code。每次保存系统自动生成一个 Git Commit通过 JGit 集成推送到内部 GitLab 的workflows/{tenantId}/{workflowId}仓库。这意味着你可以用git diff看流程变更用git revert回滚错误配置用 CI 流水线做语法校验和沙箱测试——低代码不等于无版本控制。交付层Delivery Layer这是智能体的“出厂设置”。每个WorkflowDefinition经过校验后会被编译成一个WorkflowRunnerBean注入 Spring 容器。用户通过 REST API 或 SDK 触发工作流时平台创建一个WorkflowInstance含唯一 ID、初始 State、执行上下文交由WorkflowEngine执行。WorkflowEngine是 LangGraph4j 的Graph实例它加载WorkflowDefinition构建有向无环图DAG然后驱动状态机运行。执行过程全程可追踪每个节点的输入/输出、耗时、异常堆栈、重试次数都写入 Elasticsearch。我们还内置了“人工干预点”当某个节点返回HumanInterventionRequired状态时自动在企业微信创建待办任务工程师处理后通过回调 API 将结果写回WorkflowInstance的 State引擎自动恢复执行。这才是真正的“人机协同”不是把人当黑盒而是把人当作流程中的一个可编程节点。2.2 为什么选 LangGraph4j 而不是 Spring State Machine 或 Camunda这个问题我们内部争论了整整两周。Spring State MachineSSM很成熟Camunda 在 BPMN 领域是事实标准但它们都不适合智能体工作流。原因很具体SSM 的状态是枚举不是 POJOSSM 要求你定义States如WAITING,PROCESSING,DONE和Events如START,ERROR,COMPLETE然后用withExternal().source(WAITING).target(PROCESSING).event(START)去配置流转。但智能体的工作流状态是动态的、富含数据的{userQuery: 如何更换轴承, searchResults: [...], selectedSolution: {...}, approvalStatus: pending}。SSM 无法承载这种结构化状态强行塞进去会导致状态管理混乱调试时根本不知道PROCESSING状态下searchResults是空还是有10条结果。Camunda 的 BPMN 是面向人的流程不是面向 AI 的流程BPMN 擅长描述“张三审批后李四收到通知”但它对“LLM 根据检索结果生成报告并判断是否需要人工复核”这种 AI 决策逻辑支持极弱。你需要写大量 Java Delegate 或 Script Task把 AI 逻辑硬塞进 BPMN 的 XML 里失去类型安全和 IDE 支持。更致命的是BPMN 的“并行网关”在 AI 场景下容易引发竞态两个并行节点都去调用同一个 LLM API结果互相覆盖 State。LangGraph4j 的StateGraph天然支持并发节点但所有节点共享同一个State对象且State是不可变的每次更新返回新实例从根本上杜绝了竞态。LangGraph4j 的“检查点Checkpoint”机制是救命稻草智能体工作流动辄几分钟中间可能调用多个外部 API知识库、ERP、邮件服务。如果第5步失败传统方案只能从头重跑浪费算力和时间。LangGraph4j 允许你在任意节点后设置checkpoint()引擎会自动序列化当前State到 Redis。下次重启时它能从最后一个检查点恢复跳过已成功执行的前4步。我们实测过一个包含7个节点、总耗时210秒的工作流第6步失败后从检查点恢复仅需3秒。这个能力是 SSM 和 Camunda 都不具备的。2.3 低代码的边界在哪里哪些必须写代码“低代码”不是“零代码”它的核心是把重复劳动交给平台把创造性劳动留给开发者。我们划了三条清晰的红线绝对禁止低代码的能力实现Capability ImplementationKnowledgeBaseSearch怎么连 Milvus、用什么 Embedding 模型、如何做 Rerank必须 Java 代码实现。低代码配置只能决定“用哪个知识库”不能决定“怎么检索”。状态模型定义State ClassWorkflowDefinition的stateType必须是一个真实的 Java Class比如MaintenanceWorkflowState。它必须有Data注解字段要有Schema(description ...)。平台会用 Jackson 反序列化 JSON 到这个 Class。你不能在设计器里“画”出一个状态结构那会失去编译期检查和 IDE 自动补全。异常处理策略Error Handling Policy某个节点失败后是重试3次降级到备用能力还是直接终止并通知这些策略必须在WorkflowDefinition的node配置中用代码写死比如.retryPolicy(RetryPolicy.builder().maxAttempts(3).backoff(Duration.ofSeconds(2)).build())。设计器只提供预设模板“指数退避”“固定间隔”不能让用户自由写 Groovy 脚本。必须低代码的节点连接Node Connection谁调用谁什么条件下流转必须可视化拖拽。手写Edge配置极易出错且无法做拓扑校验比如循环依赖。参数映射Parameter Mappingstate.userQuery映射到search_kb.querystate.searchResults.get(0).content映射到generate_report.prompt这种字符串到字符串的映射必须由设计器的字段选择器完成。手写 SpEL 表达式#state.searchResults[0].content容易拼错且无法做静态校验。触发条件配置Trigger Configuration工作流何时启动是 HTTP 请求是数据库表变更是定时任务这些触发器的参数URL、SQL、Cron 表达式必须在低代码面板配置平台自动生成对应的EventListener或ScheduledBean。这条边界让我们避免了“低代码陷阱”既没有陷入“所有东西都要配”的配置地狱也没有退回“所有东西都要写”的开发泥潭。业务方在设计器里花2小时能搭出80%的流程剩下的20%由开发者用1天代码补全整体效率提升5倍以上。3. 核心细节解析与实操要点3.1 LangChain4j 能力封装不只是调用 API而是构建可组合的“积木”LangChain4j 的AiServices是个好东西但直接用它封装能力会很快撞墙。比如你想封装一个“从 PDF 提取表格”的能力用AiServices.create(TableExtractor.class, model)看似简单。但实际中TableExtractor接口的方法签名是ListTable extract(InputStream pdfStream)而 LangChain4j 的AiServices默认只支持String输入输出。你得自己写PromptTemplate、OutputParser还要处理流式上传、内存溢出、超时熔断——这已经不是“封装”而是“重写”。我们的解法是绕过AiServices直接使用ChatModel和ToolExecutor把能力封装成标准的FunctionCallingModel工具。步骤如下定义能力接口Contract Firstpublic interface PdfTableExtractor { /** * 从PDF文件中提取所有表格 * param pdfUrl PDF文件的HTTP URL平台已预处理为可公开访问链接 * param pageRange 要提取的页码范围如 1-5 或 3 * return 表格列表每个表格是二维字符串数组 */ Tool(extract_tables_from_pdf) ListString[][] extract(ToolParam(pdf_url) String pdfUrl, ToolParam(page_range) String pageRange); }注意Tool和ToolParam注解这是 LangChain4j 的工具发现机制。实现能力ImplementationComponent public class PdfTableExtractorImpl implements PdfTableExtractor { private final PdfBoxTableExtractor pdfBoxExtractor; // 真正的PDF解析引擎 public PdfTableExtractorImpl(PdfBoxTableExtractor extractor) { this.pdfBoxExtractor extractor; } Override public ListString[][] extract(String pdfUrl, String pageRange) { try { // 1. 下载PDF带超时和重试 byte[] pdfBytes downloadWithRetry(pdfUrl, 3, Duration.ofSeconds(30)); // 2. 解析表格带内存限制 return pdfBoxExtractor.extractTables(pdfBytes, pageRange); } catch (Exception e) { throw new CapabilityExecutionException(PDF表格提取失败, e); } } private byte[] downloadWithRetry(String url, int maxRetries, Duration timeout) { // 使用 Resilience4j 实现指数退避重试 return Retry.decorateSupplier( retry, () - webClient.get().uri(url).retrieve().bodyToMono(byte[].class) .block(timeout) ).get(); } }注册为 LangChain4j 工具RegistrationConfiguration public class CapabilityConfig { Bean public Tool pdfTableExtractorTool(PdfTableExtractor extractor) { // 将能力实例包装成 LangChain4j 的 Tool 对象 return ToolProvider.tool(extractor); } Bean public AiModel aiModel(ChatModel chatModel, ListTool tools) { // 创建支持函数调用的模型 return AiModel.builder() .chatModel(chatModel) .tools(tools) .build(); } }这样封装后PdfTableExtractor就成了一个标准的 LangChain4j 工具可以被任何AiServices或FunctionCallingModel调用。更重要的是它天然支持 LangChain4j 的ToolExecutor可以被 LangGraph4j 的ToolNode直接消费。我们实测过一个 50MB 的 PDF提取 12 个表格平均耗时 8.2 秒错误率低于 0.3%。关键经验不要试图让 LangChain4j 去“理解”你的业务逻辑而是让它成为你业务逻辑的“调度员”。你的PdfTableExtractorImpl是核心LangChain4j 只是帮你把它暴露给 AI 模型的桥梁。3.2 LangGraph4j 工作流编排状态是灵魂节点是肌肉LangGraph4j 的StateGraph是核心但它的文档截至2024年10月非常简略很多坑得自己趟。我们总结了三个最关键的实操要点State 必须是不可变的Immutable这是 LangGraph4j 的设计哲学。你不能写state.setSearchResults(results)而必须写return state.toBuilder().searchResults(results).build()。为什么因为 LangGraph4j 的Graph在执行时会把当前State传给每个NodeNode处理后返回一个新State引擎再把这个新State传给下一个Node。如果State是可变的多个并行Node同时修改同一个对象必然数据错乱。我们一开始没注意在并行节点里直接state.addMessage(...)结果日志里看到state.messages时有时无排查了两天才发现是竞态。解决方案用 Lombok 的Builder和With注解让State类支持流畅的不可变更新Data Builder With public class MaintenanceWorkflowState { private String userQuery; private ListKnowledgeBaseResult searchResults; private String generatedReport; private String approvalStatus; // pending, approved, rejected private ListMessage messages; // 用于人机对话历史 }Node 必须是无状态的Stateless每个Node方法的签名必须是State - State不能依赖外部变量如Autowired的 Service。为什么因为 LangGraph4j 的Graph可能被序列化到 Redis用于检查点而Autowired的 Bean 是 Spring 容器管理的无法序列化。正确做法是把所有依赖注入到Node的构造函数中然后在Graph构建时用 Lambda 包装Bean public GraphMaintenanceWorkflowState maintenanceWorkflowGraph( PdfTableExtractor pdfExtractor, KnowledgeBaseSearch kbSearch, DingTalkNotifier notifier) { // 定义节点 NodeMaintenanceWorkflowState searchNode state - { ListKnowledgeBaseResult results kbSearch.search(state.getUserQuery(), manuals); return state.toBuilder().searchResults(results).build(); }; NodeMaintenanceWorkflowState notifyNode state - { if (!state.getSearchResults().isEmpty()) { notifier.send(新工单, 查询到 state.getSearchResults().size() 条结果, List.of(admin)); } return state; }; // 构建图 return StateGraph.builder(MaintenanceWorkflowState.class) .addNode(search_kb, searchNode) .addNode(notify_admin, notifyNode) .addEdge(START, search_kb) .addEdge(search_kb, notify_admin) .addEdge(notify_admin, END) .build(); }注意searchNode和notifyNode是匿名函数它们捕获了kbSearch和notifier这两个 Bean但自身不持有状态。这样Graph对象就是纯数据结构可以安全序列化。Edge 的 condition 必须是纯函数Pure FunctionEdge.from(search_kb).to(generate_report).condition(state.searchResults.size() 0)这里的 SpEL 表达式必须是无副作用的。你不能在里面调用service.doSomething()因为condition可能在引擎内部被多次调用比如做拓扑排序时。我们曾在一个 condition 里写了log.info(checking condition)结果发现日志刷屏性能暴跌。正确做法所有业务逻辑放在Node里condition只做简单的布尔判断。如果需要复杂判断先在Node里计算一个标志位比如state.toBuilder().hasResults(!results.isEmpty()).build()然后 condition 写state.hasResults。3.3 低代码设计器的关键实现让业务方真正“所见即所得”我们基于 Appsmith 二次开发的设计器核心不是炫酷的拖拽效果而是确保前端配置和后端执行的 100% 语义一致。这需要三个关键技术点Schema 驱动的表单渲染Schema-Driven Form每个Capability的inputSchema()返回一个标准 JSON Schema符合 https://json-schema.org 规范。设计器拿到这个 Schema 后用开源库react-jsonschema-form动态渲染表单。比如KnowledgeBaseSearch的 Schema{ type: object, properties: { query: { type: string, title: 查询关键词, description: 将用于语义检索 }, collectionName: { type: string, title: 知识库, enum: [manuals, troubleshooting, safety], enumNames: [设备手册, 故障排除, 安全规范] } } }设计器会自动渲染一个带标题、描述、下拉框带中文选项的表单。用户填完前端直接JSON.stringify()得到配置对象和服务端WorkflowDefinition的字段一一对应。没有魔法全是标准。实时 SpEL 表达式校验Real-time SpEL Validation当用户在“参数映射”框里输入state.searchResults[0].content时设计器不能等到保存才报错。我们集成了 Spring 的SpelExpressionParser在前端用 WebAssembly 编译了一个轻量版解析器基于 https://github.com/spring-projects/spring-framework/tree/main/spring-expression 的简化版实时校验表达式语法和字段存在性。如果用户手误写成state.searchResutls[0].content少了个l输入框立刻标红并提示“字段 searchResutls 不存在”。这个功能让配置错误率下降了 70%。GitOps 集成GitOps Integration每次点击“保存”设计器不是直接写数据库而是生成一个唯一的workflowIdUUID构建一个 Git Commit内容是workflow-definition.json文件推送到gitgitlab.internal:workflows/{tenantId}/{workflowId}.git调用平台的/api/v1/workflows/sync接口触发 Webhook平台拉取最新 Commit校验 JSON Schema然后更新数据库和 Spring 容器中的WorkflowRunnerBean。这样业务方在设计器里点一下背后就是一次标准的 Git 提交。运维可以git log查谁改了什么SRE 可以用git bisect快速定位导致故障的那次提交。低代码也可以很工程化。4. 实操过程与核心环节实现4.1 从零搭建平台环境准备与依赖管理我们用的是 Spring Boot 3.3 Java 21这是 LangChain4j 0.30 和 LangGraph4j 0.10 的最低要求。Maven 依赖不是简单地langchain4j-spring-boot-starter一把梭而是精细化拆分确保每个模块职责单一!-- 核心能力层 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.30.0/version /dependency !-- OpenAI 兼容的模型客户端我们用的是阿里云百炼 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.30.0/version /dependency !-- 向量库客户端 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-milvus/artifactId version0.30.0/version /dependency !-- 编排层 -- dependency groupIddev.langgraph4j/groupId artifactIdlanggraph4j/artifactId version0.10.0/version /dependency !-- Spring Boot 集成 -- dependency groupIddev.langgraph4j/groupId artifactIdlanggraph4j-spring-boot-starter/artifactId version0.10.0/version /dependency !-- 配置层低代码 -- dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency关键点不要引入langchain4j-spring-boot-starter。这个 starter 会自动配置一个全局的ChatModel但我们的平台需要为不同租户、不同工作流配置不同的模型有的用 Qwen有的用 DeepSeek有的用本地 Llama3。所以我们手动配置ChatModelBeanConfiguration public class ModelConfig { Bean ConditionalOnProperty(name llm.provider, havingValue qwen) public ChatModel qwenChatModel() { return OpenAiChatModel.builder() .baseUrl(https://dashscope.aliyuncs.com/api/v1) .apiKey(System.getenv(QWEN_API_KEY)) .modelName(qwen-max) .timeout(Duration.ofSeconds(60)) .build(); } Bean ConditionalOnProperty(name llm.provider, havingValue deepseek) public ChatModel deepseekChatModel() { return OpenAiChatModel.builder() .baseUrl(https://api.deepseek.com/v1) .apiKey(System.getenv(DEEPSEEK_API_KEY)) .modelName(deepseek-chat) .timeout(Duration.ofSeconds(120)) .build(); } }这样通过配置llm.providerqwen就能切换模型无需改代码。我们实测过Qwen 在中文技术文档理解上比 DeepSeek 高 12%但 DeepSeek 在长文本生成上更稳定所以平台必须支持多模型共存。4.2 构建第一个智能体简历筛选工作流“简历筛选”是客户最常问的 Demo因为它直观体现了智能体的价值从“看100份简历”到“看10份高质量简历”。我们用这个案例完整走一遍平台搭建流程。Step 1定义能力Capability我们需要三个原子能力ResumeParser解析 PDF/Word 简历提取姓名、电话、邮箱、工作经验、技能等字段。JobDescriptionMatcher计算简历与职位描述的匹配度用向量相似度 关键词加权。EmailSender给 HR 发送筛选结果邮件。ResumeParser的实现很典型它不调用 LLM而是用 Apache POIWord和 PDFBoxPDF做规则解析只有当遇到模糊字段如“3年Java经验”时才调用 LLM 做 NER。这样既快又准成本只有纯 LLM 方案的 1/5。Step 2定义状态StateData Builder With public class ResumeScreeningState { private String resumeUrl; // 简历文件URL private String jobDesc; // 职位描述文本 private ParsedResume parsedResume; // 解析后的简历对象 private double matchScore; // 匹配分数0-100 private boolean isQualified; // 是否合格 private String emailContent; // 邮件正文 }Step 3设计工作流Workflow Definition在设计器里我们拖拽三个节点parse_resume调用ResumeParser能力输入resumeUrl输出parsedResume。calculate_match调用JobDescriptionMatcher输入parsedResume和jobDesc输出matchScore和isQualified。send_email调用EmailSender输入emailContent由isQualified动态生成。连接逻辑START→parse_resumeparse_resume→calculate_match无条件calculate_match→send_email条件state.isQualified truecalculate_match→END条件state.isQualified falseStep 4配置触发器Trigger我们配置了一个 HTTP 触发器Method: POSTPath:/api/v1/screen-resumeRequest Body Schema:{ type: object, properties: { resumeUrl: {type: string}, jobDesc: {type: string} } }平台自动生成 Spring MVC Controller接收请求创建ResumeScreeningState启动WorkflowRunner。Step 5部署与测试打包成 Docker 镜像部署到 Kubernetes。用curl测试curl -X POST http://platform/api/v1/screen-resume \ -H Content-Type: application/json \ -d {resumeUrl: https://oss.example.com/resumes/123.pdf, jobDesc: 招聘Java高级工程师要求5年Spring Cloud经验...}日志显示parse_resume耗时 1.2scalculate_match耗时 0.8ssend_email耗时 0.3s总耗时 2.3s。HR 收到邮件“候选人张三匹配度 92%推荐面试”。而传统方式HR 看完100份简历要花8小时。4.3 平台可观测性不只是日志而是“工作流数字孪生”智能体工作流一旦上线最大的恐惧不是它不工作而是它“看起来在工作其实错了”。比如calculate_match节点返回了matchScore0但没报错send_email节点就发了一封“匹配度0%”的邮件HR 当场懵了。所以我们投入了30%的开发时间做可观测性目标是任何一次工作流执行都能在5秒内定位到问题节点、输入、输出、耗时、异常堆栈。我们用了三层可观测体系第一层结构化日志Structured Logging每个Node执行前后自动打印一条 JSON 日志{ workflowId: wf-abc123, instanceId: inst-xyz789, nodeId: calculate_match, status: STARTED, timestamp: 2024-10-25T14:23:45.123Z, input: {parsedResume: {name: 张三, ...}, jobDesc: Java高级工程师...}, stateVersion: 2 }和{ workflowId: wf-abc123, instanceId: inst-xyz789, nodeId: calculate_match, status: COMPLETED, timestamp: 2024-10-25T14:23:45.987Z, output: {matchScore: 92.5, isQualified: true}, durationMs: 864, stateVersion: 3 }这些日志通过 Logstash 推送到 Elasticsearch。Kibana 里输入instanceId: inst-xyz789就能看到完整的执行链路像看快递物流一样清晰。第二层指标监控Metrics我们用 Micrometer 暴露了 12 个关键指标workflow.execution.duration.seconds.max各工作流的最大执行时长workflow.node.execution.count各节点的调用次数workflow.node.error.count各节点的错误次数按异常类型分组workflow.instance.active.count当前正在运行的实例数Grafana 仪表盘上一眼就能看出哪个工作流最近错误飙升哪个节点成了性能瓶颈。比如parse_resume的error.count突然涨到每分钟10次点进去看异常类型是PdfParseException就知道是某批 PDF 格式不规范立刻通知业务方清洗数据。第三层分布式追踪Tracing集成 OpenTelemetry为每次WorkflowInstance创建一个 Trace。Span