本轮 Spring AI 面试题库(带答案)
【面试题】1. Spring AI中ChatClient和ChatModel的区别是什么?日常开发应该用哪个?
【标准答案】ChatModel是底层模型接口,直接对接大模型API,只负责发送Prompt拿回Response,是最原始的调用方式。ChatClient是Spring AI 1.0推出的统一入口,基于Fluent API设计,内部封装了ChatModel调用。关键区别在于ChatClient内置了Advisor链式机制,可以在调用前后插入横切逻辑(如记忆、RAG、日志),而ChatModel没有这层抽象。日常开发必须用ChatClient,它不仅简化了调用(链式写法一行搞定),还让你能无缝挂载各种Advisor能力。只有在需要极致轻量或自定义底层行为时才直接用ChatModel。
【面试题】2. Spring AI的Advisor机制是什么?和Spring AOP有什么异同?
【标准答案】Advisor是Spring AI对ChatClient调用链的拦截器抽象,类似于Servlet Filter的链式调用。每个Advisor可以在请求发往模型前(adviseRequest)和响应返回后(adviseResponse)做处理,比如注入上下文、记录日志、转换输出。和Spring AOP的异同:相同点都是横切关注点的解耦,都能做前置后置增强;不同点在于Advisor是面向Chat调用链的,操作的是Prompt和ChatResponse对象,而AOP是面向方法调用的,操作的是方法参数和返回值。Advisor不需要代理类生成,是Spring AI框架内建的轻量管道。典型用法:MessageChatMemoryAdvisor在请求前注入历史消息,QuestionAnswerAdvisor在请求前注入检索到的文档片段。
【面试题】3. Spring AI RAG流程中,TokenTextSplitter和similaritySearch分别起什么作用?如果检索结果相关性很差怎么优化?
【标准答案】TokenTextSplitter负责把长文档按Token数切分成小块(Chunk),是RAG入库前的预处理步骤,保证每块不超过模型的上下文窗口,同时尽量保持语义完整性。similaritySearch是根据查询向量在VectorStore中检索最相似的Top-K文档块,是RAG查询阶段的核心操作。检索结果差时的优化思路:切分策略——调小chunk size、增加overlap,减少语义截断;Embedding模型——换用更贴合业务领域的EmbeddingModel;检索参数——调大Top-K值、降低相似度阈值;查询优化——对用户query做改写或扩展后再检索;元数据过滤——利用VectorStore的filter功能缩小检索范围。最常见的问题是chunk太大导致噪音太多,先从切分粒度调起。
【面试题】4. Spring AI Function Calling中,@Bean注册函数的JSON Schema是怎么自动生成的?有哪些坑?
【标准答案】Spring AI通过反射扫描@Bean注册的Function<Input, Output>类,自动从Input类型的字段上提取描述信息生成JSON Schema,作为tool定义传给大模型。模型看到Schema后就知道该传什么参数,生成tool_call响应。主要坑点:一是Input类必须有无参构造器和标准getter,否则反射失败;二是字段描述默认用字段名推断,中文或缩写名会导致模型理解偏差,建议用@JsonProperty + @Description注解显式标注;三是嵌套对象层级太深会导致Schema膨胀,模型容易出错,Input尽量扁平化;四是Optional字段在Schema中不一定被标记为可选,可能导致模型强行填充无效值,建议用@Schema(required = false)明确标注;五是枚举类型需要@Schema(enumAsRef = true)才能正确生成枚举约束。
【面试题】5. Spring AI多模型切换怎么实现?生产环境如何做模型路由?
【标准答案】配置层面通过spring.ai.openai/chat/options/model或spring.ai.ollama/chat/options/model等属性指定模型,也可以在代码中通过ChatModel Bean的泛型或@Qualifier区分不同模型注入。Spring Boot自动配置会为每个已引入starter的模型创建对应的ChatModel Bean。生产环境的模型路由方案:方案一——用@ConditionalOnProperty做配置驱动的模型选择,不同环境配不同模型;方案二——自定义一个RoutingChatClient,内部根据请求特征(如prompt关键词、用户标签、成本预算)分发到不同ChatModel;方案三——结合Spring Cloud Gateway做网关层路由,请求头带模型标识。关键要统一ChatClient入口,路由逻辑内聚,业务代码无感知。成本敏感场景可以用Ollama本地模型处理简单请求,复杂请求路由到GPT-4。
【面试题】6. Spring AI的Structured Output如何将大模型输出直接映射到Java POJO?有什么限制?
【标准答案】核心方式有两种:BeanOutputConverter——手动创建转换器,指定POJO类,拿到模型输出后调用converter.convert()解析;ChatClient的.entity()方法——Fluent API一行搞定,ChatClient.builder().entity(MyPojo.class).call(),内部自动创建Converter并处理解析。底层原理都是生成JSON Schema约束模型输出格式,然后反序列化。关键限制:一是POJO必须可JSON序列化/反序列化,需要无参构造器和getter/setter;二是模型不一定100%遵守Schema,可能输出非法JSON导致解析异常,必须加try-catch或重试;三是嵌套对象和集合类型支持有限,Map<String, Object>等弱类型结构容易出问题;四是不支持循环引用;五是枚举字段需要模型严格输出枚举值,做不到时建议用String接收再手动转换。生产环境务必加fallback逻辑。
【面试题】7. Spring AI Chat Memory的InMemoryChatMemory为什么不能直接上生产?持久化方案有哪些?
【标准答案】InMemoryChatMemory把对话历史存在JVM内存中,应用重启即丢失,多实例部署时各实例内存隔离、记忆不共享,直接上生产会出大问题。持久化方案:方案一——基于JDBC的ChatMemory实现,把消息存数据库,最成熟可靠;方案二——基于Redis的ChatMemory,利用Redis的List结构存消息,支持TTL自动过期,适合高并发场景;方案三——自定义ChatMemory实现,对接你的存储(MongoDB、Cassandra等),只需实现get/add-clear接口。搭配MessageChatMemoryAdvisor使用,Advisor在每次调用前自动从ChatMemory加载历史注入Prompt。注意要设置对话窗口大小(maxMessages),避免历史消息撑爆Token上限。生产环境推荐数据库方案+定期清理过期会话。
【面试题】8. Spring AI微服务部署中,API Key管理有哪些最佳实践?如何防止泄露?
【标准答案】API Key管理最佳实践:一、外部化配置——绝不硬编码,通过spring.ai.openai.api-key=${OPENAI_API_KEY}引用环境变量或配置中心;二、配置中心统一管控——用Spring Cloud Config或Nacos集中管理Key,支持动态轮换;三、Vault加密存储——敏感Key存HashiCorp Vault,Spring Boot集成自动拉取;四、网关层代理——API Key只在API Gateway层持有,下游服务不直接接触Key,通过内部Token鉴权;五、Key轮换机制——定期更换Key,旧Key设置过渡期;六、审计日志——记录每次Key使用,异常调用频率告警。防止泄露:git提交时用.gitignore排除含Key的配置文件,CI/CD通过安全变量注入而非明文,生产环境禁止把Key打印到日志。
【面试题】9. Spring AI 1.0正式版相比之前Milestone版本有哪些核心变化?升级时要注意什么?
【标准答案】核心变化:一是ChatClient成为统一入口,之前散落在各ChatModel上的调用方式统一收敛到ChatClient的Fluent API;二是Advisor架构正式确立,替代了之前的RequestResponseAdvisor,支持有序链式调用,成为扩展ChatClient行为的标准机制;三是VectorStore接口统一,不同向量库实现统一接口,切换只需换依赖和配置;四是配置前缀统一为spring.ai.,之前不同模块配置不统一的问题解决了;*五是ChatModel的call/stream方法签名规范化。升级注意事项:Advisor接口变了,旧的自定义Advisor要重写;配置前缀改了,application.yml要批量替换;部分废弃API(如旧的PromptTemplate用法)需要迁移;VectorStore的add/delete方法签名可能有调整;建议先在测试环境跑通全流程再升级生产。
【面试题】10. Spring AI的Streaming响应怎么用?和普通同步调用在底层有什么区别?
【标准答案】使用方式:ChatClient调用时用.stream()替代.call(),返回Flux<ChatResponse>,可以用响应式方式逐token消费。代码示例:chatClient.prompt().user("讲个故事").stream().chatResponse().subscribe(resp -> 处理每个token)。底层区别:同步调用是一次性等待完整响应返回,HTTP连接保持到模型生成完毕;Streaming用的是SSE(Server-Sent Events),模型每生成一个token就推送一个事件,客户端逐块接收。优势在于首字延迟极低(TTFT短),用户体验好,适合对话式交互和长文本生成。注意事项:Streaming模式下BeanOutputConverter等结构化输出工具不能直接用,因为需要等完整JSON才能解析;错误处理不同,需要在subscribe的onError中捕获;背压控制需要考虑,消费速度跟不上生成速度时要做限流或缓冲。