面灵AI→

字节集团信息系统后端实习一面:Plan-Execute-Replan、SSE 与 RAG 切块

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

《面试题目》

后端项目

  1. 详细聊聊项目性能评测的流程,以及如何复现这套测试方案。
  2. 在性能测试中,你是如何与 AI 模型进行协作的?使用了哪些具体的 Prompt 技巧?

Agent 项目

  1. 为什么要选择 Plan-Execute-Replan 架构?
  2. Plan-Execute-Replan 架构与 ReAct 架构的具体区别是什么?
  3. 项目中使用的 SSE 协议,与 WebSocket 有什么区别?
  4. 为什么使用 RAG(检索增强生成)而不是直接将知识写入 Skill 中?两者的选型区别是什么?
  5. 在 RAG 中,如何决定文档切块(Chunk)的大小?如何处理切块过大导致召回无关信息的问题?
  6. 假设有很多未经规范组织的外部文档,该如何处理和切块?
  7. RAG 召回使用了哪些方式(如语义匹配)?
  8. 如果同时使用多种召回方式,如何判断和融合最终结果?

八股

  1. 对 MySQL 中的索引有什么了解?
  2. 聊聊 MySQL 中的「回表」机制。
  3. 哪些情况下会产生回表操作?

手撕

  1. 寻找二叉树的最近公共祖先(LCA)。

《参考解析》

性能评测流程要被「复现」,重点在把变量钉死。 面试官问「怎么复现这套测试方案」,想听的是你有没有把它当成一次可重复的实验来做。完整流程:① 明确目标与指标——是测吞吐(QPS/TPS)、延迟(P50/P95/P99)还是资源占用,先定基线(改前的数字),否则测完无从对比;② 固定环境——同一台机器、同一份数据量、同一个依赖版本(DB、Redis、模型服务),关掉会干扰的后台任务;③ 流量构造要贴近真实——不能只压一个接口,按线上流量比例做混合场景,参数随机化避免缓存全命中;④ 预热,JIT 编译和连接池都需要时间,冷启动的前几十秒数据要丢掉;⑤ 多轮次取值,单次跑出来的数没有意义,至少三轮取稳定区间,异常轮次先查原因再决定是否剔除;⑥ 压测工具,wrk/JMeter/k6 都可以,但要明确压测机本身不能成为瓶颈(用 top 看压测机的 CPU 与网卡是否打满);⑦ 采集全链路指标,服务端只统计 RT 不够,要同时抓 DB 慢查询、GC 日志、线程池队列长度,否则看到掉坑了也不知道掉在哪一层。复现的关键是把上面这些写成一个可执行脚本 + 一份说明,别人换台机器照跑能得到同一个数量级的结论。

用 AI 辅助性能测试,Prompt 的写法决定了产出能不能用。 有效的做法是给足上下文再要结论:「这是我的压测报告字段(贴结构)、这是目标(找出 P99 从 120ms 涨到 400ms 的原因)、请按可能性排序列出假设,每条给出验证命令」——让它做假设生成器和命令生成器,而不是让它替你下结论。几类真正好用的 Prompt 技巧:① 结构化输入输出,要求按固定字段返回(现象 / 假设 / 验证方法 / 所需指标),省掉二次整理;② 角色与约束前置,明确技术栈版本,避免它按旧版本给过时建议;③ 让它一次只做一件事,别把「写压测脚本 + 分析结果 + 改代码」塞进同一个请求;④ 要求它给出可证伪的判据,例如「如果 jstat 显示老年代在 Full GC 后不回落,就指向内存泄漏」;⑤ 把日志、EXPLAIN 结果原文贴进去,而不是转述——转述会丢掉关键信息。同时要有意识地防幻觉:涉及参数默认值、命令行为这类事实,必须自己去官方文档或机器上验一遍,模型给的排查顺序可以信,具体数字不要信。

