面灵AI→

小米 AI 全栈开发面试:Agent 架构、RAG 与多 Agent 隔离

时间
2026-09
来源
牛客网

《面试题目》

  1. 讲一下你做过的 Agent 项目整体架构,主要解决什么问题?
  2. 项目里 RAG 是怎么实现的?文档切分策略是什么?chunk size 怎么选的?
  3. 如果文档是 Markdown 格式但没有标题,你怎么切分?有没有试过按语义切分?
  4. RAG 召回效果不好的时候,你一般从哪里开始排查?给出一个系统的排查流程。
  5. Agent 评测怎么设计?评测集怎么构建?Golden Set 怎么维护?
  6. 如果人工评测有缺陷,你怎么用模型自动化评测?评测指标关注哪些?
  7. 如果 Agent 输出 JSON 格式不稳定,怎么校验?Schema 怎么设计?
  8. 如果评测集覆盖不全,怎么发现?
  9. 多 Agent 同时执行时,争抢哪些资源?你怎么做隔离?
  10. 多 Agent 之间上下文和状态怎么隔离?共享内存还是消息传递?
  11. 如果多个 Agent 同时修改同一个文件,怎么避免冲突?有没有用分布式锁?
  12. 如果某个子 Agent 挂了,怎么保证任务不中断?
  13. 子 Agent 之间任务、上下文、输出结果如何传递给中心调度 Agent?
  14. Agent 跑了 20 分钟没结束,用户等不及了,怎么停止这个任务?超时怎么设?
  15. 如果任务执行到一半中断了,下一次怎么继续执行?断点恢复怎么设计?
  16. 状态存哪里?如果恢复后状态不一致怎么办?
  17. 如果工具调用失败,重试策略是什么?重试几次?指数退避怎么设计?
  18. Agent 单次调用成本多少?怎么优化?Token 成本从哪些维度做优化?
  19. 缓存策略怎么设计?哪些结果可缓存?缓存多久?
  20. 大小模型怎么路由?什么时候用大模型,什么时候用小模型?
  21. 如果用小模型处理复杂任务导致效果下降,怎么兜底?
  22. 如果全量开放 Agent,成本和推理侧怎么扛?
  23. 设计一个”智能行程规划 Agent”,用户输入”周末去南京玩两天,预算 2000 元”,请描述工具编排、异常处理、多轮澄清。
  24. 如果第二天南京下雨了,你的 Agent 怎么应对?
  25. 如果用户临时想把两天行程压缩成一天,怎么处理?
  26. 用户需求很模糊,比如”我想找个好吃的”,你的 Agent 怎么通过多轮对话澄清需求?
  27. 澄清的策略是什么?每轮问什么问题?怎么判断澄清结束?
  28. 你了解哪些 Agent 开发框架?自研框架和开源框架的差异是什么?
  29. 你怎么理解 Agent 中的 Harness?它和 Agent 框架有什么区别?
  30. 手撕:合并区间。给定区间列表,合并所有重叠区间,例如 [[1,3],[2,6],[8,10],[15,18]] 输出 [[1,6],[8,10],[15,18]]。追问:区间插入怎么实现?区间数量到千万级、内存扛不住怎么优化?
  31. 自我介绍。

《参考解析》

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));若坐标范围有限且是整数,用差分数组或位图表示活跃区间,能把内存压到几乎与区间数无关。