面灵AI→

字节跳动抖音开放平台一面面经:TTU 指标归因、Agent 链路与 Vue 响应式

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

《面试题目》

实习项目追问

  1. 业务自定义指标 TTU 是怎么统计的?
  2. 为什么优化之后没有统计到 TTU 的涨幅?
  3. 你的优化措施里哪些会优化 TTU?说明为什么会影响。
  4. 资源调度优化具体做了哪些措施?
  5. 业务本身更关注哪个性能指标?
  6. 劣化措施是怎么做的?
  7. 拦截到的劣化事件分别来源于什么原因?
  8. 其中一个业务需求做了什么?重构前后有什么差别?
  9. 意图识别链路是怎么做的?
  10. 生成 UI 报告的能力,技术选型是怎么考量的?
  11. 最后敲定的方案里 planner 的作用是什么?
  12. Agent 业务中的作业诊断,整体链路讲一下?

八股

  1. Vue 响应式更新的原理是什么?
  2. Agent 和 LLM 的区别是什么?
  3. Agent 的循环流程是怎样的?
  4. LLM 是怎么调用工具的?
  5. Agent 的记忆怎么设计?

手撕

  1. 用有限状态机解析 Markdown 语法生成 token 列表,只包含加粗语法和普通文本。

《参考解析》

自定义指标 TTU 的统计口径

这类自定义指标必须先把三件事说清楚,否则后面的「为什么没涨」无从讨论:起点和终点(从哪个时刻开始计时、到哪个事件算结束——比如从路由进入或接口发起,到首屏可交互/关键元素渲染完成)、埋点位置(打点是在业务代码里手动上报还是在框架的统一入口上报,采样率多少、是否有上报丢失)、聚合方式(P50/P90/P95、按端与地区分层、均值是否被长尾拉偏)。回答时最好给出指标的公式与数据链路:客户端打点 → 上报 → 日志清洗 → 指标平台,并说明口径改过一次没有、改动前后是否可比。

优化了却看不到 TTU 涨幅,一般逃不出这几类原因

最可能是指标与优化目标不匹配:优化的是非关键路径(例如把不依赖接口的元素独立出来渲染,改善的是主线程占用和可交互时间),而 TTU 的瓶颈在接口响应或首屏数据依赖上,指标自然不会动。其次是测量本身的问题:打点覆盖不全或者漏埋新路径、采样率太低被噪声淹没、优化上线时间与观测窗口错位、观测期里同时有别的改动(灰度发布、上游接口变慢)把收益抵消。还有用户分布因素:优化只对某一类设备或某一地区有效,平均值被其它人群稀释;以及收益被淹没——优化省下的时间在整条链路上占比很小,落在指标的噪声带里。

正确的答法是先做分段归因:把 TTU 拆成网络、接口、渲染、资源加载几段,看每一段在优化前后的变化,确认优化命中的是哪一段;如果命中段变好了但总指标没动,就说明瓶颈在别处,下一步该去优化真正占大头的那一段。

性能指标、劣化拦截与归因

「业务更关注哪个指标」要看业务形态:内容/社交类前端通常更关注首屏可见与可交互(FCP、TTI、TTU 这类用户可感知的指标),其次是交互响应与滚动流畅度(长任务、掉帧率);纯接口服务则更关注 P95/P99 延迟与错误率。回答时把「为什么是它」讲出来——比如用户一进页面最在意内容什么时候能看到,卡顿投诉来自长任务而不是资源体积。

劣化拦截的常规做法是:先给指标定基线(按版本、端、地区分层的历史分位数),再在发布链路上设门控——预发/灰度环境跑性能对照(同一套场景、同一批设备,新版本与基线版本比较),超过阈值就拦住发布;线上则用自动告警加自动回滚兜住。关键是把对照做在同口径上:不同机型、网络条件、随机采样都会带来偏差,所以要有稳定的测试场景与足够的样本量。

劣化事件的来源按经验通常分几类:接口变更(响应变大、依赖串行化、某个下游变慢)、资源与依赖(包体积增长、第三方脚本阻塞、字体与图片未优化)、缓存(缓存命中率下降或缓存键变更导致回源)、发布与配置(灰度开关、AB 实验分流、埋点与 SDK 版本升级)、客户端环境(机型、系统版本、网络类型分布变化)。归因要做的是把这些维度都当作可下钻的标签,观察劣化集中在哪个切面上。

