Skip to content

本轮 AI+Java中级开发面试题库(带答案) ​

【面试题】1. RAG系统中,向量检索的相似度阈值如何确定?阈值设高了召回不足,设低了噪声太多,你在实际项目中怎么平衡? ​

【标准答案】没有一个放之四海皆准的阈值,核心思路是"先粗筛后精排"。实际做法分三步:第一步,用无阈值或低阈值(比如余弦相似度0.5)做粗召回,保证召回率;第二步,引入重排模型(如bge-reranker、Cohere Rerank)对粗召回结果做精排,精排模型天然能区分语义相关与噪声;第三步,如果业务对准确率要求极高,在精排后加一道阈值过滤(比如精排分数<0.6的丢弃)。还有一个工程技巧:记录用户反馈(点击/采纳/拒绝),用标注数据反推最优阈值。我之前做智能客服时,粗召回top20,精排取top5,再做阈值过滤,最终有效回答率从68%提升到89%。

【面试题】2. Java中ReentrantLock和synchronized有什么区别?什么场景下你会选择ReentrantLock? ​

【标准答案】核心区别有四点:第一,ReentrantLock是API层面实现(基于AQS),synchronized是JVM层面实现(monitorenter/monitorexit字节码指令);第二,ReentrantLock支持公平锁,synchronized只有非公平锁;第三,ReentrantLock支持可中断获取锁(lockInterruptibly),synchronized不可中断;第四,ReentrantLock支持多条件变量(Condition),synchronized只有wait/notify单条件队列。我选择ReentrantLock的场景:需要公平锁保证顺序性、需要尝试获取锁(tryLock)避免死等、需要多个等待队列做精准唤醒、需要超时获取锁。但synchronized更简单,JDK6之后有偏向锁、轻量级锁等优化,大部分场景够用,不是非要ReentrantLock不可。

【面试题】3. AI Agent中Function Calling和ReAct模式各自的优缺点是什么?你怎么选择? ​

【标准答案】Function Calling是模型原生能力,调用路径短、延迟低、结构化输出好,适合工具调用确定性强、步骤固定的场景,比如查天气、查数据库。缺点是决策链短,复杂推理能力弱,工具一多容易选错。ReAct是"推理-行动-观察"循环,模型先思考再调用工具再观察结果再推理,适合需要多步推理、动态决策的复杂任务,比如研究型问答、代码调试。缺点是token消耗大、延迟高、可能陷入循环。我的选择策略:单步工具调用用Function Calling,多步推理且需要动态调整策略的用ReAct。实际项目中也可以混合使用:外层ReAct做任务规划,内部单步用Function Calling保证效率。

【面试题】4. Spring Boot中@Autowired注入和构造器注入有什么区别?为什么推荐构造器注入? ​

【标准答案】三种注入方式:字段注入(@Autowired加在字段上)、Setter注入、构造器注入。字段注入最简洁但有三个硬伤:一是无法声明不可变性(字段不能final),二是无法在构造时做校验(依赖可能为null),三是和Spring容器强耦合,脱离容器无法使用。构造器注入的优势:第一,依赖可以是final的,保证不可变性和线程安全;第二,构造时就能发现依赖缺失,fail-fast;第三,不需要反射,符合Java约定;第四,Spring官方推荐,当只有一个构造器时甚至不用加@Autowired。实际项目中我统一用构造器注入,配合Lombok的@AllArgsConstructor简化代码。

【面试题】5. 大模型微调中,LoRA和全量微调各适用什么场景?LoRA的rank值怎么选? ​

【标准答案】全量微调更新所有参数,效果最好但显存需求巨大(7B模型至少需要60GB+),适合数据充足、任务与预训练差异大的场景。LoRA冻结原模型,只训练低秩分解矩阵(A和B),显存只需全量的1/3到1/5,适合数据量有限、任务和预训练领域相近的场景。rank值选择:一般从8或16开始,rank越大表达能力越强但越容易过拟合。经验上:简单任务(风格适配)rank=4~8,中等任务(领域知识注入)rank=16~32,复杂任务(多能力增强)rank=64~128。还有一个技巧:不同层用不同rank,比如注意力层rank高一些、MLP层rank低一些,在效果和效率间取平衡。

【面试题】6. MySQL中为什么InnoDB的主键推荐用自增ID而不是UUID?底层原理是什么? ​

