开发 Agent 项目时,团队往往会分成两派:一边是 Workflow 派,把流程用代码写死,稳定可靠但缺乏灵活性;另一边是纯 Agent 派,让大模型自由决定调用哪些工具,灵活聪明但难以控制和定位问题。Spring AI Alibaba 在 1.x 系列中把 Graph Workflow 作为一等公民引入,用图结构来编排节点,让流程在“可控”与“灵活”之间找到平衡点。本文会从概念讲起,用一个完整的“客服工单 Agent”实战项目,演示如何在 Spring Boot 中搭建 Spring AI Alibaba Graph Workflow,并加入条件路由、工具调用、人工升级分支,帮助你掌握可落地的 Agent 项目开发方式。
1. 背景:为什么 Agent 开发需要 Graph Workflow
1.1 纯 Workflow 与纯 Agent 的取舍
先看一个常见的业务场景:用户问“我想查一下订单 12345 现在到哪了”。
如果使用传统 Workflow,流程是确定的:先解析参数,再查数据库,最后套用模板返回。这种方案的好处是每个环节都可控、可复现、容易测试,但缺点也很明显:用户一旦换一种问法,比如“我昨天买的那个东西什么时候能到”,传统规则就很难匹配,需要不断堆关键词和分支。
如果使用纯 Agent 方案,把问题交给大模型,让它自己决定调用什么工具、按什么顺序调用,确实能应对更多自然语言变化。但代价是流程不再透明,用户可能被引导到错误分支,工具调用也可能发散,排查成本很高。生产环境里,这种“不可控”是团队很难接受的。
Graph Workflow 正是为了平衡这两者出现的。它把整个交互拆成一个个节点,用边来定义节点之间的流转关系,让开发者既能精确控制关键路径,又能留出“让模型判断分支”的灵活性。
1.2 Spring AI Alibaba Graph 是什么
Spring AI Alibaba 是阿里云将 Spring AI 引入 Java 生态后的企业级扩展项目。在 1.x 系列中,它除了提供 DashScope、通义千问等模型接入能力,还提供了 graph 模块,专门用于 Agent 项目的编排开发。
graph 模块借鉴了 LangGraph 的设计思路,核心思想是把 Agent 的一次运行过程建模为一张有向图。图上的每个节点负责一项独立任务,比如意图识别、工具调用、答案生成;节点与节点之间通过边连接,连接条件可以由代码固定,也可以由大模型动态决定。整体有点像一个“可编程状态机”,只不过状态里承载的是对话上下文和工具结果。
需要注意的是,这里的 Graph 指编排图,而不是图数据库。它不负责存储“用户和订单之间的关系”,而是负责描述“一次 Agent 请求从进入到结束经过哪些步骤”。这个区分很重要,很多新手容易把 Graph Workflow 和 Neo4j 这类图数据库搞混。
1.3 Graph Workflow 能解决什么问题
从工程落地角度看,Graph Workflow 带来的收益主要体现在三方面。
第一是可观测性。每个节点执行前后都能记录状态,可以很方便地打印日志、统计耗时、追踪是哪一步出了问题。相比纯 Agent 的“黑盒调用”,图的执行路径是静态可见的。
第二是可复用性。一个节点封装一个能力,比如“查询订单”“转人工”“生成回复”,这些节点可以在不同 Agent 之间复用,甚至可以组合成更大的图。项目规模变大后,维护成本比一个大循环里堆工具的方式低很多。
第三是可控性。你可以在关键路径上使用条件节点,把路由逻辑掌握在自己手里;也可以把一部分分支交给大模型判断,比如由模型决定是否需要调用工具。这种“关键节点可控、语义理解交给模型”的混合模式,正是生产级 Agent 需要的。
2. 核心概念:Node、Edge、StateGraph 与条件路由
2.1 三个核心抽象
在 Spring AI Alibaba Graph 中,最核心的三个抽象是 Node(节点)、Edge(边)和 StateGraph(状态图)。
Node 是执行单元。一个 Node 接收当前状态,执行一段逻辑,再返回更新后的状态。节点内部可以是普通 Java 代码,比如查询数据库;也可以调用大模型,比如判断用户意图。节点之间互相不知道对方存在,只通过状态传递数据。
Edge 描述节点之间的流转关系。简单边表示“上一个执行完进入下一个”,条件边则表示“根据当前状态决定进入哪个节点”。条件边是 Graph Workflow 灵活性的关键,它允许一个节点拥有多个出口,运行到这一步时再决定走哪条路。
StateGraph 是整张图的容器。它负责注册节点、编排边、控制入口和终点,最后通过 compile() 编译成可以执行的图对象。执行时,StateGraph 会从起始节点开始,按照边的关系依次执行,直到到达 END 节点。
2.2 状态是如何流转的
图中的状态通常是一个 Map 结构,键是字段名,值是字段内容。例如:
{ "question": "我想查一下订单12345现在到哪了", "intent": "ORDER", "orderInfo": "订单【12345】已签收" }每个节点从状态中读取自己需要的字段,把计算结果写入状态,再返回给图框架。图框架负责把更新后的状态传递给下一个节点。这种设计让节点之间形成了解耦:意图识别节点不关心订单查询节点怎么实现,订单查询节点也不关心答案生成节点怎么处理结果。
需要特别注意一点:节点执行时最好在原有状态对象上写入字段,而不是返回一个全新的对象。如果节点返回新对象,旧对象里的字段可能会丢失,导致后续节点读不到上下文。这个问题在真实项目中非常常见,后面会在排错部分专门展开。
2.3 Agent、Workflow、Graph 的关系
用一句话概括:Graph 是 Workflow 的超集,Agent 是 Graph 上的一种特殊运行方式。
传统 Workflow 是预先定义好的固定 DAG(有向无环图),每个节点的执行顺序在编译时就确定了。Agent 模式则可以理解为图中有更多条件边和循环边,甚至节点的路由由大模型按当前状态实时决策。Spring AI Alibaba Graph 允许你在同一张图里混用两种模式:有的分支完全固定,有的分支交给模型判断。这正是“可控 + 灵活兼备”的落地点。
从团队协作角度看也是这样。资深开发者可以把流程骨架定下来,写死关键的约束边;业务同学或算法同学则可以在允许的范围内调整条件节点的判断逻辑。图的表达能力比传统流程引擎更强,又比纯 Agent 更容易维护。
3. 环境准备与依赖配置
3.1 版本环境说明
开始动手前,先交代本文使用的技术环境。由于 Spring AI Alibaba 仍处于快速迭代阶段,具体版本号请以你的项目实际使用的版本为准,本文重点演示配置思路和代码结构。
- JDK:17 及以上,推荐 21。
- Spring Boot:3.4.x 或 3.5.x,需要与 Spring AI Alibaba 1.x 的基线版本对齐。
- Spring AI Alibaba:1.x 系列,本文以 graph 模块为例。
- 构建工具:Maven 3.6+。
- 大模型:阿里云百炼 DashScope 通义千问,也可以按 OpenAI 兼容方式接入 DeepSeek。
如果你的项目还没有确定版本,建议直接使用 Spring Initializr 创建一个 Spring Boot 3.4+ 项目,再通过 BOM 引入 Spring AI Alibaba 的版本管理。这样能减少依赖版本冲突的概率。
3.2 创建项目并引入依赖
这里以 Maven 为例。第一步,在 pom.xml 中加入 spring-ai-alibaba-bom,统一管理相关依赖版本:
<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-bom</artifactId> <version>你的SpringAIAlibaba版本</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>第二步,引入 DashScope 模型接入 starter 和 graph 模块:
<dependencies> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-graph</artifactId> </dependency> </dependencies>如果你没有使用 BOM,就需要为每个依赖单独写上 version 标签。实际开发中我建议保留 BOM,因为 Spring AI Alibaba 的多个模块之间存在版本依赖关系,BOM 能避免大量版本对齐问题。
3.3 配置大模型接入
在 application.yml 中配置 DashScope API Key 和模型名:
spring: application: name: agent-graph-demo ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus其中 DASHSCOPE_API_KEY 放在环境变量里,不要直接把密钥写死在配置文件中。qwen-plus 是通义千问的中等规格模型,适合意图识别和答案生成;如果你需要更强推理能力,可以换成 qwen-max。
如果你使用的是 DeepSeek,也可以用 OpenAI 兼容方式接入,配置如下:
spring: ai: openai: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat这里有一个常见坑:base-url 不要写多余的 /v1 路径,否则部分版本会出现请求地址拼接错误,导致接口无法访问。
4. 实战:构建一个客服工单 Agent
4.1 业务场景与流程设计
现在进入核心实战。我们构建一个客服工单 Agent,用户输入一句自然语言,Agent 需要完成三件事:识别用户意图、查询订单信息、生成最终回复。如果用户有投诉或退款诉求,则转人工处理。
整体流程设计如下:
用户输入 -> 意图识别节点 -> 条件路由(根据 intent 字段判断) -> order:查询订单节点 -> 生成回复节点 -> 输出 -> human:人工升级节点 -> 输出 -> chat:直接生成回复节点 -> 输出这个设计里,意图识别节点和条件节点展示了“灵活性”,让模型理解用户的语言;订单查询节点和人工升级节点则展示了“可控性”,关键业务分支由代码固定。
4.2 项目结构
在 src/main/java/com/example/agent 下创建如下目录结构:
agent/ ├── AgentApplication.java ├── controller/ │ └── AgentController.java ├── service/ │ └── AgentService.java ├── graph/ │ ├── IntentNode.java │ ├── OrderQueryNode.java │ ├── HumanHandoverNode.java │ ├── AnswerNode.java │ └── IntentRouter.java ├── tool/ │ └── OrderTool.java └── config/ └── AgentGraphConfig.java每个类职责单一,方便后续扩展。节点类只关注自己的逻辑,路由条件单独拆成一个类,图组装放在配置类中,入口通过 Service 暴露给 Controller。
4.3 定义图状态与工具
图状态使用 Map<String, Object> 承载。为了方便统一管理字段名,我建议定义一个常量类或直接使用工具类中的常量。为了演示简洁,这里直接在节点里使用字符串 key。
接下来定义订单查询工具类。实际项目中这里会调用订单服务或数据库,本文用一个简单方法模拟:
// 文件路径:src/main/java/com/example/agent/tool/OrderTool.java @Component public class OrderTool { public String queryOrderStatus(String orderId) { if ("12345".equals(orderId)) { return "订单【12345】已签收,签收时间为2026-01-10 14:30"; } return "订单【" + orderId + "】查询不到,请确认订单号后重试"; } public String extractOrderId(String question) { if (question != null && question.contains("12345")) { return "12345"; } return "未知"; } }这里刻意没有让大模型自由决定调用哪个工具,而是由节点里的 Java 代码显式调用工具。原因是订单号抽取和订单查询属于强逻辑步骤,交给代码执行更稳。
4.4 实现节点类
第一个节点是意图识别节点,它使用 ChatClient 调用大模型,把模型返回的意图编码写入状态:
// 文件路径:src/main/java/com/example/agent/graph/IntentNode.java public class IntentNode implements Node<Map<String, Object>> { private final ChatClient chatClient; public IntentNode(ChatClient chatClient) { this.chatClient = chatClient; } @Override public Map<String, Object> execute(Map<String, Object> state) { String question = String.valueOf(state.get("question")); String prompt = """ 你是客服系统的意图识别器,请判断用户问题的意图。 可能的意图: - ORDER:查询订单、物流信息 - HUMAN:投诉、退款、需要人工介入 - CHAT:其他闲聊 只返回意图编码,不要输出额外文字。 """; String intent = chatClient.prompt() .system(prompt) .user(question) .call() .content(); state.put("intent", intent == null ? "CHAT" : intent.trim()); return state; } }注意,Node 接口的具体包名和泛型签名在不同版本中可能有差异,请以你当前依赖的源码为准。核心逻辑是:从状态读取 question,调用大模型得到 intent,再写回状态。
第二个节点是订单查询节点。它读取 question,从中抽取订单号,调用 OrderTool 查询,把结果写入 orderInfo 字段:
// 文件路径:src/main/java/com/example/agent/graph/OrderQueryNode.java public class OrderQueryNode implements Node<Map<String, Object>> { private final OrderTool orderTool; public OrderQueryNode(OrderTool orderTool) { this.orderTool = orderTool; } @Override public Map<String, Object> execute(Map<String, Object> state) { String question = String.valueOf(state.get("question")); String orderId = orderTool.extractOrderId(question); String orderInfo = orderTool.queryOrderStatus(orderId); state.put("orderInfo", orderInfo); return state; } }第三个节点是人工升级节点。它不调用大模型,直接生成一段固定提示文案,保证投诉和退款场景不会被模型随意发挥:
// 文件路径:src/main/java/com/example/agent/graph/HumanHandoverNode.java public class HumanHandoverNode implements Node<Map<String, Object>> { @Override public Map<String, Object> execute(Map<String, Object> state) { state.put("answer", "已为您转接人工客服,工单号 20260101,请保持电话畅通。"); return state; } }第四个节点是答案生成节点。它会结合查询结果生成一段自然、友好的回复:
// 文件路径:src/main/java/com/example/agent/graph/AnswerNode.java public class AnswerNode implements Node<Map<String, Object>> { private final ChatClient chatClient; public AnswerNode(ChatClient chatClient) { this.chatClient = chatClient; } @Override public Map<String, Object> execute(Map<String, Object> state) { String orderInfo = String.valueOf(state.get("orderInfo")); String answer = chatClient.prompt() .system("你是一个客服助手,请基于查询结果生成友好、简洁的回复。") .user("订单查询结果:" + orderInfo) .call() .content(); state.put("answer", answer); return state; } }这里可以看到 Graph 的好处:答案生成节点并不关心订单信息是来自数据库、外部接口还是缓存,它只负责“把已有信息变成用户能读懂的回复”。后续如果接入更多工具,这个节点完全不需要改动。
4.5 组装 StateGraph
节点和边需要组装成图。条件路由单独提出来,逻辑更清晰:
// 文件路径:src/main/java/com/example/agent/graph/IntentRouter.java public class IntentRouter { public String route(Map<String, Object> state) { String intent = String.valueOf(state.get("intent")); if ("ORDER".equals(intent)) { return "order"; } if ("HUMAN".equals(intent)) { return "human"; } return "chat"; } }然后,在配置类中完成图组装:
// 文件路径:src/main/java/com/example/agent/config/AgentGraphConfig.java @Configuration public class AgentGraphConfig { @Bean public Graph<Map<String, Object>> agentGraph(ChatClient chatClient, OrderTool orderTool) { IntentNode intentNode = new IntentNode(chatClient); OrderQueryNode orderNode = new OrderQueryNode(orderTool); HumanHandoverNode humanNode = new HumanHandoverNode(); AnswerNode answerNode = new AnswerNode(chatClient); StateGraph<Map<String, Object>> graph = new StateGraph<>(); graph.addNode("intent", intentNode); graph.addNode("order", orderNode); graph.addNode("human", humanNode); graph.addNode("answer", answerNode); graph.addEdge("START", "intent"); graph.addConditionalEdge("intent", state -> { String intent = String.valueOf(state.get("intent")); if ("ORDER".equals(intent)) return "order"; if ("HUMAN".equals(intent)) return "human"; return "answer"; }); graph.addEdge("order", "answer"); graph.addEdge("human", "END"); graph.addEdge("answer", "END"); return graph.compile(); } }如果是 IDE 自动导包,需要注意 StateGraph 和 Graph 两个类不要导入错误。不同版本的 addConditionalEdge 签名可能有差异,如果编译报错,优先查看当前版本的 StateGraph 源码,确认是传 Map 还是传 Function。
4.6 编写入口 Service 与 Controller
Service 负责创建初始状态、调用图执行、返回结果:
// 文件路径:src/main/java/com/example/agent/service/AgentService.java @Service public class AgentService { private final Graph<Map<String, Object>> graph; public AgentService(Graph<Map<String, Object>> graph) { this.graph = graph; } public String chat(String question) { Map<String, Object> state = new HashMap<>(); state.put("question", question); Map<String, Object> result = graph.invoke(state); return String.valueOf(result.get("answer")); } }Controller 提供 HTTP 接口:
// 文件路径:src/main/java/com/example/agent/controller/AgentController.java @RestController public class AgentController { private final AgentService agentService; public AgentController(AgentService agentService) { this.agentService = agentService; } @PostMapping("/chat") public Map<String, String> chat(@RequestBody Map<String, String> request) { String answer = agentService.chat(request.get("question")); return Map.of("answer", answer); } }4.7 运行与验证
启动 Spring Boot 项目后,用 curl 模拟用户提问:
curl -X POST http://localhost:8080/chat \ -H "Content-Type: application/json" \ -d '{"question": "我想查一下订单12345现在到哪了"}'预期返回类似:
{ "answer": "您的订单【12345】已签收,签收时间为2026-01-10 14:30。如果您还有其他问题,可以随时告诉我。" }再测试一个转人工场景:
curl -X POST http://localhost:8080/chat \ -H "Content-Type: application/json" \ -d '{"question": "我要投诉,你们平台退款太慢了!"}'预期返回:
{ "answer": "已为您转接人工客服,工单号 20260101,请保持电话畅通。" }从两个请求可以看出,同一个入口,图会根据模型识别的意图走进不同分支,这就是条件路由的效果。你可以在日志中打印每个节点执行前后的状态,进一步确认流转路径。
5. 常见问题与排查思路
5.1 Spring AI 连接 DeepSeek 不输出 Content
这是 Spring AI 用户群里经常出现的问题。现象是调用 ChatClient 后返回对象不为空,但 content() 结果为 null 或空字符串。
常见原因有三个。第一,模型名配置错误,DeepSeek 的模型名应该是 deepseek-chat,如果配置成其他名称,返回结构可能不符合 Spring AI 的解析预期。第二,base-url 配置带了多余的 /v1 路径,导致请求地址错误,实际请求打到了不存在的接口。第三,用了流式调用但代码按非流式方式解析,或者响应中的字段名与 Spring AI 预期不同。
排查时可以按以下顺序处理。先看请求日志,确认请求 URL 和模型名;再打印原始响应体,观察 content 字段是否为 null;最后检查配置里的 base-url,确保结尾干净。最简单的方式是用 curl 直接请求 DeepSeek API,确认接口本身在返回标准的 chat.completion 结构。
另外一个容易被忽略的点是:DeepSeek 的 reasoning 模型会返回 reasoning_content 字段,而普通 content 字段可能为空。如果使用的是 deepseek-reasoner 模型,需要确认 Spring AI 是否支持解析该字段,否则会因为读不到 content 而返回空结果。
5.2 版本兼容问题
Spring AI Alibaba 1.x 系列基于 Spring AI 1.0,而 Spring AI 1.0 对 Spring Boot 版本有明显要求。如果看到 NoSuchMethodError、ClassNotFoundException 这类错误,大概率是 Spring Boot 或 Spring AI 版本不匹配。
建议从三方面排查:第一,确认 Spring Boot 版本在 Spring AI 支持矩阵内;第二,尽量使用 spring-ai-alibaba-bom 统一管理依赖版本;第三,检查是否同时引入了多个模型 starter,比如既引入 dashscope 又引入 openai,会造成 ChatClient 自动装配冲突。
如果升级版本后出现 API 不兼容,可以先看官方 release notes,再搜索当前版本的迁移文档。Spring AI 生态迭代很快,从 0.x 升级到 1.x,或者 1.x 内的小版本升级,都可能改包名或方法签名。
5.3 图执行无法结束或状态丢失
图执行不结束,通常是条件路由出现了循环且没有到达 END 的边。排查时可以在每个节点入口打印当前节点名和状态快照,通过日志确认实际流转路径。只要有一个节点返回了无法匹配任何条件边的结果,就可能走到错误的出口。
状态丢失则多半是节点返回了新 Map,而不是在原有 Map 上写入。解决办法是约定所有节点只修改入参 state,不要创建新的 HashMap 后把结果返回。如果有节点确实需要清理字段,可以考虑在返回前从原 Map 中 remove 掉对应 key。
还有一种情况是 execute 方法返回值被框架忽略。不同版本的 Spring AI Alibaba Graph 对节点返回值的处理可能有差异,建议先确认框架是基于返回值覆盖状态,还是基于对象引用原地生效。确认后再统一团队的节点编写风格。
5.4 工具调用比预期多
纯 Agent 模式下,大模型可能在一个回答中多次调用同一个工具,造成不必要的资源浪费。比如用户只是随口问一句“订单在哪”,模型却查询了三遍。这不是模型不聪明,而是自由调用模式下缺少约束。
在 Graph Workflow 中解决这个问题有几种方式。第一,把工具调用放在固定节点中,由 Java 代码显式调用一次;第二,在系统提示词中明确“查询一次即可,不要重复查询”;第三,使用状态中的 mark 字段记录本次会话已经调用过哪些工具,出现重复时直接返回已有结果。
从可控性角度看,最推荐第一种方式。让模型负责理解语义,让代码负责关键动作,这样既保留自然语言交互的灵活性,又避免模型在不该决策的地方瞎决策。
6. 工程化与最佳实践
6.1 节点设计原则
节点是 Graph 的基本单元,设计好坏直接影响整个项目的可维护性。我建议遵循单一职责原则:一个节点只做一件事。比如“查询订单”和“生成回复”必须拆成两个节点,不能图省事揉在一起。
如果多个节点都需要访问同一个外部服务,建议把外部服务封装成独立的 Component,节点只负责调用。这样外部服务变更时,不需要修改每个节点,只需要修改对应的服务实现。
对于需要大模型判断的节点,系统提示词要写得足够具体。不要让模型自己猜测输出格式,而是明确指定“只返回编码”“必须 JSON”等约束。输出不符合预期时,可以在节点中加一层简单的正则或 JSON 解析,失败时走兜底分支。
6.2 状态管理与命名规范
状态字段是节点之间的“协议”,命名必须一致且有含义。建议 all state key 使用大写下划线风格,例如 ORDER_INFO、HUMAN_TICKET_ID,与模型输出的字段名区分开。
状态中不要塞入不必要的对象。有些团队喜欢把整个 HTTP Request 或数据库连接对象放入状态,这会让状态变量变得不可序列化,也不便于日志打印。状态里尽量只放基础类型、字符串、JSON 字符串或轻量 DTO。
另外,涉及敏感信息时,比如用户手机号、身份证号,不要把原始值写入状态后在日志中打印。可以先脱敏再放入状态,或者在节点内部使用独立对象持有数据,只把摘要写入图状态。
6.3 可观测性与超时控制
Graph 应用上线后,最大的问题是“不知道模型这次为什么走了这条路”。所以日志记录比普通 Web 应用更重要。建议在以下位置记录日志:图开始执行时记录用户问题;每个节点执行完成后记录节点名、耗时、关键状态字段;图结束时记录最终回复和链路 ID。
如果使用了 Spring Cloud 体系,可以把链路 ID 放入状态,并透传到所有外部调用。这样当用户反馈回复不对时,可以通过链路 ID 快速找回完整执行日志。
超时控制同样不能忽视。大模型调用可能因为网络波动变慢,如果单个节点执行时间超过 10 秒,整个图体验会很差。可以在节点内部对 ChatClient 调用设置超时,也可以在图的外层使用 CompletableFuture 配合总超时机制。生产环境建议给每个节点设置独立的超时阈值,避免一个慢节点拖垮整条链路。
6.4 安全边界与最小权限
Agent 项目本质上是把自然语言转换为系统动作,所以安全边界必须清晰。
第一,工具调用权限要最小化。不要让模型能够直接调用所有方法,而是通过节点显式暴露允许调用的能力。比如订单查询节点只暴露“按订单号查询状态”这一个方法,不暴露“修改订单金额”,这样即使提示词被恶意注入,也不会扩大影响面。
第二,对模型输出做校验。模型生成的内容不能直接作为命令执行或注入数据库 SQL。涉及写操作、删除操作时,必须由代码明确控制,模型只负责生成参数,不负责执行动作。
第三,注意提示词注入。当用户输入被拼接进 system prompt 时,用户可能试图覆盖模型指令。可以考虑在节点中对用户输入进行长度限制、敏感词过滤,或者把用户输入与系统指令隔离开,尽量使用结构化参数传递用户内容。
第四,密钥管理。所有 API Key、数据库密码、第三方凭证,全部放入环境变量或配置中心,禁止提交到代码仓库。CI 阶段可以加入密钥扫描,防止误提交。
6.5 继续扩展:MCP 与 Skill
当项目变大后,会面临“工具越来越多”的问题。Spring AI Alibaba 支持 MCP(Model Context Protocol)接入外部工具服务,可以通过 MCP Server 暴露一组工具,让 Agent 动态发现并调用。MCP 的价值是标准化工具接口,让模型和工具之间的协议更统一,适合中大型团队在多个 Agent 之间共享工具。
但 MCP 也会带来可控性问题。工具越多,模型越容易选错。建议在 Graph 中仍然保留关键节点的显式调用,把 MCP 工具用在语义理解类、检索类等不需要强约束的场景。这样既能享受工具生态,也能保证核心链路稳定。
Skill 的概念也可以和图节点结合。你可以把一组复杂动作封装成一个 Skill,在图里用一个节点表示。比如“退款流程 Skill”,内部包含退款校验、审批单创建、通知用户三个步骤。从 Graph 外部看,它只是一个节点;从内部看,它又是一张子图。这种层次化设计能让大项目的图不至于变成一棵难维护的“大杂草”。
7. 总结与下一步学习路线
本文从纯 Workflow 和纯 Agent 的痛点出发,介绍了 Spring AI Alibaba Graph Workflow 的核心概念,并用一个客服工单 Agent 完成了一次完整实战。通过实战,你已经掌握了 Node、Edge、StateGraph、条件路由的使用方法,能够把“意图识别”“工具调用”“人工升级”“答案生成”这些能力组合成一条可控制的图执行链路。
接下来可以沿着三个方向继续深入。第一个方向是学习 LangGraph 设计模式,因为 Spring AI Alibaba Graph 借鉴了它的很多思想,理解 LangGraph 会让你更容易看懂源码演进;第二个方向是研究 MCP 工具接入,尝试把外部搜索、数据库查询封装成 MCP Server,再接入到你的图中;第三个方向是工程化能力,给项目补充链路追踪、状态持久化