26664 字
133 分钟
2026-10-13 AI Agent 面试训练

2026-10-13 AI Agent 面试训练#

配置读取状态:成功。实际使用配置版本 version: 4(last_reviewed: 2026-10-12),未做任何修改。

题型结构变更说明:配置 v4(2026-10-12 变更记录「题型分布调整」)把每日配额从 4 题调整为 7 题——非 Agent 基础题 2 道、AI Agent / LLM 基础题 2 道、Agent 场景题 1 道、非 Agent 通用工程场景题 1 道、LeetCode 1 道。依据「配置新增或调整从下一次训练开始生效」与「不使用提示词中的旧分类覆盖配置文件」,本次起按 7 题执行,历史训练文件不回写。编号约定:B01/B02 非 Agent 基础、B03/B04 Agent 基础、S01 Agent 场景、S02 非 Agent 场景、L01 LeetCode。

历史读取范围:better/题目索引.md(12 条)+ better/精选/README.md(1 条精选)+ 最近 30 天内全部训练文件(2026-10-10、2026-10-12,共 12 题)+ better/README.md。无更早文件,无需回溯更早正文。目标目录及父目录无 AGENTS.md / CLAUDE.md。

去重结论:强去重窗口 30 天,按标题、一级类型、二级主题、核心考点、预期结论、场景约束、LeetCode 题号七个维度比对。今日 7 题与窗口内 12 题无重复、无换皮、无考点高度重合;首次覆盖 4 个一级类型(分布式系统、编程语言与运行时、RAG 与搜索、AI 与大模型基础);LeetCode 215 首次出现,算法模式绕开已用的哈希表+双向链表与哈希表+滑动窗口。今日无复习题。

今日主题:不确定性之上如何建确定性——重试与幂等的语义边界、协程并发的执行边界、检索召回与排序的分层、微调与推理的经济学,以及在真实 SLO 下做路由降级与容量兜底。


一、今日学习主题与题型分布#

今天刻意补齐索引中标注「尚未覆盖」的四个一级类型,并按配置 v4 首次把通用工程能力放进当日配额。七道题的共同主线是:当外部依赖、模型输出与流量都不可控时,系统里哪几件事必须由我们保证确定。

编号题型一级类型考察方向题目 ID
B01基础定义(非 Agent)分布式系统幂等键、可重试性判定、退避抖动、超时预算传导与重试风暴治理Q-20261013-B01
B02基础定义(非 Agent)编程语言与运行时协程让出点、三类阻塞的隔离、取消与超时、结构化并发、并发限流Q-20261013-B02
B03基础定义(Agent)RAG 与搜索稀疏/密集互补、融合策略与分数陷阱、重排收益边界、RAG 分层归因Q-20261013-B03
B04基础定义(Agent)AI 与大模型基础微调 vs RAG/Prompt 的取舍、LoRA 与全参差异、灾难性遗忘、回归评测Q-20261013-B04
S01真实场景(Agent)LLM 应用工程 / Agent 核心多模型分级路由、降级矩阵、成本与延迟预算、缓存粒度、质量不塌方Q-20261013-S01
S02真实场景(非 Agent)数据库与存储 / 分布式系统缓存雪崩与热点 key:逻辑过期、singleflight、多级缓存、陈旧容忍Q-20261013-S02
L01LeetCode堆与优先队列 / 分治215. 数组中的第K个最大元素(Medium)Q-20261013-L01

二、今日题目(建议先独立作答)#

B01|基础定义题|分布式系统|Q-20261013-B01#

企业 Agent 平台的「工单执行 Agent」经工具网关调用内部工单系统创建单据。某次网络抖动,网关统一重试 3 次,事后发现重复创建了 137 张工单、发送了 40 条重复短信。改造方案有四派:

  • 甲:把重试去掉,失败就报错。
  • 乙:给每个请求加个 request id,服务端收到相同 id 就跳过。
  • 丙:退避从 100ms 改成 1s,之后每次乘 2。
  • 丁:超时从 3s 放宽到 30s,减少超时重试。

请回答:

  1. 为什么”加个 id 跳过”不足以解决问题?幂等键的范围、粒度、存储与生命周期分别应由谁决定?
  2. 哪些错误可重试,哪些绝不能重试?网络超时属于哪一类,为什么它最危险?
  3. 丙的退避改造缺了什么关键一环?只是放慢速度能不能解决重试风暴?
  4. 丁把超时放宽到 30s 会引入什么新问题?超时预算应该如何在调用链上分配?

难度:中高。考察方向:至少一次投递下的幂等设计、可重试性判定、抖动(jitter)的必要性、deadline 传导、重试预算与服务端自保护。


B02|基础定义题|编程语言与运行时|Q-20261013-B02#

一个 Agent 服务单次会话要并发调用 8 个外部工具,再汇总给模型做一次决策。团队用 Python asyncio 写了 await asyncio.gather(*tasks),但 P99 延迟几乎没改善。随后有人把其中一个同步 HTTP 调用包进 ThreadPoolExecutor,略有好转;又有人在某一步加了 pandas 聚合与一个回溯式正则,上线后整个服务所有并发请求一起卡住几百毫秒。另外有两个已知问题:取消一次请求后后台任务仍在跑;一个工具失败导致整批 gather 直接抛出,另外 7 个结果全丢。

请回答:

  1. 协程并发到底”并行”在哪?让出点在什么地方?为什么一个同步调用会拖垮整个循环?
  2. 阻塞 I/O、CPU 密集计算、同步第三方库,三类分别用什么手段隔离?各自的代价是什么?
  3. 取消与超时怎么做才正确?用 asyncio.wait_for 包住一个正在跑的 CPU 任务会发生什么?被取消的任务如何清理?
  4. gather 与 TaskGroup 在错误处理与任务生命周期上的差别是什么?结构化并发解决什么问题?
  5. 并发度该怎么限制?限制应放在哪一层的什么资源上?

难度:中高。考察方向:事件循环单线程语义与 await 让出点、线程池/进程池与 GIL 边界、CancelledError 传播与不可取消计算、结构化并发与任务泄漏、信号量/连接池/下游配额三层限流。


B03|基础定义题|RAG 与搜索|Q-20261013-B03#

企业知识问答 Agent 用向量召回取 top-50 拼进上下文,线上答案正确率只有 61%。四个候选方案:

  • A. 换成更大更强的 embedding 模型。
  • B. 加 BM25 做混合检索,两路结果融合后取 top-50,再上一路 cross-encoder 重排到 top-8。
  • C. 只做 query rewrite 与多查询扩展,不加稀疏检索,不加重排。
  • D. 直接把 top-8 塞进上下文,前 42 条丢掉。

请回答:

  1. 稀疏检索(BM25 类)与密集检索各自在什么情况下失效?给出具体例子,而不是泛泛而谈。
  2. 两路召回的分数怎么融合?为什么分数归一化 + 加权求和经常踩坑?**只用排名的融合(如 RRF)**规避了什么假设、又损失了什么?
  3. 重排什么时候收益有限或直接亏损?召回不足时上重排会发生什么?
  4. 上线后效果仍不达标,如何分层归因:召回缺失、切片错误、融合/重排错位、上下文位置未被利用、还是生成端问题?每层用什么指标判定?

难度:中高。考察方向:稀疏与密集的互补性、融合策略的隐含假设、两阶段漏斗的成本与延迟、以及最容易失分的错误归因。


B04|基础定义题|AI 与大模型基础|Q-20261013-B04#

同一个垂直 Agent 上线后暴露三类问题:工具参数经常漏字段、领域黑话识别不准、API 调用成本超预算。三派意见:

  • 甲:微调一个 7B 模型,用自己的数据对齐。
  • 乙:别微调,换更强的大模型 + 完善 RAG。
  • 丙:做量化部署,把成本压下来。

请回答:

  1. 微调解决什么、不解决什么?为什么”用微调注入领域知识”往往既最贵又是错的那条路?
  2. LoRA / adapter 与全参数微调在工程上的差异是什么?多租户如何复用?灾难性遗忘怎么发现?
  3. 量化与蒸馏分别换走了什么、留下了什么?收益边界在哪?
  4. 正确的决策顺序与止损机制是什么?如何防止”微调后整体上升、某个子集静默退化”?
  5. 什么时候应当先不微调,只改 prompt / 工具描述 / 检索?给出可操作的判定依据。

难度:中高。考察方向:知识注入 vs 行为对齐的分野、LoRA 的低秩假设与复用边界、灾难性遗忘与分层评测、训练/服务/迭代的完整成本模型。


S01|真实研发场景题(Agent)|Q-20261013-S01#

背景:企业售后 Agent 平台,服务 3 条业务线(退款、技术支持、账单查询),日均 40 万次会话,单会话平均 6-9 次 LLM 调用(意图识别 → 参数抽取 → 工具选择 → 结果总结 → 安全复核)。

当前架构与数据流:所有会话统一走单个外部强推理模型(唯一供应商、单一区域)。无缓存、无分级、无降级。工具层为内部 HTTP 服务与若干第三方 API。

已知现象:

  • 月度模型账单超预算 40%,其中「意图识别」「格式归一化」这类简单步骤占约 45% 的 token。
  • 会话 P95 6.8s,目标 4s;用户感知最强的是首 token 时间。
  • 供应商限流时全量返回 429,网关统一重试,队列堆积、延迟进一步恶化。
  • 一次供应商区域故障造成整体不可用 47 分钟,期间无任何降级路径。
  • 抽查发现约 18% 的调用历史上使用过几乎相同的 prompt 前缀。

目标:在不降低高风险操作正确性的前提下,成本下降 30%,会话 P95 ≤ 4s,供应商级故障时可自动降级且不中断。

限制条件:退款、改账、权限变更类操作必须使用强模型或转人工,不得自动降级;合规要求每次调用记录模型标识、版本与路由决策原因;多租户不得互相挤占配额;6 周内分阶段灰度。

请给出:能力分级与路由决策方案(含”预测式路由 vs 试后升级”的取舍)、缓存与批处理设计、降级矩阵与故障切换、失败路径与幂等、成本/延迟/可靠性分析、评测指标与上线观测、回滚策略。

难度:高。考察方向:成本-延迟-准确性三维取舍、路由误判代价的不对称性、缓存粒度与命中率的矛盾、自动降级 vs 静默质量劣化。


S02|真实研发场景题(非 Agent)|Q-20261013-S02#

背景:商品详情聚合服务(纯确定性服务,不依赖模型),Postgres 主库 + Redis Cluster 读缓存,峰值 QPS 12 万,命中率 96%,命中场景 P99 12ms。

已知现象:

  • 一次批量配置发布把约 30 万个 key 的 TTL 统一设为 3600s;一小时后这批 key 几乎同时失效,数据库瞬时 QPS 从常态 5 千冲到 9 万,连接池被打满,大面积超时,持续 6 分钟后才被人工限流挡住。
  • 一个明星商品的详情 key 让单分片 CPU 打满,同分片其他业务一起受影响。
  • 冷门商品被恶意扫描,产生大量必然不命中的查询直击数据库。

目标:峰值期间数据库 QPS 稳定在 2 万以内,P99 ≤ 20ms;单机房故障时业务可容忍分钟级陈旧,但不可用时间为零。

限制条件:不能要求调用方改造;Redis 资源有上限,不能无限扩容;允许返回有限陈旧数据,但陈旧窗口必须有上界且可观测;运营后台改价后,详情页必须在秒级感知(写-读一致性承诺)。

请给出:TTL 与失效治理方案、热点 key 与穿透治理、多级缓存的一致性取舍、失败路径(缓存/数据库不可用时的降级与恢复)、容量与应急预案、指标与上线观测、回滚策略。

难度:高。考察方向:过期模型(TTL / 逻辑过期 / 永不过期)、singleflight 与重建互斥、热 key 治理、一致性 vs 陈旧容忍 vs 吞吐的三角取舍、运营写入后的失效链路。


