☰
从ReAct到Workflow:Spring AI Alibaba企业级AI编排实战
2026/10/13 5:37:55 网站建设 项目流程

最近大半年,我一直在折腾企业级AI应用,感触最深的一件事就是:单体Agent看着很美,一旦进入真实业务流程,立刻变成一团乱麻。尤其是用到Spring AI Alibaba这套技术栈时,"从ReAct Agent到Workflow"不只是一条技术演进路线,更是被业务逼着走出来的必经之路。今天我就把这几个月踩过的坑、重构过的架构、以及Spring AI Alibaba Graph的核心用法一次性说清楚。

这篇内容适合两类人:一类是正在用LLM做企业应用,但发现单靠Agent提示词撑不住复杂业务的Java开发者;另一类是团队里负责AI系统架构,需要给老板解釋"为什么流程必须编排"的技术负责人。我会从ReAct的局限讲起,再到Graph的建模思路,最后给一个可以照抄的实战改造案例,保证你看完能直接上手评估自己手里的项目。

1. 为什么单体Agent撑不起企业流程:三个让我下定决心改造的信号

1.1 第一堵墙:推理能力越强,流程越不可控

最初我们的设想很简单:把业务逻辑全部塞进System Prompt,让模型自己"推理+行动"。以客服工单处理为例,Prompt里写好"你有工具A查订单、工具B发退款、工具C通知用户",然后期望模型自动完成整个闭环。

ReAct模式确实灵活,模型会先思考"用户的问题是订单未到",然后决定调用工具A查物流,再根据结果决定是否退款。这套机制在Demo阶段跑得飞快,但走到生产环境就露馅了。

问题出在"决策自由度"上。你给模型的自由度越高,它就越可能走出你意料之外的路径。比如模型在一次退款失败后,居然自动尝试了三次不同的工具参数,每次都是"再试一次"。三次调用就是三倍的token成本,而且每次都写进了审计日志。更麻烦的是,某次线上事故中,模型在查询订单状态时误读了返回字段,直接判断为"已退款",后续所有流程全部基于错误前提执行。

这不是模型"笨",而是ReAct的设计哲学本身就是开放的、探索式的。它适合开放域问答,但企业核心流程要求的是确定性:这一步骤执行什么、什么条件下走哪条分支、失败后做什么补偿,全部要有明确规则。把这种确定性建立在模型每次都能"想对"上面,实在太脆弱了。

1.2 第二堵墙:工具调用链断了,恢复只能靠"重跑一遍"

单体Agent的另一个致命问题,是中间环节一旦失败,整个链路都得从头再来。有一次我们的工单流程是先调用库存系统锁定商品,再调用CRM创建客户记录,最后调用通知服务发送确认邮件。结果第三步通知服务超时了,整个Agent任务报错。

按当时的处理方式,我们只能手动把整个工单重新提交一遍。问题来了:重新提交后,库存系统再次执行锁仓,导致同一位客户被锁定两次;CRM也创建了重复客户记录。因为Agent作为一个整体执行,它没有一个"断点续跑"的机制,失败了就整体回滚或整体重试,这在企业场景下是不可接受的。

我后来跟某团队聊过,他们甚至在最外层套了"大任务重试"机制——只要Agent报错就整单重来。听起来很省事,但遇到那种"第三步写库成功、第四步通知失败"的情况,重来就会产生脏数据。真正能救命的做法是:把流程拆成独立节点,每个节点有单独的重试和补偿逻辑。在单体架构里,这个目标基本没法实现。

1.3 第三堵墙:黑盒流程让测试与协作寸步难行

第三个信号其实来自QA同事的吐槽。单体Agent拿去做测试,测试人员根本不知道该怎么设计用例。你说"测一下退款失败的场景",测试用例写了一大堆Prompt变体,结果模型有时候触发成功路径、有时候触发失败路径,同样的输入跑三次三次结果都不一样。

这种不确定性直接拖垮了开发和测试的协作效率。开发人员没法对某个独立功能做单元测试,因为所有逻辑都揉在同一个Agent循环里。跨团队协作也是重灾区:运营说"退款通知文案我要改一下",开发得从一大段Prompt里捞出来改,改完还要担心会不会影响其他行为。

一把梭的Agent适合做"一个人能包圆的小事",但企业的复杂流程天生是多角色、多系统、多阶段的协作问题。流程必须先被拆解、再被编排,才能被测试、被维护。这就是我转向Graph编排的根本原因。

2. 先搞懂ReAct再谈替换:它的内功与适用边界

2.1 ReAct的循环机制复盘

