Skip to content

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

生成时间:2026-05-21 00:02

【面试题】1. RAG系统中,向量检索召回的Top-K结果经常出现语义相关但实际无关的"假阳性"文档,如何优化? ​

【标准答案】这是RAG落地最常见的问题。我从三个层面优化:第一,检索阶段——引入混合检索,把向量检索和BM25关键词检索结合,用RRF(Reciprocal Rank Fusion)融合排序,关键词检索能兜底精确匹配场景;第二,重排阶段——在Top-K之后加一个Cross-Encoder重排模型(如bge-reranker),对query和每个候选文档做精细交互打分,比Bi-Encoder的余弦相似度准得多;第三,元数据过滤——在向量库查询时带上metadata filter(比如只查最近一年、指定分类),缩小检索空间。实际项目中,混合检索+重排的组合能把准确率提升20-30%。另外chunk策略也很关键,按语义分块而非固定长度,减少噪声。

【面试题】2. Java中ReentrantLock和synchronized有什么区别?什么场景下必须用ReentrantLock? ​

【标准答案】核心区别三点:第一,可中断——ReentrantLock的lockInterruptibly()可以响应中断,synchronized不行,死锁时synchronized只能等;第二,公平性——ReentrantLock可以构造公平锁(new ReentrantLock(true)),按等待顺序获取,synchronized只有非公平;第三,条件变量——ReentrantLock可以绑定多个Condition,实现精准唤醒(比如生产者消费者用两个Condition),synchronized只有wait/notifyAll,无法精确唤醒。必须用ReentrantLock的场景:需要tryLock带超时避免死锁、需要公平排队、需要多个等待队列做精准唤醒。但synchronized更简单,JVM层面有偏向锁、轻量级锁优化,绝大多数场景够用,不要为了用而用。

【面试题】3. Spring AI中如何实现一个多轮对话的ChatClient?对话历史怎么管理? ​

【标准答案】Spring AI的ChatClient支持多轮对话,核心是管理Message列表。基本做法:维护一个List<Message>,每次用户输入创建UserMessage,模型返回的是AssistantMessage,都追加到列表里,下一轮把整个列表传给ChatClient。对话历史管理:短对话直接放内存(用ThreadLocal或Session绑定);长对话需要裁剪——保留最近N轮或按token数截断,避免超context window。生产级方案:用ChatMemory抽象,Spring AI提供了InMemoryChatMemory和WindowChatMemory(滑动窗口),开箱即用。如果需要持久化,自己实现ChatMemory接口,底层用Redis或数据库存历史。关键是超过上下文长度时要做摘要压缩,把早期对话压缩成一条SystemMessage。

【面试题】4. MySQL中InnoDB的MVCC机制是怎么实现的?ReadView在RR和RC隔离级别下有什么区别? ​

【标准答案】MVCC靠三个东西实现:隐藏列(trx_id事务ID、roll_pointer回滚指针)、undo log(存历史版本链)、ReadView(决定当前事务能看到哪个版本)。ReadView四个关键字段:creator_trx_id(创建者)、m_ids(活跃事务列表)、min_trx_id(活跃最小ID)、max_trx_id(下一个分配ID)。判断可见性:版本trx_id < min_trx_id → 可见;trx_id >= max_trx_id → 不可见;在min和max之间看是否在m_ids里,不在则可见,在则沿版本链往前找。RR和RC的区别:RR级别下ReadView在事务第一次SELECT时创建,整个事务复用同一个ReadView,所以后续其他事务的修改不可见——这就是可重复读;RC级别下每次SELECT都创建新ReadView,所以能看到其他事务已提交的最新值——读已提交。这就是RR下不会出现不可重复读,RC会出现的原因。

【面试题】5. 什么是AI Agent的ReAct模式?和纯Chain-of-Thought有什么本质区别? ​

【标准答案】ReAct = Reasoning + Acting,核心思想是让大模型交替进行"思考"和"行动"。比如用户问"北京今天天气怎么样",模型先思考"我需要查天气API",然后调用工具,拿到结果后再思考"温度25度,需要总结回答",最后输出答案。和CoT的区别:CoT是纯推理链,模型只在脑子里想,不和环境交互,遇到需要外部信息的问题就卡住了;ReAct是推理和行动交织,模型可以调用工具获取真实数据,形成一个观察-思考-行动的循环。实际开发:LangChain的ReAct Agent、Spring AI的ToolCallback都是这个模式。关键配置:设置maxIterations防止死循环,设计好工具描述让模型选对工具,异常处理要兜底(工具报错时告诉模型换个思路)。

【面试题】6. Redis分布式锁用SETNX实现有什么问题?Redlock算法是否解决了所有问题? ​