L01|LeetCode|堆与优先队列 / 分治|Q-20261013-L01#

215. 数组中的第K个最大元素(Kth Largest Element in an Array,Medium)
给定整数数组 nums 和整数 k,返回数组中第 k 个最大的元素。注意是排序后第 k 个最大的元素,不是第 k 个不同的元素。
官方链接(已核验可访问):https://leetcode.cn/problems/kth-largest-element-in-an-array/ | https://leetcode.com/problems/kth-largest-element-in-an-array/

难度:Medium(中英文页面均标注中等)。考察方向:定长小根堆维护 Top-K 与随机化快速选择(三路划分),以及二者在稳定性与常数上的工程取舍。


✋ 建议先独立作答(基础题每题 1-2 分钟口头作答,场景题每题 3-5 分钟,LeetCode 25 分钟手写),再往下看答案。



三、完整参考答案#

B01 幂等、重试、退避与超时预算#

1. 考察点#

  1. 为什么必须有幂等:在不可靠网络上,“不重试”等价于把可用性交给网络。网关没看到响应,并不代表下游没有执行。
  2. 幂等键的定义权与作用域:由谁生成、包含什么、局限到哪个范围、存多久、命中已有记录时返回什么。
  3. 可重试性判定:读与非幂等写的区别,瞬时故障 vs 永久故障,以及网络超时的”薛定谔”语义。
  4. 退避必须有抖动:确定性退避会把重试重新对齐成同步风暴。
  5. 超时预算的纵向传导:每一层都必须比下一层的”重试总耗时”更宽松,否则重试只是在刷新同一个已经到期的截止时间。

2. 准确定义与适用边界#

幂等(Idempotency):同一操作的多次执行,对系统可观察状态的影响等同于执行一次。

三个常被混用的词必须分清:

  • 幂等:重复执行的外部效果相同(多次创建只产生一张单)。
  • 并发安全:多个并发操作互不破坏(行锁、CAS、唯一约束)。它不是幂等的替代品,二者通常叠加使用:唯一索引解决同一时刻的竞争,幂等记录解决跨时间的重放。
  • 去重:常带有时间窗口含义,是幂等的一种实现手段;但”去重”暗示事后清理,而幂等必须在写入路径上同步判定。

适用边界:幂等只能保证”定义在该键内”的操作不多执行一次。它不能让一个非幂等的工具变成幂等,也不能跨语义复用同一个键。

3. 简洁示例#

本次语义动作是「给工单 T-1001 添加一条备注 X」。

幂等键 = hash(租户ID | 工具名 | 语义动作指纹 | 业务主键)
= sha256("tenant=finance|tool=add_comment|ticket=T-1001|body=X")

写入路径必须是单事务、三状态:

BEGIN;
INSERT INTO idempotency_keys (tenant, key, request_hash, state, created_at)
VALUES (?, ?, ?, 'in_progress', now())
ON CONFLICT (tenant, key) DO NOTHING;
-- 受影响行数为 0 表示已有同键记录:
-- state='in_progress' → 返回"处理中",绝不重复执行
-- state='done' → 直接回放首次执行时保存的响应
-- request_hash 不一致 → 同一个键被两个不同语义的请求复用,必须显式报错
COMMIT;
-- 顺序固定:占位 → 执行副作用 → 状态改 done 并落盘首次响应

最容易被漏掉的细节:响应必须持久化。幂等命中时不能返回”200 + 空内容”,否则调用方拿不到单据号,会认为失败而再次重试。

4. 与相近概念的区别#

概念回答的问题做不到什么
幂等键同一个请求被发送了多次覆盖不同语义操作;单独解决并发竞争(需配唯一约束)
重试请求可能失败了让一个非幂等操作变安全
事务 / 锁并发写入互不破坏跨请求、跨进程、跨时间的去重
exactly-once效果恰好发生一次端到端通常不可达;真实系统是 at-least-once + 消费端幂等

这里必须说出一句硬结论:分布式系统里没有端到端的”恰好一次”,只有”至少一次 + 幂等”或”最多一次 + 可接受丢失”。 凡是宣称端到端 exactly-once 的方案,本质都是把幂等做在了某个边界上。

5. 在 AI Agent 系统中的典型应用#

  • 工具网关:每次有副作用的工具调用都要携带幂等键。键应在编排层生成——因为重试发生在编排层,而不是在模型”决定再调一次”的时候。
  • 模型调用与工具调用的对账式区分:重试一次 LLM 调用的代价是 token 与延迟,通常不产生不可逆副作用;重试一次”转账 / 发货 / 发短信”的代价是真实世界动作。二者绝不能共用同一套重试策略。
  • HTTP 语义的正确使用:GET/HEAD/PUT/DELETE 在标准语义下幂等,POST 不幂等。无状态 RPC 风格的工具想安全重试,就必须显式携带幂等键——这是把不可靠网络变成可靠系统的唯一通道。

6. 常见误区或错误用法#

  1. 用 request id 当幂等键:request id 标识一次传输,重试时通常会被重新生成,于是去重完全失效。必须用”业务语义指纹”而非传输 id。
  2. 粒度错配:做成「会话 + 步骤序号」→ 重试时编号变化,绕过去重;做成「用户 + 天」→ 当天同类操作只能成功一次,业务直接不可用。
  3. 缺少”处理中”状态:并发重试时第二个请求看不到 done 就判定未执行,于是重复执行。必须三态:in_progress / done / failed(可重置)。
  4. 只在内存去重:进程重启或多实例部署立刻失效,幂等状态必须落在共享存储且在写入路径上同步可见。
  5. 对所有错误一律重试:对参数类 4xx 重试只会放大无效流量;被限流(429)时不遵守服务端给出的等待时间,是最典型的踩踏行为。
  6. 无限重试:生产者永远重试会把瞬时故障放大成永久雪崩。重试必须同时有次数上限与时间上限(deadline)。

7. 面试现场参考回答(1-2 分钟)#

根因不是”没去重”,而是我们把重试放在了没有幂等边界的那一层。网关没看到响应,可能是下游没执行,也可能是执行了但响应丢了;所以只要允许重试,就必须假设”可能已经执行”,也就必须让重放安全。

我的改法是四件事:第一,幂等键由编排层生成,内容包括租户、工具名、语义动作指纹和业务主键,而不是传输层的 request id;第二,幂等占位、副作用执行、结果落盘放进同一事务,状态三态化,命中已完成请求时回放首次响应而不是返回空成功;第三,按错误类型分流——超时、连接错误、5xx 可重试,429 必须遵守 Retry-After,参数类 4xx 直接失败;第四,退避必须带抖动,并给出每次调用的总重试预算(例如不超过请求量的 10%),超出即失败,避免把下游抖动放大成风暴。

丁的方案我不认同。把超时从 3s 放到 30s 会占住连接与线程,故障期间资源更快耗尽。正确做法是给整条链路一个 deadline,每层超时都从该预算里扣,且上层 deadline 必须大于下层的重试总耗时,否则重试只是在追一个已经到期的截止时间。

8. 面试官追问及回答要点#

追问:如果下游完全不支持幂等键、而且短期改不动,怎么办?

回答要点:

  1. 尽力幂等:执行前按业务指纹查本地”已发出 / 已收到”记录,命中即跳过。不严格,但能挡掉绝大多数重复。
  2. 把非幂等动作改写成”创建意图”:先落一条本地意图单并把结果回填,重复意图天然收敛为一条。
  3. 对账与补偿:事后按指纹识别重复并按策略撤销或合并,这是兜底而不是主路径。
  4. 产品层面降维:高危操作改由人工或串行队列处理,把分布式问题降级为单机问题。
  5. 诚实说明边界:“尽力幂等 + 事后对账”不提供严格保证,因此这类操作必须在产品上可撤销、有限额。

B02 协程并发模型:让出点、取消与结构化并发#

1. 考察点#

  1. 事件循环的单线程本质:协程不是并行执行,而是在 await 点显式让出。
  2. 三类阻塞的隔离手段各不相同,混用会掩盖真实瓶颈。
  3. 取消语义:取消只在让出点被处理,纯 CPU 计算不会自我中断。
  4. 结构化并发:任务生命周期必须被包在作用域内,不能凭空存活。
  5. 限流层次:并发度限制必须映射到真实受限资源。

2. 准确定义与适用边界#

协程(coroutine):可在特定位置挂起与恢复的函数。在 asyncio 中,只有 await 会让出事件循环,而且是协作式的——没有任何运行时抢占。

准确的心智模型:一条线程上有一个事件循环,循环里有若干 Task;每个 Task 从当前位置跑到下一个 await,把控制权交还循环,循环再去跑另一个就绪的 Task。由此推出三条硬结论:

  • 一段没有 await 的代码会独占整个线程,循环无法调度任何人,于是”并发”的其他请求全部停顿,表现为整体 P99 恶化而不是单个请求变慢。这正是题中回溯式正则拖垮全局的原因。
  • async def 只是把函数标记为协程;内部如果调用同步阻塞 I/O(例如未使用异步客户端的 requests),依然阻塞事件循环,gather 退化为串行加额外调度开销。
  • 协程提升的是等待 I/O 时的资源利用率,不能提升 CPU 吞吐。要用多核必须走多进程或进程池。

适用边界:I/O 密集且并发量高时收益大;CPU 密集任务必须卸载;只有当底层有原生异步驱动(HTTP、Redis、Postgres async driver)时才真正生效。

3. 简洁示例#

# 推荐写法:TaskGroup 提供结构化并发作用域
async def handle(client):
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(call_tool(client, n)) for n in TOOLS]
return [t.result() for t in tasks] # 任一失败,其余被自动取消,不会泄漏

三类阻塞的处理方式不同:

await asyncio.to_thread(requests.get, url, timeout=3) # 1) 同步阻塞库 → 线程池卸载
loop.run_in_executor(process_pool, heavy_compute, payload) # 2) CPU 密集 → 进程池,绕过 GIL
await httpx.AsyncClient().get(url) # 3) 原生异步客户端最省资源

4. 与相近概念的区别#

维度协程 async/await线程 threading进程 multiprocessing
CPU 并行否(单线程)受 GIL 限制,交替执行是
切换成本极低(用户态)高(内核态,栈内存 MB 级)很高
数据安全await 点即可见的潜在竞态点任意位置都可能被抢占地址空间隔离
适用高并发 I/O少量遗留阻塞库调用CPU 密集

一个常被忽略的优势:协程的竞态更容易推理,因为抢占点就是 await,只有跨 await 才需要考虑共享状态被别人改动。

另一个必须讲清的区别:并发(concurrency)≠ 并行(parallelism)。gather 给的是并发调度,不是并行执行;吞吐提升来自”等待时不空转”,而不是”同时算”。

5. 在 AI Agent 系统中的典型应用#

  • 工具扇出:一次会话并发调用几十个工具,是 asyncio 收益最大的场景;但每个调用都要单独超时,并用信号量限制并发,否则会一次性把下游打爆,再叠加重试风暴。
  • 流式响应 + 后台任务:一边 SSE 流式吐 token,一边后台落 trace;后台任务必须挂在 TaskGroup 作用域内,否则请求结束后仍在运行,就是任务泄漏。
  • 用户点”停止”要真的停:模型请求与本地任务可以取消,但已发出的工具调用需要向下游传播取消(关闭 HTTP 连接、调用 MCP Tasks 的协作式取消)。本地取消不等于远端停止,这是 Agent 工程里最常见的幻觉。
  • 并发上限要有依据:数值不是凭手感设 20,而是取连接池容量、下游配额、本机 CPU 核数三者最小值。