ReAct的核心可以浓缩成一个循环:Reasoning(思考)→ Acting(行动)→ Observing(观察)→ 再思考。模型先根据当前状态判断"我该调哪个工具",执行工具后把返回结果接回来,再判断下一步。

这个循环的真正优势是"自适应":你不需要提前罗列所有可能的路径,模型会自己绕路、自己换工具、自己尝试新策略。比如用户问"为什么我的积分没到账",模型可以先查订单、再查积分日志、再查活动规则,全程不需要开发者预定义路径。

但也有代价,而且代价相当隐蔽:

  • Token开销不可控:每多一轮思考-行动-观察,就多一组完整Prompt+Completion。复杂任务跑上七八轮,成本是预设流程的好几倍。
  • 日志与可审计性差:模型为什么选择这条路径?调用了哪个工具?拿到什么结果?虽然有trace,但很难转换成标准的流程审计格式。
  • 故障排查困难:一个环节返回异常数据,模型可能会带着错误信息继续循环好几轮,问题被滚雪球式放大。

我在内部培训时常打一个比方:ReAct像一个"想法很多但做事随缘"的新人,Graph则是一套"每个工位都画清楚操作手册"的流水线。新人适合处理未知问题,流水线适合稳定交付已知问题。企业的大部分核心流程其实是已知问题,没必要让新人临场发挥。

2.2 什么时候保留ReAct,什么时候上Graph

经过几次重构,我总结了下面这张决策表,可以直接拿去对照:

判断维度保留ReAct改上Graph
流程步骤步骤高度不确定,需探索式解决步骤基本固定,只有分支变化
工具依赖只用1~2个工具,单轮可解决需要4个以上工具协作
失败成本失败重试无副作用失败可能产生脏数据、重复调用
规范性要求无需审计,可接受随机策略每一步需要记录、可复盘
延迟容忍可接受长耗时思考需要稳定延迟,关键步骤有SLA
典型场景开放问答、方案生成、创意辅助工单处理、交易流程、风控审核、审批流转

判断标准很简单:如果一项任务你闭上眼睛能画出流程图,那它就不该用ReAct硬扛。反之,如果你自己也说不清楚下一步可能是什么,那Graph帮不了你,还是ReAct或纯LLM问答更合适。

当然,这两者不是互斥的。我目前用的最多的是"Graph骨架 + ReAct局部"的组合:整体流程用Graph控制走向,遇到需要开放思考的环节(比如给用户生成个性化退款方案),Graph里嵌套一个Agent节点,让模型在局部自由发挥。这样既保留灵活性,又锁住了主流程的确定性。

3. Spring AI Alibaba Graph核心建模:把流程图写进代码

3.1 核心概念炼一遍:State、Node、Edge、Condition

Spring AI Alibaba Graph是对整个编排思想的Java化落地。它借鉴了图计算的核心概念,但用Spring的风格重写了一遍,Java后端接上手感很顺。

先把这个框架最核心的四个概念讲透:

  • State(状态):贯穿整个流程的数据载体。相当于一条流水线上所有工位共用的文件夹,每个节点都能读写。在实际代码里,它通常是一个结构化的对象,你定义好字段,流程执行中不断被更新。
  • Node(节点):流程中的一个独立工作单元。可以是一个同步方法调用,也可以是一个异步任务。节点拿到State,处理完把结果写回State。
  • Edge(边):描述节点之间的连接关系,"A执行完走到B"就是一条边。
  • Condition(条件):边上可以挂条件,比如"判断结果是否合法,合法走A,不合法走B"。这是Graph能实现业务流程分支的关键机制。

这四个概念组合下来,一次流程执行就是一次"带状态、带方向"的图遍历。你写Graph的时候,本质上是在用一种可执行的方式画业务流程图。

3.2 从流程图到代码的映射关系

很多同学第一次接触Graph最大的困惑是:我脑子里有流程图,但不知道代码怎么组织。我分享一个百试不爽的建模方式:先画图,再提炼节点,最后写代码。

比如一个简单的订单审核流程:接收订单 → 风控检查 → 人工审核 → 返回结果。翻译成Graph就是四个节点和三条边。代码结构大致是这样(以当前版本的常规API示意,具体类名以官方文档为准):

GraphState state = new GraphState(); state.setOrderId("ORD-20240518-001"); Graph graph = new GraphBuilder() .state(state) .node("receiveOrder", new ActionNode(ctx -> receiveOrder(ctx))) .node("riskCheck", new ActionNode(ctx -> riskCheck(ctx))) .node("manualReview", new ActionNode(ctx -> manualReview(ctx))) .node("finish", new ActionNode(ctx -> finish(ctx))) .edge("receiveOrder", "riskCheck") .edge("riskCheck", "manualReview") .edge("manualReview", "finish") .build(); GraphResult result = graph.execute();

