Week 4 Day 29:全真面试模拟 - 苏格拉底教学
今天不教新知识。今天你是候选人,我是面试官。准备好了吗?
第一部分:模拟前的心理准备
🤔 问题1:你害怕什么?
引导你思考:
- 怕被问到不会的题?→ 面试官更想看你如何处理不会的题
- 怕系统设计说不清楚?→ 说不清楚 = 没有在脑子里跑过这个设计
- 怕 behavioral 没有好故事?→ 30天做了这么多,你已经有故事了
今天的目标不是完美,而是发现盲点。
面试是一次有时间限制的对话。你需要:
- 快速建立信任(自我介绍)
- 展示思维过程(coding,比结果更重要)
- 证明你能在生产环境设计系统(system design)
- 让面试官相信你曾经真实面对过困难(behavioral)
第二部分:90 分钟完整模拟流程
时间分配
00:00 - 05:00 自我介绍(5分钟)
05:00 - 50:00 Coding(45分钟)
50:00 - 80:00 系统设计(30分钟)
80:00 - 90:00 Deep Dive(10分钟)+ 候选人提问(5分钟)
第三部分:自我介绍(5分钟)
标准结构:过去 → 现在 → 未来
模板:
1. 简要背景(20秒)
2. 核心项目经历(2分钟,重点在你做了什么、有什么影响)
3. 当前状态(30秒,为什么在找工作)
4. 为什么对这个职位/公司感兴趣(1分钟)
5. 下一步目标(30秒)
示例(面向 Infrastructure Engineer 职位):
面试官你好,我叫 [名字]。
我有 X 年后端工程经验,主要在 Go 生态下做高并发服务和平台基础设施。
最近 30 天我系统性地构建了一个完整的 Customer Support AI Agent 系统,
涵盖 RAG 知识库、ReAct Agent 循环、人工审核工作流、以及完整的可观测性体系。
在这个项目里,我最深的体会是 Agent 系统和传统微服务的基础设施需求很不一样:
它需要专门处理 LLM 的不确定性,比如在 tool 调用层做幂等性,在 retrieval 层
控制信息质量,在输出层做 PII 过滤。这些都是传统 CRUD 服务不需要考虑的。
我对贵司这个职位感兴趣,是因为 [公司特点 + 职位方向],
这和我希望深入的方向高度契合。
我的目标是在 Infrastructure Engineer 这个方向上继续深耕,
构建支撑 AI 应用的核心基础设施。
禁忌:
- 超过 5 分钟(面试官会打断你,这很尴尬)
- 念简历(面试官已经看过了)
- 说"我负责了很多事"(说具体的一件事)
第四部分:Coding 模拟(45分钟)
题目1:LRU 缓存(Go 实现)
面试官: 请实现一个 LRU Cache,支持 Get 和 Put 操作,时间复杂度要求 O(1)。
候选人应做的事(前5分钟):
- 先问清楚约束(容量固定还是动态?线程安全?)
- 说出思路(hash map + doubly linked list)
- 再开始写代码
模拟对话:
面试官: 好,说说你的思路?
候选人: 需要 O(1) 的 Get 和 Put,就意味着两件事:
- 查找必须 O(1) → Hash Map
- 维护访问顺序并在 O(1) 删除最近最少用的 → 双向链表
两者组合:Hash Map 的 value 存双向链表的节点指针,这样 Get 时一步找到节点,
然后 O(1) 把节点移到链表头部。超容量时,O(1) 删除链表尾部节点。
完整 Go 实现:
// LRU Cache - O(1) Get and Put
package main
import "sync"
type LRUNode struct {
key, val int
prev, next *LRUNode
}
type LRUCache struct {
capacity int
cache map[int]*LRUNode
head *LRUNode // 最近使用
tail *LRUNode // 最久未使用
mu sync.RWMutex
}
func Constructor(capacity int) LRUCache {
head := &LRUNode{}
tail := &LRUNode{}
head.next = tail
tail.prev = head
return LRUCache{
capacity: capacity,
cache: make(map[int]*LRUNode),
head: head,
tail: tail,
}
}
func (l *LRUCache) Get(key int) int {
l.mu.Lock()
defer l.mu.Unlock()
node, ok := l.cache[key]
if !ok {
return -1
}
l.moveToFront(node)
return node.val
}
func (l *LRUCache) Put(key, value int) {
l.mu.Lock()
defer l.mu.Unlock()
if node, ok := l.cache[key]; ok {
node.val = value
l.moveToFront(node)
return
}
node := &LRUNode{key: key, val: value}
l.cache[key] = node
l.addToFront(node)
if len(l.cache) > l.capacity {
removed := l.removeTail()
delete(l.cache, removed.key)
}
}
func (l *LRUCache) addToFront(node *LRUNode) {
node.prev = l.head
node.next = l.head.next
l.head.next.prev = node
l.head.next = node
}
func (l *LRUCache) removeNode(node *LRUNode) {
node.prev.next = node.next
node.next.prev = node.prev
}
func (l *LRUCache) moveToFront(node *LRUNode) {
l.removeNode(node)
l.addToFront(node)
}
func (l *LRUCache) removeTail() *LRUNode {
node := l.tail.prev
l.removeNode(node)
return node
}
面试官后续问题:
面试官: 你加了 sync.RWMutex,但 Get 用 Lock() 不是更合适用 RLock() 吗?
候选人: 好问题。Get 内部会调用 moveToFront,这会修改链表结构,所以 Get 本身是有写操作的。用 RLock 会有 data race。如果要优化读并发,可以考虑分段锁或者 lock-free 结构,但那是 premature optimization。当前 Lock() 是正确的。
题目2:K 个最近的工单(Top-K)
面试官: 你有一个实时的工单流,每个工单有一个优先级分数(float64)。设计一个数据结构,支持:
Push(ticket) 插入工单
TopK(k) 返回当前分数最高的 K 个工单
思路引导:
要求插入 O(log k),查询 O(k log k)
→ 用最小堆维护大小为 K 的窗口(最小堆的堆顶是当前 TopK 中最小的)
→ 新元素 > 堆顶:替换堆顶,重新堆化
→ 新元素 ≤ 堆顶:丢弃
Go 实现:
package main
import "container/heap"
type Ticket struct {
ID string
Score float64
Summary string
}
// MinHeap 最小堆(按 Score 排序)
type MinHeap []Ticket
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i].Score < h[j].Score }
func (h MinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x any) { *h = append(*h, x.(Ticket)) }
func (h *MinHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
type TopKTracker struct {
k int
heap *MinHeap
}
func NewTopKTracker(k int) *TopKTracker {
h := &MinHeap{}
heap.Init(h)
return &TopKTracker{k: k, heap: h}
}
// Push 插入一个工单,维护 TopK
func (t *TopKTracker) Push(ticket Ticket) {
if t.heap.Len() < t.k {
heap.Push(t.heap, ticket)
return
}
// 只有比当前最小的更大才替换
if ticket.Score > (*t.heap)[0].Score {
heap.Pop(t.heap)
heap.Push(t.heap, ticket)
}
}
// TopK 返回当前 TopK(注意:顺序不保证)
func (t *TopKTracker) TopK() []Ticket {
result := make([]Ticket, len(*t.heap))
copy(result, *t.heap)
return result
}
面试官后续问题:
面试官: 如果 k 很大(比如 k=10000),且数据量也很大(100万条),这个方案还合适吗?
候选人: 当 k 接近总量时,维护 size-k 的最小堆优势减少。可以考虑:(1)用 Quickselect O(n) 平均选出 TopK;(2)如果数据有时间维度,用分桶 + 定期重算的方案。具体选哪个取决于 push 频率和 TopK 查询频率的比例。
第五部分:系统设计(30分钟)
题目:设计 Customer Support Copilot
面试官: 设计一个 AI 辅助客服系统,要求:
- 能理解用户问题,从知识库检索答案
- 复杂问题转人工处理
- 能代表用户创建 Jira 工单(需要人工审批)
- 日处理量:50,000 对话
候选人的正确打开方式(前3分钟)
先澄清需求,不要直接开画图:
-
Clarify:
- 响应时间 SLA 是什么?(假设:95th percentile < 3s)
- 支持多语言吗?(假设:中英文)
- 人工审批的 SLA?(假设:工作时间 15 分钟内)
- 数据合规要求?(假设:GDPR,PII 不能出现在日志)
-
估算规模:
50,000 对话/天
假设每对话平均 5 轮 = 250,000 次 LLM 调用/天
峰值系数 3x → ~8 QPS LLM 调用
每次调用平均 2000 tokens input + 300 tokens output
每天 token 消耗:250,000 × 2300 = 575M tokens
架构设计(候选人画图并讲解)
推荐用口头描述 + 快速草图:
用户 → API Gateway → Agent Service
│
┌──────────┼──────────┐
│ │ │
RAG Service LLM Client Tool Hub
│ │
Vector DB Jira / Slack
(Qdrant)
Agent Service → Postgres(对话历史、工单状态)
Agent Service → Redis(缓存、幂等键、Session)
Agent Service → Kafka(审核队列)→ Human Review UI
分模块讲解
1. API Gateway 职责:
- 认证(JWT 验证)
- Rate limiting(每用户 20 req/min)
- Request ID 注入(贯穿全链路的 trace id)
2. Agent Service 核心逻辑:
ReAct Loop:
Observe(user_message)
→ Think: 是否需要查 KB?需要查工单?需要人工?
→ Act: 调用 tool(search_kb / get_ticket / create_ticket)
→ Observe: 拿到 tool 结果
→ 循环直到可以生成最终回答
→ 生成 Response
3. 人工审核流程:
create_ticket tool 被调用
→ 生成工单草稿,状态 = PENDING_REVIEW
→ 发消息到 Kafka topic: ticket-reviews
→ 人工审核 UI 消费消息
→ 审核通过 → 真正创建 Jira 工单,通知用户
→ 审核拒绝 → 告知用户,Agent 给出替代方案
4. 可观测性:
- 每次 LLM 调用:记录 prompt tokens / completion tokens / latency / model
- 每次 tool 调用:记录 tool name / params / result / duration
- 关键业务指标:自动解决率、转人工率、平均响应时间
面试官 Follow-up(候选人需要准备)
面试官: 如果 RAG 知识库的内容更新了,怎么保证 Agent 用的是最新的?
候选人: 文档更新触发 pipeline:
(1)Webhook 接收文档变更事件
(2)重新 chunking + embedding 更新的文档
(3)在 Vector DB 中 upsert(用文档 ID 作为幂等键,避免重复插入)
(4)旧 chunk 标记 obsolete 或直接删除
(5)同时 invalidate Prompt Cache 中涉及该文档的条目
面试官: 这个系统的单点故障是哪里?
候选人: 几个关键节点:
- LLM API:OpenAI 故障。缓解:Fallback 到 Anthropic/Azure OpenAI,同时有 Response Cache 覆盖高频 query
- Vector DB:Qdrant 单点。缓解:主从复制,读从写主
- Postgres:对话历史存储。缓解:多副本 + 定时备份,对话历史丢失是可接受降级
- 人工审核积压:Kafka offset lag。缓解:监控 consumer lag,自动扩容 reviewer 池,超时后降级(直接拒绝,通知用户联系人工)
第六部分:Deep Dive(10分钟)
面试官通常会选你系统中某个模块深挖,常见方向:
方向1:RAG 精度问题
面试官: 你怎么衡量 RAG 检索的质量?
候选人: 三个指标:
- Recall@K:正确答案的来源文档是否在 Top-K 结果中
- Precision@K:Top-K 中有多少是真正相关的
- MRR(Mean Reciprocal Rank):正确文档平均排在第几位
在 evals 目录下维护一个测试集(golden set):50-100 个 query + 正确来源文档,每次更新知识库或调整 embedding 参数后自动跑 eval。
方向2:成本控制
面试官: 如果成本超预算了,你会怎么做?
候选人: 优先级从高到低:
- 分析成本分布(哪些 query、哪些 tool 调用消耗最多 token)
- 开启模型路由(简单 query 用 mini 模型)
- 启用 Prompt Cache(高频 query 缓存结果)
- 压缩 System Prompt(去掉冗余描述,砍掉不必要的 tool 文档)
- 如果以上不够,调整 chunk 检索数量(从 Top-5 降到 Top-3)
第七部分:Behavioral 问题 + STAR 答法
STAR 框架
Situation:当时的背景和约束
Task:你需要做什么
Action:你具体做了什么(这是面试官最关注的)
Result:结果是什么(量化)
常见问题 + 示例答案
Q: 说一个你遇到的最难的技术挑战
答案结构:
- S: 在构建 RAG Agent 时,发现 LLM 偶尔会给出自信但错误的答案,且用户很难辨别
- T: 需要在不增加太多延迟和成本的前提下,降低幻觉率
- A: 引入三层防御:(1)Retrieval 置信度阈值——相似度 < 0.75 的结果不送入 LLM;(2)LLM 回答中强制引用 source;(3)对 10% 的随机样本做 LLM-as-judge 评分,低于阈值触发 alert
- R: 幻觉率(通过 eval set 衡量)从 12% 降到 3.5%,LLM-as-judge 成本约 $0.002/评估,总体成本增加 < 5%
Q: 你遇到过意见不合的情况吗?怎么处理的?
答案结构:
- S: 团队讨论是否要在 Agent 里支持多轮对话历史(stateful conversation)
- T: 我认为应该先做 stateless,另一位同学坚持 stateful 更好
- A: 我提议先把两个方案的 pros/cons 写下来对比,同时做了一个快速 prototype 测量 stateful 对 token 成本和延迟的影响。数据显示每轮对话增加约 800 tokens,月成本多 $200
- R: 基于数据,团队决定先做 stateless,待 DAU 到一定规模后再引入有选择性的 conversation history(只保留最近 3 轮)
Q: 你如何在没有完整信息的情况下做决策?
答案结构:
- S: 需要决定 Vector DB 选型,但 Qdrant 和 Weaviate 都没有大规模压测数据
- T: 需要在一周内做决定
- A: 设计了一个快速 benchmark:用 100K 个真实 embedding 向量,测试 10 QPS 和 100 QPS 下的 P99 latency 和 recall 精度。同时对比了云托管方案的成本
- R: Qdrant 在当前规模下性能更好且成本更低,选用 Qdrant。记录了 benchmark 方法和结果,方便未来重新评估
Q: 告诉我一次你主动超出职责范围做事的经历
答案:
- S: 在做 Agent 系统时,发现 LLM 调用没有任何成本追踪
- T: 这不在我的任务范围,但如果不解决,成本会完全透明
- A: 用一个下午实现了 CostTracker 中间件,自动记录每次调用的 token 用量和费用,并做了月成本推算仪表盘
- R: 第一个月就发现了一个高频但可以缓存的 query pattern,缓存后节省了约 30% 的 LLM 调用成本
第八部分:Follow-up Questions 应对
当你不知道答案时:
-
先说你知道的边界:
"我对 X 部分比较熟,Y 的具体细节我不太确定,我的猜测是……不知道我的方向对吗?"
-
拆解问题:
"这个问题其实包含两个子问题:A 和 B。A 我有把握,B 让我想一下……"
-
主动承认并转:
"这块我确实没有深入研究过,但如果我来设计,我会从 [你知道的点] 开始考虑……"
绝对不要做:
- 沉默超过 20 秒什么都不说
- 说"我不知道"然后停止
- 编造一个听起来合理但不确定的答案(面试官一追问就穿帮)
第九部分:候选人提问(5分钟)
好的问题展示你思考过这个岗位:
关于技术:
- 你们的 AI 基础设施目前最大的技术挑战是什么?
- 团队内部的 on-call 轮换怎么做的?发生故障时的 escalation 流程是什么?
关于团队:
- 新人在头三个月通常的 ramp-up 路径是怎样的?
- 团队现在最需要解决的问题是什么?
关于产品:
第十部分:自测清单
第十一部分:作业
任务1:自我介绍录音
- 对着手机录音,完整说一遍 5 分钟自我介绍
- 回听:有没有"然后,然后,然后"的口头禅?有没有超时?
任务2:Coding 计时练习
- 限时 20 分钟,手写 LRU Cache(不看参考)
- 限时 15 分钟,手写 TopK Tracker
任务3:系统设计口头讲解
- 对着白板(或空白文档),讲完整 Customer Support Copilot 设计
- 计时 25 分钟(留 5 分钟给面试官问题)
任务4:STAR 故事准备
- 写下你这 30 天里最有挑战的 3 件技术事情
- 每个用 STAR 格式整理成 200 字以内的口头版本
下一步:Day 30
明天是最后一天:整理 GitHub repo,做最终 checklist,以及 30 天的回顾。
今晚睡前想一想:
- 今天模拟中,你回答得最流利的是哪个部分?
- 你被卡住的地方是哪里?明天重点补强。