6. 常见误区或错误用法#

  1. async def 里调同步阻塞库:判断方法不是”写了 async”,而是这一行是否真正 await 到了支持异步的底层实现。
  2. gather 不带超时与异常处理:一个慢任务拖死整批;一个失败任务让其余结果全部丢失(return_exceptions=True 至少能保住结果,但必须显式检查每个元素是否为异常)。
  3. 以为 wait_for 能中断 CPU 计算:wait_for 依赖取消机制,取消在下一个 await 点才处理;纯计算没有 await,任务会继续跑到自己结束,只是调用方先超时了。
  4. 吞掉 CancelledError:它继承自 BaseException,except Exception 抓不到;但如果写了裸 except BaseException 且不再抛出,取消就彻底失效,关停流程会被卡住。
  5. 忽略共享资源:并发放大后最先耗尽的是数据库连接池;池大小与并发量必须一起设计。
  6. 把 CPU 密集任务丢进线程池:GIL 不会带来并行,只会增加调度开销,正确归宿是进程池。

7. 面试现场参考回答(1-2 分钟)#

先说结论:gather 只在 await 点让出控制权,是协作式的。P99 没改善通常意味着某处在占着事件循环不放——同步 HTTP、CPU 密集计算,或一次回溯式正则。定位不靠猜,把事件循环的滞后(lag)作为指标监控,lag 一高就说明有人没让出 CPU。

三类东西处理方式不同:阻塞 I/O 换原生异步客户端,改不掉就用线程池卸载;CPU 密集必须走进程池,因为 GIL 决定了线程池无用武之地;CPU 密集且不可中断的任务,本地取消是幻觉,必须向下游传播取消信号。

健壮性上我用 TaskGroup 替代裸 gather:异常不再静默丢失,子任务生命周期被作用域约束,退出时不会有孤儿任务。并发度不是拍脑袋定的 20,而是连接池容量、下游配额、本机 CPU 三者取最小值;每个下游单独超时,整体再包一层 deadline。

8. 面试官追问及回答要点#

追问:任务被取消时正在写一半的数据,怎么保证不留垃圾?

回答要点:

  1. 取消是协作的:任务应在 try/finally 或 async with 中清理;finally 里不要 await 可能永不返回的东西,必要时用 asyncio.shield 保护关键补偿步骤。
  2. 用幂等键保护副作用(承接 B01):即使取消在语义上”成功”、远端其实已执行,重来一次也安全。跨层的答案永远是幂等,而不是更精细的本地逻辑。
  3. 不可中断计算放到进程池,并配合操作系统信号或超时机制终止。
  4. 用状态机而非裸标志:把这次执行记为 cancelled 而不是直接丢弃,配合对账处理”本地取消成功但远端已完成”的情况。
  5. 可观测:记录取消到真正停止的耗时分布,而不是只在日志里写一行 “task cancelled”。

B03 混合检索、融合与重排的收益边界#

1. 考察点#

  1. 稀疏与密集检索的失效模式必须具体到例子,而不是”一个语义一个字面”。
  2. 融合策略背后的假设:分数可比吗、分布稳定吗、需要校准吗。
  3. 两阶段漏斗的成本与延迟账:什么时候重排是纯亏损。
  4. 分层归因能力:能不能在指标层面判断问题出在哪一段。
  5. 重排不能替代召回这一基本认知。

2. 准确定义与适用边界#

稀疏检索(BM25 / TF-IDF 系):基于词项频率与逆文档频率的字面匹配。

  • 强项:稀有标识符的精确命中——错误码 ERR_4021、零件号 ADC-8812X、法条编号、人名、版本号。这些恰恰是向量模型最容易糊掉的东西,因为它们在语义空间中彼此很近,却从不等价。
  • 弱项:同义、上位词、跨语言表述。“怎么退款”与”申请退回款项”在词项上几乎不重叠。

密集检索(向量语义检索):把 query 与文档映射到同一向量空间,按相似度检索。

  • 强项:语义泛化、表述变体、拼写模糊。
  • 弱项:精确字符串与编号(容易把 V2 与 V3 判为很近)、罕见领域术语(不在 embedding 训练分布内)、以及否定与约束(“不能用 X 的场景”与”如何使用 X”在向量上很近)。

因此二者是互补失效,不是新旧替代。混合检索的正当性来自失效模式的正交性,而不是”多一路总归好一点”。

适用边界:查询含大量精确标识符、或领域术语不在 embedding 训练分布中时,纯向量检索必然有洞;反之,纯描述性、同义变体多的语料,密集路优势明显。

3. 简洁示例#

设备运维手册问答场景:

Query稀疏(BM25)密集(向量)该依赖谁
“ERR_4021 如何处理”精确命中,排第一与 ERR_4022 / 4023 混淆稀疏
“设备很烫怎么办”词项不重叠则可能失效稳定命中”过热保护”章节密集
“机型 ADC-8812X 的额定扭矩”型号与单位精确命中可能召回 8812Y混合,且稀疏路加权
“不能用高压水枪的部位”无法理解”不能”极易召回”如何清洗”两路都弱,需要改写或结构化过滤

融合策略分两类:

分数融合(归一化加权求和):score = α·norm(dense) + (1-α)·norm(sparse)。它要求两路分数在同一可比的尺度上。BM25 的分数无上界、随文档长度与语料统计剧烈漂移;余弦相似度挤在 0~1 的窄区间。做 min-max 归一化会被离群值拉扯——一个异常高分就能把整批分布压缩到极小范围,于是 α 需要持续重新校准,今天调好的权重明天就失效。

排名融合(RRF, Reciprocal Rank Fusion):RRF_score(d) = Σ_i 1/(k + rank_i(d)),k 常取 60。它只吃排名不吃分数,因此不要校准、不惧分布漂移。代价是丢掉了分数的”置信度差距”(第一名与第一百名的差异被压平成 1/(k+1) 与 1/(k+100)),在需要靠分数阈值做裁剪的场景里不够用。另一个实现细节:只出现在某一路结果中的文档是否参与另一路的求和,不同实现处理不一致,落地时必须明确写死。

4. 与相近概念的区别#

概念位置输入规模成本 / 延迟主要失效
稀疏 / 密集召回第一阶段全库或分片级低各自模型固有的盲区
融合(Fusion)两路之间两路 top-N极低(纯计算)融合假设错误导致负优化
重排(Rerank)第二阶段20~100 条高(cross-encoder 逐对推理)延迟与成本,且救不回召回缺失
切片(Chunking)索引之前全量文档一次性切片破坏语义,后续环节无法补救

关键认知:重排是排序变换,不是召回补充。 如果正确片段不在 top-50 里,再强的 cross-encoder 也找不出来——它只能给已有候选重新排序。题中方案 D(只留 top-8)的危险正在于此:它把”可能有用的第七名候选”直接丢弃,把 recall 上限压死在 top-8。

5. 在 AI Agent 系统中的典型应用#

  • 检索即工具:RAG 是 Agent 最常见的工具调用之一。混合检索 + 重排是标准的两级漏斗(召回 50 → 重排 8 → 进上下文)。
  • 延迟必须按候选数计算:cross-encoder 要对每个 (query, doc) 对做一次完整前向传播,50 个候选就是 50 次推理。若整条链路延迟预算 800ms,重排可能占掉 300-500ms。压缩手段包括:候选从 50 降到 20、使用蒸馏过的小重排模型、设置 200ms 硬超时并降级为融合排名、以及只对”高价值查询”重排。
  • 归因分层是本题最值钱的部分:
层症状判定指标典型原因
召回缺失正确文档不在任何一路的 top-N 中Recall@50(两路并集后)切片破坏语义、embedding 与领域不匹配、元数据过滤过严
排序错位在 top-50 但没进 top-8Recall@8 / MRR / nDCG@8融合权重错误、重排缺位、重排模型不适配领域
上下文失效正确片段进入上下文但答案仍错位置分布与”中间丢失”人工抽检片段位置靠中、多条片段互相干扰、系统指令与片段冲突
生成 / 可信问题上下文中有答案但模型没用Groundedness、引用命中率模型依赖先验、缺少引用约束、不确定时未被要求澄清

归因顺序必须是:先验召回,再验排序,最后才怀疑模型。 多数团队直接跳到”换个更大的模型”,而答案早在召回阶段就丢了。

6. 常见误区或错误用法#

  1. 召回不足靠重排救:这是最烧钱的方向性错误,付出数倍延迟却动不了根因。
  2. 把两路分数直接相加而不做验证与校准:结果通常是 BM25(分数量纲巨大)事实上拿到了接近全部的权重,另一路被完全掩盖。
  3. 把 RRF 当成数学最优融合:它是鲁棒的启发式(用排名对抗分布漂移),不是最优;需要置信度信息的场景应回到校准过的分数融合。
  4. 换了 embedding 却不重建索引:维度或模型变了必须全量重建,否则索引里混着两个不同空间的向量。
  5. 把 top-50 全塞进上下文:既费钱又拉低效果(中间位置利用率低);正确做法是重排压缩到 top-8,并在系统提示里明确”证据不足就追问或说明缺失”。
  6. 不做分层指标就上 A/B:出问题时聚合指标除了告诉你”整体掉了 3%“之外毫无定位价值。

7. 面试现场参考回答(1-2 分钟)#

我选 B,但不会砍掉 C,也不会立刻做 A。

理由是这里的失效模式高度分叉:错误码、零件号、指令编号这类精确标识符,在向量检索里会消失——它们在语义空间中彼此很近。稀疏路恰好在这些点上强壮,密集路在表述变体上占优。两路是互补失效,不是相互替代。

融合我不用”归一化加权求和”,因为 BM25 分数无上界、随语料漂移,余弦相似度挤在 0 到 1 的窄区间,两者做归一化加权需要持续校准,稍有不慎就有一路被完全掩盖。我倾向 RRF 这种只吃排名的融合:不需要校准、抗漂移,代价是损失分数间距;如果需要更精细的阈值裁剪,再换回校准过的分数融合。

重排我会做,但设硬预算:候选从 50 降到 20,用蒸馏小模型,超时 200ms 未返回就降级为融合排名。而且上重排之前先验 Recall@50 是否达标(比如 ≥ 90%),否则重排只是在给一个已经漏掉答案的候选集做精装修。

上线后效果仍不达标,就按四层归因:Recall@50 看召回、Recall@8 看排序、人工抽检位置分布看上下文利用、最后才是模型可信度。哪一层掉了就修哪一层,不轻易得出”换更大的模型”的结论。

8. 面试官追问及回答要点#

追问:RRF 的 k 怎么定?调大会发生什么?

回答要点:

  1. k 是抑制排名尾部影响的平滑因子。k 越大,不同名次之间的分差越被压平(趋向”只要进入列表就同等计分”);k 越小,头部名次的权重越突出。
  2. k = 60 是原始论文给出的经验值并有抗排名抖动的作用,但必须在自己的数据上做网格(如 10/20/60/100)并用 Recall@8 验证,不要把它当成物理常数。
  3. 注意 RRF 对弱路召回没有免疫力:如果多查询扩展产生了一路质量很差的候选,它同样会被平等计分。此时应使用加权 RRF 或先过滤弱路。
  4. 出处:RRF 由 Cormack、Clarke、Buettcher 在 2009 年 SIGIR 论文《Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods》中提出,最初用于多路查询融合(本轮未核验原文,按公开引用的通用描述陈述)。

B04 微调、LoRA、量化与蒸馏:该解决什么问题#

1. 考察点#

  1. 知识注入 vs 行为对齐的分野,这是本题的核心。
  2. LoRA 的低秩假设与它在多租户、多任务中的工程性质。
  3. 灾难性遗忘如何被发现,而不是指望它不发生。
  4. 量化与蒸馏各换走了什么、留下了什么。
  5. 成本模型不只是训练费用:还包括数据工程、迭代周期、部署运维与版本绑定。

2. 准确定义与适用边界#

