面试知识库

冲刺模拟面试 2 · 综合技术二面#

面试官画像: 大厂后端/AI 平台组 P7 工程师,拿着简历重点追项目 + 工程基础 时长: 50-55 分钟 · 难度: ★★★★★ 风格: 每个简历描述都要求”给我一个具体例子”,会往基础知识延伸


暖场(2 分钟)#

面试官: 自我介绍简短就好,一分钟。

候选人: 电子科大研二,做了两个 Agent 项目。重点说 ShoppingX——跨 6 平台 150 万商品的购物 Agent,核心解决 Agent 可靠性和延迟。已公网部署可体验。技术栈 FastAPI + LangGraph + Qdrant + OpenSearch。


第一轮 · 向量检索深挖(10 分钟)#

面试官: 150 万商品怎么存的?检索怎么做?

候选人: Qdrant 向量库。用 BGE-M3 编码商品文本得到 dense 向量,灌入 Qdrant。检索时 query 也编码成向量,做 ANN 近邻搜索,再叠多维 payload filter——platform、price_usd、rating、brand。

面试官: 为什么不用 Faiss?

候选人: Faiss 是纯 ANN 库,没有内置的 payload filter——得先查再过滤或者维护多个索引。Qdrant 原生支持向量搜索 + 标量过滤的联合查询,一次搞定。而且 Qdrant 支持落盘持久化,150 万点全 on-disk 内存占用可控。

面试官: 那精排呢?召回 Top-K 之后怎么排?

候选人: 召回 Top-K 后过 BGE-Reranker-v2-m3 cross-encoder 精排。cross-encoder 把 query 和每个候选拼在一起过一遍 Transformer,出相关性分数。比双塔的 cosine 更准但更慢,30 条约 335 毫秒。用 SiliconFlow 的 API 调用,不是本地推理。

面试官: cross-encoder 和 bi-encoder 的本质区别是什么?

候选人: bi-encoder 分别编码 query 和 doc,用 cosine 算相似度——query 和 doc 之间没有注意力交互。cross-encoder 把 query 和 doc 拼成一个序列一起过 Transformer,token 之间有完整的 self-attention——能捕捉到更细粒度的语义匹配,但无法预计算所以不能用于大规模召回。

面试官: 用了 OpenSearch 做 RAG,和 Qdrant 是什么关系?

候选人: 两层用途不同。Qdrant 做商品召回——向量搜 + 属性过滤。OpenSearch 做品类知识库 RAG——Hybrid Query(KNN 0.7 + BM25 0.3)混合检索品类爆款信息、品类属性特征。比如用户问”什么旅行包好”,先查知识库了解旅行包的关键属性维度(材质、容量、防水性),再带着这些知识去检索商品。


第二轮 · 并发与异步(10 分钟)#

面试官: 你的多 Agent 并行怎么实现的?

候选人: Python asyncio。主 Agent 通过 dispatch_tool 派发子 Agent,内部是 asyncio.create_task 创建多个子任务。子 Agent 并行跑各自的 AgentLoop,通过 asyncio.gather(return_exceptions=True) 收集结果——一个炸了不影响其他。

面试官: asyncio 的事件循环机制说一下?

候选人: 单线程事件循环。所有协程在一个线程里用 await 交替执行——IO 等待时让出控制权给其他协程。没有线程切换开销,也没有锁的问题。但 CPU 密集计算会阻塞整个循环——不过我们的瓶颈是网络 IO(LLM API 调用),正好适合 asyncio。

面试官: 如果有 CPU 密集的操作怎么办?

候选人: loop.run_in_executor 扔到线程池或进程池。但我们项目没这个需求——embedding 和 reranker 都是 API 调用,不是本地 CPU 推理。

面试官: ContextVar 了解吗?你用它做了什么?

候选人: ContextVar 是 Python 的任务局部变量——每个 asyncio Task 自动拷贝一份。我用它存 thread_id 和 session_dir,实现多会话隔离。fork 子 Agent 时 asyncio.create_task 会拷贝父的 context,子 Agent 进 thread_scope 上下文管理器设新 thread_id,退出时 reset。

关键踩坑:ContextVar 适合隔离但不适合聚合。子 Agent 设值父读不到——因为 create_task 是拷贝不是共享。需要跨子 Agent 聚合数据(比如检索预算计数),改用 module-level dict 加 session_dir 做 key。


