Appearance
九、Spring AI 高级特性与实战
本主题题目目录
点击题号跳转;右侧目录会高亮你正在阅读的题目
301. 什么是 Spring AI 框架?它有哪些核心特性?302. Spring AI 的 ETL 数据处理是怎么实现的?有什么作用?303. 什么是 Spring AI 的 Advisor 机制?它有什么作用?304. 什么是模块化 RAG 架构?Spring AI 中如何实现?305. 什么是 MCP(Model Context Protocol)?如何利用 Spring AI 实现 MCP 客户端和服务端?306. Spring AI 中如何实现多查询扩展和查询重写?307. Spring AI 中如何实现工具调用(Tool Calling)?308. Spring AI 中如何实现 Re-Reading Advisor?
覆盖 Spring AI ETL、模块化RAG、Advisor模式、MCP协议、工具管理等高级特性,共 25 题(301-325)。
301. 什么是 Spring AI 框架?它有哪些核心特性?
👔面试官:说说 Spring AI 是什么?相比直接调 OpenAI 的 API,它解决了什么问题?
🙋♂️我:Spring AI 就是 Spring 官方出的一个调用大模型的 SDK,把 HTTP 请求封装了一下,用起来方便点。
👔面试官:「封装 HTTP」?那跟 RestTemplate 调 API 有什么区别?Spring AI 的 Advisor、ETL、结构化输出这些能力,你直接用 HTTP 客户端能做吗?
🙋♂️我:Advisor 是什么?ETL 不是数据仓库的事吗,怎么跟 AI 扯上关系了?
Spring AI 远不止是「封装 HTTP」,它解决的是 AI 应用开发中的工程化问题。下面我把它的核心设计思想讲清楚。
💡 简要回答
Spring AI 是 Spring 官方推出的 AI 应用开发框架,核心定位是让 AI 能力以 Spring 的方式融入企业应用。
它的核心特性有五方面:
- 模型无关的抽象层:代码写一次,切换模型只改配置(OpenAI、Azure、Ollama、国产模型都支持)
- Prompt 模板化:用 Spring 的模板引擎管理 Prompt,支持变量注入和复用
- 结构化输出:直接把模型输出映射成 Java POJO,不用手写 JSON 解析
- Advisor 扩展机制:用 AOP 的思想在模型调用前后插入增强逻辑(RAG、记忆、安全过滤等)
- ETL 数据管道:内置文档处理、向量化、知识库构建的完整流水线
📝 详细解析
Spring AI 不是「HTTP 封装」,而是「工程化框架」
直接调 OpenAI API 的问题在于:你的业务代码里会充斥大量的字符串拼接、JSON 解析、错误处理,而且模型一换,所有接口都要重写。
Spring AI 的思路是把 AI 调用抽象成 Spring 生态里的一个标准组件,就像 JPA 抽象数据库访问一样。
核心抽象:ChatClient
java
// 定义一次,随处使用
@Autowired
private ChatClient chatClient;
// 调用模型就像调一个普通 Service
String answer = chatClient.prompt("解释什么是 Spring AI").call().content();ChatClient 是模型无关的,底层可以接 OpenAI、Azure、Claude、文心一言,甚至本地 Ollama。切换模型只需要改 application.yml:
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
# 切到 Ollama 本地模型,只需要改这里
ollama:
base-url: http://localhost:11434结构化输出:告别手写 JSON 解析
让模型输出 JSON 不难,难的是把这个 JSON 可靠地映射成 Java 对象。Spring AI 的做法是直接把 POJO 作为输出类型:
java
public record MovieRecommendation(
String title,
String reason,
List<String> similarMovies
) {}
// 直接返回 POJO,框架内部处理格式转换和校验
MovieRecommendation rec = chatClient.prompt()
.user("给我推荐一部科幻电影")
.call()
.entity(MovieRecommendation.class);框架内部会:
- 自动生成 JSON schema 塞进 Prompt
- 调用模型获取输出
- 用 Jackson 把 JSON 转成 POJO
- 校验字段完整性,缺字段会自动重试或报错
Advisor 机制:AI 调用链的 AOP
这是 Spring AI 最有设计感的特性。Advisor 允许你在「调用模型」这个动作的前后插入增强逻辑,而且多个 Advisor 可以链式组合。
java
ChatClient chatClient = ChatClient.builder(model)
// 先查知识库,把相关内容塞进上下文
.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))
// 再加载历史对话记忆
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
// 最后做安全过滤
.defaultAdvisors(new SafeGuardAdvisor())
.build();每个 Advisor 都可以:
- Before:修改发给模型的 Prompt(比如追加 RAG 检索结果)
- After:处理模型的输出(比如日志记录、结果缓存)
这种设计让 RAG、记忆、安全、缓存这些横切关注点从业务代码里彻底解耦。
ETL 数据管道:知识库构建标准化
Spring AI 把 RAG 知识库构建抽象成了标准的 ETL 流程:
java
// ETL Pipeline 的标准写法
VectorStore vectorStore = new SimpleVectorStore(embeddingModel);
new EtlPipeline()
// E - 从 PDF/Word/网页抽取文本
.from(List.of(new PdfDocumentReader("doc.pdf")))
// T - 清洗、切块、加元信息
.transform(new TokenTextSplitter())
// L - 向量化后入库
.writeTo(vectorStore)
.run();这套 API 屏蔽了底层的:PDF 解析、切块策略、Embedding 调用、向量库写入。开发者只需要配置,不需要关心实现细节。
🎯 面试总结
答这道题最容易踩的雷是说 Spring AI 只是「封装 HTTP 调用」。面试官想听到的是你对工程化设计的理解。
Spring AI 的核心价值:
- 模型无关抽象:代码和具体模型解耦,切换模型不改业务代码
- 结构化输出:直接映射 POJO,省掉手写 JSON 解析的脏活
- Advisor 机制:用 AOP 思想实现 RAG、记忆、安全等横切关注点
- ETL 管道:知识库构建标准化,屏蔽底层复杂实现
如果能结合具体场景说「用 Advisor 做 RAG 比手写 Prompt 拼接好在哪里」,会非常有说服力。
302. Spring AI 的 ETL 数据处理是怎么实现的?有什么作用?
👔面试官:Spring AI 的 ETL 是干什么的?和传统数据仓库的 ETL 有什么区别?
🙋♂️我:ETL 就是抽取、转换、加载嘛,把文档处理之后存进向量库,为 RAG 做准备。
👔面试官:具体怎么实现?DocumentReader、DocumentTransformer、DocumentWriter 分别负责什么?怎么自定义切块策略?
🙋♂️我:大概就是读文件、切块、存进去,具体 API 不太熟……
ETL 是 RAG 的「地基」,地基不稳,上面盖再漂亮的楼也容易塌。下面把 Spring AI ETL 的完整机制讲清楚。
💡 简要回答
Spring AI 的 ETL 是专门服务于 RAG 知识库构建的数据流水线,包含三个核心组件:
- DocumentReader:从各种格式(PDF/Word/网页/数据库)抽取原始文档
- DocumentTransformer:清洗、切块、添加元信息
- DocumentWriter:向量化后写入向量数据库
作用是把「原始文档」变成「可被语义检索的向量数据」,而且这个流程是标准化、可扩展的。
📝 详细解析
ETL 三步走:Reader → Transformer → Writer
java
// 完整的 ETL 流程
List<Document> documents = new PdfDocumentReader("manual.pdf").get();
List<Document> transformed = new TokenTextSplitter()
.apply(documents); // 切块
vectorStore.add(transformed); // 写入向量库DocumentReader:多格式文档抽取
Spring AI 内置了多种 Reader:
| Reader | 用途 |
|---|---|
PdfDocumentReader | PDF 文档(基于 PDFBox) |
TextReader | 纯文本文件 |
JsonReader | JSON 结构化数据 |
TikaDocumentReader | 基于 Apache Tika 的万能格式 |
PagePdfDocumentReader | 按页分割的 PDF 读取 |
自定义 Reader 只需要实现 DocumentReader 接口:
java
public class DatabaseDocumentReader implements DocumentReader {
@Override
public List<Document> get() {
// 从数据库查询文档内容
return jdbcTemplate.query("SELECT content FROM docs",
(rs, rowNum) -> new Document(rs.getString("content")));
}
}DocumentTransformer:清洗与切块
这是最影响 RAG 效果的环节。Spring AI 提供了多种 DocumentTransformer:
java
// TokenTextSplitter:按 token 数切块,默认 512 tokens
DocumentTransformer splitter = new TokenTextSplitter(
512, // chunkSize
64, // chunkOverlap(相邻块重叠,防止语义截断)
true // 保留格式标记
);
// KeywordMetadataEnricher:自动提取关键词作为元信息
DocumentTransformer enricher = new KeywordMetadataEnricher(
chatClient, // 用 LLM 提取关键词
5 // 每个 chunk 提取 5 个关键词
);
// 链式组合多个 Transformer
DocumentTransformer pipeline = documents -> {
List<Document> step1 = splitter.apply(documents);
return enricher.apply(step1);
};chunkOverlap 为什么要设置?
考虑这段文字被切成两块:
- 块1:「公司 A 于 2020 年完成了 B 轮融资,首席执行官王某表示……」
- 块2:「将继续深耕企业服务市场,目标是三年内 IPO。」
用户问「王某对公司未来的计划是什么」,「王某」在块1,「未来计划」在块2。设置 chunkOverlap=64 让两块重叠,确保检索时能拿到完整语义。
元信息标注:提升检索精准度
java
// 为每个 chunk 附加元信息
documents.forEach(doc -> doc.getMetadata().putAll(Map.of(
"source", "HR_Policy_2024.pdf",
"department", "HR",
"doc_type", "policy",
"created_date", "2024-01-15"
)));
// 检索时按元信息过滤
vectorStore.similaritySearch(
SearchRequest.query("年假政策")
.withFilterExpression("department == 'HR'")
);元信息过滤能把检索范围缩小 90% 以上,大幅提升精准度。
批量优化:处理大规模文档
java
// 批量 ETL 管道
new EtlPipeline()
.from(new PagePdfDocumentReader("large_manual.pdf"))
.transform(new TokenTextSplitter())
.writeTo(vectorStore, WriteOptions.builder()
.batchSize(100) // 每批写入 100 条
.concurrency(4) // 4 线程并行
.build())
.run();批处理和并发控制能显著加快大规模知识库的构建速度。
🎯 面试总结
Spring AI ETL 的核心设计是把 RAG 知识库构建抽象成标准流水线。
三个关键组件:
- DocumentReader:多格式文档抽取(内置 PDF/Text/JSON,可自定义)
- DocumentTransformer:切块 + 元信息增强(TokenTextSplitter 控制粒度,chunkOverlap 防截断)
- DocumentWriter:批量向量化入库
面试时要能说出 chunkOverlap 的作用(防止跨块语义丢失)和 元信息过滤的价值(精准缩小检索范围),这两个是工程实践经验。
303. 什么是 Spring AI 的 Advisor 机制?它有什么作用?
👔面试官:Spring AI 的 Advisor 是什么?跟 Spring AOP 有什么关系?
🙋♂️我:应该就是 AOP 吧,在调用模型前后做一些增强。
👔面试官:具体有哪些内置 Advisor?QuestionAnswerAdvisor、MessageChatMemoryAdvisor 分别解决什么问题?怎么自定义 Advisor?
🙋♂️我:……具体 API 不太清楚,但知道可以做 RAG。
Advisor 是 Spring AI 最有设计感的特性,不是简单套个 AOP 概念。下面把它的运行机制讲清楚。
💡 简要回答
Advisor 是 Spring AI 的横切关注点处理机制,允许你在调用 LLM 前后插入增强逻辑。
核心作用:
- 解耦关注点:RAG、记忆、安全过滤从业务代码中分离
- 链式组合:多个 Advisor 可以叠加,按顺序执行
- 复用性强:写好的 Advisor 可以在不同业务场景复用
📝 详细解析
Advisor 的核心接口
java
public interface Advisor {
// 在调用模型前执行,可以修改请求
AdvisedRequest adviseBefore(AdvisedRequest request, Map<String, Object> context);
// 在调用模型后执行,可以修改响应
AdvisedResponse adviseAfter(AdvisedResponse response, Map<String, Object> context);
}两个方法分别对应「Before」和「After」增强,和 Spring AOP 的切面概念类似,但语义是专门针对 AI 调用设计的。
内置 Advisor 一览
| Advisor | 作用 |
|---|---|
QuestionAnswerAdvisor | RAG 检索,把相关知识塞进 Prompt |
MessageChatMemoryAdvisor | 对话记忆管理,自动加载和保存历史 |
SafeGuardAdvisor | 内容安全过滤,拦截敏感输入/输出 |
SimpleLoggerAdvisor | 记录请求/响应日志 |
CacheAdvisor | 语义缓存,命中缓存直接返回 |
QuestionAnswerAdvisor:RAG 的 Advisor 实现
java
// 创建 Advisor
QuestionAnswerAdvisor qaAdvisor = new QuestionAnswerAdvisor(
vectorStore, // 向量库
SearchRequest.defaults(), // 默认检索参数
"""
请基于以下上下文回答问题:
{context}
问题:{question}
""" // Prompt 模板,{context} 会被替换为检索结果
);
// 使用 Advisor
ChatClient chatClient = ChatClient.builder(model)
.defaultAdvisors(qaAdvisor)
.build();
// 调用时自动完成:检索 → 拼 Prompt → 调用模型 → 返回答案
String answer = chatClient.prompt("公司的年假政策是什么?").call().content();Advisor 内部流程:
adviseBefore:从vectorStore检索相关内容,把{context}和{question}填充进 Prompt 模板- 调用 LLM 获取回答
adviseAfter:可选地记录日志或缓存结果
MessageChatMemoryAdvisor:对话记忆管理
java
// 基于内存的记忆存储(生产环境应该用 Redis/数据库)
ChatMemory chatMemory = new InMemoryChatMemory();
MessageChatMemoryAdvisor memoryAdvisor = new MessageChatMemoryAdvisor(chatMemory);
ChatClient chatClient = ChatClient.builder(model)
.defaultAdvisors(memoryAdvisor)
.build();
// 第一次对话
String response1 = chatClient.prompt()
.user("我叫张三")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID, "session-001"))
.call().content();
// 第二次对话(自动带上历史)
String response2 = chatClient.prompt()
.user("我叫什么名字?") // 模型能回答「张三」
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID, "session-001"))
.call().content();CHAT_MEMORY_CONVERSATION_ID 用于隔离不同用户的会话,避免记忆串台。
自定义 Advisor:实现日志记录
java
@Component
public class TimingAdvisor implements Advisor {
@Override
public AdvisedRequest adviseBefore(AdvisedRequest request, Map<String, Object> context) {
context.put("startTime", System.currentTimeMillis());
log.info("AI 请求: {}", request.userText());
return request;
}
@Override
public AdvisedResponse adviseAfter(AdvisedResponse response, Map<String, Object> context) {
long duration = System.currentTimeMillis() - (Long) context.get("startTime");
log.info("AI 响应耗时: {}ms", duration);
return response;
}
}Advisor 链的执行顺序
java
ChatClient chatClient = ChatClient.builder(model)
.defaultAdvisors(
new TimingAdvisor(), // 1. 先记录开始时间
new QuestionAnswerAdvisor(...), // 2. 检索知识库
new MessageChatMemoryAdvisor(...), // 3. 加载历史记忆
new SafeGuardAdvisor() // 4. 最后安全检查
)
.build();
// 执行顺序:
// Before: Timing → QA → Memory → SafeGuard
// 调用模型
// After: SafeGuard → Memory → QA → TimingAdvisor 链是责任链模式,Before 按添加顺序执行,After 按逆序执行。
🎯 面试总结
Advisor 是 Spring AI 的横切关注点解耦机制。
核心价值:
- 关注点分离:RAG、记忆、安全、日志从业务代码剥离
- 可组合:多个 Advisor 链式叠加,灵活配置
- 可复用:通用 Advisor 可以在不同项目复用
常见内置 Advisor:
QuestionAnswerAdvisor:RAG 检索增强MessageChatMemoryAdvisor:对话记忆管理SafeGuardAdvisor:内容安全过滤
面试时要能说出 Advisor 的执行顺序(Before 正序、After 逆序)和 多 Advisor 的组合场景(RAG + 记忆 + 安全)。
304. 什么是模块化 RAG 架构?Spring AI 中如何实现?
👔面试官:什么是模块化 RAG?预检索、检索、后检索阶段各自负责什么?
🙋♂️我:模块化就是把 RAG 拆成几个模块吧,预处理、检索、生成这样。
👔面试官:那查询重写、多查询扩展这些属于哪个阶段?Spring AI 的 Advisor 怎么对应到这三个阶段?
🙋♂️我:……具体怎么对应不太清楚。
模块化 RAG 是 Spring AI 提出的重要设计理念,把 RAG 流程拆成可插拔的阶段。下面把这套架构讲清楚。
💡 简要回答
模块化 RAG 把传统 RAG 的「检索→生成」两阶段扩展为**预检索(Pre-retrieval)、检索(Retrieval)、后检索(Post-retrieval)**三个阶段,每个阶段都可以插入不同的增强策略。
Spring AI 中通过 Advisor 链 实现模块化,每个 Advisor 可以挂载到不同阶段:
- 预检索:查询重写、多查询扩展、意图识别
- 检索:向量检索、关键词检索、混合检索
- 后检索:Rerank、上下文压缩、答案验证
📝 详细解析
传统 RAG vs 模块化 RAG
传统 RAG:
用户问题 → 向量化 → 向量检索 → 拼 Prompt → LLM 生成模块化 RAG:
用户问题 → [预检索] → [检索] → [后检索] → LLM 生成
↓ ↓ ↓
查询重写 向量检索 Rerank
多查询扩展 关键词检索 上下文压缩
意图识别 混合检索 答案验证每个阶段都是可插拔的,你可以根据场景选择不同的策略组合。
预检索阶段(Pre-retrieval)
目标:优化查询,提高检索精准度
查询重写(Query Rewriting):用户的原始问题往往口语化、有歧义,先让 LLM 改写成更适合检索的形式。
java
// QueryRewritingAdvisor 实现
public class QueryRewritingAdvisor implements Advisor {
@Override
public AdvisedRequest adviseBefore(AdvisedRequest request, Map<String, Object> context) {
String original = request.userText();
// 让 LLM 重写查询
String rewritten = chatClient.prompt()
.user("请改写以下问题,使其更适合语义检索:" + original)
.call().content();
// 用改写后的查询替换原查询
return AdvisedRequest.from(request).withUserText(rewritten).build();
}
}多查询扩展(Multi-Query Expansion):一个查询可能覆盖不到所有相关内容,让 LLM 生成多个变体查询,分别检索后合并结果。
java
// 生成 3 个变体查询,分别检索后取并集
List<String> queries = generateVariations(originalQuery, 3);
List<Document> allDocs = queries.stream()
.flatMap(q -> vectorStore.similaritySearch(q).stream())
.distinct()
.toList();上下文查询增强:如果是多轮对话,把历史上下文也考虑进去,生成更完整的查询。
检索阶段(Retrieval)
目标:从知识库召回相关文档
Spring AI 默认使用向量检索,但模块化架构支持替换检索策略:
java
// 自定义混合检索 Advisor
public class HybridRetrievalAdvisor extends QuestionAnswerAdvisor {
@Override
protected List<Document> retrieveDocuments(String query) {
// 向量检索 Top-5
List<Document> vectorResults = vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(5)
);
// 关键词检索 Top-5(BM25)
List<Document> keywordResults = keywordSearch(query, 5);
// 合并去重
return mergeAndDeduplicate(vectorResults, keywordResults);
}
}后检索阶段(Post-retrieval)
目标:优化检索结果,提升生成质量
Rerank(重排序):用 Cross-Encoder 对粗排结果重新打分。
java
// 用重排序模型对检索结果重新打分
List<ScoredDocument> reranked = reranker.rerank(query, retrievedDocs, 10);上下文压缩:检索到的文档可能太长,超过 LLM 的上下文限制,需要压缩。
java
// ContextCompressionAdvisor
public class ContextCompressionAdvisor implements Advisor {
@Override
public AdvisedRequest adviseBefore(AdvisedRequest request, Map<String, Object> context) {
List<Document> docs = (List<Document>) context.get("retrieved_documents");
// 如果总 token 数超过限制,做压缩
if (estimateTokens(docs) > MAX_CONTEXT_TOKENS) {
docs = compressDocuments(docs, MAX_CONTEXT_TOKENS);
}
context.put("retrieved_documents", docs);
return request;
}
}答案验证:检查检索到的内容是否足够回答问题,不够的话触发补充检索。
Spring AI 中的完整配置
java
ChatClient chatClient = ChatClient.builder(model)
.defaultAdvisors(
// 预检索阶段
new QueryRewritingAdvisor(chatClient), // 查询重写
new MultiQueryExpansionAdvisor(chatClient), // 多查询扩展
// 检索阶段
new HybridRetrievalAdvisor(vectorStore), // 混合检索
// 后检索阶段
new RerankAdvisor(reranker), // 重排序
new ContextCompressionAdvisor() // 上下文压缩
)
.build();🎯 面试总结
模块化 RAG 是 Spring AI 的重要设计理念。
三个阶段的分工:
- 预检索:查询优化(重写、扩展、意图识别)
- 检索:文档召回(向量、关键词、混合)
- 后检索:结果优化(Rerank、压缩、验证)
Spring AI 通过 Advisor 链 实现模块化,每个 Advisor 挂载到特定阶段。
面试时要能说出 为什么要模块化(不同阶段独立优化、策略可插拔)和 各阶段的典型策略(预检索做查询重写、后检索做 Rerank)。
305. 什么是 MCP(Model Context Protocol)?如何利用 Spring AI 实现 MCP 客户端和服务端?
👔面试官:说说 MCP 是什么?它跟传统的工具调用有什么区别?
🙋♂️我:MCP 就是 Anthropic 提出的一个协议,让模型能调用外部工具。
👔面试官:那 Spring AI 怎么接入 MCP Server?怎么自己开发一个 MCP Server 暴露服务能力?MCP 的架构分几层?
🙋♂️我:……具体实现不太清楚,只知道是个协议。
MCP 是 2024 年底 Anthropic 提出的重要协议,被称为 AI 工具世界的「USB-C 接口」。下面把这套架构和 Spring AI 实现讲清楚。
💡 简要回答
MCP(Model Context Protocol)是一套标准化协议,让 AI 应用能以统一的方式发现和调用外部工具能力。
核心架构分三层:
- Host:用户交互的 AI 应用(如 Claude Desktop、你的 Spring Boot 应用)
- Client:负责与 MCP Server 建立连接、管理通信
- Server:暴露具体能力的服务(如文件系统、数据库、搜索引擎)
Spring AI 从 1.0 版本开始原生支持 MCP,可以同时作为 MCP Client 调用外部服务,也可以作为 MCP Server 暴露自己的能力。
📝 详细解析
为什么需要 MCP?
在没有 MCP 之前,每个 AI 框架接每个工具都要写适配代码。假设有 M 个框架和 N 个工具,就需要 M×N 套适配逻辑。
MCP 的做法是:工具提供方按标准协议暴露能力,AI 应用只要支持 MCP 就能自动发现调用,不需要额外适配。
MCP 的三层架构
┌─────────────────────────────────────────┐
│ Host │
│ (用户交互的 AI 应用,如 Spring Boot) │
├─────────────────────────────────────────┤
│ MCP Client(连接管理) │
│ (Spring AI MCP 模块实现) │
├─────────────────────────────────────────┤
│ MCP Server 1 │ MCP Server 2 │
│ (文件系统服务) │ (数据库服务) │
│ filesystem │ sqlite │
└─────────────────────────────────────────┘Spring AI 作为 MCP Client
java
// 引入 MCP Client 依赖
// spring-ai-mcp-client
@Configuration
public class McpClientConfig {
@Bean
public McpClient mcpClient() {
// 创建基于 Stdio 传输的 MCP Client
return McpClient.builder()
.transport(new StdioClientTransport(
ServerParameters.builder("mcp-server-filesystem")
.args("/Users/demo")
.build()
))
.build();
}
}
// 使用 MCP 工具
@Service
public class FileService {
@Autowired
private McpClient mcpClient;
public String readFile(String path) {
// 通过 MCP 调用文件系统服务
CallToolResult result = mcpClient.callTool(
new CallToolRequest("read_file", Map.of("path", path))
);
return result.content().get(0).text();
}
}Spring AI 作为 MCP Server
java
// 引入 MCP Server 依赖
// spring-ai-mcp-server
@Component
public class WeatherMcpServer implements ToolCallbackProvider {
@Tool(description = "查询指定城市的实时天气")
public WeatherInfo getWeather(
@ToolParam(description = "城市名称,如北京、上海") String city) {
// 调用天气 API 获取数据
return weatherApiClient.query(city);
}
@Override
public List<ToolCallback> getToolCallbacks() {
return List.of(ToolCallback.from(this));
}
}
// 启动 MCP Server
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public McpServer mcpServer(WeatherMcpServer weatherServer) {
return McpServer.builder()
.transport(new StdioServerTransport()) // 或 HTTP/WebSocket
.toolCallbackProvider(weatherServer)
.build();
}
}MCP vs 传统 Function Calling
| 维度 | 传统 Function Calling | MCP |
|---|---|---|
| 工具定义 | 每个框架各自实现 | 统一协议,自动发现 |
| 通信方式 | 进程内函数调用 | 支持 Stdio/SSE/HTTP,可跨进程跨机器 |
| 生态互通 | 框架间不互通 | 任意 MCP Client 可调任意 MCP Server |
| 动态发现 | 需要代码注册 | 运行时发现可用工具 |
生产环境部署 MCP Server
java
// HTTP 传输,适合云端部署
@Bean
public McpServer mcpHttpServer(ToolCallbackProvider provider) {
return McpServer.builder()
.transport(new HttpServerTransport(8080))
.toolCallbackProvider(provider)
.build();
}
// SSE 传输,适合流式场景
@Bean
public McpServer mcpSseServer(ToolCallbackProvider provider) {
return McpServer.builder()
.transport(new SseServerTransport("/mcp"))
.toolCallbackProvider(provider)
.build();
}🎯 面试总结
MCP 的核心定位是 AI 工具的「USB-C 接口」标准。
三层架构:
- Host:用户交互的 AI 应用
- Client:连接管理(Spring AI MCP Client)
- Server:能力暴露(文件系统、数据库等)
Spring AI 支持:
- 作为 MCP Client 调用外部工具(
McpClient.callTool) - 作为 MCP Server 暴露自己的能力(
@Tool注解 +McpServer)
关键优势:统一协议,自动发现,跨框架互通。
面试时要能说出 MCP 解决的核心问题(M×N 适配成本 → 统一协议)和 Spring AI 的双向支持(Client + Server)。
306. Spring AI 中如何实现多查询扩展和查询重写?
👔面试官:RAG 中多查询扩展和查询重写有什么用?在 Spring AI 中怎么实现?
🙋♂️我:多查询扩展就是生成多个查询去检索,查询重写是把用户问题改得更好。用 Advisor 应该能实现。
👔面试官:具体怎么写 Advisor?扩展几个查询合适?重写后的查询怎么评估效果?
🙋♂️我:……具体参数不太确定。
多查询扩展和查询重写是预检索阶段的核心优化手段。下面把实现细节讲清楚。
💡 简要回答
多查询扩展(Multi-Query Expansion):用 LLM 把用户查询生成多个语义变体,分别检索后合并结果,提高召回率。
查询重写(Query Rewriting):用 LLM 把口语化、歧义化的用户问题,改写成更适合向量检索的标准形式。
Spring AI 中通过自定义 Advisor 实现,挂载到预检索阶段。
📝 详细解析
为什么需要查询优化?
用户原始查询的问题:
- 口语化:「这玩意儿怎么用」→ 向量库里有「使用指南」「操作手册」,但没有「这玩意儿」
- 歧义性:「苹果多少钱」→ 是水果还是手机?
- 过于简短:「报错」→ 什么报错?
查询重写解决语义鸿沟,多查询扩展解决召回不足。
查询重写 Advisor 实现
java
@Component
public class QueryRewritingAdvisor implements CallAdvisor {
private final ChatClient chatClient;
@Override
public AdvisedRequest adviseBefore(AdvisedRequest request, Map<String, Object> context) {
String original = request.userText();
String prompt = """
请改写以下用户查询,使其更适合语义检索:
1. 消除歧义,明确指代
2. 补充专业术语
3. 使用正式、标准的表达方式
原查询:%s
改写后:
""".formatted(original);
String rewritten = chatClient.prompt(prompt).call().content();
return AdvisedRequest.from(request)
.withUserText(rewritten)
.build();
}
}多查询扩展 Advisor 实现
java
@Component
public class MultiQueryExpansionAdvisor extends QuestionAnswerAdvisor {
private final ChatClient chatClient;
private final int queryCount; // 扩展几个查询
@Override
protected List<Document> retrieveDocuments(String query) {
// 1. 生成多个变体查询
List<String> queries = generateVariations(query);
// 2. 对每个查询分别检索
List<Document> allDocs = queries.stream()
.flatMap(q -> vectorStore.similaritySearch(
SearchRequest.query(q).withTopK(3)
).stream())
.distinct()
.toList();
// 3. 合并去重后返回
return allDocs;
}
private List<String> generateVariations(String original) {
String prompt = """
从以下用户查询生成 %d 个语义等价但表达方式不同的变体查询,
用于提高向量检索的召回率:
原查询:%s
变体查询(每行一个):
""".formatted(queryCount, original);
String result = chatClient.prompt(prompt).call().content();
// 解析 LLM 输出的多行结果
return Arrays.stream(result.split("\n"))
.filter(s -> !s.isBlank())
.map(String::trim)
.collect(Collectors.toList());
}
}扩展数量怎么定?
| 场景 | 推荐数量 | 原因 |
|---|---|---|
| 简单 FAQ | 2-3 个 | 查询本身明确,少量扩展即可 |
| 复杂技术文档 | 4-5 个 | 同一概念多种表述,需要更多变体 |
| 法律/医疗 | 3-4 个 | 术语精确,扩展过多引入噪声 |
扩展越多召回越全,但检索成本线性增长,需要权衡。
效果评估方法
java
// 对比实验:评估查询优化效果
public void evaluateQueryOptimization() {
List<TestCase> testCases = loadTestCases();
// 基线:原始查询
double baselineHitRate = evaluate(testCases, originalQuery ->
vectorStore.similaritySearch(originalQuery)
);
// 实验组:重写 + 扩展
double optimizedHitRate = evaluate(testCases, originalQuery -> {
String rewritten = rewritingAdvisor.rewrite(originalQuery);
List<String> queries = expansionAdvisor.expand(rewritten);
return mergeResults(queries);
});
System.out.println("基线命中率: " + baselineHitRate);
System.out.println("优化后命中率: " + optimizedHitRate);
}完整配置
java
ChatClient chatClient = ChatClient.builder(model)
.defaultAdvisors(
new QueryRewritingAdvisor(chatClient), // 先重写
new MultiQueryExpansionAdvisor(chatClient, vectorStore, 3), // 再扩展
new QuestionAnswerAdvisor(vectorStore) // 最后检索
)
.build();🎯 面试总结
查询优化是预检索阶段的核心工作。
两种策略:
- 查询重写:消除歧义、补充术语、标准化表达
- 多查询扩展:生成语义变体,提高召回率
实现方式:自定义 Advisor,挂载到预检索阶段。
关键参数:
- 扩展数量:2-5 个为宜,简单场景少点,复杂场景多点
- 重写模板:提示词要明确要求「消除歧义、补充术语、标准化」
面试时要能说出 为什么需要查询优化(口语化 vs 向量库语义鸿沟)和 怎么评估效果(对比命中率)。
307. Spring AI 中如何实现工具调用(Tool Calling)?
👔面试官:Spring AI 里怎么实现工具调用?跟 Function Calling 有什么区别?
🙋♂️我:就是给模型配几个工具,让模型自己决定调哪个。
👔面试官:具体怎么定义工具?模型怎么告诉你要调哪个工具?Spring AI 的工具管理和原生 Function Calling 比有什么优势?
🙋♂️我:……具体 API 不太清楚。
Spring AI 的工具调用机制比原生 Function Calling 更工程化。下面把完整实现讲清楚。
💡 简要回答
Spring AI 的工具调用基于 Function Calling 标准,但提供了更友好的封装:
- 用
@Tool注解定义工具(不需要手写 JSON Schema) - 框架自动生成工具描述和参数定义
- 支持工具回调的自动执行和结果回传
- 与 Spring 生态无缝集成(依赖注入、Bean 管理)
📝 详细解析
定义工具:@Tool 注解
java
@Service
public class WeatherTools {
@Tool(description = "查询指定城市的实时天气,包括气温、天气状况、风向")
public WeatherInfo getWeather(
@ToolParam(description = "城市名称,如北京、上海、广州") String city,
@ToolParam(description = "温度单位,celsius 或 fahrenheit", required = false) String unit) {
// 调用天气 API
return weatherApiClient.query(city, unit);
}
}
public record WeatherInfo(
String city,
double temperature,
String condition,
String windDirection
) {}@Tool 和 @ToolParam 注解让框架自动生成 JSON Schema,不需要手写。
注册工具到 ChatClient
java
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(ChatModel model, WeatherTools weatherTools) {
return ChatClient.builder(model)
.defaultTools(weatherTools) // 注册工具 Bean
.build();
}
}工具调用的完整流程
java
@Service
public class TravelAssistant {
@Autowired
private ChatClient chatClient;
public String planTrip(String destination) {
return chatClient.prompt()
.user("我打算去%s旅游,帮我查一下天气" .formatted(destination))
.call() // Spring AI 自动处理工具调用循环
.content();
}
}
// 执行流程:
// 1. 用户问「我打算去杭州旅游,帮我查一下天气」
// 2. 模型判断需要调用 getWeather 工具,参数 city="杭州"
// 3. Spring AI 自动执行 WeatherTools.getWeather("杭州")
// 4. 结果回传给模型
// 5. 模型生成最终回答「杭州今天晴,25°C,适合出游」工具调用的内部机制
用户提问
↓
LLM 判断:是否需要工具?
↓ 需要
生成 tool_calls JSON
↓
Spring AI 解析 tool_calls
↓
找到对应 Bean 方法,反射调用
↓
获取返回结果
↓
把结果塞进对话上下文
↓
再次调用 LLM
↓
返回最终答案这个过程叫做 Tool Calling Loop,Spring AI 自动完成,开发者只需要写工具逻辑。
多工具管理
java
@Configuration
public class ToolConfig {
@Bean
public ChatClient chatClient(
ChatModel model,
WeatherTools weatherTools,
SearchTools searchTools,
CalculatorTools calculatorTools) {
return ChatClient.builder(model)
.defaultTools(weatherTools, searchTools, calculatorTools)
.build();
}
}工具数量多时,建议:
- 按业务模块拆分 Tool 类
- 用 Bean 名称做命名空间
- 工具描述要精确,避免模型选错
工具上下文传递
java
// 在多轮对话中保持工具状态
ChatClient chatClient = ChatClient.builder(model)
.defaultTools(weatherTools)
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build();
// 第一轮
chatClient.prompt()
.user("杭州天气怎么样?")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID, "session-001"))
.call().content();
// 第二轮(模型记得上下文)
chatClient.prompt()
.user("那边适合穿什么衣服?") // 模型知道「那边」指杭州
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID, "session-001"))
.call().content();🎯 面试总结
Spring AI 工具调用的核心优势:注解驱动,自动 Schema 生成,Loop 自动处理。
三步使用:
- 定义:
@Tool+@ToolParam注解 - 注册:
ChatClient.defaultTools() - 调用:框架自动处理 Tool Calling Loop
与原生 Function Calling 对比:
- 更少的样板代码:不需要手写 JSON Schema
- 更好的 Spring 集成:依赖注入、Bean 管理
- 更简单的多轮对话:配合 Advisor 自动维护上下文
面试时要能说出 Tool Calling Loop 的概念和 @Tool 注解的价值(省掉手写 Schema)。
308. Spring AI 中如何实现 Re-Reading Advisor?
👔面试官:什么是 Re-Reading?怎么用 Spring AI 实现 Re-Reading Advisor?
🙋♂️我:Re-Reading 应该是让模型多读几遍文档?
👔面试官:Re-Reading 的核心是「倒排索引」思想,让模型在回答前重新「阅读」检索到的文档。具体怎么实现?和普通的 RAG 有什么区别?
🙋♂️我:……具体机制不太清楚。
Re-Reading 是一种提升 RAG 效果的技术,下面把原理和实现讲清楚。
💡 简要回答
Re-Reading 是一种文档增强策略,核心思想是:在生成答案前,让模型重新「阅读」检索到的文档,提取与问题最相关的片段。
区别于普通 RAG(直接把检索到的 chunk 塞进 Prompt),Re-Reading 多了一步「相关性筛选」,减少噪声干扰。
Spring AI 中通过自定义 Advisor 实现。
📝 详细解析
为什么需要 Re-Reading?
普通 RAG 的问题:
- 检索召回 Top-5 chunks,可能只有 2-3 个真正相关
- 不相关的 chunk 成为噪声,干扰模型生成
- 上下文窗口被无用信息占满
Re-Reading 的做法:
- 先粗检索召回一批候选文档
- 让模型筛选出与问题真正相关的片段
- 只把相关片段塞进最终 Prompt
Re-Reading Advisor 实现
java
@Component
public class ReReadingAdvisor implements CallAdvisor {
private final ChatClient chatClient;
private final VectorStore vectorStore;
@Override
public AdvisedRequest adviseBefore(AdvisedRequest request, Map<String, Object> context) {
String query = request.userText();
// 1. 粗检索:召回 Top-10 候选
List<Document> candidates = vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(10)
);
// 2. Re-Reading:让模型筛选相关片段
List<Document> relevantDocs = reReadingFilter(query, candidates);
// 3. 只保留相关文档,塞进上下文
String contextStr = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
String enhancedPrompt = """
请基于以下文档回答问题:
%s
问题:%s
""".formatted(contextStr, query);
return AdvisedRequest.from(request)
.withUserText(enhancedPrompt)
.build();
}
private List<Document> reReadingFilter(String query, List<Document> candidates) {
StringBuilder candidateText = new StringBuilder();
for (int i = 0; i < candidates.size(); i++) {
candidateText.append("[").append(i).append("] ")
.append(candidates.get(i).getContent())
.append("\n\n");
}
String prompt = """
以下是与问题相关的候选文档片段。请判断每个片段是否与问题直接相关,
返回相关片段的编号列表(逗号分隔)。
问题:%s
候选片段:
%s
相关片段编号:
""".formatted(query, candidateText);
String result = chatClient.prompt(prompt).call().content();
// 解析返回的编号列表
List<Integer> indices = parseIndices(result);
return indices.stream()
.map(candidates::get)
.collect(Collectors.toList());
}
}Re-Reading vs Rerank
| 维度 | Re-Reading | Rerank |
|---|---|---|
| 原理 | 用 LLM 判断相关性 | 用 Cross-Encoder 打分 |
| 成本 | 多一次 LLM 调用 | 小模型推理,成本低 |
| 精度 | 高(LLM 理解能力强) | 中等 |
| 速度 | 慢 | 快 |
| 适用 | 文档少、精度要求高的场景 | 文档多、需要快速过滤的场景 |
生产环境可以两者结合:先用 Rerank 快速过滤到 Top-5,再用 Re-Reading 精选 Top-3。
完整配置
java
ChatClient chatClient = ChatClient.builder(model)
.defaultAdvisors(
new QuestionAnswerAdvisor(vectorStore), // 粗检索
new ReReadingAdvisor(chatClient, vectorStore), // 精选
new ContextCompressionAdvisor() // 压缩后塞进 Prompt
)
.build();🎯 面试总结
Re-Reading 是 RAG 的后检索阶段优化策略。
核心思想:粗检索 → 精选 → 生成,减少噪声干扰。
实现方式:自定义 Advisor,在 adviseBefore 中:
- 粗检索召回候选
- 用 LLM 判断相关性,筛选相关片段
- 只把相关片段塞进 Prompt
与 Rerank 对比:
- Re-Reading:LLM 判断,精度高但慢
- Rerank:小模型打分,速度快但精度中等
面试时要能说出 Re-Reading 解决什么问题(噪声过滤)和 实现位置(后检索阶段)。