企业只要试着把AI从“问一句答一句”变成“真正跑一段流程”,大概率都会卡在同一个问题上:流程到底该让模型自己走,还是让代码硬控?前两年大家迷信ReAct Agent,觉得给模型几个工具,它就能像人一样拆解问题,结果一上生产就被真实需求教育了——动作不可控、Token烧得飞快、关键环节还说不清楚为什么走到这一步。后来大家又走向另一个极端,把所有逻辑都写成硬编码工作流,结果业务一变,代码就得跟着改,模型能力又浪费了。
真正合适的做法,是用Spring AI Alibaba Graph这种图编排能力,把ReAct Agent和Workflow揉进同一个流程体系:Agent负责需要推理和泛化的环节,Workflow负责固定、可审计的环节,两者通过图中的节点和边组织起来。这篇文章我会用一个企业工单处理场景,完整走一遍从Agent到Graph工作流的编排思路,包括节点怎么设计、状态怎么传、条件分支怎么收敛、踩过的坑怎么处理。适合正在做企业AI应用落地的开发者和架构师参考,特别是那些已经开始用Spring AI Alibaba,但还在纠结流程该怎么组织的朋友。
1. 为什么单靠ReAct Agent撑不起企业AI流程
1.1 ReAct Agent的强项与边界:能思考,但管不住过程
ReAct的核心思路是让模型在“思考-行动-观察”之间循环:模型先分析当前问题,决定调用哪个工具,拿到工具返回结果后再决定下一步动作。这种模式非常适合开放式任务,比如“帮我查一下这个订单为什么发货延迟”,模型可以自己去查订单系统、物流系统、仓库库存,最后综合给出结论。
但放企业场景里,ReAct的短板也很明显。最突出的是过程不可控:模型每一步都是自由决策,你很难保证它一定会按合规顺序走,比如先查客户协议再做退款操作,而不是跳过去直接改状态。其次是不可复现:同一个问题跑十次,中间可能调用不同的工具,走不同的路径,出了问题你都不知道该从哪里回溯。还有一个很现实的问题——Token开销,Agent一多轮思考,Prompt里塞的历史消息越来越长,单次调用的成本成倍上涨,生产环境根本扛不住。
我在模拟项目X里试过让单个ReAct Agent处理“用户投诉退款”场景,结果它为了确认退款规则,把三个文档工具来回翻了五遍,最后还猜错了一个字段含义。模型不是不够聪明,是它根本不知道“哪些环节该停下来走审批”、“哪些环节必须给用户发通知”。这些流程约束,不能靠模型自觉,得靠编排框架来管。
1.2 企业流程的真实诉求:可控、可观测、可恢复
企业里的AI流程和C端聊天机器人有一个本质区别:流程一旦跑起来,就涉及真金白银和业务合规。比如工单处理、订单变更、费用审批,任何一步出错都可能造成实际损失。所以企业AI流程的要求,核心是三件事。
第一是可控。流程走到哪一步了、为什么走这一步、下一步是什么,都必须能被业务方理解。模型可以在节点内部自由发挥,但节点之间的跳转关系必须事先定义好,不能随机发散。
第二是可观测。每个节点要有清晰的输入输出,状态变化要能被记录,出现异常时能拿到完整的链路日志。否则生产环境出了问题,你连“是哪一步把状态写坏了”都定位不到。
第三是可恢复。某个节点调外部接口超时了、模型解析返回结果格式不对、用户中途改了诉求,这些异常都要有兜底机制。要么重试,要么走人工降级,要么回退到上一个稳定状态,而不是让流程卡死在那里。
把这三个诉求摆出来,就会发现单一ReAct Agent和传统硬编码工作流都搞不定。ReAct灵活但不可控,硬编码可控但不灵活。Graph编排正是这两者之间的折中:用显式的图结构保证流程边界,用节点内的Agent保证局部智能。这也是Spring AI Alibaba Graph这类编排框架在这个节点上被关注的原因。
这里我放一张对比表,三类方案的差异一目了然:
| 能力维度 | 单一ReAct Agent | 硬编码工作流 | Graph混合编排 |
|---|---|---|---|
| 分支灵活性 | 高 | 低 | 中高 |
| 流程可审计性 | 低 | 高 | 高 |
| Token成本控制 | 差 | 好 | 好 |
| 业务变更响应 | 快但不可控 | 慢但稳定 | 折中 |
| 实现复杂度 | 低 | 中 | 中高 |
| 适合场景 | 开放问答、探索型任务 | 固定规范流程 | 企业复杂流程 |
2. Spring AI Alibaba Graph的编排模型与核心概念
2.1 Node、Edge、State:怎么把流程画成一张图
Graph编排的基本思想是:把你的业务流程拆成若干个节点(Node),节点之间有向边(Edge)连接,整个流程通过一个共享状态(State)来传递数据。可以类比成外卖厨房的出餐流水线:每个工位都是一个节点,工位之间通过传送带连接,食材和半成品放在托盘里流动,托盘就是State。
节点是执行单元,它可以是一个大模型调用、一个工具API调用、一段普通Java方法,甚至可以嵌套一个完整的子图。边决定了流程的走向,分为普通边和条件边。普通边就是执行完A节点后固定去B节点,条件边则是根据当前State里的数据判断走哪个分支。State是整个流程的“工作台”,所有节点从State里读取需要的输入,处理完再把结果写回去。
用Spring AI Alibaba Graph做编排,不需要你在代码里手写一堆if-else来串流程。你只需要定义好节点、把节点之间的连接关系声明出来,框架会按照图的结构自动推进流程执行。这种抽象方式把“业务流程”和“代码实现”之间的映射关系拉得很近,业务方画出来的流程图,技术人员基本上能直接翻译成Graph定义,沟通成本低很多。
2.2 条件分支与回边:让流程既能收敛又能循环
企业流程里最怕两种图结构:一种是一条道走到黑,没有分支;另一种是分支太多,流程失控。Graph编排里的条件分支能解决前者。
条件分支用得非常常见,比如“退款金额大于5000元需要走审批,否则自动退款”,这就是典型的条件边。在Graph里,这个判断本身也是一个节点,这个节点执行完,不是直接去固定的下一个节点,而是由它返回的结果决定后边走哪一个分支。
回边则对应流程里的循环,比如“审核不通过,退回重新修订”。没有回边的图是DAG(有向无环图),适合线性流程;有了回边,流程就有了循环能力。但回边必须设置边界条件,否则会陷入死循环。常见的做法是在State里维护一个计数器,比如循环次数上限为3次,超过就直接走人工处理分支,避免机器无限循环消耗资源。
在我设计的工单流程里,意图识别节点后面有两个分支:一个走向“查询类工单”的处理链路,一个走向“变更类工单”的处理链路。同一个入口,根据State里的intent字段值走不同路径,这就是条件分支的典型用法。
2.3 把ReAct Agent装进节点:局部自治,全局受控
Graph编排最妙的地方在于,一个节点本身可以是一个Agent。也就是说,你不需要牺牲掉ReAct的智能性,只需要把它的活动范围限制在一个节点内。
这里要重点理解“局部自治,全局受控”这八个字。Agent节点内部,模型可以自己决定调用哪个工具、按什么顺序调用,它拥有足够的自由度来处理开放问题。但整个Graph的边界是确定的:Agent节点不能自己跳出图去调用不在其工具列表里的东西,也不能绕过前面的意图识别节点直接去访问业务系统。一旦Agent节点内部跑完,它返回的结果就是一个结构化的JSON,下一跳是哪个节点,由图来决定。
这种设计解决了一个很矛盾的需求:企业既想要模型的智能,又想要流程的确定性。Graph不会让模型代替你做流程决策,它只让模型在划定的区域内发挥。比如工单场景里,意图识别交给一个Agent节点,因为它要理解用户千变万化的表达,这个需要大模型的泛化能力;意图一旦确定,后续走什么链路就是固定的业务规则,不需要模型参与,用Workflow节点就够了。
3. 企业级工单处理流程的Graph编排实战
3.1 场景拆解:一条工单从提问到关闭要经过哪些节点
我以一个典型的企业客服工单场景来演示,场景本身是模拟项目X里的,不涉及真实业务数据,但结构非常典型:用户提交一条自然语言工单,系统需要自动识别意图、抽取关键信息、生成处理方案,如果涉及金额变更还需要审批,最后通知用户处理结果。
完整的流程节点拆开来看是这样:
- 意图识别节点:接收用户原始文本,由Agent判断工单类型(查询、报修、退换、投诉)。
- 信息抽取节点:根据意图,从工单内容里抽取出必要的结构化字段,比如订单号、产品型号、问题描述、期望处理方式。
- 方案生成节点:基于抽取结果调用内部知识库,生成处理建议,这个节点也可以做成Agent节点,让它自己搜索相关规则文档。
- 审批判断节点:根据处理方案涉及的金额、权限范围,判断是否需要上级审批。
- 审批执行节点:对接审批系统,记录审批状态,这个节点也可以是人工审批的等待节点。
- 结果通知节点:调用消息服务,把处理结论通过短信或站内信发给用户。
每个节点的输入输出都依赖State。意图识别节点读入的是原始工单文本,输出的是意图类型;信息抽取节点读入的是原始文本和意图类型,输出的是结构化的抽取结果;方案生成节点读入抽取结果,输出方案摘要。用图把这些节点串起来,就形成了一个完整的处理链路。这个链路的优势是每一个环节都能单独测试和替换,业务规则变了,只改对应节点,其他不动。
3.2 Graph代码骨架:状态、节点、条件边的落地写法
我先定义整个流程共享的State,用一个类来承载所有节点之间需要传递的数据:
public class TicketState { private String userQuery; // 用户原始工单 private String intent; // 意图识别结果 private Map<String, Object> extractInfo; // 信息抽取结果 private String solution; // 处理方案 private boolean needApproval; // 是否需要审批 private String status; // 当前工单状态 // getter/setter 省略 }然后定义节点。在Spring AI Alibaba Graph的模型里,每个节点主要实现一个执行方法:从State里读数据,执行业务逻辑,把结果写回State,最后返回下一个要执行的节点标识。意图识别节点可以这样理解:
public class IntentRecognizeNode implements Node<String, TicketState> { @Override public String execute(TicketState state) { String intent = agentService.recognize(state.getUserQuery()); state.setIntent(intent); return "extractInfo"; // 无论识别成什么,下一步都是信息抽取 } }审批判断节点是一个条件节点,它会根据State里的数据决定下一跳:
public class ApprovalJudgeNode implements Node<String, TicketState> { @Override public String execute(TicketState state) { // 金额超过阈值或涉及敏感操作,需要人工审批 boolean needApproval = isOverThreshold(state.getExtractInfo()); state.setNeedApproval(needApproval); return needApproval ? "approvalProcess" : "notifyResult"; } }Graph的组装就是把节点和边声明出来,这通常是这样一个模式:
GraphBuilder<String, TicketState> builder = new GraphBuilder<>(); builder.addNode("intentRecognize", intentRecognizeNode) .addNode("extractInfo", extractInfoNode) .addNode("solutionGenerate", solutionGenerateNode) .addNode("approvalJudge", approvalJudgeNode) .addNode("approvalProcess", approvalProcessNode) .addNode("notifyResult", notifyResultNode); // 走向 builder.addEdge("intentRecognize", "extractInfo"); builder.addEdge("extractInfo", "solutionGenerate"); builder.addEdge("solutionGenerate", "approvalJudge"); // 条件分支 builder.addConditionEdge("approvalJudge", "approvalProcess", (state) -> Boolean.TRUE.equals(state.isNeedApproval())); builder.addConditionEdge("approvalJudge", "notifyResult", (state) -> Boolean.FALSE.equals(state.isNeedApproval()));注意这里的走向和分支都是可以替换的,实际项目里你可以根据业务规则做更复杂的图结构。核心思路是:节点的执行逻辑和节点之间的跳转逻辑被解耦了,改流程连接关系不需要动节点本身的代码。
3.3 状态传递与上下文管理:别让一个Map毁掉整个流程
用共享State传递数据,最方便也最危险。方便在于所有节点只要从State里取数据、写数据就行,不用设计复杂的参数传递链。危险在于如果对State的读写不加约束,几十个节点共享一个Map,字段到底是哪个节点写进去的、什么格式、有没有被覆盖,很快就会变成一团乱麻。
我在实际操作中总结了几条状态管理的约束,分享出来:
第一,每个节点写State时要单独定义输出字段,避免一个节点顺手修改了另一个节点将要读取的字段。比如意图识别节点只写intent字段,信息抽取节点只写extractInfo字段,大家各管各的,职责清晰。
第二,State里的数据尽量保持不可变结构,节点要修改某个字段时,新建一个值替换上去,而不是在原来的集合上增删元素。这样出现问题时,能通过State的快照还原现场,排查起来容易很多。
第三,跨节点的上下文传递要控制体积。有些开发者习惯把Agent运行时的完整对话历史都塞进State,结果导致State越来越大,每执行一个节点都要序列化、反序列化一遍,性能急剧下降。我的做法是,State里只保留业务流程需要的关键信息,Agent内部的对话历史用单独存储或在节点内部管理,不进全局State。
第四,条件边依赖的字段,要有默认值。比如approvalJudge节点读取needApproval字段,如果前面的节点因为异常没写入这个字段,条件边判断时会拿到null,流程走向就不可预期了。我在所有条件判断处都会做空值保护,拿不到值就走保守分支。
4. 企业落地时的三个硬伤与排查实战
4.1 状态污染:共享State被并发节点改乱了怎么办
Graph流程和企业IT系统对接后,第一个会翻车的点是并发。工单处理场景里,同一张工单可能被多个触发源更新:用户追加留言、定时任务触发超时提醒、内部系统推送订单状态变化。这些操作如果并发进入同一个Graph执行,共享的State就会发生写冲突。
我遇到过一个具体问题:两个并发执行的节点,一个把工单状态改成“处理中”,另一个同时把状态改成“已关闭”,后写的覆盖了先写的,用户收到通知说工单关闭了,但实际上处理流程才走到一半。
排查之后的解决办法是两件事。第一,在入口处做状态版本控制,State里加一个version字段,写回前检查版本号,不一致就拒绝写入并触发重新读取。第二,尽量避免多个流程实例操作同一份State,改成每个流程实例持有自己的State副本,结束时通过事务性操作合并回业务主数据。这个思路在Spring AI Alibaba Graph里同样适用:State是流程实例级别的,不是全局共享的。
4.2 循环失控:Agent节点反复执行同一个动作
前面提到回边可以支持循环,但循环在Agent节点里是个隐藏炸弹。我见过最典型的情况是:Agent在意图识别节点里,因为对某个模棱两可的意图不确定,反复调用知识库去查历史工单,查完还是不确定,又查一遍。每个循环都在消耗Token,但结果没有任何进展。
这里有两层防护措施。第一层是Agent自身要设最大迭代步数,比如限制“思考-行动-观察”循环最多不超过12次,超过之后Agent必须返回当前最佳结果而不是继续尝试。第二层是Graph层要设节点执行超时时间和循环次数上限,哪怕是Agent节点,单次执行时间超过60秒就强制中断,走降级分支。
从业务流程来讲,这么做也有合理性:一个意图识别节点如果反复确认都拿不准,说明用户的输入确实模糊,与其让机器猜,不如直接走人工介入分支,让真人员工处理。在实际工单场景里,这种降级分支是很有必要的,它保证了无论模型状态多差,流程都能收敛。
4.3 链路调试:Graph跑挂了到底看哪条日志
Graph编排把原来线性代码拆成了多个节点,好处是模块化了,坏处是出问题时定位链路变难了。以前一个方法从头到尾,异常栈一眼就能看到哪行代码出错;现在异常跨了多个节点、多次状态读写,排错需要额外的链路机制。
我的习惯是三管齐下。第一,给每次Graph执行生成一个唯一的traceId,在入口生成并塞进State里,所有节点的日志都带这个traceId。这样不管流程跳了多少个节点,只要按traceId一搜,整个链路的执行日志都能串起来。第二,每个节点执行完,把节点的输入摘要、输出摘要、耗时都记录下来,形成执行轨迹。这个轨迹的格式是统一的,方便后续做分析。第三,Graph执行引擎本身要保留最后N次执行的快照,发生异常时可以直接查看某个流程实例走到了哪个节点、哪条条件边被触发、State里最后的值是什么。
我做了一个小工具,把Node执行情况输出成表格样式,基本长这样:
| 节点名称 | 执行耗时 | 输入状态片段 | 输出状态片段 | 下一跳 |
|---|---|---|---|---|
| intentRecognize | 2100ms | userQuery: 我的订单超时未发货 | intent: 催发货 | extractInfo |
| extractInfo | 350ms | intent: 催发货 | orderId: 2025050001 | solutionGenerate |
| approvalJudge | 120ms | amount: 0, needApproval: false | needApproval: false | notifyResult |
有了这个表,整个流程的每一步走向都看得清清楚楚,不再是个黑盒。
4.4 常见问题排查速查表
我把这段时间在Graph流程编排里踩过的坑整理成一个速查表,方便大家直接对照排查:
| 症状现象 | 可能原因 | 处理方案 |
|---|---|---|
| 流程卡在某个节点不往下走 | 节点返回的next为空或非法值 | 检查该节点的返回值是否在Graph声明的边里存在 |
| Agent节点Token消耗异常高 | Agent循环没有最大步数限制 | 给Agent设置最大迭代步数,超出后强制返回 |
| 条件分支永远走同一个方向 | 条件判断字段被前面的节点覆盖 | 检查State写入顺序,给条件字段加只读约束 |
| 并发场景下状态被覆盖 | State是全局共享实例 | 改为流程实例持有独立State,加version控制 |
| 节点顺序执行但结果不对 | 节点之间隐式依赖了执行次序 | 把隐式依赖改成显式的字段传递,避免读全局变量 |
| 工单量大时执行超时 | 外部API调用串行等待 | 可用节点支持异步编排,并行调用多个外部系统 |
5. 一点个人经验
最后说点比较主观的体会。Graph编排这种模式,真正落地时最花时间的往往不是写节点,而是梳理业务边界:哪里允许模型自由发挥,哪里必须用代码硬控。这个边界画对了,Graph就是帮你把Agent和Workflow的优势都发挥出来的骨架;画错了,要么模型被规则绑死,灵活的Agent退化成一堆if-else,要么规则被模型带偏,流程散得收不住。
我在模拟项目X里反复调了几版才找到一个相对舒服的原则:凡是涉及“理解不确定人类表达”的环节,比如意图识别、方案推荐,交给Agent节点;凡是涉及“确定业务规则”的环节,比如金额判断、审批流转、通知发送,交给普通节点和条件边。理解环节让模型跑,规则环节让代码跑,整个流程就会顺畅很多。
另外,如果你们的业务流程刚开始梳理,不要一上来就把图做得特别复杂。先从一条主干链路跑通,比如从工单提交通知到用户,先串成线性流程,再加上一个条件分支,再加上Agent节点,一层层迭代。Graph编排的容量很大,但学习曲线需要一步步来。用熟练之后,你会发现企业AI复杂流程编排这件事,本质上不是让模型学会跑流程,而是让流程把模型的智能安放在正确的位置上。