Skip to content

九、Spring AI 高级特性与实战 ​

覆盖 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 的方式融入企业应用。

它的核心特性有五方面:

  1. 模型无关的抽象层:代码写一次,切换模型只改配置(OpenAI、Azure、Ollama、国产模型都支持)
  2. Prompt 模板化:用 Spring 的模板引擎管理 Prompt,支持变量注入和复用
  3. 结构化输出:直接把模型输出映射成 Java POJO,不用手写 JSON 解析
  4. Advisor 扩展机制:用 AOP 的思想在模型调用前后插入增强逻辑(RAG、记忆、安全过滤等)
  5. 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);

框架内部会:

  1. 自动生成 JSON schema 塞进 Prompt
  2. 调用模型获取输出
  3. 用 Jackson 把 JSON 转成 POJO
  4. 校验字段完整性,缺字段会自动重试或报错

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 的核心价值:

  1. 模型无关抽象:代码和具体模型解耦,切换模型不改业务代码
  2. 结构化输出:直接映射 POJO,省掉手写 JSON 解析的脏活
  3. Advisor 机制:用 AOP 思想实现 RAG、记忆、安全等横切关注点
  4. 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用途
PdfDocumentReaderPDF 文档(基于 PDFBox)
TextReader纯文本文件
JsonReaderJSON 结构化数据
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作用
QuestionAnswerAdvisorRAG 检索,把相关知识塞进 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 内部流程:

  1. adviseBefore:从 vectorStore 检索相关内容,把 {context} 和 {question} 填充进 Prompt 模板
  2. 调用 LLM 获取回答
  3. 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 → Timing

Advisor 链是责任链模式,Before 按添加顺序执行,After 按逆序执行。

🎯 面试总结 ​

Advisor 是 Spring AI 的横切关注点解耦机制。

核心价值:

  1. 关注点分离:RAG、记忆、安全、日志从业务代码剥离
  2. 可组合:多个 Advisor 链式叠加,灵活配置
  3. 可复用:通用 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 CallingMCP
工具定义每个框架各自实现统一协议,自动发现
通信方式进程内函数调用支持 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());
    }
}

扩展数量怎么定? ​

场景推荐数量原因
简单 FAQ2-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();
    }
}

工具数量多时,建议:

  1. 按业务模块拆分 Tool 类
  2. 用 Bean 名称做命名空间
  3. 工具描述要精确,避免模型选错

工具上下文传递 ​

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 自动处理。

三步使用:

  1. 定义:@Tool + @ToolParam 注解
  2. 注册:ChatClient.defaultTools()
  3. 调用:框架自动处理 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 的做法:

  1. 先粗检索召回一批候选文档
  2. 让模型筛选出与问题真正相关的片段
  3. 只把相关片段塞进最终 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-ReadingRerank
原理用 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 中:

  1. 粗检索召回候选
  2. 用 LLM 判断相关性,筛选相关片段
  3. 只把相关片段塞进 Prompt

与 Rerank 对比:

  • Re-Reading:LLM 判断,精度高但慢
  • Rerank:小模型打分,速度快但精度中等

面试时要能说出 Re-Reading 解决什么问题(噪声过滤)和 实现位置(后检索阶段)。



最后更新2026-04-26
觉得有帮助?把这个链接转给正在求职的朋友 · 用 Ctrl + K 全站搜索其它题