Spring AI 2.0实战:构建智能航空助手Agent
2026/9/1 3:32:14 网站建设 项目流程

1. Spring AI 2.0 与 AI Agent:航空业务为什么需要它

先从一个真实的业务场景说起。假设你负责一家航空公司的后端系统,乘客在 App 上提问:“明天从北京飞深圳的航班有哪些?帮我选一个下午两点左右起飞的。”如果走传统开发,你需要先做意图识别、槽位提取、航班查询、结果拼接,再到前端渲染,整个链路全靠规则和硬编码,遇到没预料到的问法就找不到答案。

Spring AI 2.0 要解决的核心问题,就是把“大模型理解自然语言”和“后端业务能力”打通。它不是一个简单的 ChatGPT 封装,而是一套面向 Java 生态的 AI 应用开发框架。你可以用非常熟悉的 Spring 风格,把大模型接入项目,再通过工具调用(Tool Calling)、结构化输出、RAG 知识库等能力,构建出真正能调数据库、查接口、维护多轮上下文的 AI Agent。

本文适合以下读者:

  • 有 Java 和 Spring Boot 基础,想学习 AI Agent 开发的后端工程师。
  • 正在调研 Spring AI 2.0 企业级落地方案的技术负责人。
  • 准备把 AI 能力接入业务系统,但不希望团队去学 Python 技术栈的开发者。

通过本文,你将掌握:

  1. Spring AI 2.0 的核心概念和项目结构。
  2. 机票查询 Agent 的工具调用完整实现。
  3. 智能客服 Agent 的 RAG 知识库接入。
  4. 行程助手 Agent 的多轮会话设计。
  5. 结构化输出的实体定义与后端对接。

整个项目以一个可落地的“智能航空助手”为案例,代码可以照搬修改后用于自己的业务系统。

2. 环境准备与项目初始化

2.1 开发环境要求

本文示例以常见的 Java 17 + Spring Boot 3 环境为例。Spring AI 2.0 本身依赖 Spring Framework 6,所以不太建议使用 Spring Boot 2.x,否则会遇到兼容性问题。

组件推荐环境说明
JDK17 或 2117 是当前主流,21 可以配合虚拟线程
Spring Boot3.2+Spring AI 2.0 需要 Boot 3
构建工具Maven 3.8+本文使用 Maven
IDEIntelliJ IDEA也可以使用 Eclipse 或 VSCode
模型 APIOpenAI 兼容接口或通义千问等以实际项目配置为准

不同大模型厂商的接口能力不完全一致,Spring AI 通过ChatModelChatClient抽象屏蔽了底层差异。换句话说,你写的业务代码不需要针对某个模型厂商做特殊处理,换模型时主要修改依赖和配置。

2.2 创建 Maven 工程

你可以直接通过 Spring Initializr 创建项目,也可以在 IDEA 中新建 Spring Boot 项目。项目名建议使用smart-aviation-agent,包名使用com.example.aviation

工程创建完成后,需要注意 Maven 仓库是否能够拉取 Spring AI 依赖。由于 Spring AI 的版本演进较快,建议在pom.xml中显式声明 BOM(Bill of Materials),避免多个依赖版本不一致。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <spring-ai.version>2.0.0</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

这里要特别提醒:Spring AI 不同小版本的 API 有变化。如果你使用的是 2.0 之前的版本,例如 0.8.x 或 1.0.x,ChatClient的链式调用风格可能不同。遇到编译错误时,先检查版本,再对照官方文档调整 API。

2.3 基础配置文件

application.yml中配置模型 API。以下示例以 OpenAI 兼容接口为例,实际项目中需要使用自己的 API Key 和模型名称。

