面灵AI→

字节中国交易与广告全栈二面:SSE 续传、虚拟列表与 HTTP/3

轮次
二面
结果
已挂
时间
2026-09
来源
牛客网

《面试题目》

  1. 自我介绍。
  2. 请从你的实习经历里挑一个你认为比较有亮点的项目来介绍一下。
  3. 你主要负责对话框那部分,包括 SSE 对话处理,是吗?
  4. SSE 对话续传、虚拟列表这部分是怎么做的?
  5. 虚拟列表的基本原理是什么?
  6. 从你的场景来看,什么情况下会用到虚拟列表?你是怎么做的?
  7. 你是从前端控制的,30 条以上开启虚拟列表,是吗?
  8. 那相当于切换了虚拟列表的组件?
  9. 如果不到 v-if 的条件、也就是 30 条的时候,不会有一个类似组件切换的过程吗?对用户体验应该有感觉吧?
  10. 也就是说在整个系统里对话超过 30 条时,你其实没有走虚拟列表的逻辑,只有历史记录或用户刷新时才会走到这个渲染组件,对吗?
  11. 屏幕区域和缓冲区是怎么设置的?
  12. 断点续传是怎么做的?
  13. Tool Calling、MCP 和 CoI 的概念区别是什么?
  14. 什么情况下应该直接去做 Tool Calling,什么情况下判断要走 MCP?
  15. 简历上写了 RAG、LangChain、LangGraph,要搭一个基础的 RAG 链路,会包含哪些流程?
  16. 检索策略一般怎么做?
  17. Rerank 重排阶段常用的策略有哪些?
  18. 你的 rerank 是怎么排的?拿到这些片段之后,怎么判断它的相关性比较高,为什么能得到相对精准的结果?
  19. 如果用模型来打分,整个 RAG 链路的成本会不会特别高?怎么解决?
  20. 什么情况下适合用 LangChain,什么情况下适合用 LangGraph?
  21. 你刚才说的 LangGraph 持久化逻辑,具体是怎么实现的?
  22. 给出一段代码,说出它们的输出顺序以及打印的结果。
  23. 在这个文档里面,我不希望 script 脚本阻塞它的执行,有哪些方法?
  24. defer 和 async 有什么区别?
  25. 请从浏览器渲染的角度介绍一下,这一段 HTML 到浏览器之后、到最终渲染出像素,中间会经过哪些阶段?
  26. stacking context 这个概念有了解过吗?
  27. 在刚才聊的这个流程里面,元素谁在上谁在下,是在哪个阶段发生的?
  28. 什么情况下元素会被提升到合成层?
  29. 什么情况下会出现层爆炸?
  30. HTTP/3 和 HTTP/2 主要有哪些特性?
  31. HTTP/2 已经做了二进制分帧和多路复用,为什么还会有阻塞?你说的阻塞是什么问题?
  32. HTTP/3 换成了 UDP,UDP 怎么保证原有 TCP 的可靠传输?
  33. TCP 的拥塞控制是怎么做的?
  34. 如果碰到错误,或者到达实际拥塞了,比如窗口一直翻倍之后,会怎么样?
  35. 快重传和快恢复是什么?

《参考解析》

SSE 对话续传:断线之后怎么接着收

SSE(text/event-stream)是单向的服务端推送。直接用 EventSource 时浏览器自带重连,并把最后收到的 id: 值通过 Last-Event-ID 请求头带回来,服务端据此从断点继续推——这是续传的标准形态。但大模型对话常用 fetch 加 ReadableStream(需要 POST、需要自定义头、需要中途取消,而 EventSource 只支持 GET 且不能改请求头),这时重连要自己实现:客户端记录已收到的消息序号作为游标,断线后带上游标重新发起,服务端从该序号继续生成或从缓冲里补齐,前端按序号去重与排序,避免重复插入或乱序。几个容易踩的点:① 续传必须幂等,靠消息 ID 去重,而不是假设”重连不会重复”;② 生成中途断线,服务端要么继续生成并缓存结果,要么让客户端用同一个请求 ID 查询任务结果(生成任务与连接解耦),否则用户会看到回答停在一半;③ 心跳(注释行或定时 ping)既防中间代理因空闲掐断连接,也让客户端更快发现连接已死;④ 代理层要关掉响应缓冲(Nginx 的 proxy_buffering off、X-Accel-Buffering: no),否则事件被攒着一起发,“流式”就没了。

