☰
企业AI流程编排实战:从ReAct Agent到Graph混合编排
2026/10/13 6:36:40 网站建设 项目流程

今年做的最折腾的一件事,是把一个 AI 工单助手从“一个大 Agent 硬刚所有问题”改造成“Graph 编排的混合流程”。改完后同一批测试用例的通过率从 67% 提到了 91%,单条工单的平均处理耗时从 40 秒降到了 6 秒,模型调用费用直接腰斩。这个数据对比让我彻底想明白一件事:企业里真正能落地的 AI 复杂流程,缺的往往不是更强的大模型,而是一套能把“模型决策”和“规则执行”捏在同一个流程里的编排引擎。

聊的框架是 Spring AI Alibaba Graph。它提供了一种在 Java 生态里实现 LangGraph 式编排的路径,让 ReAct Agent 和 Workflow 不再是二选一,而是可以放在一张图里混合使用。这篇文章我会从实战角度拆透三件事:ReAct Agent 与 Workflow 的本质边界在哪、Spring AI Alibaba Graph 里的状态/节点/条件边怎么建模、以及我用它把一个工单流转系统从 Agent 改造成 Graph 后踩过的所有坑。

如果你正处在“Agent 演示很惊艳,一上生产就失灵”的阶段,或者团队在纠结要不要把流程固定成 Workflow,这篇文章应该能帮你省不少试错时间。

1. 先别急着堆大模型:企业AI流程编排到底卡在哪

1.1 单 Agent 的失控现场

我第一次把 Agent 接进一个企业内部模拟工单系统时,理想的画面是:所有工单进来,模型自动判断意图、调用工具、给出解决方案,全程“自动驾驶”。但真实表现完全不是这样。

举个当时复现过很多次的例子。用户提交了一条报修工单:“会议室投影仪突然没画面了,后天要开重要会议,尽快处理。”大模型拿到这个问题后,先调用了一次“设备查询”工具,发现这台投影仪有两条维保记录,又回头调用“工单查询”工具去翻历史处理方案,中间还生成了两段解释性文字,最后才调用“创建维修工单”工具。整个过程跑了 9 轮工具调用,耗时 47 秒,模型输入输出加起来消耗了大量 token,其中大半是根本不需要的上下文。

更难受的是不可复现。同一个问题隔十分钟再跑一次,路径可能完全不同,有时它会先检索知识库,有时它会先询问用户更多信息。排查问题的时候,你根本说不清它这一单为什么走这条路,下个月想审计某一单的处理依据也无从下手。

这不是模型能力问题,而是编排方式问题。单 Agent 把“流程控制权”完全交给了模型,而模型天生不适合做需要严格排序、记忆状态、精确回退的事情。它擅长的是理解、生成、归纳这一类非确定性任务。

1.2 企业流程要的是哪四样东西

做企业内系统跟在个人项目里玩 Agent 完全是两种心态,核心诉求可以收敛成四点:

  • 可审计:每一步处理路径、每一次模型调用、每个状态变更都能回放。
  • 可 SLA:响应时间要可控。不能出现“运气好 3 秒,运气差 3 分钟”。
  • 可兜底:模型判断出错时,有规则或者人工环节能接住,而不是一起失控。
  • 可变更:产品说要调整流程顺序时,不用把整段逻辑全部推翻。

这四点单 Agent 几乎给不了。模型有天然的随机性,路径不可控,审计无从谈起;自由调用工具导致响应时间方差极大;模型兜底也只是“再让它想想”,而不是确定性兜底。

所以企业里真正缺的,是流程引擎。把大模型当作流程里的一个能力节点,而不是让流程围着模型转。这就是我第一次意识到需要 Graph 编排的起点。

1.3 只让模型做它擅长的事

想清楚问题之后,我开始重新划分职责。模型擅长的:意图分类、实体抽取、话术生成、非结构化信息的归纳。模型不擅长的:状态管理、路径控制、精确计算、长期规划、对每一步给出确定性证明。

于是我把方案改成了 Spring AI Alibaba Graph,把每个工单的处理过程拆成一条可描述的路径:先让模型做意图分类,分类结果交给代码决定走向;走到维修分支后再调用设备查询接口;查询结果里有一个字段专门给代码做金额判断,超过阈值才转人工审批。模型只在“分类”“信息抽取”这两个真正需要它的节点出现。

