面灵AI→

字节跳动 AI 全栈二面:AQS 原理、MySQL 索引选择与 Agent 设计

轮次
二面
时间
2026-10
来源
牛客网

《面试题目》

项目设计

  1. 请介绍一下你项目的设计思路。
  2. 这个项目是从零搭建的,还是基于已有能力迭代的?
  3. 项目是如何分层的,各层分别承担什么功能?
  4. 如何扩展新场景?扩展时会涉及哪些层的改动?
  5. 你在这个项目里遇到的最大技术挑战是什么?为什么认为它有难度?
  6. 执行过程中需要用户确认时,你如何与用户交互?
  7. 模型驱动的执行流程,是否仍然需要固定流程或脚本约束?这属于功能问题还是效果调优?

基础知识

  1. 你最熟悉哪种编程语言?
  2. AQS 的作用和基本原理是什么?为什么它能用于多种同步器?
  3. ReentrantLock 如何与 AQS 交互?加锁和释放时,内部如何操作状态与等待队列?
  4. ReentrantLock 是否支持重入?它是如何实现重入和释放的?
  5. synchronized 的同步机制是什么?
  6. 索引有哪些分类?设计索引有哪些原则?
  7. 查看执行计划时关注哪些信息?filtered 字段表示什么?
  8. 存在多个索引时,MySQL 如何选择索引?为什么可能选错?相关预估值从哪里来?

架构与系统设计

  1. Agent 应用开发有哪些基本步骤?
  2. 实现一个简单 Agent 需要哪些核心模块?模块之间如何交互?

算法与编码

  1. 手撕:给定一棵二叉树,返回它的之字形层序遍历结果(第一层从左到右,下一层从右到左,交替进行),并说明思路、构造测试用例验证结果。

《参考解析》

项目与 Agent 设计:分层、扩展,以及模型驱动流程的边界

项目介绍题(第 1、2 题)不是让你复述需求,而是给后面所有追问搭台子:先说清项目解决什么问题、服务谁、有哪些硬约束,再讲你负责的那一块,最后主动留一两个可以深挖的点。是「从零搭建」还是「基于已有能力迭代」要照实说,重点落在取舍理由上——从零意味着你要为每一层做技术选型,迭代则要讲清哪些既有约定不能动、你怎么在旧结构里加新能力。

第 3、4 题的分层与扩展是同一件事的两面。AI 应用通常能切成:交互层(前端与用户确认点)、编排层(Agent 循环、规划、工具调用与状态机)、能力层(模型、检索、外部工具)、数据与记忆层(会话、长期记忆、向量库)、支撑层(评测、观测、权限与成本)。判断分层好坏的一个实用标准是「加一个新场景要改几层」——理想情况是只新增工具定义、提示词与配置,编排层一行不改;如果每加一个场景都得改编排代码,说明场景差异没有被收敛成工具契约和配置,这是第 4 题最想听到的回答。

第 5 题问最大技术挑战,对这个岗位来说最容易被认可的是「不确定性」:模型输出不稳定、同一输入两次结果不同,难在没法像普通后端那样靠单元测试兜住。答的时候不要停在「难在效果」,要说清你怎么把它变成可衡量的东西——固定评测集、定义成功判据、跑回归看有没有退步,再讲你为此做的取舍(比如为了稳定性牺牲一点自由度,或加一层结果校验)。

第 6、7 题是模型驱动系统特有的两个问题。用户确认要按「可逆性」来设计:写文件、付款、对外发消息这类不可逆动作,必须做成显式确认点——把待执行的动作和参数呈现给用户,确认后才落地,同时保证重复确认幂等、超时能安全收尾。至于「是否还需要固定流程或脚本约束」,两者不冲突:确定性的部分(权限校验、参数校验、状态流转、重试与幂等)交给代码,这类属于功能与安全边界,错了会造成不可逆后果;不确定的部分(意图理解、规划、话术)交给模型,属于效果调优,靠评测和提示词迭代。能按这条界线把题里的两类问题分开,就答到点上了。

AQS 与 ReentrantLock:状态、队列与重入

AQS(第 9 题)的内核很薄:一个 volatile int state 加一条 CLH 变体的双向 FIFO 等待队列,再配一组模板方法——acquire / release 把排队、阻塞(park)、唤醒(unpark)的骨架写好,子类只需要实现 tryAcquire / tryRelease 定义「资源怎么算可用」。它能支撑多种同步器,正是因为这层抽象:state 的含义完全由子类定义,独占语义的 ReentrantLock、共享语义的 Semaphore 与 CountDownLatch、读写锁(把 state 的高低 16 位分别记读锁和写锁)都能挂在同一套排队与唤醒机制上。

