美团AI面试 实习一面面经

1. 自我介绍

您好,面试官好,我目前是一名大三学生,主方向是 Java 后端和 AI 应用工程。

平时主要使用 Spring Boot、Spring Cloud、Spring AI Alibaba、Redis、RabbitMQ、Elasticsearch、MySQL 等技术做 AI 相关系统开发,也比较关注 AI Agent、RAG、Workflow 以及 AI 工程化相关方向。

目前做的核心项目是企业级 RAG + Agent 推理系统,主要负责检索链路和 Agent 工作流相关功能,包括 Hybrid Recall、Rerank、多轮 Memory、Workflow Agent 等,同时也比较关注系统工程能力,比如缓存、异步、日志链路、限流、可观测性以及 AI 系统稳定性这些问题。

另外平时也会结合 Codex、Cursor 等 AI 工具辅助开发,提高开发效率和代码质量。目前也在持续学习 JVM、并发、Redis 和分布式系统设计等后端相关内容,希望后续能够继续往 AI 工程化和 Java 后端结合的方向发展。

2. 使用 AI 工具遇到的问题

第一个是上下文缺失。比如项目比较复杂时,如果不给 AI 足够上下文,它很容易生成和当前架构不一致的代码,比如包结构不对、调用不存在的方法、或者不符合现有设计规范。

第二个是"看起来对,但实际上有问题"。比如复杂业务逻辑、并发逻辑、缓存一致性这些场景,AI 很容易生成逻辑不完整的代码,需要自己再检查和调整。

第三个是长链路问题。像 Agent、Workflow、RAG 这种链路比较长的系统,如果一次性让 AI 生成完整功能,通常效果不好,所以我后来会把任务拆小,比如先定义状态流转、输入输出,再逐步生成。

3. OSI 七层模型

OSI 七层模型从下到上分别是:

物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。

物理层主要负责比特流传输,比如网线、电信号。

数据链路层负责帧传输和 MAC 地址

网络层负责IP 路由

传输层负责TCP、UDP 这种端到端通信。

应用层就是我们开发最常接触的,比如 HTTP、HTTPS、DNS(讲域名进行转化)

4. 数组和链表区别

数组底层是连续内存空间,支持随机访问,所以查询效率高

但中间插入删除效率低,因为需要移动元素。

链表内存不连续,插入删除效率更高,只需要修改指针,但随机访问效率低,需要遍历。

5. 追问一下,在实际开发中如果你需要处理一个动态变化的数据集合,比如频繁的插入和删除操作,同时还需要偶尔进行随机访问,你会选择数组还是链表?为什么?

单纯数组和链表其实都不完全合适

因为:

  • 数组随机访问快,但插入删除成本高。
  • 链表插入删除快,但随机访问慢。

如果让我选择的话,我会去选择跳表作为数据集合

跳表(SkipList)本质上是一种:

"基于链表实现的多层有序索引结构"。

它的核心目标是:

在保持链表插入删除简单的前提下,提高查询效率。

比如底层:

复制代码
1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7

这是普通链表

跳表会额外建立:

复制代码
1 ------> 4 ------> 7

甚至:

复制代码
1 -------------> 7

形成:

  • 第0层(完整数据层)
  • 第1层(稀疏索引)
  • 第2层(更稀疏索引)

查询时:

先走高层,找不到再下沉

6.请你解释一下垃圾回收机制 GC 的工作原理以及它在 Java 中的作用?

GC 的核心作用是自动回收不再使用的对象,避免内存泄漏

Java 中对象主要分配在堆中,GC 会通过可达性分析算法,从 GC Roots 出发判断对象是否还能被引用。

GC Roots 是什么

GC Roots 是 Java 垃圾回收中的:"根对象集合"

如果对象不可达,就会被回收。

对象一般会经历:

Eden → Survivor → Old

新生代主要是 Minor GC,(指新生代垃圾回收)

老年代空间不足时会触发 Full GC(整个堆的大范围垃圾回收)

7.你提到了垃圾回收机制的核心原理和分代回收策略,整体思路是对的。接下来我想深入问一下:在 Java 的垃圾回收机制中,新生代和老年代的回收算法分别适用于哪些场景?为什么会选择这些算法?

新生代一般使用:复制算法

因为新生代对象:

  • 存活率低
  • 回收频率高
  • 大部分对象 GC 一次就死掉

