☰
Spring AI Alibaba Graph:用工作流编排驯服失控的AI Agent
2026/10/20 8:05:53 网站建设 项目流程

1. 先从单Agent的“失控感”说起

我去年带着一个团队维护企业内部服务台的智能工单系统,最开始架构特别简单:一个ReAct Agent、五六个工具函数、一个聊天入口,模型自己决定要查什么、按什么顺序查,最后把答案拼出来。Demo跑得是真顺,领导看完连连点头,但一上真实流量,问题就开始往外冒。说句实话,不是模型变笨了,而是"让模型自由发挥下一步"这个设计,在企业流程里本身就很难兜底。

ReAct这个名字来自Reasoning和Acting,核心机制并不复杂:模型先生成一段推理,说清楚"我现在需要知道什么",然后调用一个工具拿到新信息,再接着推理下一步。这种循环非常适合单点问题,比如"查一下这个账号在12点到13点之间的登录日志,判断有没有异常"。但业务一旦变成"查完日志如果判断是网络问题就自动重启服务,重启后还要验证,验证失败要升级人工,同时给用户回执",纯ReAct就开始失控。

1.1 ReAct擅长的,恰恰是企业流程不擅长的

ReAct把"下一步做什么"完全交给了模型的概率输出。这对开放域的问答没问题,但对那些需要闭环、需要审计、需要在某个环节插入人工审批的企业流程,概率输出意味着不可控。你可以通过提示词约束模型,让它按步骤走,可提示词是文本,不是程序控制。换一个版本的模型,哪怕只是参数微调,可能就有工单流到一半不走的情况。

后来我们把所有线上异常事故统计了一下,发现大部分失败的根因都不在"工具调用",而在"流程没有确定性保障"。比如说,模型明明已经判断出账号是锁定状态,后面又绕回去查了一遍网络连通性;再比如说,同一个工单描述,上午走的是网络修复分支,下午因为模型输出的微小变化,走到了人工审批分支。这种不确定性在Demo里完全看不出来,但放到生产环境,运营和客服第一个不答应。

在企业里,"下一步是谁、在什么条件下触发、失败怎么办"应该是明确写出来的规则,而不是模型临场发挥的结果。这也是我后来下定决心把ReAct Agent拆开,放到一个可编排流程图里的原因。既然流程是公司多年业务经验沉淀出来的,那就应该由代码稳定地表达出来,模型只负责其中需要理解力的部分。

1.2 单个Agent做复杂流程的几个麻烦

这里我总结几个真实踩过的坑:

第一,长链路状态会丢。单Agent为了让模型记得上下文,会把很多信息塞进对话历史。对话一长就有两个副作用:一是token消耗上涨,二是模型可能忽略之前的关键结论。比如它前面已经判断"账号是锁定的",后面又去查了一次网络连通性。不是它笨,是记忆不可靠,你不能指望一个基于概率的模型天然维护好业务状态。

第二,分支逻辑难以稳定处理。工单类型本来就分网络、账号、权限、硬件等好几类,不同类别处理动作完全不一样。ReAct Agent无法保证"每次都先分类,再按分类走对应子流程",它可能会跳过分类直接处理,这在业务上很危险。最讽刺的是,你可以在提示词里写一百遍"请先分类",它还是偶尔不分类。

第三,运维观察不到中间状态。用ReAct时,整个运行过程就是一段对话记录,没有明确的节点概念。出了故障,你只能翻模型日志去猜它在哪一步卡住了。有一次用户反馈工单没有及时处理,我们查了半小时日志才发现模型在第七次工具调用后输出了一段无关内容,流程就断了。而工作流里每个节点都有名字、状态和耗时,一眼就能看出是哪一段出问题。

第四,治理和审计困难。企业流程往往要求记录"谁在哪一步做了什么"。纯Agent很难给出这个能力,因为它没有一个可枚举的动作集合。Graph工作流天然有节点与边,每一步都能落日志,审计需求轻松满足。对于要过等保、要应付内部合规检查的企业,这一条基本是刚需。

1.3 什么情况下该切换

