Week 4 Day 29:全真面试模拟 - 苏格拉底教学

今天不教新知识。今天你是候选人,我是面试官。准备好了吗?


第一部分:模拟前的心理准备

🤔 问题1:你害怕什么?

引导你思考:

  1. 怕被问到不会的题?→ 面试官更想看你如何处理不会的题
  2. 怕系统设计说不清楚?→ 说不清楚 = 没有在脑子里跑过这个设计
  3. 怕 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分钟):

  1. 先问清楚约束(容量固定还是动态?线程安全?)
  2. 说出思路(hash map + doubly linked list)
  3. 再开始写代码

模拟对话:

面试官: 好,说说你的思路?

候选人: 需要 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分钟)

先澄清需求,不要直接开画图:

  1. Clarify:

    • 响应时间 SLA 是什么?(假设:95th percentile < 3s)
    • 支持多语言吗?(假设:中英文)
    • 人工审批的 SLA?(假设:工作时间 15 分钟内)
    • 数据合规要求?(假设:GDPR,PII 不能出现在日志)
  2. 估算规模:

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:成本控制

面试官: 如果成本超预算了,你会怎么做?

候选人: 优先级从高到低:

  1. 分析成本分布(哪些 query、哪些 tool 调用消耗最多 token)
  2. 开启模型路由(简单 query 用 mini 模型)
  3. 启用 Prompt Cache(高频 query 缓存结果)
  4. 压缩 System Prompt(去掉冗余描述,砍掉不必要的 tool 文档)
  5. 如果以上不够,调整 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 应对

当你不知道答案时:

  1. 先说你知道的边界:

    "我对 X 部分比较熟,Y 的具体细节我不太确定,我的猜测是……不知道我的方向对吗?"

  2. 拆解问题:

    "这个问题其实包含两个子问题:A 和 B。A 我有把握,B 让我想一下……"

  3. 主动承认并转:

    "这块我确实没有深入研究过,但如果我来设计,我会从 [你知道的点] 开始考虑……"

绝对不要做:

  • 沉默超过 20 秒什么都不说
  • 说"我不知道"然后停止
  • 编造一个听起来合理但不确定的答案(面试官一追问就穿帮)

第九部分:候选人提问(5分钟)

好的问题展示你思考过这个岗位:

关于技术:

  • 你们的 AI 基础设施目前最大的技术挑战是什么?
  • 团队内部的 on-call 轮换怎么做的?发生故障时的 escalation 流程是什么?

关于团队:

  • 新人在头三个月通常的 ramp-up 路径是怎样的?
  • 团队现在最需要解决的问题是什么?

关于产品:

  • 你们怎么衡量 AI Agent 的产品价值?

第十部分:自测清单

  • 自我介绍能在 5 分钟内说完,且有具体项目细节
  • LRU Cache 能在 20 分钟内写完并解释清楚
  • TopK 能解释最小堆的思路,并说出时间复杂度
  • 系统设计能说出至少 4 个模块的设计决策和取舍
  • 能回答"RAG 知识库更新"和"单点故障"这两个 follow-up
  • 准备好 3 个 STAR 故事(技术挑战、冲突、主动超出职责)
  • 准备好 3 个候选人问题

第十一部分:作业

任务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 天的回顾。

今晚睡前想一想:

  • 今天模拟中,你回答得最流利的是哪个部分?
  • 你被卡住的地方是哪里?明天重点补强。