18713 字
94 分钟
2026-10-19 · AI Agent 面试训练

2026-10-19 · AI Agent 面试训练#

今日学习主题#

陈旧状态与容量边界:缓存层级里的”旧视图”如何变成线上事故。

今天五题考察同一件事的不同层——状态已经变了,但某一层仍在按旧视图工作:

  • B01 · 解析层:DNS 的 TTL 与负缓存,为什么”TTL 60 秒”不能保证 60 秒内切干净流量。
  • B02 · 模型层:上下文窗口的标称值不是可用预算,KV Cache 如何重塑成本结构。
  • S01 · 知识层:RAG 索引、语义缓存与引用锚点在文档换版后的失效与重建。
  • S02 · 基础设施层:Pod 端点摘除与进程终止的时序错位,如何变成发布窗口里的 502。
  • L01 · 算法层:依赖图里的环,让”看起来每一步都合理”的计划整体不可完成。

贯穿的方法论有两条:第一,先定位”旧视图”停在哪一层,再决定失效该由谁触发;第二,不要把任何一个上界(TTL、窗口长度、超时、grace period)误当成保证。

题目目录#

编号题目名称题型一级类型难度一句话考察点
B01DNS TTL 到期后流量为什么没切走非 Agent 基础操作系统与网络基础题(中高)TTL 只约束解析结果的缓存,约束不了已建连接与进程内地址缓存;负缓存由 SOA 承载
B02标称 128k 上下文,我能用满吗Agent 基础AI 与大模型基础基础题(中高)窗口是上限不是预算;prefill/decode 瓶颈不同;长上下文存在位置偏差
S01制度更新两周后 Agent 仍在引用废止条款Agent 场景RAG 与搜索 / AI Agent 核心场景题(高)索引是派生视图,版本与缓存失效键必须绑定知识版本;引用锚点要有失效语义
S02每次发布必有 30 秒 502非 Agent 场景操作系统与网络 / 软件工程与平台工程场景题(高)端点摘除与进程终止是两条异步链路;readiness、preStop 与连接排空各管一段
L01207. 课程表LeetCode图的基础遍历 / 拓扑排序medium有向图环检测:入度剥离与剩余节点判定

B01 · 非 AI Agent 基础题#

题目 ID:Q-20261019-B01 | 一级类型:操作系统与网络 | 二级主题:DNS 解析、TTL、负缓存与解析结果的缓存层级 | 难度:基础题(中高) | 题型:非 Agent 基础

题目#

公司把一个内部服务从旧集群迁到新集群。迁移方案是:提前一天把 DNS 记录的 TTL 从 3600 秒降到 60 秒,切换时把 A 记录指向新负载均衡的 IP。

切换后第 5 分钟监控一切正常,但 40 分钟后仍有约 8% 的请求打到已经下线的旧实例,报 connection refused。

同期上线的另一个新域名 report.internal,在部分机器上持续解析失败近 2 小时,尽管 DNS 记录早已创建,且用 dig 从另外几台机器验证结果是正确的。

请回答三个问题:

  1. DNS 记录里的 TTL 到底约束了什么,又约束不了什么?为什么 60 秒的 TTL 没有让流量在 60 秒内切干净?
  2. report.internal 的解析失败最可能是什么原因?它和普通记录的缓存有什么不同?
  3. 如果要把服务切换做成可预期、可回滚的工程动作,除了改 TTL 还需要做什么?

先思考#

  1. TTL 挂在谁身上?它是”解析答案”的属性,还是”已建立连接”的属性?一条已经握过手的 TCP 连接,之后还依赖 DNS 吗?
  2. 从应用进程一路到权威服务器,一次名字解析的结果会被几层东西记住?这些层是否共享同一个到期时间?
  3. “解析失败”本身算不算一种可以被缓存的答案?如果解析器把”这个名字不存在”也缓存起来,它的 TTL 从哪里来?
  4. 要让流量可控地离开旧实例,你需要分别对”新流量”和”存量连接”做什么?

参考答案#

结论:TTL 只是权威服务器发布的”解析结果最长可缓存时间”,它是一份上限而非保证,而且只作用于”发起新解析”这一条路径。已经建立的 TCP 连接、应用进程内持有的地址副本、以及被缓存的否定答案,都不受它的约束。

定义。 DNS 的 TTL(Time To Live)写在资源记录里,由权威服务器发布,意思是”递归解析器可以把这条答案缓存多久”。RFC 2308 明确把它表述为解析器**可以(may)缓存该答案的最长时限,并规定该 TTL 在缓存/转发时逐级递减,归零后缓存的条目不得(MUST NOT)**再被使用。

适用边界。 TTL 的效力边界有三个层次:

  • 它约束解析结果,不约束连接。 名字→IP 的映射缓存到期,只会影响”下一次解析”;已经建好的连接不知道也不关心 DNS 发生了什么。
  • 它是上限,不是指令。 权威侧给的是”最多能缓存多久”,解析器可以在本地施加更短的上限。反过来,应用侧也可以比 DNS TTL 缓存得更久——很多语言运行时的地址缓存默认缓存成功的解析结果,且不保证遵循记录里的 TTL(具体默认行为随语言版本与启动参数而异,本轮未核验各运行时的具体默认值)。
  • 各级缓存各自计时。 一次解析的结果可能同时存在于应用进程内缓存、操作系统/存根解析器、本地递归解析器,每一层独立计时。只要有一层持有旧值,“TTL 到期”就不会带来全局切换。

简洁示例。 一个服务用连接池维持 500 条到 svc-a.internal 的长连接,连接的最大生命周期设成了 30 分钟。DNS TTL 降到 60 秒并切换 A 记录后:60 秒内新发起的解析会拿到新 IP;但那 500 条长连接仍指向旧 IP,最长要 30 分钟后才重建。旧实例在第 10 分钟被下线,于是这 500 条连接对应的请求在之后 20 分钟里持续 connection refused——这就是”40 分钟后仍有 8%“的来源。

与相近概念的区别。

  • TTL 与负缓存 TTL:载体不同。 普通答案的 TTL 由回答区里的记录自己携带;而否定答案(NXDOMAIN / NODATA)的回答区是空的,没有记录来承载 TTL,因此改由权威区的 SOA 记录承载,取值为 min(SOA.MINIMUM 字段, SOA 记录自身的 TTL)。这也是为什么 RFC 2308 要求权威服务器在报告 NXDOMAIN 或 NODATA 时必须在权威区带上 SOA——否则这条否定答案无法被缓存,也无从判断缓存多久。
  • NXDOMAIN 与 NODATA:缓存键不同。 前者表示”这个名字不存在”,按 <QNAME, QCLASS> 缓存;后者是伪 RCODE,表示”名字存在但没有这个类型的记录”,按 <QNAME, QTYPE, QCLASS> 缓存。
  • DNS TTL 与 HTTP Cache-Control:层级不同。 前者约束”名字解析”这一层,后者约束”内容”这一层,两者都是陈旧容忍设计,但互不影响,不能互相替代。
  • DNS 与健康检查:职责不同。 DNS 不感知实例存活。一个已经死掉的 IP 只要还在记录里,就会被照常返回。把 DNS 当作故障摘除机制是常见错位。

第二个域名的根因。 report.internal 的持续解析失败,最典型的原因是负缓存:服务先部署、先发起解析,而 DNS 记录尚未创建;解析器收到 NXDOMAIN 后把它缓存下来,缓存时长由权威区的 SOA 决定(RFC 2308 推荐解析器本地上限取 1–3 小时,并明确指出超过一天会有问题)。此后即使记录创建正确,这些机器在该 TTL 到期前仍会直接返回”不存在”。这也解释了为什么”另外几台机器 dig 是对的”——它们的缓存里没有那条负缓存。

典型应用(通用工程)。

  • 平滑迁移:提前降 TTL(等待旧 TTL 完全过期)→ 切换记录 → 旧实例延迟下线,等其连接数归零再回收。
  • 部署顺序:先创建并验证 DNS 记录,再让服务依赖它;顺序颠倒会踩负缓存。
  • 客户端侧:给连接池设置最大连接生命周期,强制周期性重建连接,从而周期性重新解析。这是让应用侧”跟随 DNS 变化”的主要工程手段。
  • 验证:切换后用 dig 观察 TTL 递减与权威/非权威标志,而不是只看一次查询结果。

面试现场回答(约 1.5 分钟)。

