最近经常有同学私信问我:Java 后端现在到底能不能做 Agent?网上搜出来的教程十个里有八个是 Python 调用大模型接口,剩下的两个还在用小 Demo 演示“你好,世界”。一旦真到了企业级场景,要接入内部业务系统、要管理多轮记忆、要控制工具调用权限、要兼容多个模型服务商,几乎所有示例都会断在第一步。
这次我做一个判断:Java 开发者做企业级 AI Agent,SpringAI 2.0 是目前最值得跟进的路线。它不是又一个“AI 封装库”,而是把大模型能力真正拉进了 Spring 生态,用你熟悉的依赖注入、Starter、AOP、工具抽象来解决 Agent 工程化问题。本文会用“智能航空项目”作为实战背景,把多模型、Tools、MCP、多层记忆、Skills、Agent 编排串成一条完整链路,看完之后你可以直接照着搭一版属于自己的企业级 Agent 底座。
文章偏长,建议先收藏再读,尤其是最后两章的排错思路和生产建议,实战时会反复用到。
1. 这篇文章真正要解决的问题
先别急着写代码,我们先说清楚:SpringAI 2.0 到底帮你省掉了什么?
如果只用原生方式调大模型接口,你会遇到一连串重复劳动:每个模型服务商都要写一套 HTTP 客户端;工具调用要自己解析 function calling 参数;多轮对话要手动维护历史消息;换个模型要改业务代码。更麻烦的是,企业内部通常有订单系统、航班系统、会员系统,Agent 必须能安全访问这些数据,而不是靠模型“猜”答案。
SpringAI 2.0 把这几件事全部统一了:
- 模型接入层:用一套 API 对接 OpenAI、通义千问、DeepSeek 等模型服务商;
- 工具调用层:用
@Tool注解暴露 Java Bean 方法,让模型在对话中自动决定是否调用; - 协议层:通过 MCP 标准化接入外部数据和工具服务;
- 记忆层:提供会话记忆、向量化长期记忆机制;
- 编排层:支持 Skills 把提示词、工具、模型、记忆策略组合成可复用资产。
一句话:SpringAI 真正降低的不是“调用模型的成本”,而是“Agent 进入企业系统的工程成本”。
这篇文章适合谁?有 Spring Boot 基础、想做企业级 AI 应用、不想被困在 Python Demo 里的后端开发。如果你正准备把 AI 接入航空、金融、电商这类强业务系统,建议把全文读完。
2. SpringAI 2.0 核心概念与 Agent 架构
在进入航空项目之前,有几个概念必须先把边界划清楚,不然写代码时很容易混淆。
2.1 ChatClient:Agent 的统一入口
SpringAI 对外的核心门面是ChatClient,设计思路和RestClient、WebClient一脉相承。你不需要直接拿着模型 API 拼请求体,而是通过 Builder 构建一次对话会话,再通过prompt()传入用户消息,最后调用call()拿到回复。
String answer = chatClient.prompt() .user("今天从北京飞上海有哪些航班?") .call() .content();这段代码背后做了什么?SpringAI 会自动组装 messages、调用模型、解析流式响应。它让你从模型差异中解放出来,后续要切换模型服务商,业务代码基本不用动。
2.2 Model 与多模型抽象
ChatModel是模型层的核心抽象,OpenAI、通义千问、DeepSeek 都有自己的实现类。SpringAI 2.0 支持你在配置文件中指定默认模型,也可以同时注入多个ChatModel,做一个简单的模型路由。后面第四章会演示具体做法。
2.3 Tool:Function Calling 的 Spring 化
大模型本身不知道你航班系统里有什么数据。Tool 机制的本质是:允许模型生成一次“工具调用请求”,SpringAI 代替你执行 Java 方法,再把执行结果返回给模型,让它基于真实数据生成最终回复。在 SpringAI 中,这段过程被简化成@Tool注解。
@Component public class FlightTools { @Tool(description = "根据出发城市、到达城市和日期查询可用航班") public List<Flight> searchFlights( @ToolParam(description = "出发城市") String from, @ToolParam(description = "到达城市") String to, @ToolParam(description = "出发日期,格式 yyyy-MM-dd") String date) { // 调用内部航班服务,返回真实数据 return flightService.search(from, to, date); } }@ToolParam是为了让模型理解参数语义。为什么这个很重要?因为模型只是“生成参数值”,真正执行的是你的 Java 方法。参数描述越清楚,模型调用工具就越精准。
2.4 MCP:工具协议的标准化
如果 Tools 是“单机版”工具调用,MCP 就是“分布式版”。MCP 帮你解决一个问题:当工具不在当前服务进程里,而是运行在另一个系统、另一台服务器上,怎么让 Agent 发现并调用它?
MCP 提供了统一协议,相当于给 Agent 世界做了一个标准化接口。航司可以把自己的航班查询、值机服务、行李服务通过 MCP Server 暴露出去,Agent 作为 MCP Client 动态接入。这样你的工具体系不再是“代码里写死的一堆 @Tool”,而是可以跨团队、跨语言复用。
2.5 ChatMemory 与 Advisor:上下文管理
多轮对话最烦的就是“模型忘了上一句”。SpringAI 通过ChatMemory存储历史消息,通过Advisor在每次模型调用前自动把相关历史拼进 Prompt。MessageChatMemoryAdvisor是其中的关键实现,后面会配合用户 ID 实现多会话隔离。
2.6 Skills:能力资产化
Skills 可以理解为 Agent 的“预制菜”:把某个业务场景所需的提示词、工具、模型偏好、记忆策略打包成一个技能单元。运营人员提问“帮我查航班延误信息”,Agent 自动选择航班查询技能,调用对应工具,按预设语气回复。它让 Agent 从“一个 ChatClient”变成“一系列可维护的能力集合”。
2.7 架构层次对比
| 维度 | 传统硬编码方式 | SpringAI Agent 方式 |
|---|---|---|
| 模型接入 | 每个服务商写一套 Client | ChatModel 统一抽象 |
| 工具调用 | 手动解析 function calling | @Tool 注解暴露 Java 方法 |
| 外部服务 | 自行设计 HTTP/RPC 协议 | MCP 标准化接入 |
| 对话记忆 | 自己拼 messages | ChatMemory + Advisor |
| 能力复用 | 复制粘贴提示词 | Skills 配置化组合 |
| 可观测性 | 基本靠日志 | 工具调用链路清晰可见 |
看完这个对比,你已经能理解 SpringAI 2.0 的价值定位了。它不是让你“更省事地调大模型”,而是让 AI 能力变成 Spring 工程体系里一个普通但强大的组件。
3. 智能航空项目场景与前置环境准备
实战项目我选的是“智能航空助理”。你会跟着我搭一个能处理航班查询、机票预订咨询、航班动态跟踪、机场服务引导的 Agent。这个场景非常适合练手,因为它的业务边界清楚、工具职责明确、数据来源多样,刚好能覆盖全链路知识点。
3.1 场景需求
- 用户问“明天北京到成都的航班有哪些”;
- Agent 调用航班搜索工具,返回真实航班列表;
- 用户问“我订的 MU5105 现在延误没有”;
- Agent 通过 MCP 连接航班动态服务,返回最新状态;
- 用户问“我之前查过的那班最低多少钱”;
- Agent 从会话记忆中找到之前查询上下文并回答。
3.2 技术栈
- JDK 17+
- Spring Boot 3.4+
- Maven 3.9+
- Spring AI 2.0 系列依赖
- 一个可用的大模型 API(国内可使用通义千问、DeepSeek 等 OpenAI 兼容服务)
具体版本以你下载到的最新 2.x 版本为准,本文重点讲通用实现思路,不锁定小版本号。
3.3 创建 Maven 工程
pom.xml最核心的部分是引入 Spring AI BOM 和 Starter:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>2.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency> </dependencies>如果你使用通义千问等国内服务商,通常可以走 OpenAI 兼容协议,引入spring-ai-starter-model-openai后改base-url即可,不需要额外引入专用 SDK。
3.4 项目结构
smart-air-agent/ ├── pom.xml └── src/main/java/com/example/smartair/ ├── SmartAirApplication.java ├── config/SkillConfig.java ├── agent/FlightAgentService.java ├── tools/FlightTools.java ├── tools/OrderTools.java ├── mcp/McpClientConfig.java └── memory/MemoryConfig.java先不用纠结每个类怎么写,后面会逐个展开。
4. 多模型接入与切换
企业级项目基本不可能只绑定一个模型服务商。原因很现实:不同模型在中文理解、工具调用、价格、速度上各有优劣。SpringAI 的多模型支持让你可以按场景路由。
4.1 application.yml 配置
最常用的方式,是通过 OpenAI 兼容接口对接国内模型服务商。以通义千问为例:
spring: application: name: smart-air-agent ai: openai: base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${QWEN_API_KEY} chat: options: model: qwen-plus mcp: client: name: smart-air-agent enabled: true注意:base-url必须改为你实际使用的模型服务商兼容地址。如果你对接的是 DeepSeek 或其他服务商,地址和模型名都会不同,一定要以服务商官方文档为准。
4.2 注入多个 ChatModel
当项目需要同时使用多个模型时,Spring 容器会注册多个ChatModelBean。你可以把默认模型注入到主服务,把备用模型注入到路由服务。
@Service public class ModelRouter { private final Map<String, ChatModel> modelMap = new ConcurrentHashMap<>(); public ModelRouter(List<ChatModel> chatModels) { for (ChatModel model : chatModels) { // 这里以模型类名或自定义 Bean 名作为 key,实际项目中建议使用配置映射 modelMap.put(model.getClass().getSimpleName(), model); } } public ChatModel getModel(String name) { ChatModel model = modelMap.get(name); if (model == null) { model = modelMap.values().iterator().next(); } return model; } }这段代码的核心价值是:业务层不依赖具体模型实现。将来某个模型下线或涨价,只需要调整配置或增加新的实现类。
4.3 模型切换的常见误区
很多人把“多模型”理解成代码里写if (model.equals("qwen"))。一旦接入十几个模型,类就开始失控。更合理的做法是:
- 在配置文件中维护模型别名与参数的映射;
- 通过配置中心动态刷新;
- 按任务类型路由,而不是按用户输入硬编码。
多模型接入只是第一步,真正让模型“有用”的是工具调用。接下来我们进入全链路最核心的部分。
5. Tools 机制:让模型真正操作航班系统
Tools 是整个 Agent 能不能落地的关键。没有工具的模型只是个“问答机器人”,有了工具的模型才是一个能参与业务处理的“数字员工”。
5.1 定义航班查询工具
在tools包下创建一个FlightTools组件:
package com.example.smartair.tools; import java.time.LocalDate; import java.util.List; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; @Component public class FlightTools { // 在真实项目中,这里注入航班服务、航线服务 private final FlightService flightService; public FlightTools(FlightService flightService) { this.flightService = flightService; } @Tool(description = "根据出发城市、到达城市和出发日期查询航班列表") public List<Flight> searchFlights( @ToolParam(description = "出发城市,如北京") String from, @ToolParam(description = "到达城市,如上海") String to, @ToolParam(description = "出发日期,格式为 yyyy-MM-dd") String date) { LocalDate day = LocalDate.parse(date); return flightService.search(from, to, day); } @Tool(description = "根据航班号查询实时航班状态") public FlightStatus getFlightStatus( @ToolParam(description = "航班号,如 MU5105") String flightNo) { return flightService.getStatus(flightNo); } }这里有一个容易被忽略的细节:description一定要把边界条件写清楚。比如“出发日期格式为 yyyy-MM-dd”,模型才会在调用前主动把用户口语中的“明天”“下周一”换算成标准日期。
5.2 把工具注册进 ChatClient
工具不是声明了就生效,还必须告诉 ChatClient 可以使用哪些工具。可以在构建 ChatClient 时声明:
@Service public class FlightAgentService { private final ChatClient chatClient; public FlightAgentService(ChatClient.Builder builder, FlightTools flightTools) { this.chatClient = builder .defaultSystem("你是航空公司的智能服务助理,回答要求准确、简洁、礼貌。") .defaultTools(flightTools) .build(); } public String chat(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }此时调用/agent/chat时,模型会自主决定“什么时候调用 searchFlights”“什么时候调用 getFlightStatus”。
5.3 工具调用的完整时序
用户提问后,实际发生的过程是:
- 用户消息进入 ChatClient;
- 系统消息加上历史上下文;
- 模型分析后返回一个“需要调用 searchFlights”的指令;
- SpringAI 反射执行 FlightTools 里的 Java 方法;
- 工具返回真实航班数据;
- 模型基于数据生成自然语言回答。
如果企业用模板方式自己对接模型,步骤 3 到 6 要写大量解析代码。用 SpringAI 后,这些都被框架接管了。
5.4 Tools 的工程约束
Tools 不是“随便暴露一个方法”就行,生产环境里要遵循三个原则:
- 工具方法必须有入参校验,不能信任模型生成的参数;
- 工具方法必须幂等,重复调用不能产生副作用;
- 写操作类工具必须做二次确认,模型不能擅自下单、退款、删除。
这些约束会在第十章继续展开。
6. MCP 集成:连接机场航班数据服务
Tools 机制解决的是“当前应用内部的方法调用”,但真实企业里,航班数据往往分布在多个系统:机场运行系统、航司订单系统、天气服务系统。MCP 的价值就是让 Agent 像调用本地方法一样调用远程服务。
6.1 MCP 解决了什么问题
没有 MCP 的时候,每个外部系统都要单独写一个 HTTP Client 封装。系统一多,Agent 的工具注册表变得无比混乱。MCP 提供了统一协议:服务端把能力暴露为标准化工具,客户端动态发现并调用。
对航空场景来说,机场可以负责发布航班动态 MCP 服务,航司负责发布票务 MCP 服务,Agent 团队只需要接入这些 MCP Client,不需要关心对方技术栈是 Java、Go 还是 Python。
6.2 定义一个 MCP Server
如果你需要对外提供航班数据,可以直接用 SpringAI 的 MCP Server Starter:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency>然后定义一个服务类,用@Tool暴露方法:
@Component public class AirportInfoService { @Tool(description = "查询机场当前延误指数和天气简报") public String getAirportInfo(@ToolParam(description = "机场三字码,如 PEK") String airportCode) { // 对接机场内部系统 return airportSystem.getBriefInfo(airportCode); } }对于 SpringAI 来说,MCP Server 和普通@Tool的代码写法非常接近,但部署和调用方式完全不同:MCP Server 是一个独立进程或独立服务,Agent 通过协议访问它。
6.3 让 Agent 作为 MCP Client 连接服务
在客户端项目里,我们前面已经引入了spring-ai-starter-mcp-client。启动时,SpringAI 会从配置的 MCP Server 地址拉取工具定义。
spring: ai: mcp: client: name: smart-air-agent enabled: true type: SSE connection: url: http://localhost:8081/mcp当 MCP Server 启动后,Agent 项目会自动发现其中的工具,像本地工具一样注入到 ChatClient。
这里要注意:MCP 的传输方式有 Streamable HTTP、SSE 等,配置方式随版本略有差异。生产环境跨网络调用时,务必在网关层做鉴权,不能把内部 MCP Server 直接暴露到公网。
6.4 Tools 与 MCP 的边界
| 维度 | Tools | MCP |
|---|---|---|
| 调用范围 | 当前应用进程内 | 跨进程、跨服务、跨团队 |
| 开发方式 | @Tool 注解 | MCP Server + MCP Client |
| 适用场景 | 内部工具、私有逻辑 | 标准化外部能力、多方共建 |
| 维护成本 | 低 | 中,需管理协议和版本 |
| 安全控制 | 依赖应用层鉴权 | 需要传输层和应用层双重鉴权 |
实战中,两者不是二选一,而是组合使用:内部通用逻辑用 Tools,外部系统能力用 MCP。
7. 多层记忆:从对话到业务上下文
Agent 聊不了几句就“失忆”,这是影响用户体验最直接的问题。很多项目的做法是把所有消息一股脑塞进 Prompt,结果很快碰到上下文长度上限。正确的做法是分层设计记忆。
7.1 第一层:会话记忆
会话记忆解决“同一用户多轮对话之间的连续性”。SpringAI 提供了ChatMemory和MessageChatMemoryAdvisor。
package com.example.smartair.memory; import java.time.Duration; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.InMemoryChatMemory; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MemoryConfig { @Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } @Bean public MessageChatMemoryAdvisor messageChatMemoryAdvisor(ChatMemory chatMemory) { return new MessageChatMemoryAdvisor(chatMemory); } }在业务代码中,通过chatId区分不同用户:
public String chat(String userId, String userMessage) { return chatClient.prompt() .user(userMessage) .advisors(a -> a.param("chatId", userId)) .call() .content(); }chatId很关键。没有它,所有用户都会共享同一份记忆,导致用户 A 的问题被用户 B 看到,这是严重的越权事故。
7.2 第二层:业务级记忆
模型记住了“你刚才问过什么”,不代表它理解“你是什么类型的旅客”。航班场景里,用户的常驻地、常选舱位、出行偏好,应该在业务层面显式存储。
实现方式是自定义 Advisor 或工具类:用户在对话中涉及偏好信息时,调用一个“保存用户偏好”的工具;下次回答时,把偏好注入系统提示词。
@Tool(description = "保存用户出行偏好") public void savePreference( @ToolParam(description = "用户ID") String userId, @ToolParam(description = "偏好描述,如经济舱靠窗") String preference) { userPreferenceService.save(userId, preference); }这种方式比“让模型自己记住”可靠得多,因为它不依赖模型的记忆能力,而是依赖业务系统。
7.3 第三层:长期记忆与向量化
当对话历史超过模型上下文窗口,InMemoryChatMemory 会失效。更通用的方案是把历史消息写入向量数据库,需要时做相似度召回。
选型上有 Redis Vector、PGVector 等。SpringAI 提供了向量存储抽象,你可以把对话摘要向量化后存储:
@Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { return new PgVectorStore(embeddingModel, jdbcTemplate, new VectorStoreConfig()); }长期记忆的使用逻辑并不复杂:
- 每轮对话结束后,生成摘要并向量化;
- 用户发起新问题时,先检索相关历史摘要;
- 把检索结果作为上下文注入 Prompt;
- 结合会话记忆,保证上下文既有连续性又有重点。
7.4 记忆分层总结
| 层级 | 存储内容 | 典型存储 | 生命周期 |
|---|---|---|---|
| 会话记忆 | 原始消息列表 | InMemory / Redis | 一次会话 |
| 业务记忆 | 用户偏好、业务属性 | 业务数据库 | 长期 |
| 向量记忆 | 历史摘要、重要结论 | PGVector / Redis Vector | 长期 |
请注意:任何一层记忆都必须绑定用户身份和权限范围。这不仅是功能问题,更是合规问题。
8. Skills:把 Agent 能力封装为可复用资产
很多团队做完一两个 Agent 后会发现:Agent 和 Agent 之间大量代码重复,提示词、工具、参数配置散落在各种 Service 里。Skills 解决的就是这个问题。
8.1 Skills 的设计思路
Skills 借鉴了 Claude Code Skills 等声明式技能的思路:把某个业务能力所需的全部配置打包成一个独立单元。一个 Skill 一般包含:
- 技能名称与描述;
- 适用的模型;
- 可使用的工具列表;
- 系统提示词模板;
- 记忆策略。
航空项目里,可以拆出多个技能:
flight-search:航班查询与订票咨询;flight-status:航班动态跟踪;airport-guide:机场服务引导;vip-service:会员权益解答。
8.2 一个 Skill 配置示例
在 SpringAI 2.x 中,Skills 的官方 Starter 和注解实现还在快速演进,这里给出一个贴近设计思路的配置样例:
--- name: flight-status description: 查询航班实时动态,回答延误、取消、登机口变更等问题 model: qwen-plus tools: - FlightTools.getFlightStatus - AirportInfoService.getAirportInfo memory: session-and-vector --- 你负责解答航班动态相关问题。回答要求: 1. 必须优先调用工具获取最新数据,禁止凭记忆回答航班状态; 2. 如果航班延误,需要友好解释原因,并引导用户关注航司通知; 3. 当用户询问登机口变化时,主动补充值机截止时间。这段配置的价值在于:工具权限、提示词、模型偏好被集中管理。运营人员调整话术时,不需要改 Java 代码。
8.3 应用中集成 Skill
在应用层,可以通过ChatClient按技能要求组装:
@Service public class FlightStatusSkill { private final ChatClient chatClient; public FlightStatusSkill(ChatClient.Builder builder, FlightTools flightTools, AirportInfoService airportInfoService) { this.chatClient = builder .defaultSystem(SkillPrompts.FLIGHT_STATUS_SYSTEM_PROMPT) .defaultTools(flightTools, airportInfoService) .build(); } public String execute(String userId, String message) { return chatClient.prompt() .user(message) .advisors(a -> a.param("chatId", userId)) .call() .content(); } }如果想更灵活一点,可以先让一个“路由模型”判断用户问题属于哪个技能,再把请求转发给对应 ChatClient。这就是 Agent 编排。
8.4 Skills 不是银弹
Skills 让能力复用变得方便,但也带来了新问题:技能多了之后,模型可能选错技能。所以技能描述要精确,技能之间要有清晰边界。另外,技能版本管理也要跟上,否则改了一个技能配置,可能影响所有调用它的 Agent。
9. 常见问题与排查思路
实战里踩坑是必然的。下面是航空 Agent 项目中最常见的几类问题,按“现象、原因、排查、解决”列出来,建议截图保存。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型返回“没有可用工具” | ChatClient 构造时没有注册 Tools | 查看 ChatClient Builder 代码,确认 defaultTools 是否传入 | 在构建 ChatClient 时显式注册工具类 |
| 工具被调用但结果不正确 | 参数描述模糊,模型传错参数 | 打印模型返回的工具调用入参 | 完善 @ToolParam 描述,增加入参校验 |
| MCP 连接失败 | MCP Server 未启动或协议不匹配 | 查看 MCP Server 日志和链路追踪 | 检查 MCP Server 地址、鉴权信息、Starter 版本 |
| 多轮对话“失忆” | 没有设置 chatId,或 Advisor 未生效 | 检查是否配置了 ChatMemory Advisor | 使用 MessageChatMemoryAdvisor 并传 chatId |
| 上下文超长 | 历史消息无限增长 | 查看发送给模型的消息数量 | 做窗口截断或摘要压缩 |
| 中文返回乱码 | 字符集设置问题 | 检查模型返回编码 | 确保 HTTP 请求和项目统一 UTF-8 |
| 模型频繁调用错误工具 | prompt 对工具选择说明不足 | 复现并查看工具调用日志 | 在 system prompt 中说明工具选择策略;分离技能 |
| 请求超时 | 模型服务商响应慢或密钥额度不足 | 查看模型服务商控制台 | 调整超时时间,增加重试与熔断 |
| 切换模型后效果变差 | 模型能力差异 | 使用同样的测试集对比 | 按任务场景固定模型,避免所有请求共用同一模型 |
排查的第一原则是:先看工具调用链路,再看模型回复。SpringAI 产出的日志里会记录每次工具调用的入参和出参,这是定位问题的黄金信息。
10. 最佳实践与生产建议
代码能跑,和能在生产环境稳定跑,中间还隔着很长一段路。下面是几个来自真实后端工程的经验,希望你在设计时提前考虑进去。
10.1 工具方法要做“最坏情况设计”
模型生成的参数不可信。比如用户说“给我查一下所有航班”,模型可能给from=空、to=空。工具方法必须做参数校验、空值兜底和超时控制。任何工具方法都不要直接执行没有权限校验的业务操作。
10.2 写操作必须有“人审”
Agent 可以做查询,但下单、退票、改签这类操作,建议先返回待确认信息,让用户明确确认后,再由另一个专用工具执行。更严格的做法是在后端增加人工确认状态位,Agent 只能提交申请,不能直接生效。
10.3 记忆与权限强绑定
会话记忆、业务记忆、向量记忆,所有记忆都必须和用户身份绑定。chatId不能只从前端传一个字符串,应该从登录态中解析,否则任何人都能伪造别人的 chatId,读到别人的对话记录。这是安全红线。
10.4 日志与可观测性
每个工具调用的入参、出参、耗时都要记录。推荐链路追踪。当一个问题反复出现时,回放日志能快速定位是模型理解问题、工具数据问题还是代码 Bug。
10.5 多模型路由要配合评估
不要在线上随意切换模型。建议建一个“回归测试集”,把典型的用户问题和期望工具调用结果整理好。每次调整模型、Skills 或提示词后,先跑一遍测试集再上线。
10.6 Skills 要按业务域拆分
一个大型 Agent 不要试图承载所有能力。更合理的做法是:按业务域拆技能,按技能测试、发布、监控。某个技能出问题时,可以单独下线,而不影响其他能力。
10.7 关键词:灰度、回滚、备份
MCP Server、模型路由、Skills 配置都属于生产变更,建议默认走灰度发布流程。变更前备份配置,变更时观察工具调用成功率和用户反馈,异常时能快速回滚到上一个版本。
到这里,你已经从概念、代码到生产建议,完整走了一遍基于 SpringAI 2.0 的航空 Agent 全链路。最后说一句个人感受:Agent 开发最容易踩的坑,就是过度沉迷“模型能力”,忽略了工具边界、记忆隔离和权限控制。带着工程思维去使用模型,才是企业级 Agent 的正确打开方式。下一步,建议你先把 5.2 节的最小 ChatClient 跑通,再逐步加入 Tools、MCP、记忆和 Skills。跑通之后再回头看本文的生产建议,你会比一开始理解得更深。