面灵AI→

米哈游 AI Agent 一面:代码生成链路、并发隔离与手撕 LRU

轮次
一面
结果
已过
时间
2026-10
来源
牛客网

《面试题目》

  1. 介绍一下你的 Agent 代码生成链路全流程。
  2. 从用户输入到代码输出,中间经过哪些步骤?
  3. 长流程下上下文超限怎么解决?
  4. 如果 1M 上下文窗口还不够怎么办?
  5. 长流程任务中途服务重启,怎么恢复未完成的任务?
  6. 断点续传怎么设计?存了哪些状态?存在哪里?
  7. 多 Agent 同时修改同一个文件,怎么防止冲突?
  8. 有没有用分布式锁?
  9. Codebase Memory 怎么初始化和更新维护?
  10. 知识库检索在什么时机使用?具体怎么实现?
  11. 知识库检索调优做了吗?怎么调的?
  12. 代码生成有什么多 Agent 保障机制?
  13. 测试用例是怎么生成的?
  14. 大规模代码场景下 Agent 上下文怎么维护?
  15. 代码审查 Agent 怎么做的?是全量逐行扫描吗?
  16. 怎么防止用户重复创建相同的任务?
  17. 你平时常用哪些数据库?
  18. MySQL 索引底层是什么结构?
  19. 为什么选用 B+ 树而不是 B 树?
  20. B+ 树叶子节点的存储空间多大?
  21. 索引存在硬盘还是内存?
  22. Spring Bean 注入的逻辑是怎样的?
  23. 大模型的缓存机制你了解吗?
  24. 代码生成系统里长耗时 Agent 占用资源、影响并发,怎么优化?
  25. Agent 怎么实现不同任务的隔离?
  26. Agent 如何保证分析、输出结果的准确性?
  27. Agent 的评测体系、测试用例覆盖范围是怎样的?
  28. LangChain 与 LangGraph 的核心区别是什么?
  29. 你使用什么 Agent 开发框架?自研框架和开源框架的差异是什么?
  30. 线程安全的 LRU 怎么实现?
  31. 手撕:实现一个 LRU Cache,get 和 put 都是 O(1)。追问:支持泛型和过期时间怎么改?过期时间存在哪里?多线程环境下怎么保证线程安全?

具体项目追问

  1. 你的多 Agent 架构里,每个 Agent 之间怎么传递消息?
  2. 是否有人工介入?怎么控制 Agent?
  3. 有没有观测方式?
  4. 数据存在云端会有安全问题吗?
  5. 为什么采用 ForkJoinPool?