TTL 是权威服务器给出的”这条解析答案最多能缓存多久”,它是一份上限,不是到期即失效的保证,而且只作用于发起新解析的路径。所以 60 秒 TTL 没切干净,根因在两处:一是已经建立的 TCP 连接和连接池完全不看 DNS,只要连接还活着就继续打旧实例;二是应用进程内地址缓存、OS 缓存、递归解析器各自计时,任一层持有旧值都会让切换不彻底。修法是三件事一起做——提前降 TTL 并等旧 TTL 过期、给连接池设最大连接寿命强制重建、旧实例延迟下线直到连接数归零。

第二个域名是负缓存。服务先部署先解析,记录还没建,解析器收到 NXDOMAIN 就把它缓存起来了。否定答案回答区是空的,TTL 由权威区的 SOA 记录承载,取 min(SOA.MINIMUM, SOA 自身 TTL),RFC 2308 建议解析器本地上限 1 到 3 小时。所以记录后来建对了,那些机器在 TTL 到期前还是解析失败。规避方式是部署顺序反过来:先建记录并验证,再让服务依赖它。

关键点#

  1. TTL 是”最长可缓存时间”,不是”到期即失效”的保证,更不是”全局同步生效”的保证。 它只在发起新解析的路径上生效。
  2. 已建立的连接不参与 DNS。 连接池的最大连接生命周期才是决定”多久重新解析一次”的真实参数。
  3. 否定答案也会被缓存。 负缓存的 TTL 由权威区的 SOA 承载,取 min(SOA.MINIMUM, SOA 自身 TTL);NXDOMAIN 与 NODATA 的缓存键粒度不同。
  4. 缓存是分层的,且各层独立计时。 定位问题时必须逐层确认,而不是只看权威记录改没改。
  5. DNS 不是健康检查和故障摘除机制。 它返回的是名字到地址的映射,不感知实例死活。

常见误区#

  1. “TTL 设成 60 秒,60 秒后流量一定全部切走。” 错误。TTL 只约束解析结果的缓存,既约束不了已建立的连接,也约束不了应用进程内的地址副本。真正的切换完成时间由最慢的那一层决定,通常是连接池的重建周期。
  2. “解析失败不会被缓存,只是当时网络不通。” 错误。RFC 2308 已经明确负缓存不再是可选项——只要解析器做缓存,就必须同时缓存否定答案。先部署后建记录是一个会真实造成数小时故障的顺序错误。
  3. “调小 TTL 是免费的。” 错误。TTL 越小,递归解析器的查询量越大,权威服务器压力与解析延迟上升;对解析链路容量有限的场景,把 TTL 压到秒级可能制造新的瓶颈。TTL 需要在”切换速度”和”解析负载”之间取值,而不是越小越好。

延伸学习#

  • RFC 2308《Negative Caching of DNS Queries (DNS NCACHE)》(1998-03,Standards Track):负缓存的规范依据,含 SOA 承载负 TTL、NXDOMAIN/NODATA 的缓存键、权威服务器必须携带 SOA、以及 1–3 小时本地上限建议。本轮已核验全文。链接:https://datatracker.ietf.org/doc/html/rfc2308
  • 实践方向(自行验证,非链接):任选一台机器对某个域名 dig 两次,观察 TTL 递减与 flags 中 aa(权威应答)标志的变化;再对一个不存在的名字 dig 两次,观察 SOA 与其 TTL。用这两组结果把”正向缓存”与”负缓存”的载体差异落实到可观测的行为上。

B02 · AI Agent 基础题#

题目 ID:Q-20261019-B02 | 一级类型:AI 与大模型基础 | 二级主题:Attention、KV Cache 与上下文窗口的容量与成本边界 | 难度:基础题(中高) | 题型:Agent 基础

题目#

某团队把 Agent 的底座模型换成标称 128k 上下文窗口的版本,希望通过”把所有检索结果都塞进上下文”来省掉 RAG 里的重排环节。上线后发现三个现象:

  1. 单次问答的成本涨了数倍,首 token 延迟显著变长;
  2. 同样的问题,正确答案在材料中部时准确率明显低于在开头或结尾;
  3. 即使问题和昨天几乎一样,成本也没有明显下降。

请回答:

  1. 上下文窗口的标称值意味着什么?实际可用容量被哪些东西挤占?
  2. prefill 与 decode 两阶段的成本结构有什么不同?KV Cache 在其中扮演什么角色?
  3. 为什么”能放下”不等于”能被有效利用”?在 Agent 系统里,应如何据此决定”往上下文里放多少东西”?

先思考#

  1. 模型宣称的 128k,包含它自己要生成的那部分输出吗?如果一条请求要输出 4k,你能喂进去多少输入?
  2. 输入被读进来(理解上下文)和输出被一个 token 一个 token 吐出来,这两件事的瓶颈是同一种资源吗?
  3. 生成到第 n 个 token 时,前 n-1 个 token 的计算结果还需要重算一遍吗?如果不重算,代价是什么?
  4. 你已经知道检索环节可以过滤和重排。如果上下文足够大,“少放一点”还重要吗?请从成本和质量两个角度分别想。

参考答案#

结论:上下文窗口是长度上限,不是可用预算。标称值同时被系统提示、工具定义、对话历史、检索材料和输出预留共同挤占;标准全局注意力关于序列长度的计算代价是二次的,KV Cache 用显存换取解码阶段的重复计算;而且长上下文存在位置相关的质量退化——“能放下”与”能被稳定利用”是两件事。因此 Agent 的上下文策略应当是”按预算治理”,而不是”按上限塞满”。

定义。

  • 上下文窗口:单次请求中模型可处理的输入 token 与输出 token 之和的上限。它是位置/长度维度的硬约束,超出会被拒绝或截断。
  • Attention:解码器在生成每个位置时,对序列中已存在位置计算相关性并加权聚合。在标准全局注意力下,计算量随序列长度 n 呈 O(n²) 增长。
  • KV Cache:自回归生成时缓存已计算位置的 Key/Value,避免每生成一个 token 就把整个前缀重算一遍。它把解码每步的代价从”重算前缀”降为”增量一步”,代价是显存占用随序列长度线性增长。

适用边界。

  • 窗口是上限,不是预算。 可用输入 = 窗口 − 输出预留 − 系统提示 − 工具 schema − 历史 − 检索材料。接近上限时还可能触发截断,而截断往往发生在最不希望发生的位置(通常是历史或材料的尾部)。
  • “能放下” ≠ “能稳定用好”。 标称窗口是工程上可寻址的长度,不等于在该长度下质量与标称短上下文一致。
  • 这些性质是 Transformer 解码器在全局注意力下的性质。 滑动窗口注意力、稀疏注意力、线性注意力等变体会改变代价曲线的形状,本结论以主流全局注意力为准;选型时应按所用模型的实际机制判断。

简洁示例。 一次请求把 100k token 的检索材料一次性塞进输入:

  • prefill:需要一次性完成这 100k 位置的注意力计算,其成本远高于 10k 输入,且大致按二次关系放大;这是首 token 延迟的主要来源。
  • decode:生成过程中 KV Cache 需要常驻对应显存,长度越长占用越大;每步只需增量计算,但每步都要把 KV 从显存读出来,属于带宽受限。
  • 前缀缓存:如果这个 100k 前缀与上一次请求完全一致且命中前缀缓存,prefill 成本可以被大幅摊薄;一旦前缀有变化(比如把用户问题放在最前面),命中率归零,每次都要重付全量 prefill。这解释了现象 3。

与相近概念的区别。

  • 上下文窗口 vs 外部记忆 / RAG:窗口是”每轮都要重新付费的注意力预算”,外部存储是”按需取用的持久化状态”。把窗口调大不能替代检索,因为成本随长度恶化,而外部检索是按需取用。
  • KV Cache vs 前缀缓存(prompt caching):KV Cache 是单次请求内的解码复用机制,几乎总是开启;前缀缓存是跨请求复用已算好的前缀 KV,命中与否决定 prefill 是否被摊薄。两者同名不同层,混用会造成成本误判。
  • prefill vs decode:前者吞吐型、算力受限、可并行;后者延迟型、显存带宽受限、本质串行。优化手段不同(前者靠批量与分块,后者靠量化、投机解码等),不要用一个阶段的指标去推断另一个。
  • 上下文窗口 vs 输出长度上限:二者共用同一个预算,max_output_tokens 必须显式预留,否则会出现”输入塞满、输出被截断”。