server: port: 8080 spring: application: name: smart-aviation-agent ai: openai: api-key: ${OPENAI_API_KEY:your-api-key} base-url: ${OPENAI_BASE_URL:https://api.openai.com} chat: options: model: gpt-4o-mini temperature: 0.2

如果你使用的是国内模型厂商的 OpenAI 兼容接口,通常只需要修改base-urlmodel即可。为了控制成本,temperature建议设置得低一些,因为客服和机票查询场景更注重准确性,而不是创造性。

3. 整体架构与项目模块划分

3.1 功能拆解

一个完整的智能航空助手,至少包含三类 Agent:

  1. 机票查询 Agent:接收用户自然语言,解析出发地、目的地、日期、舱位等条件,调用航班查询接口,返回符合要求的航班列表。
  2. 智能客服 Agent:回答退改签政策、行李额度、值机时间、特殊旅客服务等问题,知识来源是航空公司的业务文档。
  3. 行程助手 Agent:结合用户已预订的机票,提供行程提醒、中转建议、酒店推荐等个性化服务。

这三类 Agent 在代码层面并不是互相独立的,它们可以共用一套大模型配置与工具注册机制。只是各自关注的领域和调用的工具不同。

3.2 项目目录结构

推荐使用模块化分包,按业务流程组织代码,而不是把所有类都扔在 controller 和 service 两个包下。

smart-aviation-agent ├── src/main/java/com/example/aviation │ ├── AviationApplication.java │ ├── config │ │ └── AiConfig.java │ ├── controller │ │ ├── FlightAgentController.java │ │ ├── CustomerAgentController.java │ │ └── TripAgentController.java │ ├── agent │ │ ├── FlightAgentService.java │ │ ├── CustomerAgentService.java │ │ └── TripAgentService.java │ ├── tool │ │ ├── FlightQueryTool.java │ │ ├── AirportCityTool.java │ │ └── BookingTool.java │ ├── model │ │ ├── FlightInfo.java │ │ ├── AirportInfo.java │ │ ├── FlightQueryRequest.java │ │ └── FlightQueryResult.java │ └── repository │ ├── FlightRepository.java │ └── KnowledgeBaseRepository.java └── src/main/resources ├── application.yml └── data └── flight-data.json

agent包存放核心业务逻辑,tool包存放可被大模型调用的工具类,model包存放实体和 DTO,repository包模拟数据访问层。这样的分层在发展到多个 Agent 时,维护成本会低很多。

4. 机票查询 Agent:Tool Calling 实战

4.1 什么是 Tool Calling

大模型本身并不知道你数据库里有哪些航班,也不知道舱位是否还有余票。Tool Calling 允许大模型在回答用户问题之前,先生成一个调用工具的请求,等工具返回真实数据后,再基于数据组织自然语言回复。

举个例子,用户问:“明天上海到成都的航班有哪些?”模型可能并不直接回答,而是调用queryFlights工具,传入参数from=上海to=成都date=2026-01-15,拿到真实航班列表后,再汇总给用户。

Spring AI 2.0 中,工具方法的实现方式很简单:在普通 Java Bean 的方法上加上@Tool注解,Spring AI 会自动把方法签名、参数说明、方法描述注册给模型。

4.2 定义航班实体

先定义航班实体。为了让示例可运行,这里使用内存 Map 模拟航班数据库。

package com.example.aviation.model; public class FlightInfo { private String flightNo; private String airline; private String fromCity; private String toCity; private String departureTime; private String arrivalTime; private String cabinClass; private double price; private int remainingSeats; public FlightInfo() { } public FlightInfo(String flightNo, String airline, String fromCity, String toCity, String departureTime, String arrivalTime, String cabinClass, double price, int remainingSeats) { this.flightNo = flightNo; this.airline = airline; this.fromCity = fromCity; this.toCity = toCity; this.departureTime = departureTime; this.arrivalTime = arrivalTime; this.cabinClass = cabinClass; this.price = price; this.remainingSeats = remainingSeats; } // 省略 getter/setter }

这里每个字段都有明确的业务含义,remainingSeats是余票数量,cabinClass是舱位类型。

4.3 模拟航班数据库

为了演示,我们使用FlightRepository模拟数据访问层。在实际项目中,这个类应该替换为查询数据库或调用机票系统接口的实现。

package com.example.aviation.repository; import com.example.aviation.model.FlightInfo; import org.springframework.stereotype.Repository; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @Repository public class FlightRepository { private final Map<String, FlightInfo> flightMap = new ConcurrentHashMap<>(); public FlightRepository() { // 模拟几条航班数据 flightMap.put("CA1501", new FlightInfo("CA1501", "中国国航", "北京", "上海", "08:00", "10:20", "经济舱", 1250.00, 20)); flightMap.put("MU5102", new FlightInfo("MU5102", "东方航空", "上海", "成都", "09:30", "12:40", "经济舱", 1480.00, 15)); flightMap.put("CZ3456", new FlightInfo("CZ3456", "南方航空", "广州", "北京", "14:20", "17:30", "公务舱", 3200.00, 5)); flightMap.put("CA1406", new FlightInfo("CA1406", "中国国航", "深圳", "杭州", "16:10", "18:25", "经济舱", 980.00, 8)); } public List<FlightInfo> queryFlights(String fromCity, String toCity, String date) { LocalDate queryDate = LocalDate.parse(date); // 这里没有按日期区分数据,只是演示结构 return flightMap.values().stream() .filter(f -> f.getFromCity().equals(fromCity)) .filter(f -> f.getToCity().equals(toCity)) .collect(Collectors.toList()); } public FlightInfo findByFlightNo(String flightNo) { return flightMap.get(flightNo); } }

注意:这个 Repository 使用ConcurrentHashMap是为了避免并发环境下出现线程安全问题。真实项目中,这里可以接 MyBatis 或 JPA。

4.4 定义航班查询工具

关键的一步来了。我们要把FlightQueryTool暴露给大模型。

package com.example.aviation.tool; import com.example.aviation.model.FlightInfo; import com.example.aviation.repository.FlightRepository; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import java.util.List; @Component public class FlightQueryTool { private final FlightRepository flightRepository; public FlightQueryTool(FlightRepository flightRepository) { this.flightRepository = flightRepository; } @Tool(description = "根据出发城市、到达城市和日期查询航班列表") public List<FlightInfo> queryFlights( @ToolParam(description = "出发城市,例如北京") String fromCity, @ToolParam(description = "到达城市,例如上海") String toCity, @ToolParam(description = "出发日期,格式为 yyyy-MM-dd") String date) throws Exception { List<FlightInfo> result = flightRepository.queryFlights(fromCity, toCity, date); if (result.isEmpty()) { throw new Exception("没有查询到符合条件的航班"); } return result; } }

@Tool注解中的 description 非常关键。大模型会根据这段描述来判断“用户的问题是否应该调用这个工具、参数该如何填充”。描述写得越清晰,Agent 的准确率越高。

这里直接抛出异常也是可以的。Spring AI 2.0 会把异常信息返回给模型,模型会尝试用其他方式处理,比如告诉用户换个日期或城市再查。

4.5 编写机票查询 Agent 服务

核心 Agent 服务如下:

package com.example.aviation.agent; import org.springframework.ai.chat.client.ChatClient; import org.springframework.stereotype.Service; @Service public class FlightAgentService { private final ChatClient chatClient; public FlightAgentService(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem(""" 你是一个航空机票查询助手。你的职责是帮助用户查询航班信息。 请严格遵循以下规则: 1. 当用户询问航班时,必须调用 queryFlights 工具查询真实数据。 2. 如果用户提供的出发地或目的地不是标准城市名称,请先向用户确认。 3. 查询结果需要按照时间先后顺序展示。 4. 如果航班无余票,请如实告知用户。 5. 回复使用中文,简洁友好。 """) .defaultTools(FlightQueryTool.class) .build(); } public String queryFlights(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }

defaultSystem设置的是系统提示词。这里建议把业务规则明确写进去,例如“必须调用工具查询真实数据”“无余票要如实告知”。这能显著减少模型胡编乱造的概率。

defaultTools(FlightQueryTool.class)相当于把工具注册进去。Spring AI 会自动解析工具方法、参数类型和描述,生成 JSON Schema 传给模型。

4.6 Web 接口

package com.example.aviation.controller; import com.example.aviation.agent.FlightAgentService; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/flight") public class FlightAgentController { private final FlightAgentService flightAgentService; public FlightAgentController(FlightAgentService flightAgentService) { this.flightAgentService = flightAgentService; } @PostMapping("/agent") public String query(@RequestBody String userMessage) { return flightAgentService.queryFlights(userMessage); } }

启动项目后,在命令行调用接口:

curl -X POST http://localhost:8080/api/flight/agent \ -H "Content-Type: text/plain" \ -d "明天从上海到成都的航班有哪些?"

期望的输出类似这样:

为您查询到以下航班: 1. MU5102 东方航空 上海 09:30 出发,预计 12:40 到达成都,经济舱票价 1480 元,剩余 15 个座位。

这里要注意,实际返回内容取决于大模型生成结果,不会完全一致,但大致结构相似。

5. 智能客服 Agent:RAG 知识库接入

5.1 智能客服的业务难点

航空客服知识量庞大,包括退改签规则、行李额、特殊旅客服务、延误补偿等,这些内容通常分散在 PDF、Word、网页或内部 Wiki 中。如果只靠提示词,模型无法知道这些具体规则,会出现两种问题:

  1. 直接编造规则,给用户错误承诺。
  2. 模型完全拒绝回答,客服体验差。

RAG(Retrieval-Augmented Generation,检索增强生成)解决的就是这个问题。它的思路是:先把知识文档切分成小块,向量化后存入向量数据库;用户提问时,先从向量数据库中检索出与问题最相关的几段文本,再把它们拼进上下文,让模型基于这些资料回答。

5.2 添加向量化依赖

Spring AI 2.0 支持多种向量数据库,包括 Redis、Milvus、PGVector 等。本文以简单的嵌入式向量存储为例,适合本地开发演示。

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency>

如果本地没有 PostgreSQL,也可以使用SimpleVectorStore,它把向量数据保存在内存中,适合功能演示,但不建议用于生产环境。

5.3 准备客服知识文档

resources/data/目录下创建customer-service.txt

行李额规定: 1. 国内航班经济舱免费托运额度为 20 公斤,公务舱为 30 公斤,头等舱为 40 公斤。 2. 每件托运货物长宽高之和不超过 158 厘米。 3. 锂电池和充电宝严禁托运,必须随身携带,且额定能量不超过 100Wh。 退改签规定: 1. 经济舱折扣票退票手续费为票价的 50%,改期手续费为票价的 30%。 2. 全价经济舱和公务舱在航班起飞前 2 小时可以免费改期一次。 3. 因航空公司原因导致的航班取消,可免费退改签。 值机规定: 1. 国内航班建议提前 2 小时到达机场,起飞前 30 分钟停止办理值机。 2. 网上值机在航班起飞前 24 小时开放。

5.4 实现 RAG 客服 Agent

先编写文档加载和向量存储配置:

package com.example.aviation.config; import org.springframework.ai.reader.TextReader; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; @Configuration public class KnowledgeConfig { @Bean public VectorStore vectorStore() { return SimpleVectorStore.builder().build(); } @Bean public TokenTextSplitter tokenTextSplitter() { return new TokenTextSplitter(200, 100, 5); } }

然后实现客服服务:

package com.example.aviation.agent; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.document.Document; import org.springframework.ai.reader.TextReader; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import java.util.List; @Service public class CustomerAgentService { private final ChatClient chatClient; private final VectorStore vectorStore; private final TokenTextSplitter textSplitter; public CustomerAgentService(ChatClient.Builder builder, VectorStore vectorStore, TokenTextSplitter textSplitter, @Value("classpath:data/customer-service.txt") Resource knowledgeResource) { this.vectorStore = vectorStore; this.textSplitter = textSplitter; // 加载知识文档并向量化 TextReader reader = new TextReader(knowledgeResource); List<Document> documents = reader.get(); List<Document> splitDocuments = textSplitter.apply(documents); this.vectorStore.add(splitDocuments); this.chatClient = builder .defaultSystem(""" 你是航空公司的智能客服助手。请基于提供的航空业务知识回答用户问题。 如果知识库中没有相关信息,请明确告知用户“该问题需要转人工客服处理”,不要编造答案。 """) .build(); } public String answerQuestion(String userQuestion) { return chatClient.prompt() .user(userQuestion) .advisors(a -> a .param("vectorStore", vectorStore) .param("topK", 3)) .call() .content(); } }

topK参数表示从向量库中检索最相似的几个文本片段。设置为 3,意思是每次调用最多取 3 段相关知识拼入上下文。如果知识库文档较多,可以适当调大。

需要提醒的一点是文档加载时机。当前实现是在服务构造时同步加载并向量化文档,对于小规模演示没问题,但在生产环境建议改为独立的数据初始化任务或启动时异步执行,避免拉长应用启动时间。

5.5 RAG 的局限性

RAG 并不是万能的。如果你的知识库文档质量很差、段落切分不合理,或者用户问题表述模糊,检索出来的内容可能不相关。常见优化措施包括:

  • 调整切分窗口大小,避免把一句完整话切到两个片段。
  • 使用混合检索,结合关键词和向量匹配。
  • 为不同业务领域建多个知识库,通过路由选择正确的知识库。

6. 行程助手 Agent:多轮对话与状态管理

6.1 多轮对话需要什么

机票查询属于单轮问答,用户问一句,Agent 答一次。但行程助手要复杂得多。用户可能会说:

“帮我订一张后天去成都的机票,回程时间还没定,先查一下单程。”

这句话隐含了一个事实:这是整个行程规划的开始。用户可能接着问:“到了成都之后,有哪些景点推荐?”如果你不做多轮状态管理,模型根本不知道“到了成都”指的是哪个行程。

Spring AI 2.0 提供了ChatMemory机制,用于保存会话历史。你需要给每次会话分配一个conversationId,后续请求携带同一个 ID,模型就能读到之前的对话内容。

6.2 实现行程会话服务

package com.example.aviation.agent; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.InMemoryChatMemory; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.client.ChatClient.CallResponseSpec; import org.springframework.stereotype.Service; @Service public class TripAgentService { private final ChatClient chatClient; private final ChatMemory chatMemory = new InMemoryChatMemory(); public TripAgentService(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem(""" 你是用户的私人行程助手。你可以帮助用户规划航班、酒店和目的地行程。 请结合对话历史与用户当前问题回答。 """) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); } public String chat(String conversationId, String userMessage) { return chatClient.prompt() .user(new UserMessage(userMessage)) .advisors(a -> a .param("conversationId", conversationId)) .call() .content(); } }

调用接口时,第一次传conversationId=1001,第二次继续传同一个 ID,Agent 就能记得上一次聊到哪里。

curl -X POST http://localhost:8080/api/trip/chat \ -H "Content-Type: application/json" \ -d '{"conversationId":"1001","message":"帮我规划一个后天从北京出发去成都的3天行程"}'

6.3 会话内存设置的注意事项

InMemoryChatMemory把对话历史存在应用内存中,适合单机部署和压测时的临时使用。但在生产环境,建议把历史记录持久化到 Redis 或数据库,否则应用重启后所有会话上下文都会丢失。

另外,对话历史会一直累积,时间长了可能导致 Token 数过高、响应变慢、成本增加。实际项目中需要根据时间和轮次做清理,例如只保留最近 10 轮对话。

6.4 行程数据的业务闭环

只做自然语言对话还不够,行程助手最终要能产生可执行结果。比较稳妥的做法是:Agent 在对话过程中通过工具调用把行程数据写入业务表,同时返回给用户一段可读的行程摘要。这样即使用户关闭页面,行程数据仍然保留在后端。

@Tool(description = "保存用户确认的行程计划") public String saveTrip( @ToolParam(description = "行程编号") String tripId, @ToolParam(description = "出发城市") String fromCity, @ToolParam(description = "目的城市") String toCity, @ToolParam(description = "出发日期") String startDate, @ToolParam(description = "返程日期") String endDate) { // 这里调用业务服务保存行程,并返回行程编号 return "行程保存成功,行程编号为 " + tripId; }

建议行程数据的保存动作由用户确认后触发,不要模型在对话一开始就直接写库,否则容易产生脏数据。

7. 结构化输出:让 Agent 返回实体数据

7.1 为什么需要结构化输出

自然语言回复对用户友好,但对系统不友好。如果后端需要把 Agent 的结果保存到数据库,或者供其他服务调用,更希望直接拿到 JSON 对象。Spring AI 2.0 提供了结构化输出能力,允许把模型的输出映射到普通 Java 实体类。

比如用户问完航班后,前端界面需要展示起飞时间、到达时间、价格、余票数量,这些字段需要严格匹配,不能出现模型随意换字段名。

7.2 定义结构化输出实体类

package com.example.aviation.model; import java.util.List; public class FlightQueryResult { private String summary; private List<FlightInfo> flights; public String getSummary() { return summary; } public void setSummary(String summary) { this.summary = summary; } public List<FlightInfo> getFlights() { return flights; } public void setFlights(List<FlightInfo> flights) { this.flights = flights; } }

summary是给用户看的文字概述,flights是结构化航班列表。这样的结构既方便前端渲染,也方便后端程序直接读取。

7.3 使用entity()方法接收结构化结果

public FlightQueryResult queryFlightsStructured(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .entity(FlightQueryResult.class); }

一行代码就把模型输出绑定到了FlightQueryResult上。Spring AI 底层会为FlightQueryResult生成 JSON Schema,并把模型返回的字符串反序列化成 Java 对象。

7.4 结构化输出的注意事项

实体类必须提供默认构造函数和 getter/setter,否则 Jackson 反序列化会失败。建议字段类型使用包装类型而不是基本类型,例如List<FlightInfo>而不是FlightInfo[],这样能够处理模型返回缺失字段时的空值情况。

另外,如果实体类字段比较多,模型偶尔还是会漏掉某些字段。你可以在系统提示词里强调“必须返回所有字段”,也可以在前端做一次字段完整性校验,缺失时提示用户重新生成。

8. 常见问题与排查思路

8.1 ChatClient 无法注入或编译失败

问题现象常见原因解决思路
ChatClient自动装配失败Spring AI 依赖没有正确引入检查pom.xml,确认 BOM 和 starter 依赖存在
ChatClient.Builder无法注入Spring AI 版本过旧升级到 2.0 或按旧版本 API 编写
编译报错找不到@Tool注解版本不匹配确认 Spring AI 版本,部分旧版本使用@Function注解

8.2 Agent 不调用工具

这是比较常见的问题。模型明明有工具方法,却直接凭记忆编了一个航班。

排查项操作
检查工具描述@Tool中的 description 是否清晰准确地说明了功能和参数
检查模型版本部分小模型工具调用能力较弱,建议换更强模型测试
检查系统提示词是否明确指示模型“必须调用工具”,示例:“当用户询问航班时,必须调用 queryFlights 工具”
查看日志Spring AI 会打印模型请求和响应,确认 tools 参数是否传给了模型

8.3 模型返回格式不符合预期

模型返回的中文描述很完整,但是结构化输出绑定失败。常见原因是:

  1. 实体类字段与模型输出字段名不一致。
  2. 模型返回了多余的 Markdown 代码块标记,例如把 JSON 包在 ```json 中。
  3. 实体类缺少无参构造器。

解决方案是在系统提示词中明确要求“不要输出 Markdown 代码块,直接输出 JSON”,并且使用entity()方法时尽量给模型一个示例输出。

8.4 RAG 检索不到相关知识

知识库已经加载,但是用户提问时 Agent 说不知道。可以从这几方面排查:

  1. 确认文档确实被切分并添加到向量库,打印vectorStore.similaritySearch("行李额")看是否返回结果。
  2. 检查topK是否太小,初始建议 3 到 5。
  3. 检查文档切分是否合理,如果整篇文档被切成几百个很短的小块,检索效果会变差。
  4. 确认用户问题与知识库内容是否属于同一主题。

8.5 内存溢出

如果本地运行大项目时报OutOfMemoryError: insufficient memory,通常是 JVM 堆内存不够。建议修改 JVM 启动参数:

java -Xms512m -Xmx2g -jar smart-aviation-agent.jar

在 IDEA 中,可以在 Run Configuration 的 VM options 里配置-Xmx2g。另外,InMemoryChatMemorySimpleVectorStore都会占用堆内存,生产环境要替换为 Redis 或数据库方案。

9. 最佳实践与工程化建议

9.1 提示词也要做版本管理

很多团队只对代码做 Git 管理,提示词改了就往代码里写。建议把系统提示词、工具描述、RAG 知识文档统一纳入版本管理,最好放到独立的资源目录中,方便评审和回滚。

9.2 给 Agent 建立可观测性

Agent 是一次多步调用,中间状态比普通接口复杂。排查问题时,你不仅需要知道“模型最终返回了什么”,还需要知道“模型调用了哪个工具、传了什么参数、工具返回了什么”。

推荐的日志结构:

[conversationId=1001] User: 上海到成都明天有哪些航班 [conversationId=1001] ToolCall: queryFlights(fromCity=上海, toCity=成都, date=2026-01-15) [conversationId=1001] ToolResult: FlightInfo(flightNo=MU5102, ...) [conversationId=1001] Response: 为您查询到...

Spring AI 也提供了回调机制,可以在ChatClient中注册观察者。生产环境建议把链路 Trace 接入 SkyWalking 或 Jaeger。

9.3 工具方法做好参数校验

工具方法会暴露给大模型,而大模型的参数填充偶尔会出错。例如用户说“明天”,模型可能填2026-01-13,也可能因为时区问题填错日期。工具方法内部必须校验日期格式、城市是否存在、参数是否为空,不要直接把异常抛给模型。

9.4 成本控制与限流

每次调用 Agent 都会消耗 Token。建议做好三层控制:

  1. 设置单次响应最大 Token 数。
  2. 对用户请求做频率限制,例如在网关层或 Controller 层限制每分钟调用次数。
  3. 对长时间运行的任务使用异步处理,避免阻塞 Web 线程。

9.5 安全与合规边界

Agent 涉及用户隐私时,需要特别注意:

  • 不要将用户身份信息拼入未经脱敏的提示词。
  • 客服系统涉及退改签操作前,必须完成实名认证和订单归属校验。
  • 对模型的输出增加敏感信息过滤,防止生成违规内容。
  • 涉及支付、改签、取消等关键操作,Agent 只能生成“建议操作”,最终由用户在前端二次确认后调用正式接口执行。

10. 总结与下一步学习方向

本篇文章围绕 Spring AI 2.0,从零搭建了一个可运行的智能航空助手项目。你掌握了三块核心技能:

  1. 使用@Tool注解实现航班查询工具调用,让大模型能够访问后端真实数据。
  2. 使用 RAG 技术接入航空客服知识库,让 Agent 的回答有依据。
  3. 使用ChatMemoryconversationId实现多轮行程规划与状态管理。
  4. 使用结构化输出把 Agent 结果映射到 Java 实体类,方便系统集成。

如果把这个项目延伸到真实生产,你还需要处理:向量数据库的高可用部署、模型 API 的限流与降级、Agent 链路的监控告警、用户会话数据的持久化。

学习 Spring AI 时,建议按这条路径推进:先掌握 ChatClient 基础 API,再学习 Tool Calling 和 RAG,然后研究 Memory 与 Agent 编排,最后熟悉向量数据库和模型网关的工程化方案。

代码已经放在文中的各个章节,你可以直接创建一个 Spring Boot 项目,把示例代码按目录结构粘贴进去,配置好 API Key 即可运行。如果在实操中遇到问题,欢迎在评论区讨论,也建议你打开 Spring AI 官方文档对照不同版本的 API 差异。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询