先把四件事的边界划清楚:

  • 微调(Fine-tuning):在预训练权重上用梯度更新模型参数。它擅长的是行为对齐——让模型稳定地遵循某种格式、风格、决策边界与工具使用习惯。它不擅长的是知识注入——用微调”记住”几千条频繁变动的知识,得到的往往是一个昂贵、无时效、且无法溯源的黑箱。
  • RAG:在推理时把外部知识注入上下文。它解决的是知识的时效性、可溯源、可定点更新——改文档即改知识,并能回答”依据哪一条”。
  • 量化(Quantization):降低权重或激活的数值精度(如权重量化到 int8 / int4 一类方法)来换取显存、带宽与吞吐。它改变的是表示精度,不是模型的知识或能力。
  • 蒸馏(Distillation):让小模型拟合教师模型的输出分布,得到的是更小的学生模型,能力逼近教师。它适合低成本部署,但不创造超越教师的能力。

一句话结论,也是本题的题眼:行为问题找微调,知识问题找 RAG,成本与吞吐问题找量化和蒸馏。 把三类问题打包成同一个解决方案,是这类项目最常见的烧钱方式。

适用边界(为什么”用微调注入领域知识”通常既贵又错):

  1. 知识有时效要求(价格、政策、库存、流程变更)——微调的知识被冻在 checkpoint 里,更新需重跑训练。
  2. 知识需要溯源与审计(必须回答”依据哪条规定”)——微调无法提供引用。
  3. 知识量大或频繁变化——训练成本随数据量上涨,且每次更新都要重做全套评测。
  4. 缺少高质量人工标注数据——微调的数据质量门槛远高于 RAG,劣质数据会直接放大错误行为。

3. 简洁示例#

题中三个现象的正确归类:

现象归类首选手段
工具参数漏字段行为 / 格式问题约束解码(schema 约束)→ 修正工具描述与 few-shot → 仍有明确缺口才微调
领域黑话识别不准知识 / 词表问题术语表 + 查询改写或扩展;必要时微调 embedding 模型,而不是微调 LLM
API 成本超预算经济问题分级路由(简单步骤用小模型)、缓存、批处理;最后才考虑量化部署

注意第二行那个容易被忽略的事实:知识问题在检索侧解决,往往比在生成侧解决便宜一个数量级,而且当天就能上线。 第三行同理:降本的第一杠杆是”别用贵模型做便宜事”,而不是先把权重压成低精度。

4. 与相近概念的区别#

维度全参微调LoRA / adapterRAG量化蒸馏
参数更新范围全部注入的低秩矩阵(原权重冻结)不改模型不改结构,改精度训练一个学生模型
适合问题大幅能力或行为重塑任务格式对齐、低成本多任务适配知识时效与溯源推理成本 / 显存 / 吞吐轻量部署
灾难性遗忘风险高相对低(原权重不变)无有精度损失,一般不构成”遗忘”能力上限来自教师
多租户复用每租户一份完整权重一份主干 + N 份低秩适配器,可热切换按租户隔离索引与过滤通用,一次量化多处受益一份学生服务多任务
服务复杂度高中(需 multi-LoRA 路由与版本管理)中(引入检索与上下文拼装)低低

必须说清的细节:LoRA 论文(Hu 等,arXiv:2106.09685,2021-06-17 提交,v2 于 2021-10-16 修订)的核心假设是适配过程具有低秩性,可训练参数量与显存需求因此大幅下降,且由于适配器与原权重可合并,不引入额外推理延迟(相较于会插入串行模块的 adapter)。论文以 GPT-3 175B 为例给出了可训练参数量与显存的大致下降幅度——这是论文自身的实验结论,落到具体业务仍需实测。

5. 在 AI Agent 系统中的典型应用#

  • 工具调用稳定性:症状不是”模型不知道有这个工具”,而是”不能稳定输出符合 schema 的参数”,本质是行为对齐。因此优先级是:schema 约束解码 → 工具描述与 few-shot 修正 → 失败样本驱动的小规模微调。
  • 多租户模型服务:每个租户一份 LoRA 是可行架构——一份主干加 N 份低秩适配器,成本远低于维护 N 份完整权重。但必须解决三件事:请求到适配器的确定性路由、适配器版本管理与回滚、以及冷启动与显存调度。
  • 高风险操作:正确做法是”模型提议 + 确定性规则校验 + 人工审批”,绝不能把开放性权限托付给任何单一模型的行为稳定性。

6. 常见误区或错误用法#

  1. 用微调”灌知识”,尤其是频繁变动的知识,代价高、无时效、不可溯源。
  2. 拿微调去补工程债:RAG 没做好、工具描述写得含糊,就说”模型不行,我们微调”。这是最贵的甩锅方式。
  3. 忽略灾难性遗忘:整体指标上升、子任务下降。必须按业务线 × 工具 × 语言 × 风险等级分层看评测。
  4. 以为数据量决定一切:几百条一致、覆盖目标行为的样本,常常胜过几万条松散抓取的数据;必须留独立验证集。
  5. 没有对照基线就宣称微调有效:不改 prompt、不改检索、不改上下文组装的对照都没有,就无法证明收益来自微调。
  6. 只算训练费用:真实成本包括数据工程、反复迭代、部署运维,以及模型版本绑定带来的迁移风险。

7. 面试现场参考回答(1-2 分钟)#

我先把三个问题分开:工具参数漏字段是行为与格式问题,领域黑话识别是知识与词表问题,成本超预算是经济问题。把它们打包成”我们一起微调吧”,是这类项目最常见也最贵的错误。

顺序上我选乙为主、甲为辅、丙放到最后:先用术语表、查询改写和完善 RAG 把知识问题吃掉;工具参数问题先用约束解码加 schema 校验,再加一次带错误反馈的修复式重试——这三件事今天下午就能上线,成本几乎为零,收益往往比微调更大。三步都做不动,且评测数据能证明(而不是凭感觉),才考虑微调。

真要微调,我选 LoRA 而不是全参:原权重冻结,灾难性遗忘风险更低;一份主干加 N 份低秩适配器,多租户复用与回滚都便宜。前提是路由与版本管理要跟上——请求必须确定地打到某个适配器,版本必须可审计。

评测必须分层,按业务线、工具、语言、风险等级划分,聚合指标会掩盖子集退化。同时把旧基线固定为对照组,任何子集相对下降超过阈值就否决候选模型。

成本问题不能靠量化解决。量化改善的是单次服务的经济性与吞吐,不改变 token 总量。真正的降本是分级路由加缓存——也就是今天的 S01 题;量化要等到模型和收益都被测量清楚之后,作为第二阶段,而不是第一段。

8. 面试官追问及回答要点#

追问:微调后怎么建立回归评测,防止”整体上升、子集下降”?

回答要点:

  1. 分层金标集:按业务线 × 工具 × 语言 × 风险等级分层,每层样本量足以检出真实差异;只看总体准确率一定会被掩盖。
  2. 固定对照组:同 prompt、同检索、同上下文组装,跑两次取方差,避免把随机波动当成收益。
  3. 设置否决规则:任何分层相对下降超过约定阈值(例如绝对下降 3 个百分点或相对下降 10%)即不允许上线,无论总体指标是否提升。
  4. 影子流量:把候选模型在真实流量上旁路运行,与线上模型逐条对比,重点观察高风险子集与人工接管率。
  5. 版本可回滚:模型标识、适配器版本、prompt 版本三者一起进 trace,任何一次质量回退都能定位到具体组合并快速回滚。
  6. 成本一并纳入评测:准确率、延迟、成本三个维度同时报告,避免用成本换来的准确率提升被误判为纯收益。

S01 场景题:模型路由、降级、成本与延迟优化#

1. 背景、目标、约束与已知现象(回顾)#

项内容
业务售后 Agent,3 条业务线,日均 40 万会话,单会话 6-9 次 LLM 调用
现状单一外部强推理模型、单供应商、单区域;无缓存、无分级、无降级
现象成本超预算 40%;会话 P95 6.8s(目标 4s);429 时统一重试导致队列堆积;区域故障 47 分钟不可用;18% 调用存在近乎相同的 prompt 前缀
目标成本降 30%,P95 ≤ 4s,供应商级故障自动降级不中断,高风险操作正确性不降
约束退款 / 改账 / 权限变更必须强模型或人工;每次调用记录模型标识、版本、路由原因;租户配额隔离;6 周分阶段灰度

2. 核心考点#

  1. 按”调用用途”而非”业务名称”分级,并建立可解释的路由依据。
  2. 预测式路由 vs 试后升级的取舍,以及误判代价的不对称性。
  3. 缓存粒度与命中率的矛盾,以及缓存在不确定系统里的正确性边界。
  4. 降级矩阵的层级顺序:模型级 → 供应商级 → 能力级 → 人工级,及其触发与恢复条件。
  5. 静默质量劣化的识别:自动降级期间如何知道质量掉了,而不是等到客诉。

3. 作答框架(3-5 分钟可复用)#

  1. 先量化现状(40 秒):把单次会话拆成若干”调用槽位”,给出每个槽位的 token 占比、延迟占比、失败率与容错级别,明确哪些槽位是”能力敏感”、哪些是”格式敏感”。
  2. 给预算而不是给口号(60 秒):定义会话级延迟预算(如 4s)与成本预算(元/千会话),并说明预算如何在槽位间分配。
  3. 分级与路由决策(80 秒):调用分级矩阵 → 模型层(强/中/快/本地)→ 路由方式(预测式 vs 试后升级)→ 路由纠错通道。
  4. 成本与延迟的四个杠杆(50 秒):削减(少调)、降级(换小模型)、缓存(不重算)、批处理(降单位成本)。
  5. 边界与风险收尾(30 秒):哪些操作永不允许自动降级,怎么在降级态观测质量,回滚开关在哪。

4. 参考回答(结论先行)#

结论:这题的解法不是”换一个更便宜的模型”,而是把一次会话拆成不同能力要求的槽位,给每个槽位配最小够用的模型,并给整条链路一个可观测的降级矩阵和预算闸门。

我会分四步落地。第一,按调用用途分级:意图识别、格式归一化、简单分类这类”格式敏感、能力不敏感”的槽位交给快模型;工具选择与参数抽取交给中档模型;只有涉及资金与权限、或需要多步推理的复核与复杂安抚,才交给强模型,并且这部分不允许自动降级。这部分大约能吃掉 45% 的 token 成本,因为它正好对应题中最贵却最不费脑的那部分调用。

第二,路由用预测式分级 + 试后升级兜底。预测式路由(按槽位、历史难度、租户历史表现打的分)成本低、延迟最好,但会误判;所以我给每个用快模型的槽位配一个廉价校验器——结构化 schema 校验、置信度阈值、或在关键槽位做一次小模型自评——校验不过就用同一会话上下文升级到强模型重试一次,且只重试一次。这里的关键是误判代价的不对称:把一个难题路由给快模型,代价是一次升级的额外延迟;把简单题全部路由给强模型,代价是长期的钱。

第三,缓存必须按语义等价键来做,而不是按原始字符串前缀。题中 18% 的相似前缀很重要,但真正的收益来自”工具无关意图 + 归一化参数 + 上下文版本号”三元组构成的缓存键,并且要有明确的失效规则(prompt 版本、工具 schema 版本、模型版本任一变化即全量失效)。缓存命中率我按”可缓存槽位占比”而不是”总调用占比”来算,这样这个数字才不自欺。

第四,降级矩阵按四级排布,逐层下线:模型级(强→中→快,仅适用于非资金类槽位)→ 供应商级(主供应商→备用供应商→本地小模型)→ 能力级(关闭总结润色、关闭多轮追问澄清,保留工具执行与模板化答复)→ 人工级(高风险操作转人工队列)。每一级都要有自动恢复条件与冷静期,避免抖动用户在各级间反复横跳。

最后是最容易被漏掉的一点:降级不等于没代价,只是代价换了个形式。 所以每一级降级都要在体验上可见(答复质量预期下调、响应提示”当前为快速模式”),在数据上可观测(按降级级别分桶统计任务成功率、人工接管率、客诉率),并且给一条手动回滚开关——任何一级降级只要触发质量阈值或人工判定,5 分钟内可回到上一状态。

5. 问题拆解、核心方案与关键执行流程#