在 Agent 系统中的应用。

  • 按预算而非按上限治理上下文。 给系统提示、工具 schema、历史、检索材料、输出分别设额度,留出安全余量。压缩/摘要/子 Agent 隔离的触发阈值应远低于窗口上限。
  • 检索与过滤不能因窗口变大而取消。 大量候选进入上下文既抬高成本,也会稀释注意力;重排的作用是把”可能被用到的”放在前面,这一点不因窗口变大而消失。
  • 前缀布局要稳定。 把稳定的系统提示与工具定义放在前面、把每次变化的内容放在后面,是提升前缀缓存命中率的关键;反过来布局会让命中率归零。
  • 位置敏感内容要主动摆放。 关键约束与当前目标放在稳定的前部,需要模型重点使用的材料放在尾部,比”全部堆在中间”更符合模型的实际利用特性。

面试现场回答(约 2 分钟)。

上下文窗口是输入加输出的总长度上限,不是可用预算。实际能被检索材料占用的,要扣掉系统提示、工具定义、对话历史,还要给输出预留。而且标准全局注意力的计算量随长度是二次增长的,KV Cache 换来的只是解码阶段不用重算前缀,代价是显存随长度线性增长。

所以要分开看两个阶段:prefill 一次性处理全部输入,是算力瓶颈,决定首 token 延迟;decode 逐 token 生成,是显存带宽瓶颈,决定吐字速度。成本涨几倍主要是 prefill 那一段放大的。第三个现象是前缀缓存没命中——如果把用户问题放在系统提示前面,前缀每次都变,缓存命中率就是零,成本完全摊不掉。正确做法是把稳定内容放前面、变化内容放后面。

第二个现象是位置偏差。已有研究在长上下文上发现,相关信息放在开头或结尾时性能最好,放在中部会明显下降,即使对专门的长上下文模型也是如此。所以”能放下”不等于”能被用到”,结论是:不能靠加大窗口取消重排和过滤,反而应该把进入上下文的候选数量压下来,让留下的每一条都更可能被真正用到。

关键点#

  1. 窗口是上限,预算才是设计对象。 输入可用量 = 窗口 − 输出预留 − 系统提示 − 工具定义 − 历史 − 检索材料。
  2. prefill 与 decode 是两种瓶颈。 prefill 算力受限、决定首 token 延迟;decode 带宽受限、决定吐字速度;优化与观测指标都要分开。
  3. KV Cache 是”显存换计算”。 它消除解码阶段的重复计算,但显存随长度线性增长,长上下文的直接后果是并发能力下降。
  4. 长上下文存在位置偏差。 相关信息位于中部时性能显著下降,这一结论在长上下文专用模型上同样成立。
  5. 前缀缓存的命中率由布局决定。 稳定内容在前、变化内容在后,是跨请求摊薄 prefill 成本的前提。

常见误区#

  1. “窗口标 128k,我就能用 128k。” 错误。窗口包含输出,且系统提示、工具 schema、历史都要占位;不留输出余量的直接后果是回答被静默截断,而这种截断在监控上常常只表现为”质量下降”而非”报错”。
  2. “长上下文只是多花一点钱,质量不会变差。” 错误。位置偏差是质量维度的退化,与成本无关;同时把大量不相关候选一起塞进去会稀释注意力,反而比精挑后的短上下文更差。
  3. “KV Cache 能复用,所以重复问同样的问题就便宜了。” 错误。这是把 KV Cache 和前缀缓存混为一谈。KV Cache 只在单次请求内生效;跨请求的成本摊薄依赖前缀缓存,而它的命中前提是前缀逐字节一致。稍有改动(哪怕只是把参数换个顺序)就会失效。

延伸学习#

  • 《Lost in the Middle: How Language Models Use Long Contexts》(Nelson F. Liu、Kevin Lin、John Hewitt、Ashwin Paranjape、Michele Bevilacqua、Fabio Petroni、Percy Liang;arXiv:2307.03172,2023-07-06 提交,v3 2023-11-20;发表于 TACL):在 multi-document QA 与 key-value retrieval 两个任务上系统测量位置影响,发现相关信息位于上下文开头或结尾时性能最高、位于中部时显著退化,且对明确的长上下文模型同样成立。本轮已核验摘要与版本信息。链接:https://arxiv.org/abs/2307.03172
  • 实践方向(自行测量,非链接):对自己的 Agent 做三组对照——(a) 同一问题、正确答案分别放在检索材料的首/中/尾,统计准确率差异;(b) 稳定前缀在前 vs 可变内容在前,统计缓存命中 token 数与首 token 延迟;(c) 输入长度 4k / 16k / 64k 三档,记录首 token 延迟与单轮成本,拟合出你自己的代价曲线。用这三条实测结论替代”窗口够大就行”的直觉。

S01 · AI Agent 场景题#

题目 ID:Q-20261019-S01 | 一级类型:RAG 与搜索(兼涉 AI Agent 核心) | 二级主题:知识更新滞后、语义缓存陈旧与引用溯源失效 | 难度:场景题(高) | 题型:Agent 场景

题目#

业务背景。 企业内部制度问答 Agent,服务 HR 与财务共享中心。员工问报销标准、休假规则、采购流程,答案必须可直接引用原文,因为会作为报销与审批的依据。

系统规模。 知识库 1.8 万份文档(制度、流程、SOP),切片约 42 万条;日均问答 3.5 万次;单位问答推理成本较高,因此线上开了语义缓存。

当前架构与数据流。

DMS(文档系统) --每日定时同步--> 对象存储(按 doc_id 覆盖写)
--> 解析/切片 --> 向量库(HNSW) + BM25 索引
--> [查询侧] 混合检索 -> 重排 -> Top-8 进上下文 -> 模型带引用生成
--> [缓存侧] 语义缓存: embedding 相似度 >= 0.93 且同租户命中 -> 复用答案

目标。 答案必须反映最新生效版本,且每条结论都能点开原文锚点核对。

限制条件。

  • DMS 只能提供文档级信息(doc_id、version、updated_at),没有段落级 diff。
  • 每天只有一次 3 小时的全量重索引窗口,且不能中断线上问答。
  • 语义缓存是成本控制的支柱,不能整体关闭。

已知故障现象。 某制度于 9 月 30 日发布新版本,废止了旧报销标准。到 10 月 14 日,仍有约 11% 的相关问题给出旧标准;这些答案附带的引用链接点开后是 404(原文段落已随版本下线)。同期语义缓存命中率 23%,抽查发现部分命中返回的是旧答案。

需要你回答的取舍问题。 为了保证”答案反映最新生效版本”,你愿意在什么环节付出延迟与成本?是每次查询都实时校验原文(一致性最高、成本最高),还是把一致性成本前置到写入链路(查询侧便宜、但依赖链路可靠性)?请给出你的选择与理由。

先思考#

  1. 向量索引在这个系统里是什么性质的东西——是事实的来源,还是别处事实的一份副本?如果是副本,那么”谁有权宣布它过期”?
  2. 语义缓存用相似度阈值判定命中。相似度能表达”这条答案还是最新的吗”?如果不能,缓存键里还应该有什么?
  3. 引用链接指向的是”内容”还是”位置”?当文档换了一个版本,原来的位置还存在吗?你希望它 404,还是希望它指向历史版本并明确标注”已被取代”?
  4. 文档只有版本、没有段落级 diff。这意味着一次更新会影响哪些下游对象?失效的粒度最小能到多细?
  5. 如果重索引期间新旧两版切片同时存在于向量库里,检索会怎么处理?这是特性还是缺陷?

参考答案#

结论:这是”派生视图与答案缓存的失效”问题,不是模型问题。核心治理分三步——把文档版本提升为一等公民(切片携带 doc_version、content_hash 与稳定切片 ID),把检索范围与语义缓存的失效键绑定到知识版本,把引用锚点做成不随版本下线而 404 的可解析地址。一致性成本应前置到写入链路,而不是让每次查询去实时校验原文。

先明确三层能力的归属,这是整道题的判断基线:

  • 模型能力(概率性):生成答案、决定引用哪一条、判断”这条是否过期”。不能依赖——让模型自己识别自己引用的是旧版本,本质上是用一个概率组件去保证一个应当确定的不变量。
  • Agent 编排能力:决定检索的版本范围、命中缓存后是否触发二次校验、失败时是降级还是转人工。这是可设计、可回放的部分。
  • 传统软件的确定性能力:版本指针的原子切换、检索时的版本过滤、缓存键的构造与失效、幂等写入、引用 URL 的解析与重定向。这些必须放在模型之外,用数据库事务和索引过滤实现。

