面灵AI→

10.13 字节跳动 Agent 研发一面:SSE 取舍、编排范式与长期记忆

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

《面试题目》

自我介绍与实习

  1. 请做一下自我介绍。
  2. 暑期实习转正了吗?
  3. 实习经历的后端架构是如何设计的?状态机是如何设计的?
  4. 详细讲讲实习期间从需求到上线的 AI 工作流具体是如何流转的?
  5. 人工还需要重点负责哪部分?
  6. AI 可以替代人工的部分吗?具体应该怎么做才能替代?
  7. 一次请求过程中经过了哪些层次,用到了哪些协议?

项目

  1. SSE 和 WebSocket 如何权衡?和 HTTP 是什么关系?
  2. 项目里用到了哪些 Agent 编排范式?你还了解哪些其他范式?有何区别?
  3. Multi-Agent 和 Sub-Agent 的区别是什么?如何选择?
  4. 长期记忆是如何存储、检索和加载的?用到向量库吗?

八股

  1. MySQL 的四个特性是什么?
  2. 讲讲 MySQL 的索引机制?联合索引如何匹配?什么时候索引失效?
  3. Redis 的持久化机制是什么?
  4. Redis 的过期删除机制是什么?
  5. 如何确保数据库与缓存的一致性?读写操作如何设计?
  6. SSE 是一个怎样的协议?
  7. select、poll、epoll 的区别是什么?
  8. 在超长上下文的模型下,还需要区分长短期记忆和 RAG 技术吗?

算法

  1. 手撕 LRU。
  2. 高并发下 LRU 如何扩展?
  3. LRU 和 LFU 的差别是什么?

反问

  1. 反问环节:你有什么想问面试官的?

《参考解析》

AI 工作流落地:先讲链路,再讲人机分工

「从需求到上线的 AI 工作流怎么流转」这个问题,面试官真正想看的是你能不能把一条链路拆清楚,而不是罗列用了哪些框架。可以按五段讲:需求进来先做结构化(这一步是人工定义验收标准:输入、输出、边界、失败时怎么办),再由编排层决定走哪条链(规则路由或分类器选流程),执行层调模型与工具(检索、数据库、外部 API),产出先过自动校验(格式、字段完整性、事实一致性),最后按置信度分流——高置信度直接对外,低置信度转人工。回答时把每一段的失败处理带上,比如工具超时如何降级、校验不过如何回退到上一版提示词或转人工,这是「工作流」和「把提示词串起来」的区别。

人工负责什么、AI 能替代什么,建议用「风险 × 可验证性」这两条轴来答,而不是泛泛说 AI 只是辅助。凡是结果可被机器验证、错了也不造成不可逆损失的环节(分类、抽取、摘要、初稿、召回、初步质检)都可以交给 AI,并且可以放大自动化率;凡是目标不清晰、需要业务判断、或者错误代价高且不可逆的环节(口径定义、对外承诺、审批、涉及资金与权限的动作)必须留人在环上。更实际的答法是给出量化的分工演进:先全量人工审核收集坏例,把坏例回填成评测集,等某类场景准确率稳定超过阈值再逐步放量、只留抽检。这样说的好处是承认「替代」是个过程,而不是一个开关。

至于「一次请求经过了哪些层次、用到哪些协议」,从客户端一路往外讲:浏览器或 App 走 HTTPS 到网关(鉴权、限流、路由),业务服务内部用 RPC 或 HTTP 调编排服务,编排服务访问向量库用各自的客户端协议、访问模型走 HTTPS 流式接口,向客户端回包时如果是逐字输出就换成 SSE(HTTP 长连接 + text/event-stream),需要双向交互的环节才升级成 WebSocket。能主动点出「协议的选择取决于谁先说话、要不要双向、链路上有没有会掐长连接的代理」,这题就答满了。

SSE 与 WebSocket:先分辨单向还是双向

两者的关系不是替代:SSE 是建立在 HTTP 之上的单向服务端推送,响应体是 text/event-stream,一条连接上只有服务端往客户端发;WebSocket 是独立协议,靠 HTTP 的 Upgrade 完成握手后升级成全双工的长连接,双方都能主动发。所以选型的第一问是「客户端要不要主动、频繁地说话」。模型逐 token 输出、日志跟随、任务进度这类场景是纯服务端推送,SSE 就够,还白拿浏览器自带的重连与 Last-Event-ID 续传语义,服务端实现也简单(就是一个不结束的 HTTP 响应);聊天输入、协同编辑、游戏状态同步、需要低延迟双向心跳的场景才上 WebSocket。

