字节 Agent 全栈一面:动态 UI 渲染、MCP 与 Skill 治理
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
实习与 Agent 链路
- 学校课程多不多?能实习多久?
- 你课程学得怎么样?操作系统、算法这些课都上了吗?
- 讲一下你最近这段实习做了什么。
- 在工作台里输入一句 prompt,到最终生成这些东西,整条链路是怎么转起来的?
- 这条链路看起来比较顺,Agent 在读用户 prompt 和 skill 的过程中,一轮就能完成整个订单的生成吗?还是会有多轮思考或自动添加的过程?这些是由谁来做、怎么做的?
- Agent 的循环(Agent loop)架构有了解过吗?
动态 UI 渲染与前后端通信
- 前端后续操作都基于后端返回的 JSON,这个 JSON 的生成成功率怎么样?你怎么约束它一定能进到后续链路里?
- 拿到 JSON 去渲染 widget,有没有做过技术方案调研?看过 ChatGPT 或 Claude Code 做类似的 UI 动态渲染时用的是什么方案吗?你们用的是什么方案?
- 国内像豆包、Kimi,国外像 ChatGPT、Claude Code,它们做动态 UI 渲染用的是什么方案?你们为什么采用现在这套?做过对比吗?
- 你们的 widget 是有交互的,可以点确定、编辑、删除,交互之后再去和后端通信,这块是怎么做的?
- 用户修改后的行为、他定的这些东西要存到后端数据库吧?跟后端服务的通信有吗?还是纯前端的改动?
- 组件是动态生成的,动态生成时怎么绑定某一个按钮去请求后端的哪个接口?这个方案是怎么设计的?
- 渲染出来的 UI 页面既能在端侧交互,交互完又要和后端服务通信,UI 怎么和后端服务做映射或绑定?不同的按钮或操作行为最后对应不同的请求吧?
- 有听说过 Dynamic UI 或者 A2UI 的方案吗?
- 你们这个流式渲染是怎么做的?是 Markdown 渲染吗?
- 结构化的 JSON(相当于一个小卡片)渲染出来是流式出来的,还是所有东西都准备好之后一次性出来的?
- 增量流式输出会出现哪些问题?
MCP、Skill 与工具生态
- 工具调用有一些 MCP,前面还有一些 skills,你对 MCP、skills 这些工具的了解大概是什么程度?
- 有自己实现过 MCP Server,或者自己写过 Skill 吗?
- 说一下一个 Skill 有哪些组成部分?如果让你写一个 Skill,你会怎么写?
- 你说的这些内容是写在一个文件里的吗?
- 你觉得一个项目里最多能加载多少个 skill?
前端基础、性能与手写
- 用户拿到你们站点的 URL,到在浏览器里把页面打开,这个过程经历了哪些环节?
- 这条链路上(DNS 解析、TCP 连接、HTTP 请求、服务端返回静态资源、拼接页面)每一个环节都找一个优化方案,你会怎么测、怎么优化?
- 你听说过闭包这个概念吗?
- 手写:写一个闭包函数 A,每次执行时做计数器的效果,调用一次返回 1,第二次返回 2,第三次返回 3。
- 手写:封装一个 request 函数,参数有 options(请求时透传给请求函数)、maxcount(失败时最多自动重试多少次)、interval(每次重试的间隔),用你熟悉的请求方式实现。
- 反问。
《参考解析》
从 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 更好读。