一、问题拆解。 11% 的旧答案不是单一缺陷,是五个独立失效叠加:

  1. 索引陈旧:定时同步 + 覆盖写,没有”生效版本”的概念,新版本切片写进去后旧切片仍在库里,检索可能命中任何一方。
  2. 切片身份不稳定:对象存储按 doc_id 覆盖写,切片 ID 若由内容或位置生成,换版后 ID 变化 → 引用锚点失效 → 404。
  3. 语义缓存无版本绑定:缓存键只有”租户 + 相似度”,不含知识版本,因此文档更新后旧答案继续被命中。
  4. 引用无失效语义:引用指向的是某个版本下的具体位置,该位置下线后既没有重定向也没有”已被取代”标注。
  5. 缺少时效性评测:评测集只看答案正确率,没有”引用的版本是否为当前生效版本”这一维度,所以问题上线两周才被发现。

二、核心方案。

  1. 文档版本化与不可变存储。 对象存储改为按 doc_id/version 存快照,永不覆盖。保留最近 N 个版本用于历史对比与引用回溯。
  2. 切片身份与溯源锚点。 切片记录至少包含:doc_id、version、section_path(章节路径)、char_start/char_end、content_hash、切片文本。切片 ID 必须是稳定可推导的(如 hash(doc_id, version, section_path)),保证同一位置在新旧版本间可建立对应关系。
  3. 生效版本指针 + 双版本并存。 用一张关系表存 doc_id → active_version,检索时 pre-filter version_state = active。换版时新旧切片同时存在,指针切换是单行事务,因此可瞬时回滚。
  4. 语义缓存的版本绑定。 缓存键改为 (tenant, permission_group, query_signature, knowledge_manifest_version)。相似度阈值只用于命中判定,不用于新鲜度判定。失效采用”影响集”策略:文档换版时,找出引用过该 doc_id 的 query 签名集合并定向失效,而不是全量清缓存。
  5. 引用 URL 的失效语义。 引用指向 (doc_id, version, section_path) 的逻辑地址而非对象存储的临时地址。若该版本已被取代,服务端返回历史版本内容并强制标注”v3,已被 v4 取代,点击跳转最新版本”,永不 404。
  6. 生成端约束。 提示词要求:引用必须来自本次检索返回的材料,禁止凭记忆补充条款号;若检索结果中出现多个版本的同一文档,只引用 active 版本。

三、关键执行流程。

DMS 变更事件(webhook) / 定时对账
-> 拉取 doc_id + version + 原文快照(按版本存)
-> 解析切片(按 doc_id+version 幂等写入)
-> 写入切片表 + 向量库 + BM25(标注 version_state=pending)
-> 一致性校验: 切片数/锚点可解析/抽样检索命中率/时效子集回归
-> 校验通过 => 事务切换 active_version 指针 (pending -> active, 旧版 -> superseded)
-> 定向失效语义缓存影响集
-> 灰度(先 5% 流量走新版本) -> 全量
-> 观测: 时效准确率/引用可点开率/缓存命中版本过期率

四、技术选型及依据。

环节选型依据
元数据与版本关系表作为 source of truth版本切换需要事务与可回滚,向量库不适合承载权威状态
检索保留混合检索(HNSW + BM25)故障与检索质量无关,不换引擎;只在查询上加 version_state 过滤
缓存Redis,键含清单版本失效粒度可控,支持按影响集定向删除
变更捕获DMS webhook 为主 + 按 updated_at 定时对账补偿webhook 会丢,对账保证最终一致
引用服务逻辑地址 + 版本重定向把”位置下线”转化为”版本标注”,消除 404

备选方案与不选择的原因:

  • 全量重索引(每天一次):实现最简单,但 3 小时窗口内要重切 42 万条切片,成本高、窗口内索引状态不一致,且无法做到按文档粒度的快速回滚。不采用为常态路径,保留为兜底。
  • 查询时实时校验原文(每次生成前回源拉取该文档最新版本比对):一致性最高,但每次查询多一次文档读取与可能的重新检索,延迟与成本显著上升,且把一致性依赖放到了查询关键路径上。不作为默认,仅对”时效性敏感”子集启用。

五、方案取舍。 一致性成本前置到写入链路。理由是:文档变更是低频事件(日均数十次),而查询是高频事件(3.5 万次/日);把校验放在低频侧,单次摊销成本更低,且不增加查询的 P95 延迟。代价是链路本身的可靠性成为一致性的前提,因此必须配校验、灰度与回滚。

六、失败路径与边界条件。

  • 解析/切片失败:不切换指针,标记 failed 并告警;该文档继续返回旧版本并显式标注”版本更新处理中”。
  • 校验失败:回滚指针到上一版本,进入死信队列人工处理。
  • 缓存失效失败:影响集删除失败时按 TTL 兜底,同时临时收紧相似度阈值(如 0.93 → 0.97)降低误命中,而不是放任旧答案流出。
  • webhook 丢失:定时对账(按 updated_at 扫描近 24 小时)补偿,这是最终一致的兜底。
  • 文档删除/版本回滚:删除走 tombstone 而非物理删除,保留引用可解析;回滚就是把指针指回旧版本号,天然支持。
  • 超大文档:切片数设上界并对超出部分做分层,避免单文档换版引发大范围失效风暴。
  • 同义改写导致语义缓存误命中:缓存键必须包含租户与权限组,否则会跨权限泄漏(见安全段)。

七、超时、重试、幂等与恢复。

  • 解析任务以 (doc_id, version) 为幂等键,重复投递不产生重复切片;向量写入用 upsert 到稳定切片 ID。
  • 拉取原文与写向量分别设超时,重试带指数退避与抖动,重试预算耗尽进死信队列。
  • 指针切换是单行事务,要么全成功要么全失败,不存在半切换状态。
  • 全流程以事件历史可重放:任一环节失败后可从对应事件重跑,不依赖进程的存活。

八、安全与权限。 检索必须按租户与 ACL pre-filter,不能先召回后过滤——后者会让无权限内容进入上下文,构成 Hidden Context Exposure。语义缓存键必须包含 tenant 与 permission_group,否则高权限用户的答案会被低权限用户命中。引用服务需要对 doc_id 做鉴权后再返回内容。

九、成本、延迟与可靠性。 增量解析只对变更文档,成本与变更量成正比而非与库规模成正比。双版本并存会临时扩大索引量,需设置 superseded 版本的保留期与清理策略。缓存失效用影响集而非全量,避免命中率骤降导致的成本尖峰。查询侧改动只有一个 version_state 过滤条件,对 P95 延迟基本无影响。

十、验证指标、测试与上线观测。

指标定义目标
时效准确率答案引用的版本 == 当前生效版本的比例≥ 99.5%
引用可点开率引用链接返回 200 且内容可核对的比例100%(禁止 404)
缓存”版本过期命中率”命中缓存但知识版本已变更的比例0(硬指标)
变更生效时延文档换版到线上答案生效的时间P95 < 30 分钟
检索 P95 / 单位问答成本常规观测不劣化

评测集:单独构造”版本敏感子集”——每次制度换版时,自动把变更条款生成 N 条问答加入回归集,换版前答案为旧标准、换版后应为新标准。这是唯一能提前发现问题 5 的手段。

上线观测:按 doc_id × version 分桶统计答案引用分布;对”引用了 superseded 版本”的请求直接打点告警,而不是等待用户投诉。

面试官追问与回答要点#

追问 1(定位):11% 这个数字,你怎么判断它主要来自索引陈旧还是语义缓存陈旧?
回答要点:两者在指标上可以直接分离。给每次问答打上答案来源标签(retrieval_fresh / cache_hit),并按来源分桶统计”引用 superseded 版本”的比例。如果旧答案集中在 cache_hit 桶,说明是缓存失效问题;如果集中在 retrieval_fresh 桶,说明是索引/版本过滤问题。本例中两者都存在,但修复顺序必须是先索引后缓存——缓存的失效键依赖于”存在一份权威的版本清单”,索引侧没有版本概念时缓存无从绑定。

追问 2(设计):语义缓存的影响集失效,具体怎么做?文档没有段落级 diff。
回答要点:既然只有文档级版本,影响集的最小粒度就是 doc_id。做法是给每次查询记录它实际引用过的 doc_id 集合(来自检索结果的元数据),建立 doc_id → [query_signature] 的倒排。文档换版时,取出该 doc_id 对应的 query 签名集合,逐条删除缓存。这个倒排本身也需要生命周期管理(LRU + TTL),否则会无限膨胀。粒度粗一点(到 doc)是可以接受的,因为失效偏保守只会损失命中率,不会造成错误答案。

