
做企业级AI应用一年多我最大的感受是真正难的从来不是让模型生成一段文字而是让模型的行为落在正确的流程节点上在正确的时间、用正确的结果、触发正确的下一步。单Agent的Demo谁都能跑通可一旦牵扯到订单、审批、风控、多系统联动模型自由发挥这件事就变成了事故高发区。Spring AI Alibaba Graph 就是在这一背景下被我从工具链里翻出来反复折腾的东西——它是Java生态里少有的、能正经把图编排落到Spring AI上的方案。这篇我就从ReAct Agent的痛点讲起把Graph的Node、Edge、State这些核心概念一个个掰开最后用企业售后工单的完整场景演示复杂流程到底该怎么编排。适合正在用Spring AI落地项目的后端工程师也适合那些被多Agent乱成一锅粥折磨过的架构师。1. 为什么单Agent扛不住企业复杂流程1.1 先搞清楚ReAct到底在做什么ReAct是Reasoning和Acting的组合。它的核心不是让模型说话而是让模型在思考与行动之间循环模型先分析当前情况决定调用哪个工具拿到工具结果后再决定下一步直到得出最终答案。用Spring AI写一个最简的ReAct调用其实很直接本质就是ChatClient加上工具注册String answer chatClient.prompt() .system(你是一个订单助手务必先查清事实再回答必要时调用工具。) .user(帮我查一下订单A10086到哪了) .tools(orderStatusTool()) // 注册一个查询订单的工具 .call() .content();代码看着简单但底层发生的循环是模型收到用户问题觉得需要查订单于是发起一次工具调用Spring AI拿到调用参数执行真实查询把结果拼回去模型再读结果组织语言回复。这个推理-行动-观察的循环就是ReAct Agent的基本盘它解决了纯Prompt问答拿不到实时、私有数据的问题。但问题也随之而来这个循环完全由模型自己决定何时终止、走几步、用哪些工具。对单个独立问题来说很灵活可对企业流程来说这种灵活性就是不可控性。1.2 单Agent的四个企业级硬伤我把自己在一家电商售后团队里踩过的坑整理了一下单Agent在企业场景至少有四个典型问题**第一个是不可控。**流程顺序全靠在System Prompt里用文字约定比如先查订单再查库存最后给出方案。这句话模型有时候遵守有时候不遵守尤其在上下文变长之后早期指令很容易被稀释。你没法用代码保证必须查完订单才能进退款节点。**第二个是不可观测。**模型内部调用了哪几个工具、顺序如何、哪一步耗时最长企业审计完全拿不到清晰记录。合规场景里监管要求的是流程可回溯不是一句AI处理完了。**第三个是难复用。**业务逻辑以Prompt文本的形式沉淀在Agent里换一个场景就得重新写一套Prompt。同样一条风控校验在退款流程里有在改价流程里也有但没人能拆出这个公共模块只能复制粘贴。**第四个是缺少分支与并行。**订单查询和物流查询明明没有依赖单Agent只能串行处理是否符合退款条件需要独立的规则引擎去判断单Agent却只能让模型觉得应该走哪条路。这些在流程编排里极其基础的能力单Agent天然给不了。我把这四点整理成一句话ReAct Agent擅长如何把一件事做完但不擅长保证所有事情都按规则发生。企业要的恰恰是后者。1.3 编排的本质把确定性交还代码所以后面很长一段时间我都在想能不能把AI当成流程里的一个普通环节而不是整个流程本身这个思路其实就是Workflow编排。本质上讲企业里任何成熟系统都不会让一个组件既负责决策、又负责执行、还负责记录。订单履约有状态机审批流转有工作流AI能力也应该如此。Graph编排做的正是这件事**把走哪条路的决定权从模型手里收回来交给代码里的条件分支把每一步做什么定义成明确的节点把节点间传什么收口到统一的状态对象。**模型只负责它擅长的一部分——比如意图识别、内容生成、复杂判断其余的顺序控制、并发执行、异常兜底都由编排层负责。所以我不认为ReAct Agent和Graph Workflow是对立的。它们更像两个嵌套的层级Graph是骨架ReAct Agent是骨架上的肌肉。理解了这一点Spring AI Alibaba Graph的设计理念就很好理解了。2. Spring AI Alibaba Graph的核心概念拆解2.1 图和状态机有什么关系Spring AI Alibaba Graph底层用到了状态机StateMachine的思路但暴露给开发者的是一套图编程模型。图的意思很直白节点是你要执行的业务单元边是节点之间的流转关系。你不需要像写状态机那样定义繁琐的状态事件表只需要声明从A走到B如果满足X就从B走到C框架帮你把状态迁移的脏活干了。我一开始也疑惑为什么不直接上工作流引擎用过之后我的理解是Spring AI Alibaba Graph的定位介于纯流程引擎和纯Agent框架之间。流程引擎擅长编排确定性任务但AI节点会产生不确定输出Agent框架擅长处理不确定性但缺少流程控制。Graph的方案是状态对象在每个节点间流动每个节点既可以执行普通代码也可以发起模型调用节点的输出写回状态再由边决定下一步去向。这样你既能享受流程的确定性又没丢掉AI的灵活性。2.2 三个基本元素Node、Edge、ConditionalEdgeGraph的世界里只有三个核心概念。**Node节点**是最小执行单元接口签名很朴素接收一个State返回一个State。你可以把它理解成一个函数进去做什么、出来交什么完全由你定义。比如意图识别节点订单查询节点退款判断节点。**Edge普通边**定义两个节点之间的单向流转。addNode(queryOrder, ...)之后再addEdge(intent, queryOrder)表示意图节点执行完就无条件走到查询订单节点。**ConditionalEdge条件边**是Graph最值钱的能力。它允许你在一条边后接一个路由函数由函数根据当前State的内容决定实际跳到哪个节点graph.addConditionalEdges(intent, state - { String intent state.get(intent).toString(); if (refund.equals(intent)) return refundCheck; if (query.equals(intent)) return queryOrder; return humanService; }, Map.of(refundCheck, refundCheck, queryOrder, queryOrder, humanService, humanService) );路由函数返回的是目标节点名框架根据这个返回值做跳转。这比在Prompt里写如果用户想退款就走退款流程可靠得多。条件分支成了显式代码测试、审计、维护都清晰了。还有一个特殊的终止标记END。任何节点都可以通过边连到END表示整个Graph执行结束。我习惯把每个分支最终都收敛到END避免出现图跑完了却不知道何时该停的尴尬。2.3 State节点间流动的工序卡State是Graph里所有节点共享的数据载体内部实际上是一个Map结构。我当时打了一个比方给团队它就像工厂里跟着工件走的工序卡。每个工人完成自己的工序后在卡上填上结果传给下一个人。下图里每个节点读到的是同一个State写着什么、覆盖什么决定了下游节点能看到什么。graph.addNode(intent, state - { // 取出用户问题 String question state.get(question).toString(); // 调用模型做意图识别结果写回state state.put(intent, refund); return state; });这里有一个关键设计问题State在节点间是原地修改还是不可变的Spring AI Alibaba Graph的State设计有一定的可变性操作空间但我的建议是把写入集中化、字段最小化。千万不要在五六个节点里散落着对同一字段的写入否则并发和调试都会非常难受。命名上尽量用统一的key规范比如input.xxx、result.xxx、error.xxx这算是我实测下来成本最低的纪律。还要特别提醒State最终很可能要做持久化、序列化、甚至审计落库所以不要把DataSource、HttpClient这类重对象塞进State。我见过有人把数据库连接写到State里结果节点还没走完连接已经超时了。State只放可序列化的业务数据重对象一律通过Spring容器注入到节点内部。3. 把ReAct Agent搬进Graph实操全流程3.1 工程准备与依赖配置先交代一下工程基础。我用的Spring Boot 3.2JDK 17核心就两个关键依赖Spring AI的相关starter和Spring AI Alibaba的Graph模块。具体版本号我用的是当时稳定的一对组合建议你以项目仓库的Release说明为准不要盲目追最新。dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-graph/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-dashscope/artifactId /dependency模型配置放在application.yml里spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.8这里提醒一下temperature别调到太低尤其是做意图识别、工具调用这类任务。模型温度过低时输出会偏保守有时候该调工具的它不调直接凭记忆瞎编。我一般控制在0.6到0.8之间既能保持稳定又不至于丧失调用工具的主动性。3.2 写一个最小可运行的Graph不整复杂场景先跑通一个两节点的线性流程用户进来先做一个欢迎与规则说明节点再进入模型回答节点。这个Demo的意义在于验证环境、熟悉API调用的姿势。Configuration public class GraphConfig { Bean public StateGraph simpleGraph(ChatClient chatClient) { StateGraph graph new StateGraph(); graph.addNode(greet, state - { System.out.println(进入greet节点当前问题 state.get(question)); return state; }); graph.addNode(answer, state - { String question state.get(question).toString(); String answer chatClient.prompt() .user(question) .call() .content(); state.put(answer, answer); return state; }); graph.setEntryPoint(greet); graph.addEdge(greet, answer); graph.addEdge(answer, END); return graph; } }执行时通过引擎启动MapString, Object initData new HashMap(); initData.put(question, 你们支持7天无理由退货吗); StateGraphEngine engine new StateGraphEngine(); State resultState engine.start(graph, initData); System.out.println(最终答案 resultState.get(answer));这里要注意setEntryPoint不能漏它声明了整个图的入口节点相当于流程图里的START。没有入口引擎根本不知道从哪开始跑。我最初漏掉它时直接报了找不到入口点的异常排查半天才发现是少了一行。3.3 用条件边实现智能路由线性图很快就会被业务需求打破因为真实场景一定有分支。以判断是不是退货咨询为例与其让模型在最终回答里顺便处理不如做成一个条件路由节点graph.addNode(intent, state - { String question state.get(question).toString(); chatClient.prompt() .system(你是意图分类器只能输出一个词return 或 other。) .user(question) .call() .content() .ifPresent(intent - state.put(intent, intent.trim())); return state; }); graph.addConditionalEdges(intent, state - return.equals(state.get(intent)) ? refundFlow : normalAnswer, Map.of(refundFlow, refundFlow, normalAnswer, normalAnswer) );这个模式我用得越来越顺手让模型做窄而明确的单点决策分类让代码做全局的路由跳转。模型只负责判断这句话是什么意图至于判断完往哪走、走到之后做什么全部由Graph的边决定。有一个坑必须提醒条件边路由函数返回的字符串必须和addConditionalEdges传入的Map里的key完全一致而且Map里的value又必须对应真实存在的节点名。我踩过一次节点名写了typo但Map没报错直到运行时才在路由阶段抛出节点不存在的低级错误。所以节点命名尽量全小写、无特殊字符并且把节点名抽成常量。3.4 让ReAct Agent当图中的一个节点现在回答标题里的核心问题ReAct Agent和Workflow怎么编排答案是把Agent封装成Node让它成为图里的一个普通环节。比如售后场景里用户咨询一个复杂售后问题在售后方案生成这一步我不可能用几个固定节点把所有可能性都列完这时候就需要Agent自主决定查多个工具。实现方式就是一个内置ChatClient的工具调用节点graph.addNode(customerService, state - { String question state.get(question).toString(); String answer chatClient.prompt() .system(你是一线售后先查询订单、再查询售后政策综合后给出处理方案。) .tools(orderTool(), policyTool(), refundTool()) .user(question) .call() .content(); state.put(serviceResult, answer); return state; });这个节点的内部就是完整的ReAct循环模型自主决定调用哪个工具、调用几次、何时结束。但从Graph的视角看它只是一个输入State、输出State的普通节点。外面的流程严格控制着这个Agent什么时候上场、上场后下一步是谁。所以我的结论很明确ReAct Agent负责黑盒智能Graph负责白盒流程。两者嵌套使用既保住了大模型的自由度又把流程主线牢牢握在手里。这也是我后面所有企业级项目的标准套路。4. 企业级编排实战售后工单自动处理4.1 场景拆解与节点划分空谈概念没意义我拿一个真实的售后工单自动处理场景完整过一遍。需求是用户提交一条售后问题系统自动完成意图识别、订单查询、政策匹配、风险拦截最后决定是自动退款、生成处理方案还是转人工。按Graph思路我把这条链路拆成7个节点节点名职责类型intent识别用户问题属于查询、退款、投诉中的哪类AI节点窄分类orderQuery根据上下文里的订单号查询订单信息普通代码节点policyMatch根据售后政策匹配可执行动作代码规则节点riskCheck检查退款金额、频次是否触发风控普通代码节点agentSolution调用ReAct Agent生成综合处理方案AI节点ReActautoRefund执行自动退款操作调用退款接口普通代码节点humanHandoff生成转人工摘要并写入工单队列普通代码节点节点划分有一个原则一个节点只做一件能被清晰描述的事。不要做一个万能售后节点又查订单又做政策判断还顺带调工具那样等于把Graph降级成了单Agent。拆开之后每个节点都能单独测试、单独复用这就是编排带来的工程红利。Graph定义如下graph.setEntryPoint(intent); graph.addConditionalEdges(intent, this::routeByIntent, Map.of(query, orderQuery, refund, policyMatch, complaint, humanHandoff)); graph.addEdge(orderQuery, agentSolution); graph.addEdge(policyMatch, riskCheck); graph.addConditionalEdges(riskCheck, this::routeByRisk, Map.of(pass, agentSolution, reject, humanHandoff)); graph.addEdge(agentSolution, autoRefund); graph.addEdge(autoRefund, END); graph.addEdge(humanHandoff, END);路由函数routeByIntent和routeByRisk都是普通Java方法基于State里的字段做判断写单测非常舒服。这也是用Graph之后我写代码节奏变快的原因大部分逻辑都是纯函数不依赖模型输出格式是否稳定。4.2 并行节点提升效率这个场景里有一个很容易被忽略的性能点当用户既问订单进度、又问售后政策时orderQuery和policyMatch之间没有数据依赖完全可以并行。Spring AI Alibaba Graph支持并行节点用法是定义一个并行分支集合graph.addParallelNodes(parallelInfo, Map.of( orderQuery, orderQueryNode, policyMatch, policyMatchNode ), mergeNode);parallelInfo把两个子节点并行拉起两者都完成后统一汇聚到mergeNode。我在实际项目里用这个模式把一次工单处理的平均耗时从8秒压到了5秒以内。但并行有代价多个节点同时写State的同一个key会发生覆盖。我们的解决办法是每个并行子节点只写自己命名空间下的key比如orderQuery.result和policyMatch.result最后在mergeNode里统一决定用哪个、怎么组合。并行节点调试比串行要麻烦因为日志顺序是乱的。建议给每个并行节点打上独立的追踪标识比如在State里放一个traceId日志里全程带这个ID排查时按traceId过滤就能拼出完整链路。4.3 超时、重试与人工兜底企业级AI最容易被忽略的部分是异常处理。模型服务不可能永远稳定外部接口也会抖动Graph如果不做超时和兜底整个流程会卡死在一个模型调用上。我常用的三招**第一招给AI节点设置超时。**模型调用加上Duration约束比如单次ChatClient调用超过10秒就快速失败。对时效性要求高的环节失败后走一个降级回答节点直接返回固定话术而不是让用户无限等。第二招幂等重试。查订单、查政策这类只读节点可以做简单重试遇到上游接口超时最多重试两次。但自动退款这种写操作绝对不要盲目重试宁可转人工也不能因为重试把同一笔退款提交两遍。这是我在生产环境被真实事故教育过的教训。**第三招人工兜底。**Graph一定要留一个人工坐席的出口节点。凡是风控不过、意图不明、模型连续失败的情况全部统一走humanHandoff。我现在的原则是**AI能处理的自动处理AI处理不了的必须明确转人工永远不要让它假装处理完了。**这个节点还可以顺手把AI的推理过程、关键状态字段打包成工单摘要让人工坐席不用重新看一遍聊天记录。这套组合拳下来我的售后场景线上流转成功率稳定在90%以上剩余不到10%全部老老实实进了人工队列没有任何一单被静默吞掉。5. 常见问题与排查实录Graph框架用久了哪些坑最常见、怎么定位我直接整理成一张速查表应该能帮你少走不少弯路。查问题先看State这是Graph调试的铁律。几乎所有逻辑异常都能追溯到某个节点往State里写脏数据或者没写数据。所以我的第一个建议是在关键节点出口打State快照日志字段不多就用JSON整包输出字段多就挑核心业务字段输出。部署到生产环境后这个日志就是你的第一现场。讲一个具体案例。某次线上出现退款单走到了人工队列但坐席看到的是空摘要排查发现是agentSolution节点在返回State时使用了一个新的State对象把原本的traceId字段弄丢了导致后面写入工单摘要时联不上上下文。这类节点返回了错误State引用的问题在自定义Node实现时特别常见。解决办法就是约定节点一律复用入参State只改自己该改的字段不要随意new State返回。另一个高频坑是循环不收敛。有些场景需要在图里做最多重试N次的循环如果写法是A - B - A且缺少计数条件模型一旦连续两次给出同一结果图就死循环了。我的做法是所有循环边上的路由函数都必须检查计数器比如State里维护retryCount达到上限就跳到END或者降级节点绝不允许无界循环存在。再来就是编译期不会报错、运行期才炸的节点名不一致问题。条件边Map里的key、路由函数的返回值、节点注册名这三者必须完全一致。肉眼检查不可靠建议写个启动自检应用启动时用反射把Graph里的所有节点名和所有条件边的目标集合扫一遍不一致直接Fail Fast。这个检查花不了半小时但能省下大量线上排查时间。还有一个容易被忽略的模型返回格式不稳定导致路由异常。比如我让模型输出return或other结果它输出了Return带了大写或者多了一个空格。经验是路由前统一做trim和toLowerCase并且把系统提示词里的分类标签设计得足够简单。更稳妥的方案是让模型用JSON格式输出再用Jackson解析出intent字段解析失败就默认走人工节点。最后说说并发节点里的共享变量问题。同一个Graph实例可能被多个请求并发执行节点里千万别用实例字段缓存请求级数据。所有请求级数据都走State节点自身保持无状态这就是并发安全的最大保障。我在代码评审时看到有人把userId存在节点成员变量里两个请求一来互相覆盖排查了整整一天。6. 我自己总结的一套设计原则6.1 什么时候用Agent什么时候用Graph通用判断标准我概括成一句话如果你的流程有固定步骤、有条件分支、有强管控诉求用Graph如果任务是开放式的、步骤不确定、完全依赖模型临场发挥用Agent。举具体例子。写一篇产品介绍文案步骤可多可少输出千变万化那你就让Agent自由发挥但收到工单-判断类型-查询订单-风险校验-执行退款-记录结果这条链路每一步都有明确入参和出参必须用Graph把它固化成流程。很多团队一上来就堆Agent其实是把流程问题误当成了智能问题。先画流程图凡是非AI不可的环节才考虑放Agent这是性价比最高的策略。6.2 Graph设计的四条铁律第一状态优先设计。写任何节点代码之前先想清楚State里有哪几个字段、谁负责写、谁负责读、生命周期多长。State方案定了Graph骨架基本就定了。第二节点要小、语义要纯。一个节点做一件事用动词短语命名比如queryOrder、riskCheck。节点就像函数功能越单一越容易被测试和复用。第三路由必须收敛。条件边一定要有默认分支而且默认分支最好是兜底节点转人工保证无论State数据多离谱流程都能走向一个已知出口。第四可观测性内置。给每个节点加traceId、耗时、状态快照三个基础日志。宁可日志多不可出问题后两眼一抹黑。生产环境的AI流程排查绝大多数靠的就是这份日志。6.3 再往后还能怎么扩展Graph这套机制的价值在于它能平滑承载更复杂的企业能力。我目前看到的方向有这么几个节点能力继续沉淀成可复用的组件库State的序列化机制可以支撑流程持久化和断点续跑条件路由逻辑升级成可视化配置再加上版本管理和灰度发布让AI流程像普通业务代码一样做迭代。顺着AI变成业务流程的一个标准组件这个方向后面能做的事会越来越多。我在实际项目里还有一个体会Graph项目在Java生态里还是相对年轻的遇到问题先去源码里翻比到处搜教程高效得多。尤其State的序列化逻辑和并行节点的调度源码花一个下午读完你对整个框架的理解会比看十篇文章都深。如果你正打算把企业里的AI能力从能跑升级到可控、可管、可复用那么从今天这个最小的Graph开始搭是个不错的起点。