ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Spring AI Alibaba Graph:用图工作流让Java Agent流程可控

Spring AI Alibaba Graph:用图工作流让Java Agent流程可控 Spring AI Alibaba Graph 这个项目最近在 Java 开发者做 Agent 的时候越来越常被提到。它解决的核心问题其实很实在Agent 项目不能只是一摞 prompt 加上一堆 function calling还得有清晰的流程控制。Graph Workflow 把任务执行拆成了有状态的图节点负责干活边负责流向条件边负责动态分支整个运行过程可控、可观察、可测试。这篇内容按实际落地顺序拆先讲清楚 Graph Workflow 解决什么问题再给一套可以直接复现的 Spring Boot 小项目接着处理批量任务、SubAgent、记忆和常见排查。适合已经接触过 Spring AI Alibaba或者写过简单 Agent 但觉得流程不好控制的 Java 开发者。如果你只想记住一句话Graph 模式的核心是用“显式流程”换“可控性”同时用“条件边加 LLM 决策”保留灵活性。1. 先确认一件事Graph Workflow 解决的是什么问题1.1 普通 Agent 为什么越写越难控制现在最常见的 Agent 实现方式就是模型循环加工具调用再加多轮消息记忆。任务简单的时候这种写法很舒服userMessage发进去toolCall收回来模型自己决定下一步。但一旦任务出现下面这些情况代码就会迅速失控流程有固定顺序比如先分析、再生成、再审查某一步不通过时需要回到前一步重新处理不同输入需要走不同分支系统要记录每次运行到底经过了哪些节点生产环境不能接受模型每一步都自由发挥。这些问题不是模型能力不够而是执行逻辑没有显式化。你在 prompt 里写再多“如果……那么……”模型也可能不按规则走。更麻烦的是模型一旦走错你从日志里看不出是哪一步出的问题。Graph 的思路是把执行流程作为一等公民。你在创建阶段就把节点和边定义清楚运行阶段框架只负责搬运状态。流程可以被预览、被测试、被替换而不是散落在代码和 prompt 里。1.2 Workflow、Agent、Graph 到底有什么区别这三个概念经常被混着说但实际定位不一样Workflow 是固定流程。节点固定边固定输入走到哪里下一步是什么全部提前定义。Agent 是自由调度。模型根据任务自己决定调用哪些工具、按什么顺序调用。Graph 是中件段允许你画固定的流程骨架也允许某些边根据状态动态选择走向。用一句话概括Workflow 是流程图Agent 是自由调度Graph 是可控的自由调度。Graph 特别适合那些“整体流程稳定个别环节需要模型判断”的场景。比如一个工单系统必须走“分析、生成、审查”三个阶段这是固定的但审查是否通过要不要回去重写可以由模型根据内容决定。固定骨架保底动态边保留灵活度两边都要。1.3 Spring AI Alibaba Graph 的实际定位Spring AI Alibaba 是阿里开源社区维护的 Spring AI 增强项目Graph 模块做的事情可以简单理解为把 LangGraph 那一套图编排思路搬到 Java 生态里。它对 Java 开发者非常友好不需要额外引入 Python 运行时直接在 Spring Boot 项目里定义节点和边模型接入可以走 DashScope 这类阿里云模型服务也可以按 Spring AI 规范配置 OpenAI 兼容接口。这里有一个特别容易误解的点这个 Graph 不是图数据库它不是 Neo4j 那样的东西。它是一套状态执行引擎。你在里面定义的是“流程和状态”不是“实体和关系”。如果你想做知识图谱检索增强那可以单独引入 Neo4j在某个节点内部做查询再把查询结果写入状态。这是两层能力不要混在一起。2. 搭项目前环境和状态模型比功能更重要2.1 运行环境清单先给出一个保守的环境参考。Graph 模块本质上是纯 Java 的状态机引擎对机器配置要求不高真正的资源消耗来自模型调用。我的建议是先在本地用云端模型 API 跑通再考虑部署。组件建议说明JDK17 或更高Spring Boot 3.x 的基础要求Spring Boot3.x与 Spring AI 版本强相关构建工具Maven 3.8Gradle 也可以模型服务DashScope 或 OpenAI 兼容 API需要可用 KeyRedis可选用于会话记忆持久化Neo4j可选知识图谱能力不是 Graph 必需组件Spring AI 目前还比较新版本升级后 API 变化不算少。Graph 模块作为叠加能力更容易跟着版本一起调整。所以落地时第一件事不是写代码而是确认版本组合先跑通官方示例。2.2 引入依赖和基础配置下面这段是通用思路具体坐标以 Spring AI Alibaba 官方发布页为准。实际项目里可能会出现 artifactId 调整这是正常的不要死记硬背。dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-graph/artifactId version使用你确认过的版本号/version /dependency dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-dashscope-spring-boot-starter/artifactId version使用你确认过的版本号/version /dependency模型 Key 不要写在代码里通过环境变量注入。配置文件可以这样spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY}如果你的模型接口是 OpenAI 兼容格式可以换成 openai 相关配置。关键点是 Spring AI 做了模型客户端抽象Graph 节点里拿到的是ChatModel接口不关心具体厂商。2.3 状态类是整个项目的核心很多人刚开始写 Graph会把注意力放在节点上实际上状态类才是最重要的设计。Graph 里的节点之间不直接互相调用所有数据都通过状态对象传递。这意味着状态类就是整个流程的内部数据契约。字段名、字段类型、哪些字段是节点写入的都要在一开始定清楚。我用一个非常常见的工单场景举例状态类可以设计成这样public class AgentState { private String userInput; private String category; private String draft; private String reviewComment; private boolean approved; private int retryCount; public AgentState(String userInput) { this.userInput userInput; this.retryCount 0; } // getter / setter / builder }设计状态类时我一般会注意三点字段归属明确。category是分析节点写的draft是生成节点写的reviewComment是审查节点写的不要多个节点乱写同一个字段。不要什么都塞。不要把完整的多轮对话历史、原始请求体、模型内部消息全部塞进去状态对象会越来越大。批量运行时每次都要 new 独立状态。多个任务如果共用同一个状态对象数据会串。注意状态类不要使用全局单例。Graph 本身可能被多线程调用状态对象必须是每次调用独立的。3. 从零写一个带审查环节的工单 Agent 流程3.1 场景和目标我拿一个“客户工单自动处理”的小项目来拆。任务是这样输入一段客户问题比如“我的订单三天没发货了能不能退款”分析节点判断问题类型生成节点根据问题类型写处理建议审查节点检查建议是否合理如果审查不通过回到生成节点重写最多重试两次超过重试次数输出兜底结果。这个场景很适合第一次跑 Graph。它同时覆盖了顺序边、条件边、循环和退出条件这些都是 Graph 里最常用的元素。3.2 节点写法返回变化的部分Graph 节点的典型接口是输入当前状态返回一个字段集合框架把字段合并回状态。public class AnalyzeNode implements NodeAgentState { private final ChatModel chatModel; public AnalyzeNode(ChatModel chatModel) { this.chatModel chatModel; } Override public MapString, Object apply(AgentState state) { String category classify(state.getUserInput()); return Map.of(category, category); } private String classify(String input) { // 可以先写规则也可以接模型 if (input.contains(退款) || input.contains(退货)) { return after_sale; } if (input.contains(发货) || input.contains(物流)) { return logistics; } return other; } }我建议节点里不要把所有逻辑都堆在一个类里。先写成小方法后续要替换成模型调用时只动内部实现接口保持不变。节点内部不一定非要调用模型。规则、计算、查询数据库、调用外部接口都可以做成节点。这是很多人容易忽略的点Graph 不是一个“所有环节必须用大模型”的框架它只是一个流程编排容器。3.3 生成和审查节点生成节点最简单的写法就是把分类结果和用户原问题拼进 prompt让模型输出处理建议public class DraftNode implements NodeAgentState { private final ChatModel chatModel; Override public MapString, Object apply(AgentState state) { String prompt 你是客服主管。工单内容 state.getUserInput() 分类 state.getCategory() 。请给出处理建议。; String draft chatModel.call(prompt); return Map.of(draft, draft); } }审查节点可以更务实一点先做基本规则判断比如长度是否小于 20 字、是否包含明确处理动作如果规则判断不了再让模型判断。public class ReviewNode implements NodeAgentState { private final ChatModel chatModel; Override public MapString, Object apply(AgentState state) { if (state.getDraft() null || state.getDraft().length() 20) { return Map.of(approved, false, reviewComment, 建议内容太短); } return Map.of(approved, true, reviewComment, 通过); } }这里故意用规则做审查是为了说明一个观点能用规则就用规则只有需要开放语义理解时才调用模型。这样流程更快、更省钱也更容易测试。3.4 条件边返回字符串映射到目标节点条件边是 Graph 动态能力的核心。它的写法通常是根据当前状态判断返回一个字符串框架再根据这个字符串找到下一步节点。public class ReviewRouting implements EdgeAgentState { Override public String route(AgentState state) { if (state.isApproved()) { return END; } if (state.getRetryCount() 2) { return fallback; } return rewrite; } }注意这里返回的是字符串而不是直接返回节点对象。好处是图结构在构建时是显式的你可以在创建阶段看到所有可能路径方便测试和审计。3.5 构建图和第一次运行构建图的代码逻辑上长这样StateGraphAgentState graph new StateGraph.BuilderAgentState() .addNode(analyze, new AnalyzeNode(chatModel)) .addNode(draft, new DraftNode(chatModel)) .addNode(review, new ReviewNode(chatModel)) .addNode(rewrite, new RewriteNode(chatModel)) .addNode(fallback, new FallbackNode(chatModel)) .addEdge(analyze, draft) .addEdge(draft, review) .addConditionalEdge(review, new ReviewRouting(), Map.of(END, END, rewrite, rewrite, fallback, fallback)) .addEdge(rewrite, review) .build(); AgentState initialState new AgentState(我的订单三天没发货了能不能退款); AgentState result graph.invoke(initialState); System.out.println(result.getDraft());这段代码是示例风格实际 API 名称可能随版本调整但结构思路是一致的。你重点看三件事顺序边把 analyze 指向 draft把 draft 指向 review。条件边从 review 出发根据返回值去三个不同的地方。rewrite 指向 review形成循环但循环有上限。第一次跑通过的标准很简单一条工单能走完结果里有处理建议retryCount符合预期。如果 review 一直不通过最多跑三次就会进入 fallback。4. 把模型决策真正嵌进图里结构化输出、超时和记忆4.1 节点里哪些地方应该用模型很多人第一次用 Graph会忍不住让每个节点都调一次模型。这不是不行但成本和调试难度会直线上升。我的判断标准是文本生成类任务比如写回复、写摘要、写代码用模型分类、判断、抽取类任务如果枚举值固定先尝试规则和 JSON 解析状态维护、分支选择、字段赋值永远不要用模型直接决定结构。比如前面的分类节点如果只是判断“售后、物流、其他”用 contains 就可以了。只有当客户问题表达太开放、规则覆盖不住时再引入模型。4.2 条件边尽可能使用结构化输出条件边里如果需要模型参与不要让模型返回“我觉得可以”这种自然语言再靠代码去 contains 判断。正确的做法是让模型输出固定 JSON{ approved: false, reason: 建议中没有提到退款时效 }然后节点解析 JSON取approved字段写入状态。这样条件边代码不用改只是判断依据从规则变成了解析结果。如果模型输出不稳定可以在 prompt 里给一个非常具体的 JSON 示例并说明只允许输出 JSON不要输出解释。4.3 超时、重试和异常兜底模型接口调用可能失败可能超时这是 Agent 项目里最常见的生产问题。Graph 本身解决不了这个问题但你的图结构可以提前预留处理路径。我建议在每个节点内部做 try-catch错误信息写入状态而不是直接抛出异常让整个流程中断。Override public MapString, Object apply(AgentState state) { try { String result chatModel.call(buildPrompt(state)); return Map.of(draft, result, lastError, ); } catch (Exception e) { return Map.of(draft, , lastError, e.getMessage()); } }然后在后续节点或条件边里判断lastError。如果是空字符串说明正常如果不为空进入兜底节点。这样整个流程不会因为一次模型调用失败就完全不可用。注意节点内部不要吞掉异常后继续。应该把错误信息写进状态由流程决定下一步。否则日志里看不到任何异常但输出就是不对。4.4 记忆到底存在哪里Graph 里的记忆本质上也是状态。最简单的记忆就是状态类里的一个ListString conversationHistory。每个节点往里追加消息生成节点拼 prompt 时把历史带进去。如果要做多轮会话并且会话之间需要隔离可以引入 Redis。流程大概是请求进来时从 Redis 读取历史还原成状态对象图跑完后再把状态对象写回 Rediskey 用会话 ID。这里有一个工程取舍记忆越多prompt 越长模型调用成本越高。不要无限保留历史建议做裁剪或摘要。比如超过十轮就把前面的对话摘要成一段话。5. 从 Demo 走向交付批量任务、SubAgent 和运行痕迹5.1 批量任务前必须做的三件事很多人跑通单条后马上就想开并发。我一般会先压住这个冲动。批量任务有三个前置工作第一输入必须有唯一 ID。这样即使某条失败也能定位到具体工单。第二每次运行都要 new 一个独立状态。状态对象不是线程安全的绝对不要复用同一个实例去跑不同任务。第三结果要有回执。成功、失败、重试次数、最终输出都要落到日志或数据库。否则批量跑了一百条你不知道哪些成功哪些失败。最简单的批量循环长这样for (WorkOrder order : orders) { AgentState state new AgentState(order.getContent()); try { AgentState result graph.invoke(state); saveResult(order.getId(), result.getDraft(), result.getRetryCount()); } catch (Exception e) { saveError(order.getId(), e.getMessage()); } }先串行跑 10 条看耗时和成功率。确认稳定后再用线程池控制并发。并发时还要注意模型 API 的限流。不是并发越高越好模型服务端可能限制每分钟请求数超过之后反而大量报错。5.2 主从模式把 SubAgent 当成 Tool 调用这是多 Agent 设计里很常见的主从模式。简单说就是主流程图里有一个节点这个节点内部又运行了一张子图。从主图的角度看子图就是一个工具。比如你要做一个“技术方案助手”主流程是理解需求、生成方案、审查方案。其中“生成方案”这个环节很复杂内部又有调研、代码生成、测试三个子步骤。这时候没必要把这三个子步骤全部展开到主图里把它们封装成一张子图在主图的GenerateNode里调用。public class GenerateNode implements NodeAgentState { private final StateGraphSubState subGraph; Override public MapString, Object apply(AgentState state) { SubState subState new SubState(state.getUserInput()); SubState result subGraph.invoke(subState); return Map.of(draft, result.getAnswer()); } }这样做的价值很清楚子图可以单独测试可以复用主图结构不会膨胀。风险是子图运行时间不可控外层必须加超时和失败兜底。主从模式不是银弹。如果你的主图只有两三个节点完全没必要拆子图。子图适合那些真正独立的子流程。5.3 并行分支和结果汇聚有些任务需要“同时做两件事”。比如审核一个产品评论既要判断情感又要抽取关键词最后把结果拼在一起。图编排框架可能提供扇出扇入的能力但如果你用的版本不方便也可以在一个节点内部用线程池并行处理。ExecutorService pool Executors.newFixedThreadPool(2); FutureString sentimentFuture pool.submit(() - analyzeSentiment(text)); FutureString keywordFuture pool.submit(() - extractKeywords(text)); String sentiment sentimentFuture.get(30, TimeUnit.SECONDS); String keywords keywordFuture.get(30, TimeUnit.SECONDS);注意一个原则不要在多个线程里直接修改同一个状态对象。先把并行结果放进局部变量最后通过返回的 Map 合并到状态里。5.4 运行痕迹和日志Graph 调试最大的难点是状态太多不知道哪一步改了什么。如果只在最后打印结果中间出了问题很难定位。我习惯在每一步之后记录状态快照至少包含节点名、关键字段、耗时log.info(node{}, category{}, retryCount{}, approved{}, draft, state.getCategory(), state.getRetryCount(), state.isApproved());生产环境可以把这些运行记录写到数据库之后可以通过工单 ID 回溯整个流程。这不是额外工作这是上线前必须准备的基础设施。6. 实战中最容易踩的坑按这个顺序排查6.1 节点不执行或者走到了错误分支先看节点名。很多是名称不一致导致的比如构建图时写的是analyze条件边里返回的是analysis。大小写、空格、下划线都要一致。再看条件边的返回值映射。route方法返回的字符串必须能在构建边的Map里找到对应节点。如果有分支没有配置流程会直接报错。最后看条件边内部有没有抛异常。异常如果被吞掉状态一直停留在一个默认值分支就会一直走同一条路。6.2 输出为空但流程没有报错这种情况很常见。先从代码里找源头节点返回的 Map 字段名是否和状态类里的字段名完全一致。少写一个字母、大小写不一致状态就更新不进去。然后看模型输出解析。如果模型返回了 JSON但解析失败不要静默地返回 null。解析失败应该抛异常或者返回错误标记让流程进入兜底节点。最后看 Spring 注入。节点如果是 Spring Bean注意不要用成员变量保存某个请求的临时数据。多个请求共用同一个 Bean成员变量会发生覆盖。6.3 循环不退出这是条件边最容易出问题的地方。检查三件事所有可能的返回值都有对应的边映射retryCount是在重写节点内自增的而不是在审查节点里条件边里最终能返回END或fallback。如果还不放心可以在状态里加一个stepCount每次节点执行时自增。超过最大步数后无条件走向兜底节点。这是防止悬挂循环的最后一道保险。6.4 性能和资源问题Graph 本身不会消耗太多资源真正吃资源的通常是模型并发和状态对象。状态对象不要无限膨胀。比如一直往conversationHistory里追加消息每个节点都带着完整历史跑内存和 token 成本都会上升。定期裁剪历史或者用摘要替换旧历史。还有线程池问题。节点里创建的线程池一定要记得关闭或者从外部传入共享线程池。否则重复调用几百次后线程数会失控。6.5 版本兼容性问题Spring AI 版本更新很快Graph 模块的 API 经常跟着变化。很多人把旧版本的示例代码复制到新项目里编译都过不去。我建议的排查顺序是先确认 Spring Boot 版本与 Spring AI 版本是否匹配再确认 Graph 模块版本与 Spring AI 版本是否匹配去官方示例仓库找相同版本的项目如果官方示例也跑不起来看 changelog 或 issue而不是翻旧教程。版本问题没有捷径只有锁定版本组合这一条路。7. 落地建议固定流程托底再放开 Agent 决策7.1 分三个阶段推进第一次用 Graph 做项目不建议一步到位设计一个大型多 Agent 系统。我建议按三个阶段走阶段一固定 Workflow。所有节点和边完全固定模型只在某个环节里做文本生成。这个阶段的目标是把业务流程跑通输出稳定。阶段二加入条件边。把“是否通过”“下一步走哪个分支”交给模型决策但分支集合是固定的。这一阶段开始体现 Graph 的灵活性。阶段三加入 SubAgent。把独立的子图封装成工具主流程根据业务需要选择调用。这一步之后才算真正可控和灵活兼备。三个阶段各有侧重阶段可控性灵活性适合场景调试成本固定 Workflow高低稳定业务流、内部系统低条件边 LLM中中需要判断分支的任务中子图 Agent中高多步骤、多能力组合高7.2 上线前过一遍验收清单这里列一份我自己的检查清单每次交付前都会过一遍单条输入能否稳定跑通状态字段读写是否一致模型超时后能否走兜底条件边所有返回值是否都有映射批量输入是否都有唯一 ID并行任务下状态对象是否隔离关键节点是否输出日志每次运行能否通过 ID 回溯完整流程。任何一项不满足我都不会认为这个 Agent 可以上线。Graph 项目不是写出来能跑就行关键在可维护和可排查。7.3 什么情况不需要 GraphGraph 不是所有场景的答案。如果只是单轮问答或者简单的工具调用直接写 service 更直观。如果流程非常固定没有分支没有循环没有审查那普通代码比图编排更清晰。Graph 的使用价值在于流程复杂到一定程度普通代码已经难以维护而你仍然希望流程可控。不要为了用新技术而引入状态图它是有学习成本和维护成本的。我个人比较推荐的起步方式是挑一个真实业务里稳定、有分支、需要重试的流程用 Graph 重写一遍。不要一上来就设计一个巨大的多 Agent 平台。真正落地时最该盯住的不是节点写得有多花哨而是状态是否干净、条件边是否完整、异常路径是否有人处理。踩过几次之后你会发现很多问题不是编排框架能力不够而是对流程本身的理解还不够清楚。
返回列表