和 HTTP 的关系要讲准:SSE 本身就是 HTTP/1.1 的 chunked 传输或 HTTP/2 的流,不需要额外协议;WebSocket 是「借 HTTP 握手、之后不再用 HTTP 语义」。工程上的坑反而比协议差异更值得说:SSE 在 Nginx 后面必须关掉响应缓冲(proxy_buffering off、X-Accel-Buffering: no),否则「流式」会变成攒一批再吐一批;HTTP/1.1 同域名的连接数有限(浏览器通常 6 条),多个流式会话会互相排队,HTTP/2 多路复用能缓解;WebSocket 要自己做心跳、断线重连和鉴权续期,还要考虑网关的连接数上限与扩容时的连接迁移。被追问「为什么不用长轮询」时答:长轮询每一轮都要重建请求,事件密度高时头部与建连开销明显,而且事件顺序与丢失的处理更麻烦。

Agent 编排范式、Multi-Agent 与 Sub-Agent

常见编排范式可以归类成四类,能说清各自的适用边界比背名字有用:ReAct(思考—行动—观察交替,适合工具多、路径不确定的探索型任务,代价是步数不可控、延迟与 token 消耗高);Plan-and-Execute(先出计划再逐步执行,适合步骤多且可分解的任务,好处是能提前暴露计划错误、便于人工审计划,坏处是计划一旦偏了后面全偏,需要执行中允许修订);Reflection 与自评(让执行体或另一个评审体检查产出、再迭代一轮,适合写作、代码这类可自检的任务,代价是轮流调用带来的成本,要设最大轮数);以及图或状态机式的工作流编排(节点是步骤、边是条件,流程确定、可回放、可观测,适合生产环境里对可靠性要求高的链路)。生产上通常是混合的:外层用确定性工作流框住主干,只在需要判断的节点里放一个 ReAct 或自评循环。

Multi-Agent 与 Sub-Agent 的区别,最好从「上下文与所有权」两层讲。Multi-Agent 是多个对等的执行体协作,它们之间有显式的角色分工与消息传递,好处是并行、可以互相评审,代价是通信开销大、状态容易发散、成本与延迟成倍增长,而且失败时更难定位是哪一环的问题。Sub-Agent 是主执行体把子任务委派出去,子执行体有自己独立的上下文窗口,干完只把结论或产物交回来——它的核心价值其实不是并行,而是上下文隔离:主流程的上下文里只留下摘要,不会被几十轮工具调用撑爆,也不容易被中间过程的噪声带偏。选择标准可以概括成:需要独立上下文、结果可独立验收的子任务用 Sub-Agent;需要多视角反复博弈、或者几个角色必须共享同一份状态时才上 Multi-Agent。两者都要配预算(步数、token、超时)和终止条件,否则很容易互相等待或无限往返。

长期记忆:写、存、取、忘四件事

记忆按生命周期拆三层更好答。短期是当前会话的上下文管理:消息窗口滚动、把长历史压成摘要、把关键约束(用户偏好、不可违反的规则)常驻在上下文里。长期记忆是跨会话的事实与偏好,工程上分四步:写入侧抽取结构化事实(谁、什么属性、什么时间、来源),做去重与冲突消解(同一属性冲突时按时间新近度或来源可信度裁决,不要简单追加);存储侧一般不是单一向量库,而是「关系库存结构与原文 + 向量库存语义表示 + 需要关系推理时再加图」,向量库解决的是模糊语义召回,精确过滤和更新还得靠结构化存储;检索侧用混合检索——向量召回加关键词召回,套元数据过滤(用户、时间、权限),再用 rerank 精排,这样能压住「语义相近但事实上不相关」的误召;加载侧按相关性与时效挑选,并显式带上时间戳与出处,让模型知道这条记忆有多旧。最后是遗忘:没有 TTL 和清理机制的记忆库会越用越差,噪声会持续稀释检索精度,隐私与租户隔离也要在这一层做硬边界。

向量库在这题里是加分点:小规模(几千条以内)用倒排或直接放进上下文完全够,上向量库是为了在数据量和延迟约束下做近似最近邻检索,代价是召回率下降与内存成本,所以要和关键词路、rerank 配合。被追问「长上下文模型下还要不要记忆和 RAG」时,答「长窗口降低的是分段与拼接的复杂度,不解决更新、权限、溯源和成本四件事」——原文放在外部才可增量更新、才能按权限过滤、才能给出处,成本也不会随窗口线性上涨。

