Skip to content

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

生成时间:2026-05-21 00:12 | 共10题(AI 5题 + Java 5题)


【面试题】1. RAG系统中检索到的文档片段与用户问题相关性不高时,如何优化检索质量? ​

【标准答案】这个问题本质是检索精度不够,我从三个层面优化:

检索前:优化分块策略——不用固定长度切分,改按语义边界(段落、章节)切分,chunk size 根据业务场景调整,一般500-1000 token;同时丰富元数据(来源、时间、类别),检索时用 metadata filter 缩小范围。

检索中:用混合检索——向量检索抓语义相似,关键词检索(BM25)抓精确匹配,两者结果做 RRF(Reciprocal Rank Fusion)融合排序。这比纯向量检索效果好很多,尤其是专业术语场景。

检索后:加重排序(Reranker),比如用 bge-reranker 或 Cohere Rerank 对初筛结果做二次精排;同时在 prompt 里让 LLM 自行判断上下文相关性,低质量片段直接忽略。

实际项目里,上线混合检索 + Reranker 后,回答准确率从 72% 提升到 89%,效果非常明显。


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

【标准答案】Spring AI 的 ChatClient 本身是无状态的,多轮对话需要自己管理对话历史。核心思路:

对话历史存储:用 ChatMemory 接口,Spring AI 提供了 InMemoryChatMemory(开发用)和 MessageChatMemoryAdvisor。生产环境我会自己实现 ChatMemory,底层用 Redis 或数据库,按 sessionId 存储消息列表。

关键代码结构:

java
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
    .build();

// 每次调用传入 conversationId
String response = chatClient.prompt()
    .user("用户问题")
    .advisors(a -> a.param("chat_memory_conversation_id", conversationId))
    .call()
    .content();

Token 上限处理:对话历史不能无限增长,需要做滑动窗口或摘要压缩。我一般保留最近10轮,超过就截断最旧的;或者用 LLM 对旧对话做摘要替换,保留关键上下文。

分布式场景:sessionId 绑定用户,Redis 集群存储,保证多实例都能读到历史。


【面试题】3. AI Agent 中 ReAct 模式和 Function Calling 模式有什么区别?各适合什么场景? ​

【标准答案】这两个是 Agent 执行动作的两种核心范式:

ReAct(Reasoning + Acting):模型先"思考"再"行动"再"观察",循环往复。模型自己决定调用什么工具、怎么组合。特点是灵活度高、但 token 消耗大,因为每一步都要生成推理链。

Function Calling:模型直接输出结构化的工具调用参数,由框架执行后返回结果。特点是效率高、调用精准,但工具组合能力弱,本质是单步映射。

选型建议:

  • 复杂多步推理、需要动态规划路径 → ReAct,比如"调研竞品并生成分析报告"
  • 明确的单步工具调用 → Function Calling,比如"查天气""查库存"
  • 实际项目中经常混用:简单操作用 Function Calling 快速完成,复杂链路降级到 ReAct

落地经验:用 LangChain 的 Agent 时,大部分场景 ReAct 效果更好,但成本是3-5倍。我一般先用 Function Calling 试,准确率不够再升级 ReAct,加个 token 预算阈值自动切换。


【面试题】4. 向量数据库选型时,Milvus、Pinecone、Weaviate 各有什么优劣? ​

【标准答案】这三款是主流向量数据库,我从四个维度对比:

Milvus:开源、自托管,支持大规模数据(十亿级),架构上分 Standalone 和 Cluster 模式。优势是性能强、可定制性高、社区活跃;劣势是运维复杂,依赖 etcd/MinIO 等组件。适合对数据安全要求高、有运维能力的团队。

Pinecone:全托管 SaaS,零运维,API 简洁。优势是上手极快、免运维;劣势是数据在第三方、成本随规模上升快、定制性差。适合初创团队快速验证、数据不敏感的场景。

Weaviate:开源、支持混合检索(向量+关键词),内置多种向量化和 RAG 模块。优势是功能丰富、开箱即用;劣势是超大规模下性能不如 Milvus。适合中等规模、需要混合检索的场景。

我的选型逻辑:数据量 <千万且要快速上线 → Pinecone;需要混合检索+中等规模 → Weaviate;数据量千万级以上+自建 → Milvus。实际项目中我选了 Milvus,因为数据量在5000万条,且要求私有化部署。