【标准答案】SETNX的问题:最致命的是锁过期释放但业务没执行完——客户端A拿到锁,执行慢,锁超时自动释放,客户端B又拿到了锁,两个客户端同时操作共享资源。加个value用Lua脚本检查再删(check-and-del)能解决误删,但解决不了锁续期问题。锁续期:需要后台线程定时续期(看门狗机制),Redisson已经实现了。Redlock:在多个独立Redis实例上同时加锁,多数成功才算获取锁。它解决的是单点故障问题,但没解决所有问题:依赖系统时钟同步,时钟跳变会导致锁提前过期;GC暂停也可能导致客户端以为持有锁实际已过期;还有网络分区问题。Martin Kleppmann专门写过文章批评Redlock。我的建议:大多数场景用Redisson的分布式锁+看门狗就够了;对一致性要求极高的场景,用Zookeeper或etcd,它们基于租约和共识协议,比Redlock更可靠。

【面试题】7. 大模型推理时的KV Cache机制是什么?为什么它是推理性能优化的关键? ​

【标准答案】Transformer推理是自回归的,每生成一个token都要对所有历史token做注意力计算。KV Cache就是把每层的Key和Value矩阵缓存下来,新生成token时只需要算新token的Q和已有KV的注意力,不用重复计算历史token的KV。为什么关键:没有KV Cache,生成长度为N的序列,计算量是O(N²);有了KV Cache,每步只需O(N),总计算量降到O(N²)但每步是增量的。实际影响:KV Cache占用大量GPU显存,Llama-2-70B在batch推理时KV Cache能占60%以上显存。优化手段:PagedAttention(vLLM)把KV Cache分页管理减少碎片;MQA/GQA多头共享减少KV存储量;量化KV Cache(FP8/INT8)压缩显存占用。部署大模型时,KV Cache管理是吞吐量优化的第一优先级。

【面试题】8. Spring Cloud中服务熔断和降级有什么区别?Sentinel和Hystrix的核心差异是什么? ​

【标准答案】熔断是保护调用方——当下游服务异常率超阈值时,断路器打开,后续请求直接快速失败,不给下游压力,等一段时间后半开试探恢复。降级是兜底策略——熔断触发后不是简单报错,而是返回兜底数据(如缓存值、默认值、简化结果),保证核心链路可用。简单说:熔断是机制,降级是熔断后的补偿方案。Sentinel vs Hystrix:Hystrix是线程池隔离,每个依赖一个线程池,隔离性好但线程开销大;Sentinel是信号量隔离+滑动窗口统计,轻量级。Hystrix已经停更(维护模式),Sentinel是阿里开源的,持续迭代。Sentinel的优势:支持基于QPS/线程数的流控、热点参数限流、系统自适应保护;控制台实时监控;规则动态推送(Nacos/Apollo)。实践:新项目直接用Sentinel,配合Feign使用,在@FeignClient的fallback指定降级类。

【面试题】9. 向量数据库中HNSW索引的原理是什么?它的查询性能和构建代价如何权衡? ​

【标准答案】HNSW是Hierarchical Navigable Small World,层级可导航小世界图。原理:借鉴了六度分隔理论,构建一个多层图结构——底层包含所有向量,连接密集;上层只包含部分向量,连接稀疏。查询时从最上层开始贪心搜索,找到最近邻后往下层继续搜,逐层精确定位,类似跳表的思路。性能:查询复杂度O(logN),百万级数据毫秒级响应,是目前ANN(近似最近邻)检索中召回率和速度综合最优的算法之一。构建代价:构建时间O(N·logN)·M(M是每层连接数),内存占用较大,因为要存图结构。权衡参数:M(每层最大连接数)越大召回越高但内存越大、构建越慢;efConstruction(构建时搜索宽度)越大图质量越高但构建越慢;efRuntime(查询时搜索宽度)越大召回越高但查询越慢。实践:M一般设16-64,efConstruction设200+保证图质量,efRuntime根据延迟要求调,通常100-200就够了。

【面试题】10. Java中ThreadLocal的内存泄漏问题是怎么产生的?为什么Entry要用弱引用? ​

【标准答案】ThreadLocal的底层是Thread对象里有个ThreadLocalMap,key是ThreadLocal对象的弱引用,value是强引用。泄漏产生:当ThreadLocal对象被置为null,由于key是弱引用,GC会回收key,但value是强引用不会被回收。此时Map里出现了key为null、value还在的Entry,如果线程长期存活(线程池场景),这些孤儿value就永远无法回收,造成内存泄漏。为什么用弱引用:如果key是强引用,ThreadLocal对象置null后,key仍然引用它,ThreadLocal对象本身也泄漏了——弱引用至少保证ThreadLocal对象可以被回收。如何避免泄漏:每次用完ThreadLocal务必调用remove()方法,清理整个Entry。Spring框架中RequestContextHolder就是用ThreadLocal+remove实现的。线程池场景特别注意:线程复用时上一个任务的ThreadLocal数据会污染下一个任务,必须在finally块里remove。阿里规范也明确要求这点。