字节跳动抖音开放平台一面面经:TTU 指标归因、Agent 链路与 Vue 响应式
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
实习项目追问
- 业务自定义指标 TTU 是怎么统计的?
- 为什么优化之后没有统计到 TTU 的涨幅?
- 你的优化措施里哪些会优化 TTU?说明为什么会影响。
- 资源调度优化具体做了哪些措施?
- 业务本身更关注哪个性能指标?
- 劣化措施是怎么做的?
- 拦截到的劣化事件分别来源于什么原因?
- 其中一个业务需求做了什么?重构前后有什么差别?
- 意图识别链路是怎么做的?
- 生成 UI 报告的能力,技术选型是怎么考量的?
- 最后敲定的方案里 planner 的作用是什么?
- Agent 业务中的作业诊断,整体链路讲一下?
八股
- Vue 响应式更新的原理是什么?
- Agent 和 LLM 的区别是什么?
- Agent 的循环流程是怎样的?
- LLM 是怎么调用工具的?
- Agent 的记忆怎么设计?
手撕
- 用有限状态机解析 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) 扫描、一个状态变量加一个缓冲区,比用正则去啃嵌套情况清晰得多。