Plan-Execute-Replan 与 ReAct:差别在「什么时候做全局规划」,以及错误的代价落在哪。 ReAct 是 Thought → Action → Observation 的紧循环,每一步都根据最新观测决定下一个动作,灵活、上下文短、对动态环境友好;代价是没有全局视野,容易在局部反复试探、路径冗长,任务步骤一多就烧 token 而且容易跑偏,失败时也没有「整体回退」的概念。Plan-Execute-Replan 先让模型产出一份完整计划,再逐步执行,执行过程中发现和预期不符(工具失败、结果不满足约束)就触发重规划。它的优势是:计划本身可以被人类审阅、可以并行化可以缓存、执行阶段不消耗规划 token、长任务的成功率更高;代价是初始规划一旦错了,会带着整条链路跑偏,而且计划与执行的上下文要保持同步、实现复杂度上升。选型判据很清楚:任务步骤可枚举、目标明确、允许多花一次规划成本(报告生成、多步数据处理、代码修复流水线)就上 Plan-Execute-Replan;环境反馈强烈、路径无法预判、需要边做边看(开放式检索、交互式排障、和外部系统实时博弈)就用 ReAct。实际工程里更常见的是两者混合:外层用 Planner 定里程碑,每个里程碑内部用 ReAct 循环,重规划只在里程碑失败或约束被打破时触发。这一问的追问大概率是「你怎么判断需要重规划」——答「把成功条件写成可校验的谓词,执行后校验失败就重规划,并设重规划次数上限防止死循环」。

