面灵AI→

字节跳动剪映AI前端一面面经(Agent框架+性能优化)

轮次
一面
base
深圳
时间
2026-09
来源
牛客网

《面试题目》

  1. 项目是个人项目还是公司项目?为什么不用 LangGraph 等开源框架,要自研?
  2. 讲一下 ReAct 循环的完整过程,用户发一个任务到完成中间经过哪些处理?
  3. 工具调用输出不是标准 JSON 怎么办?
  4. 多 Agent 怎么拆分?子 agent 是预定义的还是主 agent 动态创建的?
  5. 超步数熔断是固定步数吗?
  6. 如果 agent 陷入循环(没到最大步数,但前 10 步一直在重复同一操作),怎么解决?
  7. 上下文怎么管理?怎么避免超出模型上限、注意力被稀释?
  8. SSE 协议下用户断网了怎么重连?
  9. MCP client 和 server 的职责划分是怎样的?
  10. 怎么看待 MCP 和 Skill 的关系?
  11. 用户反馈 Agent 卡死不动了,怎么排查?
  12. React 页面卡顿有哪些优化手段?
  13. 线上性能怎么监测?
  14. 掉帧(FPS)怎么检测?
  15. 除了路由懒加载、资源预加载,还有什么手段优化首屏耗时?
  16. 了解 SSR 吗?讲一下原理。
  17. 性能优化做好之后,怎么防止随项目迭代劣化?比如别人改完代码把收益抹平了。
  18. 编码题:实现 LRU Cache(进阶:双向链表 + HashMap 实现 O(1))。
  19. 反问环节:团队业务方向、AI 工具使用情况、职业规划建议。

《参考解析》

ReAct 循环的完整流程:最小核心循环是「输入 → 决策 → 执行 → 回灌 → 循环」。具体为:用户消息(或上一轮的工具结果)作为输入 → 按 OpenAI 标准协议把消息列表和 tools 注册表一起传给模型 → 模型返回 tool_call(函数名 + JSON 参数)→ 本地 execution 模块路由到对应工具函数执行(本地函数或远端 HTTP/MCP)→ 执行结果作为 tool 角色消息追加到上下文 → 再次调用模型 → 直到模型返回不含 tool_call 的最终答案。工程上要在这条loop上加三道闸:max_steps(步数熔断)、超时(整体与单步)、重复检测(同样参数连续调用同一工具就注入提示或强制中断)。多 Agent 常用 supervisor 结构:主 agent 用强模型做调度,通过 prompt 把子任务派发给子 agent,每个子 agent 有自己的 prompt、工具集和适用场景,处理完把结果汇报回主 agent——本质是递归分治,好处是子任务更简单、可以换用更便宜的模型省 token。

上下文管理与压缩:作者在面试里讲的三层做法是对的,可以再讲细一点。①硬性截断:超出 token 上限时删最旧的历史,但 system prompt、工具定义、当前任务目标不能删;②加权淘汰:按引用频率/最近使用时间给消息打分,分数低的优先淘汰(本质是 LRU 思路);③分层记忆:会话记忆 / 文件夹记忆 / 系统记忆三级,把长内容写成笔记存到外部,遗忘后可以按关键词或向量检索召回(Claude Code 的 memory 就是这个形态)。压缩时的硬约束是 tool_call 与 tool_result 必须成对保留,否则 API 直接报错;另外要注意 prompt caching 的命中条件(前缀逐 token 一致),所以压缩只能动尾部、稳定内容必须放前面。SSE 断线重连:前端用 EventSource 会自动重连,但要带上 Last-Event-ID 让服务端从断点续传;或者点重试按钮时携带 session_id/thread_id,服务端从 snapshot 恢复上下文后重新开始推流。服务端侧要给每个事件带自增 id 并保留一段可重放的缓冲,否则只能整段重跑。

MCP 与 Skill 的关系:两者都在解决”让模型会用外部能力”,但机制不同。MCP 是协议层的标准化接口:Client 负责管理工具注册表、把工具 schema 传给模型、在本地或远端执行调用;Server 负责具体功能实现(文件系统、数据库、高德地图 API 等),传输方式有 stdio 和 HTTP/SSE 两种。它是强约束——工具是现成的函数,模型只能按定义好的名字和参数去调,做不了定义之外的事。Skill 更接近”读取本地的指导性文档”:把一套流程、规范、注意事项写进可被模型按需加载的文档里,模型读了之后自己决定怎么执行,属于概率性建议——“建议你这么做”,而不是”你必须这么做”。工程上的经验是:需要精确、可审计、有副作用的操作走 MCP;需要灵活判断、步骤多变的复杂流程用 Skill 承载。两者可以叠加:Skill 里说明”这一步用哪个 MCP 工具”。

