面灵AI→

字节 Agent 全栈一面:动态 UI 渲染、MCP 与 Skill 治理

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

实习与 Agent 链路

  1. 学校课程多不多?能实习多久?
  2. 你课程学得怎么样?操作系统、算法这些课都上了吗?
  3. 讲一下你最近这段实习做了什么。
  4. 在工作台里输入一句 prompt,到最终生成这些东西,整条链路是怎么转起来的?
  5. 这条链路看起来比较顺,Agent 在读用户 prompt 和 skill 的过程中,一轮就能完成整个订单的生成吗?还是会有多轮思考或自动添加的过程?这些是由谁来做、怎么做的?
  6. Agent 的循环(Agent loop)架构有了解过吗?

动态 UI 渲染与前后端通信

  1. 前端后续操作都基于后端返回的 JSON,这个 JSON 的生成成功率怎么样?你怎么约束它一定能进到后续链路里?
  2. 拿到 JSON 去渲染 widget,有没有做过技术方案调研?看过 ChatGPT 或 Claude Code 做类似的 UI 动态渲染时用的是什么方案吗?你们用的是什么方案?
  3. 国内像豆包、Kimi,国外像 ChatGPT、Claude Code,它们做动态 UI 渲染用的是什么方案?你们为什么采用现在这套?做过对比吗?
  4. 你们的 widget 是有交互的,可以点确定、编辑、删除,交互之后再去和后端通信,这块是怎么做的?
  5. 用户修改后的行为、他定的这些东西要存到后端数据库吧?跟后端服务的通信有吗?还是纯前端的改动?
  6. 组件是动态生成的,动态生成时怎么绑定某一个按钮去请求后端的哪个接口?这个方案是怎么设计的?
  7. 渲染出来的 UI 页面既能在端侧交互,交互完又要和后端服务通信,UI 怎么和后端服务做映射或绑定?不同的按钮或操作行为最后对应不同的请求吧?
  8. 有听说过 Dynamic UI 或者 A2UI 的方案吗?
  9. 你们这个流式渲染是怎么做的?是 Markdown 渲染吗?
  10. 结构化的 JSON(相当于一个小卡片)渲染出来是流式出来的,还是所有东西都准备好之后一次性出来的?
  11. 增量流式输出会出现哪些问题?

MCP、Skill 与工具生态

  1. 工具调用有一些 MCP,前面还有一些 skills,你对 MCP、skills 这些工具的了解大概是什么程度?
  2. 有自己实现过 MCP Server,或者自己写过 Skill 吗?
  3. 说一下一个 Skill 有哪些组成部分?如果让你写一个 Skill,你会怎么写?
  4. 你说的这些内容是写在一个文件里的吗?
  5. 你觉得一个项目里最多能加载多少个 skill?

前端基础、性能与手写

  1. 用户拿到你们站点的 URL,到在浏览器里把页面打开,这个过程经历了哪些环节?
  2. 这条链路上(DNS 解析、TCP 连接、HTTP 请求、服务端返回静态资源、拼接页面)每一个环节都找一个优化方案,你会怎么测、怎么优化?
  3. 你听说过闭包这个概念吗?
  4. 手写:写一个闭包函数 A,每次执行时做计数器的效果,调用一次返回 1,第二次返回 2,第三次返回 3。
  5. 手写:封装一个 request 函数,参数有 options(请求时透传给请求函数)、maxcount(失败时最多自动重试多少次)、interval(每次重试的间隔),用你熟悉的请求方式实现。
  6. 反问。

《参考解析》

从 prompt 到「可交互 UI」的完整链路:这类系统的架构原则是「模型只产出受约束的中间表示,前端只负责解释它」。一次完整链路是:用户在工作台输入 prompt → 后端组装上下文(系统提示词、命中的 skill 正文、可用工具清单、组件 schema 清单、历史对话)→ 调模型(流式返回)→ 模型产出结构化 JSON(组件树或指令序列)→ 前端增量解析并按组件 schema 渲染出真实 widget → 用户在 widget 上点确认、编辑、删除 → 事件带参数回传后端 → 后端把这次动作当作新的消息或工具结果追加进上下文 → 进入下一轮,直到模型给出终结。这里最容易讲错的点是「一轮就完成」:订单生成通常是多轮的——模型先规划、再调工具取数据、再按返回结果组织组件,最后才吐 UI;所谓「链路很顺」只是编排得好、轮数被压缩了,而不是真的单轮端到端。

