Java实现Agent工作流流程引擎:状态轮转与流式输出实战

发布时间:2026/10/2 9:06:39
Java实现Agent工作流流程引擎:状态轮转与流式输出实战 先说下背景。上个月在公司接了个AI客服Agent的活需求听着不复杂用户提问进来先做意图识别命中“查订单”就去调订单接口拿到物流信息再丢给大模型润色成一句人话返回。四步流程嘛串起来就完了。我第一版也确实写得“很顺畅”——一个方法里塞了两百多行if-else意图判断用if工具分发用switch超时重试再套个大try-catch。跑通是跑通了但代码Review的时候同事一句话把我问住了“再加两个节点你还能维护吗”这话戳到我了。回头翻了一下现在主流的Agent框架人家早就不是这么干了。核心差别就是流程不再写死在业务代码里而是被抽象成一个个节点由一个流程引擎统一驱动流转和状态管理。这篇文章就记录一下我用Java从0到1实现Agent工作流流程引擎的过程重点解决两件事节点状态轮转和流式输出。写完之后再回去看那些Agent业务代码真的舒服太多。如果你也正在用if-else堆Agent逻辑或者刚接触Agent开发不知道怎么做流程编排这篇应该能给你一个能直接落地的参考。1. Agent工作流为什么必须上流程引擎if-else的瓶颈在哪1.1 先拆解一下Agent工作流本质上是什么Agent工作流听起来玄乎但落到代码层面就三样东西节点、边、数据。节点就是一个个执行单元比如“调大模型写一段话”是一个节点“调用订单查询工具”是一个节点“判断用户意图然后走哪个分支”也是一个节点。边定义了节点之间的先后关系和条件数据就是节点之间传递的上下文比如意图识别节点输出的“查订单”要传给下游的工具调用节点。你去看LangGraph、CrewAI、Coze这些主流框架的设计基本都是这个思路。一个Agent任务就是一张有向图跑起来的时候从起点节点进入沿着边一路执行到终点节点。中间可能会走分支、可能会循环但所有节点的状态、数据流动、异常处理都被框架接管了。我当时第一版用if-else实现本质上也是在手动管理这张图的流转只不过是用代码硬编码的前一步做什么、下一步做什么、失败了怎么跳全部散落在方法体里。问题在于这种“隐式流程”只有写代码的人自己脑子里有图代码本身看不出流程长什么样。1.2 if-else写法的四个致命硬伤我在重构前特意列了一下if-else方案的痛点梳理成表格方便对比维度if-else硬编码流程引擎驱动流程可见性执行到哪一步只能靠日志猜没有统一的状态视图每个节点都有显式状态PENDING、RUNNING、SUCCEEDED一目了然重试与补偿手动加循环、套try-catch每个分支都得单独处理状态机规定FAILED可以转回PENDING引擎统一触发重试扩展成本新增一个节点主流程方法就要改一遍新增一个节点类并注册到定义文件主流程零改动并发能力多个Agent请求混在一个方法栈里上下文全靠局部变量传递每个请求独立Context对象天然适合线程池调度其实最致命的是第一个流程不可见。线上出了一个问题用户问了一句“我的快递到哪了”系统跑到第几个节点挂掉的用的哪个模型Prompt调了哪个工具if-else版本的答案是打开日志慢慢翻。而引擎版本只需要查一下节点状态表整个流转过程都摊在眼前。还有一个隐性成本if-else代码没法被团队协作复用。别人要接一个新的Agent场景只能把你的代码拷贝一份改一改越改越多最终变成一坨谁都不敢动的意大利面。引擎则天然隔离了“流程定义”和“节点实现”业务同学想加流程写配置或者拖拽可视化的基础就有了。2. 引擎核心设计节点模型与状态轮转机制2.1 五大核心组件先立起来我的引擎一开始没敢设计得太复杂就五个核心角色却能覆盖90%的Agent场景WorkflowDefinition流程定义描述有哪些节点、节点之间的边顺序、分支条件FlowNode节点执行器每个节点实现自己的业务逻辑返回执行结果NodeExecutionContext运行上下文保存输入、中间变量、各节点执行结果请求级别的独享对象NodeStateMachine节点状态机负责校验和记录状态轮转FlowEngine核心引擎按定义顺序调度节点执行发布执行事件这套分工最大的好处是每个组件职责单一。节点只管做什么引擎只管怎么调度状态机只管合法轮转上下文只管数据传递。互相之间通过接口交互你想替换任何一环都不伤筋动骨。2.2 节点状态机状态轮转表是引擎的命脉流程引擎的核心不是“按顺序调用”而是状态管理。我设计了五个节点状态覆盖了一个节点完整的生命周期状态含义触发场景PENDING待执行流程启动节点入队RUNNING执行中节点被引擎调度开始执行业务逻辑SUCCEEDED执行成功run方法正常返回结果已写入上下文FAILED执行失败run方法抛出异常或业务返回失败标记SKIPPED跳过条件分支未命中或上游失败后下游无需执行状态轮转不是随便转的。比如PENDING不能直接跳到SUCCEEDED这等于跳过执行直接给结果逻辑上就是错的。所以状态机里必须维护一张合法转移表非法转移直接抛异常。public enum NodeStatus { PENDING, RUNNING, SUCCEEDED, FAILED, SKIPPED } public class NodeStateMachine { private static final MapNodeStatus, SetNodeStatus ALLOWED Map.of( NodeStatus.PENDING, Set.of(NodeStatus.RUNNING, NodeStatus.SKIPPED), NodeStatus.RUNNING, Set.of(NodeStatus.SUCCEEDED, NodeStatus.FAILED), NodeStatus.FAILED, Set.of(NodeStatus.PENDING, NodeStatus.SKIPPED), NodeStatus.SUCCEEDED, Set.of(), NodeStatus.SKIPPED, Set.of() ); public synchronized NodeStatus transit(NodeStatus from, NodeStatus to) { SetNodeStatus allowed ALLOWED.get(from); if (allowed null || !allowed.contains(to)) { throw new IllegalStateException(非法状态轮转: from - to); } return to; } }这套状态机解决的一个实际痛点是重试。以前if-else版本里实现重试就是for循环加计数器代码丑不说失败几次之后状态往往是乱的。现在状态机里FAILED转到PENDING是合法的引擎拿到FAILED事件后检查节点的重试策略决定继续重试还是放弃跳过逻辑全部收敛在状态机规则里清晰得多。2.3 为什么状态机比if-else更适合管理流转状态机本质上是一张查表你给我当前状态和目标状态我告诉你这次转移合法不合法。而if-else是串行判断“如果a就做A否则如果b就做B……”一旦分支数量超过三个可读性就会断崖式下降而且所有分支的判断逻辑互相纠缠改一个分支可能影响其他分支。状态机的好处是把判断变成了规则。Node在什么情况下能变成什么状态是预先定义好的代码不会出现“RUNNING直接跳到SUCCEEDED但数据没写进上下文”这种漏网之鱼。这就是从“写法”往“机制”上走了一级Agent流程的稳定性和可维护性都靠这个兜底。3. 从0到1代码落地核心类与实现细节3.1 环境与依赖准备代码基于JDK 17和Spring Boot 3.x如果你还在JDK 8把record改成普通POJO、List.of改成Arrays.asList一样能用。我用到了这些依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependencyWebFlux主要是为了流式输出做准备后面会讲。如果你不想引入WebFlux用原生SseEmitter也能实现但WebFlux的Flux在链式事件处理上更顺手。3.2 核心接口节点、结果、上下文节点接口是整个引擎最小也最关键的部分。我定义了FlowNode接口每个节点实现自己的逻辑并返回结果public interface FlowNode { String id(); NodeExecuteResult run(NodeExecutionContext ctx); } public record NodeExecuteResult( NodeStatus status, Object data, String message) { public static NodeExecuteResult ok(Object data) { return new NodeExecuteResult(NodeStatus.SUCCEEDED, data, ok); } public static NodeExecuteResult fail(String msg) { return new NodeExecuteResult(NodeStatus.FAILED, null, msg); } }这里用record来承载结果简洁直观。data字段保存节点输出供下游节点读取message记录错误信息或备注。上下文对象负责传递数据需要特别注意一点每个请求必须new一个独立的上下文绝对不能做成Spring Bean单例。多个Agent请求同时跑如果共用上下文变量互相覆盖那真的是灾难现场。我的上下文用一个ConcurrentHashMap存变量所有节点的输入输出都往里面写public class NodeExecutionContext { private final MapString, Object variables new ConcurrentHashMap(); private final MapString, NodeStatus nodeStates new ConcurrentHashMap(); private final NodeStateMachine stateMachine; private final EventBus eventBus; public NodeExecutionContext(MapString, Object input, NodeStateMachine stateMachine, EventBus eventBus) { this.variables.putAll(input); this.stateMachine stateMachine; this.eventBus eventBus; } public void setVariable(String key, Object value) { variables.put(key, value); } SuppressWarnings(unchecked) public T T getVariable(String key) { return (T) variables.get(key); } public void transit(String nodeId, NodeStatus from, NodeStatus to) { NodeStatus prev nodeStates.getOrDefault(nodeId, NodeStatus.PENDING); if (prev ! from) { throw new IllegalStateException(节点 nodeId 当前状态 prev 无法从 from 转移); } nodeStates.put(nodeId, stateMachine.transit(from, to)); } }transit方法里面做了两重校验第一重是当前实际状态必须等于期望的源状态第二重是状态机校验转移是否合法。这种做法一开始看着啰嗦但线上排查节点状态错乱的时候它能在第一时间给出明确的报错信息。3.3 引擎执行器调度一切的核心引擎的作用就是拿一个流程定义按顺序把节点跑一遍并把状态轮转和事件发布出来。第一版我先做了串行执行后面再迭代成DAG并行public class FlowEngine { private final MapString, WorkflowDefinition definitions new ConcurrentHashMap(); private final NodeStateMachine stateMachine new NodeStateMachine(); public void register(WorkflowDefinition definition) { definitions.put(definition.id(), definition); } public void run(String defId, MapString, Object input, EventBus eventBus) { WorkflowDefinition def definitions.get(defId); if (def null) { throw new IllegalArgumentException(未注册的流程: defId); } NodeExecutionContext ctx new NodeExecutionContext(input, stateMachine, eventBus); eventBus.publish(new EngineEvent(workflow_start, defId, null, System.currentTimeMillis())); for (String nodeId : def.orderedNodeIds()) { FlowNode node def.node(nodeId); ctx.transit(nodeId, NodeStatus.PENDING, NodeStatus.RUNNING); eventBus.publish(new EngineEvent(node_start, nodeId, null, System.currentTimeMillis())); long begin System.currentTimeMillis(); try { NodeExecuteResult result node.run(ctx); if (result.status() NodeStatus.SUCCEEDED) { ctx.transit(nodeId, NodeStatus.RUNNING, NodeStatus.SUCCEEDED); ctx.setVariable(nodeId _result, result.data()); eventBus.publish(new EngineEvent( node_end, nodeId, result.data(), System.currentTimeMillis())); } else { ctx.transit(nodeId, NodeStatus.RUNNING, NodeStatus.FAILED); eventBus.publish(new EngineEvent( node_fail, nodeId, result.message(), System.currentTimeMillis())); handleRetry(def, node, ctx, eventBus); } } catch (Exception ex) { ctx.transit(nodeId, NodeStatus.RUNNING, NodeStatus.FAILED); eventBus.publish(new EngineEvent( node_fail, nodeId, ex.getMessage(), System.currentTimeMillis())); handleRetry(def, node, ctx, eventBus); } } eventBus.publish(new EngineEvent(workflow_end, defId, ctx.getSnapshot(), System.currentTimeMillis())); } }执行器的核心动作就四步状态置为RUNNING、发布开始事件、执行节点、根据结果发布不同事件并转移状态。节点自身没有任何状态字段状态全部收敛在Context里这样引擎天然支持并发——同一个节点类可以同时被几十个请求执行只要各自的Context是独立的。我特意把EventBus设计成接口引擎不关心事件被推给谁。生产环境我可以接入WebSocket、SSE或者消息队列测试环境我直接写一个List收集事件做断言非常灵活。3.4 用节点注册表替代if-else分发干掉那个万恶的switch之前if-else版本最难看的代码是工具分发判断工具类型是“查订单”还是“查物流”再决定调哪个方法。这套逻辑现在用一个Map注册表就解决了Component public class ToolRegistry { private final MapString, ToolHandler handlers new ConcurrentHashMap(); public void register(String toolName, ToolHandler handler) { handlers.put(toolName, handler); } public ToolHandler get(String toolName) { ToolHandler handler handlers.get(toolName); if (handler null) { throw new IllegalArgumentException(未注册的工具: toolName); } return handler; } }工具节点变成这样public class ToolNode implements FlowNode { private final String id; private final String toolName; private final ToolRegistry registry; public ToolNode(String id, String toolName, ToolRegistry registry) { this.id id; this.toolName toolName; this.registry registry; } Override public String id() { return id; } Override public NodeExecuteResult run(NodeExecutionContext ctx) { ToolHandler handler registry.get(toolName); Object input ctx.getVariable(input); Object output handler.execute(input); return NodeExecuteResult.ok(output); } }新增工具的时候只需要实现一个ToolHandler接口并在注册表里register引擎和节点代码一行不用动。这就是开闭原则的价值对扩展开放对修改闭合。3.5 几种关键节点的实现思路LLM节点是Agent工作流里最常用的节点。我封装了一个ChatClient接口不管你是接OpenAI、通义千问、DeepSeek还是公司自研的模型服务统一成两个方法public interface ChatClient { String chat(String prompt); void chatStream(String prompt, ConsumerString onDelta); }LLM节点就是拿上下文里的变量渲染出Prompt然后调ChatClient把结果写回上下文public class LLMNode implements FlowNode { private final String id; private final String promptTemplate; private final ChatClient chatClient; Override public NodeExecuteResult run(NodeExecutionContext ctx) { String prompt renderTemplate(promptTemplate, ctx); String answer chatClient.chat(prompt); ctx.setVariable(id _output, answer); return NodeExecuteResult.ok(answer); } }条件节点是流程里做分支决策的关键。它本身不产生业务结果只返回一个nextNodeId告诉引擎下一步走哪里public class ConditionNode implements FlowNode { private final String id; private final PredicateNodeExecutionContext condition; Override public NodeExecuteResult run(NodeExecutionContext ctx) { boolean match condition.test(ctx); ctx.setVariable(id _match, match); if (match) { return new NodeExecuteResult(NodeStatus.SUCCEEDED, true, match); } return new NodeExecuteResult(NodeStatus.SKIPPED, false, skip); } }这样设计后业务方加一个“是否需要工具调用”的判断条件只需要传一个Lambda进去完全不用改引擎。4. 流式输出怎么搞事件驱动加SSE实战4.1 为什么Agent必须流式输出流式输出是Agent和传统接口最大的体验差异。大模型生成一段内容可能要好几秒如果憋到最后一次性返回用户盯着空白页面干等体验相当糟糕。流式输出就是让大模型边生成边把token推给前端用户第一时间就能看到“它开始说话了”。对Agent工作流来说流式还有一层的价值执行过程的可见性。用户能看到“当前正在理解需求”“正在搜索资料”“正在生成正文”而不是干等一个转圈。这种过程透明的体验能显著降低用户对AI结果的不信任感。4.2 基于事件总线的流式方案我的事件总线很简单就是发布订阅模型public class EventBus { private final ListEventListener listeners new CopyOnWriteArrayList(); public void subscribe(EventListener listener) { listeners.add(listener); } public void publish(EngineEvent event) { for (EventListener listener : listeners) { listener.onEvent(event); } } }引擎发布事件LLM节点在流式返回每个token时也发布事件。前端订阅感兴趣的事件类型就能实时看到流程推进。整套机制是事件驱动的引擎不关心事件的消费者是谁前端要SSE就接SSE要WebSocket就接WebSocket要存数据库做审计也随意。LLM节点流式版本长这样public class StreamLLMNode implements FlowNode { private final String id; private final String promptTemplate; private final ChatClient chatClient; Override public NodeExecuteResult run(NodeExecutionContext ctx) { String prompt renderTemplate(promptTemplate, ctx); StringBuilder buffer new StringBuilder(); chatClient.chatStream(prompt, token - { buffer.append(token); ctx.publish(new EngineEvent( token, id, Map.of(delta, token), System.currentTimeMillis())); }); return NodeExecuteResult.ok(buffer.toString()); } }这里我用Map.of(delta, token)作为payload前端拿到后直接拼接delta字段就能渲染出完整回答。如果要做流式增量渲染前端只需要维护一个字符串变量不停append即可。4.3 前端SSE对接与并发注意事项Spring Boot 3里用SseEmitter对接非常简单一个Controller方法搞定RestController public class AgentController { private final FlowEngine flowEngine; GetMapping(value /api/agent/run, produces text/event-stream) public SseEmitter run(RequestParam String defId, RequestParam String question) { SseEmitter emitter new SseEmitter(0L); EventBus eventBus new EventBus(); eventBus.subscribe(event - { try { emitter.send(SseEmitter.event() .name(event.type()) .data(event.payload())); } catch (Exception ex) { emitter.completeWithError(ex); } }); CompletableFuture.runAsync(() - { flowEngine.run(defId, Map.of(question, question), eventBus); }).whenComplete((result, ex) - emitter.complete()); return emitter; } }前端JavaScript消费const es new EventSource(/api/agent/run?defIdcontent-agentquestion写一篇600字的AI产品介绍); es.addEventListener(node_start, (e) { const data JSON.parse(e.data); renderLog(▶ 节点开始: data.nodeId); }); es.addEventListener(token, (e) { const data JSON.parse(e.data); outputEl.textContent data.delta; }); es.addEventListener(node_end, (e) { const data JSON.parse(e.data); renderLog(✓ 节点完成); }); es.addEventListener(workflow_end, () { renderLog( 流程结束); es.close(); });SseEmitter有个坑EventSource默认只能GET请求参数要放在query string里POST用不了。另外前端页面如果加了代理记得把缓冲关掉Nginx里加proxy_buffering off;否则流式内容会被代理层攒住不发。并发方面SseEmitter的send方法是线程安全的吗我在实际使用中确认过SseEmitter内部对send做了同步控制所以可以多个线程调用send。但还是要小心不要在FlowNode里直接持有前端线程做阻塞调用大模型流式返回时token回调本来就发生在HTTP客户端线程里直接send没问题但如果你的节点是耗时计算任务就得把执行线程放到独立线程池让Tomcat的工作线程尽早释放。5. 完整Demo从提问到成稿的内容创作Agent5.1 流程定义六个节点一条链我用一个“内容创作Agent”作为完整示例用户输入一个主题Agent先理解需求再搜索资料然后规划大纲接着撰写正文最后润色检查。流程定义如下WorkflowDefinition definition WorkflowDefinition.builder() .id(content-agent) .addNode(new StartNode(start)) .addNode(new StreamLLMNode(understand, 你是一个产品运营专家请把用户需求扩展成一段200字的需求描述{{question}}, chatClient)) .addNode(new ToolNode(search, zhihuSearch, toolRegistry)) .addNode(new StreamLLMNode(outline, 根据以下资料生成文章大纲{{search_result}}, chatClient)) .addNode(new StreamLLMNode(write, 请根据大纲撰写文章正文要求逻辑清晰、语言生动{{outline_result}}, chatClient)) .addNode(new LLMNode(review, 请对以下文章进行润色和错别字检查输出最终版本{{write_result}}, chatClient)) .addNode(new EndNode(end)) .edge(start, understand) .edge(understand, search) .edge(search, outline) .edge(outline, write) .edge(write, review) .edge(review, end) .build(); flowEngine.register(definition);每个节点执行完结果会以“节点ID _result”的key写进上下文。下游节点想引用上游的输出直接用{{节点ID_result}}模板变量就行这个渲染逻辑在renderTemplate统一处理。5.2 引擎执行过程状态轮转日志跑一遍流程EventBus收集到的事件序列大致是workflow_start node_start start node_end start node_start understand token understand 用户希望获得一篇关于AI产品的介绍... token understand 重点包含产品功能、应用场景和竞争优势... node_end understand node_start search node_end search node_start outline token outline node_end outline node_start write token write token write ... node_end write node_start review node_end review node_start end node_end end workflow_end你仔细看这个序列它在回答两个核心问题当前跑到哪个节点了node_start/end事件当前生成到什么程度了token事件。这两个信息组合起来前端就能做一个非常漂亮的过程可视化面板用户看到的不再是“一个转圈”而是Agent正在思考的每一步。5.3 测试驱动如何验证流程正确性我建议给引擎写一个基于事件断言的测试不要只靠肉眼看日志。思路是跑流程、收集事件、断言事件序列和最终上下文Test void contentAgentShouldCompleteAllNodes() { ListEngineEvent events new ArrayList(); EventBus bus new EventBus(); bus.subscribe(events::add); flowEngine.run(content-agent, Map.of(question, 介绍一款AI客服产品), bus); ListString completedNodes events.stream() .filter(e - node_end.equals(e.type())) .map(e - e.nodeId()) .collect(Collectors.toList()); assertEquals(List.of(start, understand, search, outline, write, review, end), completedNodes); }等节点状态轮转逻辑后续接上数据库持久化之后这个断言还能扩展检查每个节点在数据库里的终态是不是SUCCEEDED。这套测试驱动方式在Agent项目里特别值得投入因为流程编排出Bug的体感太差了——用户那边卡住不动你还不知道卡在哪个环节。6. 常见问题排查与避坑指南实操经验6.1 六个高频问题一张表排干净问题现象可能原因排查方法节点重复执行失败重试时上下文未回滚节点里写入了脏数据重试前检查节点是否可重入必要时清空节点写入的临时变量流程一直卡在RUNNING节点内阻塞调用未设置超时HTTP客户端一直挂起给ChatClient和工具调用统一设置连接超时和读超时SSG前段一直转圈不出内容Nginx缓冲未关或SseEmitter没设置超时关proxy_bufferingnew SseEmitter(0L)表示不超时多个请求数据互相污染上下文被设计成了单例BeanContext必须是请求级new出来的NGINX容器禁止共享变量节点状态机报非法流转代码里手动put状态绕过了transit方法所有状态修改必须走NodeExecutionContext的transit方法流式token顺序错乱LLM流式回调在多线程下并发送给前端对单节点的token事件串行化用一个队列或锁保证顺序6.2 四个保命经验第一个经验流程定义必须只读。WorkflowDefinition一旦注册就不要再允许修改节点和边了。我踩过一次坑开发环境注册完流程后临时加了个调试节点结果线上流量刚好打到那个还没注册完的定义上直接空指针。现在我的做法是Definition构建完就深拷贝注册表里只放不可变副本。第二个经验LLM客户端一定要做超时和重试隔离。大模型接口偶尔慢是常态如果不设读超时引擎里那个节点会一直占着线程并发一高线程池就炸了。我给ChatClient包了一层超时控制和熔断逻辑连续失败3次直接短路10秒避免故障扩散到整个Agent链路。第三个经验上下文快照要支持序列化。Agent运行过程中间突然挂了如果上下文能序列化到磁盘或数据库就可以从某个节点恢复执行而不是从头开始。这套机制我后补的但建议你在设计Context时就把快照字段设计好不然后面加字段全是兼容噩梦。第四个经验节点日志必须带traceId。一次Agent运行会横跨多个节点、多次外部调用日志里没有traceId的话排查问题的时候得把几十个线程的日志人工拼起来。我在NodeExecutionContext里加了一个traceId字段初始化时UUID生成所有节点日志都带这个ID线上定位慢节点从分钟级降到秒级。6.3 Agent怎么扛并发引擎设计成无状态热词里正好有个“AI Agent怎么扛并发”这块我多说两句。引擎扛并发的最大秘诀就是无状态FlowEngine和WorkflowDefinition做成Spring单例Bean但所有运行期数据全部放到NodeExecutionContext里每次run请求new一个Context。节点实现类本身也不能持有请求级数据只能通过Context读写。这样设计后同一个流程定义可以同时被成千上万个请求执行彼此完全隔离。再配合一个带界的线程池处理流式回调整体并发能力非常可观。等JDK 21的虚拟线程普及之后这种无状态引擎的并发模型还能更简单——每个请求直接分配一个虚拟线程包一层结构化并发连手动线程池都省了。收个尾一点个人经验和踩坑心得这个流程引擎从设计到落地我最大的体会是做Agent开发别把大模型当主角要把流程当主角。大模型只是节点里的一个执行器真正决定Agent行为边界和稳定性的是那个把节点串起来、管住状态、保住上下文的东西。还有一个小技巧分享给你写节点的时候一定要给每个节点设计好“幂等性”。Agent跑着跑着断了、重试、超时都是常态。如果节点本身不是幂等的比如“发送邮件”节点执行到一半超时了引擎判断FAILED但邮件其实已经发出去。这种问题靠状态机也兜不住得在业务层设计幂等键。节点负责“做”引擎负责“管到”但“做对了没”这事最终还得靠业务逻辑自己兜底。后续这个引擎我准备往两个方向扩展一个是把流程定义DSL化用JSON或YAML描述节点和边这样前端可视化编排的底座就齐了另一个是把节点状态持久化到数据库支持流程中断后的断点恢复。等这两个能力做完再来写个续篇。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询