你看,这段代码跟流程图几乎一一对应。每个node后面的Lambda就是一个可独立测试的节点函数,任意节点出问题,你都能单独拎出来排查。这就是Graph第一眼的价值:可读性。流程长什么样,代码就长什么样。

3.3 状态设计是Graph成败的一半

节点和边是Graph的骨架,State才是Graph的灵魂。我在多个项目里发现,流程跑出问题,十有八九是State设计不合理。

先说原则:State应该是"只增不改"的演进式设计。不要把State设计成每个节点随手往里面塞字段的"万能Map",那样很快你会分不清某个字段是哪个节点、在哪个阶段写入的。

我在工程里习惯把所有状态字段包在一个上下文对象里,每个节点命名规范统一,比如inputOrderId、riskResult、manualReviewOpinion,一眼就能看出这个字段由谁产出。同时,State里不要存放大数据体,比如长文本内容、文件流,尽量只放引用(比如文件路径或存储ID),避免流程流转时把JVM内存撑爆。

另外要注意State的线程安全设计。Graph本身支持并行节点(这个后面细说),如果多个分支节点同时写State里的同一个字段,就会出现竞争问题。我的方案是:并行分支之间尽量保证各写各的字段,需要合并时才在汇聚节点统一处理。这一点在建模初期就要想清楚。

4. 实战拆解:把客服工单处理从ReAct重构成Graph

4.1 原始ReAct实现长什么样,痛点在哪

我们团队当时最典型的案例是"客服工单自动处理"。用户提交工单,系统判断问题类型、查订单、决定是否退款/补发/转人工,最后通知用户。最初用ReAct实现,核心逻辑就是给模型配置了一堆工具和方法。

第一版跑起来效果还行,简单工单能自动处理掉,但生产环境一压就出问题。最典型的几次事故:

  • 模型在判断"是否退款"时,没有参考公司最新的售后政策,自作主张给了一个不符合规定的退款金额。
  • 用户在工单里说"我不要退款,我要补发",模型仍然执行了退款,因为它只看到了历史订单中的退款记录,没有真正理解用户意图。
  • 整个流程对耗时没有预期,有的工单跑了两分钟还没结束,用户已经等得不耐烦了。

分析下来,核心问题就是第1节讲的:ReAct的自由度不适合用在带规则的业务决策上。退款金额的确定、补发还是退款,这些本来就是确定的业务规则,应该由代码判断,而不是让模型临场发挥。

4.2 图编排的目标流程:六个节点的状态机

我们重构后的Graph流程定了六个节点,走一遍:

  1. 工单接收节点:拉取工单基础信息,存入State,生成处理流水号。
  2. 问题分类节点:用LLM做一次文本分类(工单内容 → 问题类型),这一步保持模型推理,但只输出结构化结果。
  3. 规则判定节点:纯代码逻辑,根据问题类型和用户信息走分支:
    • 退款类 → 走退款处理
    • 补发类 → 走补发处理
    • 其他 → 转人工
  4. 退款处理节点 / 补发处理节点:调用对应业务系统,做幂等校验、写操作日志。
  5. 通知节点:调用通知服务,给用户发送处理结果。
  6. 结束节点:整理审计日志,关闭工单。

关键的分支逻辑放在第3步,用代码判断,而不是让模型选择。LLM在整个流程中的角色只剩下"文本分类"和"话术生成"这两个真正需要语言能力的环节。

4.3 完整改造步骤与关键代码

改造步骤我按下面这个顺序推进,每一步都验证过再走下一步:

第一步:定义State结构。