Agent 卡死怎么排查:分本地和线上两条路。本地能复现时:打开 console 和断点,用二分法定位是哪个模块卡住——是模型请求没返回(网络/网关超时)、还是工具执行阻塞(下游无响应、死循环)、还是前端等待状态没被清除(事件流断在半路、状态机少了终态)。线上无法复现时:①按用户 ID 查会话日志,看最后一次 update 的时间戳,判断是”停在哪一步”;②查是否有工具被反复调用、上下文无限增长、内存占用异常;③查执行容器是否一直处于 running 状态(说明进程没退出,多半是死循环或阻塞等待);④查 SSE 连接是否在网关层被静默掐断(CDN 后面的长连接最容易出这个问题,表现为服务端以为在推、前端收不到)。产品侧的兜底很重要:前端要有超时状态和”重试/中断”按钮,后端要有 max_steps 和心跳,让”卡死”最终变成”明确失败”而不是永远转圈。

前端性能:卡顿、掉帧与首屏。React 卡顿的常见手段分几类:①减少渲染量——长列表用虚拟列表(react-window、react-virtualized,或用 Intersection Observer 自己实现)、图片懒加载(Intersection Observer 监听进入视口再加载)、React.memo/useMemo/useCallback 避免无效重渲染、状态下沉避免整棵树重渲;②降低单帧耗时——把长任务切片(requestIdleCallback、scheduler、时间分片)、Web Worker 处理计算密集任务;③资源侧——图片转 WebP/AVIF、按需加载、tree shaking 减小包体积、CDN 就近分发。FPS 检测:用 requestAnimationFrame 递归打点,计算相邻两帧时间差,超过 16.7ms(60Hz)就算掉帧,统计一秒内的帧数和长帧占比;也可以用 Performance 面板手动录制看 Frames 轨道与 Long Tasks,或用 PerformanceObserver 监听 longtask,线上则看 Web Vitals 的 INP(交互到下一次绘制的延迟)。首屏优化除了路由懒加载与资源预加载,还有:SSR/SSG 直出首屏 HTML、CDN 分发、开启 tree shaking、图片压缩与懒加载、字体 font-display: swap 与子集化、关键 CSS 内联、HTTP 缓存与 preconnect。SSR 原理:服务端执行组件渲染成 HTML 字符串返回,浏览器先展示静态内容(首屏快、利于 SEO),同时下载 JS 进行 hydration(注水)——把事件监听与状态挂到已有 DOM 上;要注意 hydration 期间服务端与客户端渲染结果必须一致,否则报 hydration mismatch。

性能防劣化(这道题是这场面试的分水岭):思路是”把性能变成可回归的指标,而不是一次性的运动”。落地手段:①CI 卡点——接入 Lighthouse CI,在 PR 上自动跑并设 performance budget(比如 LCP < 2.5s、包体积增量 < 20KB),超阈值直接 fail 阻塞合并;②包体积门禁——用 size-limit、webpack-bundle-analyzer 在 CI 里对比基线,超过就要求说明或拆包;③运行时监控——接入 Web Vitals(LCP/INP/CLS)上报,按版本和页面维度看趋势,异常自动告警,这样即使逃过 CI 也能在线上发现;④节点级耗时打点——像作者说的那样给关键阶段(请求、渲染、计算)打点上报,定位慢在哪个环节;⑤文化上——把性能指标写进需求验收标准,Code Review 时把明显影响性能的写法(大列表不虚拟化、图片不压缩、依赖整包引入)当问题提出来。

编码题:LRU Cache。用 JS 的 Map 最省事——Map 保持插入顺序,get 命中后 delete 再 set 就把它移到末尾,put 时如果容量超了就删掉 map.keys().next().value(最久未使用的那个):

class LRUCache {
  constructor(capacity) { this.cap = capacity; this.map = new Map(); }
  get(key) {
    if (!this.map.has(key)) return -1;
    const val = this.map.get(key);
    this.map.delete(key); this.map.set(key, val);   // 刷新为最近使用
    return val;
  }
  put(key, value) {
    if (this.map.has(key)) this.map.delete(key);    // 先删旧的
    this.map.set(key, value);
    if (this.map.size > this.cap) {
      this.map.delete(this.map.keys().next().value); // 淘汰最久未使用
    }
  }
}

进阶要求手写「双向链表 + HashMap」:HashMap 负责 O(1) 定位节点,双向链表负责维护使用顺序——头部是最新使用,尾部是最久未使用;get 命中后把节点移到头部;put 时若 key 存在则更新值并移到头部,不存在则新建节点插到头部,容量超限就删尾节点并同步清理 map。用虚拟头尾哨兵节点可以省掉大量边界判断(不用判断前驱后继是否为 null),这是写这道题最容易出错的地方。两个数据结构各司其职、操作都是 O(1),所以整体 O(1)。

面试官反馈的启示:面试官明确说”前面项目问答都挺好,但缺陷是编码能力”——现在大家都用 AI 写代码,但 AI 生成的代码常常不符合架构规范、埋坑,所以手写代码仍然要练。同时给了”T 字型”路线的建议:简历上写了什么就要对什么熟悉(写了后端就会被问后端),精力有限就选一个方向做深。这两条对准备 AI 前端方向很有参考价值——Agent 是必问方向,但前端基本功(手写题、性能、浏览器原理)照样跑不掉。