Agent loop 的架构与停止条件:本质是一个 while 循环:组装上下文 → 调模型 → 解析输出(要调工具还是要收尾)→ 执行工具并把结果写回消息列表 → 判断是否继续。工程上真正难的是循环的边界:最大轮次、总超时、token 与费用预算、需要人工确认的动作,任何一条触发都必须能收敛;还要防死循环(同一工具同样参数重复调用要打断)、处理工具失败(返回结构化错误让模型自纠,而不是抛异常终止整个会话)、支持取消(用户点停止要立刻中止并清理副作用)、全程可追溯(每轮的提示词、工具入参出参、耗时都记 trace,否则线上问题无法复盘)。范式上 ReAct(Thought–Action–Observation)是最基础的,Plan-and-Execute(先出计划再逐步执行)和 Reflexion(执行后自我批判再重试)是常见变体,本质都是在这五步上换编排方式。

结构化输出的成功率怎么保:分四层来答最清楚。① 约束生成层——优先用模型厂商的结构化输出或 function calling 的严格模式(把 JSON Schema 交给解码器,从采样阶段就保证合法),比在提示词里写「请输出 JSON」可靠得多。② 提示层——schema 直接放进提示词并给一两个 few-shot,字段尽量减少嵌套、能扁平就扁平,枚举用 enum 而不是自由文本。③ 校验与自愈层——服务端按 schema 校验,失败时把校验报错原文连同原始输出喂回去重生成一次,这一步能把成功率再抬一截;仍失败就退到纯文本回答的兜底分支——链路上宁可降级成「显示一段话」,也不能把不可解析的中间态丢给前端去渲染。④ 观测层——按模型版本、按 schema、按提示词版本统计首次通过率与修复后通过率,换模型前先跑回归。反面做法是用正则「抠」JSON 当主路径,它既掩盖问题,也拿不到可靠的错误信息。

动态 UI 的业界方案与选型:主流就三条路。① 受控组件库 + 声明式描述(最主流):前端预置一套白名单组件,模型只能输出「组件名 + props + actionId」,前端按注册表渲染——安全、可测、可控,代价是表达力受限于预置组件,ChatGPT 的应用与画布、大多数 to C 的生成式 UI 走这条。② 沙箱执行:模型产出 HTML/JS 或自包含代码,客户端放进 iframe 沙箱里跑,Claude 的 Artifacts 是典型——表达力最强,但必须配能力白名单、CSP、跨域隔离与资源限制,任何把模型输出直接 innerHTML 的做法都是 XSS 洞。③ 标准化协议:A2UI / Dynamic UI 这类方向想做的事,是把「UI 描述」抽成 agent 与渲染端之间的通用中间层——agent 产出声明式 UI 描述,客户端负责渲染并把用户事件按约定回传,好处是同一个 agent 能同时喂给 Web、App 和桌面端。被问「为什么用你们这套」时,答题重点在取舍:能枚举的动作走白名单,用灵活性换可控性;把安全边界(模型输出是不可信输入)、回归测试成本、端侧兼容成本摆出来,比单纯说「我们用了某某方案」有说服力。

交互事件与后端接口怎么绑定:两种实现。静态动作表:前端维护 actionId → 后端接口 的映射,模型只能输出已注册的 actionId 与参数,点击时前端查表发请求——好处是模型无法凭空创造接口调用,参数也能按 schema 校验。动态绑定:描述里直接带 endpoint 与参数模板,灵活但等于把接口暴露给模型的输出,需要服务端做白名单与参数二次校验。无论哪种都要守住三条:模型输出是不可信输入(actionId 与参数都要在服务端校验,不能只信前端);写操作要幂等(重复点击、网络重试都会重复提交,要带幂等键);状态以服务端为准(前端只做乐观更新,失败要回滚)。原帖里「都对应一个后端接口、只改入参」就是一种典型的收敛设计:把动作收敛到少数几个通用接口上,靠动作类型与参数区分行为,接口数量可控、鉴权也只需做一遍。

流式渲染与增量输出的坑:如果 JSON 要整块拿到才渲染,首屏等待长、体验差;改成流式之后会踩这些问题:① 半截 JSON 不可解析——要么用增量 JSON 解析器逐块吐出已完成的部分,要么把输出约定成 JSONL 或事件流,一条一条下发;② Markdown 半截渲染会闪——未闭合的 **、代码块、表格会让页面先错后对,需要对「悬挂标记」做缓冲或占位;③ 布局抖动——组件先渲染成骨架屏,结构稳定后再替换,避免反复重排;④ 未完成态下的交互——用户可能在流还没结束时点按钮,此时服务端状态不完整,要禁用或排队,并提供取消;⑤ 断流——连接中断要能续传,或保留已渲染内容再重试,别整块清空;⑥ 顺序与背压——分块必须按序应用,客户端渲染跟不上时要合并小分片,而不是无限堆任务队列。顺带能提一句传输选型会加分:单向推送生成结果用 SSE 足够,需要双向交互才上 WebSocket。