追问 3(边界):如果 DMS 连版本号都给不准,只有 updated_at,方案要怎么退化?
回答要点:此时无法建立”版本”这一等公民,退化为内容哈希版本:对原文快照计算 content_hash,以 (doc_id, content_hash) 作为版本标识。section_path + content_hash 仍能提供稳定切片 ID 与溯源锚点,只是失去”版本先后关系”这一语义——引用标注退化为”内容已变更”而非”v3 已被 v4 取代”。同时要向 DMS 侧提出明确的需求:版本号是审计与回滚的前提,缺失它意味着无法回答”三个月前这条制度是什么”,这在合规场景里是硬缺口。

关键点#

  1. 索引是派生视图,不是事实来源。 必须有权威侧的版本清单,且失效由权威侧触发,而不是让检索侧自己判断新鲜度。
  2. 语义缓存的相似度阈值只管”命中”,不管”新鲜”。 新鲜度必须由缓存键中的版本维度保证,两者不能混用。
  3. 切片 ID 与引用锚点必须在切片阶段就写入,且要稳定可推导。 事后补溯源等于重新做一次切片。
  4. 引用不应 404。 旧版本应保留可访问并标注”已被取代”,404 会直接摧毁用户对答案的信任。
  5. 一致性成本前置到低频的写入链路,查询侧只加一个版本过滤条件。

常见误区#

  1. “把相似度阈值调高就能避免旧答案。” 错误。相似度衡量的是查询之间的接近程度,与知识是否更新完全正交。同一句话在换版前后的相似度可能接近 1.0,调阈值只会降低命中率、推高成本,对新鲜度毫无帮助。
  2. “让模型在生成时自己检查引用是不是最新版本。” 错误。这是把一个必须确定的不变量交给概率组件。模型既不知道当前的生效版本号,也无法可靠判断自己看到的材料属于哪个版本;版本判定必须落在检索过滤与缓存键上。
  3. “每天全量重索引一次,这个问题就自然消失了。” 错误。全量重索引解决不了切片 ID 不稳定与引用 404,也解决不了缓存陈旧;而且重索引窗口内新旧切片混杂,若无版本过滤,检索结果本身就是不确定的。

小型实践(30–60 分钟)#

用本地一个 20 份文档的小知识库做一次端到端验证:

  1. 建一张 documents 表(doc_id、version、content_hash、active)和一张 chunks 表(chunk_id = hash(doc_id, version, section_path)、char_start、char_end、text)。
  2. 写脚本把文档切成切片并写入两张表,给其中一份文档造 v1 与 v2 两个版本(v2 修改某一具体条款)。
  3. 实现两个查询路径:A 路径不带版本过滤,B 路径只查 active。分别提问同一条款,对比答案。
  4. 加一层”语义缓存”(用 dict 模拟即可),键先写成 (query),观察换版后仍返回旧答案;再把键改成 (query, active_manifest_version),验证换版后自动失效。
  5. 输出一份对照表:旧方案的失效表现 vs 新方案的命中/失效行为。

延伸学习#

  • pgvector 官方 README(HNSW / IVFFlat、filtered ANN 的先召回后过滤陷阱、迭代扫描、分区):本轮的检索侧改动本质上是在 ANN 查询上加过滤条件,理解”带过滤的近似检索为什么会退化”直接决定方案是否可用。本轮沿用此前核验结论,未重新抓取正文。链接:https://github.com/pgvector/pgvector
  • 实践方向(自行验证,非链接):在你们自己的向量库上做一次实测——把过滤条件从”无过滤”改为”按版本过滤”,记录召回率与 P95 延迟的变化。如果召回率明显下降,说明过滤选择性过高触发了退化,需要改用按版本分区或迭代扫描,而不是简单加一个 WHERE。

S02 · 非 AI Agent 场景题#

题目 ID:Q-20261019-S02 | 一级类型:操作系统与网络(兼涉软件工程与平台工程) | 二级主题:优雅终止、就绪探针、端点摘除与长连接排空 | 难度:场景题(高) | 题型:非 Agent 场景

题目#

业务背景。 订单查询 API,面向 App 与第三方商户,是交易链路上的强依赖服务。

系统规模。 Kubernetes Deployment,12 副本;日均 4000 万请求,稳态 P95 45ms、错误率 0.02%。

当前架构与数据流。

客户端(HTTP/1.1 keep-alive, 600s)
-> Nginx Ingress(对上游保持长连接池)
-> Service(ClusterIP)
-> Pod(12 副本, Go 服务)

发布方式为滚动更新,maxUnavailable=25%、maxSurge=25%。

目标。 发布过程零感知:发布窗口内 5xx 比例 < 0.01%,单次发布总时长 < 6 分钟。

限制条件。 不能停机发布;不能要求所有客户端改造(第三方商户的 SDK 版本不可控);单次请求最长 30 秒(一个聚合查询)。

已知故障现象。 每次发布都出现约 30 秒 的 502 尖峰,错误率峰值 1.8%。查看 Pod 事件:状态正常进入 Terminating,没有 OOM、没有 panic、没有重启。稳态一切正常,只在发布窗口出问题。

需要你回答的取舍问题。 要让”摘除端点”与”终止进程”严格有序,最直接的办法是在两者之间插入一段等待。这段等待会拉长发布时长。你如何在”发布速度”与”请求无损”之间取值?请给出可量化的依据。

先思考#

  1. 当你删除一个 Pod 时,“从 Service 的 endpoints 里摘掉它”和”容器进程收到终止信号”这两件事,是同一时刻发生的吗?谁先谁后?
  2. 一个 Pod 处于 Running 状态,是否就意味着它在接收流量?反过来,它被标记为 Terminating 时,是否就不再接收流量了?
  3. 进程收到终止信号后立刻退出,那些”已经建立但还没返回”的长连接请求会怎样?客户端看到的是 502 还是别的?
  4. 即使 endpoints 已经摘掉了这个 Pod,Ingress 那一侧的连接池里可能还缓存着什么?摘除信息传播到它需要多久?
  5. 新 Pod 那边呢?它启动后多久开始接流量?如果它的依赖还没预热完成就开始接流量,会发生什么?

参考答案#

结论:这不是应用崩溃,是”端点摘除”与”进程终止”两条异步链路的时序错位。Pod 被删除的时刻,它仍在 Service 的 endpoints 里,也会继续收到新请求;而进程一旦开始关闭,正在处理的请求就被中断。修复是三段式——启动侧用就绪探针保证新 Pod 真正可服务才入流量;终止侧用 preStop 等待端点摘除传播完成;进程侧收到信号后先排空存量请求再退出。

一、问题拆解。 502 尖峰来自三个独立缺陷,必须分别修:

  1. 终止侧时序错位(主因)。 删除 Pod 是一个”广播事件”:kubelet 开始终止容器,同时 EndpointSlice 控制器开始摘除端点,两者并行且异步。容器可能已经停止 accept,而 endpoints 里还留着它,于是新请求继续被转发到一个正在关闭的进程上。
  2. 上游存在陈旧视图。 即使 endpoints 已摘除,Ingress 的连接池仍持有指向该 Pod 的已建立连接,摘除信息传播到上游需要时间。这段窗口内上游还会继续转发。
  3. 启动侧可能也在贡献错误。 如果 Pod 没有配置就绪探针,它在容器启动后就会被加入 endpoints,而此时连接池、缓存、配置可能尚未预热完成——新 Pod 的前若干请求同样会失败。

二、核心方案。

  1. 启动侧:配置就绪探针。 就绪探针必须反映”真正能服务”的状态,而不是”进程活着”。Kubernetes 官方文档对就绪探针的语义是:探测成功则 Pod 被标记为 ready 并接收来自 services 的流量,探测失败则被标记为 unready 且不接收任何 service 的流量。因此探针应覆盖依赖预热、连接池建立、必要配置加载;periodSeconds 与 failureThreshold 要匹配真实预热时长。
  2. 终止侧:preStop 钩子插入等待。 容器的 preStop 钩子会在容器进入 Terminating 状态之前运行。在其中 sleep 一段大于端点传播时延的时间(通常 5–15 秒,需实测确定),让摘除先传播完成,再让容器真正开始关闭。这是代价最低、框架无关的做法。
  3. 进程侧:优雅关闭 + 连接排空。 收到 SIGTERM 后:停止接受新连接 → 对 keep-alive 连接返回 Connection: close(HTTP/1.1)或发送 GOAWAY(HTTP/2),主动告知上游不要再复用 → 等待存量请求完成,但必须设上界 → 退出。
  4. 优雅期限要算出来,不要抄默认值。 Kubernetes 默认授予的优雅终止期限是 30 秒。正确取值应满足:terminationGracePeriodSeconds > preStop sleep + 最大排空时长 + 安全余量。本例最大请求 30 秒,若 sleep 取 10 秒,则期限至少应设为 45–50 秒,而不是默认的 30 秒。
  5. 上游与客户端兜底。 Ingress 侧开启被动健康检查(连续失败即临时摘除后端)并允许把幂等失败重试到另一个后端;客户端侧用指数退避 + 抖动重试。