老年代一般使用:标记整理

因为老年代对象特点是:

  • 存活率高
  • 对象数量大
  • 生命周期长

8.如果频繁出现 Full GC,如何排查和优化?

繁出现 Full GC,一般说明 JVM 内存压力比较大,常见原因包括老年代空间不足对象晋升过快、大对象频繁创建、内存泄漏

排查时我一般会先看 GC 日志,重点关注 Full GC 的频率、停顿时间以及 GC 前后老年代内存变化 。如果 Full GC 后内存下降不明显,通常会怀疑存在内存泄漏

然后会通过 jstat 观察各区域内存变化,再导出堆 dump(JVM 某一时刻的"内存快照),结合 MAT 分析大对象和引用链,重点排查缓存、大集合、ThreadLocal、MQ 消息堆积等问题

优化上会根据具体原因处理,比如:

  • 内存泄漏就修复引用问题
  • 缓存增加淘汰和过期策略
  • 减少大对象和长生命周期对象
  • 调整堆大小和新生代比例

9.谈谈 SQL 查询的优化方法以及如何排查慢查询问题?

老生常谈的问题,不再赘述

10.你提到了慢查询 explain 分析以及索引优化等方法,这些都是常见的手段。那么我想追问一下,当你通过 explain 发现某个查询的 type=ALL,并且扫描行数非常多时,你会如何具体优化这个查询?能否结合一个场景来说明?

如果 explain 发现 type=ALL,说明 SQL 走了全表扫描,而且扫描行数很多,这种情况下我会先看 where 条件、order bygroup by 字段是否有合适索引,再判断是不是索引失效

1.如果没有建立对应的索引,则补充创建索引

2.检查是否索引失效

3.是否数据区分度太低/数据返回量太大导致

4.order by / group by 无法利用索引(没有遵循最左原则)

比如有一张订单表 order_info,数据量几百万,现在有这样一个查询:

复制代码
select *
from order_info
where user_city = '北京'
  and create_time >= '2026-01-01'
order by create_time desc;

如果 explain 发现 type=ALL,说明它没有走索引,而是扫全表。这个时候我会考虑建立联合索引:

复制代码
create index idx_city_time
on order_info(user_city, create_time);

这样数据库可以先根据 user_city 缩小范围,再根据 create_time范围查询和排序,扫描行数会明显减少。

11.在你提到的联合索引中,字段的顺序是 user_city、create_time你是如何确定这个顺序的?为什么不是排列方式?能否详细说明一下背后的逻辑?

我会把 user_city 放前面,是因为这个查询通常会先按城市过滤:

复制代码
where user_city = '北京'
  and create_time >= '2026-01-01'

这里 user_city 是等值查询,放在联合索引最左边,可以先快速缩小数据范围;然后再利用 create_time 做范围查询。

如果反过来建成:

复制代码
(create_time, user_city)

create_time 是范围查询时,范围查询后面的字段通常就很难继续充分利用索引过滤了,user_city 的过滤效果可能会变弱。

因为联合索引在 B+ 树里是按字段顺序排序的。

比如索引是:

复制代码
(create_time, user_city)

它的排序方式相当于:

复制代码
先按 create_time 排序
create_time 相同的情况下,再按 user_city 排序

所以如果你写:

复制代码
where create_time >= '2026-01-01'
  and user_city = '北京'

create_time 是范围查询,数据库只能先定位到一大片时间范围:

复制代码
2026-01-01 之后的所有数据

这时候后面的 user_city 已经不是一个连续、有序的小范围了,MySQL 很难再继续利用索引精准定位,只能在这个时间范围内再过滤 user_city

所以一般经验是:

等值查询字段优先,范围查询字段靠后。

12.你需要设计一个简单的缓存系统来提高数据库查询效率,请描述你会如何实现,并考虑缓存更新和失效的策略

我会采用 Redis + MySQL 的缓存架构,核心思路是 Cache Aside 旁路缓存模式

查询时,先查 Redis,如果命中就直接返回;如果没命中,再查 MySQL,然后把查询结果写入 Redis,并设置过期时间,后续相同请求就可以直接走缓存,减少数据库压力。

更新时,我一般不会直接更新缓存,而是采用:

先更新数据库,再删除缓存。

因为数据库才是最终数据源,缓存只是加速层。更新数据库成功后删除缓存,下一次查询发现缓存不存在,就会重新从数据库加载最新数据。

同时为了防止缓存问题,我会做几个保护:

第一,给缓存设置合理 TTL,避免长期脏数据

第二,为了防止缓存穿透,对于不存在的数据,可以缓存空值,或者使用布隆过滤器。

第三,为了防止缓存击穿,对于热点 key 失效,可以加互斥锁,避免大量请求同时打到数据库。

第四,为了防止缓存雪崩,可以给过期时间加随机值,避免大量 key 同一时间失效。

13.我继续问一个细节问题,你提到的缓存更新时采用更新数据库后删除缓存的策略,这种方式在高并发场景下可能会出现什么潜在问题?你会如何优化?

线程 A 读数据,发现缓存不存在,于是去查数据库,查到了旧值。

这时线程 B 更新数据库成功,并删除缓存。

随后线程 A 把刚才查到的旧值写回 Redis

这样缓存里又变成了旧数据,后续请求就会读到脏缓存

第一,可以采用延迟双删。也就是更新数据库后先删除一次缓存,隔一小段时间再删一次,尽量把并发读写过程中回写的旧缓存清掉。

第二,可以通过 MQ异步删除缓存。数据库变更后发送消息,保证缓存最终被删除,适合一致性要求更高的场景。

14.你提到使用分布式锁来解决并发问题,具体来说,你会选择什么工具或技术来实现分布式锁?为什么?

我一般会优先选择 Redis+lua脚本 实现分布式锁

  • Redis 性能高、实现简单、延迟低,适合大多数业务并发控制场景
  • lua脚本可以实现一些原子操作,比如判断删除等可能导致并发问题的操作

15.你要提供一个文本生成 HTTP 接口给业务方调用,请你设计请求与返回的关键字段,至少包含上下文、模型、参数、输出结构、错误码以及用于追踪的一次调用 ID。你会如何支持流式返回?

我会把这个接口设计成一个统一的文本生成接口,比如:

复制代码
POST /api/ai/text/generate

请求里至少包含这些字段:

复制代码
{
  "requestId": "req-xxx",
  "model": "qwen-plus",
  "stream": true,
  "messages": [
    {
      "role": "system",
      "content": "你是一个文本生成助手"
    },
    {
      "role": "user",
      "content": "帮我生成一段营销文案"
    }
  ],
  "parameters": {
    "temperature": 0.7,
    "maxTokens": 1024,
    "topP": 0.8
  },
  "outputFormat": {
    "type": "text",
    "language": "zh-CN"
  },
  "context": {
    "bizType": "marketing",
    "userId": "10001",
    "conversationId": "conv-xxx"
  }
}

这里**requestId 用于一次调用的链路追踪** ;model 指定模型;messages 表示上下文parameters****控制生成效果;outputFormat 约束返回结构context 用于业务方传入业务上下文。

流式返回我会用 SSE 实现,也就是接口返回:

复制代码
Content-Type: text/event-stream

服务端逐步推送 token 或片段:

复制代码
event: message
data: {"requestId":"req-xxx","delta":"你好"}

event: message
data: {"requestId":"req-xxx","delta":",这是生成内容"}

event: done
data: {"requestId":"req-xxx","finishReason":"stop"}

同时我会在流式接口里处理超时、客户端断开、异常关闭和日志记录。每个 chunk 都带上 requestId,方便排查问题。

相关推荐
卷帘依旧2 小时前
怎么保证AI生成的代码是符合预期的
面试
卷帘依旧2 小时前
RAG(Retrieval-Augmented Generation)完全指南(deepseek生成)
面试
卷帘依旧2 小时前
知识切分与维护相关知识介绍
面试
卷帘依旧2 小时前
RAG 的设计问题与局限性分析
面试
小为资料库3 小时前
2026年5月16日教资面试真题汇总(中小幼各科全)
面试·职场和发展
卷帘依旧3 小时前
模式驱动开发(SSD)
面试
星辰_mya3 小时前
彩云之上——[特殊字符]的架构师
java·后端·微服务·面试·架构
BING_Algorithm4 小时前
深入理解JVM垃圾回收
jvm·后端·面试
Larry_Yanan4 小时前
QML面试常见问题(一)QML中组件呈现方式的方法有哪些
开发语言·c++·qt·ui·面试