字节飞书后端一面凉经
字节飞书后端一面凉经
(其实这是我第一次的面试,说出来都有点让人发笑,前面从来没有面试过)一共面了40分钟左右,感觉自从那个项目了里面的没回答好,我就感觉要寄了
一、项目深挖:RAG知识库系统架构、流程、踩坑问题
- 整体架构与运行流程
我的 RAG 系统分为 文档接入层、文本处理层、向量存储层、缓存加速层、检索问答层 五层:
- 文档接入:支持多格式文档上传,解析 PDF、Word、TXT 文本内容;
- 文本预处理:清洗无效字符、分段、分块切片,避免上下文过长;
- 向量化:调用 Embedding 模型将文本块转为向量;
- 持久化存储:向量存入向量数据库,原始文档信息、文档状态、热点检索数据缓存到 Redis;
- 检索问答:用户提问 -> 问题向量化 -> 相似度检索 TopK -> 拼接上下文 -> LLM 生成答案返回。
- 核心踩坑问题(你当时没说清楚的:多文档 Redis 覆盖问题)
(我本来要说的遇到的问题,脑子一短路说到redis覆盖了,我真是一个sn)
问题现象:
多文档连续上传,新文档缓存直接覆盖旧文档缓存,导致旧文档检索失效、问答只能命中最新一篇文档。
问题根因:
原代码:
1 | func ImportDoc(vs store.Store, content string) (int, error) { |
改后代码:
1 | func ImportDoc(vs store.Store, filename string, content string) (int, error) { |
原因
1 | chunker.SplitText 每个文档独立编号,都是从 0 开始。Qdrant 里 id 是 point 的唯一标识。 |
二、Redis & MySQL 相关八股
(然后换方向了不问项目了 (看我卡太久了))
1. Redis 和 MySQL 的区别
- 定位不同:MySQL 是磁盘持久化数据库,做主存储;Redis 是内存缓存,做加速、临时存储、计数器、分布式锁。
- 速度不同:Redis 纯内存操作,百万 QPS;MySQL 磁盘 IO,速度慢几个量级。
- 持久化:MySQL 强持久;Redis 默认内存丢失,可 RDB/AOF 持久化。
- 数据结构:MySQL 关系型表结构;Redis 丰富五大数据结构,适合各类场景。
- 事务:MySQL 支持完整 ACID 事务;Redis 仅支持单命令原子,不支持复杂事务。
- 使用场景:MySQL 存业务核心数据;Redis 做缓存、限流、分布式锁、队列、排行榜。
2. Redis 基本数据类型
5 大基础类型:String、List、Hash、Set、ZSet
- String:字符串、计数器、缓存、分布式锁
- List:双向链表,消息队列、栈
- Hash:用户信息、对象缓存
- Set:去重、交集、差集、好友列表
- ZSet:有序集合、排行榜、延时任务
3. Redis ZSet 底层实现
ZSet 底层由 dict 哈希表 + skiplist 跳表 双结构组成:
- dict(哈希表):key 是 member 元素,value 是 score,提供 O(1) 判重、查分数、去重能力;
- skiplist(跳表):存储 score+member 有序节点,提供排序、区间查询、排名、分页能力。
配合关系:
写操作同步更新两张结构,读操作按需选择:查分数走哈希,排序区间走跳表,互补短板。
4. 跳表的实现原理
跳表是 多层稀疏索引的有序双向链表,用来替代平衡树,实现 O(logn) 增删查:
- 第 0 层是完整有序双向链表,存全部数据;
- 上层每一层都是下层的稀疏索引,节点逐层减半;
- 插入节点时通过 1/2 概率随机生成层高,天然概率平衡;
- 查询从最高层向下跳,大幅跳过无效节点,不用遍历全链表。
5. 跳表时间复杂度为什么是 O(logn)?(你当时没说清的核心)
标准满分推导:
- 跳表每层节点数量近似下层的 1/2,是等比数列;
- 总层数 h 满足:n / 2^h = 1,推导出 h = log₂n;
- 查询、插入、删除时,每层最多走常数步,仅逐层下沉;
- 总操作次数和层数正相关,所以平均时间复杂度为 O(logn)。
补充:最坏概率极低退化为 O(n),工程忽略,平均稳定 logn。
三、Go Channel 原题问答
- Channel 的类型
- 按方向:只读 chan<-、只写 <-chan、读写 chan
- 按缓冲:无缓冲通道、有缓冲通道
- 有缓冲、无缓冲 Channel 区别 & 应用场景
无缓冲 chan
- 特点:收发必须配对,同步阻塞;发送必等接收,接收必等发送
- 场景:同步通信、协程通知、信号等待(一次性任务通知)
有缓冲 chan - 特点:缓冲区未满不阻塞,满了才阻塞;异步解耦
- 场景:生产者消费者、协程池、流量削峰、批量任务
四、算法原题:零钱兑换(完全背包 DP)
题目描述
给你一个整数数组 coins 表示不同面额硬币、整数 amount 表示总金额。每种硬币数量无限,求凑成总金额的最少硬币个数,无法凑成返回 -1。
解题思路
经典 完全背包动态规划
- 状态定义:dp[i] 凑金额 i 的最少硬币数
- 初始化:dp[0]=0,其余初始为无穷大(不可达)
- 状态转移:dp[j] = min(dp[j], dp[j-coin]+1)
- 最后判断:dp[amount] 若仍为无穷大返回 -1,否则返回答案
C++ 满分可运行代码
1 | #include <vector> |
复杂度
- 时间:O(amount * n)
- 空间:O(amount)
五、本次面试复盘 & 优化总结
- 本次暴露的问题
- 项目问题表达不熟练:踩坑问题明明准备过,临场逻辑混乱、说不清楚根因和方案;
- 底层原理口述薄弱:跳表原理、复杂度推导无法简洁清晰输出;
- 心态紧张:首面紧张导致卡顿,平时会的题临场卡壳。
- 改进方案
- 所有项目踩坑必须固化成 现象-根因-方案-收获 四段式话术;
- 所有数据结构、八股必须会 口述原理+复杂度推导,不能只看得懂;
- 算法题保证中等DP、滑动窗口、链表、哈希熟练手写,零卡顿。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 乃敢与君绝!