MCP 与 Skill 的分工:这两个不是一回事。MCP 是协议层,规范底座是 JSON-RPC 2.0,server 对外提供 tools、resources、prompts 三类原语,连接建立时先 initialize 协商协议版本与能力,本地走 stdio、远程走 Streamable HTTP,有会话状态;它解决的是「N 个客户端 × M 个工具」的适配爆炸,让工具实现一次、所有支持 MCP 的客户端都能用。Skill 是提示词资产,本质是给模型的作业指导书,靠渐进式披露省上下文:常驻上下文的只有 name 和 description(这份索引用来自动触发),命中之后才把 SKILL.md 正文读进来,正文写清适用场景、步骤、约束与示例,并附带可执行的脚本与模板文件。所以「一个 Skill 有哪些组成部分」的答案是:元数据(名称、描述、触发条件)+ 指令正文 + 可选的资源(脚本、参考文档、模板,按需加载而不是一次全读)。于是「一个项目最多能加载多少 skill」的上限不是上下文长度决定的——常驻的只是索引,真正的瓶颈是模型选路的准确率:skill 越多、描述越重叠,误触发和选错就越多。工程做法是合并重叠能力、把常驻清单控制在几十个量级、其余靠检索按需加载;描述文案本身要当接口来维护,改一次要跑一遍触发回归,否则触发面会漂移。

前端全链路性能优化,先测量再动手:链路是:浏览器缓存命中判断 → DNS 解析(浏览器缓存 → hosts → 递归解析 → 根、顶级域、权威)→ TCP 三次握手 → TLS 握手 → 发送 HTTP 请求 → 服务端处理(负载均衡 → 应用 → 缓存与数据库)→ 返回 HTML → 解析 HTML 构建 DOM、加载 CSS 构建 CSSOM → 合成渲染树 → 布局 → 绘制 → 合成,其间还要下载子资源并执行 JS。逐环节各给一个抓手:DNS 用 dns-prefetch 与 preconnect、走就近权威解析或 HTTPDNS;连接用 HTTP/2 多路复用配合 TLS 1.3,条件允许上 HTTP/3(QUIC 的 0-RTT、弱网表现更好)并开启 keep-alive;服务端压 TTFB(缓存、SSR 或流式 SSR、减少同步下游调用);静态资源走 CDN、内容哈希加长缓存、Brotli 压缩、图片换 WebP 或 AVIF 并做响应式;渲染侧内联关键 CSS、JS 用 defer 或 async 并做代码分割、给 LCP 元素加 preload 与优先级提示、避免布局抖动。最后一定要补度量口径:Lab 数据(Lighthouse)看可复现的问题,RUM 的 LCP、INP、CLS 看真实用户,先定位瓶颈再优化——面试官问「你会怎么测」,想听的就是这句。

闭包与计数器:闭包是「函数加上它定义时捕获的词法环境」;只要内部函数引用了外层作用域的变量、并且活得比外层函数的这次调用更久,这个环境就不会被回收。它的危害主要是两条:内存(被闭包持有的变量及其引用的对象一直不释放,事件监听里持有大对象或 detached DOM 是经典泄漏)和语义陷阱(闭包捕获的是变量本身而不是快照,循环里用 var 注册回调,所有回调最后都会读到同一个终值,要改用 let 或 IIFE 固化)。计数器题要的就是保存状态:let count = 0; function A() { return ++count; },或者 const A = (() => { let c = 0; return () => ++c; })()——两种写法都靠闭包持有计数变量,区别只是状态放在模块作用域还是函数作用域里。

手写带重试的 request:这是思路题,重点是讲清语义与边界,代码反而次要。

function request(options, maxcount = 3, interval = 1000) {
  return new Promise((resolve, reject) => {
    let attempt = 0;
    const run = () => {
      fetch(options.url, options)
        .then((res) => {
          if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status });
          return res;
        })
        .then(resolve)
        .catch((err) => {
          if (++attempt > maxcount) return reject(err);
          setTimeout(run, interval);
        });
    };
    run();
  });
}

要主动说出的点:maxcount 的语义先和面试官确认(「最多重试 3 次」通常指总请求 4 次);只重试值得重试的错误——网络异常、超时、5xx、429 可以重试,400、401、403、404 重试没有意义;退避要用指数加随机抖动,固定 interval 在下游故障时会形成重试风暴;重试必须和幂等配套,写操作要带幂等键,否则一次点击可能下三笔单;要支持取消(AbortController),并把超时实现进去(fetch 默认没有超时);更清晰的版本用 async/await 加 for 循环写,比递归 Promise 更好读。