AI+Java中级开发面试题库
生成时间:2026-05-21 00:22 | 题目数:10
【面试题】1. RAG系统中检索到的文档片段与用户问题语义不匹配,导致大模型产生幻觉回答,你如何优化?
【标准答案】 这是个很典型的RAG痛点,我会从三个层面优化:
第一,检索质量:换用混合检索——向量检索负责语义相似,BM25关键词检索负责精确匹配,两者得分加权融合,能大幅降低"语义近但意思不同"的误召回。另外chunk切分策略也很关键,按语义段落切比固定512 token切效果好,避免关键信息被截断到两个chunk里。
第二,重排序:检索召回top-20后,用cross-encoder重排模型(比如bge-reranker)做精排,取top-5喂给LLM。cross-encoder比bi-encoder的向量检索精度高很多,因为它能同时看到query和document做交互attention。
第三,后处理兜底:让LLM在回答时标注信息来源,如果检索结果里没有支撑证据,prompt里明确要求回答"根据已有信息无法确定",而不是编造。同时可以在生成后做一次自洽性校验——让LLM判断自己的回答是否有检索片段支撑。
实际项目里这三步组合下来,幻觉率能降到可接受水平。
【面试题】2. 说说Java中ReentrantLock和synchronized的区别,什么场景下必须选ReentrantLock?
【标准答案】 核心区别三点:
实现层面:synchronized是JVM层面的内置锁,通过monitorenter/monitorexit字节码指令实现;ReentrantLock是JUC包下的API层面锁,基于AQS实现。
功能层面:ReentrantLock有三个synchronized不具备的能力——
- 可中断获取锁:
lockInterruptibly(),等待锁的过程中可以响应中断,synchronized不行; - 超时获取:
tryLock(timeout),避免死锁,synchronized拿不到就死等; - 公平锁:构造函数传
true,先到先得,synchronized是非公平的。
使用层面:synchronized自动释放锁(代码块结束或异常),ReentrantLock必须手动unlock()且放finally里。
什么场景必须选ReentrantLock?——需要超时获取避免死锁的场景(比如多把锁交叉获取)、需要公平排队的场景(交易系统按顺序处理)、需要Condition精确唤醒的场景(生产者消费者模型用多组Condition替代notifyAll的盲目唤醒)。
如果只是简单互斥,synchronized更简洁,JDK6之后有偏向锁、轻量级锁等优化,性能不差。
【面试题】3. Spring AI中如何实现一个多轮对话的记忆管理?如果对话历史超出上下文窗口怎么处理?
【标准答案】 Spring AI提供了ChatMemory接口来管理对话历史,常用实现是InMemoryChatMemory或持久化到Redis的实现。
基本用法:创建ChatClient时注入ChatMemory,每次调用用.advisors(MessageChatMemoryAdvisor.builder(chatMemory).build()),框架会自动在请求前加载历史消息、请求后保存新消息。
超出上下文窗口的处理策略:
- 滑动窗口:只保留最近N轮对话,
MessageChatMemoryAdvisor支持配置maxMessages,超出的自动丢弃,简单有效但丢失早期上下文; - 摘要压缩:把早期对话用LLM生成摘要,替代原文存入记忆,既保留语义又压缩token。Spring AI可以自定义Advisor实现,在加载历史后、发送前做一轮摘要;
- 混合策略:最近5轮保留原文,更早的做摘要,兼顾精度和长度。
实际项目中我推荐方案3,摘要成本可接受,效果最好。
【面试题】4. MySQL中有一条查询SQL走索引了但还是很慢,你会从哪些方面排查?
【标准答案】 走索引不等于快,我从这几个方向排查:
EXPLAIN看执行计划:确认type是不是ref/range,如果是index(全索引扫描)或all,说明索引没真正起作用。看rows预估扫描行数,如果很大说明索引选择性差。
索引失效的常见原因:查询条件用了函数(
WHERE YEAR(create_time)=2025)、隐式类型转换(varchar字段传了int值)、最左前缀没满足(复合索引(a,b)只查了b)、OR条件中有一列没索引、LIKE左模糊(LIKE '%abc')。回表成本:走二级索引但select的列不在索引里,每行都要回表。扫描1万行就回表1万次。解决方案:覆盖索引,把查询列加入索引,或者用延迟关联(先查主键再回表)。
索引区分度:
SHOW INDEX看Cardinality,如果索引列值重复率高(比如性别字段),优化器可能放弃索引选择全表扫描。这时force index或改查询条件。锁等待:
SHOW PROCESSLIST看是否有锁等待,慢可能是被其他事务阻塞而不是SQL本身慢。数据量问题:表太大,即使走索引,随机IO也多。考虑分区或归档冷数据。
【面试题】5. 设计一个AI Agent的Tool调用架构,让它能调用企业内部多个API,怎么保证安全性和可靠性?
【标准答案】 这是Agent落地的核心问题,我的设计思路:
架构层:
- Tool层抽象为统一接口,每个Tool描述清楚name、description、parameters(JSON Schema),LLM根据描述选择调用哪个Tool;
- Tool Registry做注册中心,所有Tool统一管理,支持动态启用/禁用。
安全性:
- 权限隔离:每个Tool配置最小权限,比如"查询订单"Tool只能调读接口,不能调写接口。Tool执行时用独立的service account,不共享凭证;
- 参数校验:LLM生成的参数必须过JSON Schema校验,防止注入攻击。比如SQL查询Tool,参数只能传where条件片段,不能传完整SQL;
- 审批流:高风险操作(删除、转账)走人工审批,LLM生成操作意图,人确认后执行;
- 审计日志:每次Tool调用记录who/when/what/result,不可篡改。
可靠性:
- 超时+重试:每个Tool设超时时间,失败按指数退避重试,最多3次;
- 熔断:某个API连续失败N次,自动熔断,Agent降级为告知用户"该服务暂不可用";
- 沙箱执行:代码执行类Tool在沙箱中运行,限制网络和文件访问;
- 结果校验:Tool返回结果做格式校验,异常结果不传给LLM,避免污染后续推理。
Spring AI框架下可以用FunctionCallback注册Tool,结合Spring Security做鉴权,比较成熟。
【面试题】6. Java中CompletableFuture的thenCombine和thenCompose有什么区别?实际项目中怎么选?
【标准答案】 两个方法都是组合两个异步任务,区别在于第二个任务的输入:
thenCombine:两个任务并行执行,都完成后把两个结果一起传给合并函数。签名:thenCombine(CompletableFuture<B>, (A, B) -> C)。适用于两个独立任务的结果合并,比如同时查用户信息和订单信息,合并成VO返回。
thenCompose:第一个任务完成后,其结果作为第二个任务的输入,类似flatMap。签名:thenCompose(A -> CompletableFuture<B>)。适用于两个任务有依赖关系,第二个任务需要第一个的结果才能执行,比如先查用户ID,再用ID查用户详情。
简单记:thenCombine是"并行合并",thenCompose是"串行串联"。
项目中的选型原则:如果两个任务无依赖、可并行,用thenCombine减少总耗时;如果有依赖关系,用thenCompose避免阻塞等待。一个常见错误是把thenCompose用成了thenApply(返回值不是CompletableFuture),这会导致嵌套的CompletableFuture<CompletableFuture<>>,要注意。
【面试题】7. 大模型微调中LoRA的原理是什么?什么场景适合用LoRA而不是全量微调?
【标准答案】 LoRA(Low-Rank Adaptation)的核心思想:微调时不改原始权重W,而是加一个低秩分解的增量ΔW = A×B,其中A是d×r,B是r×d,r远小于d。只训练A和B,原始权重冻结不动。
为什么有效?——大模型微调时权重变化矩阵本质上是低秩的,不需要全量参数来拟合。实验证明r=8或16就能达到接近全量微调的效果。
LoRA适合的场景:
- 数据量小(几千到几万条):全量微调容易过拟合,LoRA参数少反而不容易;
- 显存有限:7B模型全量微调需要多卡,LoRA单卡24G就能跑;
- 多任务适配:同一基座模型训多个LoRA,切换时只换LoRA权重,不用重载模型;
- 风格/格式对齐:比如让模型按特定格式输出、特定角色对话,变化不大,LoRA够用。
全量微调更适合:注入大量新知识(比如医疗垂域,需要模型"记住"新的事实)、数据量很大(10万+)、基座模型本身能力不够需要大幅调整。
实际落地建议:先试LoRA,效果不够再升级到全量。大部分业务场景LoRA就够了。
【面试题】8. Redis分布式锁用SET NX实现有什么问题?Redlock算法能解决吗?
【标准答案】 SET key value NX EX seconds实现分布式锁,有三个经典问题:
锁过期但业务没执行完:设置10秒过期,但业务跑了15秒,锁自动释放,其他线程拿到锁,出现并发问题。解决方案:看门狗机制(Redisson的实现),后台线程定期续期,默认每10秒续30秒,业务完成主动释放。
锁误删:线程A的锁过期释放,线程B拿到锁,线程A执行完把B的锁删了。解决方案:value设为唯一标识(UUID),释放时Lua脚本先比较value再删除,保证只删自己的锁。
主从切换导致锁丢失:主节点加锁成功但还没同步到从节点就挂了,从升为主后锁丢失,另一个客户端也能加锁。这是单节点方案的硬伤。
Redlock就是为了解决问题3:向N个独立Redis实例(5个)加锁,超过半数(3个)成功且总耗时没超过锁过期时间才算加锁成功。理论上能解决主从切换问题。
但Redlock有争议:Martin Kleppmann批评它在时钟跳变、进程暂停(GC)场景下不安全,推荐用fencing token方案。Antirez反驳说这些极端场景概率极低。
实际项目中:大多数场景用Redisson的单节点锁+看门狗就够了,对正确性要求极高(金融)的用ZooKeeper或etcd的分布式锁。
【面试题】9. 什么是AI Agent中的ReAct模式?和纯Prompt Chain有什么优劣?
【标准答案】 ReAct是Reasoning + Acting的循环模式:LLM先思考(Thought)→ 决定采取什么行动(Action)→ 观察执行结果(Observation)→ 再思考→再行动,直到得出最终答案。每一步都是LLM自主决策下一步做什么。
和Prompt Chain的对比:
Prompt Chain是预定义固定的执行流程:步骤A的输出喂给步骤B,步骤B的输出喂给步骤C,像流水线。优点是确定性强、可调试、成本低(每步只调一次LLM)。缺点是不灵活,遇到异常路径无法处理。
ReAct的优点:LLM根据中间结果动态调整策略,比如查到一个数据发现不对,可以换一个Tool重试;出错时可以自我纠正。更接近人类的"边想边做"。
ReAct的缺点:token消耗大(每一步都要推理+决策)、延迟高(多轮LLM调用)、可能陷入死循环(反复尝试但无法解决)、可控性差(LLM可能选错Tool)。
选型建议:流程确定用Prompt Chain(比如内容生成流水线:摘要→翻译→润色);需要动态决策用ReAct(比如数据分析Agent:根据数据特征选择不同的分析方法)。实际项目中经常混合使用——主流程用Chain,关键决策点用ReAct。
【面试题】10. Kafka如何保证消息不丢失?从生产者、Broker、消费者三个环节说明。
【标准答案】 消息丢失可能发生在三个环节,逐个解决:
生产者端:
- 设置
acks=all(或-1),要求所有ISR副本确认写入才算成功。默认acks=1只有Leader确认,Leader挂了就丢。 - 设置
retries为较大值(比如Integer.MAX_VALUE),配合max.in.flight.requests.per.connection=1或开启幂等生产者(enable.idempotence=true),保证重试不重复不乱序。 - 使用回调
Callback确认发送结果,失败时做补偿(重试或记录到本地/死信队列)。
Broker端:
min.insync.replicas=2,至少2个副本同步成功才返回ACK,配合acks=all使用。如果只有1个ISR,Leader挂了数据就没了。unclean.leader.election.enable=false,禁止非ISR副本成为Leader,避免数据丢失但牺牲了可用性。- 设置合理的
replication.factor=3,保证副本冗余。
消费者端:
- 关闭自动提交offset:
enable.auto.commit=false,改为业务处理完成后手动提交。 - 采用"处理完再提交"模式:先消费处理,成功后
commitSync(),失败则不提交下次重新消费。 - 注意幂等消费:因为可能重复消费(处理成功但提交前crash),消费逻辑要幂等,比如用唯一ID去重。
三端配合,才能真正实现at-least-once语义。要exactly-once的话,Kafka提供了事务API,但复杂度显著增加,大部分场景at-least-once+幂等消费就够了。