不是说ReAct不好。我的经验是,判断标准很简单:如果这个任务允许模型"答不上来或者换条路再试",单Agent没问题;如果任务要求"必须走完这几步,且每一步结果决定下一步",那就要用工作流来兜底。后者正是Graph编排的领域。

Spring AI Alibaba Graph做的就是这件事,它把一个工作流抽象成有向图,节点放业务逻辑,边决定流转方向,模型只是某个节点上的一个执行者。这样既保留了AI语义理解的能力,又收回了流程控制权。接下来我先把它的核心设计讲清楚。

2. Spring AI Alibaba Graph是怎么把流程变成图的

如果之前你没接触过Graph类的工作流,可以把Spring AI Alibaba Graph理解成Java版的状态机加DAG。你定义一批节点,每个节点干一件明确的事,再定义节点之间的边,规定从那走到哪。图被编排好之后,调用一次就相当于从入口节点开始,沿着边一路执行到终点,所有中间数据放在一个共享状态对象里流动。

2.1 状态就是整个流程的“公共白板”

Graph运行的核心是一个状态模型,你可以把它当作整个流程的公共白板。所有节点往白板上写东西,后面的节点再基于白板内容做判断。节点执行时都能拿到当前这趟运行的状态,节点修改完状态返回,图框架负责把更新合并进整个运行上下文。这个设计和LangGraph的思路很像,理解起来不费劲:你只需要想清楚流程里有哪些"数据"需要被共享和传递。

我经常用一张表格来设计白板字段:工单ID、用户反馈内容、故障分类、检查结果列表、处理动作、处理结果、验证结论、通知状态。每个节点读自己需要的字段,写自己负责的字段,跟多人协同编辑同一个文档差不多。节点之间不直接通信,一切通过白板,这让每个节点都能单独测试,这一点在企业项目里非常重要。

举个例子,如果诊断节点发现"账号锁定",它只需要在白板上写"category=ACCOUNT_LOCKED"和"evidence=登录失败次数超过5次",后续的账号修复节点自然会去读这两个字段,并决定下一步动作。流程整体是一条数据流,而不是一堆互相调用的Java方法,这让代码的可读性和可维护性都上升了一个台阶。

2.2 节点做执行,边做路由,条件边做分支

图模型里有三类核心东西:节点、普通边、条件边。

节点好理解,一段Java代码,一个方法或者lambda,负责一段业务逻辑。普通边就是"A结束后一定去B",比如受理节点结束后一定进入诊断节点。条件边是重点:它从源节点出发,根据当前状态里的某个字段,动态决定去哪个目标节点。比如诊断节点后面有三个出口:

条件去向
故障分类 = 网络网络修复节点
故障分类 = 账号账号解锁节点
其他人工审批节点

条件边把原来ReAct里"模型自由选择的下一步",变成了"基于明确状态做出的路由决策"。你可以让模型负责分类,它只输出一个分类结果,然后程序根据这个结果走边。这就是"模型的归模型,流程的归流程"。这种拆分看起来简单,却是整个企业级AI流程稳定性的基石。

2.3 为什么这个能力放在Java生态里很重要

很多团队用Python做AI原型,但落地到企业核心链路时,Java仍然占主流。Spring AI Alibaba Graph能直接嵌在Spring Boot应用里,因为它的节点就是普通Java方法,不引入Python运行时,不增加异构服务。你可以在一个项目里同时维护REST接口、MQ消费、定时任务和这个AI工作流,用同一套日志、监控、链路追踪体系。

对我们这种要交付给运维团队的工程来说,这个收益非常实在——你的AI流程不是一个独立的黑盒系统,而是一个能被工程化管理的模块。模型请求失败可以走Spring的重试机制,状态快照可以存到数据库,节点方法可以写单元测试,一切都可以纳入现有的DevOps流程。这是很多Python脚本项目做不到的,也是企业技术负责人最关心的一点。

3. 上手准备:先从一个能用的Agent开始

