本轮 AI+Java中级开发面试题库(带答案)
生成时间:2026-05-20 23:22 | 题目数:10
【面试题】1. RAG系统中,向量检索召回的Top-K结果经常包含不相关文档,如何优化检索质量?
【标准答案】这是个很实际的问题,单纯靠向量相似度排序确实容易引入噪声。我的优化思路分三层:
第一,检索前优化——改进Chunking策略,不要无脑按固定长度切分,改用语义切分(按段落/句子边界),或者用Parent-Child Chunking:小块检索、大块送入LLM,兼顾精度和上下文完整。查询侧做Query Rewriting或HyDE,先生成假设性文档,再拿去检索,比直接用原始query效果好很多。
第二,检索中优化——采用混合检索,向量检索+关键词检索(BM25)加权融合,向量擅长语义匹配,BM25擅长精确词频匹配,互补效果明显。再加一个Reranker(比如BGE-Reranker或Cohere Rerank),对Top-K候选做二次精排,把真正相关的提到前面。
第三,检索后优化——加元数据过滤,比如按时间、部门、文档类型先缩小范围再检索。还可以让LLM对检索结果做相关性判断,过滤掉不相关的再生成答案。
实际项目中,混合检索+Reranker这一组合基本是标配,效果提升非常明显。
【面试题】2. Java中ConcurrentHashMap在JDK 7和JDK 8中的实现有什么区别?为什么JDK 8要改?
【标准答案】核心区别就一句话:JDK 7用Segment分段锁,JDK 8改用CAS+synchronized+Node数组+链表/红黑树。
JDK 7的实现是Segment数组,每个Segment继承ReentrantLock,默认16个Segment,并发度最高16。put操作先定位Segment,再对Segment加锁,然后操作Segment里的HashEntry数组。缺点是Segment数量初始化后不能扩容,并发度固定,而且Segment本身也占内存。
JDK 8砍掉了Segment,直接用Node数组,锁粒度细到每个桶(数组的每个位置)。put时如果桶为空,用CAS写入;如果桶不为空,用synchronized锁住头节点再操作。这样并发度等于数组长度,随扩容动态增长。还有一个重要改进:链表长度超过8转红黑树,查询从O(n)降到O(logn),极端hash冲突场景性能大幅提升。
为什么改?一是Segment分段锁粒度太粗,16个Segment意味着最多16线程并发写入;二是synchronized在JDK 6之后做了大量优化(偏向锁、轻量级锁、自旋锁),性能已经不比ReentrantLock差,而且synchronized是JVM内置的,不需要额外AQS队列开销;三是CAS+空桶无锁写入,大多数场景下根本不走锁路径,吞吐量更高。
【面试题】3. Spring AI和LangChain在AI应用开发中各有什么特点?你会如何选择?
【标准答案】这两个框架定位相似但生态不同。
LangChain是Python生态的,核心优势是生态丰富、组件多、迭代快、社区活跃。它的核心抽象是Chain和Agent,支持大量工具集成(搜索引擎、数据库、API等),文档和案例也多。但缺点是抽象层多、版本迭代太频繁经常Breaking Change、复杂链路调试困难,Python本身在企业级项目中的工程化能力也偏弱。
Spring AI是Java/Spring生态的,核心优势是与Spring Boot深度集成、企业级工程化能力强、安全/监控/配置管理开箱即用。它用ChatClient作为核心抽象,支持多种模型提供商(OpenAI、Azure、Ollama等),还提供了VectorStore抽象、函数调用(Function Calling)、结构化输出等能力。缺点是生态还在成长期,社区和组件数量不如LangChain。
我的选择逻辑:如果是纯AI原型验证、数据处理管道、需要大量AI工具链集成,选LangChain,Python生态更成熟;如果是企业级应用、已有Spring技术栈、需要和业务系统深度集成、关注稳定性和可维护性,选Spring AI,Java的工程化优势在长期项目中更明显。很多团队其实是Spring AI做业务层+Python做模型层,各取所长。
【面试题】4. JVM中Full GC频繁触发,你会怎么排查和优化?
【标准答案】Full GC频繁是线上最常见的问题之一,排查思路很明确:
第一步,看GC日志确认触发原因。常见的有三种:Old区满了、Metaspace满了、System.gc()被显式调用。用-XX:+PrintGCDetails -XX:+PrintGCDateStamps开启日志,或者用jstat -gcutil看各区域使用率。
第二步,根据原因分别处理:
如果是Old区频繁满,大概率是内存泄漏。用jmap -histo:live看哪些对象占比高,再结合jmap -dump导出堆dump,用MAT或VisualVM分析,找出GC Root到泄漏对象的引用链。常见的泄漏场景:ThreadLocal没remove、静态集合只add不remove、缓存没有淘汰策略、监听器没注销等。
如果是Metaspace满,通常是动态代理或反射生成大量类。检查是否有CGLIB动态代理滥用、Groovy脚本热加载等场景,调整-XX:MaxMetaspaceSize。
如果是System.gc()调用,可能是NIO的DirectByteBuffer回收触发的,加-XX:+DisableExplicitGC禁掉,但要注意这会让堆外内存回收出问题,更好的方案是调整DirectByteBuffer使用方式。
优化层面:合理设置堆大小和新生代老年代比例(-Xmn),选择合适的GC器(G1对大堆和延迟敏感场景更友好),确保Survivor区足够大避免对象过早晋升。如果确认是对象确实多不是泄漏,考虑优化数据结构减少内存占用,或者引入堆外缓存。
【面试题】5. 设计一个AI Agent时,如何处理工具调用(Tool/Function Calling)的错误和重试?
【标准答案】AI Agent的工具调用不像传统API调用那么简单,因为中间隔了一个LLM的决策,错误处理要分层设计:
第一层,工具注册时的防御:每个工具的参数Schema要严格定义,类型、范围、必填项都写清楚。LLM生成参数不符合要求时,直接返回结构化错误信息而不是抛异常,让LLM能理解并修正。
第二层,执行时的重试策略:区分可重试错误和不可重试错误。网络超时、限流429、临时服务不可用503属于可重试,用指数退避重试。参数格式错误、权限不足、业务逻辑校验失败属于不可重试,直接把错误信息回传给LLM,让它换一种方式或换一个工具。重试次数上限建议3次,超过就终止当前工具调用。
第三层,Agent循环层的兜底:设置最大工具调用轮次(比如10轮),防止LLM陷入反复重试的死循环。如果连续3次调用同一工具都失败,强制切换策略。还要有全局超时,单个Agent任务不超过合理时间。
第四层,回退方案:关键工具调用失败时要有降级策略,比如搜索工具不可用时切换到知识库检索,外部API不可用时用缓存数据。
实际编码中,Spring AI的Function Calling支持自定义异常处理,LangChain的Tool decorator也能加retry装饰器,但核心逻辑还是要在Agent的编排层做好容错设计。
【面试题】6. MySQL中InnoDB的MVCC机制是如何实现的?它解决了什么问题,又有什么局限?
【标准答案】MVCC是InnoDB实现读写不冲突的核心机制,主要靠三个东西:隐藏列、Undo Log和ReadView。
每行数据有两个隐藏列:trx_id(最后修改该行的事务ID)和roll_pointer(指向Undo Log中的上一版本)。每次更新操作不会覆盖旧数据,而是把旧版本写入Undo Log,新版本写入聚簇索引,通过roll_pointer串成版本链。
ReadView是事务快照读时创建的,包含四个关键字段:creator_trx_id(创建者事务ID)、m_ids(生成时所有活跃事务ID列表)、min_trx_id(活跃事务最小ID)、max_trx_id(下一个待分配事务ID)。判断可见性时:trx_id < min_trx_id → 可见;trx_id >= max_trx_id → 不可见;trx_id在min和max之间且不在m_ids中 → 可见,在m_ids中 → 不可见,沿版本链往前找。
它解决了什么?读不阻塞写、写不阻塞读,极大提升了并发性能。在RC隔离级别下,每次SELECT都生成新ReadView;在RR隔离级别下,整个事务只在第一次SELECT时生成ReadView,所以RR级别下同一事务内多次读结果一致,解决了不可重复读问题。
局限:MVCC只能解决快照读的并发问题,当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)还是需要加锁。MVCC不能完全解决幻读,RR级别下当前读还是可能读到新插入的行,需要Next-Key Lock配合。Undo Log不能无限保留,长事务会导致Undo Log膨胀,影响性能和磁盘空间。
【面试题】7. 提示词工程中,Chain-of-Thought和Few-Shot分别适用于什么场景?可以组合使用吗?
【标准答案】这两个是提示词工程里最核心的技术,适用场景不同但可以组合。
Chain-of-Thought(CoT)适合需要复杂推理的任务——数学计算、逻辑推理、多步决策。核心是在提示中引导模型"一步一步思考",让模型把中间推理过程显式写出来。关键发现是:CoT对小模型提升有限,对大模型(参数量足够)提升显著,因为大模型才有足够的"隐含推理能力"被激发出来。零样本CoT就是加一句"Let's think step by step",少样本CoT是在示例中展示推理过程。
Few-Shot适合任务格式明确但规则不好用语言描述的场景——特定风格的文本生成、格式化输出、分类任务。通过给3-5个示例,让模型从示例中归纳模式。Few-Shot的核心是示例的选择和排序:示例要和当前任务相关,示例之间的标签分布要均衡,最新的示例放最后(recency bias)。
组合使用非常常见且有效:Few-Shot CoT,即每个示例都包含完整的推理过程。比如数学题,给3道例题,每道都展示解题步骤,然后让模型解新题。实验证明Few-Shot CoT效果比单独用CoT或Few-Shot都好,尤其在GSM8K等推理benchmark上提升明显。
实际落地中,我一般这样选:简单分类/格式化 → Few-Shot;复杂推理 → CoT;复杂推理+格式要求 → Few-Shot CoT。还有一个实用技巧:CoT的推理过程可以让模型输出JSON格式,方便程序解析中间步骤。
【面试题】8. Redis分布式锁用Redisson实现时,看门狗机制是怎么工作的?什么情况下会失效?
【标准答案】Redisson的看门狗机制是解决分布式锁续期问题的,非常精巧。
基本原理:Redisson加锁时如果不指定leaseTime,默认锁超时30秒(lockWatchdogTimeout参数),然后启动一个后台线程(看门狗),每隔10秒(超时时间的1/3)检查一次:如果持有锁的客户端还活着且锁还被持有,就自动续期到30秒。这样只要客户端正常运行,锁就不会过期释放。
源码层面的流程:tryLock()时如果加锁成功,会调用renewExpiration()方法,里面用HashedWheelTimer设一个延迟任务,时间到后检查Redis中锁的hash结构是否还存在且持有者还是自己,如果是就pexpire续期,然后递归调用自己安排下一次续期检查。
什么情况会失效?第一,手动指定了leaseTime,Redisson认为你自己管理过期时间,看门狗不会启动,这是最常见的"坑"。第二,Redisson实例被shutdown,看门狗线程跟着死,锁自然过期。第三,应用发生Full GC STW超过锁超时时间,看门狗线程暂停,锁过期后其他客户端获取锁,原客户端GC恢复后可能出现双持锁。第四,Redis主从切换,主节点加锁成功但还没同步到从节点就挂了,从节点升为主没有锁信息,其他客户端可以加锁成功——这就是RedLock要解决的问题。
生产建议:能用leaseTime明确指定的场景就指定,不需要看门狗;对锁可靠性要求极高时用RedLock(多节点加锁,半数以上成功才算获取锁);业务层要做幂等设计,即使锁偶尔失效也不会造成数据不一致。
【面试题】9. 大模型微调中,LoRA的原理是什么?和全量微调相比有什么取舍?
【标准答案】LoRA(Low-Rank Adaptation)的核心思想很简单:大模型参数矩阵W虽然很大,但微调时真正变化的"有效信息"是低秩的,可以用两个小矩阵的乘积来近似。
具体做法:原始权重W ∈ R^(d×k)冻结不更新,新增两个矩阵A ∈ R^(d×r)和B ∈ R^(r×k),其中r远小于d和k(典型值4-64),微调时只训练A和B。前向传播时输出变为Wx + BAx,BA就是低秩增量。微调完成后,可以把BA合并回W,推理时不增加任何计算开销。
和全量微调的取舍:
参数量:全量微调要训练所有参数,LoRA只训练0.1%-1%的参数。一个7B模型全量微调需要28GB+显存,LoRA用8-bit量化可以在单卡24GB上跑。
效果:全量微调在数据充足时效果上限更高,LoRA在大多数下游任务上能达到全量微调90%-95%的效果。但数据分布和预训练差异特别大的场景(比如从通用模型微调到医学领域),LoRA的适应能力不如全量微调。
灵活性:LoRA最大的优势是可以插拔。为不同任务训练不同的LoRA权重,推理时动态切换,一个基座模型服务多个业务。全量微调每个任务要维护一份完整模型副本。
实用建议:先试LoRA,效果不够再逐步增大rank(从8到32到64),还不够考虑全量微调。QLoRA(4-bit量化+LoRA)是目前性价比最高的方案。数据量小于1000条时LoRA更容易过拟合,要注意正则化和学习率设置。
【面试题】10. 设计一个微服务系统,服务A调用服务B,服务B又调用服务C,如何保证整个调用链路的数据一致性?
【标准答案】跨服务调用链路的一致性是分布式系统的经典难题,要根据业务场景选方案:
方案一:可靠消息最终一致性(推荐首选)。服务A执行本地事务后,通过消息队列通知服务B,服务B消费消息执行本地事务后再发消息通知服务C。关键是用事务消息或本地消息表保证"本地事务+消息发送"的原子性。RocketMQ的事务消息原生支持这个模式;如果用Kafka/RabbitMQ,就用工单表模式:A先写业务数据+消息记录到同一数据库,再由定时任务扫描发送。这种方式是最终一致性,延迟通常秒级,大部分业务场景够用。
方案二:TCC(Try-Confirm-Cancel)。每个服务提供三个接口,Try预留资源,Confirm确认执行,Cancel回滚释放。由TCC协调器(如Seata)统一编排。优点是强一致性,能实时回滚;缺点是业务侵入大,每个服务要写三个接口,还要处理空回滚、幂等、悬挂三个问题。适合资金交易等一致性要求极高的场景。
方案三:Saga模式。正向调用各服务,如果某一步失败,反向依次执行补偿操作。适合调用链长、参与服务多的场景。Seata的Saga模式支持状态机编排。补偿操作要幂等,而且补偿本身也可能失败,需要重试+人工介入兜底。
我的实践选择:90%的业务场景用方案一就够了,最终一致性+对账补偿。资金类场景用TCC。超长链路(5个以上服务)用Saga。不管哪种方案,都有几个通用原则:接口必须幂等、必须有超时重试、必须有监控告警、必须有对账机制兜底——没有任何分布式事务方案能保证100%成功,最终要靠对账来发现和修复不一致。