问题拆解

  1. 成本项分类:哪些 token 是”必须用这么贵的模型”产生的,哪些是”惯性”产生的。先用采样 trace 把 40 万会话的调用按槽位打标签,得到 token × 单价 × 调用次数的三维账。
  2. 延迟项分解:首 token 时间、工具执行时间、总结生成时间分别占多少。P95 6.8s 里如果工具占了 4s,那么优化模型是南辕北辙;必须先证明延迟归属,再动手。
  3. 失败项分解:429 属于”请求过多”而非”服务异常”,对它做统一重试是把限流信号当成故障信号,属于典型的处理错配。

核心方案

会话请求
→ 意图与难度打分(快模型 / 规则特征,< 150ms)
→ 槽位分级:P0 资金与权限 / P1 工具选择与参数 / P2 格式与归一 / P3 润色与总结
→ 模型路由(P0→强模型不可降级;P1→中档;P2/P3→快模型)
→ 每槽位执行 + schema/置信度校验
├─ 通过 → 继续
└─ 不通过 → 升级模型重跑一次(仅一次),仍失败 → 走降级或转人工
→ 会话级预算闸门:累计延迟/成本超阈值 → 后续槽位强制降档
→ 落 trace(模型标识、版本、路由原因、是否升级、是否降级)

关键执行流程

  1. 先做只读影子:新路由在影子模式下与旧链路并行跑两周,产出分级准确率与成本模拟报表,不动线上结果。
  2. 第二步开缓存层:只缓存明确幂等、无副作用、时效性可控的槽位(意图识别、格式归一化),先证明质量指标不动。
  3. 第三步开降级路由:先在非资金业务线(技术支持)灰度 5% → 20% → 50%,每档设 24 小时冷静期。
  4. 第四步开供应商切换:做单区域故障演练,验证 47 分钟不可用这一类场景能否压缩到分钟级。

6. 技术选型、依据与备选#

决策选择依据备选及不选的原因
路由方式预测式打分 + 廉价校验器 + 单次升级大多数请求难度可预测;升级通道覆盖误判纯试后升级:每次都先试小模型,失败率高时总延迟更差
模型层三级(强 / 中 / 快),必要时加本地小模型层太少节省有限,层太多路由成本高且难以评测单一替代模型:把可用性押在另一个单点上
缓存键语义等价三元组 + 版本哈希原始字符串前缀命中率低且极易误命中纯前缀哈希:prompt 微改即全失效,且可能把不同语义判为同一请求
缓存位置先做同步短 TTL 缓存,确认收益后再引入批处理批处理会改变延迟语义,不应与降延迟目标并行引入一开始就上 Batch API:省一半钱但破坏首 token 体验
供应商冗余主 + 备商用模型 + 本地降级模型区域级故障必须靠多供应商,而不是多区域重试只做多区域:区域级防控不了供应商级事件
预算控制会话级 + 租户级双闸门单会话闸门防个别失控,租户闸门防互相挤占仅全局限速:多租户下必然互相伤害

7. 失败路径、边界条件、超时、重试、幂等与恢复#

失败场景处理关键细节
429 限流遵守 Retry-After,按指数退避 + 抖动重试,计入重试预算不得对所有错误统一重试;重试预算用尽即降级而不是无限排队
5xx / 连接超时换同层备用模型;同层全失败则降一级模型每次降级必须在 trace 中记录,供事后归因
单个槽位升级后仍失败走确定性兜底:模板答复或转人工兜底路径必须预先验证,不能是”当时随手写的分支”
缓存返回错误结果校验器与下游业务规则做二次确认;命中不等于可信对 P0/P1 槽位默认不缓存;缓存只为可容错槽位服务
供应商整体不可用切换备用供应商;不可用则降为本地模型 + 模板答复切换要带冷静期,避免主备之间来回抖动
重复调用风险工具调用仍带幂等键(承接 B01),模型调用本身无副作用但计费重试与升级都必须复用同一幂等键,避免重复创建工单/退款
会话已过半才降级保留已完成槽位的上下文,后续槽位降档不允许”重新组织整个会话”,那会把延迟翻一倍
恢复逐级恢复 + 观察窗口 + 自动回滚开关恢复比降级更容易出事故,必须比降级更保守

8. 安全、权限、成本、延迟与可靠性分析#

  • 安全与权限:路由是执行路径的一部分,不是旁路。降级路径不得绕过授权与审计——任何一级降级仍然经过同一套权限校验、工具白名单与审计落库。合规要求的”模型标识 + 版本 + 路由原因”必须写进 trace,且该记录不可被降级逻辑裁剪。
  • 授权边界不能交给模型:是否允许退款、金额上限、是否需要人工审批,这些是确定性系统的职责;模型只能提议。降级到更小模型时,能力下降不能导致权限扩张。
  • 成本:目标 30% 的构成建议拆开承诺——路由分级约贡献 20-25%(基于那 45% 简单步骤的 token),缓存贡献 5-15%(取决于可缓存槽位占比),批处理留作第二阶段。把三个来源分开写,才能在某一项不达预期时知道 why。
  • 延迟:模型分级、缓存、减少重试三者共同作用。但要警惕”降级反而更慢”的错觉:一次升级重试的总耗时 = 快模型 + 校验 + 强模型,若升级率过高(例如 > 20%),快模型路径就比直接上强模型更贵更慢。升级率是这套方案的生命线指标。
  • 可靠性:多级降级本身引入了更多代码路径,更多路径意味着更多未测分支。因此每级降级都必须有常态化演练(故障注入、影子回归),而不是上线后第一次在故障中被人肉验证。

9. 评测指标、测试方法与上线观测#

离线评测

  • 分层金标集(业务线 × 槽位 × 风险等级),重点看 P0 子集(资金/权限)在降级模型下的表现,P0 的正确率目标应接近满分,任何下降直接否决。
  • 路由准确率:预测式路由把”本该上强模型”的请求误判为快模型的比例,以及对应的升级率。这两个指标一起决定成本收益与延迟风险。
  • 成本模拟:用影子模式的 trace 重放,计算新路由下的模拟账单与旧账单差值。

在线指标(按降级级别分桶上报)

类别指标
质量任务成功率、工具调用精确率/召回率、人工接管率、客诉率、P0 子集正确率
性能首 token 时间 P50/P95、会话 P95、升级重试率、缓存命中率(按可缓存槽位口径)
成本元/千会话、元/成功任务、token 分布(按模型层与槽位)
可靠性各模型层可用率、降级触发次数与持续时长、故障演练 RTO

上线观测与告警

  1. 每个指标按”是否处于降级态”拆成两条曲线:只看合并曲线会在降级期间被正常态掩盖。
  2. 降级触发本身要告警,并关联当时的质量指标面板。
  3. 设置质量熔断:某降级级别下的人工接管率或失败率超过阈值,自动回到上一级别。
  4. 灰度节奏:影子 → 非资金业务线 5% / 20% / 50% → 资金业务线只读验证 → 资金业务线仅启用缓存与批处理(不启用模型降级)。按合规约束,退款、改账、权限变更类操作不允许自动降级,只允许强模型或转人工;每档之间留 24 小时冷静期与回滚开关。

10. 三个渐进追问及回答要点#

追问 1:预测式路由怎么打分?用模型打分会不会又要多花一次调用?

要点:优先用便宜特征——槽位类型、历史同类请求的成功模型、prompt 结构复杂度、工具 schema 复杂度、租户历史难度分布;必要时用一个极小的分类器或快模型做一次”难度二分类”,其成本必须低于它省下的钱,并用离线重放证明净收益。核心原则是:打分本身的成本要进入成本账,不能被漏掉。

追问 2:降级到小模型后,怎么知道质量有没有静默掉下去?

要点:三条腿——(1)在线代理指标:工具调用失败率、澄清追问率、会话轮次异常增长、人工接管率;(2)异步抽样用强模型-as-judge 做旁路评分,注意 judge 本身的偏差与位置敏感性,必须与人工标注定期校准;(3)用户侧显性信号:重问率、否定反馈、转人工请求。没有代理指标就不要自动降级,只能降级后立即告知用户”当前为快速模式”。

追问 3:备用供应商的切换怎么验证不是在”用延迟换可用”?

要点:演练必须包含”满载切换”而不是空载切换——备份供应商的容量需要预先压测与配额预留,否则故障时会集体涌入并被打回 429。切换要分批(按比例放量而非全量瞬切),并有回滚通道与双写时期的成本上限。演练指标是 RTO 与切换期间的成功率,而不是”能不能切成功”。

11. 常见误区(4 个)#

  1. 把”换便宜模型”当成全部答案:不改分级、不设升级通道,结果高风险子集静默退化,客诉起来才发现。
  2. 对 429 做统一重试:把限流信号当故障信号处理,是这次”队列堆积”的直接原因。正确做法是遵守 Retry-After、退避带抖动、并计入重试预算。
  3. 缓存按原始 prompt 字符串做键:要么命中率极低,要么在语义不同但字面相近的请求上误命中;且 prompt 版本变化后必须全量失效,否则会命中旧语义的结果。
  4. 只看聚合指标掩盖降级态:合并曲线上看不出降级期间的质量恶化;必须按降级级别分桶,并让降级事件本身可告警。

12. 30-60 分钟小型实践#

用 200 条历史会话 trace,做一份”路由收益与风险”的定量报告:

  1. 把每次调用打上槽位标签(意图/抽取/选择/总结/复核),统计每个槽位的 token 与延迟占比。
  2. 设定一个朴素路由策略(例如:意图与格式归一走快模型,其余不变),用离线重放估算成本下降幅度。
  3. 对每个”被降级的槽位”定义校验器(schema 校验或关键词/置信度检查),估算升级率。
  4. 输出三个数字:预估成本下降百分比、预估 P95 变化、以及在 P0 子集上是否出现任何质量下降。P0 出现下降即判定策略不可用。

13. 三层能力划分(本题核心,必须说清)#

层负责什么本题中具体承担的事项不能承担什么
模型的概率性能力理解意图、抽取参数、生成自然答复、判断难度边界意图识别、参数抽取、总结润色、自评置信度权限判定、金额合规、幂等保证、限流公平性
Agent 编排 / 状态与工作流槽位编排、路由决策、升级重试、上下文与记忆、工具序列路由打分、升级通道、会话级预算闸门、工具调用编排与幂等键下发模型的语义正确性、下游可用性
传统确定性系统授权、限流、预算、审计、缓存一致性、故障切换与回滚权限校验、租户配额、重试预算与退避、trace 落库、降级开关与回滚、成本计量语义理解、复杂推理

一句话:模型的判断永远是提议,编排决定流程如何容错,确定性系统守住边界并给出可审计的事实。


S02 场景题:缓存雪崩与热点 key#

1. 背景、目标、约束与已知现象(回顾)#

项内容
架构调用方 → 商品详情聚合服务 → Redis Cluster(读缓存)→ Postgres(主库)
规模峰值 QPS 12 万,命中率 96%,命中场景 P99 12ms,常态数据库 QPS 约 5 千
现象30 万 key 同 TTL(3600s)同时失效 → 数据库瞬时 QPS 9 万 → 连接池打满 → 6 分钟大面积超时;明星商品 key 打满单分片 CPU;冷门商品被扫描产生大量穿透查询
目标峰值期数据库 QPS ≤ 2 万,P99 ≤ 20ms;单机房故障容忍分钟级陈旧但不可用时间为零
约束调用方不可改造;Redis 资源有上限;允许有限陈旧但陈旧窗口有上界且可观测;运营改价后详情页秒级感知

2. 核心考点#

  1. 过期模型的选择:TTL、逻辑过期、永不过期三者的语义差异与各自的失败模式。
  2. 重建互斥(singleflight / 锁)与”重建风暴”的区别,以及避免用 Redis 分布式锁写出新的一致性问题。
  3. 热点 key 的横向拆解方式与代价(key 分片、本地缓存、读写分离)。
  4. 穿透治理(空值缓存、布隆过滤器/集合)与其假阳性代价。
  5. 一致性 vs 陈旧容忍 vs 吞吐的三角取舍,以及运营写入后的失效链路设计。