《参考解析》

  1. 代码生成链路全流程:需求理解与澄清(把模糊需求变成可验证的目标)→ 仓库检索与上下文组装(Codebase Memory 里的符号与依赖、相关文件片段、团队规范)→ 任务规划(拆成有序的 TODO,产出每一步的验收标准)→ 逐任务生成改动(输出可应用的补丁而不是整文件重写,减少误伤)→ 确定性校验(编译、类型检查、lint、单元测试)→ 失败回灌自修复(带着报错信息重试,有次数上限)→ 人工确认后提交。工程上的关键是每一步都有明确产物和校验点,且整条链路可中断、可回放。
  2. 上下文超限,以及 1M 窗口也不够怎么办:换更大的窗口只是把问题推后,不是解——真实仓库的代码量远超任何窗口,成本与延迟随长度上升,关键信息在超长上下文里还会被稀释。真正的做法是分层与按需:把上下文分成「永不淘汰的」(任务目标、硬约束、接口签名)、「按需检索的」(相关代码片段、历史决策)、「可压缩的」(对话历史、工具输出)三层;检索优先于全量——用符号表、调用图和关键词召回相关片段而不是整目录贴进去;长内容一律外置成文件或工件,上下文里只留路径和摘要;只喂 diff 触及的文件做增量上下文;再用「子 Agent 各自小上下文、只往上传结论」的分治方式,把单次决策的上下文压到很小。
  3. 服务重启后的恢复与断点续传:把流程实现成持久化的状态机,每完成一步就落一次 checkpoint,内容包括 task_id、当前节点、节点输入输出、已产出工件的引用、token 与成本。重启后按 task_id 找到最近一次 checkpoint 从中断点继续,未完成的步骤幂等重放。几个必须说到的点:步骤要幂等,同一 step 重跑不产生副作用,有副作用的外部动作(写库、提 PR、发消息)要有唯一键去重或者补偿;工件(生成的代码、日志、中间结果)落对象存储或文件系统,状态里只存引用,避免 checkpoint 膨胀;任务持有租约(lease)并续期,防止两个 worker 同时恢复同一个任务造成重复执行。
  4. 多 Agent 改同一文件的冲突与锁:最稳的做法是工作区隔离——每个 Agent 在自己的分支或独立 worktree、独立沙箱里干活,改完再合并,从一开始就不共享可写文件。如果确实要共享,就引入并发控制:乐观锁(版本号或内容 hash,提交时 CAS 比对,冲突就重读重算)、悲观锁(Redis SET key value NX PX ttl 加唯一 value 防误删、配租约续期,避免持锁者挂了导致死锁)。更实用的是按文件或目录分区,让一个文件在同一时刻只有一个 owner;冲突要显式暴露给人或让模型基于最新版本重新应用,绝不能静默覆盖。答题时补一句:能用数据库唯一约束、状态机 CAS 这类天然幂等的手段解决时,优先别上分布式锁。
  5. Codebase Memory 与知识库检索:初始化是全量扫描仓库产出结构化索引——文件树、符号表(类与函数签名)、import 与调用关系、模块职责说明、文档与规范,落到可检索的存储里(向量加关键词,需要图查询就再存一层调用图)。维护走增量:按 git diff 或文件 mtime 只重建受影响的文件,配提交钩子或定时任务刷新;每条索引带上 commit sha,和当前代码不一致就判定过期重建。检索的时机是「需要外部事实或团队约定」的时候——编码规范、接口文档、历史相似改动、架构决策记录;不要每一步都检索,噪声和成本都不划算。实现上是 query 改写、向量与关键词混合召回、rerank 精排、结果带出处。调优别凭感觉调 top_k,建一个带标注的离线集,看召回率、MRR 和最终采纳率,哪一段掉就改哪一段。
  6. 代码审查 Agent 与测试用例生成:审查不要全量逐行扫,成本和噪声都撑不住。分层来做:先用静态工具和规则(lint、类型、复杂度、SAST)过滤机械问题,模型只审需要判断的部分(逻辑错误、边界、并发、安全、与既有架构的一致性);审查范围以变更及其上下文为主(改动函数的调用方、相关测试);大 diff 分片并行审再按严重度去重汇总。测试用例要从需求或接口契约出发生成,而不是从实现倒推,覆盖正常、边界、异常和并发路径;生成完必须真的跑一遍,失败的用例要么是 bug 要么是用例本身错,需要人工裁决;被 mock 掉的假设要显式标出来。
  7. 防止重复创建任务与任务隔离:去重靠幂等键加数据库唯一约束——客户端生成 idempotency key(或对任务内容做规范化 hash),服务端落库时靠唯一索引兜底,重复请求直接返回同一个 task_id;只在应用层「先查再插」在并发下不可靠。再配前端防抖和提交按钮锁。隔离则是每个任务独立上下文、独立工作目录与沙箱,独立资源配额(CPU、内存、磁盘、token 上限),按队列调度,长任务和短任务分池,避免长任务把并发槽位占满拖垮短任务的响应。
  8. MySQL 索引与 B+ 树:InnoDB 的索引是 B+ 树——非叶子节点只存键值和指向下层的指针,叶子节点存键加数据行(聚簇索引)或主键(二级索引),所有叶子用双向链表串起来。不用 B 树的原因:B 树每个节点都要存数据,同样大小的页里能放的键更少,扇出变小、树更高,查一次要多几次磁盘 IO;范围查询和排序要在不同层级的节点间来回跳,而 B+ 树的数据全在叶子上且有序相连,范围扫描就是顺着链表走,同时任何查询的路径长度都一样,性能稳定可预期。叶子节点能存多大不用背固定数字:一页就是一个节点,InnoDB 默认页大小 16KB,所以一个叶子节点能放多少行取决于行本身多大;面试官真正想听的是扇出的估算方式——用「页大小 ÷(键长 + 指针长)」算非叶子节点的扇出,再乘上叶子能放的行数,就能解释三层 B+ 树为什么能撑住千万级数据。索引在硬盘还是内存:索引本身持久化在磁盘上的表空间文件里,访问时按页加载进 Buffer Pool 缓存,命中内存就不用读盘,所以「在内存里」只在热数据的前提下成立,冷数据依然要 IO——B+ 树的意义正是把一次查询的随机 IO 次数压到两三次。
  9. Spring Bean 注入的逻辑:容器启动时扫描注解或读取配置类,把类解析成 BeanDefinition 注册进容器;然后按定义实例化对象,做属性填充(构造器注入优先,再 setter、字段注入),接着走初始化回调(Aware 系列、@PostConstruct、InitializingBean,再经过 BeanPostProcessor 生成 AOP 代理),之后才是可用的单例;容器关闭时执行销毁回调。单例之间的循环依赖由三级缓存解决,但只对 setter 或字段注入有效,构造器注入形成的循环依赖会在启动时直接报错——这也是官方推荐构造器注入的原因之一,问题早暴露。
  10. 大模型的缓存机制:几层要分开说。① 请求内的 KV Cache,自回归解码时复用历史 token 的 K/V,避免每步重算;② 跨请求的前缀缓存(prompt cache / prefix cache),前缀完全一致时直接复用那部分 KV,能显著降低首 token 延迟和成本——工程含义是固定内容(系统提示、代码库公共部分、规范)放最前面,变化的内容放后面;③ 结果或语义缓存,相同或高度相似的 query 直接返回历史答案,但要管好时效性,改一次代码或换一次模型就要失效;④ 上下文层面的去重与分层,同一份检索结果不要每轮都重新贴。再往外还有 continuous batching、PD 分离这类提升整体吞吐的调度手段。
  11. 长耗时 Agent 的并发优化:核心是别让长任务把资源(连接、线程、沙箱槽位)长时间占住。做法是把任务拆成有界步骤、每步之间释放资源,模型调用走异步 IO 加队列,统一做限流和优先级;沙箱设 CPU、内存、磁盘配额和执行超时,超时强制回收;用 checkpoint 让任务能暂停和让出,长任务放低优先级队列、短交互走独立快通道;对单用户的并发数设上限,防止一个用户刷满全池;再用链路埋点区分瓶颈到底在模型调用、检索还是沙箱执行,别笼统地说「优化并发」。
  12. 准确性保障与评测体系:准确性不能靠「让模型更仔细」,要靠确定性校验兜住——生成的改动必须过编译、类型检查和测试,不通过就带着报错重试(有次数上限);补丁做 AST 级校验并限制可改范围(不许改测试、不许动配置);结论要求带引用出处;信息不足时要求模型显式提问而不是猜。评测分三层:组件级(检索召回率与 MRR、编译通过率、单测通过率)、端到端(任务成功率、人工采纳率或编辑距离、实际合并率)、成本与延迟(token 数、耗时、每任务花费),再加一层安全(有没有越权改动、有没有引入漏洞)。测试集从真实的、脱敏过的历史任务里构造,按难度和仓库规模分层;每次换提示词、模型或框架都跑一遍回归对比,别用单次体感做判断。
  13. LangChain 与 LangGraph:LangChain 偏组件与链,把模型、提示、工具、检索器封装成可组合的积木,用 LCEL 串起来,适合线性或简单分支的流水线;LangGraph 是以图或状态机组织 Agent 的运行时,有节点、边、共享 state、checkpoint 持久化、人工介入中断和循环,适合多步、有回环、要求可恢复的复杂 Agent。区别的实质是控制流的表达能力和状态管理,不是功能多少。自研与开源框架的取舍:开源上手快、生态全、新模型新能力跟得快,代价是抽象层厚、出错难调、升级常常有破坏性变更、上下文组装和重试语义不容易精细控制;自研可控、方便针对自己的场景做上下文管理、缓存、可观测和评测,代价是重复造轮子、维护成本高、跟进新能力慢。常见的落地方式是外围用框架、核心链路自研或只沿用框架里可替换的那一层。
  14. 手撕 LRU:泛型、过期时间与线程安全:基础结构是哈希表(key 到节点指针)加双向链表(头部最新、尾部最旧),get 命中就把节点摘下来挂到头部,put 时超出容量就淘汰尾部并同步删哈希表项,两个操作都是 O(1)。泛型用模板或泛型容器把 key/value 类型参数化即可。过期时间的做法是在节点里存 expireAt,get 时先判断是否过期,过期就惰性删除并返回未命中;同时要有兜底清理(定时任务或淘汰时的顺带清理),否则没人访问的过期 key 会一直占着内存;如果还要支持按过期时间排序,可以再挂一个按 expireAt 排序的结构(有序集合或最小堆)。多线程安全最直接的是给 get 和 put 各加一把互斥锁,锁内只做哈希和链表操作、绝不在锁里做 IO;想提升并发可以分段锁(按 key hash 分桶,每桶一把锁)或把链表操作与 map 操作合并到同一临界区避免中间态;注意用读写锁收益有限,因为 get 命中也要改链表,本质是写操作;另外返回 value 时要考虑拷贝还是引用,避免调用方拿到被并发修改的对象。
  15. 多 Agent 之间的消息传递与人工介入:Agent 之间通过一份显式 schema 的共享状态(state)或消息队列交换结构化数据,而不是把自然语言对话当接口——这样每一步的输入输出都可校验、可持久化、可在 checkpoint 里恢复;需要串行的用状态机驱动,可以并行的用 ForkJoinPool 这类工作窃取线程池把子任务拆开(回答「为什么用 ForkJoinPool」就落在分治加工作窃取能均衡长短不一的子任务、减少线程空转上,同时要说明池大小与阻塞任务的隔离,别让阻塞调用占满公共池)。人工介入一般设计成几个明确的卡点:计划确认、敏感操作前确认、失败重试到上限时转人工;控制手段是暂停/恢复/取消的指令加超时兜底。可观测性是必需的——每一步的输入输出、工具调用、耗时和 token 都落盘,能按 task_id 回放整条轨迹,否则线上问题只能靠猜。数据安全方面,代码这类敏感内容要考虑出云的边界:脱敏、分区存储、访问审计、加密与最小权限,并明确哪些内容允许发往外部模型、哪些必须走私有链路。