字节 AI 应用工程师一面:数据分析 Agent 设计与 Go 并发
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
开场与算法
- 自我介绍。
- 现场手写:1~N 个数中选 k 个,给出所有可能的组合。追问:时间复杂度和空间复杂度是多少?
项目深挖(Agent 相关)
- 为什么项目不用 Claude Code?
- 你的 Agent runtime 是自己搭建的吗?
- 你的 agent 基座在 memory 压缩、同步 / 异步、session 总结上是怎么做定制的?
- 迭代时如何确保 AI 改正的地方是真的有效?人工有没有做指引和约束?
- 迭代后怎么验证效果?(回归评测、能力评测、灰度、影子 case)
- 你在这个项目里主要承担什么角色?团队规模多大?
- 评测中如果出现”小层面提优、大方向负优化”,怎么办?
场景设计题:数据分析 Agent
- 假设让你做一个数据分析 Agent,老板经常问”为什么今天这个数据异常 / 标高了”,你会怎么设计?
- Memory 你会怎么设计?
- 上下文怎么管理?
- 需要几个 Agent?架构怎么设计?
- 如果业务表结构变了、加了新表,怎么处理?
- 具体怎么落地实现?你觉得难点是什么?
服务治理 / 线上问题排查
- 如果一个服务突然变得很卡、很慢,你会怎么排查?
- 如果是 CPU 太高呢,怎么定位?
- 这些你实际工作中接触过吗?
数据库
- 数据库索引、事务了解吗?索引的底层原理是什么?
- 最左前缀匹配是什么原理?为什么会失效?
- 读已提交和可重复读的区别?
- 从原理层面讲讲可重复读是怎么实现的?
- 如果要放弃可重复读、退到读已提交,你会怎么判断与取舍?如何判定当前服务需要的隔离层级?
计算机网络
- 介绍一个数据包打到服务器网卡、一直到应用程序的全流程。
- 介绍一下 TCP 中发送窗口是如何调整的?具体根据拥塞情况调节的机制是什么?
Go / 并发
- 为什么用协程?协程和线程的区别?
- 阻塞状态的 goroutine 是怎么处理的?和线程有什么不同?
- goroutine 和线程切换的时候有什么不同?
- 为什么用户态调度会比内核态快?底层发生了什么?
- 假设服务器上有 10000 个 goroutine,此时它们是如何调度的?
《参考解析》
数据分析 Agent 的架构。核心矛盾是”口径不能错”:老板问的”数据异常”通常不是查不出来,而是查出来的口径和他脑子里的不一致。可行设计是分层——语义层(指标定义、维度、同环比基准)单独抽出来做成可检索的知识(指标字典 / metric catalog),而不是写进 prompt;检索层接 Text2SQL 或已有的 BI 接口,优先走预定义指标而不是自由 SQL,能命中口径就走指标、命不中才落到 SQL 生成,并对生成的 SQL 做方言校验与 LIMIT 保护;执行层拿到数据后做异常归因,把”总量变化”按维度下钻,用贡献度排序找出真正拉动的维度;表达层只输出”结论 + 支撑数据 + 置信度”,不确定就说不知道。Agent 数量上,一个规划 Agent 加一组工具通常够用,拆多 Agent 的收益主要在”检索与校验分离”这类需要独立上下文的环节,而不是越多越好。
表结构变了怎么办。不要靠人工同步 prompt。做法是把 schema 做成运行时按需检索的资源:Agent 每次需要哪张表就查一次元数据(表名、字段、注释、枚举取值),并在提示里标注”以下 schema 是实时读取的”。另外准备一层语义映射,把”GMV""日活”这类业务词映射到真实字段,schema 变更时只更新映射。真正的难点是语义而非结构:字段改名、口径调整、口径被多个团队各写一份,靠元数据解决不了,需要指标口径本身有单一权威来源。
评测”小层面提优、大方向负优化”。这说明评测集只覆盖了局部。解法是把评测分层并绑定权重:工具调用级(选中率、参数正确率)、任务级(端到端成功率、步数、成本)、系统级(用户采纳率、人工接管率)。同时保留一套”回归底线”用例——宁可少测几个新 case,也要保证核心链路的用例每次全跑,且用固定版本做对比,避免”改了 A 但顺手换了模型”这类混淆变量。灰度与影子模式是必要补充:线上真实流量分流到新版本,用同一输入比对两个版本的输出差异,人只看有分歧的部分。
服务突然变卡 / CPU 高怎么查。先分方向再深入:看监控是”全链路都慢”还是”只有某个接口慢”——全链路的优先怀疑下游依赖、数据库、连接池或 GC;单接口的看这个接口有没有新上的逻辑、慢 SQL、N+1 查询。工具顺序:top 看进程级 CPU 与内存,top -Hp 找热点线程,jstack 抓线程栈看线程卡在哪(等锁、等 IO、跑 GC),jstat -gcutil 看 GC 频率与耗时。CPU 高但吞吐下降的典型原因是自旋和锁竞争;CPU 高且吞吐正常的往往是流量本身涨了。数据库侧的慢查询和连接池打满经常伪装成”应用变卡”,排查时一定要看下游而不是只盯应用。
最左前缀为什么会失效。联合索引 (A, B, C) 在 B+ 树上是按 A → B → C 依次排序的:只有先确定 A,B 才是有序的;跳过 A 直接查 B,整棵树在 B 维度上无序,无法二分定位。导致失效的常见写法有:对索引列做函数或运算(WHERE DATE(create_time)=?)、隐式类型转换(字符串列传数字)、以 % 开头的 LIKE、范围条件之后的列(A=? AND B>? AND C=? 时 C 用不上有序性)。另外要区分”用了索引”和”用足了索引”——ICP 能在索引层过滤但不会减小子树扫描范围。
可重复读的实现:MVCC。每行有隐藏的事务 id 与回滚指针,undo log 串成版本链;事务开始时生成 ReadView,记录当时活跃的事务集合,读取时沿版本链找到第一个对本事务可见的版本。读已提交与可重复读的差别就在 ReadView 的生成时机——读已提交每条语句都重新生成一次(所以能看到别人新提交的数据),可重复读在事务内第一次读时生成一次并复用(所以前后一致)。取舍的判断依据是业务语义:需要”同一事务内多次读同一份数据保持一致”就用可重复读;只要求”不读到未提交数据”、且希望并发更高,则读已提交更合适。财务对账这类场景偏前者,纯展示类查询偏后者。
TCP 发送窗口与拥塞控制。发送窗口取”接收方通告窗口”和”拥塞窗口 cwnd”的较小值。cwnd 的调节分阶段:慢启动阶段每收到一个 ACK 就翻倍(指数增长)直到达到慢启动阈值;拥塞避免阶段每 RTT 线性加一;出现超时重传判定为严重拥塞,cwnd 直接降到 1 并重回慢启动,阈值砍半;收到三个重复 ACK(快重传)则判定为轻度拥塞,cwnd 减半并进入快恢复。现代内核默认用 CUBIC,丢包后按三次曲线恢复而非 Reno 的线性;BBR 则不再以丢包为信号,而是直接估测带宽与最小 RTT,在有丢包的长肥链路上表现更好。
GMP 调度。G 是 goroutine,M 是内核线程,P 是逻辑处理器(持有运行队列与本地缓存),GOMAXPROCS 决定 P 的数量。G 被放进 P 的本地运行队列,M 必须绑定一个 P 才能执行 G,所以同一时刻最多有 P 个 G 真正并行。阻塞的 goroutine 处理方式与线程不同:发生 channel 阻塞、锁等待、网络 IO 这类可被 runtime 感知的阻塞时,G 被挂起并从 M 上摘下来,M 转去跑别的 G,不占用内核线程;只有发生无法被 runtime 拦截的系统调用(比如阻塞式文件 IO)时,M 才会带着 G 一起阻塞,runtime 会创建或唤醒新的 M 来接管这个 P,保证其他 G 继续跑。这就是用户态调度更快的原因:切换只需保存少量寄存器、完全在用户态完成,不陷入内核、不切换地址空间、也不刷 TLB,开销大约是内核线程切换的十分之一量级。10000 个 goroutine 时,它们分散在各个 P 的本地队列和全局队列里,M 空闲时会先看本地队列、再看全局队列、最后去别的 P 那里偷一半(work stealing),所以能被均匀摊到有限的 P 上并发推进。