三、关键执行流程。

[新 Pod] 容器启动 -> 依赖预热 -> readinessProbe 通过 -> 加入 endpoints -> 开始接流
[旧 Pod] 收到删除请求
-> EndpointSlice 控制器开始摘除(异步传播) ┐
-> preStop sleep(N 秒) ├ 这两条并行
-> sleep 结束, kubelet 发送 SIGTERM ┘
-> 应用: 停止 accept / 返回 Connection: close / 开始排空
-> 存量请求完成(有上界) -> 进程退出
-> 超时未退出 -> SIGKILL 强制终止

四、技术选型及依据。

环节选型依据
启动准入readinessProbe官方语义直接决定 endpoints 成员资格,是唯一被 Service 认可的准入机制
终止等待preStop exec: sleep N在容器进入终止态之前运行,恰好覆盖端点传播的窗口;与应用语言无关
优雅期限显式设置 terminationGracePeriodSeconds必须大于 sleep + 排空时长,否则期限一到就被 SIGKILL,等待与排空全部作废
连接排空应用层主动关闭上游连接池不会主动感知,必须由服务端发信号
上游兜底被动健康检查 + 幂等重试覆盖仍未被摘除的残余窗口

备选方案与不选择的原因:

  • 只调大 terminationGracePeriodSeconds:不解决时序错位。摘除与终止仍然竞争,只是把”被中断前的等待”拉长了。无效方案。
  • 改用 readinessGate 或应用内就绪门控:能更精确地控制准入,但引入额外控制器与运维复杂度。作为进阶手段保留,不作为首选。
  • 用服务网格(如 sidecar 主动 draining):能力更强,但为一次发布问题引入网格,成本与风险不匹配。过度设计。
  • 改为蓝绿/金丝雀发布:能彻底隔离,但需要双倍资源与流量切换机制。作为长期演进方向,不是当前最小修复。

五、方案取舍。 在”发布速度”与”请求无损”之间,等待时长 N 的取值依据是实测的端点摘除传播时延,不是拍脑袋。量化方法:在 Pod 上记录删除事件时间戳,在 Ingress 侧记录最后一次把请求转发给该 Pod 的时间戳,两者之差即为需要覆盖的窗口;取该分布的 P99 作为 N,再加 20–30% 余量。本例若实测 P99 为 6 秒,取 N = 8–10 秒。

代价是发布时长增加:本例 12 副本、每批 25%(3 个 Pod)、共 4 批,每批增加 10 秒等待,总发布时长增加约 40 秒,远低于 6 分钟上限。这笔交换是明确划算的——用 40 秒换掉 1.8% 的错误率峰值。

六、失败路径与边界条件。

  • preStop 卡住:期限一到强制 SIGKILL,请求同样中断。因此 preStop 必须自带超时,且始终小于期限。
  • 排空上界:排空上限 < terminationGracePeriodSeconds − preStop sleep。剩余时间不足时,应主动对仍未完成的请求返回可重试错误,而不是等 SIGKILL。
  • maxUnavailable 过大:终止期容量下降,叠加稳态流量会放大错误。本例 25% 意味着同时有 3 个 Pod 在终止,需确保剩余 9 个能承载全部流量;必要时降低该值或提高副本数。
  • 长连接场景:WebSocket / SSE 无法在短期限内排空,强行排空等于中断业务。应单独处理:要么显著延长该类服务的期限,要么依赖客户端重连机制。
  • 就绪探针过于严格:依赖抖动会让 Pod 被反复摘除,反而制造错误。探针应只检查本 Pod 能否服务,不检查下游依赖的可用性(否则会引发全实例同时摘除的雪崩)。
  • 同时缩容与发布:两个控制器同时动 endpoints,会让容量窗口更难推理。发布期间应避免并发的扩缩容操作。

七、超时、重试、幂等与恢复。 网关层重试只针对”幂等且未收到响应体”的失败(如连接建立失败、503),避免重复提交写操作;写接口需要客户端携带幂等键。客户端重试采用指数退避 + 抖动,避免在发布窗口形成重试风暴。发布失败时用 rollout undo 回滚到上一版本,回滚本身同样要走一遍优雅终止流程。

八、性能与可用性。 增加的等待直接进入发布时长,需确认仍在预算内(本例 +40 秒)。滚动期间要依靠 maxSurge 提供容量冗余,保证摘除窗口内容量不下降。就绪探针的探测频率会带来自身开销,探测间隔应显著大于单次探测成本。

九、验证指标、测试与上线观测。

指标定义目标
发布窗口 5xx 比例发布期间 5xx / 总请求< 0.01%
发布窗口错误持续时间错误率超过基线的时长0
端点摘除传播时延删除事件 → endpoints 实际移除观测并取 P99,作为 N 的依据
readiness 首次通过时延容器启动 → 探针首次成功稳定且小于发布预算
终止期未完成请求数SIGKILL 时被中断的请求数0
发布总时长开始 → 全部就绪< 6 分钟

测试方法:在预发环境以恒定 QPS 压测,同时强制执行一次滚动更新,采集上述指标;把错误按”启动期 / 终止期 / 稳态”分桶,确认错误归属。修复后重复同一实验做 A/B 对比。这个实验必须在预发做,不能直接在生产验证。

上线观测:按 Pod 生命周期阶段给错误打标签;对”发布窗口”这一时间区间单独设告警,而不是只看全天错误率——1.8% 的峰值 30 秒,在全天均值上几乎看不出来,这正是它长期没被发现的原因。

面试官追问与回答要点#

追问 1(机制):为什么”Pod 已经 Terminating 了”这件事,不能让 Service 立刻停止转发?
回答要点:因为 Service 的转发依据是 endpoints(EndpointSlice)成员列表,而 Pod 进入 Terminating 与 endpoints 移除是两条独立的控制回路——前者由 kubelet 执行,后者由 EndpointSlice 控制器完成并经 API 传播到每个节点的 kube-proxy / Ingress。这是一个最终一致的过程,没有任何机制保证在容器收到信号之前完成。理解这一点,就理解了为什么必须在中间插入显式等待。

追问 2(取值):preStop 的 sleep 到底该设多少?给一个可复现的定参方法。
回答要点:不要用经验值。做法是在 Pod 侧记录收到 SIGTERM 的时间戳 t0,在 Ingress 访问日志里找出该 Pod IP 最后一次收到请求的时间戳 t1,取 t1 − t0 的分布,用 P99 加 20–30% 余量作为 sleep 值。规模变化、节点数变化都会影响这个值,因此需要定期重测,而不是设一次就永远不变。

追问 3(边界):如果服务里有 WebSocket 长连接,30 秒排不完,怎么办?
回答要点:长连接不能套用短请求的排空思路。三条路:一是显著延长该 Deployment 的 terminationGracePeriodSeconds(代价是发布更慢,且节点驱逐时同样受限);二是服务端在终止前主动发送关闭帧,让客户端按协议重连到新实例(需要客户端配合,本例第三方 SDK 不可控,可行性低);三是把长连接服务从短请求服务拆分出来单独发布与治理。推荐第三条——混合部署会让一个 Deployment 的终止策略同时受两种约束,最终两头都不满足。

关键点#

  1. Pod 删除与端点摘除是两条并行的异步链路,容器可能在仍接收流量时就开始关闭,这是 502 的根因。
  2. 就绪探针决定 endpoints 成员资格:探测成功才接收 service 流量,失败则完全不接收。存活探针管的是重启,不影响 endpoints。
  3. preStop 在容器进入终止态之前运行,是在”摘除”与”终止”之间插入等待的唯一通用手段。
  4. 优雅期限必须按 sleep + 排空 + 余量 计算。默认 30 秒在本例(最大请求 30 秒)下明显不够,期限一到 SIGKILL 会让所有等待作废。
  5. 上游连接池持有陈旧后端,服务端必须主动发信号(Connection: close / GOAWAY)才能让其停止复用。

