Shopee AI 基础设施一面:BM25、向量检索与 chunking 策略
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 介绍项目
- 讲一下 BM25 和向量检索,余弦相似度和内积的差别是什么?
- 对于不同类型的文件,chunking 的策略是什么?
- BERT 和 GPT 的区别是什么?
- Python 中
==和is的区别? - 深拷贝和浅拷贝的区别?
- Python 的 GIL 是什么?
《参考解析》
BM25 与向量检索的差别,本质是「词面匹配」与「语义匹配」。 BM25 属于稀疏检索:把文档表示成词袋向量,用 TF-IDF 的改进形式打分——词频项做了饱和处理(出现次数多到一定程度收益递减),并引入文档长度归一化,IDF 衡量词的稀有度;它的强项是精确匹配(专有名词、型号、错误码、人名),弱项是不知道同义词、改写句和上下位关系。向量检索把文档和查询都编码成稠密向量,用近似最近邻(HNSW、IVF-PQ)找距离最近的若干条,能处理语义泛化,但会「语义相近但事实不对」——比如问「退款流程」召回「退货政策」。所以生产上普遍做混合检索 + 融合(BM25 与向量各自召回后用 RRF 融合分数或加权),再用 cross-encoder 重排。余弦相似度与内积的区别:余弦等于内积除以两边模长,所以余弦只看方向、对内积的尺度不敏感;内积同时受方向与模长影响。工程含义是:如果向量做过 L2 归一化,两者排序完全等价(归一化后模长为 1);没归一化时用内积会让「长文档/高频词」的向量(模长更大)占优。所以实践中要么统一归一化后用内积(等价于余弦、计算更快,很多向量库的内积实现更快),要么明确用余弦距离;别混用,否则阈值和召回结果都对不上。
chunking 策略要按「文档结构」而不是一刀切。 常见做法:Markdown/HTML 按标题层级切(保留 # 层级作为元信息,天然带语义边界);代码按函数/类切(用 AST 或语言解析器,切在语法边界上,避免把函数切两半);表格不要跨行切(一行一条记录,保留表头作为上下文);PDF 要先做版面分析,按段落/章节切并在元信息里带页码,扫描件要先 OCR;对话/日志按会话或时间窗口切。参数上一般以「语义完整」优先,token 数控制在几百量级并设 10%–20% 重叠,重叠是为了防止答案正好跨在边界上被切断。工程上比参数更重要的是每个 chunk 带足够的元信息(来源、标题路径、页码、时间、权限标签),因为这些元信息既用于过滤,也用于最终的引用溯源。还要提一句:没有普适的最优大小,必须用评测集验证——同一套数据上切分大小对召回率的影响常常比换模型还大。
BERT 与 GPT 的区别可以从「架构、训练目标、适用任务」三层答。 架构上 BERT 是 Transformer 的 Encoder(双向注意力,每个位置都能看到左右上下文),GPT 是 Decoder(因果掩码,只能看到左侧);训练目标上 BERT 用 MLM(随机遮词预测)+ NSP,GPT 用自回归的下一个词预测;因此 BERT 类模型擅长理解型任务(分类、抽取、句子相似度、重排、Embedding),一次前向给出全部位置的表示,天然适合做 encoder 与 cross-encoder;GPT 类模型擅长生成型任务,天然支持流式输出与上下文学习,也是现在 Agent/对话的基础。工程上的取舍:Embedding 与 Reranker 通常用 BERT 系(小、快、可批量),生成用 GPT 系。补充一句现代趋势:encoder-only 与 decoder-only 的边界在模糊(decoder-only 也能做 embedding),选型还是要看延迟、成本与效果评测。
Python 的三问给的判据都很明确。 == 比较的是值相等(会调用 __eq__),is 比较的是对象身份(内存地址,即 id() 是否相同);所以 a == b 为真不代表 a is b。CPython 里小整数(-5~256)与短字符串会驻留(interning),导致 a = 256; b = 256; a is b 为 True,这属于实现细节,不能依赖;判断 None 必须用 is None(因为单例且更快)。深拷贝与浅拷贝:copy.copy 只复制最外层容器,内部元素仍是同一对象引用(改内层会互相影响);copy.deepcopy 递归复制所有层级,两者独立,代价是可能很慢并要处理循环引用(deepcopy 用 memo 字典处理);赋值 b = a 根本不复制,只是多一个引用。GIL(全局解释器锁)是 CPython 的实现细节:同一进程内同一时刻只有一个线程能执行 Python 字节码,所以多线程不能并行跑 CPU 密集任务(只能并发,靠切换改善 IO 等待),多线程在 IO 密集场景依然有效(等待时会释放 GIL);绕开方式是多进程(multiprocessing)、C 扩展里释放 GIL(NumPy 计算、zlib 等)、或用 asyncio 做 IO 并发。3.13 起有实验性的 free-threading 构建(无 GIL),但生态兼容性仍在推进。