虚拟列表:可视区、缓冲区与”30 条才启用”的取舍

虚拟列表只渲染可视区域内的元素:容器高度固定,监听滚动算出起始下标,只渲染 [startIndex - buffer, endIndex + buffer] 这一段,用上下两个撑高的占位元素(或用 transform: translateY)把滚动条长度维持成全部内容的高度。缓冲区在可视区上下各多渲染若干条,防止快速滚动时白屏:太小会闪,太大就增加渲染与内存开销。定高实现最简单,能直接算下标;不定高要先按估算高度渲染,再用 ResizeObserver 回填真实高度并修正偏移,一般还要缓存已测量的高度,工程复杂度明显上升。

整场被反复追问的那个点很关键:如果只在”超过 30 条”时切换到虚拟列表组件,那么 30 条前后就是两套渲染路径,切换组件会丢失 DOM 状态(滚动位置、动画、输入框焦点、展开态),用户会有明显跳变感;而”只有历史记录或刷新时才走虚拟列表”意味着新消息追加阶段根本没享受到虚拟化,长会话照样卡。更稳的做法是始终用同一套虚拟列表、只调参数,追加新消息时保持滚动锚点(在底部时自动跟随,用户上滑后不打断),并给一个显式的”回到底部”按钮。

Tool Calling 与 MCP 的边界

Tool Calling 是模型输出结构化的调用意图(工具名加参数),由宿主程序执行并把结果回填,本质是一套 schema 约定加解析。MCP 在它之上解决”工具从哪来、怎么被发现”:工具、资源与提示词由 MCP server 暴露,客户端通过 JSON-RPC 列出并调用,于是同一份工具实现可以给多个宿主复用。判断标准是复用范围与所有权:工具只服务自己这一套系统,直接内置 Tool Calling 更简单、少一层进程间通信、调试容易;工具要跨应用共享,或背后是别人维护的服务(数据库、内部平台、第三方 SaaS),标准化接入才划算。被追问”什么时候选哪个”时,能同时讲成本与收益(多一层协议就多一份部署与鉴权成本)比背概念更讨喜。

RAG 链路、rerank 策略与成本控制

基础链路是:解析 → 切块 → 嵌入 → 索引 → 查询改写 → 召回(向量、关键词或混合)→ 重排 → 组装上下文 → 生成。rerank 是”粗召回之后的精排”:先用便宜的方式召回几十到上百条候选,再用更贵但更准的模型给候选打分排序,只把前几条放进上下文——两阶段的意义就是把昂贵计算限制在少量候选上。常见策略有几类:交叉编码器(把 query 与文档拼在一起过模型,效果通常最好但在线推理贵)、用 LLM 直接打分或排序(灵活、可解释,但延迟与 token 成本高)、轻量模型或统计特征(BM25 分数、词覆盖率、位置权重),以及专门训练的 rerank 模型。

成本控制的手段:严格限制进入 rerank 的候选条数;缓存相同查询的 rerank 结果与相同文档的分数;把打分模型量化或换小模型;批量化打分,别一条一次请求;对简单问题走廉价路径、只对难题加重排;用离线评估证明”加 rerank 到底值不值这点成本”。评估要分开看召回质量(Recall@k)与最终答案质量,否则无法判断该优化哪一层。

LangChain 与 LangGraph:编排与持久化

