面灵AI→

字节跳动前端一面(26 题,含 RAG 与 SSE)

轮次
一面
时间
2026-09
来源
牛客网

《面试题目》

  1. 自我介绍。
  2. 最难定位的问题是什么?
  3. 怎么区分简单问题和复杂问题?
  4. 首 token 多久算合理?
  5. 首 token 有哪些优化方案?
  6. TCP、UDP 的区别?
  7. 慢启动、拥塞避免、快速重传和快速恢复分别解决了什么问题?
  8. SSE 和 WebSocket 的协议特性、重连方式以及使用场景?
  9. AI 为什么选择 SSE 而不是 WebSocket?
  10. 进程、线程和协程的区别?
  11. 进程间的通信方式有哪些?
  12. 什么是闭包?适用场景?会造成什么问题?怎么处理?
  13. 从 URL 到浏览器要经历什么步骤?
  14. 重排、重绘和合成分别是什么?
  15. defer 和 async 的区别是什么?
  16. DNS 流程。
  17. 移动端适配方案、样式适配方案、根元素大小。
  18. 浏览器缓存策略。
  19. Cache-Control 有哪些值?
  20. 浏览器跨域问题及解决方案。
  21. Vite 和 Webpack 的区别是什么?Vite 为什么在生产环境需要 Rollup?
  22. 如果多个请求因为 token 过期返回 401,怎么避免并发刷新和重复重试?
  23. 从用户提问开始,完整描述一次 RAG。
  24. Top-K 和 Top-P 的区别,怎么调整这两个值?
  25. 向量检索的算法有哪些?
  26. 手撕:用 JS 实现一个并发限制的异步调度器。

《参考解析》

首 token 时间多长算合理、怎么优化:体感上首 token 在 1 秒内算好,2 秒是可接受上限,超过 3 秒用户会明显觉得卡(纯长文生成场景可以放宽,但一定要有骨架屏/光标反馈)。优化从链路上拆:① 建连成本——预连接/预热(DNS + TLS + HTTP/2 多路复用),网关侧就近接入;② 请求前处理——缩短提示词、把系统提示和知识库做前缀缓存(prompt caching),检索阶段并行化并把「检索」和「首轮生成」做成流式衔接;③ 模型侧——用小模型做首句/过渡语,或让上游尽早 flush 第一个 token;④ 传输侧——用 SSE 边生成边推,禁用中间层压缩缓冲(X-Accel-Buffering: no、不要经过会缓冲的代理),避免 Nagle 与延迟 ACK 叠加;⑤ 前端——先渲染用户消息和占位,首 token 到达立即上屏,不要把整个响应等完再渲染。

