字节 AI 全栈工程师面经
字节 AI 全栈工程师面经
wwxdsg面试方向:AI 全栈 / Agent 工程正式岗位
复盘时间:2026 年 9 月 1 日
说明:问题顺序经过重新整理。技术答案是面试结束后我回到资料和项目代码里补全的版本,不是把现场回答修成一份高分面经。
写在前面:我不是不会,是脑中没有一张能走通的图
面试结束后,我没有觉得题目特别偏,反而有一种更难受也更具体的感觉:每道题我都知道它在问什么,但一旦要从头讲顺,表达就开始变空。
我知道 Redis 有内存、事件循环和 I/O 多路复用;知道 InnoDB 常用 B+ Tree;知道 Harness 和权限、预算、状态、验证有关;也知道 LRU 是哈希表加双向链表。可这些答案在脑中更像散落的标签,而不是一套能运行的系统。
真正被追问时,散点知识帮不了我。对方不会只问“Redis 为什么快”,而会继续问:你的项目里为什么用它,缓存没命中怎么办,任务状态为什么不能只放 Redis,数据库又承担什么角色。Harness 也一样,不能只说“它约束模型”,还要说清它在防什么、怎样落到一次 Run 里、哪些能力目前还没有。
LRU 最直接。我很快说出了“哈希表加双向链表”,也知道目标是 O(1)。可对着空白编辑器,我没有把这句话变成代码。类怎样起、节点怎样接、辅助函数先写哪个,这些平时被 IDE、AI 和已有代码骨架悄悄接管的事,一下全变成了阻塞点。
这次复盘后,我不想再把面试准备理解为“补一份更全的题单”。我更需要的是两张能从头走到尾的项目图:一张是用户打开一个 Web 应用后,请求如何穿过网络、缓存和数据库;另一张是 Agent 收到任务后,模型、工具、状态和验证如何组成一个可靠循环。
题目不是靠口诀绑在一起的。它们本来就在同一条链路上。
这轮实际问了什么
下面是面试中的问题。顺序经过整理,但问题本身保留。它们表面上分散,实际上可以归到两条项目链路和一组实现题里。
第一条链路:一次 Web 请求如何变成一次可恢复的业务动作
- 手机打开一个 Web 应用,完整过程是什么?
- DNS 是什么?TCP 和 UDP 有什么区别?
- Redis 为什么快?在你的项目里做了什么?
- MySQL、InnoDB 和 B+ Tree 分别是什么关系,为什么索引常用 B+ Tree?
- 什么是缓存穿透、击穿和雪崩?
第二条链路:一次 Agent Run 怎样不被模型带偏
- 一个 Agent 从输入到输出会经过什么?
- Harness 到底是什么,为什么需要它?
- 遇到过大模型输出不符合预期吗,怎么处理?
- 怎样用程序约束大模型输出?
- Agent 和 Transformer 分别处在系统的哪一层?
从设计落到实现:算法与抽象能力
- 手写 LRU。
- 筷子上蚂蚁碰撞后掉头,最早和最晚全部掉落时间怎么求?
- AI Coding 岗位为什么还会要求无工具手写代码?
后面的内容不是逐题标准答案,而是我用项目把这些问题重新走通的过程。读者可以先看问题,再跟着两条链路理解答案从哪里来。
一、从一次真实请求开始:网络、Redis、MySQL 到后台任务
我把 TikTok Analyzer 想成一个具体场景:用户在手机或浏览器里打开一个项目,查看视频列表,然后点击“分析”。如果能把这次动作顺着讲完,DNS、TCP、Redis、MySQL、缓存问题和 RQ 任务队列就不再是六道互不相干的题。
输入 URL |
这不是为了背流程图,而是为了让每个概念都有前因后果。被问到一个点时,我先回到这次请求停在哪一段,再解释这一段为什么要这样设计。
浏览器为什么不能直接“找到后端”
用户输入 URL 后,浏览器先把它拆成协议、域名、路径和参数。它会检查缓存、Service Worker、HSTS 和已有连接;没有可用地址时,才去做 DNS 解析。浏览器、操作系统和递归 DNS 解析器都会缓存记录,未命中时才沿着根、顶级域和权威 DNS 的层级找到 A、AAAA 或 CNAME 记录。
这就是 DNS 在整条链路中的位置:浏览器知道域名,但还不知道该把包发给谁。普通 DNS 查询常用 UDP 53,因为请求小且不需要先建连接;响应过大、区域传送或重试等场景可以使用 TCP。DoH 和 DoT 只是把这个解析过程放进不同的加密传输方式里。
拿到 IP 后,HTTP/1.1 和 HTTP/2 通常基于 TCP。TCP 的三次握手不是一句“建立连接”就结束了,它要确认双方的收发能力并同步初始序列号。HTTPS 还要完成 TLS 握手:验证证书、协商算法并建立会话密钥。到了 HTTP/3,传输基础变成 QUIC/UDP,但可靠传输、拥塞控制和加密仍由 QUIC 在用户态补上。
所以 TCP 和 UDP 的差别也不该死记成表格。我要先问自己:这一层需要什么?如果我需要有序、可靠的字节流和成熟的流控,TCP 合适;如果我更在乎低延迟或独立数据报,UDP 合适;QUIC 则说明“基于 UDP”不等于应用一定不可靠。
请求到了服务器后,Nginx 负责接收和转发,FastAPI 才进入认证、参数校验和业务处理。后端返回 HTML、JSON、图片或静态资源后,浏览器再构建 DOM、CSSOM、渲染树,完成布局、绘制和合成;React 一类前端还会下载脚本、绑定事件并继续请求 API。这样再回答“手机打开 Web 应用发生什么”,我不是在背七层网络,而是在讲一次真实请求怎样走到界面可用。
Redis 在这里解决的是“快”和“调度”,不是“保存一切”
当用户反复打开项目、表格或视频列表时,每次都从 SQL 查一遍没有意义。TikTok Analyzer 用 Redis 缓存项目和表格列表、视频列表、Run 状态、模型和声音列表,也缓存调用成本更高的商品资料和分析结果。缓存 Key 会带上用户和查询维度,避免不同用户拿到彼此的数据。
Redis 快的原因也因此变得好理解。大多数数据操作发生在内存里,避开了磁盘随机 I/O;常用数据结构和内部编码让单条命令的处理路径很短;核心命令通常串行执行,少了大量锁竞争;事件循环和 I/O 多路复用又让一个进程能管理很多连接。Pipeline 的价值也自然出现:有时瓶颈不是 Redis 的计算,而是客户端与 Redis 之间的一次次网络往返。
“Redis 是单线程所以快”只说到了其中一层。Redis 6 以后部分网络 I/O 可以多线程处理,而大 Key、KEYS、复杂集合运算、持久化压力、内存淘汰和网络拥塞一样会拖慢它。真正该记住的是:它快,是因为把常见操作放在一条很短、很少等待磁盘和锁的路径上;不是因为某个神奇关键词。
在这个项目里,Redis 还有第二种身份:RQ 的任务基础设施。视频分析、抽帧、脚本和分镜改写、图文生成都不适合卡在 FastAPI 请求里。API 先创建 SQL Job,再向 RQ 投递任务并立即返回 job_id,Worker 在后面运行,前端通过状态查询展示进度。
这里最重要的不是记住“用了 Redis + RQ”,而是解释边界:
Redis 负责“快”和“调度”,SQL 负责“准”和“可恢复”。
任务状态、最终结果和 checkpoint 仍要写进 SQL。Redis 可能过期、淘汰、清空或重启,RQ 也不是业务事实库;如果它们一出问题,用户的任务历史不该永久消失。这个决策同时回答了“Redis 在项目里做了什么”“为什么不用 Redis 保存全部状态”“异步任务怎么恢复”三个问题。
走到数据库时,B+ Tree 不是一张孤立的图
缓存未命中,或者需要读取权威状态时,请求会落到 MySQL/InnoDB。MySQL 是数据库管理系统,InnoDB 是默认存储引擎;它不只“存表”,还负责数据页、Buffer Pool、事务、锁、redo log、undo log 和 MVCC。
我现在会从“磁盘按页读取”开始解释 B+ Tree。数据库不是每查一个值就从磁盘拿一个单独节点,而是希望一次读到一个包含大量索引项的数据页。B+ Tree 的非叶子节点主要放键和子节点指针,所以一个页能容纳很多分支,树会更矮。定位大量数据时,通常只要访问很少几层页。
数据或数据指针集中在有序叶子节点,叶子之间又有链接,因此等值查询、范围扫描和排序都能沿着同一套结构完成。Hash 的等值查询很快,但不适合范围和排序;红黑树等二叉树分支少、树会更高;普通 B Tree 的内部节点也存数据,在相同页大小下能容纳的索引项更少。
继续沿着一条项目查询走,就能说清聚簇索引和二级索引。InnoDB 的主键索引叶子通常保存整行数据,这就是聚簇索引;二级索引叶子保存二级索引列和主键值,查询字段不全在二级索引里时,需要再按主键回到聚簇索引取整行。能直接从二级索引拿到所有字段时,才叫覆盖索引。
短且递增的主键也不再是面试偏好,而是存储代价问题:二级索引都要带主键,主键太大会放大空间;递增写入更接近索引尾部,通常减少随机页访问和页分裂。UUID 不是不能用,只是要清楚它换来了哪些写入和空间成本。
缓存穿透、击穿和雪崩,是这条链路被并发放大的三种方式
当一个不存在的商品 ID 被不断请求,Redis 永远没有结果,SQL 就会被重复访问,这是缓存穿透。参数校验、缓存空值、布隆过滤器和限流的共同目标,是别让“不存在”的请求无限穿过缓存。
当一个热点视频或热点配置刚好过期,大量用户同时请求它,所有人一起回源重建,这是缓存击穿。互斥重建、请求合并、逻辑过期和热点预热的目标,是让一群请求只派一个代表去做重建,其余请求等待或读短暂旧值。
当很多 Key 因为相同 TTL 一起过期,或者 Redis 整体不可用,大量请求同时压到 SQL,这才是缓存雪崩。随机 TTL、分批预热、限流降级、多级缓存和高可用,都是在避免“同一时刻所有保护层一起消失”。
这些不是标准答案的三行对照表,而是三个不同的故障来源:数据根本不存在、一个热点失效、很多保护一起失效。方案也都有代价:空值缓存可能遮挡刚写入的数据,布隆过滤器有误判,互斥会增加等待,逻辑过期会允许短暂旧数据。只要能从请求怎样被放大来推导方案,就不需要靠相似名词硬分辨。
二、再从一次 Agent Run 开始:模型、Harness、工具与验证
第二张图来自 OpsMind。用户提出一个数据问题时,系统不是把原话扔给模型,然后相信它的自然语言回答。一个可用的 Agent,需要让模型在程序控制下阅读上下文、选择工具、获得 observation、修正计划并在边界内结束。
用户问题 |
这条链路把 Agent、Harness、大模型异常、结构化输出、工具调用和 Transformer 放进了同一个上下文里。面试时我不该先背定义,而应该先讲:如果模型会猜错、会编造、会重复调用工具,我们怎样让这条链路仍然可控?
Transformer 负责预测下一步,不负责替系统承担后果
大语言模型的底层仍然是在预测下一个 Token。文本经过 Tokenizer 变成 Token ID,映射为向量并加入位置信息;每层用 Query、Key、Value 计算注意力,再经过多头注意力、残差连接、归一化和前馈网络,最后得到词表上的 logits。Decoder 的 causal mask 不允许当前位置看到未来 Token,KV Cache 则缓存历史 Key 和 Value,减少自回归生成时的重复计算,但会随上下文增长占用更多显存。
这些机制解释了模型为什么能根据上下文继续生成,却没有让模型天然拥有数据库权限、真实世界知识或函数执行能力。模型所谓的 Tool Call,本质还是生成了工具名和结构化参数;真正执行、超时、重试、权限和审计,都在模型外。
因此我会把 Agent 拆成两层:Transformer 解决“下一步生成什么”,Runtime 解决“这一步是否允许发生、发生后如何验证、失败后怎样恢复”。混在一起讲时,Harness 很容易沦为一个空泛的名词。
Harness 的起点不是框架,而是模型不可靠
我现在更愿意把 Harness 定义为:
包裹在模型、Runtime 和工具外围,负责约束、观测、验证和恢复的一套工程环境。
它防的不是一个抽象的“模型不好用”,而是具体后果:模型选错工具、填错参数、无限循环、上下文漂移、生成能执行却不正确的 SQL,或者在权限不足时仍然尝试产生外部副作用。
放回 OpsMind,这些组件才有落点。默认 QueryRuntime 管理一次 Run 的事件、取消、超时和终态;Graph-based Harness Preview 用显式 Investigation Graph、Capability Tool Gateway 和 SQLiteRunStore 保存 State、Event、Checkpoint、Interrupt 与 Resume。不支持的请求仍回到 Legacy 路径。
这样的设计不是为了把术语堆满,而是为了回答几个现实问题:用户中断后能否恢复,工具结果能否回溯,长任务怎样不无限运行,模型越权时在哪里被挡住。上下文管理负责只带任务相关的 Schema、文件和历史;工具网关做白名单、参数 Schema、资源级授权、超时和返回裁剪;预算与终止条件限制轮次、Token、费用、总时间和重复调用;人工确认则留在高风险写操作之前。
边界也要一起说。当前 Harness 是 opt-in 的本地单机能力,不是分布式工作流平台;SQLiteRunStore 可以恢复本机任务状态,但不等于跨机器协调或 exactly-once。能把“已经实现的保护”和“还没有的分布式保证”讲在同一段里,才是项目经验,而不是项目包装。
模型输出通过验证后,才能算系统输出
如果模型说它生成了一段 JSON、一个 SQL 或一份操作计划,系统不能因为格式看起来像就相信它。更可靠的路径是:模型先输出候选结构,程序解析并校验 Schema,再校验跨字段业务规则;失败时把精确错误反馈给模型做有限 repair,仍失败就显式失败或转人工确认。
模型候选输出 |
在 OpsMind 的 SQL 场景里,模型不能随意宣布某张表或字段存在。系统先让它读取真实 Schema,再通过 RequestGuard、SQLValidator、SQLRepairer 和只读 DBConnector 约束执行。RelationGraph 提供候选关联路径,执行后还要检查 JOIN 膨胀、结果粒度和证据。SQL 能跑通,不等于业务答案就可信。
这也让我更容易回答“大模型输出不符合预期怎么办”。先不要条件反射式改 Prompt。结构错误、字段缺失、违反业务规则、编造事实、工具循环、超时限流和上游故障,根因不同,处理路径也不同。需要先记录模型、输入、Schema 版本、原始输出、finish reason、耗时和错误类型,再决定是强约束输出、有限 repair、工具证据、重试、降级还是人工审核。
一句我现在更愿意记住的话是:
Prompt 告诉模型“应该怎样做”,程序决定模型“最多能做什么、什么结果才算成功”。
三、LRU 和蚂蚁题不该漂在项目外面
面试里的 LRU 看起来突然从工程系统跳到了算法题,但它其实就是缓存最朴素的资源管理问题:内存有限,什么数据该留下,什么数据该被淘汰。
回到 TikTok Analyzer 的缓存场景。项目列表、视频详情、模型元数据和分析结果不可能无限留在内存里。假设一个缓存要优先保留最近访问的 Key,就需要在读到 Key 时快速找到它,也要快速把它标记为“刚刚使用过”;空间不够时,还要迅速移走最久未访问的那个。
哈希表负责 O(1) 找到节点,双向链表负责 O(1) 把节点移到头部或从尾部淘汰。头部表示最近使用,尾部表示最久未使用。两个哨兵节点把空链表、首节点和尾节点这些边界情况收进统一结构里。这样 LRU 不再是一个孤立模板,而是“缓存如何在有限空间里保留更可能再次被用到的数据”的代码版本。
class Node: |
我当时的问题不是没听过这个结构,而是没有把“不变量”翻译成落笔顺序。下次从空白开始写,我先写 Node 和两个哨兵,再写 _remove、_add_first,最后才写 get 和 put。每一步都对应一个明确动作,代码就不会只剩“我知道应该用双向链表”。
筷子上的蚂蚁题也是同一种训练:不是把题目归进“智力题”,而是找一个能消掉复杂度的等价关系。相同速度的蚂蚁碰撞后掉头,如果忽略身份,效果等价于直接穿过。因此位置为 x 的蚂蚁,最早掉落时间看 min(x, L-x) / v,最晚掉落时间看 max(x, L-x) / v,所有蚂蚁的答案取各自时间的最大值。
它和 LRU 不属于同一个业务链路,但都在问同一件事:能不能先找出系统真正要维护的关系,再把无关复杂度扔掉。LRU 里是不变量,蚂蚁题里是身份可交换性,缓存和 Harness 里则是权威状态与非权威状态的边界。
四、以后怎么准备:不是背答案,是反复走项目
这次让我放弃了“每个知识点单独背一页”的准备方式。那种方式会让我在看到题目时有熟悉感,却在追问一开始就断线。更可靠的是先选一个自己真的做过的场景,把它从用户动作走到最终结果。
对我来说,目前最值得反复走的是两条:TikTok Analyzer 的“打开项目并发起分析”,和 OpsMind 的“提出数据问题并完成一次可信 Run”。前者可以自然带出 DNS、TCP/TLS、Nginx、FastAPI、Redis、RQ、SQL、B+ Tree 和缓存故障;后者可以自然带出 Transformer、上下文、Tool Call、Harness、权限、状态、验证、恢复和人工确认。
真正被问到某个点时,我不需要回忆一张答案卡,而是把自己放回对应位置:请求现在走到哪里?这里的瓶颈或风险是什么?我为什么选这个设计?它底层依赖什么机制?它还没有解决什么?
这是一条输出路径,不是记忆口诀:
真实场景 |
例如 Redis,不从“它为什么快”起手,而是从“用户反复打开项目列表,我们不想每次都打 SQL”起手。于是缓存出现了;缓存失效后为什么要回 SQL,SQL 为什么仍是事实源,热点 Key 失效后为什么会击穿,也跟着出现。底层的内存、命令路径和事件循环,是为了解释“为什么这层能快”,而不是一组必须先背出的词。
Harness 也一样,不从定义起手,而是从“模型可能生成错误 SQL 或做出越权操作”起手。权限、工具网关、预算、状态、验证和 checkpoint 都是在回答:我们怎样不让一次不可靠生成直接变成不可靠副作用。最后再主动说出本地单机、opt-in、非 exactly-once 的边界,说明我知道系统现在停在哪里。
接下来我会用更笨但更有效的方式练习:关掉资料,先在纸上画出这两条链路;再随机挑其中一个节点,用 30 秒讲清它在整条链路里的职责;然后接受“为什么”“替代方案是什么”“你项目里真的做到了吗”这三类追问。讲不出来时,不急着把答案抄回笔记,而是找到自己在链路上断掉的位置。
算法也按同样方式训练。不是反复看标准实现,而是从空白写出 LRU 的结构,并在每一步说出要维护的不变量。AI 可以帮我审查实现、补测试、制造边界用例,但不能替我完成从设计到代码的最后一段翻译。
结语
这次面试没有让我得出“八股没有意义”或“AI 能写代码,所以人不需要基础”这样的结论。它只是把一件原本容易被工具遮住的事摆到了我面前:AI 能降低语法和样板代码成本,却不会替我建立底层模型,也不会替我承担判断责任。
我真正要补的,不是再收藏更多资料,而是把已有项目里的设计、故障和取舍重新走通。下一次被问到 Redis、Harness 或 LRU,我希望自己先想到的不是某一句定义,而是那条已经能在脑中跑起来的链路。