这次我们直接看一个比较重型的实战项目:SpringAI 2.0 企业级 AI Agent 智能航空项目。它不是简单跑一个 Hello World,而是把多模型接入、Tools 函数调用、MCP 协议集成、多层记忆、Skills 技能封装和 Agent 编排串成一条完整链路,目标场景是航空业务系统的智能化改造。
如果你正在做 Java 技术栈的 AI 应用,或者团队已经选型 SpringAI,但还没想清楚 Agent 怎么落地,这篇文章可以直接收藏。下面会从项目能力、架构设计、部署方式、功能验证、API 调用、性能观察和问题排查几个维度展开,尽量把整个项目拆到可以直接照着做。
1. 核心能力速览
在展开细节之前,先把项目的整体能力列出来。这里按企业级 AI Agent 项目的通用能力项整理,具体参数需要根据实际代码和配置确认。
| 能力项 | 说明 |
|---|---|
| 项目类型 | Spring Boot 3.x + SpringAI 2.0 企业级 AI Agent 应用 |
| 核心功能 | 多模型接入、Tools 工具调用、MCP 协议集成、多层记忆、Skills 技能封装、Agent 任务编排 |
| 业务场景 | 智能航空项目,覆盖航班查询、旅客服务、机场信息、票务售后等场景 |
| 多模型支持 | 可通过 SpringAI 统一接口接入 OpenAI、Ollama、DeepSeek、智谱、通义等模型 |
| 工具能力 | @Tool 注解定义函数,支持 Function Calling 自动选择工具并执行 |
| MCP 支持 | 基于 SpringAI MCP 模块接入 MCP Server,获取外部数据和能力 |
| 记忆能力 | 支持 ChatMemory 等多层记忆策略,区分短期会话记忆与长期业务记忆 |
| Skills 封装 | 将提示词、工具策略和业务逻辑封装为可复用的技能模块 |
| Agent 编排 | 基于 ChatClient + Advisor 实现多步骤任务编排与工具调度 |
| 启动方式 | 标准 Spring Boot 应用,Maven 打包后 java -jar 启动 |
| 接口 API | 提供 REST 接口,支持同步、流式、多轮会话和批量任务调用 |
| 资源需求 | 如果使用云端模型 API,对本地资源要求低;如果接本地 Ollama,则需要配置显卡或大内存 |
| 适合人群 | Java 后端开发、架构师、AI 应用开发团队 |
从能力面看,这个项目覆盖的是 SpringAI 2.0 的核心功能面,不是单一模型的 Demo,而是把企业落地时需要用到的集成方案都纳入进来了。
2. 适用场景与使用边界
项目命名为“智能航空”,说明业务场景有明确指向,但技术链路本身可以复用到其他行业。
适合的场景包括:
- 企业级 AI 助手:把内部业务系统以 Tools 方式开放给大模型,让模型能够查询航班、提交订单、获取旅客信息。
- 多模型替换与灰度:通过 SpringAI 统一抽象,不修改业务代码即可切换底层大模型,适合多供应商策略。
- MCP 生态集成:通过标准 MCP 协议接入数据服务、知识库、第三方工具,减少定制化开发。
- 旅客服务自动化:多轮对话中维护上下文记忆,完成从“查航班”到“选座位”到“值机提醒”的长链路任务。
- AI 能力中台建设:把 Skills、Tools、记忆策略沉淀为可复用模块,供多个业务线使用。
需要注意边界:
- 涉及航班信息、旅客个人信息、票务订单等数据,必须经过授权和脱敏处理,不能在未授权环境下使用真实数据。
- 大模型的工具调用不一定 100% 稳定,涉及支付、改签、退款等敏感操作必须有二次确认和人工兜底。
- 如果使用本地模型,性能与显存占用取决于模型规模和推理配置,需要提前做压力测试。
- MCP 服务端和客户端版本以及 SpringAI 版本之间存在兼容性问题,升级依赖时要谨慎验证。
- 不要将生产环境下的大模型 API 密钥提交到公开仓库,也不要在日志中打印完整的上下文字段。
3. 智能航空项目架构与核心设计思路
这个项目的架构可以拆成五层来看。
3.1 应用入口层
基于 Spring Boot 3.x 构建,通过 REST Controller 暴露 HTTP 接口。业务上分为两个入口方向:
- 面向 C 端旅客的“智能客服 / 出行助手”接口。
- 面向内部运营人员的“数据查询 / 业务处理 / 任务分发”接口。
3.2 模型接入层
SpringAI 2.0 通过 ChatClient 和 ChatModel 统一了模型调用方式。无论底层是 OpenAI 还是 DeepSeek、Ollama、通义,业务代码里看到的都是同一个客户端 API。
核心代码结构如下:
@Autowired private ChatClient chatClient; public String chat(String userMessage) { return chatClient.prompt(userMessage) .call() .content(); }在这个项目中,模型接入层还要处理一个关键问题:不同业务场景用不同模型。比如简单问答用轻量模型,复杂推理用强模型,票据信息抽取用本地私有化模型。这部分会做一个按场景路由的逻辑。
3.3 工具调用层(Tools)
Tools 是所有 Agent 能力的基础。SpringAI 2.0 支持通过注解声明一个方法为可调用工具,模型在回答时会判断是否需要调用这个工具,以及传入什么参数。
@Component public class FlightTools { @Tool("查询指定日期、航段之间的航班信息") public String queryFlights( @ToolParam(description = "出发城市") String departure, @ToolParam(description = "到达城市") String arrival, @ToolParam(description = "日期,格式 yyyy-MM-dd") String date) { // 调用真实的航班查询服务 return flightService.search(departure, arrival, date); } @Tool("查询航班实时状态") public String queryFlightStatus( @ToolParam(description = "航班号") String flightNo) { // 调用航班动态接口 return flightService.status(flightNo); } }3.4 中层能力层
包含以下关键模块:
- MCP 客户端模块:连接外部 MCP Server,获取天气、地理、机场延误信息等。
- 记忆模块:会话记忆与长期记忆分段存储。
- Skills 模块:把常用任务沉淀成技能模板。
- Agent 编排模块:组合工具、模型和记忆完成多轮任务。
3.5 数据与外部系统层
项目对接航班数据库、旅客系统、订单系统、机场信息接口和其他 MCP 服务。这里的重点是所有外部资源都通过 Tools 或 MCP 暴露给大模型,不直接让模型访问数据库。
4. 多模型接入与路由配置
SpringAI 2.0 的多模型接入方式和早期版本不同。2.x 版本把 model 包拆分得更加清晰,每个模型有独立的 starter。
以 OpenAI 和本地 Ollama 为例,先在 pom.xml 中加入依赖:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-ollama</artifactId> <version>2.0.0</version> </dependency>然后在 application.yml 中配置模型服务地址和密钥:
spring: ai: openai: api-key: ${OPENAI_API_KEY} base-url: ${OPENAI_BASE_URL:https://api.openai.com} chat: options: model: ${OPENAI_MODEL:gpt-4o-mini} temperature: 0.7 ollama: base-url: http://127.0.0.1:11434 chat: options: model: qwen2.5:7b这里的配置遵循一个原则:所有密钥和地址都从环境变量读取,不要硬编码。生产环境可以统一在配置中心管理。
在 Java 代码中,按场景调用不同模型:
@Service public class ModelRoutingService { private final ChatModel openAiChatModel; private final ChatModel ollamaChatModel; public ModelRoutingService( @Qualifier("openAiChatModel") ChatModel openAiChatModel, @Qualifier("ollamaChatModel") ChatModel ollamaChatModel) { this.openAiChatModel = openAiChatModel; this.ollamaChatModel = ollamaChatModel; } public String route(String scene, String prompt) { if ("complex_reasoning".equals(scene)) { return ChatClient.create(openAiChatModel) .prompt(prompt) .call() .content(); } // 默认走本地模型,节省成本 return ChatClient.create(ollamaChatModel) .prompt(prompt) .call() .content(); } }更精细的做法是基于 ChatClient Builder 构建多个配置实例,把系统提示词、上下文窗口、模型参数都固化在对应的 ChatClient 里。这样业务层不需要关心模型类型,只关心 ChatClient 实例即可。
5. Tools 与 Function Calling 实战
Tools 是整个 Agent 项目中最容易踩坑,也是最重要的部分。SpringAI 2.0 中,把工具直接注册到 ChatClient 上即可,模型会自行决定是否调用。
5.1 定义航班查询工具
创建一个 Spring 组件,类中的方法用@Tool注解标记:
@Component public class FlightTools { @Tool("根据出发地、目的地和日期查询可用航班列表") public String searchFlights( @ToolParam(description = "出发城市") String from, @ToolParam(description = "到达城市") String to, @ToolParam(description = "出发日期") String date) { // 实际开发中这里调用航班服务接口 return """ [ {"flightNo":"CA1234", "from":"北京", "to":"上海", "time":"08:00-10:30", "price":980}, {"flightNo":"MU5678", "from":"北京", "to":"上海", "time":"12:30-14:50", "price":760} ] """; } @Tool("根据航班号和日期查询航班实时动态") public String getFlightStatus( @ToolParam(description = "航班号") String flightNo, @ToolParam(description = "日期") String date) { if ("CA1234".equals(flightNo)) { return "航班CA1234: 准点到达"; } return "航班" + flightNo + ": 延误,预计晚到30分钟"; } }5.2 把工具绑定到 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 ask(String question) { return chatClient.prompt(question).call().content(); } }这里的核心机制是:SpringAI 会根据@Tool注解的方法生成 JSON Schema,然后在请求大模型时把工具描述传过去。模型返回一个“执行指令”,SpringAI 负责执行 Java 方法并再次把结果传回模型,最终生成回答。开发者只需要关心方法实现和返回值格式。
5.3 工具调用的异常处理
工具方法抛出异常时,SpringAI 会把这个错误信息传回模型,由模型生成一段“工具调用失败”的提示语。实际项目中最好在工具方法内部捕获业务异常,返回结构化错误信息。比如:
@Tool("查询航班价格") public String queryPrice(String flightNo) { try { return priceService.get(flightNo); } catch (Exception e) { return "查询航班价格失败:" + e.getMessage(); } }这样模型可以基于错误信息继续与用户交互,而不是直接中断整个会话。
6. MCP 服务接入与协议集成
MCP 是这段开发链路里的重点,也是最近 Air 应用开发中讨论最多的一块。MCP 本质上是一个标准化的“接口描述 + 工具调用”协议,SpringAI 2.0 对它做了内置支持。
6.1 MCP 在项目里的作用
在航空业务场景中,MCP 适合接入这些外部能力:
- 机场天气查询服务。
- 机场延误公告。
- 城市地面交通信息。
- 酒店、租车等第三方服务。
- 企业内部的航班数据 MCP Server。
引入 MCP 后,业务系统不再需要为每类外部数据开发一套定制接口,而是通过统一的 MCP Client 协议连接。数据提供方只要实现 MCP Server 标准,接入方就能直接使用。
6.2 SpringAI 接入 MCP Client
在 Spring Boot 配置文件中启用 MCP 客户端:
spring: ai: mcp: client: enabled: true name: flight-mcp-client type: SYNC在代码中通过 MCP 工具访问外部服务:
@Service public class McpWeatherService { private final ToolCallbackProvider mcpToolProvider; public McpWeatherService(ToolCallbackProvider mcpToolProvider) { this.mcpToolProvider = mcpToolProvider; } public List<ToolCallback> getMcpTools() { return Arrays.asList(mcpToolProvider.getToolCallbacks()); } }更完整的做法是把 MCP 工具注册到 ChatClient 中:
ChatClient chatClient = ChatClient.builder(chatModel) .defaultTools(mcpToolProvider.getToolCallbacks()) .build();6.3 MCP 与 Tools 的区别
项目里同时存在@Tool和 MCP,两者的关系是:@Tool是进程内的、由本项目自己实现的函数能力;MCP 是跨进程的、通过标准协议从外部服务获取的工具能力。两者都表现为“给模型提供的工具”,但生命周期和管理方式不同。
实际开发中建议:
- 业务核心能力用本地
@Tool实现,保证速度和可控性。 - 跨系统、跨团队的数据能力用 MCP 暴露,降低耦合。
- 如果是独立部署的 MCP Server,避免每个业务实例重复建立连接,考虑连接池和超时控制。
7. 多层记忆与会话管理
企业级 Agent 和单轮问答最大的区别就是记忆。这个项目强调“多层记忆”,也就是不是只有当前聊天窗口的上下文,而是会区分不同层级的记忆并使用不同的存储方式。
7.1 ChatMemory 接口与常见实现
SpringAI 中记忆的底层接口是ChatMemory:
public interface ChatMemory { List<Message> get(String conversationId); void add(String conversationId, List<Message> messages); void clear(String conversationId); }常用实现有:
InMemoryChatMemory:基于内存存储,适合单机测试。CassandraChatMemory:基于 Cassandra 存储。- 自定义
JdbcChatMemory:存到关系型数据库,适合企业项目。
7.2 在 ChatClient 中启用记忆
记忆不是直接塞给 ChatModel,而是通过 Advisor 在请求前后处理消息。
@Service public class MemoryFlightAgent { private final ChatClient chatClient; private final ChatMemory chatMemory; public MemoryFlightAgent(ChatClient.Builder builder, ChatMemory chatMemory, FlightTools flightTools) { this.chatMemory = chatMemory; this.chatClient = builder .defaultSystem("你是航空智能客服,请基于上下文和工具调用结果回复用户。") .defaultTools(flightTools) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); } public String chat(String userMessage, String conversationId) { return chatClient.prompt(userMessage) .advisors(a -> a.param("chat_memory_conversation_id", conversationId)) .call() .content(); } }7.3 多层记忆的实现思路
在航空场景下,可以把记忆拆成三层:
- 会话层:单次对话内上下文,通常 10~20 轮,用 ChatMemory 管理。
- 用户偏好层:常驻旅客信息、常飞航线、偏好座位、饮食偏好。使用 Redis 或数据库存储,在构造系统提示词时注入。
- 业务状态层:记录用户已经完成的步骤,比如已选航班、已提交订单、待支付状态。这部分要落在业务数据库,不能只放在模型上下文里。
代码示意:
public String buildContext(String userId) { String preference = userPreferenceService.get(userId); String flightOrder = orderService.getPendingOrder(userId); return """ 用户偏好:%s 待处理订单:%s """.formatted(preference, flightOrder); }多层记忆的核心原则是:模型上下文只保存“对话推理需要的信息”,持久化信息及时落库,避免把整个业务库都塞进提示词。
8. Skills 技能封装与复用
Skills 在这个项目中指的是把某类任务的处理方式封装成可复用模块,可能包括提示词模板、输入参数校验、工具组合策略和回调处理。
8.1 Skills 与 Tools 的区别
很多开发者在最开始会混淆这两个概念。简单理解:
- Tools 是能力:底层一个方法,模型可以调用它。
- Skills 是策略:针对某类任务的完整处理方案,可能同时使用多个 Tools,并配有专门的系统提示词。
举例:给用户“办理值机”是一个 Skill,它会查看用户航班信息,调值机接口,返回登机牌。其中“查航班”是 Tool A,“调值机接口”是 Tool B,而“值机 Skill”决定了何时调用、参数怎么取、失败怎么响应。
8.2 Skills 的代码组织方式
在 Spring 项目中,我建议用配置类来定义 Skill:
@Component public class CheckInSkill { private final ChatClient chatClient; public CheckInSkill(ChatClient.Builder builder, FlightTools flightTools, PassengerTools passengerTools) { this.chatClient = builder .defaultSystem(""" 你是值机助手。你的任务: 1. 先查询用户的航班信息。 2. 确认旅客身份。 3. 查询是否支持在线值机。 4. 调用值机接口完成办理。 如果任何一步失败,请明确告诉用户失败原因,不要编造结果。 """) .defaultTools(flightTools, passengerTools) .build(); } public String checkIn(String userId, String flightNo) { return chatClient.prompt("用户ID: %s, 航班号: %s, 帮我办理值机".formatted(userId, flightNo)) .call() .content(); } }8.3 Skills 的管理与发现
企业项目里 Skills 数量多了以后,需要做统一管理:
- 按业务域分包:flight、ticket、passenger、airport。
- 每个 Skill 提供元信息描述,便于上层 Agent 路由。
- 关键 Skill 可以单独写单元测试,用固定输入验证输出。
Skills 这套思路并不复杂,难的是把它做得足够“业务化”。一个高可用的 Skill 必须经过反复的真实对话测试,把边界情况都覆盖到。
9. Agent 编排与多步骤任务测试
当多模型、Tools、MCP、记忆、Skills 都准备到位之后,下一步是把它们编排成一个 Agent。
9.1 一个典型的航空多步任务
旅客说:“帮我查一下明天从北京到深圳的航班,然后给我订最早那一班。”
这个请求在传统系统里需要写死逻辑,但 Agent 场景下模型会自动拆解:
- 调用
searchFlights查询航班列表。 - 对比航班时间,选出最早一班。
- 调用
createOrder创建订单。 - 如果用户之前保存过乘机人信息,则自动填充。
- 返回订单确认信息。
9.2 Agent 方法定义
@Service public class FlightAgent { private final ChatClient chatClient; public FlightAgent(ChatClient.Builder builder, FlightTools flightTools, OrderTools orderTools, ChatMemory chatMemory) { this.chatClient = builder .defaultSystem(""" 你是航空出行助手。请严格按以下顺序处理: 1. 查询航班信息必须使用工具。 2. 下单前必须与用户确认乘机人和航班。 3. 订单创建成功后输出订单号。 """) .defaultTools(flightTools, orderTools) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); } public String execute(String prompt, String conversationId) { return chatClient.prompt(prompt) .advisors(a -> a.param("chat_memory_conversation_id", conversationId)) .call() .content(); } }这段代码最核心的是defaultSystem中的顺序约束,它决定了 Agent 行为是否符合业务规范。
9.3 流式输出与长任务处理
对于耗时较长的查询和生成,建议使用流式接口,提升用户体验:
public Flux<String> stream(String prompt) { return chatClient.prompt(prompt).stream().content(); }Controller 层返回Flux<String>,前端可以通过 SSE 方式消费。
如果任务链路复杂,涉及多次工具调用,建议在服务端做异步任务队列,不要让 HTTP 请求长时间占用线程。
10. 接口 API 调用示例
项目作为 Spring Boot 应用,天然提供 REST API。下面给出一套通用的接口设计。
10.1 开启接口服务
启动 Spring Boot 后,默认端口由server.port配置决定:
server: port: 808010.2 对话接口
@RestController @RequestMapping("/api/agent") public class AgentController { private final FlightAgent flightAgent; public AgentController(FlightAgent flightAgent) { this.flightAgent = flightAgent; } @PostMapping("/chat") public ResponseEntity<Map<String, String>> chat(@RequestBody ChatRequest request) { String reply = flightAgent.execute(request.message(), request.conversationId()); return ResponseEntity.ok(Map.of("reply", reply)); } @PostMapping("/chat/stream") public Flux<String> chatStream(@RequestBody ChatRequest request) { return flightAgent.stream(request.message()); } }请求体定义:
public record ChatRequest(String message, String conversationId) {}10.3 使用 curl 测试
curl -X POST http://127.0.0.1:8080/api/agent/chat \ -H "Content-Type: application/json" \ -d '{ "conversationId": "user-001", "message": "帮我查明天北京到上海的航班" }'预期返回结果是一个 JSON,其中reply字段会包含模型生成的自然语言回答。
10.4 使用 Python 调用接口
import requests url = "http://127.0.0.1:8080/api/agent/chat" payload = { "conversationId": "user-001", "message": "帮我查明天北京到上海的航班" } response = requests.post(url, json=payload, timeout=60) print(response.json())如果接入流式接口,可以使用requests的流式读取,或者直接使用httpx:
import httpx with httpx.stream("POST", "http://127.0.0.1:8080/api/agent/chat/stream", json=payload) as r: for line in r.iter_text(): print(line, end="")10.5 批量任务设计
批量任务是企业级场景的刚需,例如每天定时批量处理旅客的航班变动通知。推荐的做法是:
- 维护一个任务表,存储任务的批次号、状态、输入参数。
- 使用线程池或分布式任务框架异步执行。
- 每条任务独立调用 Agent 接口。
- 记录每次调用的 Token 消耗和执行耗时。
- 失败任务自动重试,最多重试 3 次。
@Service public class BatchTaskService { private final ThreadPoolTaskExecutor executor; public void submitBatch(List<BatchTask> tasks) { for (BatchTask task : tasks) { executor.execute(() -> { try { String result = flightAgent.execute(task.getPrompt(), task.getConversationId()); task.markSuccess(result); } catch (Exception e) { task.markFailed(e.getMessage()); } }); } } }11. 资源占用与性能观察方法
这个项目是纯 Java 服务,本身内存占用取决于 Spring Boot 应用和请求并发量。真正的资源压力来自两部分:大模型 API 的响应速度,以及本地模型的推理资源。
11.1 云端模型场景
如果使用 OpenAI、DeepSeek、通义等云端模型 API,本地服务不需要 GPU,主要关注:
- JVM 堆内存:建议至少 512MB,具体看并发量。
- 网络延迟:外部模型 API 延迟一般在几百毫秒到几秒。
- Token 消耗:多轮对话和 Tools 描述会显著增加 Token 数,需要做成本监控。
11.2 本地 Ollama 场景
如果要把整个链路在本地跑起来,可以在开发机安装 Ollama,拉取一个小模型试验:
ollama pull qwen2.5:7b ollama serve本地 7B 模型在 M 系列芯片或 8G 以上显存的显卡上可以运行,但速度和质量不如云端大模型。生产环境不推荐这种组合。
11.3 如何观测性能
建议在 Spring Boot 中集成以下观测手段:
- 使用 Micrometer 记录每次请求耗时。
- 记录模型调用的 Tokens 使用量。
- 对工具调用方法埋点,统计执行时长。
- 对接入的 MCP Server 做超时和熔断。
12. 常见问题与排查方法
结合 SpringAI 项目在本地开发和企业部署中常见的坑,整理如下:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动时提示 ChatClient 注入失败 | 未配置模型 API Key,或 ChatClient.Builder 不存在 | 检查 application.yml 中的 spring.ai 配置 | 在环境变量中配置模型 API Key,确认依赖已引入 |
| 模型返回不调用工具 | 工具方法没有被注册到 ChatClient | 检查 defaultTools 是否已加入工具类 | 确认工具类是 Spring Bean,并在 ChatClient 构建时传入 |
| 调用工具报“无法解析方法参数” | @ToolParam 描述不完整,模型生成参数不匹配 | 查看模型返回的 function call 日志 | 为每个参数补充完整 description 和类型 |
| MCP 连接失败 | MCP Server 未启动,或客户端 url 配置错误 | 检查 target url 是否可达 | 先单独验证 MCP Server 是否在运行,再启动 Spring 应用 |
| 多轮对话中历史消息累积过多 | 上下文超出模型窗口限制 | 观察请求体的大小和模型报错 | 使用 MessageWindowChatMemoryAdvisor 限制窗口大小,或做摘要压缩 |
| 工具执行后返回内容模型无法理解 | 工具返回值格式不清晰 | 查看工具返回的 JSON 字符串 | 返回结构化 JSON,并加入必要的字段说明 |
| 流式接口首次响应过慢 | 模型首 Token 延迟高 | 打印请求和响应时间戳 | 如果使用本地模型,考虑使用云端模型或提升硬件配置 |
| API 返回 401 / 403 | API Key 无效或没有权限 | 查看响应头和日志 | 更换合法密钥,确认模型服务商账号内余额充足 |
| 批量任务大量失败 | 并发数过高触发接口限流 | 查看日志中的 HTTP 状态码 | 在批量任务中引入重试和退避策略 |
排查这类问题最有效的方法,是把 SpringAI 的请求日志打开,查看模型返回的原始内容和 tool call 记录。只有看到真实链路,才能判断是模型理解问题、工具注册问题还是网络配置问题。
13. 最佳实践与使用建议
把这个项目跑通不难,但要用好、上生产,还需要遵循一些工程化建议。
13.1 保持工具返回结构化
工具返回的内容不要是野文本,尽量输出 JSON 格式。模型对结构化内容的解析能力远强于自由文本。比如查询航班的工具返回:
{ "flightNo": "CA1234", "from": "北京", "to": "上海", "departureTime": "08:00", "arrivalTime": "10:30", "price": 980, "status": "正常" }13.2 系统提示词要写清边界
不要期望模型自己知道该做什么。每一步该调用什么工具、什么时候停止、什么时候要求用户确认,都要在系统提示词里规定清楚。
13.3 对敏感操作增加确认机制
改签、退票、支付这类操作,Agent 不能直接执行。建议流程:
- Agent 生成操作方案。
- 前端展示给用户确认。
- 用户确认后调用独立的业务接口。
- 接口内部校验权限和数据。
13.4 记忆不能盲目增长
任何长期运行的 Agent 都要面对上下文膨胀问题。建议:
- 设置消息窗口上限,比如最近 10 轮。
- 对长对话做摘要,保留关键用户意图。
- 用户偏好写入业务库,不参与模型上下文。
13.5 模型与业务隔离
工具方法内部不能直接访问生产库,建议经过服务层和鉴权层。模型只接受工具返回的结果,不允许模型生成 SQL 后直接查询数据库。
14. 总结与下一步
这个 SpringAI 2.0 智能航空 Agent 项目最值得学习的地方,不是它解决了某一个算法问题,而是把企业级 Agent 需要的整套工程能力串起来了:多模型选型、工具注册、MCP 标准协议接入、多层记忆、Skills 封装以及最终的任务编排。纯 Java 技术栈的团队完全可以直接复用类似架构。
从验证顺序来看,建议先跑通最简单的“单模型 + 单 Tool”链路,确认 ChatClient 能正确调用工具;然后加入记忆,验证多轮对话的上下文是否稳定;接下来再考虑 MCP 接入和 Agent 编排。每一步都单独验证,不要一开始就把所有变量堆在一起,否则出了问题很难定位。
最容易踩的坑有三个:一是工具方法没有注册到 ChatClient,导致模型不调用工具;二是 MCP 服务连接失败但应用启动时不报错,直到实际调用才暴露;三是多轮对话后 Token 消耗暴涨,上下文超限。这三个问题在开发周期里迟早会遇到,可以先在本地准备对应的测试用例,免得生产环境再手忙脚乱。
后续可以扩展的方向也很多。比如把这个项目改造成航空部门内部的智能运营助手,接入更多业务工具;或者把 Skills 抽取成可配置的 JSON 策略,让产品人员可以调整提示词流程;再往前走一步,可以引入多 Agent 协作,让查询代理、订票代理、客服代理分工协作,形成真正完整的航空出行智能体。