说再多概念,不如先跑起来一个真实可用的Agent。我建议不要一上来就写Graph,先把Agent本身调通,确认模型能调用工具、能回答业务问题,再把它放流程里。

3.1 工程与配置

我用的是Spring Boot加Spring AI Alibaba,依赖方式直接看项目文档,关键是配置文件里把模型服务地址和密钥填好。下面是一个简化的配置示例,具体参数名以你项目里实际依赖版本为准:

spring: ai: alibaba: cloud: api-key: ${CLOUD_API_KEY} chat: options: model: your-chat-model temperature: 0.3

temperature我习惯设在0.2到0.4之间。工具调用场景下,温度太高会带出随机性,太低又可能让模型生成过于保守的表述。实测下来0.3是个不错的起点。如果你想追求极致的确定性,甚至可以调到0.1,但那样会让模型在需要一点发散思维的场景里变得呆板,所以建议按任务性质分开配置。

3.2 注册工具函数

Spring AI里工具函数就是普通的java.util.Function,Bean注册后可以被模型自动调用。业务上常见的就是查询工单状态、查登录日志、查配置项这一类。这里我给一个简化版:

public record QueryRequest(String ticketId) {} public record QueryResponse(String status, String owner) {} @Bean @Description("根据工单编号查询当前处理状态") Function<QueryRequest, QueryResponse> queryTicketStatus() { return request -> { // 真实环境这里会查数据库或调用内部服务 return new QueryResponse("PROCESSING", "ops-team"); }; }

工具定义的细节很重要:@Description写得好不好,直接影响模型能不能正确选择这个工具。我见过很多人随便填一句,结果模型把"查库存"的工具拿去回答"查订单"的问题。描述里要写清楚输入参数是什么、返回什么、适合什么场景,最好加上一两个典型例句。比如"当用户询问工单处理进度时使用"就比"查询工单状态"要明确得多。

3.3 让模型自动决定调用

在Controller里创建一个ChatClient,把工具注册进去,模型就能在对话中自主选择调用:

@RestController public class AgentController { private final ChatClient chatClient; public AgentController(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem("你是企业服务台助手,只根据事实回答,不要编造工单状态。") .build(); } @PostMapping("/assistant") public String assistant(@RequestBody String question) { return chatClient.prompt() .user(question) .functions("queryTicketStatus") .call() .content(); } }

跑起来之后,你可以先做几组冒烟测试:把工单ID给模型,看它会不会主动调用查询工具;给它一个库里不存在的ID,看它会不会胡编。如果它能正确调用工具并且不编答案,说明这个Agent基本合格。这时候你已经能解决"单点查询"类的问题,但离企业级流程还差得远。

3.4 为什么只靠Agent还不够

我来说个更具体的例子。有一次测试人员问Agent:"工单WO-1001处理到哪一步了?"它正确查了工单状态,回答"处理中"。测试人员又问:"那这个工单为什么还没好?"模型开始根据工单描述猜测可能是网络问题,接着自己调了网络工具,然后直接断言"是网络问题"。

问题来了,模型跳过了账号检查、应用日志检查等多个步骤,直接得出结论,而且没有验证。这在单点问答里问题不大,但如果这个结论直接推给用户或者自动触发修复动作,那就是事故。所以我才坚持要把Agent拆进Graph,用流程约束住它。模型可以给出假设,但流程必须负责验证和收口。

4. 用Graph编排一个真正的多步工作流

还记得我前面说的那个业务吗?"先查工单,再做分类,按分类走处理路径,结束后验证并通知用户。"现在我用Graph把它建模出来。先说一个通用提示:Spring AI Alibaba Graph还在快速迭代,下面代码是按当时的API风格写的,你实际用的时候,对照一下当前版本的类名和方法签名,整体结构不用大改。

4.1 定义流程状态

我先定义一个简单的状态模型,用一个Map承载所有共享数据。Graph框架需要知道状态的类型,所以用一个工厂方法或者构造器来产生状态:

StateGraph<Map<String, Object>> graph = new StateGraph.Builder<>( MapStateSchema::new )

实际工程里我更推荐定义一个工单状态的POJO,字段类型清晰,不容易在协作中写错key。Map状态写起来快,但团队大了之后,"ticketId"和"ticket_id"这种命名混乱会浪费很多排查时间。所以这里给个建议:小型demo用Map没问题,正式项目里建议用带类型的StateSchema。状态类设计得好,整个流程的逻辑都会清晰很多。

4.2 把业务动作拆成节点

节点是一个个方法,每个方法接收状态、处理一段业务、返回新的状态。把之前Agent的动作拆开:

Map<String, Object> receiveNode(Map<String, Object> state) { // 这里写受理日志,把工单ID登记到流程上下文 state.put("status", "RECEIVED"); state.put("category", classify(state.get("message"))); return state; } Map<String, Object> networkFixNode(Map<String, Object> state) { // 这里调用网络服务重启、刷新配置等动作 state.put("handleResult", "NETWORK_RESET_DONE"); return state; } Map<String, Object> verifyNode(Map<String, Object> state) { // 调用验证服务看业务是否恢复 state.put("verifyPassed", verify(state.get("ticketId"))); return state; }

每个节点只感知自己需要的字段,不依赖别的节点内部逻辑。一台服务器上的重启脚本、一个库存接口、一个审批服务,都可以包成节点。这样做还有个额外好处:就算某个节点崩溃,整个图不会跟着崩,框架可以按你设定的重试策略重新执行单个节点。

4.3 用条件边控制分支

接下来定义边。受理之后进入分类,分类节点要判断去哪个处理路径,这就是条件边:

.addConditionalEdges("分类", state -> { String category = (String) state.get("category"); if ("网络".equals(category)) return "网络修复"; if ("账号".equals(category)) return "账号解锁"; return "人工审批"; }, Map.of( "网络修复", "网络修复", "账号解锁", "账号解锁", "人工审批", "人工审批" ))

这里的关键是把"分类"这个判断交给了大模型,但把"按分类走哪条路"交给了程序。模型即使在分类上有一点抖动,后续路由仍然是确定性的。你甚至可以不做LLM分类,改用规则关键词分类,那整个路由就完全由代码控制,稳定性更高。我在生产环境里混合了两种:先跑规则匹配,匹配不上才让模型分类。

4.4 组合成一个图并运行

普通边和条件边组合起来:

StateGraph<Map<String, Object>> graph = new StateGraph.Builder<>(MapStateSchema::new) .addNode("受理", this::receiveNode) .addNode("分类", this::classifyNode) .addNode("网络修复", this::networkFixNode) .addNode("账号解锁", this::accountFixNode) .addNode("人工审批", this::approvalNode) .addNode("验证", this::verifyNode) .addNode("通知", this::notifyNode) .addEdge(START, "受理") .addEdge("受理", "分类") .addConditionalEdges("分类", state -> { ... }, Map.of(...)) .addEdge("网络修复", "验证") .addEdge("账号解锁", "验证") .addEdge("人工审批", "验证") .addEdge("验证", "通知") .addEdge("通知", END) .build(); StateSnapshot result = graph.invoke(Map.of( "ticketId", "WO-2025-0312", "message", "CRM系统登录页一直在转圈" ));

调用返回一个StateSnapshot,记录了整趟运行最终状态和节点执行信息。在生产里,我会把这个快照持久化一份,作为流程审计日志。你甚至可以把这个快照关联到工单记录上,后续排查问题时按工单ID直接查全链路,非常方便。

这只是一张顺序加分支的图,已经能覆盖不少企业场景了。如果业务里还要并行检查和等待超时,Spring AI Alibaba Graph也给并行节点这类扩展点,我把真实场景里的并行拆解放到下一节讲。

5. 回到那个让我头疼的场景:故障工单闭环

前面拆得比较抽象,我把真实场景完整摆出来。当时做的是企业服务台故障工单,用户报"内部系统不可用",工单系统会把问题推给这个AI流程。最开始的纯Agent版本有两次比较大的线上事故:一次是模型把"账号锁定"误判成"网络问题",让网络团队白忙半天;另一次是模型在"验证"步骤说"应该没问题了",但用户实际仍然登录不了,问题又被退回客服。这两次之后,没人敢直接信Agent的结论了。

5.1 流程设计稿

于是我们重新设计了流程,按"先确定性分类、再模板化处理、最后强制验证"的节奏来:

步骤节点输入输出
1受理工单内容工单ID、状态=受理中
2智能分类用户描述故障分类:网络/账号/应用/其他
3并行检查工单ID、分类检查结果集合
4分支处理分类+检查结果处理动作与结果
5强制验证工单ID验证是否通过
6通知与归档验证结果通知记录、归档状态

智能分类节点内部调用大模型,但它只输出一个枚举值,不直接做处理动作。这一步把风险隔离在很小的范围里:就算模型分类错了,顶多路由到错误的处理分支,后面还有验证节点兜底。所以我不追求模型的分类准确率百分之百,我更关心流程能不能靠验证步骤兜住分类错误。

5.2 并行节点是怎么用的

检查环节用到了并行执行。三个检查动作彼此独立:网络连通性检查、账号状态检查、应用日志检查。如果用普通顺序节点,一次工单要串行几十秒,用户和客服都等不住。并行节点把它们同时跑,最终汇总成一份检查报告,后面再按结果处理。

并行节点的价值不仅在于快,还在于它会强制你思考节点之间的依赖关系。哪些动作可以同时做,哪些必须等前一步结果,这本身就是一次很好的流程梳理。我见过不少团队写流程时把所有步骤串成一长条,一加并行就懵,原因就是没有认真想清楚数据依赖。Graph的并行能力逼着你去建模"哪些数据是彼此独立的",这个思考过程比框架本身更值钱。

5.3 上线之后实际看到的变化

切到Graph工作流之后,最直观的变化是可观测性。每次调用,我们都能从StateSnapshot里看到它走过了哪几个节点、每个节点耗时、状态字段的变更过程。以前排查一个工单要靠日志里的模型token猜测,现在直接看节点执行记录,五分钟就能定位到是网络修复节点超时,还是验证节点返回false。

流程的稳定性比纯Agent时代明显提升。因为所有分支都是显式定义,我们给每个分支都补了测试,覆盖了"网络故障恢复""账号锁定解锁""验证失败转人工"等典型路径。回归测试从原来的"跟模型猜谜"变成了"断言状态结果",意义完全不一样。模型在流程里退化成一个个被约束的组件,反而发挥了自己的优势——分类、总结、生成通知文案,这些事它干得很好。

6. 踩坑记录:直接照抄会翻车的几个细节

这段我想重点写。Graph工作流框架能帮我们解决很多问题,但踩坑也不少。我把最典型的几个列出来,希望你不用重走一遍。

6.1 共享状态不允许随意覆盖

Graph里的状态是整个流程共享的,所有节点都能读写。团队成员一多,就有人为了图方便,在一个节点里覆盖掉别的节点写过的字段。比如通知节点顺手把"status"改成了"DONE",后面审计节点拿到的状态就被污染了。

我们后来的规范是:每个节点只写自己负责的字段前缀,比如检查节点都写check_开头的key,处理节点都写handle_开头的key;不准跨字段修改。Review的时候盯着这一点,问题少了很多。这跟多线程编程里"不要随便改别人的共享变量"是一个道理,只不过这里共享的是流程状态。

6.2 条件边的目标节点别写错

条件边返回值和目标节点名必须完全一致,否则运行时会出现"找不到下一个节点"的异常。我们曾犯过一个低级错误:条件返回"人工审批",但图上节点名写的是"人工审批处理",排查了半天才发现少打了两个字。建议你把目标节点名定义成常量集合,别到处手写字符串。

另外,条件边的handler最好写成纯函数,不要在里面调外部服务。因为你可能给条件边也加了重试,一旦外部服务超时,路由本身就被拖住了。条件路由应该是基于状态的快速判断,所有需要外部信息的判断提前放到前置节点里去。

6.3 重试要保证幂等

工作流跑起来之后,网络或服务抖动是常态。你可能很自然地给节点加重试策略,但必须注意节点本身要能做幂等:重新执行一遍不能产生副作用。比如"通知用户"节点如果重复执行,用户会被短信轰炸。我们把通知节点设计成先查"是否已通知过",如果通知过了就直接返回已通知,保证重试安全。

这个设计要提前想好,不然后面加班改代码的就是你。写节点之前先问一句"这个节点如果执行两次,会产生什么后果",答不上来就先别写。

6.4 别只依赖整图测试

Graph最好测了,因为每个节点都是普通方法。我会做三层测试:

第一层,单节点测试。直接给状态对象调用节点方法,断言返回状态,不启动Spring容器,跑得飞快。

第二层,子图测试。从某个中间节点作为入口开始跑,只覆盖一条业务路径。这一步能抓住大部分边配置错误。

第三层,整图回归。模拟完整输入走到END,用真实模型做端到端验证。因为涉及模型调用,这条测试慢、贵,不能天天全量跑。

企业项目建议至少把第二层做扎实,多数路由错误都发生在中间子路径上。如果团队里有刚接触Agent的同学,我会建议他从第一层开始写,这样他对节点职责会理解得更深。

6.5 模型输出要限制格式

在Graph里我用模型做分类时,一开始让模型直接输出"网络、账号、应用、其他"这几个词,结果模型偶尔输出"是网络问题"或者"网络/账号",导致路由判断失败。后来把所有分类节点改成让模型输出JSON:{"category": "网络"},再用程序解析这个JSON字段。解析失败就落到默认分支走人工审批,绝不让流程因为格式问题卡死。

这也算一个通用经验:跟模型打交道时,给它的输出加上硬约束,而不是靠提示词软约束。硬约束可能仍然有失败率,但通常很稳定,软约束失败率则是“薛定谔式”的。

7. 一点编排体会

项目上线后,我经常被人问:既然Graph什么都能编排,是不是以后都用Graph,别用Agent了?我不这么看。Graph给的是流程骨架,Agent给的是场景里的智能。一个会写诗、会翻译的Agent放到流程图里反而有可能被约束住;反过来,一个需要走审批、走验收入库的流程,如果完全交给Agent自由发挥,企业是承担不起那种不确定性的。

7.1 我的取舍原则

我现在每次接到需求,先问三个问题。第一,这个任务是不是一次性的单点问答?如果是,ReAct Agent直接上。第二,这个任务是否需要多步骤闭环,有没有明确的成功标准?如果有,必须走工作流。第三,这个流程允许分支和回退吗?如果允许,Graph就是最合适的选择。

在这个项目里,Spring AI Alibaba Graph最大的价值不是性能,也不是什么炫酷效果,而是把AI流程变成了普通Java工程就能维护的东西。节点是普通方法,边是显式声明,状态字段可审计,测试可断言。这一套组合拳下来,模型升级、提示词调整、流程新增分支,都不再是那个让人晚上睡不着觉的"AI黑盒子"。

7.2 给后来者的三个建议

最后给大家三个小建议。

第一,第一版永远先做顺序图,把主线跑通再加条件边和并行节点。我见过太多人上来就想做并行和分支,结果图越画越乱,最后自己都解释不清每一条边为什么存在。流程编排的核心是清晰,不是复杂。

第二,状态字段一定要克制。每加一个字段,都会影响后面所有节点的判断逻辑。字段越少,流程越不容易出错。我们后来砍掉了一半的冗余字段,节点的写权限也收紧了,状态冲突的概率立刻降了下来。

第三,记得把StateSnapshot落库。哪怕你只是简单存成JSON,排查问题时都会省很多力气。这个快照就是AI流程的"操作日志",没有它,出问题的时候你只能靠回忆和猜。

如果你也正在纠结怎么编排企业里的复杂AI流程,我建议你不要在理论上想太久,先拿一个真实的小流程跑起来,哪怕只是"查状态、判断、通知"三步。跑起来之后,你自然能感受到"流程确定性"这四个字的分量。

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

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

立即咨询