意图识别链路

一个完整的意图识别链路可以拆成五段:输入预处理(获取用户原始 query,补上下文——上一轮意图、当前页面/会话状态、用户身份)、召回与分类(规则/词典先兜住高频明确表达,再用分类模型或大模型做意图判别,必要时先做 query 改写或纠错)、结构化输出(把结果约束成固定的意图标签加槽位参数,用 schema 校验,不接受自由文本)、置信度决策(低于阈值时走澄清追问,而不是硬猜;多个意图接近时让用户确认)、路由与执行(把意图与槽位交给对应的工具、流程或后端能力,返回结果后再拼自然语言回复)。工程上还要补两件事:兜底策略(识别不出时给默认意图或转人工)和效果度量(用真实日志做离线评估集,改规则或换模型都要回归)。

生成 UI 报告的选型与 planner 的作用

「生成 UI 报告」这类需求通常有三条路线,各有取舍:模板渲染——模型只产出结构化数据(JSON),前端用固定模板渲染,可控性最好、样式统一、容易校验和降级,代价是灵活性有限;模型直接生成结构化的页面描述(schema 化的组件树),在模板与自由生成之间取平衡,但需要有严格的 schema 校验与非法结构的兜底;让模型生成代码/HTML,灵活度最高,但可控性、安全性与一致性最差,一般只在受控沙箱或一次性场景使用。选型的判断标准是可控性、可校验性、多端一致性、降级成本四项:报告这类会被反复查看、需要稳定排版的内容,通常选前两条,把生成的不确定性收敛到数据层。

planner 在方案里的作用是承担「想清楚再动手」的那一层:接住用户目标,结合可用工具与数据,拆出有依赖关系的步骤序列(先查什么、再算什么、最后生成什么),决定每一步用哪个工具和参数,并在执行结果与预期不符时重新规划。它和 executor 的分工要讲清楚——planner 只产出步骤和判断,不做实际执行;executor 一次只做一步、只回摘要与状态;再配一个独立的校验环节检查产物是否达标(schema、规则、数值范围),不合格就带着失败证据回到 planner。这样拆的价值是把「判断」和「事实核验」分开,避免执行者自己证明自己正确。

Agent 业务中作业诊断的整体链路

可以按「采集 → 归一 → 判定 → 建议 → 闭环」讲:采集各类信号(日志、指标、错误堆栈、配置、变更记录),做归一与关联(把同一个任务/请求的上下文拼起来,标注时间线与版本);再用规则与大模型结合做判定——确定性问题(阈值超限、明确的错误码)交给规则,模糊问题(现象与原因之间是概率关系)交给模型并让它给出依据;然后输出可执行建议与置信度,并标注证据出处;最后是闭环,把诊断结论与人工/自动修复的结果回收,作为评估集持续校准判定逻辑。链路里必须有的工程件是:状态落盘与可回溯、每一步的可观测(调用轨迹、耗时、token)、失败分类与降级(诊断不出来时明确说需要哪些信息,而不是编一个原因)。

Vue 响应式更新原理

Vue 2 用 Object.defineProperty 递归把 data 的每个属性改成 getter/setter:渲染时触发 getter 完成依赖收集(把当前组件的 Watcher 记进该属性的 Dep),数据变更时 setter 通知 Dep 里的 Watcher 更新。它有两个已知短板——无法监听对象新增/删除属性(要靠 Vue.set)和数组下标赋值,且初始化时就得递归遍历整个对象。

Vue 3 换成 Proxy 代理整个对象,配 effect / reactive / ref 实现:读取时在 track 里记录「当前正在运行的副作用」与该属性的对应关系,写入时在 trigger 里找出相关副作用重新执行。Proxy 天然支持新增/删除属性和数组索引,且是惰性递归——只有真正访问到嵌套对象时才代理它,初始化更快。两者共同的机制是异步批量更新:同一次事件循环里的多次数据变更会去重、合并成一次组件更新,等微任务队列用 nextTick 刷出,这也是「改完数据立刻读 DOM 读不到新值」的原因。computed 基于这套依赖系统做缓存,依赖不变时不会重新求值。