3. 作答框架(3-5 分钟可复用)#

  1. 先定量化冲击(40 秒):同时失效的 key 数 × 单 key 重建耗时 × 单实例并发 = 落到数据库的瞬时 QPS,明确”击穿”是数学问题而不是运气问题。
  2. 按三类问题分别给出机制(100 秒):过期(TTL 抖动 + 逻辑过期)、单 key 重建(singleflight + 兜底陈旧值)、热点(本地缓存 + key 分片)、穿透(空值缓存 + 前置集合)。
  3. 讲清一致性取舍(50 秒):能容忍多久陈旧、陈旧的观测口径是什么、强一致路径(改价)怎么走。
  4. 讲清降级与恢复(40 秒):Redis 不可用怎么办、数据库不可用怎么办、人工开关在哪。
  5. 用指标收尾(20 秒):命中率、数据库 QPS、穿透量、陈旧度分布、重建次数分桶。

4. 参考回答(结论先行)#

结论:这次事故的根因是把缓存的过期时间做成了全局同步事件,而不是选择了错误的缓存产品。修复要同时做三件事:过期去同步、重建去并发、热点去单点,再加一条写入驱动的失效链路来保证”改价秒级生效”这个承诺不破。

第一,过期去同步。所有 TTL 必须加随机抖动(例如 base + random(0, 20%·base)),写入时逐 key 生成,而不是配置里一个统一值;批量发布要按 key 分批并设置不同的基准值。这一步几乎零成本,就能把”同时失效”变成”均匀分布”。

第二,重建去并发。缓存未命中时,同一 key 只允许一个回源重建,其余并发请求等待并复用结果——这就是 singleflight(进程内的键级互斥),而不是去抢一把 Redis 分布式锁。分布式锁在这里是错误选型:它把”回源风暴”换成了”抢锁风暴”,并额外引入锁超时与误释放的新问题。进程内互斥的粒度正好匹配”每个实例只回源一次”,跨实例的那一点冗余(N 个实例最多 N 次回源)通常是可接受的,因为 N 远小于 QPS。

第三,热点去单点。明星商品的单分片被打满,是因为这一个 key 承担了全部读流量。两个手段叠加:本地进程缓存(短 TTL,比如 1-3 秒,命中后连 Redis 都不访问,读 QPS 下降一个数量级)+ key 分片(把 detail:sku123 拆成 detail:sku123:{0..7},读取随机选一片,写入时全部失效)。本地缓存的代价是数据陈旧窗口放大,所以必须显式声明并观测上界;key 分片的代价是写入失效要触达所有分片,且必须保证”全部失效”这一步可重试、可校验。

第四,穿透治理。冷门商品被扫描产生必然不命中的查询,要分两层处理:不存在的结果也要缓存(短 TTL 空值,防止同一 id 反复回源)+ 前置集合(用 Bloom filter 或等价集合挡掉完全不存在的 id)。必须承认 Bloom filter 有假阳性,因此它只能是”挡明显不存在的 id”的前置过滤,不能作为数据源真值;且它需要随商品上下架同步维护,维护失败时要能降级。

最后,也是最容易被忽略的一环:“改价秒级生效”不能被任何缓存机制破坏。 正确做法是写入驱动的主动失效——运营侧写库成功后发出失效消息(事务提交后再发),消费者按 SKU 删除所有副本(Redis 各分片 + 本地缓存广播),并对失败重试与补偿。至于”允许分钟级陈旧”的那部分场景,走的是 TTL 自然过期,两条路径在数据契约上必须分开写明,不能混用同一个 TTL 去同时满足”秒级”和”允许分钟级”。

5. 问题拆解、核心方案与关键执行流程#

问题拆解

  1. 雪崩的数学:30 万 key 同时失效,如果回源一次 5ms、服务有 60 个实例、每个实例并发 200,瞬时落到数据库的请求数远超其承载能力(观测到 9 万),而常态只有 5 千。这不是”缓存没命中”的小概率事件,而是必然事件。
  2. 击穿 vs 雪崩 vs 穿透必须分清楚,三者机制完全不同:
    • 击穿:单个热 key 失效的瞬间,大量并发同时回源。
    • 雪崩:大批 key 在同一时间窗口集体失效。
    • 穿透:查询的 key 根本不存在,缓存永远不命中,请求直达数据库。
  3. 一致性目标的拆分:不同字段有不同的一致性要求。价格/库存类必须强感知,描述/评价摘要类可容忍分钟级陈旧。混在一个 JSON 里用同一个 TTL,就无法同时满足两者。

核心方案

读取路径:
本地缓存(短 TTL,热点为主,命中即返回)
↓ miss(进程内 singleflight 互斥)
Redis(逻辑过期 value = {data, logical_expire_at, version})
↓ miss 或已逻辑过期
回源 DB(singleflight 保证同一进程内同一 key 仅一路)
↓
回写 Redis(TTL = base + 抖动),并标记 version
写入路径(运营变更):
Postgres 事务提交 → 发出失效事件 → 消费者按 SKU 删除 Redis 各分片 + 本地缓存失效广播
→ 失败重试 + 死信告警 + 定时对账补偿

关键执行流程

  1. 先做低成本止血:TTL 抖动 + 空值缓存 + singleflight,这三步不需要扩资源。
  2. 再做热点治理:本地缓存(带陈旧上界监控)+ 热 key 分片,先在明星商品灰度。
  3. 最后做失效链路:写入驱动的主动失效,先双轨运行(主动失效 + TTL 兜底),确认无误后缩短 TTL。
  4. 全程保留手动开关:本地缓存可一键关闭、分片可一键回退、主动失效可一键停用。

6. 技术选型、依据与备选#

决策选择依据备选及不选的原因
过期策略抖动 TTL + 逻辑过期抖动解决同步失效;逻辑过期让回源可控并可返回旧值永不过期:内存无上界,且运营变更一旦漏删就是永久脏数据
重建互斥进程内键级 singleflight粒度正好匹配”每实例回源一次”,无跨网络开销Redis 分布式锁:把回源风暴换成抢锁风暴,并引入锁超时/误释放
热点治理本地缓存 + key 分片组合本地缓存吃掉绝大多数读分片解决单分片 CPU 上限只加 Redis 节点:热点仍在单一分片,加节点无效
穿透治理空值缓存 + 前置集合两层分别处理”已知不存在”与”未知 id”只做空值缓存:无限 id 空间下缓存被垃圾 key 撑爆
失效方式写入驱动主动失效 + TTL 兜底满足秒级承诺且兜底可靠纯依赖消息:消息丢失即永久脏数据,必须有 TTL 兜底与对账
陈旧观测version 字段 + 陈旧度直方图陈旧窗口必须可量化观测,而不是”应该没问题”只看命中率:命中率高不代表数据新鲜

7. 失败路径、边界条件、超时、重试、幂等与恢复#

失败场景处理关键细节
缓存集群部分不可用降级为直读数据库 + 严格限流;本地缓存继续提供陈旧数据必须有”数据库保护闸门”,绝不能让缓存失效变成数据库过载
数据库慢或不可用返回逻辑过期但仍在 TTL 容忍内的旧值;超过陈旧上界则返回明确降级响应降级响应必须与”数据缺失”区分,便于调用方与用户理解
singleflight 持有者崩溃设置回源超时;超时后释放互斥并由下一个请求重试互斥必须有超时,否则会成为新的阻塞点
主动失效消息丢失TTL 兜底过期 + 定时对账作业比对 version消息只是加速器,不能是唯一保证
本地缓存脏数据失效广播 + 短 TTL 双保险 + 陈旧度监控广播失败必须告警并可手动触发全量清空
热 key 写入风暴(每次请求都写)只有回源成功才写;且写操作本身有版本号单调性检查防止旧版本覆盖新版本(写乱序)
Redis 扩容 / 迁移期间双写或双读过渡,禁止在迁移中同时改变 TTL 策略变更一次只做一件事,否则事故无法归因
恢复顺序先恢复数据库保护闸门 → 再放开缓存写入 → 最后放开全流量恢复比降级更危险,必须分步且可观测

8. 安全、权限、成本、延迟与可靠性分析#

  • 一致性 vs 吞吐:这是本题的核心取舍。允许陈旧换来的是数据库 QPS 从 9 万降到 2 万以内,代价是陈旧窗口。正确做法不是”选一边”,而是按字段分级:强一致字段走主动失效与小 TTL,可陈旧字段走大 TTL,并在接口契约里写明每个字段的陈旧上界。
  • 陈旧必须有上界且可观测:逻辑过期时间戳写进 value,读取时记录”数据年龄”,按直方图上 P99。没有这个指标,“允许陈旧”就是一句无法验收的空话。
  • 实时 vs 异步:运营改价后的失效走异步消息+兜底 TTL,是”尽量实时 + 保证收敛”。如果业务要求严格实时,应改为写后强读主库或带版本校验的读路径,而不是靠缓存层保证。
  • 成本:本地缓存的隐性成本是内存与一致性运维;key 分片的隐性成本是写入放大(删 N 份)与运维复杂度。两者都应限定在热点集合内,不要对全量 SKU 生效。
  • 可靠性:多级缓存意味着多级状态。每一级都必须有独立的关闭开关与降级路径,否则多级叠加后的故障模式会超过团队的推理能力。

9. 评测指标、测试方法与上线观测#

核心指标

类别指标
保护层数据库 QPS(绝对值与突增倍数)、连接池使用率、慢查询数
缓存层命中率(全局与热点分别看)、回源次数分桶、singleflight 等待时长 P99
陈旧度数据年龄 P50/P99、超过上界的请求比例、主动失效端到端延迟 P99
穿透空值缓存命中数、前置集合拦截数、不存在 id 的回源量
可用性降级触发次数与持续时长、降级态下的成功率

测试方法

  1. 故障注入:模拟 30 万 key 同时失效(清理指定前缀),观察数据库 QPS 峰值是否被闸门压在 2 万以内。
  2. 热点压测:单一 SKU 打满流量,验证本地缓存与分片是否生效、单分片 CPU 是否下降。
  3. 一致性压测:运营侧高频改价,测量写后读到新值的 P99 延迟(目标秒级)。
  4. 回滚演练:本地缓存关闭、分片回退两个开关都要在流量中验证过一次,而不是写在文档里。

上线观测

  • 命中率必须拆成”本地缓存命中”与”Redis 命中”两条曲线,聚合曲线无法判断两级各自的贡献。
  • 数据年龄直方图要在改价链路有故障时立刻上升,这是主动失效是否健康的最早信号。
  • 数据库 QPS 设置突增倍数告警(如 5 分钟内增长 > 3 倍),而不是绝对值告警——绝对值告警在流量自然增长时会长期误报。

10. 三个渐进追问及回答要点#

追问 1:singleflight 用进程内互斥,N 个实例最多回源 N 次,够不够?

要点:够,因为 N(实例数)远小于 QPS 级别;真正的风暴是”每个请求都回源”。如果确实需要跨实例收敛,才引入分布式锁或租约,但要付出额外复杂度,并且必须处理锁超时导致的双写与误释放。先量级估算,再决定是否引入分布式协调,这是判断成熟度的地方。

追问 2:本地缓存导致脏数据怎么处理?

要点:三个手段叠加——短 TTL(陈旧上界)、失效广播(主动)、陈旧度监控(观测)。必须承认广播可能失败,因此 TTL 是最后保证;也必须承认存在窗口,因此要把窗口写进接口契约并对超过上界的请求计数。

追问 3:Redis 集群整体不可用时,如何保证”不可用时间为零”这个目标?

