面灵AI→

拼多多 Agent 开发一面:RAG 切块、混合检索与 BM25

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

《面试题目》

代码题

  1. 手写三数之和。
  2. 手写轮转数组。

项目与 RAG / Agent 追问

  1. 你的 chunking 方案是什么?块切得太大、切得太小分别会出现什么问题?
  2. 你项目里这套切块方案的优点和缺点是什么?
  3. Top-K 召回结果有噪声时,有哪些优化策略?
  4. 端到端延迟偏高,你的解决方案是什么?
  5. 你是否用过 jev 模型?它能不能适配你的项目场景?
  6. 你的项目里怎么做混合检索?
  7. BM25 的原理是什么?
  8. 一个 MCP 服务的全链路流程是怎样的?
  9. 你怎么评估项目的效果?
  10. 文档版本更新怎么处理?文档同时有多个版本共存又怎么处理?
  11. 你的记忆机制包含哪些信息?
  12. 反馈机制是否有效?(指用户对回答的反馈)
  13. 测试集怎么创建和评估?
  14. 你的项目有联网搜索功能吗?
  15. 项目怎么部署的,架构是什么?
  16. 沙箱的作用是什么?

反问

  1. 团队用的开发语言是什么?
  2. Agent 的实际效果和发展方向如何?

《参考解析》

切块粒度:先想清楚检索和生成要的不是同一种块

块切太大,单块里塞进了多个主题,向量被稀释成「语义平均」,相似度排序就不准;同时进上下文的 token 变多,成本和延迟一起涨,长上下文中间的信息还容易被忽略。块切太小,语义不完整、指代丢失,检索命中了也答不全,模型只能靠拼接硬凑,幻觉概率上升。工程上的折中是「小块检索、大块生成」:索引时按语义或标题层级切成小单元,命中后再回溯它的父块或前后窗口一起送进模型,块间留少量重叠防止答案正好被切在边界上。至于某套方案好不好,判断标准就三条:跨段落的答案能不能被召回、块内主题是否单一、以及解析出来的结构(标题、表格、代码)有没有被破坏——按 markdown 标题切分实现简单、结构清晰,但对超长段落和表格就无能为力,这是最常见的取舍点。

混合检索与 BM25

BM25 是词袋精确匹配的代表,本质是 TF-IDF 的改良:词频用饱和函数(参数 k1)抑制「出现一百次就等于重要一百倍」的线性增长,再用文档长度归一化(参数 b)避免长文档天然占优,有些实现还给 query 中的词加权重。它的长处是专有名词、编号、代码符号这类字面匹配,短板是不懂同义改写和语义泛化——而向量检索恰好相反。所以混合检索就是两路并行召回后融合,最省事的融合方式是 RRF(倒数排名融合):只看排名不看分数,省掉了两套分数量纲对齐的麻烦;要更精细就用带权重的分数归一化,权重按业务调。Top-K 里的噪声有几个递进的解法:先上 rerank(cross-encoder 逐对精算相关性)做精排,再用分数阈值直接截断低质结果,用 MMR 之类做去冗余,配合 metadata 过滤把明显不相关的范围排掉;再往前一步是改 query——改写、拆解、HyDE 生成假设答案再检索,以及多路召回投票。要注意 rerank 是延迟大户,条数和模型大小都得压着用。

延迟拆解、MCP 全链路与部署架构

端到端延迟要按环节拆开量:query 改写与 embedding、检索(向量库与 BM25 可以并行)、rerank、以及 LLM 生成的首 token 与总时长,不拆开就只能凭感觉优化。常见的组合拳是召回并行化、缓存(embedding 结果、高频问题的语义缓存、prompt 前缀缓存)、用一个小模型承担路由和改写、流式输出先把首 token 吐出去、控制送进模型上下文的条数与长度。MCP 的全链路是:宿主作为 client 与 server 建立连接(本地 stdio 或远程 HTTP),initialize 握手协商协议版本与能力,tools/list 拉到工具清单注入模型;模型决定调用后 client 发 tools/call 带上参数,server 执行并以 content 形式返回(文本、图片或资源引用),结果回填给模型继续推理,结束再走关闭流程。这条链路上必须自带参数 schema 校验、超时与错误回传(标记 isError 让模型自纠而不是直接崩)、鉴权与最小权限;沙箱正是在这里起作用——把模型生成的代码或高权限工具的副作用关进隔离环境,限制文件与网络访问,防止一次 prompt injection 就造成数据外泄,所以哪怕当前项目里它「没有实际作用」,也值得说清它在什么情况下是必需的。部署架构通常是不挑会话的无状态服务多副本加网关,向量库、关系库、Redis 各自独立,文档解析与 embedding 入库这类重活走异步任务队列,避免拖慢在线请求。

文档版本、记忆、评估与反馈

文档多版本共存的核心是给每个 chunk 打上 doc_id + version + status,检索时按 metadata 过滤只命中当前生效版本;更新用「先写新版本、再切生效指针」的方式,出问题能一键回滚,旧版本同时还能支撑「某个时间点的历史版本问答」这类需求。删除要软删并定期清理向量,否则删了还能被检索出来是最常见的脏数据事故。记忆机制要分层说清:会话内是最近 N 轮原文加滚动摘要,跨会话是用户画像、偏好与历史结论这类事实槽位;每一类都要回答什么时候写、什么时候召回、新旧冲突谁覆盖谁、过期怎么失效,只答「存了哪些信息」会显得没设计过。反馈机制是否有效,判据不是有没有点赞点踩按钮,而是它能不能变成动作:点踩要能带上原因分类,badcase 要能被归集和抽样标注,再回流成评测集或检索、prompt 的改动,最后在下一轮评测里看到指标变化——没有闭环的反馈只是装饰。测试集要从真实日志里分层抽样(按意图、难度、是否需要多跳、有没有标准答案),检索层看 recall@k 和 MRR,生成层看要点覆盖、引用正确率和人工可用性打分;LLM 打分只做初筛、必须人工抽验,每次改动都跑回归,避免修好一个坏掉一个。

两道手写题

三数之和是排序加双指针的固定套路:排序后固定第一个数,剩下两个指针向中间收,复杂度 O(n²);真正的考点是去重——第一个数的位置要跳过重复值,双指针收缩时左右两侧也各自要去重,否则结果里全是重复三元组,另外还有首数大于 0 就直接剪枝。轮转数组的最优解是三次反转:先把 k 对 n 取模,整体反转、再反转前 k 个、最后反转剩下的,时间 O(n)、空间 O(1);用额外数组是 O(n) 空间但写起来最不容易错,环形替换法空间最优却要处理 gcd(n, k) 个环的收尾。开口先把边界说全(空数组、n 小于 2、k 为 0 或 n 的倍数),再动手,比直接写代码更容易拿分。