第三轮 · 基础八股快问(10 分钟)#

面试官: 简历写了 Redis,分布式锁怎么实现?

候选人: SET key value NX PX milliseconds。NX 保证只有一个客户端能设成功,PX 设过期时间防死锁。释放时用 Lua 脚本先 GET 比对 value 再 DEL——防止误删别人的锁。Redisson 的看门狗机制会自动续期。

面试官: Redis 和 MySQL 数据一致性怎么保证?

候选人: 最常见是 Cache Aside 模式——先更新 DB 再删缓存。存在短暂不一致窗口但概率极低。如果要强一致,可以用延迟双删或者 Canal 监听 binlog 异步更新缓存。我在 ShoppingX 里没有用 Redis 做缓存——用的是 Qdrant 和 OpenSearch,但原理是相通的。

面试官: MySQL 的 MVCC 简单说下?

候选人: 多版本并发控制。每行数据有隐藏的事务 ID 和回滚指针,形成版本链。读操作通过 ReadView 判断哪个版本对当前事务可见——Read Committed 每次 SELECT 生成新 ReadView,Repeatable Read 只在第一次 SELECT 时生成。所以 RR 级别下同一事务内多次读结果一致。

面试官: 索引的 B+ 树为什么比 B 树好?

候选人: 三点。第一,B+ 树非叶子节点只存 key 不存数据,同样大小的磁盘页能放更多 key,树更矮 IO 更少。第二,叶子节点用链表连起来,范围查询顺着链表走不用回溯。第三,所有数据都在叶子节点,查询路径长度一致、性能稳定。


第四轮 · 项目弱点追问(8 分钟)#

面试官: 你的数据 Amazon 占了 99%,用户搜手机你能搜出来手机吗?

候选人: 坦率说——搜手机出配件是一个已知 bad case。根因是数据不是算法:Kaggle 数据集里手机类目下大量是手机壳、充电线。我们试了黑名单过滤和品类过滤,都失败了——因为 category 字段本身不可信,整机和配件标同一个类目。这是数据质量问题,不是检索算法能解决的。

面试官: 那评测分数可信吗?数据都偏了。

候选人: 评测种子集是手写的 20+ 条 query,覆盖了旅行装备、数码产品、日用品等品类。Judge 模型按 P0/P1/P2 三级评分。局限性我承认——种子集规模小,且没有 ground truth(没有标注”正确答案”),评的是”推荐合理性”而非”检索准确率”。如果接入 Amazon ESCI 数据集做 ground truth,能更科学。

面试官: 这个项目最大的技术债是什么?

候选人: 三个。第一,LLM 单点依赖——切换 provider 需要适配不同的 API 格式、cache 机制、tool calling 协议。第二,进程内状态——没有 checkpointer,进程挂了任务就丢了。第三,数据偏斜——Amazon 99% 导致”跨平台比价”实际上只在 Amazon 内部比。每个都有升级路径但目前没做——这是有意的 scope 控制。


第五轮 · 延迟优化(5 分钟)#

面试官: 端到端 40 秒对用户来说还是很长。还能怎么优?

候选人: 当前 96% 是 LLM 解码。继续优化的方向:第一,用更快的模型——比如 Claude Haiku 替代部分非关键调用。第二,流式输出——商品卡先出(items_preview 事件),推荐文案逐字补(summary_delta),用户感知延迟大幅降低。第三,确定性预算——限制主 loop 最多 5 轮,超了强制收尾。第四,投机解码——但这需要 provider 支持,不在我控制范围内。

面试官: 流式输出的技术方案?

候选人: WebSocket 推事件。shopping_summary 工具先输出 items_preview 事件把商品列表推给前端渲染卡片,然后 LLM 逐 token 生成推荐文案,每个 token 包成 summary_delta 事件推送。前端拿到 items_preview 立刻渲染商品卡,文案区域逐字填充。用户几乎立刻看到结果,心理等待从 40 秒降到 5-8 秒。


收尾(2 分钟)#

面试官: 有什么想问我的?

候选人: 团队现在用 Agent 的场景是什么?是内部工具提效还是面向用户的产品?实习生参与的话,从哪个模块切入比较合适?