SSE 与 WebSocket:一个把服务端推送塞进了 HTTP,一个重新开了条双向协议。 SSE 是纯文本的单向流,服务端 → 客户端,走普通 HTTP 长连接(Content-Type: text/event-stream,分块传输),浏览器自带断线重连和 Last-Event-ID 续传;WebSocket 通过 HTTP Upgrade 握手后切换成独立的全双工协议(ws://),双向、二进制友好、延迟更低。选型看三件事:方向——只需要服务端往下推(大模型流式输出、进度条、行情推送)SSE 够用,客户端要主动频繁发消息(协同编辑、游戏、语音)就得 WebSocket;基础设施——SSE 就是一个 HTTP 响应,鉴权、Cookie、网关、负载均衡、日志埋点全都是现成的,而 WebSocket 要额外处理 Upgrade 穿透、Nginx 的 proxy_read_timeout、网关对长连接的支持;连接数成本——SSE 在 HTTP/1.1 下受浏览器单域名 6 连接限制,开多个 tab 就容易互相饿死,HTTP/2 多路复用能缓解。AI 应用里普遍选 SSE,正是因为「一次提问、一段流式回答」本就是单向的,用 WebSocket 是杀鸡用牛刀,还要额外维护心跳和重连逻辑。需要双向时(用户中途打断生成、多轮实时语音)再上 WebSocket 或 WebRTC。

RAG 还是 Skill:本质是「知识放上下文里」与「知识放外部按需取」的取舍。 直接写进 Skill(本质是写进系统提示词或工具描述)的优点是零检索延迟、结果确定、模型一定会看到,缺点是吃 token、有上限、更新要改提示词、没法做权限控制,所以只适合少量、稳定、全局通用的规则(流程说明、字段定义、话术规范)。知识一旦是「大、变、有权限边界」的,就必须 RAG:文档量大到一个数量级就塞不进上下文;内容天天变,改索引比改提示词安全得多;多租户场景下检索时带 tenant_id 过滤天然实现了权限隔离,而提示词里的内容对所有人生效。这三个判据(规模、变更频率、权限)就是选型答案。更成熟的做法不是二选一:Skill 负责「怎么做事」,RAG 负责「用什么事实做事」——Skill 里写清检索的时机、query 构造方式和引用规范,具体知识交给检索。面试官如果追问「那为什么不直接把文档全塞进长上下文模型」,答案回到成本与准确率:长上下文的单价高、且存在「中间信息被忽略」的注意力衰减问题,检索把噪声提前过滤掉了。

切块大小:没有万能值,只有「按语义切 + 事后可调」。 决定 chunk 大小的三个约束:embedding 模型的窗口与语义表征能力(chunk 太长,向量被稀释成一个「什么都沾一点」的平均值,检索精度下降;太短,一句话讲不完一个完整事实,答案被切碎)、下游 LLM 的上下文预算(一次要塞进去几个 chunk 加上对话历史)、以及文档自身的结构(FAQ 一条一问一答就按条切,技术文档按标题层级切,代码按函数切)。工程上的默认起点是递归字符切分 + 200~500 token + 10%~20% 重叠,再用评测集去调,而不是拍一个数。切块过大导致召回无关信息,有四条常用解法:① 小 chunk 检索、大 chunk 喂给模型(parent-child / sentence-window:子块建索引保证精度,命中后把父块或前后若干句一起送进 prompt 保证完整性);② 加 rerank,用交叉编码器把「语义相似但没有答案」的块压下去;③ 元数据过滤,用标题路径、时间、类型先缩小范围;④ 检索后的上下文压缩,让模型只抽取与问题相关的句子。这题的加分项是主动提「切块策略必须进评测流程」——改了切块方式要重跑召回评测,否则你根本不知道是变好了还是变差了。

未经规范组织的外部文档:先治理,再切块,最后才谈检索。 直接对一堆 PDF、Word、网页、扫描件做 embedding,结果一定是脏进脏出。处理链路要分四步:① 抽取与清洗——PDF 分「有文本层」和「扫描件」两类,后者必须走 OCR(注意版面还原);去掉页眉页脚、水印、目录、免责声明这些每页重复的噪声,它们会严重污染向量;② 结构化与归一化——统一编码和换行,把表格转成 Markdown 或明确的分隔格式(表格直接按文本切是灾难),识别并保留标题层级作为元数据(h1/h2/h3 路径本身就是最好的过滤维度);③ 去重与打标——近似重复文档(同一份材料的多个版本)要先去重,否则检索出来前五条全是同一段内容;每条 chunk 打上来源、部门、时间、文档类型的元数据标签,供后续过滤和引用溯源;④ 分层切块——先按文档结构切大块(章/节),再在大块内部按语义切小块,并把标题路径拼进 chunk 正文一起做 embedding(这一步对命中率提升很明显,因为「这句话在讲什么」往往要靠标题补全)。最后一定要留人工抽检环节:随机抽 50~100 条 chunk 看切得对不对,这一步比调参数更能发现问题。

多路召回怎么融合:先归一化,再排序,最后重排。 不同召回源的分数根本不可比——BM25 是无上界的实数,向量相似度是余弦值,还有基于规则的布尔命中,直接加权求和等于让量纲决定胜负。主流做法有两种:① RRF(Reciprocal Rank Fusion),只用排名不用分数,score = Σ 1/(k + rank_i)(k 通常取 60),简单、免调参、对不同检索器都稳定,是目前最常用的基线;② 分数归一化后加权(min-max 或 z-score 归一化再乘权重),可以体现「向量渠道更重要」这类先验,但权重得靠评测集调出来。融合之后还有两步不能省:去重(多路召回经常会命中同一个 chunk 的不同切法,按文档 ID + 内容哈希去重)和重排(用 rerank 模型对融合后的 Top-50 重排取 Top-5 进 prompt,交叉编码器的精度远高于任何融合公式)。判断融合方案好不好,只能靠评测集上的 recall@k 和 NDCG:固定 query 集合和标注答案,逐个方案跑数,别的都是猜测。还有一条实践建议——按 query 类型路由(含专有名词、编号、报错码的走关键词,口语化描述走向量,两者都像就混合),比所有 query 无脑混合召回更准也更省。

MySQL 索引与回表:把 B+ 树、聚簇与二级索引串起来讲。 InnoDB 的主键索引是聚簇索引,叶子节点直接存整行数据;二级索引的叶子节点存的是「索引列的值 + 主键值」。所谓回表就是:通过二级索引找到主键后,还要再拿主键去聚簇索引里查一次完整行——这是两次 B+ 树查找,代价明显更高。设计索引的核心目标之一就是用覆盖索引避免回表:如果一个查询需要的所有列都在索引里(EXPLAIN 的 Extra 会显示 Using index),引擎在二级索引上就能拿全数据,不用回表。什么时候会回表?① 查询的列超出了索引覆盖范围(select * 最容易踩);② 走二级索引的范围查询,取到一批主键后逐条回表;③ 排序、分组、join 的驱动/被驱动列没被索引覆盖;④ 索引下推(ICP)不生效时,一些本可以在索引层过滤的条件下推到了回表之后。对应的优化手段:把高频查询的列做成联合索引实现覆盖;只 select 需要的列;利用最左前缀让联合索引被用上;用 EXPLAIN 确认 type、key、rows、Extra 是否符合预期。这里还有个面试常追问的点——回表不一定坏,如果结果集很小(比如主键等值查询),回表代价可以忽略;真正出问题的是「二级索引区分度低 + 结果集大」的组合,此时优化器甚至可能放弃索引直接全表扫描。