Skip to content

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

生成时间:2026-05-22 21:47


【面试题】1. RAG系统中检索到的文档片段噪声很大,导致大模型生成的回答经常"跑偏",你会怎么优化? ​

【标准答案】这个问题很典型,RAG的核心瓶颈往往不在生成而在检索。我会从三个层面优化:

第一,检索前优化:改进Chunking策略——别用固定长度切分,改用语义切分(按段落/章节边界),chunk大小控制在512 token左右,重叠50-100 token防止关键信息被截断。同时给文档加元数据标签(来源、时间、类别),检索时可以过滤。

第二,检索中优化:把纯向量检索换成混合检索——向量检索抓语义相似,BM25关键词检索抓精确匹配,两者用RRF(Reciprocal Rank Fusion)融合打分。另外引入Reranker(比如bge-reranker或Cohere Rerank),对初筛的top-20文档做精排,只取top-5进LLM。

第三,检索后优化:在Prompt里加一句"如果以下参考资料不足以回答问题,请直接说明",避免模型硬编。还可以做Self-RAG,让模型自己判断检索内容是否相关,不相关就跳过或重新检索。

实际效果上,加Reranker通常是最立竿见影的一步,准确率能提升15%-30%。


【面试题】2. Java中ConcurrentHashMap在JDK 7和JDK 8中的实现有什么区别?为什么JDK 8要改? ​

【标准答案】JDK 7用的是Segment分段锁:把数据分成16个Segment,每个Segment是一把ReentrantLock,相当于16个小的HashMap。并发度由Segment数决定,默认16。

JDK 8改成了CAS + synchronized:取消Segment,直接在Node数组的每个槽位加锁。锁粒度从Segment级别降到单个桶级别,并发度从固定16提升到数组长度。

改的核心原因:

  • 并发度太低:Segment数一旦初始化就不能扩容,16个Segment意味着最多16个线程同时写,高并发场景不够用
  • 内存浪费:Segment本身也是HashEntry数组,两层嵌套结构冗余
  • 查询效率:JDK 7查询需要两次hash定位(先找Segment再找桶),JDK 8只需一次

另外JDK 8还把链表转红黑树的阈值设为8,链表长度超过8且数组长度>=64就树化,查询从O(n)变成O(logn)。

总结就是:锁粒度更细、结构更简洁、查询更快、内存更省。


【面试题】3. 你在项目中用Spring AI搭建了一个对话服务,上线后发现首token响应延迟高达5秒,怎么排查和优化? ​

【标准答案】5秒的首token延迟,对于对话场景不可接受。我按这个思路排查:

第一步,定位瓶颈在哪:用Stopwatch或AOP打点,把整个链路拆开计时——用户请求到达Controller → 检索/上下文拼接 → 调模型API → 收到首token。看时间花在哪。

第二步,逐层优化:

  • 模型API调用:确认用的是streaming模式而非同步模式。同步要等全部生成完才返回,streaming首token通常1-2秒就到。Spring AI里用stream()而不是call()
  • 上下文太长:如果塞了太多历史消息或RAG文档,prompt token数巨大,预处理就慢。做上下文裁剪——只保留最近5轮+摘要,RAG文档top-3够了
  • 网络延迟:如果调海外模型API,光网络RTT就几百毫秒。考虑换国内模型或用代理加速
  • 连接池:HTTP连接复用,别每次请求都建新连接,用WebClient的连接池配置
  • 模型选型:如果是小对话,用轻量模型(如GPT-4o-mini),大模型首token本身就慢

第三步,加缓存:对高频相似问题做语义缓存——把query向量做key,命中就直返,不走模型。Spring AI支持ChatMemory和矢量缓存。

通常streaming+上下文裁剪这两步做完,首token能从5秒压到1-2秒。


【面试题】4. MySQL中有个查询SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID' ORDER BY create_time DESC LIMIT 20,user_id和create_time上各有单独索引,但EXPLAIN显示走了全表扫描,为什么?怎么优化? ​

【标准答案】走了全表扫描,最可能的原因是优化器认为全表扫描成本更低——比如orders表数据量不大,或者user_id的区分度太低(大部分记录user_id都相同),单独走user_id索引再回表反而更慢。

另一个可能是索引选择问题:MySQL一次查询只会选一个索引,它不会同时用user_id索引和create_time索引。如果它选了create_time索引,排好了序但要扫大量数据过滤user_id;选user_id索引,又要在结果上做filesort。两个都不划算,优化器干脆全表扫。

优化方案:建联合索引。关键是列的顺序:(user_id, status, create_time)。

为什么这个顺序:

  • user_id在前做等值过滤,大幅缩小范围
  • status也是等值,进一步缩小
  • create_time放最后,在前面等值过滤后天然有序,避免filesort