SSE 与 WebSocket 的对比,以及 AI 为什么用 SSE:SSE 是单向(服务端 → 客户端)的 HTTP 长连接,Content-Type: text/event-stream,基于纯文本事件流(data: / event: / id: / retry:),浏览器自带 EventSource(注意它不能自定义 header、默认只支持 GET,要带鉴权通常用 fetch + ReadableStream 手动解析),断线自动重连并可携带 Last-Event-ID 续传。WebSocket 是全双工(ws://,一次 HTTP Upgrade 握手后切到二进制帧协议),适合双向高频交互(IM、协同编辑、游戏)。AI 场景选 SSE 的原因:生成是单向流式推送,不需要客户端在流上回传;SSE 走标准 HTTP/HTTPS,天然复用现有网关、鉴权、CDN、日志与压缩体系,实现与排障成本都低;流式响应天然是「一问一答」,全双工的复杂度没有必要。代价是每个连接占用一个 HTTP 连接、HTTP/1.1 下同域连接数有限(HTTP/2 下多路复用可缓解),以及需要自己处理心跳与超时。

慢启动、拥塞避免、快速重传、快速恢复:TCP 的拥塞控制四个阶段。慢启动:cwnd 从 1~10 个 MSS 开始,每收到一个 ACK 就翻倍(指数增长),快速探到链路容量;到 ssthresh 后进入拥塞避免:每个 RTT 只加 1 个 MSS(线性增长),避免过快压垮链路;一旦超时重传,判定拥塞严重,ssthresh = cwnd/2、cwnd = 1,重新慢启动——这是快速重传/快速恢复要解决的问题:超时并不一定意味着严重拥塞。快速重传:收到 3 个重复 ACK 就立刻重传丢失报文,不用等 RTO 超时;快速恢复:既然还能收到重复 ACK 说明网络仍在传数据,于是 ssthresh = cwnd/2、cwnd = ssthresh + 3 MSS(拥塞窗口减半但不回到 1),继续拥塞避免。配合的还有流量控制(接收方 rwnd,滑动窗口,零窗口探测)——流量控制保护接收方,拥塞控制保护网络,答题时务必把这两者区分开。

401 并发刷新与重复重试:核心是把刷新动作收敛成一个单例 Promise。做法:模块级维护 refreshPromise 和 isRefreshing 状态;拦截器里遇到 401 时先判断是不是刷新接口本身(刷新接口返回 401 就直接登出,防止死循环),若 refreshPromise 已存在就 await 它(每个请求排队等待),否则发起一次刷新并把 Promise 存起来;刷新成功后清空 refreshPromise 并用新 token 重放原请求,失败则清空登录态跳登录页。重放要打标记(如 config._retry = true)保证只重试一次,避免「401 → 刷新 → 又 401」的无限循环;同一时刻的请求还可以做去重队列(成功的统一 resolve,失败的统一 reject)。多标签页场景可用 BroadcastChannel 或 localStorage + storage 事件让刷新跨页只发生一次;更稳的方案是刷新 token 一次有效 + 服务端做刷新令牌轮换,前端只负责收敛并发。

一次完整的 RAG 是什么样:① 用户提问;② 可选改写——多轮对话时先用历史把「它呢?」补全成独立问题,再做同义/多语言扩展,拆成多个子查询;③ 检索——把问题向量化,在向量库做近似最近邻检索(HNSW/IVF-PQ,度量常用余弦或内积),同时可跑关键词检索(BM25)做多路召回,再用 RRF 或加权分数融合,必要时加 rerank 模型精排;④ 过滤与截断——按权限、时间、来源过滤,按 token 预算截断并做去重(同一文档的相邻片段合并);⑤ 组装提示——把片段带来源标记拼进上下文,配合系统提示约束「只依据给定材料回答、无依据就说不知道」;⑥ 生成——流式输出,并在前端展示引用来源;⑦ 后处理与评估——答案与引用一致性校验、敏感词与安全过滤、埋点记录召回率/命中率/badcase 回流;⑧ 索引侧离线维护——文档解析、按语义切块(带重叠)、embedding、增量更新与失效删除。前端在这条链路里负责的通常是流式渲染、引用高亮、停止生成与重试、反馈按钮。

Top-K 与 Top-P 怎么调:两者都作用在采样阶段,但形状不同。Top-K 取概率最高的 K 个候选,是固定数量截断,缺点是在概率分布平坦时可能截掉合理词、在分布尖锐时又引入了过多噪声;Top-P(核采样)取累计概率达到 P 的最小候选集合,是自适应数量截断,分布尖锐时候选很少、平坦时放开更多,因此更适合作为默认手段。工程调法:需要稳定、可复现(如结构化输出、代码)就把 temperature 调低(00.3)并收紧 top_p(0.70.9);需要创意/多样性(文案、头脑风暴)提高 temperature(0.81.0)并放宽 top_p(0.90.95);top_k 一般作为兜底上限(如 20~50)与 top_p 同时开,注意不同厂商对两者的生效顺序和是否同时生效的实现不一致,通常建议「温度为主、Top-P 微调、Top-K 兜底」,且只在同一模型内做对比,改完要用固定评测集回归,不要凭单条输出调参。

手撕:并发限制的异步调度器:思路是维护 running 计数与等待队列,add 时若未达上限直接执行,否则入队;每个任务 finally 里递减计数并取出下一个排队任务执行。要点是异常也要释放名额(放 finally)、返回的 Promise 要能拿到各自任务的结果、任务抛错不能中断调度器。等价实现可以用 Array.from({length: limit}) 起 limit 个常驻 worker,循环从共享的任务数组里取任务(用索引/shift)直到取空,这种写法更短且天然限流。进阶追问通常是:支持动态调整并发上限、支持优先级队列、支持取消(AbortController)——回答时先说清「调度器只负责限流,任务本身的取消要靠 AbortSignal 透传」。

面试复盘:这场面试的特点是浏览器/网络基础与 AI 应用结合得很紧(SSE、首 token、RAG、向量检索),说明该团队在做 AI 相关的前端产品。准备这类面试时,把「流式渲染链路」当成一条主线来串:建连 → 首 token → 增量渲染 → 中断/重试 → 引用展示,中间任何一层的原理与优化都能延展成题目。