LangChain 的定位是组件库加链式编排:模型、提示词模板、输出解析器、检索器、工具各自封装成可组合的积木,用表达式把线性或分支流程串起来,适合”输入经过若干固定步骤得到输出”的场景,上手快、生态广。LangGraph 把流程显式建模成有状态的图:节点是函数、边是转移(支持条件边与环),核心能力是状态管理、持久化(checkpointer)、人工介入与时间旅行。持久化表现为每一步之后把状态快照写进存储(内存、Postgres、Redis 等),于是同一会话可以按 thread_id 恢复、可以在某一步暂停等人确认再继续、失败后从最近的检查点重放。判断标准很直观:流程是固定 DAG、没有循环和中断需求,用 LangChain 的链就够了;需要多轮工具循环(ReAct 类)、要中途人工审批、要断线续跑或长任务,就值得上 LangGraph。两者不是竞品,实际项目里常见的是节点内部用 LangChain 的组件、外层用 LangGraph 编排。

浏览器渲染管线、堆叠上下文与合成层

从 HTML 到像素大致经过:解析 HTML 建 DOM、解析 CSS 建 CSSOM(script 默认阻塞解析,defer 等文档解析完按序执行,async 下载完就执行且不保证顺序)→ 合并成渲染树(只含可见元素)→ Layout 计算几何位置与尺寸 → Paint 生成绘制指令 → Composite 合成最终画面。

堆叠顺序(stacking context)决定”谁在上谁在下”:它形成于根元素、带 z-index 的定位元素、以及设置了 transform、filter、opacity 小于 1、will-change 等属性的元素,每个堆叠上下文内部独立排序——所以元素先后的判定发生在绘制阶段,而层级信息与几何在布局阶段就已收集确定。z-index 只在同一堆叠上下文内有效,父子之间比 z-index 常常无效,这是前端高频踩坑点。

合成层提升的触发条件包括 transform: translateZ(0) 之类的 3D 变换、will-change: transform/opacity、video/canvas、position: fixed、backdrop-filter 等。好处是动画只走合成器、不触发重排重绘;代价是每层都要独立的显存与合成开销(纹理上传、额外内存)。层爆炸就是滥用提升的结果:给列表里成百上千个元素都加 will-change,显存被吃光、合成时间暴涨,反而更卡。正确做法是只提升真正在做动画的少数元素、动画结束移除 will-change,并用 transform/opacity 而不是 top/left 做动画。

HTTP/2 的队头阻塞、HTTP/3 的 QUIC 与 TCP 拥塞控制

HTTP/2 引入二进制分帧、多路复用、头部压缩与服务器推送,用一条 TCP 连接承载多个流,解决的是 HTTP/1.1 的应用层层头阻塞(响应必须排队)。但它没解决传输层的队头阻塞:TCP 要求字节流按序交付,任何一个包丢失,后面所有流的数据都要在内核缓冲区里等重传,多路复用反而把一条连接上的所有请求绑在了一起。HTTP/3 把传输换成基于 UDP 的 QUIC,在用户态实现流控、重传与拥塞控制,每个流独立排序与重传,丢包只阻塞那一个流;顺带还有更快的握手(集成 TLS 1.3,1-RTT 甚至 0-RTT 恢复)与连接迁移(用连接 ID 而不是四元组标识,切网不掉线)。代价也要知道:UDP 常被中间设备限速、用户态协议栈 CPU 开销高于内核 TCP、可观测性与排障工具生态不如 TCP。

TCP 拥塞控制方面,慢启动按指数增长(每 RTT 翻倍)直到阈值,之后转拥塞避免线性增长;用超时判断拥塞时把阈值砍半、窗口重置为 1 重新慢启动;收到三个重复 ACK 则触发快重传(不等超时立即重传丢失段)与快恢复(阈值砍半,窗口设为阈值加 3 MSS 后直接进入拥塞避免,不再回到慢启动)——这也是 Tahoe 与 Reno 的分水岭。现代内核默认 CUBIC(用三次函数替代线性增长),BBR 则基于带宽与时延估计而不是丢包。能把”丢包即降窗会让高带宽高时延链路上的吞吐偏低”这个动机说清,比背算法名字更有说服力。