字节跳动 AI 全栈二面:AQS 原理、MySQL 索引选择与 Agent 设计
- 轮次
- 二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
项目设计
- 请介绍一下你项目的设计思路。
- 这个项目是从零搭建的,还是基于已有能力迭代的?
- 项目是如何分层的,各层分别承担什么功能?
- 如何扩展新场景?扩展时会涉及哪些层的改动?
- 你在这个项目里遇到的最大技术挑战是什么?为什么认为它有难度?
- 执行过程中需要用户确认时,你如何与用户交互?
- 模型驱动的执行流程,是否仍然需要固定流程或脚本约束?这属于功能问题还是效果调优?
基础知识
- 你最熟悉哪种编程语言?
- AQS 的作用和基本原理是什么?为什么它能用于多种同步器?
- ReentrantLock 如何与 AQS 交互?加锁和释放时,内部如何操作状态与等待队列?
- ReentrantLock 是否支持重入?它是如何实现重入和释放的?
- synchronized 的同步机制是什么?
- 索引有哪些分类?设计索引有哪些原则?
- 查看执行计划时关注哪些信息?filtered 字段表示什么?
- 存在多个索引时,MySQL 如何选择索引?为什么可能选错?相关预估值从哪里来?
架构与系统设计
- Agent 应用开发有哪些基本步骤?
- 实现一个简单 Agent 需要哪些核心模块?模块之间如何交互?
算法与编码
- 手撕:给定一棵二叉树,返回它的之字形层序遍历结果(第一层从左到右,下一层从右到左,交替进行),并说明思路、构造测试用例验证结果。
《参考解析》
项目与 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 按深度往对应层的列表里填、最后统一按需反转,空间换成递归栈。测试用例至少覆盖:空树、单节点、只有左孩子的链状树、满二叉树,以及三层以上的树用来验证奇偶层方向确实交替。