MySQL 与 Redis 八股

MySQL 的四个特性就是 ACID:原子性靠 undo log 回滚,一致性是前三者加上约束与业务逻辑共同保证的目标,隔离性靠锁与 MVCC(读已提交/可重复读的差别就在快照的生成时机),持久性靠 redo log 的 WAL 与刷盘策略。索引这块的答题骨架是「结构 → 匹配规则 → 失效场景」:InnoDB 用 B+ 树,非叶子节点只存键、叶子节点串成链表,所以树矮、范围扫描友好;联合索引按定义顺序逐列匹配,等值条件要满足最左前缀,范围条件之后的列用不上索引(这是「联合索引如何匹配」最容易被追问的一句);覆盖索引能避免回表,索引下推能把过滤下推到存储引擎层。索引失效的典型场景:列上做函数或运算、隐式类型转换(字符串列传数字)、like '%x' 前导模糊、or 连接非索引列、以及在区分度极低的列上建索引导致优化器直接全表扫。答完可以补一句:最终以 explain 的 type/key/rows 为准,别凭记忆判断。

Redis 持久化是两条线可以混用:RDB 是某个时间点的快照,fork 加写时复制生成,文件小、恢复快,但两次快照之间会丢数据;AOF 记录写命令,appendfsync 有 always/everysec/no 三档,对应不同的可靠性与性能取舍,AOF 重写用来压缩体积,现在默认还可以用 RDB 加 AOF 的混合格式兼顾恢复速度。过期删除是「惰性删除 + 定期抽样删除」的组合,不是定时器逐键删(那样开销不可接受,也删不干净),内存到上限后由 maxmemory-policy 决定淘汰——LRU、LFU、随机、ttl 几类,其中 Redis 的 LRU 是抽样近似实现。

缓存一致性这题的关键是先说清一致性等级:绝大多数业务要的是最终一致而不是强一致。主流做法是 Cache Aside——读时未命中回源并写缓存,写时先更新数据库再删除缓存(删除而不是更新,避免并发写把旧值写进去,也避免无访问的键被反复刷写)。剩下的窗口期靠三招收:删除失败进重试队列或订阅 binlog 补偿(canal 这类方案把删除动作变成可靠消息,顺序也更好保证);延迟双删应对「读请求在更新前拿到旧值、又在删除后才回填」的极小概率场景;热点键重建时加互斥(单飞),避免缓存击穿时数据库被打穿。还要区分穿透(查不存在的键,用空值缓存或布隆过滤器)与雪崩(大量键同时过期,用随机 TTL 与多级缓存)。最后补一句:涉及资金、库存这类必须强一致的读,直接读主库,别在缓存上较劲。

select/poll/epoll 与 LRU 手撕

IO 多路复用这三者是同一条演进线:select 用位图传 fd 集合、每次调用都要把集合从用户态拷到内核态、返回后还要线性遍历找出就绪的 fd,且有 1024 的 fd 上限;poll 换成了数组,去掉了数量上限,但拷贝与 O(n) 遍历依旧;epoll 在内核维护事件表,epoll_ctl 注册一次,epoll_wait 靠就绪链表直接返回活跃 fd,复杂度与「就绪数」而不是「总数」相关,同时支持水平触发与边沿触发(ET 要求一次把数据读干净直到 EAGAIN,通常配非阻塞 fd),还能用 EPOLLONESHOT 避免多线程惊群。追问点常落在「LT 和 ET 该选哪个」以及「epoll 为什么不适合普通文件」。

手撕 LRU 的标准实现是哈希表加双向链表:哈希表负责 O(1) 找到节点,双向链表维护访问顺序,get 命中就把节点移到表头,put 时若超容量就摘掉表尾。工程实现里更值得讲的是并发版本:最简单的是给整个 LRU 加一把锁(够用但争用高),进阶是按 key 分片成多个独立 LRU 降低锁冲突,或者用近似 LRU 换取吞吐(Redis 就是采样若干键淘汰其中最久未用的),工业级缓存(Caffeine)用的是 W-TinyLFU,把「近期访问」和「访问频次」结合起来。与 LFU 的差别可以一句话概括:LRU 看最近一次访问时间、对突发流量友好但容易被一次性的扫描数据污染;LFU 按访问频次淘汰、对稳定的热点更友好,但要额外维护计数并做频次衰减,否则旧热点会赖着不走。真实场景里往往是两者结合加上过期时间,答的时候把这个演进讲出来比只写一个双向链表更值钱。