Agent 与 LLM、循环、工具调用、记忆

Agent 和 LLM 的区别:LLM 是单次生成——给一段上下文,返回一段文本,没有目标、没有状态、不会自己决定下一步;Agent 是在 LLM 外面套的一层运行时,多了四样东西:循环(多轮执行直到达成目标)、工具(能读文件、查库、调接口,从而改变外部世界)、记忆/状态(跨轮保留计划、中间结果与结论)、终止与兜底条件(步数、token、时间预算,失败重试与降级)。一句话:LLM 负责「想」,Agent 负责「想完之后去做、看结果、再决定下一步」。

循环流程:组装上下文(系统提示 + 历史 + 工具定义)→ 调模型 → 解析输出(是自然语言还是工具调用)→ 执行工具 → 把结果作为观测回灌进上下文 → 判断是否满足终止条件(没有新的工具调用、任务验收通过、达到预算上限、连续多轮无新信息),满足就收尾,否则进入下一轮。工程上要额外处理上下文压缩、重复调用去重和路径震荡。

LLM 怎么调用工具:靠函数调用(function calling / tool call)协议。宿主把每个工具的名字、用途描述和参数的 JSON Schema 一起放进请求;模型判断需要调用时,不返回自然语言,而是返回一个结构化的调用对象(工具名 + 参数 JSON);宿主解析后真正执行,再把执行结果作为一条 tool 消息回灌,模型基于结果继续生成或发起下一次调用。所以效果好坏很大程度取决于工具描述——描述里写清什么时候该用、什么时候不该用、返回什么,参数用 schema 严格约束;执行层还要做参数校验、超时、幂等与错误回灌(把错误信息交给模型让它换路)。

记忆设计:分短期与长期。短期是上下文窗口内的最近若干轮原始对话,超限时做裁剪或摘要(摘要要保留数字、文件名、结论这类硬信息);长期把稳定的东西外置——用户偏好、项目背景、任务结论写进外部存储(文件/数据库/向量库),需要时按 query 检索召回,而不是每轮全量带上。三条工程纪律:写入要过滤(不是所有对话都值得记,重复和低信息量内容进库只会污染召回)、冲突要覆盖(用户改了口径,旧记忆要失效而不是并存)、召回要限额并标时效(有数量和 token 上限,标明来源与时间)。任务清单、中间结果这类状态更适合写成结构化数据由程序维护,比让模型从聊天记录里反推可靠。

手撕:用有限状态机解析 Markdown 的加粗语法

题目只要求处理加粗 ** 和普通文本,正好适合显式状态机。先定义状态:TEXT(正在累积普通文本)、STAR1(遇到一个 *,可能是加粗开头也可能是普通星号)、BOLD(在加粗内容里)、BOLD_STAR1(在加粗内容里遇到一个 *,等下一个判断是否闭合)。

转移规则:

  • TEXT:遇到 * → 进 STAR1;其它字符 → 追加到当前普通 token。
  • STAR1:再遇到 * → 进 BOLD,丢弃这两个 *;其它字符 → 把之前那个 * 作为普通文本吐出,按当前字符重新处理。
  • BOLD:遇到 * → 进 BOLD_STAR1;其它字符 → 追加到当前 bold token。
  • BOLD_STAR1:再遇到 * → 闭合,吐出一个 bold token,回到 TEXT;其它字符 → 把 * 和该字符都追加进 bold token,回到 BOLD。

收尾时按当前状态决定:如果停在 STAR1,那个星号当普通文本输出;如果停在 BOLD 或 BOLD_STAR1,说明 ** 没有闭合,按 CommonMark 的处理是整体当作普通文本(把那两个 * 和内容还原成普通文本),这一点主动说出来能体现你想过边界。其它要主动交代的边界还有:**** 这种空内容、**a**b**c** 连续成对、\* 转义(题目没要求就说明按不处理)、以及跨行不合并。实现上就是一次 O(n) 扫描、一个状态变量加一个缓冲区,比用正则去啃嵌套情况清晰得多。