这就是最左前缀原则的实际应用——等值条件列在前,排序/范围列在后。

如果status区分度很高(PAID占比小),也可以考虑(status, user_id, create_time),具体看数据分布。用EXPLAIN验证type=ref、Extra里没有Using filesort就对了。


【面试题】5. 什么是AI Agent?和普通的Prompt + LLM调用有什么本质区别?请结合一个实际场景说明。 ​

【标准答案】普通LLM调用是单轮输入输出——你给Prompt,它给回答,结束。模型没有自主行动能力。

AI Agent的核心区别是自主决策 + 工具调用 + 循环执行:

  1. 自主决策:Agent自己判断下一步该做什么,不是人预设流程
  2. 工具调用:Agent可以调用外部工具(搜索、数据库、API),不只靠自身知识
  3. 循环执行:Agent观察执行结果,决定是继续还是结束,形成"感知-决策-行动"循环

实际场景:比如一个客服Agent。普通LLM只能靠训练数据回答"我的订单到哪了";Agent会:①调用订单查询API获取物流信息 → ②发现异常 → ③调用物流系统查询详情 → ④判断需要升级处理 → ⑤调用工单系统创建工单 → ⑥把结果整理回复用户。

整个过程Agent自主规划步骤,用户只问了一句话。这就是ReAct(Reasoning + Acting)模式——先推理"我需要查什么",再行动"调用工具",再根据结果继续推理。

LangChain的Agent、Spring AI的Function Calling都是这个思路的实现框架。


【面试题】6. JVM中年轻代用的是复制算法,为什么不用标记-整理?老年代为什么又不用复制算法? ​

【标准答案】年轻代用复制算法的原因:

  • 年轻代对象"朝生夕死",99%都是垃圾。复制算法只需要复制存活对象,存活少时效率极高
  • 复制算法没有碎片问题,内存整齐,分配快(指针碰撞)
  • HotSpot默认Eden:S0:S1=8:1:1,实际只浪费10%的空间,代价可接受

如果年轻代用标记-整理:每次GC都要扫描整个空间标记、再移动对象、更新引用,大部分对象都是垃圾,等于白忙活。

老年代不用复制算法的原因:

  • 老年代存活率高,用复制算法要复制大量对象,效率很低
  • 而且如果存活率超过50%,8:1:1的空间根本放不下存活对象,需要额外担保机制
  • 老年代对象移动代价大——很多对象被长期引用,移动后更新引用的开销很大

所以老年代用标记-整理(CMS用标记-清除,有碎片问题),虽然要整理但不需要额外空间。

总结:算法选择的核心是根据对象存活特征匹配——存活少用复制,存活多用标记-整理。


【面试题】7. 大模型微调中LoRA的原理是什么?为什么不直接全参数微调?LoRA的rank参数怎么选? ​

【标准答案】LoRA原理:全参数微调要更新模型所有参数(比如7B模型就是70亿参数),LoRA的核心思想是——微调不需要改变所有参数,只需要在原权重矩阵旁加一个低秩分解矩阵。

具体来说:原始权重W是d×d的矩阵,LoRA冻结W,新增两个小矩阵A(d×r)和B(r×d),微调时只训练A和B。r远小于d(比如r=8或16),参数量从d²降到2×d×r,降低了几个数量级。

不全参数微调的原因:

  • 显存不够:7B模型全参微调需要80GB+显存,LoRA只需20GB左右
  • 训练成本:全参微调时间、算力、数据量要求都高得多
  • 灾难性遗忘:全参微调容易忘掉预训练知识,LoRA冻结原权重天然保留
  • 部署灵活:LoRA权重只有几MB到几十MB,可以动态加载切换多个LoRA适配器

rank参数选择:

  • 简单任务(风格调整、格式适配):r=4~8够用
  • 中等任务(领域知识注入):r=16~32
  • 复杂任务(新语言、新能力):r=64~128
  • 实际操作:从小r开始,看Loss曲线,加到收益递减就停。r=16是常见起步值

【面试题】8. Redis分布式锁用SET key value NX EX实现,但在主从切换时可能丢失锁,怎么解决? ​

【标准答案】这题问的是Redis主从异步复制的经典问题:客户端A在Master拿到锁,Master还没把锁数据同步给Slave就挂了,Slave升为新Master后没有锁记录,客户端B也能拿到同一把锁——锁就失效了。

解决方案按可靠性递增:

方案一:RedLock算法。Redis官方作者Antirez提出:用5个独立Redis实例(不同机器),向所有实例加锁,超过半数(3个)加锁成功且总耗时没超过锁过期时间,才算获取成功。释放时向所有实例发释放。缺点是实现复杂、延迟高、有争议(Martin Kleppmann写过文章反对)。