这一步改完,系统才第一次像个正式软件了:有状态、有分支、有退出条件,而不是一段“看模型心情”的黑盒循环。

2. ReAct Agent和Workflow,本质区别不是“有没有工具调用”

2.1 ReAct 不是一种框架,而是一种推理模式

ReAct 这个词经常被误解为代表某种 Agent 框架,其实它描述的是一种循环模式:Reason(思考)→ Act(行动)→ 观察结果 → 再思考,如此往复。模型在每一轮推理时决定要不要调用工具、调用哪个工具、怎么解读工具返回的结果。

用一句话类比:把一个实习生丢到一个任务面前,他自己查资料、自己问人、自己试错,直到把任务完成。这就是 ReAct 的核心价值——在开放域里探索。

在 Spring AI Alibaba 里实现一个简单的 ReAct 形态并不复杂。定义几个工具,让 ChatClient 自行决定调用:

public class DeviceService { @Tool("根据设备名称查询设备维修记录") public String queryMaintenanceHistory(String deviceName) { // 实际调用设备管理系统的 REST 接口 return "该设备近一年维修 2 次,最近一次为投影灯泡故障"; } @Tool("根据设备名称查询设备状态") public String queryDeviceStatus(String deviceName) { // 返回设备在线状态、故障码、剩余寿命等 return "状态异常,故障码 E-302,剩余寿命 12%"; } }
ChatClient chatClient = ChatClient.builder(chatModel).build(); String answer = chatClient.prompt() .user("会议室投影仪没画面了,帮我查一下可能的原因") .tools(new DeviceService()) .call() .content();

这段代码跑起来后,模型会自己决定先查状态、再查维修记录,而不是按代码写死的顺序去查。它就是最典型的 ReAct Agent。

但 ReAct 的代价也很明显:每一步“思考”都要计费,循环次数没有天然上限,执行路径高度随机,模型可能连续调用同一个工具两次而不自知。适合它的场景是信息综合、开放诊断、边界模糊的任务,而不适合固定交易类流程。

2.2 Workflow 是“把流程钉死”的执行模型

Workflow 的思路刚好像是另一个极端:每个节点做什么、执行顺序是什么、什么条件下走哪个分支,全部由代码预先定义。模型只在某个特定节点被调用,比如“生成一段回复话术”,它永远没有控制流程走向的机会。

还是用实习生类比:这次他不是自己探索,而是站在流水线上,工序单规定了它第 1 步贴标签、第 2 步装箱、第 3 步过安检,每一步做完必须盖章确认。

Workflow 的价值在于确定性和可审计性。同样一个输入进来,永远走同一条路径,响应时间稳定,任何一步出错都能直接定位到具体节点。合规场景、支付场景、售后流程这类强流程业务,几乎只能采用它。

它的问题则是:如果流程本身边界不清,提前画不出来,Workflow 也白搭。比如客户说“我有个问题但我也说不清是什么问题”,这种开放场景很难写进预设代码。

2.3 关键洞察:Graph 是光谱而不是二选一

这是全文我最想强调的一点。很多人把 ReAct 和 Workflow 当成一个非黑即白的选择题,但 Graph 的基本思想是:它们是一条光谱的两端,你可以在一张图里同时容纳这两端。

Spring AI Alibaba Graph 的实现思路深受 LangGraph 启发:用 State(状态)、Node(节点)、Edge(边)三个概念来构建执行图。有些节点是普通代码节点、有些节点是 LLM 节点、有些节点内部甚至可以再嵌一个 ReAct Agent,边上再挂条件决定走向。

三种模式的差异我做了张表,方便对照:

维度ReAct Agent纯 WorkflowGraph 混合编排
流程确定性低,模型自己决定路径高,代码决定全部路径中高,关键路径固定,局部开放
Token 消耗高,思考和反复调用都计费低,只在模型节点使用中,可精确控制模型节点的位置
出错排查难,路径不可复现容易,每步可回放容易,特定节点可单独调试
变更成本改 Prompt 就能试改代码、重新发版局部改某个节点或边的条件
适合场景开放问题、复杂诊断强审计、高 SLA、合规敏感企业级主流选择

我的实际判断非常偏实用:企业 90% 的场景应该以 Workflow 为主干,把 Agent 作为局部节点的增强能力,而不是让整个系统从头到尾都是 Agent。判断依据只有一个问题——这一步需要模型“现场决策”吗?如果下一步路径事前能枚举出来,就不需要 Agent;如果实在枚举不出,再把这一步处理逻辑包成一个 Agent 节点放进去。

3. Spring AI Alibaba Graph的建模前提:把流程拆成状态、节点和边

3.1 三个核心概念怎么理解

Graph 的建模语言只有三个核心概念,理解了它们,整个编排思路就通了。