ReentrantLock 的交互方式(第 10 题):内部 Sync 继承 AQS。非公平版加锁时先直接 CAS 把 state 从 0 改成 1,成功就 setExclusiveOwnerThread(当前线程) 抢到锁;失败才走 acquire,把当前线程包成 Node 入队并 park。释放时 tryRelease 每次把 state 减一,只有减到 0 才真正清空持有线程并唤醒后继节点。公平版多一步:入队前先看队列里有没有前驱,有就老老实实排队,所以吞吐略低但不会插队。

重入(第 11 题)靠两个字段配合:exclusiveOwnerThread 判断是不是同一个线程,state 当计数器。同一线程再次加锁时 state 递增、不再入队,释放时递减,减到 0 才算真正释放。所以 ReentrantLock 的 lock/unlock 必须成对——漏掉一次 unlock,锁永远不会释放,这也是它比 synchronized 更容易写错的地方。

synchronized(第 12 题)是 JVM 内建、基于对象监视器(monitor)的实现,对象头 Mark Word 记录锁状态,按竞争程度从偏向锁、轻量级锁(CAS 自旋)膨胀到重量级锁(涉及内核 mutex 与线程挂起唤醒);需要注意偏向锁在较新的 JDK 里已被废弃并默认关闭,回答时别把它当成必然存在的一环。和 Lock 的取舍是:synchronized 由 JVM 保证异常时自动释放、写法简单,适合绝大多数场景;Lock 换来的是可中断获取、tryLock 超时、公平锁和多个 Condition 条件队列这些能力,只在确实需要时才用。

MySQL 索引:分类、设计原则与优化器怎么选

索引分类(第 13 题)可以按三个维度说:按数据结构分 B+ 树索引与哈希索引(后者只有 Memory 这类引擎支持,且只能等值);按 InnoDB 的存储形态分聚簇索引(主键索引,叶子节点直接存整行数据,所以「主键要短且递增」)与二级索引(叶子存索引列加主键值,查非索引列要回表);按用途分主键、唯一、普通、联合、前缀和全文索引。设计原则上,选择性高(区分度大)的列放前面,联合索引遵循最左前缀,顺序按「等值条件在前、范围条件在后」排,能用覆盖索引就别让它回表;同时要控制索引数量,每个索引都会放大写入和更新的代价。低区分度的列(性别、状态位)单独建索引基本没有收益。

执行计划(第 14 题)主要看四项:type 反映访问方式(const / eq_ref / ref / range / index / ALL,越靠右越差)、key 是实际选中的索引、rows 是预估扫描行数、Extra 里的 Using index 表示覆盖索引、Using filesort 和 Using temporary 是常见的性能警报。filtered 表示存储引擎返回的行在 Server 层再经过 WHERE 条件过滤后剩下的百分比,值越低说明引擎层扫了大量最终会被丢掉的行,通常意味着索引选择性差或条件没法下推——它主要用来估算 join 和回表的代价,想看真实执行情况要用 EXPLAIN ANALYZE 拿实际行数对比估算值。

索引选择(第 15 题)由优化器按成本模型决定:它会在候选索引里估算扫描行数、回表次数、是否覆盖、能不能避免排序,再挑总代价最低的那个。成本来自统计信息——索引基数(cardinality)、数据页采样结果,配合 innodb_stats_persistent 这类持久化统计和一组描述随机读、顺序读、比较代价的常量。所以选错的典型原因都指向估算失真:统计信息过期或采样偏差导致基数估算错、数据分布倾斜(某个值占了绝大多数行)、范围条件的行数估算不准、回表代价被低估、代价常量与真实硬件(SSD 还是机械盘)不符。实践中先用 ANALYZE TABLE 刷新统计,再不行用 optimizer trace 看它在几个候选里怎么算的;FORCE INDEX 只当诊断手段,别写进业务代码,那是在替优化器背锅。

手撕:二叉树之字形层序遍历

思路是标准 BFS 加一个方向标记:用队列按层处理,每层开始前先记下当前队列长度作为本层的节点数,按这个数量循环出队;奇数层按左到右的顺序收集,偶数层改用头插(或收集完再反转本层结果)。两个细节值得主动说:一是子节点入队始终按「先左后右」,只有收集结果的顺序在变,别把入队顺序也翻过来;二是用 size 快照分层,比额外记层号判断更简单也更稳。复杂度是 O(n) 时间、O(w) 空间(w 为最大层宽),如果面试官追问「能不能不用队列」,可以提 DFS 按深度往对应层的列表里填、最后统一按需反转,空间换成递归栈。测试用例至少覆盖:空树、单节点、只有左孩子的链状树、满二叉树,以及三层以上的树用来验证奇偶层方向确实交替。