Spring AI Alibaba 这个项目我从 1.0.0-M1 就开始跟了,中间踩了不少坑,最近把生产环境的几个智能体服务重构了一轮,顺手把整套"Agent荚"的设计思路沉淀成这篇文章。先说清楚,"Agent荚"不是一个官方名词,是我自己做的一套把多个智能体封装成独立运行单元的架构方案。你可以理解成 K8s 里的 Pod:一个荚里装着若干个 Agent,共享运行时、工具集和记忆策略,对外暴露统一接口,内部自由协作。
这篇文章适合两类人。第一类刚接触 Spring AI Alibaba,想知道怎么把一个智能体从 demo 变成能上线的工程;第二类想做多智能体编排,但还没想清楚代码结构怎么组织。我会把环境搭建、核心组件拆解、MCP 服务接入、多 Agent 协作和知识库增强全部过一遍,也会把生产环境里踩过的几个坑原原本本写出来。注意,这不是一篇纯概念文章,大部分代码都是可以直接跑起来再改的。
1. 先从一张"豆荚图"说起:Agent荚到底是什么
1.1 为什么是"荚"不是"框架"
我在做智能体的时候,最早遇到的问题是:用 Spring AI Alibaba 写一个简单的 ChatClient 调用通义千问很容易,但一旦任务稍微复杂一点,比如用户问"帮我查一下杭州未来三天的天气,顺便把会议提醒设在明天上午十点",你就得让模型调天气工具、调日历工具、再整理结果。这时候如果所有工具都塞在同一个 Service 里,代码很快变成一锅粥。
所以我引入了一个"荚"的概念。一个 Agent荚 是一个独立的智能体运行单元,里面装几个固定角色:
- Planner:负责拆解任务,决定先做什么、后做什么;
- Executor:真正执行工具调用或检索动作;
- Memory:短期记忆和长期记忆的统一入口;
- Tools:这个荚独有的工具集,比如天气、数据库查询、MCP 客户端等;
- Router:负责判断用户请求归属于哪个荚。
为什么叫"荚"而不叫"框架"?因为框架是全局的,但荚是局部的。每个荚可以有自己的大模型配置、自己的提示词体系、自己的工具白名单。你做销售智能体和做客服智能体,完全可以各自封装成一个荚,互不干扰,需要协作时再通过一个顶层 Router 来分发。
1.2 它能解决什么现实问题
很多人问:直接用一个大模型 + 一堆 tools 不行吗?不是不行,而是到了生产环境,你真的会遇到下面这些麻烦:
第一,职责边界不清。所有工具都注册给同一个模型,模型会做出很多超出预期的事情。你的金融工具、天气工具、写作文案工具混在一起,模型经常选错。把工具拆到不同的荚里,模型的选择空间变小,准确率明显上升。
第二,提示词互相污染。给客服 Agent 写的系统提示词,可能对编程 Agent 产生副作用。每个荚一个 System Prompt,每个荚自己管理,改动一处不影响其他。
第三,记忆管理混乱。多轮对话的记忆如果全局共享,用户聊完天气再去聊代码,前面的天气上下文还在消耗 token。按荚隔离记忆,可以精确控制每个场景的上下文窗口。
第四,线上挂了一个 Agent,你根本不知道影响了谁。做成"Agent荚"以后,每个荚有独立的健康检查、日志、QPS 统计,哪个荚出了问题直接就定位到了。
我现在的写法是这样的:一个 Spring Boot 服务负责启动若干"荚",每个荚都是一个独立的组合类,通过构造器注入各自的 ChatModel、Memory、Tool 列表和配置对象。对外暴露 HTTP 接口时,统一走一个/api/pod/{podName}/chat的入口,由 Router 决定找哪个荚处理。
2. 项目骨架搭建:依赖、配置一个都不能错
2.1 依赖引入:Spring AI Alibaba 的几个关键 starter
先说版本。Spring AI Alibaba 1.0.0-M3 对应 Spring Boot 3.3.x,如果你用的是更新的主干版本,请务必确认 BOM。我生产环境用的是 Spring Boot 3.3.5 + Spring AI Alibaba 1.0.0-M3,稳定性目前看是可以的。
pom.xml 里最少要有这些:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> <relativePath/> </parent> <dependencies> <!-- Spring AI Alibaba 对 DashScope 的 starter --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> <version>1.0.0-M3</version> </dependency> <!-- Spring AI 基础能力 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter</artifactId> </dependency> <!-- Web 接口 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MCP 客户端,后面接入外部服务用 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-mcp-client</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-M6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>一个容易忽略的点:spring-ai-bom 的作用是统一管理 Spring AI 各模块的版本。Spring AI Alibaba 的 starter 本身也依赖 Spring AI,如果你不引入 BOM,某些模块可能会因为版本不一致出现方法签名找不到的问题。建议把 Spring 官方的依赖管理放进来,Spring AI Alibaba 的 starter 版本和 Spring AI BOM 版本尽量对齐,别一个用 M3 一个用 M1。
2.2 工程配置:模型、密钥、超时的完整示例
配置是根据实际部署来定。我用的是阿里云百炼平台的 DashScope,所以配置相对简单:
spring: application: name: agent-pod-demo ai: dashscope: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/api/v1 chat: client: observations: include: - completion-tokens - prompt-tokens model: chat: qwen-plus几个我强烈建议在生产环境压一遍的参数:
spring.ai.dashscope.base-url:如果你不用默认的网关地址,或者要自建代理转发,这个值要改成你自己的网关。默认是 DashScope 官方地址,国内网络环境直接用没问题。spring.ai.model.chat:推荐先用 qwen-plus 跑通,再换 qwen-max 对比效果。qwen-plus 性价比高,qwen-max 在复杂推理上更强,但 token 成本也更高。- 超时配置:Spring AI 里可以通过
spring.ai.dashscope.http.connection-timeout和read-timeout控制。默认超时可能偏短,大模型生成长文本时容易触发超时重试,我一般把读超时调到 60 秒,一开始用默认配置经常在看到一半答案就报 503。
2.3 一个能跑的最小 Agent
配置好了怎么验证?写一个最小的 ChatClient。这一步不要急着设计荚,先把链路打通:
@Service public class MinimalAgentService { private final ChatClient chatClient; public MinimalAgentService(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem("你是一个严谨的智能体助手,回答问题时给出关键步骤。") .build(); } public String chat(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }再暴露一个 Controller:
@RestController @RequestMapping("/api/chat") public class ChatController { private final MinimalAgentService chatService; public ChatController(MinimalAgentService chatService) { this.chatService = chatService; } @PostMapping public Map<String, String> chat(@RequestBody Map<String, String> body) { String reply = chatService.chat(body.get("message")); return Map.of("reply", reply); } }启动后直接 POST 一个 JSON:
{"message": "请用一句话介绍你的能力"}能返回正常内容就说明 Spring AI Alibaba 链路已经通了。这一步如果都走不通,后面全部白搭,先排查 API Key、网络、模型名三个点。
3. 往里塞"豆子":Agent荚的核心能力拆解
3.1 工具调用:让模型的手伸出去
一个没有工具调用的 Agent 只能聊天,不能干活。Spring AI 的 Function Calling 机制做得比较顺手,你只需要写一个带 @Tool 注解的类:
@Component public class WeatherAgentTools { @Tool(description = "查询指定城市的实时天气,返回天气状况和温度") public String getCurrentWeather(String city) { // 真实项目这里会去调第三方天气 API if ("杭州".equals(city)) { return "晴,26℃,东北风3级"; } if ("北京".equals(city)) { return "多云,22℃,微风"; } return "暂无该城市数据"; } }然后在构建 ChatClient 的时候把这个工具注册进去:
this.weatherClient = builder .defaultSystem("你是天气助手,只能回答天气相关问题。") .defaultTools(new WeatherAgentTools()) .build();Spring AI Alibaba 会在模型判断需要调用工具时,把"天气查询"这个函数的描述和参数 schema 自动发给模型,模型返回一个 function call 请求,框架再去执行你写的 Java 方法,把结果返回给模型继续生成。整个过程对使用者来说是透明的。
但有个坑:工具描述一定要写清楚,而且参数名要有实际含义。我见过有人写@Tool(description = "f1"),参数叫x,模型根本不知道该怎么传。工具 description 直接决定了模型能不能正确选择工具,肉眼看不出的问题,往往就是描述太模糊了。
3.2 记忆管理:会话内与长期记忆
做智能体,最怕用户连着问几个问题,结果第二个问题里全是代词,"那个城市""刚才说的那位",模型一头雾水。解决这个问题要靠记忆。
Spring AI 里提供了 Advisor 机制和 ChatMemory。先引入依赖:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-advisors-starter</artifactId> </dependency>然后配置:
@Configuration public class AgentPodConfig { @Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } @Bean public Advisor messageChatMemoryAdvisor(ChatMemory chatMemory) { return new MessageChatMemoryAdvisor(chatMemory); } @Bean public ChatClient weatherPodChatClient(ChatClient.Builder builder, Advisor memoryAdvisor) { return builder .defaultSystem("你是天气助手。") .defaultTools(new WeatherAgentTools()) .defaultAdvisors(memoryAdvisor) .build(); } }这里有个关键操作:MessageChatMemoryAdvisor会按照会话 id 把历史消息存进 ChatMemory,并在每次新提问时自动拼装历史上下文。你需要给每个会话指定一个 conversationId,一般在 Controller 层从请求头里取:
String conversationId = request.getHeader("X-Conversation-Id"); chatClient.prompt() .user(message) .advisors(advisor -> advisor.param("chat_memory_conversation_id", conversationId)) .call() .content();生产环境我很不建议一直用 InMemoryChatMemory,服务一重启记忆全没了。我后来换成了 Redis 实现的 ChatMemory,把历史消息存到 Redis 里,多实例部署时才不会出现用户上个请求打到 A 机器、下个请求打到 B 机器导致上下文错乱。
长期记忆又是一个层面的问题。这里的方案是:对用户的关键信息做结构化抽取,存进数据库。比如用户说"我叫张三,偏好简洁答复",一个抽取 Agent 把这个信息转成 JSON 存起来。下次对话时,系统提示词里注入这些画像信息。短期记忆管上下文,长期记忆管偏好,两者别混。
3.3 规划执行:从"一句话"到"一串动作"
单纯让模型直接调一次工具,处理不了复杂任务。比如用户说"帮我整理本周工作周报,并发送给项目经理",这个任务至少包含三个步骤:整理会议纪要、生成周报、找到收件人并发送邮件。这就是典型的 Planner-Executor 模式。
我实现了一个轻量级 Planner,核心思路是用一次模型调用生成 JSON 步骤数组,然后遍历执行:
public class ReActPlanner { private final ChatClient plannerClient; private final ToolExecutor toolExecutor; public ReActPlanner(ChatClient plannerClient, ToolExecutor toolExecutor) { this.plannerClient = plannerClient; this.toolExecutor = toolExecutor; } public String execute(String task) { String prompt = """ 你是一个规划器。请把下面的任务拆解成最多3个步骤。 每一步必须满足: 1. 可以用一个工具调用完成 2. 不依赖上一步的返回值 3. 输出严格 JSON 数组,格式为 [{"step": "步骤说明", "tool": "工具名", "input": "工具入参"}] 任务:%s """.formatted(task); String planJson = plannerClient.prompt().user(prompt).call().content(); // 这里建议用 Jackson 解析,同时做一层异常兜底 List<Step> steps = JsonUtils.parseArray(planJson, Step.class); StringBuilder result = new StringBuilder(); for (Step step : steps) { String toolResult = toolExecutor.execute(step.tool(), step.input()); result.append(step.step()).append(":\n").append(toolResult).append("\n"); } return result.toString(); } }这个方案最大的坑是"模型输出格式不稳定"。你明明要求输出 JSON,模型偶尔还是会在 JSON 前后夹带解释性文本,比如"好的,我来拆解任务:\n[{"step":"...""。直接 JsonUtils.parseArray 会炸。我后来的做法是让模型开启 JSON 模式:
// Spring AI 1.0 中部分模型支持 responseFormat 参数 ChatResponse response = chatModel.call( new ChatRequestBuilder() .model("qwen-plus") .messages(...) .responseFormat(ResponseFormat.JSON) .build() );再配合一段字符串清洗逻辑,去掉 JSON 前后的非结构文本,兜底解析。如果还不放心,就在解析失败时提示模型"你刚才的输出格式不对,请重新输出纯 JSON",做一次重试。Agent 的可靠性不是指望模型永远不犯错,而是你要设计好"它犯错之后怎么办"。
4. MCP 服务接入:Agent荚的"外接器官"
4.1 MCP 的基础工作方式
MCP 全称 Model Context Protocol,是最近智能体生态里非常热的协议。简单说它把"工具"标准化了:以前每个外部系统都要给模型单独封装一套工具接口,现在只要那个系统实现一个 MCP Server,任何支持 MCP 的 Agent 都能直接发现并使用它的工具。
MCP 有两种常见传输方式:
- stdio:客户端以子进程方式启动一个 MCP Server,通过标准输入输出通信。适合本地工具,比如文件系统操作、命令行工具;
- SSE / Streamable HTTP:通过网络连接远程 MCP Server,适合跨机器、跨团队提供服务。
为什么说它是 Agent荚的"外接器官"?因为有了 MCP,你不必把每个工具都写死在代码里。比如你的 Agent 需要一个查询内部 Wiki 的能力,只要把 Wiki 系统包成一个 MCP Server,在配置里声明一行,Agent荚就能把它当成本地工具一样调用。新增能力不需要发版,改配置重启就行。
4.2 在 Spring AI Alibaba 中接入 MCP 服务
接入一个现成的 MCP Server,核心是配置。Spring AI 的 MCP Client starter 会自动把 MCP 暴露出来的工具注册到 ChatClient 的工具列表里。
先加依赖:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-mcp-client</artifactId> </dependency>连接本地 stdio 类型的 MCP Server,配置长这样:
spring: ai: mcp: client: connections: - name: filesystem-server enabled: true type: stdio command: npx args: - -y - "@modelcontextprotocol/server-filesystem" - /tmp如果你想连接一个远程 SSE 类型的 MCP Server:
spring: ai: mcp: client: connections: - name: remote-wiki-server enabled: true type: sse sse: url: http://your-mcp-server-host:8000/mcp配置好之后,在构建 ChatClient 时不需要手动注册工具。Spring AI 的 MCP Client 会动态发现远程工具,你只要在 defaultTools 里不写死,或者让 MCP Tool 名称在最后统一注册。我的习惯是这样:
@Bean public ChatClient customerPodChatClient(ChatClient.Builder builder, Advisor memoryAdvisor) { return builder .defaultSystem("你是客服助手,遇到客户需要查订单时使用订单查询工具。") .defaultAdvisors(memoryAdvisor) .build(); }但这里有个关键坑:多个 MCP Server 同时存在时,工具名可能冲突。比如 A 系统有一个getUserInfo,B 系统也有一个getUserInfo,MCP Client 注册时会报 Bean 冲突。我的建议是给每个 MCP Server 设置不同的工具名称前缀,或者通过配置项指定只暴露哪些工具:
spring: ai: mcp: client: connections: - name: order-server tools: - "getOrderInfo" - "queryOrderList"4.3 自己写一个简单的 MCP Server
有时候你需要把一个内部服务封装成 MCP Server。这里我用 Node.js 写一个最简版本,Spring AI Alibaba 里直接配置 stdio 就能调用:
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const server = new McpServer({ name: "internal-ping", version: "1.0.0" }); server.tool("ping", { target: { type: "string" } }, async ({ target }) => { return { content: [{ type: "text", text: `pong: ${target}` }] }; }); const transport = new StdioServerTransport(); await server.connect(transport);然后在 Spring 配置里连接它:
spring: ai: mcp: client: connections: - name: ping-server enabled: true type: stdio command: node args: - /path/to/ping-server.js需要注意:MCP Server 返回内容的格式是 MCP 协议规定的 TextContent 数组,不是自由文本。你调试的时候如果发现 Agent 那边收到的是奇怪的对象而不是字符串,先检查 MCP Server 的返回结构对不对。我在初期写 MCP Server 时经常忘记包{ content: [...] }这个结构,导致 Agent 一直拿不到实际内容。
5. 多 Agent 协作与知识库:把单荚升级成集群
5.1 多 Agent 路由与编排
当系统里有多个 Agent荚,比如"天气荚""订单荚""文档荚",你就需要一个路由器来决定用户消息交给谁。
这个 Router 本身也可以是一个大模型调用:
@Service public class AgentRouter { private final ChatClient routerClient; private final Map<String, AgentPod> podRegistry; public AgentRouter(ChatClient.Builder builder, List<AgentPod> pods) { this.routerClient = builder .defaultSystem("你的任务是把用户请求分类到正确的 Agent。可选类别:" + pods.stream().map(AgentPod::getPodName).toList()) .build(); this.podRegistry = pods.stream() .collect(Collectors.toMap(AgentPod::getPodName, p -> p)); } public AgentPod route(String message) { String podName = routerClient.prompt() .user(message) .call() .content(); AgentPod pod = podRegistry.get(podName.trim()); if (pod == null) { // 兜底到通用荚 return podRegistry.get("general"); } return pod; } }我把每个 Agent荚实现成一个统一定义的接口:
public interface AgentPod { String getPodName(); String process(String userMessage, String conversationId); }这样新增一个荚、注册到 List 里,Router 自动就多了一个可路由的目标,业务代码不用改。生产环境里我建议给 Router 加一个"拒识"能力:如果用户请求跟所有 Agent 都不匹配,不要硬分,直接走人工兜底流程,比如转接客服或者回复"这个问题我暂时无法处理"。硬分导致答非所问,用户体感比问不到更糟。
5.2 知识库接入:向量化与检索增强
智能体答不出内部知识,原因往往不是模型笨,而是模型根本没见过你的私有文档。所以知识库构建是 Agent 落地绕不开的一步。
Spring AI 对 RAG(检索增强生成)的封装比较成熟。基本流程是:文档切分成 chunk,embedding 模型把 chunk 转成向量,存入向量数据库;用户提问时,把问题也转成向量,检索出最相关的文档片段,拼进提示词让模型回答。
Spring AI Alibaba 对接 DashScope 的 embedding 模型很容易:
spring: ai: dashscope: embedding: api-key: ${DASHSCOPE_API_KEY} model: text-embedding-v4向量库可以选择 PGVector,需要引入依赖:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pgvector-store</artifactId> </dependency>然后声明 VectorStore:
@Bean public VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) { return new PgVectorStore(jdbcTemplate, embeddingModel, new PgVectorStoreConfig.Builder() .withTableName("agent_docs") .build()); }文档入库时,用这个代码:
public void ingest(String content, String docId) { TextDocument doc = TextDocument.builder() .id(docId) .text(content) .build(); vectorStore.add(List.of(doc)); }查询时先检索再问答:
public String askWithKnowledge(String question) { List<Document> docs = vectorStore.similaritySearch( SearchRequest.query(question).withTopK(3) ); String context = docs.stream().map(Document::getText) .collect(Collectors.joining("\n---\n")); return chatClient.prompt() .system("请基于下面的资料回答问题,资料中没有的信息不要编造:\n" + context) .user(question) .call() .content(); }这里我踩过最深的坑是 chunk 大小。中文场景下 chunk 切得太大,向量检索精度下降;切得太小,上下文碎片化,模型拼接后语义不连贯。我目前用的是 300 到 500 字一个 chunk,重叠 50 字,对不同文档类型再做微调。先跑一轮真实业务问题,看检索召回质量再调。别迷信某个固定值。
5.3 Agent荚的管理与可观测性
一个服务里跑了好几个 Agent荚,最尴尬的是线上出问题说不清是哪个荚导致的。所以我把每个荚的调用都记录了一份结构化日志:
@Component public class PodCallLogger { private final MeterRegistry meterRegistry; public PodCallLogger(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } public void record(String podName, long costMs, boolean success) { meterRegistry.counter("agent.pod.call", "pod", podName, "result", success ? "success" : "fail" ).increment(); meterRegistry.timer("agent.pod.cost", "pod", podName) .record(Duration.ofMillis(costMs)); } }Micrometer 接上 Prometheus 以后,每个荚的 QPS、成功率、平均耗时就都出来了。社区里也有人开源了 Spring AI Alibaba 的管理后台,可以用来查看 Agent 的运行日志和调用量,我试过一版,基础功能够用,但线上还是建议自己按需定制。
还有一个经验:每个荚的调用都应该有独立的 traceId,贯通日志、数据库、外部 API。我用的是 Spring Cloud Sleuth 的轻量替代方案,在每个请求入口生成 traceId,传给大模型调用的元数据里,这样排查问题时能完整还原一次对话链路。没有 traceId,多 Agent 协作的应用排障就是灾难。
6. 常见问题与避坑实录
6.1 版本不兼容与依赖冲突
Spring AI 的版本迭代速度很快,M1、M2、M3 之间的 API 变化都很大。我遇到过最典型的问题:Spring AI Alibaba 1.0.0-M3 依赖的 Spring AI 核心版本,和项目里其他 Spring AI 组件版本不一致,导致运行时出现NoSuchMethodError。排查了半天,最后发现是 BOM 没对了。建议直接用 Spring AI Alibaba 官方提供的 BOM,不要自己手工指定 Spring AI 各组件版本。另外 Spring Boot 3.4 出来后,如果你暂时不想处理 Java 17 之外的升级问题,就钉在 Spring Boot 3.3.x。
6.2 模型输出不稳定
模型有时候会"自由发挥",这个问题在日常对话里问题不大,但一旦进入工具调用、JSON 解析场景就非常致命。我现在的策略是三层兜底:
- 尽量用模型的响应格式参数,能开 JSON 模式就开 JSON 模式;
- 解析失败时,不是直接报错,而是把错误信息反馈给模型"你刚才输出格式不对,请重新输出合法 JSON",最多重试两次;
- 如果两次都失败,放弃本次工具调用,给用户返回"暂时无法完成"。
记住一个原则:Agent 不是一次性把结果给到位的,它是一个尝试、纠错、再尝试的循环。你把循环写稳了,模型偶尔抽风就不会伤到核心链路。
6.3 MCP 连接与鉴权坑
MCP 最大的坑在远程连接上。stdio 模式是本地子进程,相对安全。但远程 SSE 连接有几个问题:鉴权、重连、超时。目前很多自建 MCP Server 根本不带鉴权,直接裸奔在内网,这个问题在生产环境非常危险。我的建议:远程 MCP 至少加一个简单的 API Key 校验,或者放到服务网格里限制来源。另外,MCP 连接断了以后 Spring AI 的重连策略要看版本,老版本需要手动重启,新版本好些。如果你要长期跑,建议监控 MCP 连接状态,断开时及时告警。
6.4 超时与重试策略
大模型接口的延迟波动非常离谱,高峰时段 qwen-plus 生成一段 500 字的回答可能花 20 秒。如果你在 HTTP 请求层设置了 10 秒超时,就会频繁触发超时。我的经验是分两层设置:HTTP 客户端读超时设置 60 秒,业务层设置一个"软超时",比如超过 45 秒后返回一个中间态给用户"正在整理结果,请稍后查询",不让用户一直白等。
重试方面要小心:幂等操作可以重试,非幂等操作不能盲目重试。比如让模型生成邮件内容,重试没问题;但如果工具已经创建了订单,你再重试一次就是重复下单。所以重试策略要放在工具执行层,而不是盲目放在大模型调用层。一个大模型调用失败,你重试它是安全的;一个工具执行失败,你重试前要先判断这个工具是否幂等。
7. 最后说几句实操体会
做了这几个版本的 Agent 项目,我越来越觉得"别为了 Agent 而 Agent"这句话才是最重要的。很多时候一个简单的 RAG 流程就能解决 80% 的需求,没必要上多 Agent 编排。Agent荚这套设计,最大的价值不是技术上的突破,而是让团队里的人能清晰地说出"我这个荚负责什么,别人不要碰"。
我用 Spring AI Alibaba 这段时间的感受是:它的上手曲线比我想象中平缓,尤其是工具调用和 Advisor 机制,解决了很多之前要手写死记硬背的工程问题。但目前整个 Spring AI 生态还在快速变化中,不建议把核心逻辑和某个具体版本绑得太死。我的做法是:把 Agent 的核心抽象做成接口,Spring AI Alibaba 只是其中一种实现,将来框架升级、甚至换模型厂商,都不至于推倒重来。
最后再分享一个小技巧:不要把大模型返回结果直接透传给用户。我现在所有 Agent 荚都加了一个"结果校验"环节,核心字段存在性校验、敏感信息过滤、格式规范化。大模型是会一本正经胡说八道的,你可以在提示词里千叮咛万嘱咐,但永远比不过最后一道代码兜底。希望这些经验能帮你在做智能体的路上少踩几个坑。