10.13 字节跳动 Agent 研发一面:SSE 取舍、编排范式与长期记忆
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
自我介绍与实习
- 请做一下自我介绍。
- 暑期实习转正了吗?
- 实习经历的后端架构是如何设计的?状态机是如何设计的?
- 详细讲讲实习期间从需求到上线的 AI 工作流具体是如何流转的?
- 人工还需要重点负责哪部分?
- AI 可以替代人工的部分吗?具体应该怎么做才能替代?
- 一次请求过程中经过了哪些层次,用到了哪些协议?
项目
- SSE 和 WebSocket 如何权衡?和 HTTP 是什么关系?
- 项目里用到了哪些 Agent 编排范式?你还了解哪些其他范式?有何区别?
- Multi-Agent 和 Sub-Agent 的区别是什么?如何选择?
- 长期记忆是如何存储、检索和加载的?用到向量库吗?
八股
- MySQL 的四个特性是什么?
- 讲讲 MySQL 的索引机制?联合索引如何匹配?什么时候索引失效?
- Redis 的持久化机制是什么?
- Redis 的过期删除机制是什么?
- 如何确保数据库与缓存的一致性?读写操作如何设计?
- SSE 是一个怎样的协议?
- select、poll、epoll 的区别是什么?
- 在超长上下文的模型下,还需要区分长短期记忆和 RAG 技术吗?
算法
- 手撕 LRU。
- 高并发下 LRU 如何扩展?
- LRU 和 LFU 的差别是什么?
反问
- 反问环节:你有什么想问面试官的?
《参考解析》
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 按访问频次淘汰、对稳定的热点更友好,但要额外维护计数并做频次衰减,否则旧热点会赖着不走。真实场景里往往是两者结合加上过期时间,答的时候把这个演进讲出来比只写一个双向链表更值钱。