方案二:续约(看门狗机制)。加锁后启动后台线程定时续期,持有锁期间不断延长过期时间。Redisson就是这种实现——默认锁30秒,每10秒(1/3时间)续期一次。主从切换窗口很小,续约能大幅降低风险。

方案三:换ZooKeeper/etcd。ZK基于ZAB协议保证强一致,etcd基于Raft。写锁必须过半节点确认才成功,不存在主从切换丢锁问题。而且ZK支持临时顺序节点,天然实现公平锁和可重入锁。

实际选择:大部分业务场景用Redisson(看门狗+可重入)就够,主从切换概率低、影响可控。金融级强一致要求用ZK/etcd。RedLock用得相对少,复杂度高收益有限。


【面试题】9. 设计一个多模态AI应用:用户上传商品图片,系统自动生成电商详情页文案+SEO关键词+短视频脚本。请描述架构方案。 ​

【标准答案】这是一个典型的多模态+多任务场景,我按模块拆解:

1. 输入处理层

  • 图片上传后先做预处理:压缩、格式统一、去背景(可选)
  • 调用多模态大模型(如GPT-4o/Qwen-VL)做图片理解,提取:商品类别、外观特征、材质、适用场景、目标人群
  • 输出结构化的商品特征JSON

2. 任务编排层

  • 用LangChain/LangGraph做任务DAG编排,三个生成任务可以并行:
    • 文案生成:基于特征JSON + 品牌语料库 + 文案模板,调LLM生成
    • SEO关键词:基于特征JSON + 搜索热词接口 + 竞品分析,调LLM生成
    • 短视频脚本:基于特征JSON + 商品卖点 + 热门脚本结构模板,调LLM生成
  • 每个任务独立的Prompt + Few-shot示例,保证输出格式稳定

3. 质量控制层

  • 每个任务输出后做自动审核:文案查敏感词、关键词查搜索量合理性、脚本查时长匹配
  • 审核不通过则自动重试(最多3次),调整Prompt参数

4. 技术栈选择

  • 后端:Spring Boot + WebFlux(异步非阻塞)
  • AI框架:Spring AI(集成多模型)或LangChain4j
  • 多模态模型:GPT-4o / Qwen2.5-VL
  • 文本模型:Claude / GPT-4o-mini(成本控制)
  • 缓存:相同/相似图片的生成结果缓存,避免重复调用
  • 队列:RabbitMQ解耦上传和生成,支持批量

5. 优化点

  • 图片理解结果缓存,相似图复用
  • A/B测试不同Prompt版本的效果
  • 加入用户反馈闭环,用RLHF优化生成质量

【面试题】10. Java线程池的核心参数有哪些?在实际项目中怎么设置线程池参数?遇到过什么坑? ​

【标准答案】七个核心参数:

  1. corePoolSize:核心线程数,线程池常驻线程
  2. maximumPoolSize:最大线程数,任务多时扩到这个数
  3. keepAliveTime:非核心线程空闲存活时间
  4. unit:存活时间单位
  5. workQueue:任务队列,核心线程满后任务先进队列
  6. threadFactory:线程工厂,自定义线程名(排查问题必备)
  7. rejectedExecutionHandler:拒绝策略,队列和线程都满时怎么处理

参数设置思路:

  • CPU密集型(计算、加密):corePoolSize = CPU核数 + 1
  • IO密集型(网络、数据库):corePoolSize = CPU核数 × 2 或用公式 CPU核数 × (1 + IO等待时间/CPU计算时间)
  • 混合型:拆成两个池,CPU密集一个池,IO密集一个池

实际踩过的坑:

  1. 队列用无界队列:Executors.newFixedThreadPool底层是LinkedBlockingQueue默认Integer.MAX_VALUE,任务堆积不拒绝,OOM。解决:用有界队列,队列长度根据下游承受能力设
  2. 拒绝策略用默认AbortPolicy:直接抛异常,线上任务直接丢了。解决:生产环境用CallerRunsPolicy(让提交线程自己跑)或自定义策略(记录日志+持久化到DB后续重试)
  3. 核心线程不超时:core线程默认永不销毁,池子空闲也占资源。解决:allowCoreThreadTimeOut(true)让核心线程也能超时回收
  4. 线程名没自定义:线程名叫pool-1-thread-1,出问题时jstack根本分不清哪个业务。解决:自定义ThreadFactory,命名如"order-process-{n}"

一句话总结:别用Executors快捷方法,用ThreadPoolExecutor手动建池,队列必须有界,线程名必须自定义。