【面试题】5. 大模型微调时 LoRA 和全量微调(Full Fine-tuning)怎么选?LoRA 的原理是什么? ​

【标准答案】LoRA 原理:核心思想是冻结原始模型权重,在 Transformer 的注意力层旁路插入低秩矩阵 A 和 B,其中 A 是降维矩阵(d×r),B 是升维矩阵(r×d),r 远小于 d。训练只更新 A 和 B,参数量通常是原模型的 0.1%-1%。

$$\Delta W = A \times B$$

选型对比:

维度LoRA全量微调
显存原模型的 1/3-1/2需要完整模型参数
训练速度快 2-3 倍慢
效果大部分场景接近全量理论最优
适用场景领域适配、风格调整底层能力根本性改变

实际选择:

  • 任务是"让模型更懂某个领域"(如医疗问答、法律咨询)→ LoRA 足够
  • 任务是"改变模型的基本能力"(如学会新语言、全新推理范式)→ 考虑全量微调
  • 资源有限 → 只能选 LoRA

落地技巧:LoRA 的 rank 参数一般 8-64,rank 越高效果越好但显存越多;我一般从 r=16 开始试。另外 QLoRA(4-bit 量化 + LoRA)可以在单张 3090 上微调 7B 模型,性价比极高。


【面试题】6. Java 中 ConcurrentHashMap 在 JDK 1.7 和 1.8 的实现有什么区别?为什么 1.8 改用红黑树? ​

【标准答案】JDK 1.7:分段锁(Segment),整个 Map 分成 16 个 Segment,每个 Segment 是一个小的 HashMap,加 ReentrantLock。不同 Segment 可以并发操作,理论上最大并发度就是 Segment 数。

JDK 1.8:取消了 Segment,改用 CAS + synchronized(锁桶的头节点)。数据结构和 HashMap 1.8 一致,数组 + 链表 + 红黑树。

为什么改?

  1. 锁粒度更细:1.7 锁的是 Segment(包含多个桶),1.8 锁的是单个桶,并发度从 16 提升到桶的数量
  2. 查询效率:1.7 的 Segment 内是链表,hash 冲突严重时遍历 O(n);1.8 链表长度超 8 自动转红黑树,查询 O(log n)
  3. CAS 减少锁开销:写操作先尝试 CAS,失败才加 synchronized,大部分情况下无锁

红黑树的好处:极端场景下(hash 冲突非常多),链表遍历从 O(n) 降到 O(log n)。8 个节点时链表和红黑树查找效率差不多,所以阈值选 8;低于 6 又退回链表,因为树节点占内存更多。

面试加分点:size() 方法 1.8 用了 LongAdder 思路,分 baseValue + CounterCell,减少竞争。


【面试题】7. MySQL 中 InnoDB 的 MVCC 机制是如何实现的?RR 隔离级别下会出现幻读吗? ​

【标准答案】MVCC 核心实现:每行数据有两个隐藏列——trx_id(最后修改的事务ID)和 roll_pointer(指向 undo log 的指针)。读操作通过 ReadView 决定可见性。

ReadView 四个关键字段:

  • m_ids:生成 ReadView 时所有活跃事务ID列表
  • min_trx_id:m_ids 中的最小值
  • max_trx_id:下一个将分配的事务ID
  • creator_trx_id:创建该 ReadView 的事务ID

可见性判断:trx_id < min_trx_id → 可见;trx_id >= max_trx_id → 不可见;trx_id in m_ids → 不可见;否则可见。不可见就沿 undo log 链找更早版本。

RR 下会幻读吗?

  • 普通 SELECT:不会,因为 RR 级别整个事务只生成一次 ReadView,快照读始终一致
  • 当前读(SELECT ... FOR UPDATE / INSERT / UPDATE):会!当前读不走 MVCC,直接读最新数据,别的事务新插入的行能看到

解决方案:当前读 + Next-Key Lock(记录锁 + 间隙锁),锁住范围内的间隙,阻止其他事务插入新行,从而避免幻读。

面试关键点:MVCC 解决的是快照读的一致性,幻读问题的完整解决靠的是 MVCC + Next-Key Lock 配合。


【面试题】8. Redis 分布式锁用 SETNX 实现有什么问题?Redlock 算法可靠吗? ​