常见误区#

  1. “配了存活探针就够了。” 错误。存活探针失败只会导致容器重启,它完全不影响 endpoints 成员资格。控制流量准入的是就绪探针,两者不能互相替代;只配存活探针的 Pod 在启动瞬间就会被加入 endpoints。
  2. “收到 SIGTERM 后立刻退出就是优雅关闭。” 错误。立刻退出会中断所有已建立但尚未返回的长连接请求,客户端看到的正是 502。优雅关闭的含义是”停止接收新的、完成已有的”,两者缺一不可。
  3. “把 terminationGracePeriodSeconds 调大到 120 秒就安全了。” 错误。如果 preStop 与排空逻辑没改,容器仍会在收到 SIGTERM 后很快退出,多出来的期限只是闲置;而如果容器真的卡住不退出,120 秒又会让发布变得极慢。期限是约束,不是修复——它必须配套于 preStop 与排空,且按公式计算。

小型实践(30–60 分钟)#

用本地 kind 或 minikube 复现并修复:

  1. 部署一个 Go/Python 服务,其中一个接口 sleep 20 后返回,模拟长请求;Deployment 2 副本,前面挂一个 Service。
  2. 用 hey 或 wrk 以恒定 QPS 打流量,同时执行 kubectl rollout restart,统计 5xx 数量——应当能稳定复现错误。
  3. 依次加入三处改动,每次重复压测并记录:
    • 只加 readinessProbe;
    • 再加 preStop: exec: sleep 10;
    • 再显式设置 terminationGracePeriodSeconds: 45。
  4. 输出三列的对比表:5xx 数量、错误持续时间、发布总时长。用来回答”哪一处改动贡献了多少收益”。

延伸学习#


L01 · LeetCode#

题目 ID:Q-20261019-L01 | 一级类型:图的基础遍历 / 拓扑排序 | 难度:medium

题目#

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1。

选修某些课程之前需要一些先修课程。先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] 表示:如果要学习课程 ai,则必须先学习课程 bi。

判断是否可能完成所有课程的学习?如果可以,返回 true;否则返回 false。

先思考#

  1. 把每门课看成一个点、[ai, bi] 看成一条”从 bi 指向 ai”的有向边。什么情况下”不可能完成”?用图论的语言描述它。
  2. 一门课什么时候可以开始学?这个条件只和它自己有关,还是和它的所有前驱有关?能否把”可以学的课”组织成一个不断补充的队列?
  3. 如果图里有环,环上的课会怎样?它们会不会进入你在第 2 步构造的队列?
  4. 如果最后剩下一些课从来没进入过队列,你能得出什么结论?反过来,如果全部都进入过,说明什么?

参考答案#

题号 207 | 中文名:课程表 | 英文名:Course Schedule | 难度:Medium

官方链接(本轮已核验中英文页面,题号、标题、难度、示例与约束完全一致):

题目描述。 必须选修 numCourses 门课程(编号 0 到 numCourses - 1)。给定先修关系数组 prerequisites,其中 prerequisites[i] = [ai, bi] 表示学习 ai 之前必须先完成 bi。判断能否完成所有课程。

输入 / 输出。

  • 输入:整数 numCourses,二维整数数组 prerequisites。
  • 输出:布尔值——能完成所有课程返回 true,否则返回 false。

约束。

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites 中所有课程对互不相同

示例。

示例输入输出说明
1numCourses = 2, prerequisites = [[1,0]]true学 1 之前先学 0,可行
2numCourses = 2, prerequisites = [[0,1],[1,0]]false互相为前置,形成环,不可行

考察的数据结构与算法模式。 有向图、入度表、拓扑排序(Kahn 算法)/ 环检测。

解题思路。 结论是:能否完成全部课程,等价于课程依赖图中是否存在有向环。

推导过程分三步:

  1. 建模。 每门课是一个节点;[ai, bi] 表示 bi → ai 的有向边(bi 是 ai 的前驱)。
  2. 观察可学条件。 一门课只有在所有前驱都已完成后才能学。把”还剩几个未完成前驱”记为该节点的入度。入度为 0 的课当前就可以学。
  3. 剥离。 把所有入度为 0 的课放入队列;每完成一门课,就把它指向的所有后继的入度减 1,新产生的入度为 0 的课继续入队。这是一个”不断剥离可学课程”的过程。

关键判断来了:如果图中存在环,环上每个节点至少有一个前驱也在环上,因此环上节点的入度永远不可能降到 0,它们永远不会入队。于是:

  • 最终入队(被剥离)的节点数 等于 numCourses → 无环 → 可以完成;
  • 小于 numCourses → 剩余节点构成一个(或多个)环 → 不可完成。

正确性说明。

  • 充分性(无环 → 算法能剥离全部节点):有向无环图必然存在至少一个入度为 0 的节点(否则沿着前驱回溯会无限走下去,在有限图中必然重复,构成环,矛盾)。剥离它之后,剩余子图仍然无环,同理继续。由归纳法,算法能剥离全部节点。
  • 必要性(算法剥离全部 → 无环):若存在环,环上任一节点 v 的前驱 u 也在环上,u 只有被剥离才会让 v 的入度减 1;而 u 的剥离又依赖它的前驱……形成循环依赖,环上没有任何节点能被剥离。因此能被剥离全部节点时,图中不可能有环。
  • 综上,算法返回 true 当且仅当图无环,当且仅当课程可以全部完成。

时间复杂度:O(n + m),其中 n = numCourses、m = prerequisites.length。建图 O(m),每个节点恰好入队一次 O(n),每条边在入度递减时被访问一次 O(m)。

空间复杂度:O(n + m)。邻接表 O(n + m),入度数组与队列 O(n)。

Python 参考代码。

from collections import deque
from typing import List
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
# graph[b] 存放所有以 b 为前驱的课程
graph = [[] for _ in range(numCourses)]
indegree = [0] * numCourses
for course, pre in prerequisites:
graph[pre].append(course)
indegree[course] += 1
# 入度为 0 的课程当前即可学习
queue = deque(i for i in range(numCourses) if indegree[i] == 0)
completed = 0
while queue:
cur = queue.popleft()
completed += 1
for nxt in graph[cur]:
indegree[nxt] -= 1
if indegree[nxt] == 0:
queue.append(nxt)
# 有环时,环上课程永远无法入队,completed 会小于 numCourses
return completed == numCourses

关键边界情况。

  • prerequisites 为空:没有任何依赖,直接返回 true(所有课程入度为 0)。
  • 自环 [a, a]:课程以自己为前置,indegree[a] == 1,永不入队,返回 false。
  • 多个互不相连的环:只要存在任一环,剩余节点数即大于 0,返回 false。
  • 环外还挂着链:链上节点可能被剥离,但环上不会;用 completed == numCourses 判定,而不是”队列是否为空”。
  • numCourses = 1 且 prerequisites 为空:返回 true。
  • 注意边的方向:[ai, bi] 是 bi → ai,建反了会得到错误答案(反图同样能检测环,但必须先确认方向定义一致)。

测试用例(5 个,均已在本地执行通过)。

#输入输出覆盖点
12, [[1,0]]true官方示例 1,简单链
22, [[1,0],[0,1]]false官方示例 2,二元环
33, []true无依赖,全入度 0
43, [[1,0],[1,2],[2,1]]false环外挂链,环上为 1 ↔ 2,课程 0 可完成但整体不可完成
54, [[1,0],[2,0],[3,1],[3,2]]true菱形 DAG,合法拓扑序存在

关键点#

  1. 建模方向:[ai, bi] 对应 bi → ai,边指向”被依赖者之后的课”。
  2. 不变量:队列中的节点入度恒为 0,且每个节点至多入队一次。
  3. 终止判据是计数而非队列空:completed == numCourses 直接对应”是否有节点留在环里”。
  4. 环上入度永不为 0,这是算法能检测环的根本原因。
  5. 复杂度是线性的,来自”每节点一次、每边一次”,不是来自 BFS 层数。

常见误区#

  1. 把边的方向建反后仍套用同一个解释。 反图本身也能检测环(入度换成出度即可),但混用会让推理过程自相矛盾。做法是:先固定”边 = 依赖关系方向”,再统一推导。
  2. 用”队列是否为空”作为成功判据。 队列最终总会空,无论有没有环。正确的判据是被剥离的节点数是否等于总课程数。
  3. 认为需要真的输出一个学习顺序。 本题只要求判定可行性;输出顺序是 210 题的要求。判定题不需要保存拓扑序,只需计数。

延伸学习#

  • LeetCode 210. 课程表 II(Course Schedule II):同一模型的自然进阶——要求输出任意一个可行学习顺序,即在 Kahn 算法中记录出队顺序;若最终数量不足则返回空数组。本轮未核验该页面正文,仅按题目编号与名称陈述。链接:https://leetcode.cn/problems/course-schedule-ii/
  • 与 AI Agent 工程的联系(自然存在):依赖环检测是 Agent 编排里的常客——工具调用的前置关系、子任务分解出的 DAG、技能包之间的依赖声明,都需要在使用前判定”是否存在不可满足的循环依赖”。可迁移的能力是:把一个”每一步看起来都合理”的计划,还原成一张图,再用不变量(入度能否归零)判定整体可行性。