  • State(状态):所有节点共享的数据对象,一个节点消费它、返回新的它,下一个节点再拿到更新后的它。类比像一张工单在各个处理部门之间流转,工单本身的内容就是 State,流转过程就是图。
  • Node(节点):一段业务逻辑的函数,输入是 State,输出也是 State。节点内可以是普通 Java 代码、REST 调用、也可以是大模型推理。
  • Edge(边):节点间的连接关系,决定执行顺序。普通边是“无条件走下一步”,条件边则是根据 State 里的某个字段决定走 A 分支还是 B 分支。

Spring AI Alibaba Graph 默认是内存态执行。也就是说,State 在进程内传递,整个图的运行是一次函数调用链。这个设计的好处是简单直接,坏处是进程重启后执行状态会丢,后面的踩坑部分我会专门讲怎么处理。

3.2 状态设计的第一原则:字段所有权

我在最早写 Graph 时犯过一个低级错误:把整个 State 设计成一个大而全的 DTO,所有节点拿到就随手往里塞字段。前几个节点串行跑没问题,一上并行节点立刻炸了:两个并行分支同时往state.setResult()写东西,后写的把先写的覆盖了。

这里给出一个第一原则:每个字段必须有唯一的所有者节点。并行分支可以各自维护自己的私有字段,最后通过一个合并节点把它们汇总到公共字段上。

以我工单系统为例,State 大致长这样:

public class WorkOrderState { // 基础信息,由入口节点写入 private String orderId; private String userText; // 分类结果,由 classify 节点写入 private String intent; // repair / refund / complaint / unknown // 设备信息,由 inspectDevice 节点写入 private String deviceName; private String faultCode; private BigDecimal repairCost; // 审批信息,由 approval 节点写入 private Boolean needApproval; private String approvalUrl; // 最终结果,由 merge 节点统一汇总 private String finalResult; // 审计追踪,所有节点都可追加 private List<String> auditLog; }

字段分区后,并行分支之间的冲突就自然消失了。任何节点想写repairCost,都要经过“拥有该字段的节点”来写,其他节点只能读。

3.3 节点怎么写才像“企业级”

企业级节点和 Demo 里节点的最大区别,是每个节点都要留下痕迹。我在每个节点外面套了一层统一的包装逻辑,记录四样东西:节点名、开始时间、结束时间、传入传出状态的摘要。

public StatefulNode<WorkOrderState> auditedNode(String nodeName, StatefulNode<WorkOrderState> inner) { return state -> { long start = System.currentTimeMillis(); try { WorkOrderState out = inner.apply(state); state.getAuditLog().add(String.format( "[%s] %s 完成,耗时 %d ms", LocalDateTime.now(), nodeName, System.currentTimeMillis() - start)); return out; } catch (Exception e) { state.getAuditLog().add(String.format( "[%s] %s 失败: %s", LocalDateTime.now(), nodeName, e.getMessage())); state.setFinalResult("系统处理失败,已转人工兜底"); return state; } }; }

节点内抛出的异常不要直接往外炸,而是写进 State 的错误标记里,把流程引导到兜底节点。这是后文“回退机制”的基础,也是让 Graph 面向生产可用的关键一步。

4. 跑一个真实案例:智能工单流转从Agent退回Graph的全过程

4.1 案例需求:企业内部服务台的工单自动流转

我拿模拟项目 X 举例。它的业务规则是这样的:用户提交一段问题文本,系统先判断意图;如果是报修,去查对应设备的当前状态,根据预估维修费用决定自动派单还是走人工审批;如果是退款,走退款审核;如果是投诉,直接转人工;分类不出来,也要有一个兜底节点接住。

这个系统一开始用的是整体 ReAct Agent,效果我在开头就说了:路径不可控、token 消耗高、经常绕路。后来我用 Spring AI Alibaba Graph 重写,把意图分类保留给模型,其余全部交给确定性的节点和条件边。

4.2 基础依赖与配置

Graph 模块的依赖很简单:

<dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-graph-starter</artifactId> <version>1.0.0-M6.1</version> </dependency>

这里提醒一下:Spring AI Alibaba 的版本迭代比较快,1.0.0-M 系列的 API 在不同小版本之间可能有命名微调,我下面写的代码基于我当时的版本,思路不变,如果个别方法名对不上,翻一下当前版本官方示例即可。

模型配置我用的是 DashScope 的 endpoint,只要你项目里配了任何 OpenAI 兼容的模型服务,改成自己的 base-url 和 api-key 也一样跑:

spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus

4.3 四个核心节点实现

第一步,分类节点。它是唯一一个不确定的模型节点,用 ChatClient 让模型输出一个 JSON,内容只有意图和提取出的关键实体。为了避免模型乱说话,我在 Prompt 里给了一个严格的输出约束:

public class ClassifyNode implements StatefulNode<WorkOrderState> { private final ChatClient chatClient; @Override public WorkOrderState apply(WorkOrderState state) { String prompt = """ 你是工单分类器,根据用户输入返回 JSON,不要输出其他内容。 格式:{"intent":"repair|refund|complaint|unknown","device":"设备名或null","amount":"金额数字或0"} 用户输入:%s """.formatted(state.getUserText()); String raw = chatClient.prompt().user(prompt).call().content(); // raw 可能是带 ```json 围栏的,需要清洗 String json = cleanJson(raw); // 用 Jackson 解析成 Map 后写回 State // intent、deviceName、repairCost 等字段在此节点被写入 return state; } }

第二步,设备状态查询节点。它调用设备管理系统提供的 REST 接口,把故障码、剩余寿命、预估费用拉回来。这个节点没有任何模型参与,执行时间是稳定的毫秒级:

public class InspectDeviceNode implements StatefulNode<WorkOrderState> { @Override public WorkOrderState apply(WorkOrderState state) { String deviceName = state.getDeviceName(); // GET /device/{name}/status DeviceStatus status = deviceClient.fetchStatus(deviceName); state.setFaultCode(status.getFaultCode()); state.setRepairCost(status.getEstimatedCost()); return state; } }

第三步,费用判断节点。这里我必须强调一个点:分支条件不要用模型判断,用代码判断。费用到底超没超阈值,这种确定性判断交给模型纯属浪费 token 还引入不稳定。我这里直接在条件边里写:

boolean needApproval = state.getRepairCost().compareTo(new BigDecimal("5000")) > 0; state.setNeedApproval(needApproval);

第四步,通知节点。无论哪个分支最终走完,都要往审计里写一条结果并生成给用户的回复文本。

4.4 用 StateGraph 把节点串起来

核心的编排代码长这样:

StateGraph<WorkOrderState> graph = new StateGraph<>(WorkOrderState.class) .addNode("classify", new ClassifyNode(chatClient)) .addNode("inspect", new InspectDeviceNode()) .addNode("autoDispatch", new AutoDispatchNode()) .addNode("humanApproval", new HumanApprovalNode()) .addNode("notify", new NotifyNode()) .addNode("fallback", new FallbackNode()) // 入口 .addEdge("__start__", "classify") // 条件路由:根据 classify 节点写入的 intent 决定走向 .addConditionEdge("classify", List.of("inspect", "refundHandle", "complaintHandle", "fallback"), state -> switch (state.getIntent()) { case "repair" -> "inspect"; case "refund" -> "refundHandle"; case "complaint" -> "complaintHandle"; default -> "fallback"; }) // 维修分支:查询设备状态后,根据费用决定自动派单还是人工审批 .addConditionEdge("inspect", List.of("autoDispatch", "humanApproval"), state -> Boolean.TRUE.equals(state.getNeedApproval()) ? "humanApproval" : "autoDispatch") // 所有处理分支最后都汇到 notify .addEdge("autoDispatch", "notify") .addEdge("humanApproval", "notify") .addEdge("refundHandle", "notify") .addEdge("complaintHandle", "notify") .addEdge("fallback", "notify") .addEdge("notify", "__end__") .compile();

执行时只需构造初始状态,调用invoke:

WorkOrderState initialState = new WorkOrderState(); initialState.setOrderId("WO-2024-001"); initialState.setUserText("会议室投影仪突然没画面了,后天要开重要会议,尽快处理"); WorkOrderState finalState = graph.invoke(initialState); System.out.println(finalState.getAuditLog()); System.out.println("最终处理结果:" + finalState.getFinalResult());

整条链路非常清晰:分类是唯一的模型节点,之后是“规则 + 代码 + 少量模型”的混合执行。业务负责人来问“这单为什么走审批”,直接把审计日志翻出来,每一跳都有节点名和时间戳,根本不用猜。

4.5 改造前后对比

同样是那批之前 Agent 跑得很吃力的测试用例,改造后的数据变化是:

  • 路径确定性:从“每单路径都可能不同”变成“相同条件相同路径”。
  • 单均耗时:从平均 40 秒降到 6 秒,因为中间不再有多余的模型思考轮次。
  • Token 消耗:只有分类节点和通知节点各调一次模型,费用直接降了一半以上。
  • 排查效率:以前看一条日志要看几十行工具调用记录,现在看审计日志里的节点链路就够了。

这套实践让我坚定了一个原则:模型只做它不可被替代的那一步,其他全交给代码和规则。

5. 上线之后才踩到的坑:并发覆盖、模型JSON、状态快照与人工兜底

5.1 并行节点把 State 写串了

系统接入真实流量后,我很快加了并行节点:人工审批的同时,并行跑一个风险扫描节点。表面看两个分支互不影响,但它们最终都往 State 里写“结果相关字段”时,晚写的那一个覆盖了先写的。

解决办法是前面已经提到的字段所有权设计。并行分支各自写私有字段,比如approvalResult和riskResult,最后用一个 merge 节点读取两边结果,汇总到finalResult。Graph 本身不帮你做并发状态控制,它只是一个函数执行引擎,“谁写什么”必须由建模的人自己设计清楚。

5.2 模型返回的 JSON 根本不是干净的 JSON

第一个版本里,我在分类节点直接拿chatClient.call().content()的返回值去用 Jackson 解析,结果生产环境很快出现一批解析异常。模型有时会输出带 ```json 围栏的代码块,有时在 JSON 前后夹带一句“好的,这是你要的结果:”,更离谱的是偶尔会在 JSON 末尾多一个逗号。

处理办法是两层:先清洗字符串,再走容错解析。清洗函数去掉围栏和前后缀,解析失败时返回一个unknown意图走兜底节点,而不是让整个 Graph 抛异常卡死。

private String cleanJson(String raw) { String s = raw.trim(); if (s.startsWith("```")) { s = s.replaceAll("^```[a-zA-Z]*\\n", "").replaceAll("\\n```$", ""); } int start = s.indexOf('{'); int end = s.lastIndexOf('}'); if (start >= 0 && end > start) { return s.substring(start, end + 1); } return s; }

这个过程看起来土,但非常实用。模型输出的稳定性永远不能假设 100%,Graph 的容错设计必须从第一个模型节点就做好。

5.3 图里嵌 Agent 节点,循环次数不设上限会出事

我在某个分支里嵌入了一个小范围的 ReAct Agent,让它去综合多个信息源给出处理建议。开发环境下跑得好好的,一并发量上来就出了各种“慢请求”。

原因很简单:Agent 节点的循环次数没有设上限,遇到复杂问题它可能连续调用十几个工具,单次请求耗时几分钟。这直接破坏了 Workflow 主干辛苦建立起来的 SLA。

后面我在 Agent 节点外层加了三道闸:最大迭代次数限制、单次调用超时、总耗时超时。超时后不走“继续思考”,而是把这条分支标记为“处理失败”并转入人工兜底节点。宁可让人来做,也不能让它无限空转下去。

5.4 进程一重启,挂在“人工审批”的单子全丢了

Graph 是内存态执行,这意味着只要服务没重启,一切正常;一旦重启,所有停留在中间状态的任务全部丢失。我曾在灰度环境里重启过一次服务,结果当时所有等待人工审批的工单直接消失,业务方还以为是系统吞单了。

应对办法是按“快照 + 事件”的思路补了一层持久化。Graph 每一步节点完成后,把当前 State 序列化到数据库里,记录节点名、执行版本号和变更时间。进程重启后,扫描状态为“执行中”的单子,从最近完成的那个节点重新恢复后续分支。

// 伪代码示意 for (WorkOrderSnapshot snap : snapshotDao.findPending()) { WorkOrderState state = snap.deserializeState(); // 从 snap.lastNode() 的下一个节点开始继续执行 graph.invokeFrom(state, snap.lastNode()); }

这层不是框架自带的能力,是要自己造的轮子。在企业场景里,它比某个花哨的 Agent 特性重要得多。

5.5 别让模型去决定流程走向

最后这个坑有点反直觉:条件路由是流程里最容易出问题的地方,但我见过不少人把路由判断也交给模型。比如在条件边里再让模型判断“这笔费用算不算高”。

这不是聪明,是自找麻烦。费用阈值是确定性规则,模型判断不仅多花 token,还会因为 prompt 措辞不同而出现摇摆不定的结果。Graph 里条件边的职责应当全部由确定性代码承担,模型只需要负责上游的意图分类和信息抽取,输出结构化的中间结果,路由交给switch就够了。

6. 我的选型判断:哪些环节该用Agent,哪些环节该钉死成Workflow

6.1 三个问题,快速判断边界

现在我设计任何一条 AI 流程时,都会先对着每个环节问三个问题:

  • 这个环节的结果必须是确定的吗?如果是,直接写代码或规则,不碰模型。
  • 下一步路径事前能枚举出来吗?如果能,用条件边;只有枚举不出来才考虑 Agent。
  • 失败成本高吗?如果高,必须有回退节点和人工兜底,绝不放给 Agent 自由重试。

这三问能过滤掉大部分“为了 AI 而 AI”的设计。分类、抽取、总结、话术生成,这些都是模型的活;比较大小、判断状态、按条件跳转、执行某个外部动作,这些都是代码的活。

6.2 什么场景真的需要 ReAct Agent

在我的工单系统里,真正还需要 Agent 的只剩一个环节:用户问题异常模糊且跨多个业务域时,先让 Agent 做一轮“诊断式探索”,再把它探索的结果落成结构化意图,交给后面的 Workflow 执行。

所以我的建议不是消灭 Agent,而是限制它的使用边界。把 Agent 当作一个局部的、有超时和重试上限的复杂节点,放进以 Workflow 为主干的 Graph 里,这样才能既享受它的探索能力,又不被它的失控路径绑架。

6.3 渐进式改造路线

如果你现在手里正好是一个已经开始发散的 Agent 项目,别急着推翻重来,按这个顺序渐进改造:

第一步,把业务方描述的全部处理路径画成图,确定有哪些节点、哪些分支、哪几个退出条件。这一步只画流程,不写代码。

第二步,把图中能用规则和代码实现的节点全部实现掉,模型节点暂时用“返回固定值”的方式占位,先把整条链路跑通。

第三步,再把真正需要模型的位置换成 LLM 节点,比如意图分类、摘要总结、话术生成。此时整个系统已经有稳定的主干和清晰的审计结构。

第四步,如果某个位置确实需要自由探索,再在该节点内嵌一个 ReAct Agent,并加上本章讲过的迭代上限和超时控制。

这套路线我实践下来非常稳,每次只动一个节点,出问题可以精准回滚,不会出现“改个流程全家翻车”的场面。

最后再分享一个很实用的小技巧:从 Graph 入口生成一个traceId放进 State 里,每个节点写审计日志时都带上它,排查跨节点问题时你会在日志系统里省下大把时间。这个习惯我在改造成 Graph 之后才开始做,后悔没早点加。

流程可解释,AI 才可信。它能让模型在生产环境里变成一个标准化的、可监督的组件,而不是躲在黑盒里的不确定性来源。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询