快手前端一面(RAG 链路与 AI Coding)
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- DFS 实时循环具体说一下?
- 那如果我要让它成环呢?
- 为什么选 pgvector?
- 为什么选 BM25?
- 为什么选 RRF?
- 多 store 分片解释一下。
- 解析缓冲和渲染缓冲分别解释一下作用。
- 节奏控制机制解释一下。
- SSE 不完整行怎么处理?
- 移动端有经验吗?
- AI Coding 环节:现场用 AI 完成题目并讲解思路。
《参考解析》
为什么选 pgvector:核心是「不引入新的存储组件」。pgvector 是 PostgreSQL 的扩展,向量以 vector 类型存在普通表里,因此能和阿里的业务数据同库同事务——权限、备份、监控、迁移全都复用现有 PG 运维体系,还能在一条 SQL 里做「向量相似度 + 业务字段过滤(WHERE)+ 排序分页」的混合查询,不用像独立向量库那样在应用层做两次查询再做结果合并。它的索引支持 IVFFlat 与 HNSW(HNSW 召回与延迟更好、建索引更慢更占内存),距离算子支持 L2、内积、余弦。选它的代价与边界也要说:数据量到千万级以上、要求极高 QPS 或需要分布式水平扩展时,PG 的召回延迟和资源隔离会成为瓶颈,这时才考虑 Milvus/Qdrant 这类专用向量库;另外 pgvector 的过滤是在索引结果上做(后过滤),高选择性过滤条件下可能召回不足,需要分区或提升 ef_search/probes 来补。
为什么选 BM25:向量检索擅长语义相似(「怎么提高页面打开速度」能召回「性能优化」),但对精确匹配很弱——专有名词、错误码、函数名、型号、人名这类低频词,embedding 往往表达不好,而 BM25 基于词频与逆文档频率,正好对这类 token 精确命中。BM25 在 TF-IDF 上的两个改进:词频饱和(k1 控制,词出现很多次也不会让分数无限增长)和文档长度归一化(b 控制),所以在长短文档混杂的语料里更稳。它还不需要 GPU 与训练、可解释(能直接看到命中了哪些词)、构建索引便宜。中文场景要配好分词器(jieba/IK)并维护领域词典,否则专有名词会被切碎;另外 BM25 无法理解同义改写,这就是必须与向量检索并用的原因。
为什么用 RRF 做融合:多路召回(向量 + BM25,甚至多路向量)得到的分数不可直接比较——余弦相似度是 01(或 -11),BM25 是无上界的正数,直接加权求和会被量纲主导,归一化又会受异常值影响、且每次查询的分布不同。RRF(Reciprocal Rank Fusion)只看排名不看分数:score(d) = Σ 1/(k + rank_i(d)),k 常取 60。优点是不需要调权重、对不同检索器的分数量纲天然免疫、对单路异常分数鲁棒,实现只要几行代码;代价是丢弃了分数的置信信息(第一名和第二名差距多大它不关心),所以有监督数据时用学习式融合(如按特征的 LTR)会更好;另外 RRF 的 k 会影响头部集中度,k 小则头部差距大。工程上再叠加一层 rerank 模型做精排,通常收益比调融合权重更大。
多 store 分片:指把不同来源/不同租户/不同模态的向量放在**独立的集合(store)**里,检索时分别查询再合并。这样做的理由:① 隔离——权限、租户、语言、文档类型彼此不干扰,召回不会被无关语料稀释(尤其是不同 embedding 模型不能混在同一个索引里);② 可运维——单个 store 可以独立重建索引、独立扩容、独立换模型,重建某个语料不影响其他;③ 可路由——按查询意图先选 store(比如先判断是问代码还是问制度),减少无效检索。代价是查询要扇出到多个 store(延迟取决于最慢的一路,要并发 + 超时 + 部分失败容忍)、结果合并要做去重与分数归一(用 RRF 正好合适)、以及要维护「路由规则 + 兜底全查」的策略。回答时把「为什么分」和「分完怎么合」两段都讲了,才算答完整。
解析缓冲与渲染缓冲:放在流式渲染的上下文里理解——SSE 来的 token 是碎片化的字符流,一到达就立刻 setState 渲染会导致高频重渲染、Markdown 结构在中间态被反复重排(比如 ** 只来了一半)、代码块高亮闪烁。常见做法是两级缓冲:解析缓冲(把字符累积成「完整的行/块」再交给 Markdown 解析器,保证语法完整,同时在这里做增量解析复用已生成的 AST);渲染缓冲(把解析结果按帧合并,用 requestAnimationFrame 或 1650ms 的时间片批量提交,控制每秒的重渲染次数)。这样做的收益是渲染次数可控、滚动不抖、长回答的渲染成本从 O(n²) 降到接近线性。顺带要提「节奏控制」:当推理速度快于渲染/网络消费速度时,需要背压——按固定间隔(如每 3050ms)从缓冲区取一批吐给 UI,而不是原文照抄;同时保留「立即显示首 token」的特例,因为首 token 的体感最重要。
SSE 不完整行怎么处理:SSE 的协议单位是「事件」,以空行分隔,字段是 data:、event:、id:、retry:,网络分片会在任意位置切断,所以不能假设一次 read() 就拿到完整事件。正确做法是维护一个文本缓冲区 + 游标:把新到达的 chunk 追加到缓冲末尾,然后按 \n\n(兼容 \r\n\r\n)切分,只有拿到以空行结尾的完整事件才解析;剩下不完整的部分留在缓冲区等下一批数据,绝不要直接丢弃或强行解析。逐步解析时还要处理:同一事件的多个 data: 行要用 \n 拼接、data: 后的前导空格要去掉一个、以 : 开头的是注释行(跳过)、收到 [DONE] 之类的结束标记要终止并关闭连接。若用 fetch + ReadableStream,还要用 TextDecoder({stream: true}) 处理多字节 UTF-8 被切断的情况(中文尤其常见,否则会出现乱码);每条事件处理要能容忍 JSON 解析失败并记录日志,而不是让整个流崩掉。
面试复盘:这场面试的题目有明显特征——「DFS 实时循环/成环」「pgvector、BM25、RRF」「多 store 分片」「解析缓冲/渲染缓冲/节奏控制」「SSE 不完整行」合起来就是一条 RAG + 流式渲染的链路,说明该前端岗位深度参与 AI 产品实现。原帖还记录了一个 AI Coding 插曲:本想自己下载题目再动手,结果 AI 下载完直接就把题做了,只能尴尬地当场向面试官讲思路。经验教训很直接——AI Coding 环节的评分点是「你的思路和取舍」,用 AI 之前先看清题目要求、把 AI 当补全而不是替考,遇到 AI 直接给出完整答案时要主动说出「我准备怎么做、为什么这么做、AI 方案哪里不对」,把过程讲出来比结果更重要。