延伸资料#

按”官方规范 → 原始论文 → 官方文档”优先级排列,均为本轮实际核验过的来源。

主题资料类型核验状态
B01 负缓存与 SOA TTLRFC 2308《Negative Caching of DNS Queries (DNS NCACHE)》,1998-03,Standards Track标准规范已核验全文:负缓存不再是可选部分;权威服务器 MUST 携带 SOA;负 TTL 取 min(SOA.MINIMUM, SOA 自身 TTL);NXDOMAIN 按 <QNAME,QCLASS>、NODATA 按 <QNAME,QTYPE,QCLASS> 缓存;本地上限建议 1–3 小时,超过一天有问题
B02 长上下文位置偏差《Lost in the Middle: How Language Models Use Long Contexts》,arXiv:2307.03172,2023-07-06 提交 / v3 2023-11-20,TACL原始论文已核验摘要与版本:相关信息在开头或结尾时性能最高,中部显著退化,对长上下文专用模型同样成立
S02 就绪探针语义Kubernetes 官方文档 · Configure Liveness, Readiness and Startup Probes官方文档已核验原文:探测成功则 Pod 标记 ready 并接收来自 services 的流量;失败则 unready 且不接收任何 service 流量
S02 优雅终止Kubernetes 官方文档 · Pod Lifecycle官方文档部分核验:已确认”默认优雅终止期限 30 秒”与”preStop 钩子在容器进入终止态之前运行”两条原文;SIGTERM→SIGKILL 的完整步骤与端点传播不同步的声明,本轮页面正文抓取不完整,未核验
S01 向量检索过滤pgvector 官方 README(HNSW / IVFFlat / filtered ANN)官方文档沿用此前核验结论,本轮未重新抓取
L01LeetCode 207 中英文官方题页官方题库已核验:题号、标题、难度、两个示例、五条约束中英文一致;英文页通过率 52.4%(随时间变化)

本轮未核验、不得作为事实引用的项: LeetCode 210 的题目正文;各语言运行时 DNS 地址缓存的具体默认值;RFC 1034/1035 的具体条文;Kubernetes 端点传播时延的具体数值(需实测)。


当日重点#

  1. 不要把”上界”当成”保证”。 TTL 是最长缓存时间、上下文窗口是长度上限、terminationGracePeriodSeconds 是最长等待——三者都只对”合规的对象”生效,都不承诺全局同步。
  2. 定位陈旧状态要逐层问”这一层缓存了什么、由谁失效”。 DNS 是解析结果、语义缓存是答案、向量索引是知识的副本、Ingress 连接池是后端地址、KV 是注意力中间态——五者的失效主体完全不同。
  3. 新鲜度不能交给相似度,也不能交给模型判断。 版本必须是显式的、可比较的、由权威侧发布的;过滤与缓存键是确定性组件,不属于模型职责。
  4. 一致性成本前置到低频侧。 S01 把校验放在文档变更(日均数十次)而非查询(3.5 万次/日)上;S02 把等待放在发布(每天几次)而非每次请求上。同一条经济学。
  5. 修复要配对,不能只调参数。 调小 TTL 不解决连接池;调大优雅期限不解决时序错位;调高相似度阈值不解决知识陈旧。
  6. 缺失的评测维度就是盲区。 S01 有两周盲区是因为评测集没有”时效准确率”;S02 长期未被发现是因为只看全天错误率均值。指标定义决定了你能看见什么。

运行信息#

配置读取状态。 成功。实际使用 题库配置.md 版本 6(last_maintained: 2026-10-19)。版本 6 的变更记录为「每日训练收敛为 5 道」,与本次任务配额一致:非 Agent 基础 1、Agent 基础 1、Agent 场景 1、非 Agent 场景 1、LeetCode 1。无暂停类型;「观察中的新技术」共 8 项,均未标记「允许试学」,本次未从中出题。未修改配置文件。

历史读取与去重来源。

  • 已读取:题目索引.md(当前 40 条记录,覆盖 2026-10-10 至 2026-10-16,indexed_runs: 6)、精选/README.md(4 条精选题)、README.md、题库配置.md、prompts/generate-data-from-template.prompt.md。
  • 重要冲突(需人工确认):2026-10/2026-10-19-AI-Agent-面试训练.md 与 2026-10/2026-10-19-AI-Agent-周复盘.md 在磁盘上不存在,git status 显示两者均为已删除(D)且未提交;题目索引.md 亦被回退到 40 条 / indexed_runs: 6 的状态(早于当日 10:59 那次生成)。本次按「当天文件不存在」处理,重新生成当日训练文件。周复盘文件不在本次任务范围内,未重建。
  • 连带影响:精选/README.md 中记录「Q-20261012-B02 于 2026-10-19 经周复盘 C01 发出故障定位变体,待作答」——该变体随周复盘文件一起缺失。该条目在 精选/README.md 与独立精选文件中仍有完整记录,未丢失;但对应的复习动作本轮未执行,建议下次周复盘或人工补发。
  • 去重补充依据:当日被删除的那次生成曾产生 7 道题(数据库索引与执行计划回归、内存/GC/容器 OOM、提示缓存断点与前缀布局、MCP _meta 追踪上下文传播、第三方 MCP server 供应链准入与撤销、消费组再平衡与并行度上界、LeetCode 322)。这些记录来自任务日志而非当前索引,本次已作为去重依据避开,未重复生成同题或换皮题,也未复用 LeetCode 322。

去重结果。 与索引窗口内 40 题、以及上述当日已删除记录的 7 题逐一比对:

  • 题目标题、一级类型、二级主题、核心考点、故障现象、预期结论均不重合。
  • LeetCode:已用 146、3、215、739、875、124(索引);322(当日已删除记录,一并避开)。本次选 207(图 BFS/DFS 拓扑排序),符合”轮换到链表或图的 BFS/DFS、避免 DP / 栈 / 二分 / 哈希主导”的轮换建议。
  • 场景题已用类型累计 15 类,本次新增 2 类:RAG 知识更新滞后与引用溯源失效(Agent 场景)、滚动发布中的优雅终止与端点摘除时序(非 Agent 场景),此前均未出现。

一级类型覆盖(本次新增后)。 操作系统与网络 3、AI 与大模型基础 3、RAG 与搜索 3;其余类型维持不变。覆盖次数最少的方向现为 上下文工程与能力封装、编程语言与运行时、Agent 评测与可观测性、LLM 应用工程(各 2 次),可作为后续轮换优先项。

验证。 文件已写入且存在;5 道题齐全(B01 / B02 / S01 / S02 / L01);题量分布符合配置 v6;题目 ID 全文一致;无占位符残留;LeetCode 参考代码已在本机 Python 3.13 执行,7 个测试用例全部通过。本文件仅写入本地 Obsidian 目录,未做远端同步验证。


附:题目分类与标签#

题目 ID编号题型一级类型二级主题标签
Q-20261019-B01B01非 Agent 基础操作系统与网络DNS 解析、TTL、负缓存与解析结果的缓存层级AI-Agent、面试训练、操作系统与网络、DNS、TTL、负缓存、RFC2308、连接池
Q-20261019-B02B02Agent 基础AI 与大模型基础Attention、KV Cache 与上下文窗口的容量与成本边界AI-Agent、面试训练、AI与大模型基础、Attention、KV-Cache、上下文窗口、Lost-in-the-Middle
Q-20261019-S01S01Agent 场景RAG 与搜索 / AI Agent 核心知识更新滞后、语义缓存陈旧与引用溯源失效AI-Agent、面试训练、RAG与搜索、知识更新、引用溯源、语义缓存、版本化
Q-20261019-S02S02非 Agent 场景操作系统与网络 / 软件工程与平台工程优雅终止、就绪探针、端点摘除与长连接排空AI-Agent、面试训练、Kubernetes、优雅终止、readinessProbe、preStop、滚动发布
Q-20261019-L01L01LeetCode图的基础遍历 / 拓扑排序有向图环检测与入度剥离AI-Agent、面试训练、拓扑排序、环检测、BFS、LeetCode-207
2026-10-19 · AI Agent 面试训练
https://vissssa.github.io/posts/ai-agent-interview-2026-10-19/
作者
Siegward
发布于
2026-10-19
许可协议
CC BY-NC-SA 4.0