【标准答案】基础 SETNX 的问题:

  1. 死锁风险:客户端加锁后宕机,锁永远不释放 → 解决:SET key value NX EX ttl,原子性设置过期时间
  2. 误删别人的锁:A 的锁过期后 B 加锁,A 恢复后 DEL 把 B 的锁删了 → 解决:value 设为唯一标识(UUID),删除前先比较
  3. 判断和删除非原子:比较 value + DEL 之间可能被其他客户端抢先 → 解决:用 Lua 脚本保证原子性
lua
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
  1. 锁续期问题:业务执行时间超过 TTL → 解决:看门狗机制(Redisson 的 Watchdog),后台线程自动续期

Redlock 算法:在 N(通常5)个独立 Redis 实例上加锁,超过半数加锁成功且总耗时小于锁过期时间才算成功。

Redlock 可靠吗? 有争议。Martin Kleppmann 写过文章批评:时钟漂移、GC 停顿可能导致多个客户端同时持有锁。Antirez(Redis 作者)做了反驳但未完全解决问题。

我的实践:

  • 单实例够用 → Redisson 的 RLock(看门狗 + Lua 原子操作),够稳
  • 对一致性要求极高 → 用 ZooKeeper 或 etcd 的分布式锁(基于租约+Watch)
  • 不强求极端场景下的完美 → Redisson 实际够用,99.9% 的业务场景没问题

【面试题】9. Kafka 如何保证消息不丢失?生产者、Broker、消费者三个环节分别怎么处理? ​

【标准答案】消息丢失有三个环节,逐一分析:

生产者端:

  • 设置 acks=all(或 -1),消息必须写入所有 ISR 副本才算成功
  • 设置 retries > 0,网络抖动时自动重试
  • 使用回调 Callback 确认发送结果,失败做补偿(本地缓存+定时重发)

Broker 端:

  • min.insync.replicas=2,至少2个副本同步成功才返回 ACK,配合 acks=all 使用
  • unclean.leader.election.enable=false,禁止非 ISR 副本成为 Leader,防止数据丢失
  • 副本数 replication.factor≥3

消费者端:

  • 关闭自动提交 enable.auto.commit=false
  • 业务处理完成后手动提交 offset:consumer.commitSync()
  • 注意:先消费再提交,否则消费失败但 offset 已提交就丢消息

Exactly-Once 语义:

  • Kafka 0.11+ 支持幂等性(enable.idempotence=true)和事务
  • 消费-处理-写入 Kafka 全链路事务保证,但性能有损耗

我的实践:金融场景用 acks=all + 手动提交 + 死信队列(DLQ),消费失败的消息进 DLQ 人工处理,保证不丢不漏。


【面试题】10. Spring Cloud 中服务熔断、降级、限流三者有什么区别?Sentinel 如何实现这三种能力? ​

【标准答案】三者区别:

  • 熔断:当下游服务故障率超过阈值,自动断开调用链路,快速失败,类似电路保险丝。过一段时间后半开试探恢复
  • 降级:非核心服务不可用时,返回兜底数据或简化逻辑,保证核心链路可用。比如商品推荐挂了,返回热门商品列表
  • 限流:控制请求速率,防止流量洪峰压垮服务。比如 QPS 超 1000 就排队或拒绝

关系:限流是预防→熔断是检测到故障后保护→降级是故障后的补偿策略

Sentinel 实现:

  • 限流:基于滑动窗口算法统计 QPS,支持直接拒绝、排队等待、预热三种流控效果。配置 FlowRule 设阈值
  • 熔断:支持三种策略——慢调用比例、异常比例、异常数。用滑动窗口统计指标,触发后进入熔断状态(默认10秒),之后半开探测
  • 降级:通过 @SentinelResource 的 fallback 和 blockHandler 指定降级逻辑。blockHandler 处理限流/熔断,fallback 处理业务异常
java
@SentinelResource(value = "getOrder",
    blockHandler = "getOrderBlock",
    fallback = "getOrderFallback")
public Order getOrder(Long id) { ... }

// 限流/熔断降级
public Order getOrderBlock(Long id, BlockException e) {
    return new Order("系统繁忙,请稍后重试");
}

// 业务异常降级
public Order getOrderFallback(Long id, Throwable t) {
    return new Order("服务暂时不可用");
}

生产配置建议:核心接口设 QPS 限流 + 慢调用熔断(RT > 1s 比例超 50% 熔断)+ fallback 返回缓存数据,三件套配合才能真正保住系统稳定性。