public class WorkOrderState extends GraphState { private String workOrderId; private String userMessage; private String problemType; // 问题分类结果 private String actionType; // refund / resend / manual private BigDecimal refundAmount; private String noticeMessage; // getter / setter 省略 }

第二步:定义节点函数。分类节点保留LLM,但输出约束成枚举:

node("classify", ctx -> { WorkOrderState state = ctx.getState(); String type = llmService.classify(state.getUserMessage()); state.setProblemType(type); return type; })

第三步:用条件边定义分支。

.edge("classify", "ruleJudge") .condition("ruleJudge", "refundHandler", state -> "refund".equals(state.getActionType())) .condition("ruleJudge", "resendHandler", state -> "resend".equals(state.getActionType())) .condition("ruleJudge", "manualHandler", state -> "manual".equals(state.getActionType()))

这段代码的意图非常直白:分类完成后进入规则判定,根据actionType的值走三个不同的下游节点。测试的时候,你只需要单测ruleJudge这个函数返回不同枚举时边的走向对不对,不用关心模型怎么想。

第四步:执行流程。

GraphResult result = graph.execute(); WorkOrderState finalState = result.getState(); // finalState里可以拿到最终的noticeMessage、refundAmount等

4.4 对比重构前后的硬数据

改造完上线跑了一个月,我把两组数据拉出来对比:

指标ReAct版Graph版
平均处理耗时65秒18秒
Token消耗/工单约9.8k约2.3k
自动处理成功率61%89%
规则类错误(违规退款等)每月3~5次0次
故障恢复方式整单重跑节点级重试

我最满意的是第二行:Token成本直接降了四分之三。原因是Graph把大量不需要语言能力的步骤(规则判断、状态流转、幂等校验)从模型手中拿走了,模型只剩分类和话术生成两个轻量调用。省下来的不只是钱,还有处理延迟和出错率。

注意:重构不是彻底抛弃LLM,而是把LLM放到它擅长的位置。分类、总结、话术生成这些语言理解任务交给模型;参数校验、规则判定、事务处理这些确定性任务交回代码。Graph的核心价值之一,正是帮你完成这种"人机分工"。

5. 复杂流程里躲不开的分支、并行与循环:真实场景怎么设计

5.1 确定性分支:用规则替LLM做判断题

第4节的工单案例里已经用了条件边做分支,这里再深入说一下设计理念。

企业流程里存在大量"判断题",比如:用户欠费吗?余额充足吗?商品在售后期内吗?之前ReAct版把这些判断题交给了模型,让它读完数据"自行判断"。现在我用Graph的条件边,把判断题全部改成代码表达式——因为这根本不是语言问题,是逻辑问题。

条件边的判定函数就是普通的Java Lambda,写起来非常顺手:

.condition("checkBalance", "paymentHandler", state -> state.getBalance().compareTo(state.getAmount()) >= 0) .condition("checkBalance", "insufficientHandler", state -> state.getBalance().compareTo(state.getAmount()) < 0)

这里有个经验:条件边覆盖的分支一定要配一个"兜底分支"。比如判断余额充足走支付、不充足走提示,但万一金额字段为null呢?加了兜底分支后,任何异常输入都会走到一个明确节点,而不是让流程戛然而止。没有兜底的Graph,等于没有异常处理的代码。

5.2 并行节点:同时跑多个独立子任务后如何汇总

真实业务里最常遇到的另一个需求是并行。比如风控审核时,需要同时查黑名单、查历史违约记录、查关联交易,三个查询互相独立,串行跑要30秒,并行跑只要10秒。

Spring AI Alibaba Graph支持并行节点,多个节点可以挂在同一个上游节点下面同时执行。我的用法是:

.edge("riskInit", "blacklistCheck") .edge("riskInit", "historyCheck") .edge("riskInit", "relationCheck")

这三个check节点并发执行,全部跑完后汇聚到下一个节点。汇聚节点的职责是:读取三个分支各自写入State的字段,合并出最终风控结论。

并行设计里最容易踩的坑,我在3.3节已经提过——State的字段冲突。三个分支同时写riskResult字段,后写的会覆盖先写的,结果就是数据丢失。我在项目里的方案是给每个分支定义独立的字段:blacklistResult、historyResult、relationResult,然后再有专门的汇总节点把它们聚合成一个finalRiskResult。这个设计虽然看起来"多此一举",但它保证了并行安全和可追踪性。

5.3 循环与重试的边界:别让流程陷入死循环

Graph天然支持循环(比如"未审核通过则返回重新处理"),但循环也是最容易失控的地方。我见过一个悲剧:某个审批流程做了"未通过自动退回"的逻辑,结果人工审核没配置退出条件,工单在"审核→退回→重新提交→审核"之间无限循环,把审批系统接口打到熔断。

给循环加边界,我有三条铁律:

  • 每次循环必须更新关键计数:State里放一个loopCount字段,每经过一遍循环节点就+1。
  • 循环出口永远有上限:比如最多重试3次,超过直接转人工或结束。
  • 重试逻辑要考虑幂等:重试前检查"这一步是否已经执行过、执行结果是否已落库",避免重复扣款、重复发单。
.condition("reviewNode", "retryNode", state -> !state.isApproved() && state.getLoopCount() < 3) .condition("reviewNode", "manualIntervention", state -> !state.isApproved() && state.getLoopCount() >= 3)

循环不是免费午餐,它本质上是把"系统不确定性"转嫁给业务流程。能用一次判断解决的就别用循环;必须循环时,把循环体和退出条件设计得越窄越好。

6. 企业落地时绕不开的工程化问题:可观测性、版本与成本

6.1 每一步都有迹可循:节点级链路追踪

从单体Agent换成Graph之后,最大的工程红利其实是可观测性。以前排查Agent问题时,只能看一大坨Prompt日志和工具调用记录;现在每个节点天然是一个可追踪的"步骤",定位问题简单太多。

我在工程里给每个节点加了一层统一的日志包装:节点进入时记录一条日志,带上工单号、节点名、时间戳;节点结束时记录结果摘要和耗时。这样从日志平台随便搜一个工单号,就能把整个流程的每一步时间线拉出来。

调试Graph脚本时,这一步的价值极大。某次线上工单在"退款处理"节点失败,日志显示是上游传给它的refundAmount格式异常。我顺着状态快照一看,发现是分类节点把金额字段误写成了字符串类型。以前这种问题在单体Agent里要翻半天对话记录,现在一眼定位到节点边界。

6.2 超时与熔断不只是加一个try-catch

企业AI流程跟外部系统打交道,超时是常态。Graph里的每个Node都应该有独立的超时控制和失败策略,不能寄希望于最外层的一个大try-catch。

我在项目里给每个节点配置了两层兜底:

  • 超时控制:节点执行超过指定阈值就中断,走失败分支。
  • 降级策略:失败后不是直接结束,而是降级到"简化版本"。比如LLM分类节点超时,降级成规则关键字匹配;通知服务不可用,降级成短信模板发送而不是卡住整个流程。

这里有个容易被忽略的点:LLM调用的失败模式跟普通RPC不一样。普通接口超时是秒级的,LLM的响应时间可能从3秒到60秒剧烈波动。如果给LLM节点设置的超时太短,会频繁误杀正常请求;太长又容易拖垮整体SLA。我建议先跑一段压测,统计P50和P99延迟,再分别设置"正常超时"和"熔断阈值",中间留出缓冲。

6.3 图的版本演进:如何让新旧流程平滑过渡

企业流程不是上线就完了,政策一变,流程就要改。Graph的优势在版本管理上体现得很明显——整个流程是一个明确定义的"产物",你可以给每条边、每个节点打版本号。

我的做法是:同一条业务线维护多个Graph版本,线上流量先切小部分到新版本,观测指标无异常再全量切换。比如退款流程从v2调整成v3,先让5%的工单走v3,对比自动处理成功率、客服介入率、客诉率,达标后再逐步放量。

Spring AI Alibaba Graph的State设计也天然适合版本演进:新版本可以往State里加字段,但如果要删字段或改字段语义,就要特别注意兼容性——旧版本流程可能还在跑,它依赖的字段不能被新版本随意改动。我们团队的习惯是State字段一旦上线就不改语义,确实要改就新增字段并存续一整个发布周期。

6.4 成本账本:多少个LLM调用,每一美元花在哪

最后聊一个老板最爱问的问题:成本。单体Agent最难控成本,因为它每个决策步骤都要调用模型;Graph则可以精确控制"哪里该用模型、哪里不该用"。

以一个完整工单处理流程为例,我们可以这样分配:

流程步骤是否调用LLM成本策略
工单接收否纯代码,零成本
问题分类是用小模型/低档位模型
规则判定否纯代码,零成本
客服话术生成是按用户情绪选择档位
审计日志否纯代码,零成本

流程建模完成后,我建议顺手做一张这样的"成本清单",它能帮你发现两个问题:哪些步骤的LLM调用其实是可以砍掉的,哪些步骤的小模型换成大模型能带来明显的准确率提升。另外,Shared的State数据流也能做缓存复用——比如同一用户短时间内提交多个相似工单,分类结果可以直接命中缓存,省掉重复调用。

写在最后:编排的本质是"把不确定性装进笼子"

Graph这几个月用下来,我最大的体会是:编排不是把简单事情变复杂,而是把复杂事情变可控。ReAct给了AI极大的自由度,让它像人一样临场思考;但企业的核心流程要的不是"像人一样思考",而是"像机构一样规范"——每个动作有依据、每个分支有规则、每个失败有补偿。Graph恰好提供了这样一套把流程固化成代码的容器。

如果你手头的项目还在用单体Agent扛核心业务,我建议你按第2.2节的表格对号入座一下。只要有一项命中"上Graph"那一列,就不妨先拿一个最小流程做试点。我个人实际操作下来,改造收益最快见效的往往不是延迟,而是"规则类错误清零"和"排查问题的时间从小时级降到分钟级"这两件事。先跑通一个流程,再把经验复制到其他场景,这条路比一上来重构全部系统稳妥得多。

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

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

立即咨询