要点:靠”陈旧数据 + 严格限流 + 明确降级”三件套,而不是靠”继续打数据库”。缓存整体失效时数据库无法承载全流量,所以必须有预设的限流阈值与降级响应;本地缓存作为最后一层继续提供可接受陈旧的数据。这里的关键是提前压测出数据库的实际承载上限,把限流阈值设在它之下。

11. 常见误区(4 个)#

  1. 把 TTL 写成统一常量:等于把缓存失效变成一个同步事件,这是本次事故的根因。
  2. 用 Redis 分布式锁做回源互斥:把回源风暴换成抢锁风暴,并引入锁超时、误释放与羊群效应等新问题。
  3. 以为本地缓存没有成本:它用内存换来一致性窗口,必须有上界、有广播、有监控,否则就是不可控脏数据。
  4. 主动失效不信兜底:只依赖消息删除,消息丢失或消费者故障时会留下永久脏数据;必须有 TTL 兜底与定时对账。

12. 30-60 分钟小型实践#

用本地 Redis + Postgres(或 mock 数据源)搭一个最小复现与修复:

  1. 写一个读路径:先 Redis,miss 则回源;记录每个请求的回源次数。
  2. 用 1000 个 key、统一 TTL 5s、并发 200 请求,观察失效瞬间的回源峰值(复现雪崩)。
  3. 加入 TTL 抖动与进程内 key 级 singleflight,重复压测,对比回源峰值下降倍数。
  4. 再加逻辑过期与”返回旧值 + 异步重建”,记录数据年龄的最大值与 P99。
  5. 输出一张对比表:三种配置下的回源峰值、P99 延迟、最大陈旧时长。

13. 三层能力划分(本题同样适用)#

层承担什么边界
应用层逻辑过期模型、重建互斥、热点治理、降级与闸门不能凭空知道数据新鲜度,必须有可观测字段
缓存与消息系统提供低延迟读、TTL、发布订阅传递失效事件不保证事件必达,不保证强一致
数据库(确定性系统)数据的真值与写入一致性,是所有 retry 的最终裁判不承担读 QPS 峰值,其容量上限决定限流阈值

注:本题为非 Agent 通用工程场景,核心解法不依赖模型或 Agent。与 Agent 工程仅有上下文联系,而非解题依赖:Agent 商品问答类应用同样面临会话级缓存的热点与失效一致性问题,上述机制可以原样复用。


L01 LeetCode 215:数组中的第K个最大元素#

1. 基本信息#

项内容
题目 IDQ-20261013-L01
题号215
中文名数组中的第K个最大元素
英文名Kth Largest Element in an Array
难度Medium
官方链接中文:https://leetcode.cn/problems/kth-largest-element-in-an-array/ ;英文:https://leetcode.com/problems/kth-largest-element-in-an-array/
核验状态已核验:本轮访问中英文官方页面,标题、难度、描述、示例与约束一致(2026-10-13)

2. 题目描述#

给定整数数组 nums 和整数 k,返回数组中第 k 个最大的元素。

需要特别注意的是:题目要求的是排序后第 k 个最大的元素,而不是第 k 个不同的元素(即重复元素要按重复次数计入)。

英文页面额外提问 “Can you solve it without sorting?”,中文页面则直接要求:必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

3. 输入、输出、约束与示例#

输入:整数数组 nums,整数 k。
输出:一个整数。

约束:

  • 1 <= k <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

示例 1、示例 2(原文):

输入: [3,2,1,5,6,4], k = 2
输出: 5
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4

4. 主要考察的数据结构或算法模式#

  1. 优先队列(堆):维护大小为 k 的小根堆,是”Top-K”问题的标准解法。
  2. 快速选择(Quickselect):快速排序的分区思想 + 只递归一侧,平均线性时间。
  3. 模式识别:当答案只依赖于”第 k 名”而不需要完整有序序列时,不必付出全排序的代价。

5. 渐进式提示(不直接泄露答案)#

提示 1:如果先把整个数组排序再取第 k 个,代价是 O(n log n)。但我们要的只是一个位置的值,并不需要完整的顺序。能不能把代价降下来?

提示 2:设想只维护”目前见到的最大的 k 个数”这一个集合。当新来的数比集合里最小的那个还小时,它永远不可能是答案,可以直接丢弃。什么样的数据结构支持”快速拿到集合最小值”与”替换最小值”两个操作?

提示 3:另一种思路来自快速排序的 partition:一次分区后能确定某个元素的最终位置 p。如果 p 恰好是我们要的下标,任务就结束了;否则只需要进入包含目标的那一侧继续,另一侧可以直接丢弃。这如何让平均复杂度变成线性?

6. 直观方案及其局限#

方案 A:直接排序

return sorted(nums)[len(nums) - k]
  • 复杂度:O(n log n) 时间,O(n) 或 O(log n) 额外空间(取决于实现)。
  • 局限:不满足题目要求的 O(n);做了大量与答案无关的比较。在 n = 10^5 的规模下通常仍可通过,但它无法用来回答面试官真正想听的问题——“你能不能只做必要的那部分工作”。

方案 B:全部入大根堆,弹出 k 次

  • 复杂度:O(n + k log n) 时间,O(n) 空间。
  • 局限:当 k 接近 n/2 时接近 O(n log n);而且需要先把全部数据放进堆里。

方案 C:定长小根堆(推荐解之一)

维护恰好 k 个元素的小根堆,堆顶就是这 k 个里最小的那个,也就是全数组的第 k 大。

  • 复杂度:O(n log k) 时间,O(k) 空间。
  • 局限:严格来说不是 O(n)(当 k 与 n 同阶时退化为 O(n log n)),但实现极简、稳定、无最坏退化,是工程首选。

方案 D:随机化快速选择(满足 O(n) 要求的解)

  • 平均 O(n) 时间、O(1) 额外空间(原地分区)。
  • 代价:最坏情况 O(n^2)(可通过随机化让它在实践中几乎不发生);实现更容易出错,尤其是有重复元素时。

7. 推荐方案的推导过程#

推导链:

  1. 目标是”第 k 大”→ 转换为升序数组中的下标 target = n - k。这是一个关键的坐标转换,很多人在这里差一也可以错。
  2. 全排序做了多余的功:它把每一对元素的相对顺序都确定了,而我们只需要一个位置。减掉多余的功就是本题的灵魂。
  3. 沿两条路走:
    • 保留”够用的信息”:只需要截至目前最大的 k 个数,其余都可以丢弃。于是维护一个大小为 k 的小根堆——堆顶是”守门员”,任何比它还小的元素永远进不了答案。
    • 逐步缩小搜索区间:一次 partition 可以确定基准值的最终位置。比较 target 与划分后的区间边界,就能确定答案在左还是在右,另一侧整段丢弃。这是典型的分治减治(decrease-and-conquer)。
  4. 处理重复元素:朴素二路分区在大量重复元素时会退化。把分区改成三路划分(小于 / 等于 / 大于基准),重复元素一次成批定位,target 落在”等于区间”时立即返回,等于顺便解决了重复元素语义。
  5. 随机化选取基准,避免输入本身有序时退化到 O(n^2)。

8. 正确性说明(不变量法)#

以三路划分的快速选择为例,每轮循环维持如下不变量:

对当前区间 [left, right],答案(target 对应的值)一定落在这个区间内。

分区结束后的位置关系为:

[left, lt) < pivot
[lt, gt] == pivot
(gt, right] > pivot

由于数组升序排列后,三段在整体有序序列中的相对位置是确定的(lt 之前的都小于 pivot,gt 之后的都大于 pivot),因此:

  • 若 target < lt:第 target 名一定落在小于 pivot 的那一段,令 right = lt - 1,不变量保持。
  • 若 target > gt:第 target 名落在大于 pivot 的那一段,令 left = gt + 1,不变量保持。
  • 若 lt <= target <= gt:第 target 名就落在等于 pivot 的区间里,答案就是 pivot,返回 nums[lt]。

终止性:每轮迭代区间长度严格减小(因为至少 pivot 所在的等值区间被排除,或目标段严格变短),因此必然在有限步终止。初始时答案为全数组的第 target 名,不变量成立;终止时返回值即正确答案。

关于”排序后第 k 大而不是第 k 个不同”:三路划分保留了重复元素(等值区间整体参与),因此返回值自然满足”重复计数”语义,这一点恰恰是三路划分优于朴素实现的第二个理由。

9. 时间复杂度与空间复杂度#

快速选择(随机化 + 三路划分)

  • 时间:平均 O(n)(每一层期望处理量按常数比例递减:n + n/2 + n/4 + ...);最坏 O(n^2),随机化后在工程上几乎不可触发。
  • 空间:O(1) 额外空间(原地交换),递归改为循环后连栈开销也没有;Python 版本注意避免列表切片产生额外内存。

定长小根堆

  • 时间:O(n log k);当 k 很小时接近线性,当 k 与 n 同阶时接近 O(n log n)。
  • 空间:O(k)。

工程取舍:追求严格 O(n) 且数据量极大、内存敏感 → 快速选择;追求实现稳健、最坏情况可控、代码简短难错 → 小根堆。面试中两种都要能写出来,并说清为什么选哪一个。

10. Python 参考代码(可直接运行)#

import random
from typing import List
class Solution:
"""推荐解:随机化快速选择 + 三路划分,平均 O(n) 时间、O(1) 额外空间。"""
def findKthLargest(self, nums: List[int], k: int) -> int:
target = len(nums) - k # 第 k 大在升序数组中的下标
left, right = 0, len(nums) - 1
rng = random.Random(0x5EED) # 固定种子便于复现调试
while left <= right:
pivot = nums[rng.randint(left, right)]
lt, i, gt = left, left, right
# 荷兰国旗三路划分
while i <= gt:
if nums[i] < pivot:
nums[lt], nums[i] = nums[i], nums[lt]
lt += 1
i += 1
elif nums[i] > pivot:
nums[i], nums[gt] = nums[gt], nums[i]
gt -= 1
else:
i += 1
# [left, lt) < pivot, [lt, gt] == pivot, (gt, right] > pivot
if target < lt:
right = lt - 1
elif target > gt:
left = gt + 1
else:
return pivot
return nums[left]
import heapq
class SolutionHeap:
"""备选解:大小为 k 的小根堆,O(n log k) 时间、O(k) 空间,最坏情况可控。"""
def findKthLargest(self, nums: List[int], k: int) -> int:
heap = []
for x in nums:
if len(heap) < k:
heapq.heappush(heap, x)
elif x > heap[0]:
heapq.heapreplace(heap, x)
return heap[0]

11. 关键边界情况#

  1. k = 1:第 1 大即最大值。target = n - 1,三路划分会一直向”大于”区间收缩,正确。
  2. k = n:第 n 大即最小值。target = 0,与 k = 1 对称。
  3. 大量重复元素:朴素二路分区会退化;三路划分让等值区间一次定位,target 落在等值区间时立即返回。这是本题最容易被面试官加码的地方。
  4. 负数:与正数逻辑一致,注意初值不要设 0 之类的哨兵。
  5. 已排序输入:随机化选基准前,若固定取 nums[left] 作为 pivot,升序输入会退化到 O(n^2)。随机化是必须的。
  6. 原地修改:本解会打乱输入顺序。若调用方要求不修改原数组,需先拷贝(空间换约定)。

12. 测试用例(5 个)#

def check():
cases = [
([3, 2, 1, 5, 6, 4], 2, 5), # 官方示例 1
([3, 2, 3, 1, 2, 4, 5, 5, 6], 4, 4), # 官方示例 2(含重复)
([5], 1, 5), # 单元素
([2, 2, 2, 2], 2, 2), # 全部相同
([-1, -2, -3, -4], 1, -1), # 全负数 + k=1
([5, 4, 3, 2, 1], 5, 1), # 逆序输入 + k=n
]
for nums, k, expected in cases:
arr1, arr2 = list(nums), list(nums)
got1 = Solution().findKthLargest(arr1, k)
got2 = SolutionHeap().findKthLargest(arr2, k)
assert got1 == expected, (nums, k, got1, expected)
assert got2 == expected, (nums, k, got2, expected)
print("all passed")
check()

