小米 AI 全栈开发面试:Agent 架构、RAG 与多 Agent 隔离
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 讲一下你做过的 Agent 项目整体架构,主要解决什么问题?
- 项目里 RAG 是怎么实现的?文档切分策略是什么?chunk size 怎么选的?
- 如果文档是 Markdown 格式但没有标题,你怎么切分?有没有试过按语义切分?
- RAG 召回效果不好的时候,你一般从哪里开始排查?给出一个系统的排查流程。
- Agent 评测怎么设计?评测集怎么构建?Golden Set 怎么维护?
- 如果人工评测有缺陷,你怎么用模型自动化评测?评测指标关注哪些?
- 如果 Agent 输出 JSON 格式不稳定,怎么校验?Schema 怎么设计?
- 如果评测集覆盖不全,怎么发现?
- 多 Agent 同时执行时,争抢哪些资源?你怎么做隔离?
- 多 Agent 之间上下文和状态怎么隔离?共享内存还是消息传递?
- 如果多个 Agent 同时修改同一个文件,怎么避免冲突?有没有用分布式锁?
- 如果某个子 Agent 挂了,怎么保证任务不中断?
- 子 Agent 之间任务、上下文、输出结果如何传递给中心调度 Agent?
- Agent 跑了 20 分钟没结束,用户等不及了,怎么停止这个任务?超时怎么设?
- 如果任务执行到一半中断了,下一次怎么继续执行?断点恢复怎么设计?
- 状态存哪里?如果恢复后状态不一致怎么办?
- 如果工具调用失败,重试策略是什么?重试几次?指数退避怎么设计?
- Agent 单次调用成本多少?怎么优化?Token 成本从哪些维度做优化?
- 缓存策略怎么设计?哪些结果可缓存?缓存多久?
- 大小模型怎么路由?什么时候用大模型,什么时候用小模型?
- 如果用小模型处理复杂任务导致效果下降,怎么兜底?
- 如果全量开放 Agent,成本和推理侧怎么扛?
- 设计一个”智能行程规划 Agent”,用户输入”周末去南京玩两天,预算 2000 元”,请描述工具编排、异常处理、多轮澄清。
- 如果第二天南京下雨了,你的 Agent 怎么应对?
- 如果用户临时想把两天行程压缩成一天,怎么处理?
- 用户需求很模糊,比如”我想找个好吃的”,你的 Agent 怎么通过多轮对话澄清需求?
- 澄清的策略是什么?每轮问什么问题?怎么判断澄清结束?
- 你了解哪些 Agent 开发框架?自研框架和开源框架的差异是什么?
- 你怎么理解 Agent 中的 Harness?它和 Agent 框架有什么区别?
- 手撕:合并区间。给定区间列表,合并所有重叠区间,例如
[[1,3],[2,6],[8,10],[15,18]]输出[[1,6],[8,10],[15,18]]。追问:区间插入怎么实现?区间数量到千万级、内存扛不住怎么优化? - 自我介绍。
《参考解析》
RAG:切分策略与召回排查
chunk size 不是拍脑袋定的,它由三个约束夹出来:embedding 模型的有效上下文(多数模型 512 token 左右之后语义被平均掉)、答案粒度(FAQ 问答适合 200500 token,长文档总结适合 8001500 token)、以及召回后塞进生成 prompt 的 token 预算。工程上常用的做法是 300~800 token + 10%~20% overlap,overlap 是为了防止答案正好被切在两块中间。真正提效果的是按结构切(标题层级、段落、表格、代码块整体保留),定长切分只是兜底。
Markdown 没有标题时的切分:先按空行分段,再按列表项、代码围栏、表格行这些天然边界聚合到目标长度;如果还不行,用语义切分——对相邻句子算 embedding 相似度,在相似度骤降处下刀,或者用一个小模型判断”这两段是否还在讲同一件事”。语义切分成本高、块长不均匀,通常只在结构信息完全缺失(PDF 抽取、聊天记录)时用。更实用的组合是 parent-child 分块:用小块(句子级)做检索保证命中精度,命中后返回它所属的父块给模型保证上下文完整。
召回差的排查顺序,一定是先定位是检索问题还是生成问题:把召回的 chunk 原样喂给模型看它能不能答对(oracle 测试),能答对说明是检索的锅。然后按流水线逐段查:① query 侧——口语化、缩写、专有名词与文档用词不一致,解法是 query 改写、多查询扩展、HyDE;② 分块侧——切太碎丢上下文,切太大稀释语义;③ 向量侧——embedding 模型不适配领域(中文技术文档、代码符号),换模型或做微调;④ 排序侧——top-k 直接取向量相似度往往不够,加 cross-encoder rerank 通常是最便宜的一档提升;⑤ 召回通道——纯向量对精确串(报错码、函数名、型号)很弱,补一路 BM25,再用 RRF 融合;⑥ 过滤侧——用 metadata(时间、模块、版本)把候选范围缩小;⑦ 生成侧——prompt 里明确要求”只依据给定资料回答并给出来源”,并检查是不是上下文被截断了。
Agent 评测与结构化输出
Golden Set 的来源要两条腿走路:线上真实 badcase 回填(占大头,最有区分度)+ 按能力维度人工构造(覆盖长尾和边界)。每条样本至少要有输入、期望行为(不是逐字答案,而是要点 rubric:必须命中哪些事实、禁止出现什么、格式要求),以及难度和场景标签。维护的关键是版本化 + 回归:修一个 badcase 就回填一条,每次改 prompt / 换模型 / 调 chunk 都跑全量回归,指标按桶对比而不是只看总分,否则改好一个坏三个看不出来。
人工评测的缺陷是贵、慢、不一致,所以用模型做评审(LLM-as-a-judge):固定评审 prompt、给出参考要点、要求逐条打分并说明理由,输出结构化分数;位置偏差用随机交换 A/B 顺序抵消,长度偏差用长度归一化或成对比较缓解;上线前必须拿几百条人工标注和模型评审算一次一致率(比如 Cohen’s kappa),一致率低就迭代 rubric 而不是直接信它。指标分层看:检索层 recall@k / MRR,工具层工具选择正确率与参数正确率,生成层忠实度(faithfulness,答案是否都能在资料里找到依据)、答案相关性、格式合规率,系统层端到端成功率、P95 延迟、单次成本。覆盖不全怎么发现:把评测集按意图和场景分桶,和线上真实流量的分布做对比,哪个桶的样本占比严重低于流量占比就是空白区;再对线上 badcase 做 embedding 聚类,看有没有整簇场景压根没进评测集。
JSON 不稳定要三层防线:第一层用模型原生能力,function calling / JSON mode / 受约束解码(grammar、outlines)比 prompt 里写”请输出 JSON”可靠得多;第二层是强校验,JSON Schema 里把 required、类型、enum 写全,加 additionalProperties: false,不要让模型自由发挥字段名;第三层是容错解析与重试,先做片段提取和修复(剥掉 JSON 代码围栏、去尾逗号、补括号),失败就把校验器的报错原文回填让模型自修正,重试上限 2~3 次;仍然失败就降级——拆成多次单字段生成,或者转人工/返回可读错误。校验永远放在服务端,模型输出的工具名不能直接拿去路由函数。
多 Agent 的隔离与冲突
多 Agent 争抢的资源是:LLM 的并发与速率配额、外部 API 的 QPS 与配额、工作目录与文件、数据库连接与行锁、共享状态库/内存、GPU。隔离的总体原则是状态私有 + 消息传递,而不是共享内存:每个子 Agent 持有自己的上下文和 scratchpad,只通过中心调度器(或消息总线)交换任务、结果和摘要;上下文隔离靠”传摘要不传全量历史”,否则中心 Agent 的上下文会被子 Agent 的过程日志撑爆,且互相污染。
多个 Agent 改同一个文件,最好的解法是不让它们共享可写文件:各自在独立工作目录产出 diff/patch 或版本化产物,由调度器串行合并;必须共享时用租约(lease)而不是长时间持锁——Redis SET key value NX PX 30000 加 fencing token(每次加锁自增版本号),写入时校验 token 单调递增,防止”锁过期后老持有者继续写”这种分布式锁经典漏洞;再叠加乐观并发(文件/记录带版本号,CAS 更新失败就重新读取合并)。子 Agent 挂了要保证任务不中断,靠三件事:调度器持心跳/租约探测存活;长任务拆成可独立重试的原子步骤、中间产物落盘,失败只重跑当前步;重试到上限就把该子任务标记失败并把结构化错误上抛,让中心 Agent 决定换 Agent、换模型还是降级返回部分结果。工具调用重试只针对可重试错误(超时、429、5xx、连接重置),参数错误和 4xx 不重试;指数退避带抖动(base 0.5s 起、×2、上限 1030s、±20% jitter),重试 23 次并设整条链路的重试预算;写操作必须带幂等键,服务端按键去重,避免重试变成重复下单。
超时取消与断点恢复
超时分两层:整个任务一个总 deadline,每一步(每次 LLM 调用、每次工具调用)一个短超时,并用剩余预算做动态收敛。取消一定要协作式:把 cancellation token / context 一路传进 LLM 客户端和工具执行器,主循环每步检查一次;不可中断的阻塞调用是取消失效的常见根因,工具侧要能主动 kill。用户取消后不要直接丢弃,返回已完成步骤和部分结果,并把这次运行完整落盘以便恢复。
断点恢复的骨架是状态机 + append-only 事件日志:每一步记录步骤 id、输入摘要、输出/产物引用、工具调用的幂等键、当前计划版本、状态迁移。恢复时先对账——拿幂等键去查外部副作用是否已经生效,再决定这一步是跳过还是重跑,绝不能盲目重放;然后从最后一条”已完成”事件重放,把未完成步骤继续跑。状态一致性靠”一步一个事务”保证:状态写入和副作用要么在同一事务里,要么用 outbox + 幂等键;状态存哪取决于规模,单机用 SQLite/Postgres 行级状态表就够,多实例要 Redis + DB 双层(Redis 做热状态与租约,DB 做持久真相),关键是只有一个写者(按任务 id 分片加锁),否则恢复时会出现两个 worker 同时推进同一任务。恢复后仍不一致时,以外部系统的实际状态为准做补偿(重新拉取、幂等重放),而不是相信本地缓存的状态。
成本、缓存与大小模型路由
Token 成本从三个维度压:输入侧——精简 system prompt、工具列表按需注入而不是一次塞 50 个、历史做摘要压缩、把稳定不变的前缀放在最前面吃 prompt cache(Anthropic/OpenAI 都按前缀命中打折);输出侧——限制 max tokens、要求紧凑输出、能用结构化字段就别让模型写长篇解释、非必要不用长 CoT;调用次数侧——合并同质步骤、没有依赖的工具调用并行、能从确定性代码算出来的就别问模型。Agent 单次成本要按”任务”而不是”调用”算:建一个每次运行的 token/费用埋点,按任务类型和模型分组看 P50/P95,否则永远不知道贵在哪一类任务上。
缓存设计按数据特性分:可缓存的是 embedding、检索结果、确定性工具结果(查天气以外的静态查询)、FAQ 类问答、以及 prompt 前缀的 KV cache;不可缓存的是依赖实时状态和带副作用的结果。TTL 按数据新鲜度定——embedding 与文档向量可以很长(内容变更时显式失效),检索结果几分钟到几小时,实时状态秒级或不缓存。语义缓存(把 query 向量化后查相似问过的请求)能明显降本,但阈值要设高(0.95 以上)并做参数比对,否则”北京到上海”和”上海到北京”会互相命中。路由策略是规则 + 小分类器先判:意图识别、字段抽取、query 改写、格式转换这类任务交给小模型;多跳推理、复杂规划、长上下文综合交给大模型。为了不掉点,先小后大并带质量闸门——小模型输出过低置信度自检、或校验不通过时升级到大模型重跑,同时对升级路径打埋点,用同一批评测集对比”全大模型”和”路由方案”的成功率差,差值在阈值内才敢放量。
手撕:合并区间
标准解法是排序后一次扫描,时间复杂度 O(n log n)、空间 O(n)(不含排序开销):
def merge(intervals):
intervals.sort(key=lambda x: x[0])
res = []
for s, e in intervals:
if res and s <= res[-1][1]:
res[-1][1] = max(res[-1][1], e)
else:
res.append([s, e])
return res
细节是判断条件用 s <= res[-1][1](端点相接也算重叠,要按题意确认)、合并时取 max 而不是直接覆盖(新区间可能被完全包含)。
区间插入的变体:因为原区间列表已有序且互不重叠,可以 O(n) 扫描——把结束时间早于新区间起点的直接放入结果,然后与所有重叠区间求并集(起点取 min、终点取 max),最后放入剩下的。
千万级区间的追问考的是内存:不能一次全读进内存,先做外部排序(按起点分块排序落盘再归并),或者边读边合并(前提是数据已按起点有序,可在数据源侧/索引侧保证);如果还要支持随机查询,改用区间树或线段树(查询 O(log n + k));若坐标范围有限且是整数,用差分数组或位图表示活跃区间,能把内存压到几乎与区间数无关。