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