13. 面试官可能提出的变体或追问#

变体:如果是数据流——元素逐个到来且需要随时回答”当前第 k 大”,怎么办?

要点:小根堆方案天然支持流式处理(每个新元素到来时做一次 push/heapreplace),答案是堆顶;而快速选择需要看到全部数据,是批处理算法。这一问直接把两种解法拉到不同的适用场景上,也是本 Heap 解的立身之本。追问常延伸为 LeetCode 703(数据流中的第 K 大元素)。

其他可能追问:

  • “如果 k 很大接近 n 呢?” → 堆解退化为 O(n log n),可反过来维护大小为 n-k+1 的大根堆,或直接快速选择。
  • “如何做到稳定 Insurance 的最坏 O(n)?” → 可提及中位数的中位数(BFPRT)选主元,理论上严格线性,但常数很大,工程上很少使用。

14. 可迁移的解题能力#

  1. “只要一个答案就别做全套工作”:把 O(n log n) 的全序转换成 O(n) 的选择问题,是 top-k、分位数、中位数类问题的通用思路。
  2. 减治(decrease-and-conquer)模式:partition 后只递归一侧,与二分查找共享同样的思维骨架——每步都丢弃一块确定无关的区域。
  3. 数据结构选择要看操作集合:频繁的”取最小值 + 替换最小值”正是堆的用武之地,因为它不需要随机访问。这与 LRU 缓存(LeetCode 146)里”操作集合决定数据结构”是同一套判断方法。
  4. 随机化对抗结构化输入:有序或近乎有序的输入会让确定性的 pivot 策略退化,随机化是低成本的保险。
  5. 重复元素的处理意识:朴素分区在重复值上的退化非常隐蔽,三路划分是通用的加固手段。

15. 与 AI Agent 工程的关系(自然联系)#

  • 重排 / Top-K 选择:RAG 中候选若干文档只需要取 top-k,不需要对全部候选做全序排序;同样地,日志/Trace 里只需要”最慢的 10 次调用”,用小根堆流式维护即可。这正是堆解的 migrate 场景。
  • 延迟预算下的”够用即可”:与 S01 的路由同理——宁可用”够用的最小模型/最小候选集”完成任务,也不要为不存在的需求付出全量代价。

四、延伸资料#

按优先级排序,均标注核验状态;未核验的条目不作事实引用。

官方文档与 spec(优先级 1)

原始论文(优先级 2)

  • Hu 等,《LoRA: Low-Rank Adaptation of Large Language Models》,arXiv:2106.09685(2021-06-17 提交,v2 于 2021-10-16 修订)—— 已核验题目、作者与提交记录(本轮访问 arXiv 摘要页)。
  • Cormack、Clarke、Buettcher,《Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods》,SIGIR 2009 —— 本轮未核验原文,按公开引用的通用描述陈述,仅在 B03 作为 RRF 出处说明。

官方工程博客(优先级 3)

  • Marc Brooker,《Exponential Backoff And Jitter》,AWS Architecture Blog(2015-03-04 发布,2023-05 更新)—— 已核验标题、作者与日期;文中同时给出 Amazon Builders’ Library 的 “Timeouts, retries and backoff with jitter”。该文是 B01 中”退避必须带抖动”的直接依据,并明确指出无抖动的退避仍会产生调用簇。

软件开发基础(优先级 3-4)

  • Python 官方文档 asyncio 协程与任务章节 https://docs.python.org/3/library/asyncio-task.html —— 本轮抓取为空,未能核验正文内容;请以本机 Python 3.13 对应版本文档为准(TaskGroup、wait_for、to_thread 等 API 的行为描述以本地实测为准)。
  • Redis 官方文档关于缓存过期与内存回收的章节 —— 本轮未核验具体页面,S02 的答案不依赖任何特定版本的 Redis 行为描述。

本轮未涉及前沿事实判断题:今日 7 题的参考答案不依赖任何 2026 年的框架版本、模型榜单或行业趋势,因此不存在时效性断言风险。


五、当日重点与复习建议#

今日重点(按优先级)#

  1. 幂等是跨层的系统属性,不是某段 if 判断(B01)。记住三件事:键用语义指纹而非传输 id、状态三态化、命中 done 时必须回放首次响应。这是今天与 Agent 工具调用最直接相关的落地能力。
  2. 协程的性能问题几乎都是”有人在占着事件循环”(B02)。能说出 let-out 点模型、三类阻塞的隔离手段、以及”本地取消必须向下游传播”,这三项是区分度最高的答案。
  3. RAG 归因的顺序不可逆:召回 → 排序 → 上下文 → 生成(B03)。“召回不足时上重排”是最贵的方向性错误。
  4. 行为问题找微调,知识问题找 RAG,成本问题找量化与路由(B04)。能把这句话连同各自的失效边界一起讲清楚,基本就是中高级的判定线。
  5. S01 与 S02 的共同骨架:都是”在不确定的依赖上,用确定性机制守住边界”——前者用路由矩阵与降级分桶,后者用过期模型、重建互斥与失效链路。

易错点自查#

易错点正确表述
“exactly-once 可以端到端实现”真实系统是 at-least-once + 消费端幂等
“退避加乘 2 就够了”必须加抖动,否则重试仍会同步成簇
“async 就是并行”协程是协作式并发;CPU 密集要走进程池
“wait_for 能中断 CPU 计算”取消只在 await 点生效
“RRF 是最优融合”是鲁棒启发式,牺牲了分数间距信息
“重排能救召回”重排是排序变换,不能补充召回
“用微调注入领域知识”知识不该进权重,应进检索;权重负责行为
“量化能降 token 成本”量化改善单次服务经济性,不改变 token 总量
“429 也一律重试”遵守 Retry-After,并计入重试预算
“缓存 TTL 统一配置即可”TTL 必须逐 key 抖动,否则失效变成同步事件
“回源互斥要用分布式锁”优先进程内 singleflight,避免把回源风暴换成抢锁风暴

复习建议#

  • 明日 10 分钟快速自查:能否在不看笔记的情况下,画出今天两个场景题的降级矩阵(S01 四级 / S02 三层保护),并说出每一级的触发条件与恢复条件。
  • 3 天后(2026-10-16):重做 S02,把”陈旧窗口 - 吞吐量 - 数据库 QPS”三者的关系画成一张曲线图,这是最容易在面试后续追问里露怯的点。
  • 7 天后(2026-10-20):把 B03 的四层归因表套到自己的项目上,逐层给出实际指标——如果能给出真实数字,这次面试就有很大的加分空间。
  • 精选题目:Q-20261012-B02(Structured Output 与 Tool Calling 的边界)建议复习日 2026-10-19,复习角度从”定义边界”转为”故障定位”(线上失败率从 0.3% 涨到 4% 时的分层归因与处置)。本轮未执行精选,也未修改精选索引。

下一次训练的轮换建议(供去重参考)#

  • 尚未覆盖的一级类型:操作系统与网络、软件工程与平台工程(作为基础题);上下文工程与能力封装、工具与互操作协议、AI Agent 核心、Agent 评测与可观测性、AI 安全与治理、LLM 应用工程在最近 30 天内已有覆盖,短期内应更换二级知识点或转入 31-90 天弱去重窗口。
  • 场景题可用类型:上下文超限与压缩、Memory 写入召回与遗忘、Agent 评测体系设计、线上故障定位与可观测性、多租户隔离与治理、Tool Calling 失败与越权治理、跨组织多 Agent 协作的信任边界(今日已用:路由降级 / 缓存雪崩)。
  • LeetCode 已用题号:146(哈希表+双向链表)、3(哈希表+滑动窗口)、215(堆 + 快速选择)。下次建议轮换到栈与队列、二叉树、二分查找或基础动态规划,避免再次使用哈希表主导的模式。

六、题目分类与标签#

编号题目 ID一级类型二级主题难度LeetCode ID是否复习题是否精选标签
B01Q-20261013-B01分布式系统幂等、重试、退避与超时预算基础题(中高)否否幂等、重试、退避抖动、超时预算、重试风暴
B02Q-20261013-B02编程语言与运行时异步并发模型与结构化并发基础题(中高)否否asyncio、事件循环、协程取消、结构化并发、并发限流
B03Q-20261013-B03RAG 与搜索混合检索、融合策略与重排归因基础题(中高)否是(2026-10-13 精选)混合检索、BM25、RRF、重排、分层归因
B04Q-20261013-B04AI 与大模型基础微调、LoRA、量化蒸馏与取舍基础题(中高)否否微调、LoRA、量化、蒸馏、灾难性遗忘、成本模型
S01Q-20261013-S01LLM 应用工程 / AI Agent 核心模型路由、降级、成本与延迟优化场景题(高)否否路由分级、降级矩阵、缓存、成本治理、灰度
S02Q-20261013-S02数据库与存储 / 分布式系统缓存雪崩、热点 key 与陈旧容忍场景题(高)否否缓存雪崩、singleflight、热点分片、逻辑过期、一致性
L01Q-20261013-L01堆与优先队列 / 分治Top-K 维护与随机化快速选择medium215否否堆、快速选择、三路划分、Top-K

七、本次生成元信息#

配置读取状态:成功。实际使用 题库配置.md 版本 version: 4(last_reviewed: 2026-10-12),未做任何修改。暂停类型为空;「观察中的新技术」未标记”允许试学”,本次未从中出题。

题型结构变更执行说明:配置 v4 的每日配额为「非 Agent 基础 2 + Agent 基础 2 + Agent 场景 1 + 非 Agent 场景 1 + LeetCode 1」共 7 题,与自动化提示词中的旧 4 题结构不一致。按任务规则「配置新增或调整的类型从下一次训练开始生效」与「不使用提示词中的旧分类覆盖配置文件」,本次起执行 7 题结构。

本次参考过的历史题库文件

文件用途
better/题库配置.md题型配额、启用类型、暂停类型、观察项(只读)
better/题目索引.md12 条历史题目的去重比对基准
better/精选/README.md精选题目与复习节奏(本次未精选、未修改)
better/README.md目录索引表的追加约定
better/2026-10/2026-10-10-AI-Agent-面试训练.md30 天窗口强去重比对(8 题)
better/2026-10/2026-10-12-AI-Agent-面试训练.md30 天窗口强去重比对(4 题)

去重执行结果

  • 强去重窗口:2026-09-13 至 2026-10-13(实际仅 2 份训练文件、12 题)。
  • 比对维度:题目标题、一级类型、二级主题、核心考点、预期回答结论、场景约束与故障现象、LeetCode 题号、语义相似度。
  • 结果:无重复题、无换皮题、无考点高度重合。一级类型首次覆盖 4 个(分布式系统、编程语言与运行时、RAG 与搜索、AI 与大模型基础);LeetCode 215 首次出现,算法模式与已用的哈希表+双向链表(146)、哈希表+滑动窗口(3)均不同。
  • 复习题:无。31-90 天窗口内无历史题目;超过 90 天的历史不存在。

周期复盘状态:累计训练次数 3 次(2026-10-10、2026-10-12、2026-10-13),未达到 7 次阈值,本次未触发周期复盘。若按每日一次推算,预计 2026-10-16 前后触发首次周复盘。

精选处理:本次未收到”精选第 X 题 / 加入精选 / 收藏”等明确意图,未执行精选,未修改 精选/README.md 与其索引。

作答与状态:本文件状态为 generated。不存在任何作答记录,因此不声称已练习、已掌握或已答错;题目索引.md 中今日 7 题的状态保持 generated。

远端同步状态:本文件已成功写入本地 Obsidian 目录。未做远端同步验证,不声称已同步。

2026-10-13 AI Agent 面试训练
https://vissssa.github.io/posts/ai-agent-interview-2026-10-13/
作者
Siegward
发布于
2026-10-13
许可协议
CC BY-NC-SA 4.0