2026-10-19 · AI Agent 面试训练
今日学习主题
陈旧状态与容量边界:缓存层级里的”旧视图”如何变成线上事故。
今天五题考察同一件事的不同层——状态已经变了,但某一层仍在按旧视图工作:
- B01 · 解析层:DNS 的 TTL 与负缓存,为什么”TTL 60 秒”不能保证 60 秒内切干净流量。
- B02 · 模型层:上下文窗口的标称值不是可用预算,KV Cache 如何重塑成本结构。
- S01 · 知识层:RAG 索引、语义缓存与引用锚点在文档换版后的失效与重建。
- S02 · 基础设施层:Pod 端点摘除与进程终止的时序错位,如何变成发布窗口里的 502。
- L01 · 算法层:依赖图里的环,让”看起来每一步都合理”的计划整体不可完成。
贯穿的方法论有两条:第一,先定位”旧视图”停在哪一层,再决定失效该由谁触发;第二,不要把任何一个上界(TTL、窗口长度、超时、grace period)误当成保证。
题目目录
| 编号 | 题目名称 | 题型 | 一级类型 | 难度 | 一句话考察点 |
|---|---|---|---|---|---|
| B01 | DNS TTL 到期后流量为什么没切走 | 非 Agent 基础 | 操作系统与网络 | 基础题(中高) | TTL 只约束解析结果的缓存,约束不了已建连接与进程内地址缓存;负缓存由 SOA 承载 |
| B02 | 标称 128k 上下文,我能用满吗 | Agent 基础 | AI 与大模型基础 | 基础题(中高) | 窗口是上限不是预算;prefill/decode 瓶颈不同;长上下文存在位置偏差 |
| S01 | 制度更新两周后 Agent 仍在引用废止条款 | Agent 场景 | RAG 与搜索 / AI Agent 核心 | 场景题(高) | 索引是派生视图,版本与缓存失效键必须绑定知识版本;引用锚点要有失效语义 |
| S02 | 每次发布必有 30 秒 502 | 非 Agent 场景 | 操作系统与网络 / 软件工程与平台工程 | 场景题(高) | 端点摘除与进程终止是两条异步链路;readiness、preStop 与连接排空各管一段 |
| L01 | 207. 课程表 | 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 从另外几台机器验证结果是正确的。
请回答三个问题:
- DNS 记录里的 TTL 到底约束了什么,又约束不了什么?为什么 60 秒的 TTL 没有让流量在 60 秒内切干净?
report.internal的解析失败最可能是什么原因?它和普通记录的缓存有什么不同?- 如果要把服务切换做成可预期、可回滚的工程动作,除了改 TTL 还需要做什么?
先思考
- TTL 挂在谁身上?它是”解析答案”的属性,还是”已建立连接”的属性?一条已经握过手的 TCP 连接,之后还依赖 DNS 吗?
- 从应用进程一路到权威服务器,一次名字解析的结果会被几层东西记住?这些层是否共享同一个到期时间?
- “解析失败”本身算不算一种可以被缓存的答案?如果解析器把”这个名字不存在”也缓存起来,它的 TTL 从哪里来?
- 要让流量可控地离开旧实例,你需要分别对”新流量”和”存量连接”做什么?
参考答案
结论: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 到期前还是解析失败。规避方式是部署顺序反过来:先建记录并验证,再让服务依赖它。
关键点
- TTL 是”最长可缓存时间”,不是”到期即失效”的保证,更不是”全局同步生效”的保证。 它只在发起新解析的路径上生效。
- 已建立的连接不参与 DNS。 连接池的最大连接生命周期才是决定”多久重新解析一次”的真实参数。
- 否定答案也会被缓存。 负缓存的 TTL 由权威区的 SOA 承载,取
min(SOA.MINIMUM, SOA 自身 TTL);NXDOMAIN 与 NODATA 的缓存键粒度不同。 - 缓存是分层的,且各层独立计时。 定位问题时必须逐层确认,而不是只看权威记录改没改。
- DNS 不是健康检查和故障摘除机制。 它返回的是名字到地址的映射,不感知实例死活。
常见误区
- “TTL 设成 60 秒,60 秒后流量一定全部切走。” 错误。TTL 只约束解析结果的缓存,既约束不了已建立的连接,也约束不了应用进程内的地址副本。真正的切换完成时间由最慢的那一层决定,通常是连接池的重建周期。
- “解析失败不会被缓存,只是当时网络不通。” 错误。RFC 2308 已经明确负缓存不再是可选项——只要解析器做缓存,就必须同时缓存否定答案。先部署后建记录是一个会真实造成数小时故障的顺序错误。
- “调小 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 里的重排环节。上线后发现三个现象:
- 单次问答的成本涨了数倍,首 token 延迟显著变长;
- 同样的问题,正确答案在材料中部时准确率明显低于在开头或结尾;
- 即使问题和昨天几乎一样,成本也没有明显下降。
请回答:
- 上下文窗口的标称值意味着什么?实际可用容量被哪些东西挤占?
- prefill 与 decode 两阶段的成本结构有什么不同?KV Cache 在其中扮演什么角色?
- 为什么”能放下”不等于”能被有效利用”?在 Agent 系统里,应如何据此决定”往上下文里放多少东西”?
先思考
- 模型宣称的 128k,包含它自己要生成的那部分输出吗?如果一条请求要输出 4k,你能喂进去多少输入?
- 输入被读进来(理解上下文)和输出被一个 token 一个 token 吐出来,这两件事的瓶颈是同一种资源吗?
- 生成到第 n 个 token 时,前 n-1 个 token 的计算结果还需要重算一遍吗?如果不重算,代价是什么?
- 你已经知道检索环节可以过滤和重排。如果上下文足够大,“少放一点”还重要吗?请从成本和质量两个角度分别想。
参考答案
结论:上下文窗口是长度上限,不是可用预算。标称值同时被系统提示、工具定义、对话历史、检索材料和输出预留共同挤占;标准全局注意力关于序列长度的计算代价是二次的,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 那一段放大的。第三个现象是前缀缓存没命中——如果把用户问题放在系统提示前面,前缀每次都变,缓存命中率就是零,成本完全摊不掉。正确做法是把稳定内容放前面、变化内容放后面。
第二个现象是位置偏差。已有研究在长上下文上发现,相关信息放在开头或结尾时性能最好,放在中部会明显下降,即使对专门的长上下文模型也是如此。所以”能放下”不等于”能被用到”,结论是:不能靠加大窗口取消重排和过滤,反而应该把进入上下文的候选数量压下来,让留下的每一条都更可能被真正用到。
关键点
- 窗口是上限,预算才是设计对象。 输入可用量 = 窗口 − 输出预留 − 系统提示 − 工具定义 − 历史 − 检索材料。
- prefill 与 decode 是两种瓶颈。 prefill 算力受限、决定首 token 延迟;decode 带宽受限、决定吐字速度;优化与观测指标都要分开。
- KV Cache 是”显存换计算”。 它消除解码阶段的重复计算,但显存随长度线性增长,长上下文的直接后果是并发能力下降。
- 长上下文存在位置偏差。 相关信息位于中部时性能显著下降,这一结论在长上下文专用模型上同样成立。
- 前缀缓存的命中率由布局决定。 稳定内容在前、变化内容在后,是跨请求摊薄 prefill 成本的前提。
常见误区
- “窗口标 128k,我就能用 128k。” 错误。窗口包含输出,且系统提示、工具 schema、历史都要占位;不留输出余量的直接后果是回答被静默截断,而这种截断在监控上常常只表现为”质量下降”而非”报错”。
- “长上下文只是多花一点钱,质量不会变差。” 错误。位置偏差是质量维度的退化,与成本无关;同时把大量不相关候选一起塞进去会稀释注意力,反而比精挑后的短上下文更差。
- “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%,抽查发现部分命中返回的是旧答案。
需要你回答的取舍问题。 为了保证”答案反映最新生效版本”,你愿意在什么环节付出延迟与成本?是每次查询都实时校验原文(一致性最高、成本最高),还是把一致性成本前置到写入链路(查询侧便宜、但依赖链路可靠性)?请给出你的选择与理由。
先思考
- 向量索引在这个系统里是什么性质的东西——是事实的来源,还是别处事实的一份副本?如果是副本,那么”谁有权宣布它过期”?
- 语义缓存用相似度阈值判定命中。相似度能表达”这条答案还是最新的吗”?如果不能,缓存键里还应该有什么?
- 引用链接指向的是”内容”还是”位置”?当文档换了一个版本,原来的位置还存在吗?你希望它 404,还是希望它指向历史版本并明确标注”已被取代”?
- 文档只有版本、没有段落级 diff。这意味着一次更新会影响哪些下游对象?失效的粒度最小能到多细?
- 如果重索引期间新旧两版切片同时存在于向量库里,检索会怎么处理?这是特性还是缺陷?
参考答案
结论:这是”派生视图与答案缓存的失效”问题,不是模型问题。核心治理分三步——把文档版本提升为一等公民(切片携带 doc_version、content_hash 与稳定切片 ID),把检索范围与语义缓存的失效键绑定到知识版本,把引用锚点做成不随版本下线而 404 的可解析地址。一致性成本应前置到写入链路,而不是让每次查询去实时校验原文。
先明确三层能力的归属,这是整道题的判断基线:
- 模型能力(概率性):生成答案、决定引用哪一条、判断”这条是否过期”。不能依赖——让模型自己识别自己引用的是旧版本,本质上是用一个概率组件去保证一个应当确定的不变量。
- Agent 编排能力:决定检索的版本范围、命中缓存后是否触发二次校验、失败时是降级还是转人工。这是可设计、可回放的部分。
- 传统软件的确定性能力:版本指针的原子切换、检索时的版本过滤、缓存键的构造与失效、幂等写入、引用 URL 的解析与重定向。这些必须放在模型之外,用数据库事务和索引过滤实现。
一、问题拆解。 11% 的旧答案不是单一缺陷,是五个独立失效叠加:
- 索引陈旧:定时同步 + 覆盖写,没有”生效版本”的概念,新版本切片写进去后旧切片仍在库里,检索可能命中任何一方。
- 切片身份不稳定:对象存储按
doc_id覆盖写,切片 ID 若由内容或位置生成,换版后 ID 变化 → 引用锚点失效 → 404。 - 语义缓存无版本绑定:缓存键只有”租户 + 相似度”,不含知识版本,因此文档更新后旧答案继续被命中。
- 引用无失效语义:引用指向的是某个版本下的具体位置,该位置下线后既没有重定向也没有”已被取代”标注。
- 缺少时效性评测:评测集只看答案正确率,没有”引用的版本是否为当前生效版本”这一维度,所以问题上线两周才被发现。
二、核心方案。
- 文档版本化与不可变存储。 对象存储改为按
doc_id/version存快照,永不覆盖。保留最近 N 个版本用于历史对比与引用回溯。 - 切片身份与溯源锚点。 切片记录至少包含:
doc_id、version、section_path(章节路径)、char_start/char_end、content_hash、切片文本。切片 ID 必须是稳定可推导的(如hash(doc_id, version, section_path)),保证同一位置在新旧版本间可建立对应关系。 - 生效版本指针 + 双版本并存。 用一张关系表存
doc_id → active_version,检索时 pre-filterversion_state = active。换版时新旧切片同时存在,指针切换是单行事务,因此可瞬时回滚。 - 语义缓存的版本绑定。 缓存键改为
(tenant, permission_group, query_signature, knowledge_manifest_version)。相似度阈值只用于命中判定,不用于新鲜度判定。失效采用”影响集”策略:文档换版时,找出引用过该doc_id的 query 签名集合并定向失效,而不是全量清缓存。 - 引用 URL 的失效语义。 引用指向
(doc_id, version, section_path)的逻辑地址而非对象存储的临时地址。若该版本已被取代,服务端返回历史版本内容并强制标注”v3,已被 v4 取代,点击跳转最新版本”,永不 404。 - 生成端约束。 提示词要求:引用必须来自本次检索返回的材料,禁止凭记忆补充条款号;若检索结果中出现多个版本的同一文档,只引用 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 侧提出明确的需求:版本号是审计与回滚的前提,缺失它意味着无法回答”三个月前这条制度是什么”,这在合规场景里是硬缺口。
关键点
- 索引是派生视图,不是事实来源。 必须有权威侧的版本清单,且失效由权威侧触发,而不是让检索侧自己判断新鲜度。
- 语义缓存的相似度阈值只管”命中”,不管”新鲜”。 新鲜度必须由缓存键中的版本维度保证,两者不能混用。
- 切片 ID 与引用锚点必须在切片阶段就写入,且要稳定可推导。 事后补溯源等于重新做一次切片。
- 引用不应 404。 旧版本应保留可访问并标注”已被取代”,404 会直接摧毁用户对答案的信任。
- 一致性成本前置到低频的写入链路,查询侧只加一个版本过滤条件。
常见误区
- “把相似度阈值调高就能避免旧答案。” 错误。相似度衡量的是查询之间的接近程度,与知识是否更新完全正交。同一句话在换版前后的相似度可能接近 1.0,调阈值只会降低命中率、推高成本,对新鲜度毫无帮助。
- “让模型在生成时自己检查引用是不是最新版本。” 错误。这是把一个必须确定的不变量交给概率组件。模型既不知道当前的生效版本号,也无法可靠判断自己看到的材料属于哪个版本;版本判定必须落在检索过滤与缓存键上。
- “每天全量重索引一次,这个问题就自然消失了。” 错误。全量重索引解决不了切片 ID 不稳定与引用 404,也解决不了缓存陈旧;而且重索引窗口内新旧切片混杂,若无版本过滤,检索结果本身就是不确定的。
小型实践(30–60 分钟)
用本地一个 20 份文档的小知识库做一次端到端验证:
- 建一张
documents表(doc_id、version、content_hash、active)和一张chunks表(chunk_id = hash(doc_id, version, section_path)、char_start、char_end、text)。 - 写脚本把文档切成切片并写入两张表,给其中一份文档造 v1 与 v2 两个版本(v2 修改某一具体条款)。
- 实现两个查询路径:A 路径不带版本过滤,B 路径只查
active。分别提问同一条款,对比答案。 - 加一层”语义缓存”(用 dict 模拟即可),键先写成
(query),观察换版后仍返回旧答案;再把键改成(query, active_manifest_version),验证换版后自动失效。 - 输出一份对照表:旧方案的失效表现 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、没有重启。稳态一切正常,只在发布窗口出问题。
需要你回答的取舍问题。 要让”摘除端点”与”终止进程”严格有序,最直接的办法是在两者之间插入一段等待。这段等待会拉长发布时长。你如何在”发布速度”与”请求无损”之间取值?请给出可量化的依据。
先思考
- 当你删除一个 Pod 时,“从 Service 的 endpoints 里摘掉它”和”容器进程收到终止信号”这两件事,是同一时刻发生的吗?谁先谁后?
- 一个 Pod 处于
Running状态,是否就意味着它在接收流量?反过来,它被标记为Terminating时,是否就不再接收流量了? - 进程收到终止信号后立刻退出,那些”已经建立但还没返回”的长连接请求会怎样?客户端看到的是 502 还是别的?
- 即使 endpoints 已经摘掉了这个 Pod,Ingress 那一侧的连接池里可能还缓存着什么?摘除信息传播到它需要多久?
- 新 Pod 那边呢?它启动后多久开始接流量?如果它的依赖还没预热完成就开始接流量,会发生什么?
参考答案
结论:这不是应用崩溃,是”端点摘除”与”进程终止”两条异步链路的时序错位。Pod 被删除的时刻,它仍在 Service 的 endpoints 里,也会继续收到新请求;而进程一旦开始关闭,正在处理的请求就被中断。修复是三段式——启动侧用就绪探针保证新 Pod 真正可服务才入流量;终止侧用 preStop 等待端点摘除传播完成;进程侧收到信号后先排空存量请求再退出。
一、问题拆解。 502 尖峰来自三个独立缺陷,必须分别修:
- 终止侧时序错位(主因)。 删除 Pod 是一个”广播事件”:kubelet 开始终止容器,同时 EndpointSlice 控制器开始摘除端点,两者并行且异步。容器可能已经停止 accept,而 endpoints 里还留着它,于是新请求继续被转发到一个正在关闭的进程上。
- 上游存在陈旧视图。 即使 endpoints 已摘除,Ingress 的连接池仍持有指向该 Pod 的已建立连接,摘除信息传播到上游需要时间。这段窗口内上游还会继续转发。
- 启动侧可能也在贡献错误。 如果 Pod 没有配置就绪探针,它在容器启动后就会被加入 endpoints,而此时连接池、缓存、配置可能尚未预热完成——新 Pod 的前若干请求同样会失败。
二、核心方案。
- 启动侧:配置就绪探针。 就绪探针必须反映”真正能服务”的状态,而不是”进程活着”。Kubernetes 官方文档对就绪探针的语义是:探测成功则 Pod 被标记为 ready 并接收来自 services 的流量,探测失败则被标记为 unready 且不接收任何 service 的流量。因此探针应覆盖依赖预热、连接池建立、必要配置加载;
periodSeconds与failureThreshold要匹配真实预热时长。 - 终止侧:preStop 钩子插入等待。 容器的
preStop钩子会在容器进入Terminating状态之前运行。在其中sleep一段大于端点传播时延的时间(通常 5–15 秒,需实测确定),让摘除先传播完成,再让容器真正开始关闭。这是代价最低、框架无关的做法。 - 进程侧:优雅关闭 + 连接排空。 收到 SIGTERM 后:停止接受新连接 → 对 keep-alive 连接返回
Connection: close(HTTP/1.1)或发送GOAWAY(HTTP/2),主动告知上游不要再复用 → 等待存量请求完成,但必须设上界 → 退出。 - 优雅期限要算出来,不要抄默认值。 Kubernetes 默认授予的优雅终止期限是 30 秒。正确取值应满足:
terminationGracePeriodSeconds > preStop sleep + 最大排空时长 + 安全余量。本例最大请求 30 秒,若 sleep 取 10 秒,则期限至少应设为 45–50 秒,而不是默认的 30 秒。 - 上游与客户端兜底。 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 的终止策略同时受两种约束,最终两头都不满足。
关键点
- Pod 删除与端点摘除是两条并行的异步链路,容器可能在仍接收流量时就开始关闭,这是 502 的根因。
- 就绪探针决定 endpoints 成员资格:探测成功才接收 service 流量,失败则完全不接收。存活探针管的是重启,不影响 endpoints。
- preStop 在容器进入终止态之前运行,是在”摘除”与”终止”之间插入等待的唯一通用手段。
- 优雅期限必须按
sleep + 排空 + 余量计算。默认 30 秒在本例(最大请求 30 秒)下明显不够,期限一到 SIGKILL 会让所有等待作废。 - 上游连接池持有陈旧后端,服务端必须主动发信号(
Connection: close/GOAWAY)才能让其停止复用。
常见误区
- “配了存活探针就够了。” 错误。存活探针失败只会导致容器重启,它完全不影响 endpoints 成员资格。控制流量准入的是就绪探针,两者不能互相替代;只配存活探针的 Pod 在启动瞬间就会被加入 endpoints。
- “收到 SIGTERM 后立刻退出就是优雅关闭。” 错误。立刻退出会中断所有已建立但尚未返回的长连接请求,客户端看到的正是 502。优雅关闭的含义是”停止接收新的、完成已有的”,两者缺一不可。
- “把
terminationGracePeriodSeconds调大到 120 秒就安全了。” 错误。如果 preStop 与排空逻辑没改,容器仍会在收到 SIGTERM 后很快退出,多出来的期限只是闲置;而如果容器真的卡住不退出,120 秒又会让发布变得极慢。期限是约束,不是修复——它必须配套于 preStop 与排空,且按公式计算。
小型实践(30–60 分钟)
用本地 kind 或 minikube 复现并修复:
- 部署一个 Go/Python 服务,其中一个接口
sleep 20后返回,模拟长请求;Deployment 2 副本,前面挂一个 Service。 - 用
hey或wrk以恒定 QPS 打流量,同时执行kubectl rollout restart,统计 5xx 数量——应当能稳定复现错误。 - 依次加入三处改动,每次重复压测并记录:
- 只加 readinessProbe;
- 再加
preStop: exec: sleep 10; - 再显式设置
terminationGracePeriodSeconds: 45。
- 输出三列的对比表:5xx 数量、错误持续时间、发布总时长。用来回答”哪一处改动贡献了多少收益”。
延伸学习
- Kubernetes 官方文档 · Pod Lifecycle:优雅终止期限默认为 30 秒;
preStop钩子在容器进入终止态之前运行。本轮已核验这两条原文。链接:https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/ - Kubernetes 官方文档 · Configure Liveness, Readiness and Startup Probes:就绪探针语义原文为”探测成功则 Pod 被标记为 ready 并接收来自 services 的流量;探测失败则被标记为 unready 且不接收任何 service 的流量”。本轮已核验该句原文。链接:https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
L01 · LeetCode
题目 ID:
Q-20261019-L01| 一级类型:图的基础遍历 / 拓扑排序 | 难度:medium
题目
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1。
选修某些课程之前需要一些先修课程。先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] 表示:如果要学习课程 ai,则必须先学习课程 bi。
判断是否可能完成所有课程的学习?如果可以,返回 true;否则返回 false。
先思考
- 把每门课看成一个点、
[ai, bi]看成一条”从bi指向ai”的有向边。什么情况下”不可能完成”?用图论的语言描述它。 - 一门课什么时候可以开始学?这个条件只和它自己有关,还是和它的所有前驱有关?能否把”可以学的课”组织成一个不断补充的队列?
- 如果图里有环,环上的课会怎样?它们会不会进入你在第 2 步构造的队列?
- 如果最后剩下一些课从来没进入过队列,你能得出什么结论?反过来,如果全部都进入过,说明什么?
参考答案
题号 207 | 中文名:课程表 | 英文名:Course Schedule | 难度:Medium
官方链接(本轮已核验中英文页面,题号、标题、难度、示例与约束完全一致):
题目描述。 必须选修 numCourses 门课程(编号 0 到 numCourses - 1)。给定先修关系数组 prerequisites,其中 prerequisites[i] = [ai, bi] 表示学习 ai 之前必须先完成 bi。判断能否完成所有课程。
输入 / 输出。
- 输入:整数
numCourses,二维整数数组prerequisites。 - 输出:布尔值——能完成所有课程返回
true,否则返回false。
约束。
1 <= numCourses <= 20000 <= prerequisites.length <= 5000prerequisites[i].length == 20 <= ai, bi < numCoursesprerequisites中所有课程对互不相同
示例。
| 示例 | 输入 | 输出 | 说明 |
|---|---|---|---|
| 1 | numCourses = 2, prerequisites = [[1,0]] | true | 学 1 之前先学 0,可行 |
| 2 | numCourses = 2, prerequisites = [[0,1],[1,0]] | false | 互相为前置,形成环,不可行 |
考察的数据结构与算法模式。 有向图、入度表、拓扑排序(Kahn 算法)/ 环检测。
解题思路。 结论是:能否完成全部课程,等价于课程依赖图中是否存在有向环。
推导过程分三步:
- 建模。 每门课是一个节点;
[ai, bi]表示bi → ai的有向边(bi是ai的前驱)。 - 观察可学条件。 一门课只有在所有前驱都已完成后才能学。把”还剩几个未完成前驱”记为该节点的入度。入度为 0 的课当前就可以学。
- 剥离。 把所有入度为 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 dequefrom 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 个,均已在本地执行通过)。
| # | 输入 | 输出 | 覆盖点 |
|---|---|---|---|
| 1 | 2, [[1,0]] | true | 官方示例 1,简单链 |
| 2 | 2, [[1,0],[0,1]] | false | 官方示例 2,二元环 |
| 3 | 3, [] | true | 无依赖,全入度 0 |
| 4 | 3, [[1,0],[1,2],[2,1]] | false | 环外挂链,环上为 1 ↔ 2,课程 0 可完成但整体不可完成 |
| 5 | 4, [[1,0],[2,0],[3,1],[3,2]] | true | 菱形 DAG,合法拓扑序存在 |
关键点
- 建模方向:
[ai, bi]对应bi → ai,边指向”被依赖者之后的课”。 - 不变量:队列中的节点入度恒为 0,且每个节点至多入队一次。
- 终止判据是计数而非队列空:
completed == numCourses直接对应”是否有节点留在环里”。 - 环上入度永不为 0,这是算法能检测环的根本原因。
- 复杂度是线性的,来自”每节点一次、每边一次”,不是来自 BFS 层数。
常见误区
- 把边的方向建反后仍套用同一个解释。 反图本身也能检测环(入度换成出度即可),但混用会让推理过程自相矛盾。做法是:先固定”边 = 依赖关系方向”,再统一推导。
- 用”队列是否为空”作为成功判据。 队列最终总会空,无论有没有环。正确的判据是被剥离的节点数是否等于总课程数。
- 认为需要真的输出一个学习顺序。 本题只要求判定可行性;输出顺序是 210 题的要求。判定题不需要保存拓扑序,只需计数。
延伸学习
- LeetCode 210. 课程表 II(Course Schedule II):同一模型的自然进阶——要求输出任意一个可行学习顺序,即在 Kahn 算法中记录出队顺序;若最终数量不足则返回空数组。本轮未核验该页面正文,仅按题目编号与名称陈述。链接:https://leetcode.cn/problems/course-schedule-ii/
- 与 AI Agent 工程的联系(自然存在):依赖环检测是 Agent 编排里的常客——工具调用的前置关系、子任务分解出的 DAG、技能包之间的依赖声明,都需要在使用前判定”是否存在不可满足的循环依赖”。可迁移的能力是:把一个”每一步看起来都合理”的计划,还原成一张图,再用不变量(入度能否归零)判定整体可行性。
延伸资料
按”官方规范 → 原始论文 → 官方文档”优先级排列,均为本轮实际核验过的来源。
| 主题 | 资料 | 类型 | 核验状态 |
|---|---|---|---|
| B01 负缓存与 SOA TTL | RFC 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) | 官方文档 | 沿用此前核验结论,本轮未重新抓取 |
| L01 | LeetCode 207 中英文官方题页 | 官方题库 | 已核验:题号、标题、难度、两个示例、五条约束中英文一致;英文页通过率 52.4%(随时间变化) |
本轮未核验、不得作为事实引用的项: LeetCode 210 的题目正文;各语言运行时 DNS 地址缓存的具体默认值;RFC 1034/1035 的具体条文;Kubernetes 端点传播时延的具体数值(需实测)。
当日重点
- 不要把”上界”当成”保证”。 TTL 是最长缓存时间、上下文窗口是长度上限、
terminationGracePeriodSeconds是最长等待——三者都只对”合规的对象”生效,都不承诺全局同步。 - 定位陈旧状态要逐层问”这一层缓存了什么、由谁失效”。 DNS 是解析结果、语义缓存是答案、向量索引是知识的副本、Ingress 连接池是后端地址、KV 是注意力中间态——五者的失效主体完全不同。
- 新鲜度不能交给相似度,也不能交给模型判断。 版本必须是显式的、可比较的、由权威侧发布的;过滤与缓存键是确定性组件,不属于模型职责。
- 一致性成本前置到低频侧。 S01 把校验放在文档变更(日均数十次)而非查询(3.5 万次/日)上;S02 把等待放在发布(每天几次)而非每次请求上。同一条经济学。
- 修复要配对,不能只调参数。 调小 TTL 不解决连接池;调大优雅期限不解决时序错位;调高相似度阈值不解决知识陈旧。
- 缺失的评测维度就是盲区。 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-B01 | B01 | 非 Agent 基础 | 操作系统与网络 | DNS 解析、TTL、负缓存与解析结果的缓存层级 | AI-Agent、面试训练、操作系统与网络、DNS、TTL、负缓存、RFC2308、连接池 |
| Q-20261019-B02 | B02 | Agent 基础 | AI 与大模型基础 | Attention、KV Cache 与上下文窗口的容量与成本边界 | AI-Agent、面试训练、AI与大模型基础、Attention、KV-Cache、上下文窗口、Lost-in-the-Middle |
| Q-20261019-S01 | S01 | Agent 场景 | RAG 与搜索 / AI Agent 核心 | 知识更新滞后、语义缓存陈旧与引用溯源失效 | AI-Agent、面试训练、RAG与搜索、知识更新、引用溯源、语义缓存、版本化 |
| Q-20261019-S02 | S02 | 非 Agent 场景 | 操作系统与网络 / 软件工程与平台工程 | 优雅终止、就绪探针、端点摘除与长连接排空 | AI-Agent、面试训练、Kubernetes、优雅终止、readinessProbe、preStop、滚动发布 |
| Q-20261019-L01 | L01 | LeetCode | 图的基础遍历 / 拓扑排序 | 有向图环检测与入度剥离 | AI-Agent、面试训练、拓扑排序、环检测、BFS、LeetCode-207 |