字节飞书后端一面凉经
(其实这是我第一次的面试,说出来都有点让人发笑,前面从来没有面试过)一共面了40分钟左右,感觉自从那个项目了里面的没回答好,我就感觉要寄了


一、项目深挖:RAG知识库系统架构、流程、踩坑问题

  1. 整体架构与运行流程

我的 RAG 系统分为 文档接入层、文本处理层、向量存储层、缓存加速层、检索问答层 五层:

  1. 文档接入:支持多格式文档上传,解析 PDF、Word、TXT 文本内容;
  2. 文本预处理:清洗无效字符、分段、分块切片,避免上下文过长;
  3. 向量化:调用 Embedding 模型将文本块转为向量;
  4. 持久化存储:向量存入向量数据库,原始文档信息、文档状态、热点检索数据缓存到 Redis;
  5. 检索问答:用户提问 -> 问题向量化 -> 相似度检索 TopK -> 拼接上下文 -> LLM 生成答案返回。
  6. 核心踩坑问题(你当时没说清楚的:多文档 Redis 覆盖问题)

(我本来要说的遇到的问题,脑子一短路说到redis覆盖了,我真是一个sn)

问题现象:
多文档连续上传,新文档缓存直接覆盖旧文档缓存,导致旧文档检索失效、问答只能命中最新一篇文档。
问题根因:

原代码:

1
2
3
4
5
6
7
8
9
10
11
func ImportDoc(vs store.Store, content string) (int, error) {
chunks := chunker.SplitText(content, config.Cfg.Chunk.Size, config.Cfg.Chunk.Overlap)
for _, c := range chunks {
vec, err := embedder.EmbedderCache(c.Text)
if err != nil {
return len(chunks), err
}
vs.Add(c.ID, c.Text, vec) // ← 每个文档 chunk 都从 0 开始
}
return len(chunks), nil
}

改后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func ImportDoc(vs store.Store, filename string, content string) (int, error) {
hash := md5.Sum([]byte(filename)) // ← 新增
docBase := int(binary.BigEndian.Uint32(hash[:4])) * 100000 // ← 新增

chunks := chunker.SplitText(content, config.Cfg.Chunk.Size, config.Cfg.Chunk.Overlap)
for _, c := range chunks {
vec, err := embedder.EmbedderCache(c.Text)
if err != nil {
return len(chunks), err
}
vs.Add(docBase + c.ID, c.Text, vec) // ← 全局唯一 ID
}
return len(chunks), nil
}

原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
chunker.SplitText 每个文档独立编号,都是从 0 开始。Qdrant 里 id 是 point 的唯一标识。

文档 A(1.txt) :chunk ID → 0, 1, 2, 3, 4, 5, 6
文档 B(go笔记.md) :chunk ID → 0, 1, 2, 3, ..., 16

文档 B 的 chunk 0 → 覆盖 Qdrant 里文档 A 的 chunk 0 ← 这就是你搜不到的原因
## 修复思路:用文件名 MD5 的前 4 字节当文档序号,乘 100000 保证偏移够大:

文档 A:md5("1.txt")[0:4] = 0xA3F8C2D1 → docBase = 2750522065 * 100000
chunk ID → 275052206500000, 275052206500001, ...

文档 B:md5("go笔记.md")[0:4] = 0x7B2E9F4A → docBase = 2067054410 * 100000
chunk ID → 206705441000000, 206705441000001, ...

两个文档的 chunk ID 永远不会冲突

二、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 跳表 双结构组成:

  1. dict(哈希表):key 是 member 元素,value 是 score,提供 O(1) 判重、查分数、去重能力;
  2. skiplist(跳表):存储 score+member 有序节点,提供排序、区间查询、排名、分页能力。
    配合关系:
    写操作同步更新两张结构,读操作按需选择:查分数走哈希,排序区间走跳表,互补短板。

4. 跳表的实现原理

跳表是 多层稀疏索引的有序双向链表,用来替代平衡树,实现 O(logn) 增删查:

  1. 第 0 层是完整有序双向链表,存全部数据;
  2. 上层每一层都是下层的稀疏索引,节点逐层减半;
  3. 插入节点时通过 1/2 概率随机生成层高,天然概率平衡;
  4. 查询从最高层向下跳,大幅跳过无效节点,不用遍历全链表。

5. 跳表时间复杂度为什么是 O(logn)?(你当时没说清的核心)

标准满分推导:

  1. 跳表每层节点数量近似下层的 1/2,是等比数列;
  2. 总层数 h 满足:n / 2^h = 1,推导出 h = log₂n;
  3. 查询、插入、删除时,每层最多走常数步,仅逐层下沉;
  4. 总操作次数和层数正相关,所以平均时间复杂度为 O(logn)。
    补充:最坏概率极低退化为 O(n),工程忽略,平均稳定 logn。

三、Go Channel 原题问答

  1. Channel 的类型
  • 按方向:只读 chan<-、只写 <-chan、读写 chan
  • 按缓冲:无缓冲通道、有缓冲通道
  1. 有缓冲、无缓冲 Channel 区别 & 应用场景
    无缓冲 chan
  • 特点:收发必须配对,同步阻塞;发送必等接收,接收必等发送
  • 场景:同步通信、协程通知、信号等待(一次性任务通知)
    有缓冲 chan
  • 特点:缓冲区未满不阻塞,满了才阻塞;异步解耦
  • 场景:生产者消费者、协程池、流量削峰、批量任务

四、算法原题:零钱兑换(完全背包 DP)

题目描述
给你一个整数数组 coins 表示不同面额硬币、整数 amount 表示总金额。每种硬币数量无限,求凑成总金额的最少硬币个数,无法凑成返回 -1。
解题思路
经典 完全背包动态规划

  1. 状态定义:dp[i] 凑金额 i 的最少硬币数
  2. 初始化:dp[0]=0,其余初始为无穷大(不可达)
  3. 状态转移:dp[j] = min(dp[j], dp[j-coin]+1)
  4. 最后判断:dp[amount] 若仍为无穷大返回 -1,否则返回答案

C++ 满分可运行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <vector>
#include <algorithm>
using namespace std;

int coinChange(vector<int>& coins, int amount) {
int INF = amount + 1;
vector<int> dp(amount + 1, INF);
dp[0] = 0;

for(int i = 1; i <= amount; i++){
for(int c : coins){
if(c <= i){
dp[i] = min(dp[i], dp[i - c] + 1);
}
}
}
return dp[amount] == INF ? -1 : dp[amount];
}

复杂度

  • 时间:O(amount * n)
  • 空间:O(amount)

五、本次面试复盘 & 优化总结

  1. 本次暴露的问题
  • 项目问题表达不熟练:踩坑问题明明准备过,临场逻辑混乱、说不清楚根因和方案;
  • 底层原理口述薄弱:跳表原理、复杂度推导无法简洁清晰输出;
  • 心态紧张:首面紧张导致卡顿,平时会的题临场卡壳。
  1. 改进方案
  • 所有项目踩坑必须固化成 现象-根因-方案-收获 四段式话术;
  • 所有数据结构、八股必须会 口述原理+复杂度推导,不能只看得懂;
  • 算法题保证中等DP、滑动窗口、链表、哈希熟练手写,零卡顿。