字节跳动广告 Agent 一二面:skill 平台与秒杀设计
- 轮次
- 一面+二面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 工作经历细节提问
- 有没有准备设计题和八股文?(没准备就直接问项目)
- Redis 怎么实现分布式锁?
- MySQL 的聚簇索引
- 设计题:如何设计一个内部 skill 平台,包含 skill 的上传、下载、发布;本地 runtime 的设计
- 算法题:找出最长的回文串
- (二面)工作经历细节提问
- (二面)有没有用 AI 提效?如果当时没有的话,再做一次会怎么做?
- (二面)设计题:设计营销会场秒杀系统
- (二面)能不能现场演示一下你自己的 demo?
- (二面)skill 和 Function Call 的区别?
- (二面)算法题:给定一组个位数集 {2,4,9} 和一个 int 数字 num,找到可以用集合组成的比 num 小的最大数字
《参考解析》
1. Redis 分布式锁。最小可用实现是 SET lock_key unique_value NX PX 30000:NX 保证互斥、PX 防死锁、unique_value 用于校验持有者。解锁必须用 Lua 脚本「比对 value 再删除」,否则可能删掉别人重新拿到的那把锁。要主动补三个坑:①业务执行时间可能超过锁的过期时间,需要看门狗续期(Redisson 的 lockWatchdogTimeout),或者把过期时间设得远大于 P99 并配合超时中断;②主从异步复制下主节点宕机、锁还没同步到从节点,从节点升主后别人能拿到同一把锁——方向是 Redlock(多节点多数派,但有争议且延迟高)、基于共识的 etcd/ZooKeeper,或者干脆让下游操作幂等(唯一索引、状态机加 fencing token);③锁本质上只是性能优化,正确性最终要由下游幂等兜底,这句话在面试里比背 API 更能加分。
2. MySQL 聚簇索引。InnoDB 的主键索引就是聚簇索引,B+ 树叶子节点直接存整行数据,所以按主键查一次树查找就能拿到行;二级索引的叶子存的是主键值,要查非索引列就得拿主键再回聚簇索引查一次,这就是回表。由此衍生几个常见追问点:为什么推荐自增主键——随机主键(UUID)会造成页分裂和随机 IO,自增则是顺序追加,页利用率高;覆盖索引能免回表,EXPLAIN 里的 Using index 就是覆盖索引,Using filesort 说明排序没走索引;二级索引叶子按索引列有序,所以「按二级索引列排序 + 主键范围」的查询效率高。为什么用 B+ 树而不是 B 树或哈希:非叶节点只存键、扇出大、树高低,且叶子之间有链表,范围查询友好。
3. 内部 skill 平台怎么设计。拆成四块回答。①模型层:skill 的元数据(名称、描述、版本、入参出参 schema、依赖的工具与权限、作者)+ 内容(提示词、脚本、资源文件),版本不可变、语义化版本管理,便于回滚和灰度。②上传与发布链路:上传进对象存储并做 schema 校验与安全校验(脚本类 skill 必须沙箱执行、限制网络与文件访问),审核后发布,发布产生不可变版本号和 manifest,客户端按 manifest 拉取。③本地 runtime:负责拉取与缓存 skill、依赖隔离(每个 skill 独立依赖目录或容器,避免版本冲突)、执行超时与资源限额、日志与调用审计,以及断网时的本地降级。④权限与治理:谁能发布、谁能调用、调用配额、灰度与 A/B、废弃版本的下线过渡期。面试里点出「安全沙箱」和「版本不可变 + 灰度」这两条,比画一张架构图更有说服力。
4. 秒杀系统设计。核心是「把流量挡在数据库之前」和「绝不超卖」。分层看:①客户端按钮置灰、答题验证码、静态资源走 CDN,把大部分请求掐死在源头;②网关与服务端做限流(按用户和 IP 两个维度的令牌桶)、同一用户请求去重、黑名单;③库存预热到 Redis,用 Lua 脚本把「校验库存 + 扣减 + 记录用户」做成一次原子操作,避免 GET 和 DECR 之间的竞态;④扣减成功后写 MQ 异步下单,接口先返回「排队中」,订单服务消费后落库,数据库再加「用户 + 商品」唯一索引兜底防重;⑤少卖的兜底是超时未支付或下单失败时回补库存,并做定时对账;⑥商品详情这类读多写少的数据走缓存加本地缓存,热点 key 做副本打散。要主动说明「Redis 库存和数据库订单是两套存储、最终一致」,并给出限流阈值是怎么算出来的。
5. skill 与 Function Call 的区别。Function Calling 是模型协议层的能力:你提供 JSON Schema 描述的函数签名,模型在生成时选择调用哪个函数并给出参数,本质是把自然语言意图映射成结构化调用,它只覆盖「一次调用的语法约定」,不管这个能力怎么分发、怎么版本管理、需要什么权限。Skill 是上层的封装单元:一份可复用的能力包,除了可调用接口,还包括触发时机与提示词、前置依赖、资源文件(模板、脚本)、权限声明和版本号,可以被安装、按需加载和检索,token 开销也更可控。一句话区分:Function Call 解决「怎么调」,Skill 解决「调什么、什么时候调、谁能调」——Claude 的 Skills 与 MCP 的 tools 就是这两个层次的具体形态。
6. 用集合拼出小于 num 的最大数字。思路是贪心加回溯,先尝试构造和 num 位数相同的数:把集合排序去重,从左到右逐位在集合里找「不大于 num 对应位」的最大数字;如果某一位能找到严格小于的数字,后面的位全部填集合里的最大值就是答案;如果这一位只能取相等,就继续下一位;如果某一位连集合里最小的数字都比它大,就回溯到上一位把它调小一格,之后所有低位填最大值。如果连最高位都放不下(或 num 本身只有一位且集合里所有数字都大于等于它),答案就退化成「位数比 num 少一位、每位都填集合最大值」的数,例如 num = 10、集合 {2,4,9} 时答案是 9。必须主动提两个边界:集合含 0 时的前导零问题(首位不能为 0)、以及 num 为负数或 0 时的定义。位数很少时也可以直接回溯枚举所有由集合数字组成且小于 num 的数取最大值,但要说明复杂度是集合大小 ^ 位数。