字节中国交易与广告全栈二面:SSE 续传、虚拟列表与 HTTP/3
- 轮次
- 二面
- 结果
- 已挂
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 自我介绍。
- 请从你的实习经历里挑一个你认为比较有亮点的项目来介绍一下。
- 你主要负责对话框那部分,包括 SSE 对话处理,是吗?
- SSE 对话续传、虚拟列表这部分是怎么做的?
- 虚拟列表的基本原理是什么?
- 从你的场景来看,什么情况下会用到虚拟列表?你是怎么做的?
- 你是从前端控制的,30 条以上开启虚拟列表,是吗?
- 那相当于切换了虚拟列表的组件?
- 如果不到 v-if 的条件、也就是 30 条的时候,不会有一个类似组件切换的过程吗?对用户体验应该有感觉吧?
- 也就是说在整个系统里对话超过 30 条时,你其实没有走虚拟列表的逻辑,只有历史记录或用户刷新时才会走到这个渲染组件,对吗?
- 屏幕区域和缓冲区是怎么设置的?
- 断点续传是怎么做的?
- Tool Calling、MCP 和 CoI 的概念区别是什么?
- 什么情况下应该直接去做 Tool Calling,什么情况下判断要走 MCP?
- 简历上写了 RAG、LangChain、LangGraph,要搭一个基础的 RAG 链路,会包含哪些流程?
- 检索策略一般怎么做?
- Rerank 重排阶段常用的策略有哪些?
- 你的 rerank 是怎么排的?拿到这些片段之后,怎么判断它的相关性比较高,为什么能得到相对精准的结果?
- 如果用模型来打分,整个 RAG 链路的成本会不会特别高?怎么解决?
- 什么情况下适合用 LangChain,什么情况下适合用 LangGraph?
- 你刚才说的 LangGraph 持久化逻辑,具体是怎么实现的?
- 给出一段代码,说出它们的输出顺序以及打印的结果。
- 在这个文档里面,我不希望 script 脚本阻塞它的执行,有哪些方法?
- defer 和 async 有什么区别?
- 请从浏览器渲染的角度介绍一下,这一段 HTML 到浏览器之后、到最终渲染出像素,中间会经过哪些阶段?
- stacking context 这个概念有了解过吗?
- 在刚才聊的这个流程里面,元素谁在上谁在下,是在哪个阶段发生的?
- 什么情况下元素会被提升到合成层?
- 什么情况下会出现层爆炸?
- HTTP/3 和 HTTP/2 主要有哪些特性?
- HTTP/2 已经做了二进制分帧和多路复用,为什么还会有阻塞?你说的阻塞是什么问题?
- HTTP/3 换成了 UDP,UDP 怎么保证原有 TCP 的可靠传输?
- TCP 的拥塞控制是怎么做的?
- 如果碰到错误,或者到达实际拥塞了,比如窗口一直翻倍之后,会怎么样?
- 快重传和快恢复是什么?
《参考解析》
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 则基于带宽与时延估计而不是丢包。能把”丢包即降窗会让高带宽高时延链路上的吞吐偏低”这个动机说清,比背算法名字更有说服力。