【标准答案】核心原因是B+树页分裂问题。InnoDB使用聚簇索引,数据按主键顺序存储在B+树叶子节点。自增ID是顺序插入的,每次只需要在最后追加,页的填充率高。UUID是无序的,每次插入的位置随机,会导致频繁的页分裂:原本满的页要拆成两半,大量数据移动,同时产生页碎片,导致空间浪费和IO放大。实际影响:自增ID插入性能是UUID的3~5倍,且索引更紧凑,缓存命中率更高。另外UUID占36字节(字符串)或16字节(binary),自增ID只占4~8字节,二级索引存储主键值,主键越大二级索引也越大,又进一步影响性能。所以除非有分布式ID的强需求,否则优先自增ID。

【面试题】7. 你在项目中用Spring AI开发AI应用时,如何处理大模型输出不稳定(同样的问题不同次调用结果差异大)的问题? ​

【标准答案】大模型输出不稳定是特性不是bug,工程上从三个层面应对:第一,参数层面——降低temperature(0.1~0.3适合事实性问答)、设置top_p、固定seed(部分模型支持)来减少随机性;第二,Prompt层面——用结构化Prompt(指定输出格式、给few-shot示例、加约束条件如"必须基于给定信息回答,不确定的说不确定"),约束越明确输出越稳定;第三,工程层面——加输出校验和重试机制,用JSON Schema校验输出格式,格式不对就重试(最多3次);对关键场景加人工审核兜底。还有一种做法是"多路调用+投票":同一个问题调用3次取多数一致的结果,牺牲延迟换稳定性。

【面试题】8. Java中ThreadLocal的原理是什么?为什么会有内存泄漏问题?在线程池场景下怎么正确使用? ​

【标准答案】ThreadLocal的原理:每个Thread对象内部有一个ThreadLocalMap,key是ThreadLocal对象的弱引用,value是存储的值(强引用)。获取值时先拿到当前线程的ThreadLocalMap,再以ThreadLocal对象为key查value。内存泄漏原因:ThreadLocal被GC后,key变成null,但value是强引用不会被回收。如果线程一直存活(线程池场景),这些null key的value就永远无法回收,造成内存泄漏。线程池下的正确用法:第一,必须在finally中调用remove()清理,用try-finally包裹;第二,如果用Spring的RequestContextHolder这类框架自带的ThreadLocal,框架会自动清理;第三,可以用InheritableThreadLocal实现父子线程传值,但线程池下要用TransmittableThreadLocal(阿里开源)替代,因为线程池会复用线程,InheritableThreadLocal只在创建时继承一次。

【面试题】9. 设计一个支持多轮对话的RAG系统,用户问"它支持哪些功能?"这种省略主语的追问,你如何让系统理解上下文? ​

【标准答案】这是多轮对话中的指代消解问题。方案分三层:第一层,对话历史拼接——把最近N轮对话拼进Prompt,让大模型自己理解上下文,简单但token消耗大,超出窗口长度就失效;第二层,查询改写(Query Rewrite)——在检索前用一个LLM把模糊问题改写为独立完整的问题,比如"它支持哪些功能?"→"iPhone 15 Pro支持哪些功能?"改写后再做向量检索,精度大幅提升;第三层,对话状态跟踪——维护一个结构化的对话状态(当前实体、已讨论的属性等),新问题结合状态做意图理解。实际项目中我推荐第二层方案,性价比最高:用小模型做Query Rewrite,延迟可控,效果明显。另外还有一个工程细节:历史对话不能无限拼接,要有滑动窗口或摘要压缩策略。

【面试题】10. Redis的Cluster模式和Sentinel模式有什么区别?各自适用什么场景? ​

【标准答案】Sentinel模式是主从架构的高可用方案,核心功能是自动故障转移——Sentinel监控主节点,主节点挂了自动把从节点升为主节点。但数据容量受单机内存限制,写能力也受单节点限制。Cluster模式是分布式方案,数据按slot(16384个)分片到多个主节点,每个主节点负责一部分slot,支持水平扩展。Cluster也有故障转移能力,每个主节点可以有从节点。选择标准:数据量小(单机能存下)、QPS不高但需要高可用——用Sentinel,简单可靠;数据量大需要分片、写QPS高需要分散到多节点——用Cluster。注意Cluster的限制:不支持跨slot的多key操作(mget、事务等),需要用Hash Tag({tag})把相关key映射到同一slot。实际项目中,中小规模用Sentinel+读写分离就够了,大规模才需要Cluster。