美团AI全栈二面:RAG切块、多Agent协作与缓存一致性
- 轮次
- 二面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 手撕:实现 LRU 缓存。(追问:怎么支持泛型?怎么加过期时间?怎么保证多线程安全?读写锁会不会饿死写线程?)
- 做一下自我介绍,重点讲一个你主导的项目。
- 你这个智能运维 Agent 为什么做?解决什么痛点?
- 系统整体运转流程讲一下,中间件选型依据是什么?
- 为什么用 RAG?现在上下文窗口很大,为什么不直接把原始文档都给 Agent?
- RAG 和「把文件交给 Agent,让它自己 grep 检索」各有什么优劣?
- RAG 里切块、索引、存储、检索、排序,哪些步骤最影响最终效果?
- Markdown 文件没有标题,怎么切分?
- chunk size 怎么定?切太大、切太小分别有什么问题?
- BM25 关键词检索和向量检索各有什么优缺点?
- 两路召回结果怎么融合?RRF 的 k 值怎么设?
- 短 query 和长 chunk 向量化后怎么比较?有没有做过对比?
- 如果知识库里没有答案,怎么让模型不乱说?
- 多 Agent 协作时,任务怎么分配?状态怎么管理?结果怎么汇总?
- 多 Agent 并行执行,怎么防止上下文和状态互相污染?
- 工具调用失败、超时、重复调用,分别怎么处理?
- 设计一个「智能行程规划 Agent」:用户说「周末去南京玩两天,预算 2000」,你怎么做工具编排?
- 如果第二天南京下雨,Agent 怎么应对?
- 如果用户临时想把两天压缩成一天,怎么处理?
- 用户需求很模糊,比如「我想找个好吃的」,怎么做多轮澄清?
- 设计一个「电商搜索排序 Agent」,用户搜「连衣裙」,你怎么设计排序模型?特征有哪些?
- AI Coding:给你 query 和商品特征,输出排序分数,要提升 NDCG@10,你会怎么搭链路?
- 缓存穿透、缓存击穿、缓存雪崩的区别是什么?分别怎么解决?
- Redis 分布式锁怎么实现?锁过期了业务还没执行完怎么办?
- 先更新数据库再删缓存,还是先删缓存再更新数据库?为什么?
- MySQL 索引底层为什么用 B+ 树?和 B 树有什么区别?
- 如果 Agent 服务挂了,怎么保证用户无感知?有没有做熔断、降级、重试?
- 你怎么评估一个 Agent 的效果?准确率、召回率、转人工率、端到端成功率怎么设计?
《参考解析》
手撕 LRU 的追问链:基础版是哈希表 + 双向链表,哈希表 O(1) 定位节点,双向链表 O(1) 完成「移到头部」和「淘汰尾部」。泛型的做法是把节点类参数化成 Node<K, V>,HashMap<K, Node<K, V>> 做索引,注意 equals/hashCode 的契约。加过期时间是在节点上挂 expireAt,get 时先判过期、过期即摘除并返回 null,再配一个后台线程或惰性清理防止死节点堆积内存——惰性清理省事但会让内存高峰滞后,定时清理要控制扫描频率。多线程安全有三档:直接给 get/put 加 ReentrantLock(最简单,但读也被串行化);用 ConcurrentHashMap 做索引再加分段锁或 synchronized 锁桶;读多写少时上读写锁。读写锁的经典追问是写饥饿——读锁持续被抢会让写线程无限等待,对策是公平模式(new ReentrantReadWriteLock(true),代价是吞吐下降)或给等待中的写线程提权、设读锁持有上限。面试里比较稳的收尾是补一句:真正的生产缓存(Caffeine)用的是 W-TinyLFU 淘汰加窗口分段,比手写 LRU 抗扫描能力强。
RAG 与长上下文的取舍:上下文窗口再大也有三个问题:成本和延迟随 token 线性上涨;无关文档会稀释关键信息,出现 Lost in the Middle——模型对上下文中段内容的注意力明显弱于首尾;而且长上下文下的推理质量并不稳定。RAG 的价值是只把最相关的片段注入,token 少、延迟低、信噪比高。但 RAG 不是万能的:索引有构建与维护成本,文档更新要重建或增量更新,切块会丢上下文。所以真正的答案是混合——知识库这类相对稳定的语料走 RAG;代码仓库这种频繁变更的场景直接让 Agent grep 最新文件更靠谱;更进一步的 Agentic RAG 是让 Agent 自己决定这一轮该检索还是直接读文件。
RAG 里最影响效果的环节:切块和排序。切块决定召回的基本单元,粒度错了后面全错——按固定长度切会把代码块、表格、条款拦腰截断,检索到的片段本身语义已经不完整。排序决定注入上下文的顺序,最相关的必须落在开头或结尾,否则被 Lost in the Middle 吃掉。可以量化的经验:切块从固定长度改成按语义边界切,召回率能抬 20 个点以上;排序上加一层 Reranker(Cross-Encoder 精排)再提 10 个点左右。Markdown 没有标题时按空行分段,代码块、表格、列表项、引用块整体保留不切——这些结构被切断语义就废了,宁可超一点 chunk size 也要整块留下。chunk size 要按文档类型定:技术文档 512 tokens 左右,合同类 256 tokens 左右,代码按函数切;且要留 10%~15% 的重叠窗口防止跨块句子被截断。256/512/1024 对比下来,512 在 Recall@5 和 MRR 上通常最平衡——256 召回高但精确率低,1024 精确率高但召回掉。
BM25 与向量的融合、RRF 的 k:BM25 强在精确匹配,专有名词、数字、代码符号都敏感,无训练成本、可解释;弱在语义泛化,同义词匹配不到。向量检索强在语义,能匹配同义与相关概念;弱在对低频词和稀有专名不敏感,还依赖 embedding 模型的训练/推理成本。两者互补,生产上走混合检索。融合用 RRF(Reciprocal Rank Fusion):score = Σ 1/(k + rank_i),只吃排名不吃分数,所以不需要把 BM25 分和余弦相似度做归一化——这正是它比加权求和稳的原因。k 通常取 60:k 越小头部权重越尖锐,容易把头几名的偶然性放大、长尾好结果被淹没;k 越大排名差异越被抹平。RRF 之后一定要再 Rerank,否则送进 LLM 的仍是粗排结果。短 query 对长 chunk 还有一个信息密度不对等的问题,解法是 Query 扩展(把「降价」扩写成「降价 促销 优惠 折扣」)或 HyDE(先让 LLM 生成一个假设答案,用答案的 embedding 去检索);实测 Query 扩展性价比最高,HyDE 效果略好但多一次生成开销。
防幻觉的三层防护:第一层在 prompt 里明确「信息不足就直说无法确定,不要编造」,并给 few-shot 展示拒答正例;第二层在检索侧设相似度阈值,低于阈值的片段不注入上下文,直接返回「未找到相关信息」;第三层是输出后的事实核查,把答案里的关键实体、数字、API 名抽出来与检索原文比对,检索结果里没有依据的内容标为可疑,触发重生成或降级为「暂无依据」。核心思路是逼模型「有依据地说话」,而不是靠 prompt 求它老实。评估时要能把「检索没找到」和「找到了但模型编了」分开——这两类的修法完全不同。
多 Agent 的任务分配、状态与隔离:任务分配由 Planner 把目标拆成子任务 DAG,按依赖关系决定并行还是串行,没有依赖的并行下发,有依赖的按拓扑序推进。状态分两层:全局状态(任务进度、已完成子任务、中间产物)存在 Redis 之类的共享存储里;每个 Worker 有独立的局部状态(自己的上下文与推理历史)。结果由 Supervisor 汇总,冲突时用加权投票或 LLM 仲裁,最终对外只由 Supervisor 输出一个结论。隔离是防污染的关键:上下文隔离——每个 Agent 独立上下文窗口,彼此只通过显式消息通信,绝不共享对话历史;状态隔离——全局状态对 Worker 只读,写操作统一走 Supervisor 串行化,避免并发写覆盖;资源隔离——每个 Agent 独立沙箱或命名空间,工具调用互不干扰。踩过的坑很典型:早期 Agent 共享上下文,A 的历史被 B 看见,B 的回答直接跑偏;没有全局状态时 Worker 不知道彼此进度,重复执行同一子任务。
工具调用失败的兜底:先把错误分类——超时、网络抖动、限流是可重试的,参数错误、鉴权失败、业务规则拒绝是不可重试的,后者重试只是浪费额度。可重试的错误用指数退避加抖动,最多三次,并且要有全局限流防止重试风暴打垮下游。超时处理要先看工具是否幂等:幂等才能安全重试,不幂等就返回失败让模型换方案。重复调用用「工具名 + 参数」做去重缓存在短时间内只发一次。涉及写操作(下单、支付、发消息)必须在调用前生成唯一 request_id,工具端拿它做幂等键,否则重试就会重复下单——这是 Agent 场景里最容易出真事故的地方。
缓存一致性:先更新数据库再删缓存(Cache Aside)优于先删缓存再更新库。理由是先删缓存再更新库的时间窗里,读请求会把旧值从库里捞回来写进缓存,造成长期不一致;而先更新库再删缓存,即使删缓存失败,缓存里只是旧值,下次读会从库里纠正回来,不一致窗口更短。删缓存失败的补偿有三条路:消息队列异步重试删除;订阅 binlog(Canal)在数据变更后自动删缓存;设较短 TTL 兜底。更严的并发场景用延迟双删——更新库后删一次,隔几百毫秒再删一次,盖住「读线程在两次删之间把旧值写回缓存」的窗口;或者在缓存里存版本号,写缓存时校验版本,旧版本直接丢弃。根本上要接受一个事实:缓存与数据库的强一致代价极高,工程上追求的是把不一致窗口压到业务可接受的范围。
Agent 效果评估:分三层指标。结果层看端到端任务成功率、用户满意度、平均交互轮数、单次成本、转人工率——转人工率尤其关键,它衡量的是 Agent 能力与用户信任的双重缺口。过程层看工具选择准确率(该调的调没调、参数对不对、有没有多调)、规划质量、ReAct 步数效率。系统层看 P95 延迟、错误率、token 消耗。指标选择要看场景:审核类看准确率与召回率,问答类看答案正确率与有依据率,推荐类看点击率。只盯端到端成功率的问题是定位不到病灶,所以要建黄金测试集做轨迹比对——把实际执行轨迹和预期轨迹逐步对齐,就能指出是哪一步的决策偏了。评测集本身要版本化、冻结,防止调 prompt 时把测试集过拟合掉。
Agent 服务降级:三级降级——一级切备用模型(主模型限流或故障时),二级从自治 Agent 降到固定 Workflow(能力下降但可用),三级返回友好错误页。熔断用 Hystrix/Sentinel 那套:连续失败超阈值就切断调用,过一段时间半开试探。重试遵循前面的可重试/不可重试分类。关键是降级要对用户透明——用户只感到「这次回答稍慢」,不该看到任何内部切换痕迹。配套要有监控告警,让运维知道当前处于降级状态,否则降级会静默变成常态。
限流与成本:Agent 单次调用成本高、延迟长,所以限流要比普通接口更保守。令牌桶按每秒固定速率发令牌,桶空即拒绝或排队;同时对单个用户和全局分别设配额,防止一个用户的长任务占满并发槽。延迟分析的标准做法是分段计时:把任务拆成模型调用、检索、工具调用、上下文构建几段,各自记 P50/P95/P99,一眼看出瓶颈在哪。长任务从 5 分钟涨到 20 分钟最常见的根因是上下文越滚越长导致推理时间线性上升,压缩上下文往往比扩容机器更有效。