23857 字
119 分钟
2026-11-02 AI Agent 面试训练
AI Agent 面试训练
2026-11-02

2026-11-02 AI Agent 面试训练#

今日学习主题#

有条件的承诺——保证的强度、生效条件与失效兜底。

今天五道题表面分属垃圾回收、技能分发、协议扩展、数据库备份与数组算法,实际都在问同一件事:某个东西声称能保证什么、这个保证在什么条件下成立、不成立时由谁兜底。

  • 垃圾回收只保证”不可达的内存终将被回收”,不保证”内存占用会下降”,也不保证”程序不再需要的内存会被释放”。
  • 技能清单只声明”有什么指令与配套文件”,不保证”模型会照做”,Beta 扩展甚至不保证下一版兼容。
  • 协议扩展的阶段(Experimental / Beta / Stable / Core)就是一份保证强度的分级表:阶段决定你能指望什么,而不是名字听起来有多正式。
  • 备份只保证”存在一份副本”,不保证”能恢复到某个时间点”,更不保证”恢复得回来”——归档链条上任何一段缺口都会让承诺失效。
  • LeetCode 31 的”下一个排列”也有一个显式的边界承诺:不存在更大排列时,必须回绕到字典序最小排列。

记忆口径:凡是保证,先问三件事——谁承诺、承诺到什么程度、失效时有没有确定性兜底。

题目速览#

编号题目 ID题目名称题型一级类型难度一句话考察点
B01Q-20261102-B01垃圾回收:可达性、并发标记清除与内存占用的取舍非 Agent 基础编程语言与运行时基础题(中高)GC 回收的是”不可达”而非”不再需要”,调优旋钮交换的是 CPU 与内存而不是把内存变小
B02Q-20261102-B02Skills over MCP 与本地技能目录的取舍Agent 基础上下文工程与能力封装基础题(中高)技能是”怎么做”的程序性知识,分发通道改变的是上下文成本、来源可信度与兼容性保证,不改变它不是安全边界
S01Q-20261102-S01官方或第三方 MCP 扩展的接入评审Agent 场景工具与互操作协议(兼软件工程与平台工程)场景题(高)成熟度阶段决定可指望的稳定性,pin 策略与 revision 错配检测是把”期望”变成”可判定事实”的机制
S02Q-20261102-S02备份、时间点恢复与恢复演练非 Agent 场景数据库与存储(兼软件工程与平台工程)场景题(高)备份的存在不等于可恢复,PITR 的承诺边界由基础备份时间、WAL 完整性与时间线三者共同决定
L01Q-20261102-L0131. 下一个排列LeetCode数组与字符串(原地变换)medium从右找升序对、交换、反转后缀;完全降序时回绕到升序

B01 · 非 AI Agent 基础题#

题目 ID:Q-20261102-B01 一级类型:编程语言与运行时 二级主题:Runtime、GC 与性能分析 题型:非 Agent 基础 标签:#垃圾回收 #可达性 #GOGC #内存限制 #性能分析

题目#

一个 Go 写的 Agent 网关服务部署在内存上限 2 GiB 的容器里。上线后观测到:堆占用长期稳定在 1.1 GiB 左右,GC CPU 占比约 18%,P99 延迟稳定。但每隔 3 到 4 天容器会被 OOM Kill 一次,重启后一切正常。

团队做了两次调整:

  1. 把 GOGC 从 100 调到 50,堆占用降到 700 MiB 左右,GC CPU 占比涨到约 32%,OOM Kill 仍然每 3 到 4 天发生一次。
  2. 改回 GOGC=100,同时设置 GOMEMLIMIT=1800MiB,堆占用被压在 900 MiB 附近,GC CPU 占比升到约 45%,但仍然偶发 OOM Kill。

请回答:

  1. 垃圾收集器到底保证了什么、不保证什么?
  2. 为什么”堆占用稳定”和”仍然 OOM”可以同时成立?
  3. GOGC 与 GOMEMLIMIT 分别交换了什么?为什么设置了内存限制还会被 OOM Kill?

先思考#

  • 先想清楚”垃圾”的定义:GC 判定一个对象是垃圾,依据的是可达性还是程序在语义上还需要它?这两者什么时候会分叉?
  • 再想”堆占用稳定”说明了什么:它说明存活集稳定,还是说明没有泄漏?
  • 然后想 GOGC 的作用位置:它是让内存变小的旋钮,还是决定”下一轮 GC 何时启动”的旋钮?如果目标堆大小 = 存活堆 + 存活堆与根集之和 × GOGC/100,那么调小 GOGC 换来的是什么?
  • 最后想容器内存上限与 GOMEMLIMIT 的差别:一个是内核的硬约束,一个是运行时的软目标。Go 运行时”看不见”的内存有哪些?

参考答案#

结论:GC 保证”不可达的内存终将被回收”,不保证”进程的内存占用会下降”,也不保证”程序语义上不再需要的内存会被释放”;GOGC 交换的是 GC 的 CPU 开销与堆内存开销,而 GOMEMLIMIT 是一个 best effort 的软限制,运行时为了不陷入 thrashing 会主动允许超限,因此它不能替代容器内存上限中的余量设计。

定义。 Go 官方 GC 指南对垃圾回收的定义是:

“At a high level, a garbage collector (or GC, for short) is a system that recycles memory on behalf of the application by identifying which parts of memory are no longer needed.”

判定方式是可达性:GC 从根(roots,例如局部变量与全局变量)出发遍历对象图,能被扫描过程发现的对象就是可达(reachable)的,即可存活。Go 的 GC 采用标记-清除(mark-sweep)技术:

“Go’s GC uses the mark-sweep technique, which means that in order to keep track of its progress, the GC also marks the values it encounters as live. Once tracing is complete, the GC then walks over all memory in the heap and makes all memory that is not marked available for allocation. This process is called sweeping.”

Go 的 GC 有四个容易被记错的性质:

  • 非移动(non-moving):官方原文 “We call a GC that moves objects in this way a moving GC; Go has a non-moving GC.” 因此不会有压缩(compaction)带来的碎片整理收益,也不存在转发指针。
  • 大部分并发:官方原文 “The Go GC, however, is not fully stop-the-world and does most of its work concurrently with the application.” 目的是降低应用延迟。
  • 并非由语言规范保证:官方原文 “Note that the existence of a garbage collector as described by this guide is not guaranteed by the Go specification, only that the underlying storage for Go values is managed by the language itself.”
  • Go 官方指南并未把 Go 的 GC 描述为分代式:指南通篇按 mark-sweep、non-moving、concurrent 描述,没有分代相关的表述。分代是另一类收集器的设计选择,例如 Oracle Java 21 调优指南对 G1 的定义是 “G1 is a generational, incremental, parallel, mostly concurrent, stop-the-world, and evacuating garbage collector…”,并明确 “Space-reclamation efforts concentrate on the young generation where it is most efficient to do so”。分代收集器的设计前提是弱分代假设(多数对象朝生夕死),Go 的 GC 并不依赖这个假设。

适用边界。 官方指南把最关键的一句说得很直接:

“How much memory is live at any given point in time is a property of the program, and not something the GC can directly control.”

也就是说,GC 的控制对象是”什么时候开始收集、花多少 CPU”,不是”存活集有多大”。一个对象只要还挂在某个可达的引用链上,无论程序在语义上是否还需要它,GC 都不会回收它。官方还指出对象死亡在语言层面不可观察:“The death of an object is not an observable event at the language level.”

简洁示例。 一个”永久增长”的 map 缓存:

var cache = map[string][]byte{}
func handle(key string) {
if _, ok := cache[key]; !ok {
cache[key] = heavyLoad(key) // 永不驱逐、永不过期
}
use(cache[key])
}

每个 cache[key] 都从全局变量 cache 可达,因此永远存活。堆占用”稳定在 1.1 GiB”只能说明存活集趋于饱和,不能说明没有泄漏。定位这类问题要看分配速率而不是堆占用:官方指南指出,堆 profile 的四种视图中,“For the purposes of reducing GC costs, alloc_space is typically the most useful view as it directly corresponds to the allocation rate.”

与相近概念的区别。

概念判定依据谁负责
不可达内存(真垃圾)从 roots 出发扫描不到GC 保证最终回收
可达但语义上无用(逻辑泄漏)只有业务语义能判断只能由程序显式释放引用
堆占用(heap size)存活堆 + 本轮新分配由存活集与 GOGC 共同决定
进程 RSS堆 + 栈 + 运行时结构 + 运行时看不见的内存只有部分受 GC 影响

第二组区别是 GOGC 与 GOMEMLIMIT:

  • GOGC 的官方定位是 “At a high level, GOGC determines the trade-off between GC CPU and memory.” 目标堆公式官方写作 “Target heap memory = Live heap + (Live heap + GC roots) * GOGC / 100”,并且明确 “The key takeaway is that doubling GOGC will double heap memory overheads and roughly halve GC CPU cost, and vice versa.” 所以调小 GOGC 是用 CPU 换内存,方向正确但代价明确,且对逻辑泄漏无效。
  • GOMEMLIMIT 是 Go 1.19 引入的软内存限制,官方定义为 “This memory limit sets a maximum on the total amount of memory that the Go runtime can use”,并且明确 “the memory limit is defined to be soft. The Go runtime makes no guarantees that it will maintain this memory limit under all circumstances; it only promises some reasonable amount of effort.”

典型应用(通用工程)。 在容器里跑带 GC 的运行时,正确的容量模型是:容器上限 = Go 运行时可知内存 + 运行时不知道的内存 + 余量。官方给出的经验值是 “In this case, a good rule of thumb is to leave an additional 5-10% of headroom to account for memory sources the Go runtime is unaware of.” 这些”运行时不知道的内存”包括 C 侧分配(cgo)、内核态为进程保留的页缓存与 socket 缓冲、以及线程栈等。因此 2 GiB 的容器里把 GOMEMLIMIT 设成 1800 MiB 看似留了 200 MiB,若实际存在 cgo 分配或大量并发连接缓冲,仍可能越界。

同时要理解软限制为何故意”软”:官方把持续 GC 导致程序无法取得进展的现象称为 thrashing——“This situation, where the program fails to make reasonable progress due to constant GC cycles, is called thrashing. It’s particularly dangerous because it effectively stalls the program.” 为了避免 thrashing,GC 对自己可用 CPU 时间设了约 50% 的上限:

“This limit is currently set at roughly 50%, with a 2 * GOMAXPROCS CPU-second window. The consequence of limiting GC CPU time is that the GC’s work is delayed, meanwhile the Go program may continue allocating new heap memory, even beyond the memory limit.”

官方解释了 50% 这个数字的来源:“the program will slow down at most by 2x, because the GC can’t take more than 50% of its CPU time away.” 这就是为什么案例中 GC CPU 占比升到 45%(接近 50% 上限)却仍然 OOM:GC 已经在拿满它能拿的 CPU,但分配速度超过了回收速度,运行时只能选择越过软限制,最终由内核的 OOM Killer 执行硬限制。

面试现场回答(约 90 秒)。 “GC 的保证只有一个:从根不可达的内存,最终会被回收。它不保证内存占用下降,也不保证程序语义上不再需要的内存被释放——存活集有多大是程序的属性,官方原文说这是 GC 不能直接控制的事情。所以’堆占用稳定但仍然被 OOM Kill’不矛盾:稳定的堆说明存活集饱和,典型的例子是一个永不驱逐的全局 map 缓存,它从全局变量可达,GC 永远不会碰它。

GOGC 和 GOMEMLIMIT 是两个不同的旋钮。GOGC 决定下一轮 GC 何时启动,本质是在 GC 的 CPU 开销和堆内存开销之间做交换,官方结论是 GOGC 翻倍则堆开销翻倍、GC CPU 大约减半,反过来也一样。所以把 GOGC 从 100 调到 50 换来 32% 的 GC CPU 是正常的,但它治不了逻辑泄漏。GOMEMLIMIT 是软限制,运行时只承诺 reasonable amount of effort,为了避免 thrashing 会主动允许内存越过限制,GC 能占用的 CPU 时间上限大约是 50%,配 2×GOMAXPROCS 的 CPU-second 窗口。

排查顺序我会这样走:先看 alloc_space 而不是堆占用,定位分配热点;再看 RSS 与 Go runtime 报告的内存之差,把 cgo、连接缓冲这些运行时看不见的部分算进去;容器上限要给这些留 5% 到 10% 的余量。真正的修复是对缓存加上容量上限与过期,让引用在语义失效的同时也变成不可达。”

关键点#

  1. 存活集是程序的属性,不是 GC 的旋钮。 官方原文:“How much memory is live at any given point in time is a property of the program, and not something the GC can directly control.” 因此”堆占用稳定”既不能证明没有泄漏,也不能证明内存安全。
  2. GOGC 交换的是 CPU 与内存,不是”把内存变小”。 官方结论:翻倍 GOGC → 堆开销翻倍、GC CPU 约减半;反之亦然。它治不了可达但无用的内存。
  3. GOMEMLIMIT 是软限制且刻意可超限。 官方:“the memory limit is defined to be soft”,“it only promises some reasonable amount of effort”;GC CPU 上限约 50%(2 * GOMAXPROCS CPU-second 窗口),超限是为了避免 thrashing。
  4. 容器上限是内核的硬约束,必须给运行时看不见的内存留余量。 官方经验值 5%–10%。Go 运行时报告的内存 ≠ RSS。
  5. 延迟与吞吐的取舍同样受 GC 频率支配,不只是并发与否。 官方列出 GC 的延迟来源之一是 “Scheduling delays because the GC takes 25% of CPU resources when in the mark phase”;而分代收集器(如 G1)明确不是实时收集器——“It tries to meet set pause-time targets with high probability over a longer time, but not always with absolute certainty for a given pause.”

常见误区#

  1. “调小 GOGC 就能降内存,没有副作用。” 错。官方公式与结论表明,降低 GOGC 会让 GC 更频繁,代价是 GC CPU 升高、应用可用的 CPU 减少;案例中 GC CPU 从 18% 涨到 32% 正是这个代价。更关键的是,对逻辑泄漏完全无效——对象仍可达。
  2. “设置了 GOMEMLIMIT,就不会 OOM 了。” 错。软限制只承诺 reasonable amount of effort,且官方明确说明为了避免 thrashing,运行时会故意让内存越过限制,同时把 GC 的 CPU 占用压在约 50% 以内。真正兜底的是内核 OOM Killer,它按 cgroup 硬限制执行。
  3. “Go 的 GC 是分代的,老对象会被移到老年代,所以调大 GOGC 没风险。” 错。Go 官方 GC 指南按 mark-sweep、non-moving、mostly concurrent 描述,没有分代表述;G1 才是 “generational … and evacuating”。把分代收集器的心智模型套到 Go 上,会错误地预期”晋升”与”压缩”行为的存在。

延伸学习#

  • Go 官方《A Guide to the Go Garbage Collector》(go.dev/doc/gc-guide,文中明确当前描述对应 Go 1.19):本文所有 Go 侧原文出处,含目标堆公式、GOGC 权衡结论、软内存限制与 thrashing、堆 profile 四种视图。
  • Oracle《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide, Release 21》G1 章节(docs.oracle.com/en/java/javase/21/gctuning/):分代收集器的对照样本,含 “G1 is the default collector.”、暂停时间目标不是实时保证、以及内存耗尽时回退 Full GC。注意:MaxGCPauseMillis 的默认值本轮未从该页取到,不作引用(未核验)。

追问#

追问:容器里已经设了 GOMEMLIMIT=1800MiB 和 2 GiB 的 cgroup 上限,为什么还会被 OOM Kill?如果只能改一个数字,你会改哪一个?

回答要点:

  1. 两个限制的强度不同:cgroup 上限是内核硬约束,GOMEMLIMIT 是运行时软目标,官方明确不保证在所有情况下维持。
  2. GOMEMLIMIT 只覆盖 Go 运行时可知的内存(Sys - HeapReleased,等价于 /memory/classes/total:bytes - /memory/classes/heap/released:bytes),官方因此建议额外留 5%–10% 余量给运行时不知道的内存来源。
  3. 即使如此,GC 也会为避免 thrashing 而允许越限(GC CPU 上限约 50%)。所以”设了软限制就不会 OOM”是错误预期。
  4. 若只能改一个数字,先降 GOMEMLIMIT 到留出真实余量(例如 1500–1600 MiB 并实测非 Go 侧内存),观察 GC CPU 是否逼近 50%;但这只是止血。
  5. 真正的修复在应用层:给缓存加容量上限与 TTL,让语义失效与不可达同时发生;否则所有旋钮都只是在推迟 OOM。

B02 · AI Agent / LLM 基础定义题#

题目 ID:Q-20261102-B02 一级类型:上下文工程与能力封装 二级主题:Agent Skills 规范、渐进式披露与上下文成本;Skills over MCP 与本地技能目录的取舍 题型:Agent 基础 标签:#Agent-Skills #渐进式披露 #Skills-over-MCP #上下文成本 #供应链

题目#

你们要给公司的 Agent 平台接入一套”财务对账”能力包,内容包含:SKILL.md(流程说明与约束)、若干参考文档、一个 scripts/reconcile.py。

团队有两个方案:

  • 方案 A:把技能包放进 Agent 运行时的本地技能目录,随镜像发布,启动扫描目录加载元数据。
  • 方案 B:把技能包作为 MCP 资源,通过 Skills over MCP 扩展由远端 MCP server 分发,Agent 运行时连接后按需读取。

请回答:

  1. Agent Skill 是什么?它的三层渐进式披露分别加载什么、成本如何?
  2. 两个方案在上下文成本、版本与更新节奏、来源可信度、离线可用性、兼容性保证上各有什么差异?
  3. Skills over MCP 当前处于什么成熟度阶段?这对”能不能上生产”意味着什么?
  4. 技能是不是安全边界?

先思考#

  • 先区分三件事:能力(能不能做)、程序性知识(怎么做)、证据(依据什么做)。Skill 属于哪一类?
  • 再想成本:如果每次会话都把技能正文全量塞进上下文,100 个技能会怎样?三层披露分别在哪一步省下 token?
  • 然后想分发:本地目录与远端 MCP 资源,谁来控制”我加载的是哪一版”?版本漂移会体现在哪里?
  • 最后想信任:技能包里带 scripts/,执行它等价于什么?谁为它的行为负责?

参考答案#

结论:Skill 是”怎么做”的程序性知识载体,靠三层渐进式披露把常驻成本压到元数据一级;本地技能目录与 Skills over MCP 的差别不在”能不能用”,而在上下文成本的落点、版本控制的归属方、以及兼容性保证的强度——后者当前是 Beta 且官方明确”destination not yet stated”,因此它是分发通道的选择题,不是安全边界的选择题。

定义。 Agent Skill 是一个目录化的能力包:以 SKILL.md(带 frontmatter 的指令文件)为入口,附带可选的参考文档与可执行脚本。它的核心价值不是”给模型一个新能力”,而是把流程性、程序性的知识从提示词里外置成一个可版本化、可按需加载的单元。

规范对 frontmatter 有硬约束:name 长度上限 64 字符,description 上限 1024 字符。这不是排版要求,而是成本约束——description 决定模型在未加载正文时能否正确判断”这个技能是否相关”,写得含糊就等于让路由失效。

三层渐进式披露与上下文成本。

层加载内容何时加载量级
第 1 层:元数据name + description常驻,所有技能都可见每个技能约百 token 量级
第 2 层:正文SKILL.md 主体判定相关后才加载建议控制在数千 token 量级
第 3 层:配套资源参考文档、脚本、模板执行中按需读取只进不出,读完即弃

需要记住两个边界:引用只有一层深(技能正文直接引用的文件会被发现,再往下的嵌套引用不保证被遍历);脚本本身不进上下文,被调用时才以执行结果的形式进入,这是技能成本远低于”把所有文档塞进提示词”的根本原因。

适用边界。 Skill 解决的是”知识外置 + 按需加载”,它不解决:

  • 能力本身:能不能调用某个系统,取决于有没有对应工具与权限,与有没有技能无关。
  • 执行保证:模型是否照做,是概率性的;技能是强提示,不是约束。
  • 安全边界:技能包里的 scripts/ 一旦被执行,等价于在 Agent 的运行时里执行任意代码。技能不是沙箱,也不是权限模型。

简洁示例。 财务对账技能的第一层:

---
name: finance-reconciliation
description: 按会计期间对账银行流水与内部订单,输出差异清单;仅在用户提供对账期间且具备财务只读凭证时使用。
---

description 里同时写清适用场景与前置条件,模型在未读正文时就能判断是否加载;若只写”财务相关技能”,则每次财务话题都会误命中,把第二层正文拉进上下文。

与相近概念的区别。

概念回答什么问题是否消耗常驻上下文是否概率性
Skill这件事该按什么流程做仅元数据常驻是(是否被遵守不确定)
Tool / MCP 工具能对这个系统做什么操作工具定义常驻(且顺序影响提示缓存命中)否(调用即执行)
长期记忆关于这个用户/租户我知道什么按需召回召回是检索行为,写入需闸门
MCP 资源一段可寻址的内容读取时才进上下文否(确定性读取)
提示词 / 系统消息这一轮怎么表现全程常驻是

关键区分:Skill 是知识的分发单元,Tool 是能力的执行单元。 一个技能可以”教”模型如何调用三个工具,但它不能授予调用权限——授权必须在模型之外的策略执行点判定。

两个方案的取舍。

维度方案 A:本地技能目录方案 B:Skills over MCP
上下文成本元数据常驻由本进程承担,正文从磁盘读,无网络往返元数据与正文都要经 MCP 资源读取,多一次网络往返与失败面
版本与更新随镜像/挂载发布,版本由部署流水线决定,回滚即回滚镜像由远端 server 决定,客户端读到哪一版取决于 server 与缓存,版本漂移风险更高
兼容性保证无协议依赖,只依赖运行时自身的加载规则依赖扩展规范:官方扩展页标注 Skills over MCP 为 “Beta, destination not yet stated”
来源可信度由构建与制品签名体系背书由 MCP server 的身份与连接背书,需要额外的来源验证与运行时审计
离线/隔离可完全离线,适合强隔离环境需要连通,适合集中管控、多客户端共享、频繁更新
适用范围单运行时、单租户环境多客户端、跨组织分发、集中更新

成熟度意味着什么。 MCP 官方扩展总览页把扩展生命周期分为三阶段(Experimental / Beta / Stable)并可晋升进核心,并明确规定:

  • “An extension starts as Experimental and must pass through Beta.”
  • “Each Beta extension states its intended destination, Stable or Core, which may change while it is in Beta.”
  • “An extension’s identifier does not change when its stage changes.”
  • Beta 阶段的变更规则:“prefer additive changes… Breaking changes are allowed but should be rare and batched, announced in the Working Group or Interest Group first, and recorded in a changelog. An extension that makes a breaking change should add or increment a revision string in its settings object, so that peers can detect a mismatch.”
  • 默认关闭:“Extensions are always disabled by default and require explicit opt-in from the developer, at every stage.”
  • Experimental 的 pin 要求:“Implementers should pin to a tag or commit, because two revisions of an Experimental extension need not be compatible.”

Skills over MCP 的官方描述为 “Discover workflow instructions and read supporting files through MCP resources.”,阶段标注为 Beta, destination not yet stated(与 Server Cards 相同;对比之下 MCP Tasks 标注为 “Beta, to Core”,OAuth Client Credentials、Enterprise-Managed Authorization、MCP Apps 为 Stable)。

工程含义:Beta 不等于不能上生产,但等于”你必须自己承担变更成本”。 具体做法是:在 settings 对象里读取并记录 revision 字符串,遇到未知或不匹配的 revision 拒绝启用并降级;把”技能被加载了哪一版”写进审计;把破坏性变更的入口收敛成一次受控升级而不是依赖自动跟随。

典型应用(在 Agent 系统中)。

  1. 控制常驻上下文的规模:技能数量可以从几十扩到上百而不炸上下文,因为常驻的只有元数据;这也意味着提示缓存的前缀里,技能元数据部分必须保持稳定顺序,否则每次重排都会击穿缓存。
  2. 把”流程”从提示词里外置:对账、报销、发布审批这类有固定步骤的任务,用技能承载步骤与约束,比写进系统提示词更容易做版本管理与回归评测。
  3. 集中分发与多客户端共享:Skills over MCP 的价值在于一处更新、多处生效,适合跨团队的规范类技能(如”事故复盘模板”)。
  4. 风险面同步扩大:技能包是可执行内容。来源验证(谁签的、哪一版)、运行时审计(加载了哪个技能、执行了哪个脚本)、以及把 scripts/ 放进沙箱,三者缺一不可。

面试现场回答(约 90 秒)。 “Skill 是程序性知识的载体,不是一个新能力。它的核心机制是三层渐进式披露:常驻的只有 name 和 description,量级在百 token 上下,所以规范才会对 description 做 1024 字符的硬约束——这一层写不好,路由就失效;判定相关之后才加载正文;参考文档和脚本是执行中按需读取的。引用只有一层深,这是很多人踩的坑。

本地目录和 Skills over MCP 的选择,我认为是分发通道的选择题。本地目录版本由部署流水线决定,回滚就是回滚镜像,可离线、可强隔离;Skills over MCP 的价值是一处更新多处生效、适合跨团队集中管控,代价是多一次网络往返、版本漂移由远端决定、来源可信度要额外背书。

关键在成熟度:官方扩展页把 Skills over MCP 标为 Beta,destination not yet stated,而 Tasks 是 Beta to Core,授权类和 MCP Apps 已经是 Stable。Beta 不是不能上生产,而是变更成本要自己扛:扩展默认关闭需要显式 opt-in,Beta 的破坏性变更要递增 revision 字符串让对端检测错配,Experimental 则必须 pin 到 tag 或 commit。所以我会在 settings 里读 revision,未知或不匹配就拒绝启用并降级。

最后一句:技能不是安全边界。包里的 scripts 一旦执行,等价于在 Agent 运行时里跑任意代码,所以来源验证、运行时审计、沙箱这三件事必须在外面做。”

关键点#

  1. 常驻的只有元数据,正文与资源按需。 规范的 name ≤ 64 / description ≤ 1024 是成本约束而非格式约束;description 决定未加载正文时的路由质量。
  2. 引用只有一层深。 技能正文直接引用的配套文件会被发现,嵌套更深的引用不保证被遍历,超出的内容要显式列出路径。
  3. Skill ≠ Tool ≠ 记忆。 技能教”怎么做”,工具决定”能做什么”,记忆存”知道什么”;授权永远在模型之外。
  4. Beta 是”保证强度”而非”成熟度的形容词”。 Skills over MCP 标注为 “Beta, destination not yet stated”;Beta 允许破坏性变更但须进 changelog 并递增 revision;扩展默认关闭、需显式 opt-in。
  5. 技能包是可执行内容,不是文档。 scripts/ 的执行等价于任意代码执行,来源验证与沙箱必须在能力之外提供。

常见误区#

  1. “把技能全量塞进系统提示词,效果更好。” 错。这直接放弃了渐进式披露唯一的收益——常驻成本与技能数量解耦。技能数一多,上下文被正文挤占,反而压缩了任务本身可用的上下文,且工具定义的顺序若随之变动还会击穿提示缓存。
  2. “Skills over MCP 是官方能力,所以可以当稳定依赖。” 错。官方扩展页明确标注为 Beta 且 destination not yet stated,并规定 Beta 阶段”破坏性变更是允许的,只是应当少见且批量”。正确做法是把 revision 当作可判定的契约字段,而不是指望版本号或语义化版本。
  3. “技能里写了’不要执行 X’,就不会执行 X。” 错。技能是提示性上下文,不是运行时策略。约束必须外置到工具网关的策略执行点、权限系统与审批流程;这与项目级约定文件只能”提示”而不能”约束”是同一类问题。

延伸学习#

  • MCP 官方《Extensions Overview》(modelcontextprotocol.io/extensions):扩展生命周期三阶段与变更审批权限表、Beta 破坏性变更与 revision 字符串要求、Experimental 的四条 ground rules(含 pin 到 tag/commit)、晋升 Beta 与 Stable 的门禁、以及各官方扩展当前阶段与去向。本轮已核验原文。
  • Agent Skills 规范站(agentskills.io/specification):name ≤ 64 / description ≤ 1024 的硬约束、三层披露的 token 量级与”引用一层深”限制。数值沿用 2026-10-14 的核验记录,本轮未重复抓取。

追问#

追问:技能数量从 20 个涨到 200 个,Agent 的首次响应变慢且成本上升,但每次会话实际只用到 1 到 2 个技能。你会怎么排查与优化?

回答要点:

  1. 先判定成本落在哪一层:如果 200 个技能的元数据全部常驻,是线性增长;如果连正文都被提前加载,说明渐进式披露没生效(常见原因是客户端一次性读取全部资源,或把技能正文拼进了系统消息)。
  2. 修正常驻层:只保留 name + description;正文在判定相关后才读;脚本与参考文档在需要时才读,读到的内容不回灌常驻上下文。
  3. 检查元数据质量:description 写得太泛会导致误命中,把不相关技能的第二层拉进来。要写清适用场景与前置条件,并对”命中但随后未使用正文”的比率做观测。
  4. 检查提示缓存:技能元数据在提示前缀中的顺序与内容必须稳定,任何重排或文案变更都会让前缀缓存失效,表现为 TTFT 上升与缓存命中率下降。这属于能力封装与成本工程的交叉点。
  5. 若仍不够,再考虑按租户/场景做技能子集路由(二级路由),而不是压缩 description——后者会直接损害判定质量。

S01 · AI Agent 真实研发场景题#

题目 ID:Q-20261102-S01 一级类型:工具与互操作协议(兼软件工程与平台工程) 二级主题:MCP 扩展成熟度生命周期、pin 策略、revision 错配检测、升级门禁与降级路径 题型:Agent 场景 核心考点:阶段判读与保证强度、契约的可判定化、确定性降级、能力三层归属 标签:#MCP扩展 #成熟度阶段 #revision #降级 #供应链

题目#

业务背景。 你们运营一个企业级 Agent 平台,客户可以把自己或第三方的 MCP server 接入进来,平台自身也依赖若干 MCP 扩展提供能力。

系统规模。 200+ 租户;注册 MCP server 40 余个(自建 11 个、第三方 30+);日均工具调用 120 万次;Agent 运行时有 Python 与 TypeScript 两套,各自独立升级 SDK。

当前架构与数据流。 Agent 运行时 → 统一工具网关(唯一的策略判定点与执行点)→ 各 MCP server。客户端在每个请求的 _meta["io.modelcontextprotocol/clientCapabilities"].extensions 中声明支持的扩展,服务端在 server/discover 响应的 capabilities.extensions 中声明。

本次要接入的三个扩展。

扩展官方标注阶段用途
io.modelcontextprotocol/tasksBeta,去向 Core长耗时工具调用改造
io.modelcontextprotocol/skillsBeta,destination not yet stated通过 MCP 资源分发技能与配套文件
com.vendor/doc-intel第三方 Experimental文档抽取增强

目标。 三个能力都要能上生产,且任何一次扩展侧的变更都不能变成线上事故。

限制条件。

  1. 不能要求 30+ 第三方 server 在同一天升级。
  2. 不允许把未成熟能力默认对所有租户开启。
  3. 变更窗口只在工作日白天,回滚必须在分钟级完成、且不能依赖重新发版。
  4. 合规要求:审计链上必须能回答”某次调用当时启用的是哪个扩展的哪一版”。

已发生的故障。 一次 SDK 例行升级后 6 小时内,文档抽取结果大面积错乱:第三方把 doc-intel 的 settings 字段重命名,客户端按旧语义解析,静默产出错误结果;同期 tasks 的 revision 从 1 跳到 2,部分客户端识别不出差异,继续按旧轮询语义工作,导致长任务既重复派发又被误判超时。

需要你做取舍的问题。 能力先进性与稳定性之间如何分级?pin 到固定版本与跟随主干之间如何选?协商失败时降级到什么、由谁判定?

先思考#

  • 先看阶段表:Experimental / Beta / Stable 三个阶段,各自”谁能改、改成什么样、实现者能指望什么”。这张表本质上是什么?
  • 再想故障的根因形状:两次事故都不是”网络失败”或”超时”,而是双方对语义的理解不同却都认为协商成功。这说明缺的是什么机制?
  • 然后想”版本”:为什么语义化版本号在这里不够用?revision 字符串与版本号的区别是什么?
  • 最后想降级:如果某个扩展不可用,业务上应该退化成什么?谁能做这个判定——模型、编排层,还是网关?

参考答案#

结论:这不是”要不要用某个扩展”的问题,而是”为每个成熟度阶段配一套确定性的准入、检测与降级机制”的问题。核心方案是三件事——按阶段分级准入(Experimental 必须 pin 且禁入生产数据面、Beta 需要 revision 白名单 + 灰度 + 可关闭开关、Stable 才允许默认开启)、把兼容性从”版本号约定”改成”可判定的 revision 契约 + 一致性场景门禁”、以及把降级判定放在模型之外的统一网关。

问题拆解。 两起事故的共同结构是:协商成功 ≠ 语义一致。客户端与服务端都声明支持同一个扩展标识符,却对 settings 的字段语义理解不同,而协议本身没有强制的”版本一致性”握手。因此要拆成四个子问题:

  1. 我依赖的到底是什么? 扩展标识符、阶段、以及该阶段承诺的稳定性强度。
  2. 我能不能判定”对端的这一版我验证过”? 需要把模糊的”支持某扩展”变成”支持某扩展的某个 revision,且我已跑过一致性场景”。
  3. 判不出来的时候怎么办? 必须有确定性的默认动作(拒绝启用并降级),而不是让模型或调用方”试一试”。
  4. 出问题时怎么回滚? 回滚必须是配置级开关,分钟级生效,不依赖发版。

核心方案:分级准入 + revision 契约 + 网关侧降级。

第一层——建立扩展台账并判读阶段。阶段的权威来源不是文档标题,而是规范所在的目录:官方扩展页明确 “Inside the repository, the directory that holds an extension’s specification shows its stage: specification/experimental/, specification/beta/ or specification/stable/”,晋升进核心或已结束的扩展移到 specification/archived/。台账要记录:扩展标识符、阶段、declared destination、已验证 revision 集合、负责团队、是否有官方 SDK 参考实现。

第二层——按阶段分级准入。官方对三个阶段的定义(“What implementers can expect”一列)就是准入策略的依据:

  • Experimental:官方原文是 “Nothing. The design may change or disappear.”,并且 ground rules 明确 “Implementers should pin to a tag or commit, because two revisions of an Experimental extension need not be compatible”。因此策略是:必须 pin 到 tag 或 commit;只能在非生产环境或按租户白名单试用;不得接触生产数据面;Core Maintainers 保留随时归档或移除的权力,所以要有”扩展消失”的兜底。
  • Beta:官方原文 “Real usage is welcome. Breaking changes are documented.”,且 “Each Beta extension states its intended destination, Stable or Core, which may change while it is in Beta”。策略是:允许进生产,但必须挂 revision 白名单 + 按租户灰度 + 可一键关闭;变更须经 CI 一致性场景验证后才把新 revision 加进白名单。
  • Stable:官方原文 “Behavior is fixed within a protocol revision.”,且 “A published revision is immutable”,发布形态为 specification/stable/<name>/2026-07-28/。策略:允许默认开启(仍须显式 opt-in),并按核心协议修订绑定。

第三层——把兼容性判定化。官方规定 Beta 阶段 “An extension that makes a breaking change should add or increment a revision string in its settings object, so that peers can detect a mismatch.” 因此:

  • 客户端从 settings 中读取 revision,与本地”已验证 revision 集合”比对。
  • 命中已验证集合 → 启用;未知、缺失或更高但未验证 → 拒绝启用,走降级路径并打点告警。
  • 关键:不得把”标识符匹配”当作”语义一致”。标识符在阶段变化时不变(“An extension’s identifier does not change when its stage changes.”),破坏性变更也不再更换标识符——官方明确 “Breaking changes keep the extension’s identifier. A new identifier … is used only when old and new behavior must coexist under one protocol revision.”(此规则取代了早期要求换标识符的约束)。所以标识符不能承担版本判定的职责。

第四层——确定性降级。官方规定:“If the other side’s capabilities omit extensions, or omit a given extension, treat that extension as unsupported and follow Graceful Degradation.” 降级矩阵要在网关里作为配置数据维护:

扩展不可用降级行为是否可自动
tasks退回同步调用,超时按三态处理(成功/失败/未决),长任务改由编排层的持久任务表承载是
skills退回本地技能目录同技能包;无对应本地包则该技能不可用,明确告知而非静默跳过是
doc-intel退回基础抽取器,并在结果中标记 degraded,由上层决定是否要求人工复核是,但需提示

执行流程。

  1. 准入评审:读官方扩展页与仓库目录判读阶段;第三方扩展额外要求提供规范文本、维护者、变更策略与至少一份参考实现。
  2. 契约固化:把该扩展的 settings schema 与关键行为写成一致性场景(含正常、字段缺失、未知 revision、对端 omit 四类)。
  3. CI 门禁:SDK 或服务端实现升级时跑一致性场景;新 revision 只有在场景全绿后才加入白名单。
  4. 灰度:先在 1% 租户、再 10%、再全量,每档观察窗口内对比成功率、P95 与错误构成。
  5. 上线观测:记录每次调用实际启用的扩展与 revision;监控 revision 分布、未知 revision 命中率、降级触发率。
  6. 回滚:关闭开关 → 全网生效,无需发版;开关状态与生效时间进审计。

技术选型与依据。

选择依据备选与代价
阶段以规范所在目录为准,而非文档标题官方明确目录承载阶段,文档标题会滞后只看扩展总览页会漏掉刚移动的扩展;需两者交叉核对
revision 作为契约字段官方规定它是让对端检测错配的机制,是唯一被规范化的可判定信号语义化版本号:规范未要求,且包管理器排序与依赖解析不适用于协议协商
Experimental 强制 pin 到 tag/commit官方 ground rule 原文要求跟随主干:会拿到不兼容修订,且失去安全修复的可控节奏
降级判定放在统一网关网关是唯一执行点,可做到确定性与可审计放在模型或编排层:判定变成概率性,且无法保证所有路径都经过
server/discover 结果按 ttlMs 缓存响应自带 TTL 与 cacheScope每请求重新发现:延迟与成本不可接受;无 TTL 的永久缓存:变更无法生效

方案取舍。

  1. 能力先进性 vs 稳定性:不追求一刀切。Tasks 标注 “Beta, to Core”,属于方向明确、值得投入;Skills over MCP 是 “Beta, destination not yet stated”,方向未定,投入要更轻(保持可替换);第三方 Experimental 只给白名单。取舍依据是declared destination,而不是功能听起来多诱人。
  2. pin vs 跟随主干:Experimental 必须 pin(无兼容性保证);Beta 采用”跟随但受控”——自动拉取新 revision 到预发,只有通过一致性场景才允许进生产白名单;Stable 才允许常规跟随。
  3. 统一策略 vs 按扩展分级:分级。统一策略会导致”要么全禁、要么全开”,把 Beta 的好能力与 Experimental 的风险绑在一起。
  4. 自动降级 vs 显式失败:读路径可自动降级并标记;写路径与高风险操作不得自动降级,必须显式失败或转人工——降级改变了语义,写操作上”静默换一种做法”是事故来源。
  5. 集中代理 vs 各客户端自行订阅:对不支持订阅或 revision 检测能力不足的客户端,由平台侧集中代理统一协商与降级,代价是代理成为新的关键依赖,需要高可用设计。

失败路径、边界条件与恢复。

  • 失败路径:对端 omit extensions 或 omit 该扩展 → 视为不支持 → 降级;revision 未知或不匹配 → 拒绝启用 → 降级;一致性场景失败 → 门禁拦截,不进白名单;SDK 自动升级引入不兼容实现 → 依赖锁定 + 升级流水线强制跑场景。
  • 边界条件:revision 缺失时按”未验证”处理(FAIL-CLOSED),不能按”等于最新版”处理;对端声明支持但运行时行为不符(如声明 tasks 却不支持轮询)→ 用行为探针判定,而不只看声明;扩展被归档或移除 → 台账里标记并强制走降级。
  • 超时:能力协商要有独立 deadline,不得与工具调用超时共用;协商超时按”不支持”处理并允许下次重试,而不是按”支持”放行。
  • 重试:协商失败不重试——它是确定性判定,重试不改变结果,只增加延迟;需要重试的是底层网络请求,且应有次数与总时长双上限加抖动。工具调用的重试另按错误分类(可重试 / 永久失败 / 未决)治理。
  • 幂等:扩展的启用与否不改变副作用的幂等要求;幂等键仍由参数指纹承担,放在工具层,与扩展解耦。降级切换期间同一逻辑操作可能走不同实现,幂等键必须跨实现一致。
  • 恢复:开关级回滚(分钟级,配置下发 + 版本号单调 + 哈希对账确认生效);若因降级导致结果质量下降,通过标记 degraded 让下游与人工复核介入;审计保留”当时启用的扩展 + revision + 是否降级”三元组,支持事后归因。

安全、成本、延迟与可靠性。

  • 安全:扩展声明不得作为授权依据——这与静态卡片不得作为安全或访问控制依据是同一原则,运行时值优先于声明。Experimental 扩展禁止处理生产数据与敏感租户数据;第三方扩展须过供应链评审(来源、维护者、发布渠道、依赖);审计必须记录实际生效的 revision,而不是”启用了某扩展”。
  • 成本:每个请求在 _meta 携带能力声明会增加少量带宽;server/discover 结果按 ttlMs 缓存可摊薄;一致性场景与灰度观察本身是成本,但远低于一次静默语义错乱的代价。
  • 延迟:协商走缓存,正常路径几乎无感;降级路径的延迟必须单独观测(降级后 P95 是否显著变差),不能与正常路径混在一个指标里。
  • 可靠性:扩展台账与白名单服务必须是高可用配置数据,且客户端要有 last-known-good 本地副本——配置服务不可用时应 fail-static(沿用上次已知良好的白名单),而不是 fail-open(放行一切)或 fail-close(全部停用)。

验证指标与上线后观测。

类别指标
协商健康度协商失败率、超时率、revision 未知/不匹配命中率(按扩展拆分)
降级健康度降级触发次数与占比、降级后成功率与 P95 差值、降级态持续时长
变更门禁一致性场景通过率、新 revision 从提出到进白名单的时长
归因能力审计三元组(扩展 + revision + 是否降级)覆盖率;事故回溯能否在 10 分钟内定位到具体 revision
回滚能力开关生效时长、哈希一致率、回滚后错误率回归曲线
业务影响按扩展拆分的错误率、因扩展变更导致的质量退化告警数

小型实践(30 至 60 分钟)。 写一个”扩展协商守卫”的最小实现:输入是服务端 server/discover 的 capabilities.extensions 与客户端本地的已验证 revision 白名单,输出三态决策(启用 / 降级 / 拒绝)与降级目标;再补四个一致性场景(字段缺失、未知 revision、对端 omit、声明支持但行为探针失败),把 FAIL-CLOSED 的分支全部走通。

三层能力归属。

  • 模型能力:不参与任何阶段判读、revision 校验或降级决策。模型可以”用”扩展,但不能决定”启不启用”。
  • Agent 编排能力:声明支持的扩展、选择降级后的执行路径、在结果中标记 degraded 并决定是否要求人工复核。
  • 确定性系统能力:阶段台账、revision 白名单与错配检测、一致性场景门禁、灰度与开关回滚、审计三元组记录。凡是”判定”都在这一层。

关键点#

  1. 阶段就是保证强度表。 Experimental 一栏官方写的是 “Nothing. The design may change or disappear.”;Beta 是 “Real usage is welcome. Breaking changes are documented.”;Stable 是 “Behavior is fixed within a protocol revision.” 准入策略应直接由这张表推导。
  2. 标识符不承担版本职责。 阶段变化不改标识符,破坏性变更也不再更换标识符(除非新旧行为须在同一协议修订下共存)。判定的唯一规范化信号是 revision 字符串。
  3. FAIL-CLOSED 是默认值。 revision 缺失、未知或未验证 → 拒绝启用并降级;对端 omit → 视为不支持。绝不能按”最新版”乐观处理。
  4. 降级判定必须在模型之外、执行点之前的统一网关。 写路径与高风险操作不得自动降级。
  5. 审计记三元组而非二元状态。 “扩展 + revision + 是否降级”才能在事后回答”那次调用到底按哪种语义执行的”。

常见误区#

  1. “Beta 就是快稳定了,按 Stable 用。” 错。官方明确 Beta 允许破坏性变更,只是应当少见、批量、提前在工作组公布并记进 changelog;且 “Each Beta extension states its intended destination, Stable or Core, which may change while it is in Beta”。Skills over MCP 当前就是 “destination not yet stated”。
  2. “用语义化版本号管扩展兼容性就够了。” 错。规范没有给扩展规定语义化版本号,只规定了 revision 字符串作为对端错配检测机制;包管理器的版本排序逻辑(以及日期版本号转语义化版本后的排序倒置问题)根本不适用于协议协商。
  3. “协商成功就等于两端语义一致。” 错。两起事故都发生在这个假设上。标识符相同、声明相同,settings 字段语义仍可能不同;因此除了 revision 契约,还需要一致性场景与行为探针。
  4. “降级就是静默换个实现继续跑。” 错。降级改变了语义,读路径可以在结果里标记 degraded 后继续,写路径与高风险操作必须显式失败或转人工,否则就是把一种静默失败换成另一种。

延伸学习#

  • MCP 官方《Extensions Overview》(modelcontextprotocol.io/extensions):三阶段表与各阶段”实现者能指望什么”、Experimental 四条 ground rules(含 pin 到 tag/commit)、Beta 破坏性变更与 revision 要求、Beta/Stable 晋升门禁(第二份 SEP + 两个独立且可互操作的实现 + 一致性场景)、晋升核心后的共存与退场规则、以及 “Extensions are always disabled by default and require explicit opt-in from the developer, at every stage.”。本轮已核验原文。
  • SEP-3392: Extension Lifecycle(modelcontextprotocol/modelcontextprotocol PR #3392,2026-10-29 合并,Status: Final):扩展生命周期的完整规范文本,含 Stages、Transition、Identifiers and compatibility、Promotion to Core、Conformance 各节。阶段判定与晋升规则的权威出处(依据见本目录 题库配置.md 2026-11-02 变更记录)。

追问#

追问 1:如果 Beta 扩展做了破坏性变更,但维护者忘记递增 revision,你怎么发现?

回答要点:

  1. 先承认 revision 是必要但不充分的条件:它让诚实的对端能被检测,但不能防止漏报。
  2. 用一致性场景做持续门禁,而不是一次性评审:字段存在性、类型、取值域、以及关键行为的端到端断言(例如 tasks 的轮询语义、取消语义)。
  3. 加行为探针:在生产以极低比例发送规范化探针请求,校验返回结构与语义是否仍符合已验证 revision 的行为。
  4. 用结果侧指标兜底:按扩展拆分错误率、降级后 P95 差值、以及”结果置信度异常”类信号;本案例中抽取结果错乱首先表现为下游校验失败率上升。
  5. 契约测试要覆盖”字段缺失”与”字段重命名”两类——后者在没有 schema 校验时会静默通过,因此必须对未知字段做严格策略(拒绝或告警,而非忽略)。

追问 2:一个扩展被晋升进核心规范之后,客户端还要不要继续声明它?什么时候可以停?

回答要点:

  1. 官方规则:当 Standards Track SEP 在协议修订 R 把扩展晋升为核心后,该特性对声明 R 及更高版本的请求即为核心行为,无论是否声明扩展。
  2. 对早于 R 的修订,扩展的最终修订仍然适用,因此仍支持旧协议版本的客户端应当继续声明该扩展,以便与旧对端协商。
  3. 停止声明的条件是对端不再支持任何早于 R 的修订,而不是”晋升了就可以删”;规范明确没有独立的 sunset 期。
  4. 工程落地:把”是否声明”作为按协商协议版本分支的逻辑,并在台账里记录每个扩展晋升进核心的修订号 R;不要靠人工记忆清理。
  5. 这与本 S01 的台账是同一份数据——阶段、declared destination、晋升修订号 R 应存在一起。

追问 3:第三方扩展的服务器在评审通过后偷偷改了实现,你怎么控制?

回答要点:

  1. 把评审从”一次性准入”改成”持续门禁”:一致性场景在第三方每次发布时由平台侧自动跑,失败即把该 revision 移出白名单。
  2. 契约之外加来源与发布渠道约束:明确维护者、发布产物签名、以及变更公告渠道;Experimental 扩展必须 pin 到 tag 或 commit,禁止跟随分支。
  3. 运行时可快速隔离:按租户、按 server、按扩展粒度的一键关闭开关,分钟级生效且不依赖发版;这是把”发现”变成”止损”的关键。
  4. 数据面限制:未成熟或来源不可控的扩展不得接触生产敏感数据;按租户白名单而非全局开启。
  5. 审计保留实际生效的 revision 与是否降级,使得事后能定位”哪些调用受过影响”,而不是只知道”今天关掉了某个扩展”。

S02 · 非 AI Agent 真实研发场景题#

题目 ID:Q-20261102-S02 一级类型:数据库与存储(兼软件工程与平台工程) 二级主题:备份、连续归档与时间点恢复(PITR);RPO/RTO 的工程口径与恢复演练 题型:非 Agent 场景 核心考点:备份的存在不等于可恢复;PITR 的承诺边界由基础备份、WAL 连续性与时间线三者共同决定;静默失败的归档 标签:#PITR #WAL归档 #RPO #RTO #恢复演练

题目#

业务背景。 一个 SaaS 订单平台,核心数据落在单个 PostgreSQL 集群(1.2 TB),主库加两个流复制从库。

系统规模。 日增 WAL 约 180 GB;订单表 9000 万行;日均写入 300 万条订单变更。

当前架构与数据流。 archive_command 把已写满的 WAL 段推送到对象存储;每周日凌晨 02:00 做一次基础备份;监控只检查”备份任务退出码是否为 0”和”对象存储里昨天有没有新文件”。恢复演练从未做过。

目标。 出现误操作或逻辑损坏时,能把数据恢复到出事前的那一刻,且尽量少丢掉出事之后的合法写入。

限制条件。

  1. RPO ≤ 5 分钟、RTO ≤ 60 分钟(对客承诺)。
  2. 恢复过程不能停主库、不能影响未受影响租户的读写。
  3. 恢复动作本身必须可回滚——恢复错了不能变成第二次事故。
  4. 合规要求:任何数据修复都要能给出受影响行的清单与前后值。

故障现象。 周二 14:32,一个批量脚本少写了 WHERE 条件,把 40 万行订单的金额改成了同一个错误值,16:40 被发现。团队立刻准备做时间点恢复,却发现:archive_command 从周一 03:00 起静默失败,到周二 05:00 才被修好,对象存储里存在约 26 小时的 WAL 缺口;最近一次成功的基础备份是上周日 02:00。

需要你做取舍的问题。 整库回退到误操作前一刻,还是保留误操作之后的合法写入?当 PITR 链条断裂时,用什么兜底?

先思考#

  • 先问承诺:RPO ≤ 5 分钟是谁在承诺?它由什么机制兑现?如果 WAL 归档断了 26 小时,“5 分钟”这个数字还成立吗?
  • 再想监控为什么没发现:退出码为 0 与”对象存储里有新文件”这两条检查,漏掉了哪一类失败?
  • 然后想恢复的下界:用一次基础备份能不能恢复到”这次备份还在进行中”的时刻?WAL 有缺口意味着什么?
  • 最后想”整库回退”的代价:把整库倒回 14:31,14:32 到 16:40 之间没有问题的那部分写入怎么办?

参考答案#

结论:备份的存在不等于可恢复。PITR 的承诺下界由三件事共同决定——基础备份的结束时间、从基础备份到目标时刻的 WAL 是否连续、以及时间线是否正确;本例中 WAL 断链使”恢复到 14:31”不再可能,正确做法是先把止血与影响集确定下来,再用”影子实例 + 定向回灌 + 审计补偿”替代整库回退,并把归档的成功而非执行纳入监控与演练。

问题拆解。

  1. 这次到底能恢复到哪个时间点? 这是第一性问题,必须先用事实回答,而不是先动手。
  2. 恢复目标应不应该包含误操作之后的写入? 整库回退一定丢掉这部分;定向修复可以保留。
  3. 当 PITR 链条断裂时靠什么兜底? 只剩应用层的可审计变更记录。
  4. 怎么保证下次不再静默失败? 监控的对象要从”任务执行过”改成”链条完整且可用”。

核心方案。

第一步:确定可恢复下界。 官方对连续归档的要求是:

“To recover successfully using continuous archiving (also called ‘online backup’ by many database vendors), you need a continuous sequence of archived WAL files that extends back at least as far as the start time of your backup.”

并且有明确的硬边界:

“The stop point must be after the ending time of the base backup, i.e., the end time of pg_backup_stop. You cannot use a base backup to recover to a time when that backup was in progress. (To recover to such a time, you must go back to your previous base backup and roll forward from there.)”

本例的时间线:基础备份在上周日 02:00 结束;WAL 缺口是周一 03:00 至周二 05:00。缺口位于基础备份之后、误操作之前,因此从基础备份重放时会在周一 03:00 处断掉——无法到达周二 14:31。可恢复的最晚时刻是周一 03:00 之前(严格说是缺口开始前的最后一个完整 WAL 段所对应的位置)。

于是得到一个残酷但必须说出口的结论:对客承诺的 RPO ≤ 5 分钟,在归档断链的 26 小时里已经失效,而且是静默失效。

第二步:止血与锁定影响集。

  1. 立即撤销批量脚本的执行权限并停掉相关调度,防止二次破坏。
  2. 保留现场:不要立刻重启、不要立刻 VACUUM、不要做可能改变现状的维护动作;先把当前集群的物理状态与受影响表的统计固化下来。
  3. 确定影响集:按脚本的执行时间与条件反查受影响的 40 万行主键,导出”当前错误值”清单。这一步是合规要求,也是后续回灌的核对基准。

第三步:选择恢复方式——不整库回退。

即便 WAL 完整,整库 PITR 到 14:31 也会丢掉 14:32 至 16:40 之间的合法写入(新订单、支付回调、状态流转)。正确做法是影子恢复 + 定向回灌:

  1. 在一台独立实例上恢复基础备份,配置 restore_command 从归档取 WAL,重放到目标点。
  2. 官方把停止点称为 “recovery target”,可选方式为 “by date/time, named restore point or by completion of a specific transaction ID”,并明确 “As of this writing only the date/time and named restore point options are very usable, since there are no tools to help you identify with any accuracy which transaction ID to use.” 因此在重要批量操作前应当主动创建命名还原点(pg_create_restore_point),而不是事后靠时间戳猜——时间戳的精度与事务边界不对齐,是这类事故里最常见的二次伤害来源。
  3. 在影子实例上导出受影响 40 万行的正确旧值,形成一份可核对的补丁集。
  4. 在主库上按主键做定向更新回灌,带幂等键与行级版本校验,更新前后行数与校验和必须对上。
  5. 全过程主库不停机,未受影响的租户无感。

第四步:归档断链时的兜底。

如果 PITR 只能回到周一 03:00,整库回退意味着丢掉约一天半的写入,业务上不可接受。此时唯一可靠的兜底是应用层的可审计变更记录:

  • 若系统有 outbox / 审计表 / 变更流水(记录变更前后的值与操作者),可以直接用它生成反向补偿。
  • 若只有”当前值”而没有”前值”,那么这 40 万行的旧值在技术上已经不可恢复——这正是”未演练的备份等于没有备份”的另一种形态:没有前值记录的写操作,等同于不可逆操作。
  • 兜底之后的动作是把”高风险批量写必须落 before-image”变成写入链路的硬性要求,也就是把一致性成本前置到低频的写入侧。

第五步:时间线与试错。 官方说明:

“Whenever an archive recovery completes, a new timeline is created to identify the series of WAL records generated after that recovery. The timeline ID number is part of WAL segment file names so a new timeline does not overwrite the WAL data generated by previous timelines.”

这带来两个工程约定:一是恢复要在一台独立实例上做,不要在生产实例上反复重放(每次恢复都产生新时间线,且可能覆盖原本可回退的路径);二是如果第一次恢复的目标点选错了,可以”return to step 1”重来,用不同时间线分支试错,而不是在原实例上覆盖。官方同时给出硬限制:“You cannot recover into timelines that branched off earlier than the base backup.”

执行流程。

  1. 止血(撤权限、停调度、保留现场)→ 2. 计算可恢复下界并对外同步真实 RPO → 3. 确定影响集(40 万行主键 + 当前错误值)→ 4. 影子实例恢复基础备份并配置 restore_command 重放 → 5. 导出正确值补丁集并与影响集核对 → 6. 主库定向回灌(幂等键 + 行版本校验 + 校验和核对)→ 7. 业务侧复核与对账 → 8. 复盘:归档监控、命名还原点、演练机制。

技术选型与依据。

选择依据备选与代价
用命名还原点而非时间戳官方明确只有 date/time 与 named restore point “very usable”;时间戳与事务边界不对齐仅用时间戳:边界行可能半更新,且需要反复试错
影子实例恢复 + 定向回灌保留误操作之后的合法写入,主库不停机整库 PITR 切换:丢数据、停机、且不可回滚
独立实例做恢复每次恢复产生新时间线;官方建议可反复试错在生产实例重放:可能覆盖可回退路径
归档命令拒绝覆盖已有文件官方:“Archive commands and libraries should generally be designed to refuse to overwrite any pre-existing archive file. This is an important safety feature…”允许覆盖:两个库写同一目录或重试逻辑出错时会静默损坏归档
高风险批量写落 before-image归档断链时唯一可用的兜底不落:不可逆,只能靠整库回退并接受数据损失

方案取舍。

  1. 整库回退 vs 定向修复:默认选定向修复。整库回退只在影响面无法界定(例如删库、表结构损坏)时才用,且必须接受数据损失并提前通知。
  2. 基础备份频率 vs WAL 存储成本 vs 恢复时长:官方给出直接的取舍关系——“Since you have to keep around all the archived WAL files back to your last base backup, the interval between base backups should usually be chosen based on how much storage you want to expend on archived WAL files. You should also consider how long you are prepared to spend recovering… the system will have to replay all those WAL segments, and that could take awhile if it has been a long time since the last base backup.” 每周一次全量意味着最长要重放 7 天 WAL,这与 RTO ≤ 60 分钟是冲突的——本例的备份频率与目标本就不匹配。
  3. RPO 严格 vs 成本:5 分钟的 RPO 要求归档接近实时且被监控;若做不到,就应如实下调承诺,而不是保留一个虚假数字。
  4. 在线修复 vs 停机修复:在线修复复杂度高但业务无损;停机修复简单但放大故障。选择依据是业务对可用性的容忍度与影响集是否可界定。

失败路径、边界条件与恢复。

  • 失败路径:WAL 缺口 → 重放无法跨越,只能回到缺口前;归档文件损坏 → 官方 “If recovery finds corrupted WAL data, recovery will halt at that point and the server will not start. In such a case the recovery process could be re-run from the beginning, specifying a ‘recovery target’ before the point of corruption so that recovery can complete normally.”
  • 边界条件:restore_command 必须对不存在的文件返回非零退出码——官方明确 “The command will be called requesting files that are not present in the archive; it must return nonzero when so asked. This is not an error condition.” 返回 0 会让恢复误以为拿到了空文件;同时被信号终止或 shell 错误(如 command not found)会导致恢复中止,这与”文件不存在”是两类失败,监控要区分。
  • 另一条容易漏的边界:官方提醒 “WAL archiving will allow you to restore any modifications made to the data in your PostgreSQL database, [but] it will not restore changes made to configuration files (that is, postgresql.conf, pg_hba.conf and pg_ident.conf)“,配置文件要走常规文件备份。
  • 逻辑备份的边界:官方明确 “pg_dump and pg_dumpall do not produce file-system-level backups and cannot be used as part of a continuous-archiving solution. Such dumps are logical and do not contain enough information to be used by WAL replay.”
  • 超时与重试:归档失败的重试必须不覆盖已有文件并保留失败计数;恢复过程的重放时间要有预估(重放量 × 单段耗时),超时不等于失败,但要能中断并从头重来。
  • 幂等:回灌更新必须用主键 + 幂等键,重复执行结果一致;补丁集本身要带校验和,导入前核对行数。
  • 恢复:回灌前先备份受影响行的当前值(即便它是错的),保证回灌动作本身可回滚;回灌后按影响集逐行核对,而不是抽样。

性能、可用性与验证指标。

  • 监控指标(这是本例最该修的地方):最新已归档 WAL 的年龄(归档延迟,而非”有没有新文件”)、pg_stat_archiver 的失败计数、基础备份完成时刻与耗时、归档存储的 WAL 连续性校验(定期从基础备份做一次真实重放)。
  • 演练指标:每月一次的恢复演练实测 RTO(从触发到可服务)、恢复到目标点的准确率(影响集前后值核对通过率)、演练环境与生产隔离证明。
  • 可用性:主库全程不停机;影子实例与生产网络隔离,只读,且不得写回生产归档目录(否则两个集群写同一归档目录,正是官方警告要防范的情形)。

验证方式与上线后观测。

  1. 归档链路:把”最新归档 LSN 年龄超过 5 分钟”做成 P1 告警,而不是”任务退出码非 0”。
  2. 可恢复性:定期(至少每月)从最近一次基础备份做一次真实恢复到独立实例并跑通业务校验脚本——官方在开工前就给了同样的建议:“you should set up and test your procedure for archiving WAL files before you take your first base backup”,并提醒 “You are advised to monitor the archiving process to ensure that it is working as you intend.”
  3. 修复正确性:40 万行逐行前后值比对 + 与财务侧的金额合计对账 + 受影响租户的抽样人工复核。
  4. 后续观测:回灌后 24 小时内对该表的写入错误率、对账差异数、以及因修复引发的重复更新告警。

小型实践(30 至 60 分钟)。 在本地起一个 PostgreSQL,配置 WAL 归档到另一个目录;做一次基础备份,写入若干行后故意执行一条错误的 UPDATE(不带 WHERE);在一台独立实例上用 restore_command 恢复到误操作之前的命名还原点;最后用主键比对的方式把受影响的几行定向回灌回主库,并把”归档目录里最新 WAL 的年龄”做成一个可告警的数值。

关键点#

  1. RPO 是链条最弱一环的属性,不是备份策略的属性。 本例 WAL 断链 26 小时,5 分钟的 RPO 静默失效;监控对象必须是”链条完整且可用”,不是”任务执行过”。
  2. PITR 的下界是基础备份的结束时刻。 官方:“The stop point must be after the ending time of the base backup”;且必须有一段连续的 WAL 覆盖到目标点。
  3. 整库回退是最后手段,默认应做定向修复。 整库回退必然丢掉误操作之后的合法写入;命名还原点让边界可控。
  4. 归档命令不得覆盖已有文件;restore_command 对缺失文件必须返回非零。 这两条都是官方明确的安全要求,违反会导致静默损坏或静默错误。
  5. 没有 before-image 的批量写,等同于不可逆操作。 归档断链时唯一可用的兜底是应用层的可审计变更记录。

常见误区#

  1. “备份任务退出码是 0,说明备份成功。” 错。本例正是这样失效的:归档命令静默失败,退出码检查与”目录里有新文件”都发现不了链条断裂。官方对归档监控的要求是 “monitor the archiving process to ensure that it is working as you intend”,并对未正确监控的后果给出警告——“the backup might not include all of the WAL files and will therefore be incomplete and not able to be restored.”
  2. “有 pg_dump 就够了。” 错。官方明确逻辑转储 “cannot be used as part of a continuous-archiving solution”,它不含 WAL 重放所需的信息;逻辑备份与 PITR 是互补而非替代。
  3. “恢复就是把整库倒回出事前那一刻。” 错。这会丢掉出事之后的所有合法写入,且不可回滚。应先界定影响集,优先做影子恢复加定向回灌。
  4. “恢复演练只需要验证数据库能启动。” 错。“能启动”只证明重放完成,不证明数据正确、不证明耗时达标、也不证明影响集界定正确。演练必须跑业务校验脚本并计时。

延伸学习#

  • PostgreSQL 官方文档《Continuous Archiving and Point-in-Time Recovery》(当前版本 18 的第 25.3 节):连续归档的四项优势、逻辑备份不可用于 WAL 回放、恢复目标三种方式与可用性限定、时间线与分支、以及归档命令不得覆盖已有文件与 restore_command 的返回约定。本轮已核验原文。
  • PostgreSQL 官方文档《Reliability and the Write-Ahead Log》与备份相关的监控章节:pg_stat_archiver 等归档状态视图的用法,可作为”归档延迟告警”的落点(具体视图字段本轮未逐字核验,实施前需以当前版本文档为准)。

追问#

追问 1:归档命令为什么必须拒绝覆盖已存在的归档文件?如果为了简化重试逻辑允许覆盖,会发生什么?

回答要点:

  1. 官方把这条定性为安全特性:“Archive commands and libraries should generally be designed to refuse to overwrite any pre-existing archive file. This is an important safety feature to preserve the integrity of your archive in case of administrator error (such as sending the output of two different servers to the same archive directory).”
  2. 允许覆盖的第一个风险是串号:两个集群(或一次恢复的影子实例与主库)写同一个归档目录时,同名 WAL 段被互相覆盖,链条看似完整实则混入了另一个集群的历史,重放结果不可预测。
  3. 第二个风险是重试语义错误:归档失败后重试若允许覆盖,会把”部分写入的坏文件”固化成正式归档文件;拒绝覆盖则让失败可观测、可告警。
  4. 正确的重试设计是:写临时文件 → 原子改名 → 若目标已存在则失败并告警,由人工或带审计的清理流程处理。
  5. 测试建议:官方也建议 “test your proposed archive library to ensure that it does not overwrite an existing file”。

追问 2:WAL 完整时,为什么仍然不直接把主库整库 PITR 到误操作前一刻?

回答要点:

  1. 整库 PITR 会把所有数据倒回目标点,包括那些在误操作之后正常产生的写入(新订单、支付回调、状态机流转)。本例的 14:32 到 16:40 之间有 2 小时合法写入,业务上不能丢。
  2. 整库回退通常伴随停机与连接切换,放大故障窗口;且在回退后发现问题(例如目标点选错)时,回退动作本身很难再回滚。
  3. 正确路径是在影子实例恢复,导出受影响 40 万行的正确值,再在主库做定向回灌,并带幂等键与行版本校验。
  4. 适用整库回退的情形是影响面无法界定(删库、表结构损坏、大范围逻辑污染)或影响集大到定向修复不可行,此时必须提前通知并接受数据损失。
  5. 为了让这条路径可行,重要批量操作前应主动创建命名还原点,而不是事后用时间戳逼近。

追问 3:恢复演练怎么做才算真的验证过?

回答要点:

  1. 判定标准不能是”数据库起来了”。官方在开工前就要求先测通归档流程(“test your procedure for archiving WAL files before you take your first base backup”),演练则要把恢复端到端跑完并计时。
  2. 演练必须包含三个断言:能到达目标点(影响集前后值符合预期)、耗时满足 RTO、以及恢复环境与生产网络隔离且不回写生产归档目录。
  3. 演练应至少每月一次,且每次覆盖不同的目标点类型(时间戳、命名还原点),避免只验证最好走的那条路径。
  4. 把演练结果量化并进看板:实测 RTO、影响集核对通过率、最近一次成功演练距今时长。
  5. 演练的副产品是暴露配置漂移:postgresql.conf、pg_hba.conf 不在 WAL 里,演练能验证它们是否另有备份——官方明确提醒 WAL 归档不覆盖这些配置文件。

L01 · LeetCode#

题目 ID:Q-20261102-L01 LeetCode 题号:31 中文名:下一个排列 英文名:Next Permutation 难度:medium 一级类型:数组与字符串(原地变换) 标签:#数组 #原地 #双指针 #字典序

题目#

整数数组的一个排列就是将其所有成员的某种排列顺序。数组的下一个排列是指其整数的下一个字典序更大的排列:如果把所有排列按字典序从小到大排成一列,那么”下一个排列”就是紧随其后的那个。如果不存在下一个更大的排列,则必须把数组重排为字典序最小的排列(升序)。

给你一个整数数组 nums,找出并原地修改为它的下一个排列。必须原地修改,只允许使用常数额外空间。

输入与输出。 输入 nums: List[int];无返回值,直接原地修改 nums。

约束。

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 100

示例。

输入输出
[1,2,3][1,3,2]
[3,2,1][1,2,3]
[1,1,5][1,5,1]

主要考察的数据结构或算法模式。 数组原地变换;从右往左寻找”第一个破坏单调性的位置”;反转构造最小后缀。

先思考#

  1. 想清楚”最小的更大排列”长什么样:要让整体增大得尽可能少,应该改动最靠右的哪一位?如果从最右边往左看,后缀是一段降序(非递增),这段后缀处在什么状态?
  2. 找到那个”可以被增大”的位置之后,应该拿后缀里的哪个数和它交换?为什么不能随便挑一个更大的?
  3. 交换之后,后缀要变成什么顺序才能让整体最小?在手上是降序的前提下,怎么在原地、常数空间内变成升序?
  4. 边界:如果整个数组都是降序,说明什么?此时”下一个排列”是什么?

参考答案#

题号 31|中文名 下一个排列|英文名 Next Permutation|难度 medium。

官方链接。

两页的题号、标题、三示例与两约束完全一致;英文页显示 Accepted 2,518,025 / 5.4M,Acceptance Rate 46.5%(会随时间变化)。注意:本轮抓取时中英文两页均未渲染难度标签,难度 medium 依据 LeetCode 标准分类记录,未直接核验。

题目描述(信息完整,不照抄原文长段)。 求数组的下一个字典序排列;不存在时回绕到升序;必须原地、常数空间。

解题思路(推荐方案)。

设数组为 a,长度为 n。

  1. 从右往左找第一个”升序对”:找到最大的下标 i 使得 a[i] < a[i+1]。等价说法:跳过最长的非递增后缀 a[i+1..n-1]。若找不到这样的 i(即整个数组非递增),说明当前已是字典序最大排列,直接反转整个数组得到升序,结束。
  2. 在后缀里找最小的更大者:后缀是非递增的,因此从右往左遇到的第一个满足 a[j] > a[i] 的位置,就是后缀中大于 a[i] 的最小元素。
  3. 交换 a[i] 与 a[j]。
  4. 反转后缀 a[i+1..n-1]:交换后后缀仍是降序(见正确性说明),反转后变成升序,即该后缀能达到的最小排列。

复杂度:三次线性扫描,时间 O(n),额外空间 O(1)。

正确性说明。

  • 第 1 步之后,后缀 S = a[i+1..n-1] 是非递增的,因此 S 已经是它在当前元素集合下的最大排列,任何只重排后缀的做法都无法让整体变大——必须增大 a[i]。
  • 第 2 步选的是后缀中大于 a[i] 的最小值:因为 S 降序,越往右越小,从右往左第一个 > a[i] 的元素正是这个最小值。若选更大的元素,得到的排列会不必要地偏大。
  • 第 3 步交换后,后缀仍保持非递增:设 j 是后缀中最右的满足 a[j] > a[i] 的位置,则 a[j+1] <= a[i](否则 j 不是最右);同时 a[j-1] >= a[j] > a[i]。因此 a[j-1] > a[i] >= a[j+1],非递增性质在接缝处成立。
  • 第 4 步把非递增后缀反转为升序,使后缀取到最小排列。此时 a[i] 增大了最小可能的量,后缀取到最小可能的值,因此整体是大于原排列的最小排列,即字典序的下一个。
  • 若第 1 步找不到 i,整个数组非递增,它是字典序最大排列,按题意回绕到最小排列,即反转全部。

时间复杂度。 O(n):找 i、找 j、反转后缀各至多 O(n)。空间复杂度。 O(1):原地交换与反转,不借助额外数组。

可运行的 Python 参考代码。

from typing import List
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
n = len(nums)
# 1. 从右往左找第一个升序对 a[i] < a[i + 1]
i = n - 2
while i >= 0 and nums[i] >= nums[i + 1]:
i -= 1
# 2 & 3. 若存在,在降序后缀中找最小的更大者并交换
if i >= 0:
j = n - 1
while nums[j] <= nums[i]:
j -= 1
nums[i], nums[j] = nums[j], nums[i]
# 4. 反转后缀;i == -1 时等价于反转整个数组
left, right = i + 1, n - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1

关键边界情况。

  1. 长度为 1:i 初值为 -1,跳过交换,反转区间 [0, 0] 不变,结果正确。
  2. 完全降序(字典序最大):i 停在 -1,反转整个数组得到升序,即题面要求的回绕。
  3. 完全升序:i = n - 2,只需交换最后两个元素,反转区间长度为 1,结果正确。
  4. 重复元素:两处判断必须用严格比较(nums[i] >= nums[i+1] 与 nums[j] <= nums[i]),相等时跳过;若写成 > / <,相等元素会被误判为可增大位置,产出错误结果。
  5. 最小值边界:nums[i] 可能为 0(约束允许),比较逻辑不依赖具体数值,只依赖大小关系。

测试用例。

#输入期望输出覆盖点
1[1,2,3][1,3,2]官方示例,完全升序
2[3,2,1][1,2,3]官方示例,完全降序回绕
3[1,1,5][1,5,1]官方示例,重复元素
4[1,3,2][2,1,3]后缀需反转多个元素
5[5][5]单元素边界
6[2,3,1,3,3][2,3,3,1,3]重复元素 + 反转区间
7[1,5,1][5,1,1]i = 0,反转整个后缀

上述用例已在本机执行并与暴力解(枚举所有排列取字典序最小的大者)交叉验证,全部通过。

变体或追问。 如果要求”第 k 个下一个排列”(k 很大),或者要求返回上一个排列(字典序更小的最大者),算法要怎么改?

回答要点:

  1. 上一个排列是完全对称的:从右往左找第一个 a[i] > a[i+1](跳过非递减后缀),在后缀里从右往左找第一个 a[j] < a[i](即小于 a[i] 的最大值),交换后反转后缀;若找不到则整个数组非递减、已是最小排列,回绕到降序。
  2. 重复调用 k 次的做法是 O(k·n),只在 k 很小时可接受;k 很大时应改用字典序排名第 r 的排列的阶乘进制(康托展开逆运算)直接构造,代价 O(n²) 或 O(n log n)(取决于实现),且不再需要”当前排列”这个输入。
  3. 追问常考的点是原地与常数空间:任何借助排序(sorted(nums[i+1:]))的实现都不满足题面约束,正确做法是反转,因为后缀已知是降序。

关键点#

  1. 改动点必须尽可能靠右:跳过最长非递增后缀,找到最右的”可增大位置” i。
  2. 交换对象必须是后缀中大于 a[i] 的最小者,而不是任意一个更大的元素。
  3. 后缀反转而不是排序:后缀已知降序,反转即升序,且是原地 O(1);用排序会违反空间约束。
  4. 比较必须用严格不等号,重复元素下相等必须跳过。
  5. i == -1 时反转整个数组,这是”不存在更大排列则回绕到最小”的边界分支,不能漏。

常见误区#

  1. 用排序代替反转。 错。题面要求原地且常数额外空间;sorted(nums[i+1:]) 会产生新数组。而且排序是多余的——后缀已经是降序,反转就够了。
  2. 从右往左找 j 时写错比较方向。 错。后缀是降序的,因此从右往左第一个 > a[i] 的元素才是”最小的更大者”;如果从左往右找,会拿到后缀中的最大值,产出偏大的排列。
  3. 把 i 的判定写成 nums[i] > nums[i+1](或把循环条件写成 nums[i] != nums[i+1])。 错。相等元素不构成”可增大位置”,必须用严格 <;否则 [1,1,5] 会给出错误结果。
  4. 忘记”完全降序”的回绕分支。 错。i 为 -1 时若不反转整个数组,就会原地返回,违反题面”必须重排为字典序最小排列”的要求。

延伸学习#

  • LeetCode 官方题 31(中英文页见上):题面、示例与约束的唯一权威来源;讨论区有官方的双指针图解。
  • LeetCode 46(全排列)/ 60(第 k 个排列):本题的自然延伸——从”求下一个”到”枚举全部”与”按排名直接构造”(康托展开逆运算),用于回答上面的变体追问。两页本轮未抓取,题号与主题按标准分类记录(未核验)。

该题训练的可迁移解题能力。 三个:一是从右往左找第一个破坏单调性的位置,这是处理”下一个 / 上一个 / 第一个违反约束处”类问题的通用手法;二是把结构性质(后缀降序)转化为 O(1) 的实现手段(反转即升序),而不是无脑诉诸通用算法;三是边界分支显式化——“不存在”不是意外,而是题面规定的回绕行为。

与 AI Agent 工程的联系(弱,仅类比)。 “改动点越靠右,保留的前缀越长”与提示缓存的布局原则形状相同:上下文里变化的部分必须放在末尾,固定部分(系统提示、工具定义、技能元数据)放在前面且保持稳定顺序,才能持续命中缓存。这与 B02 提到的”技能元数据顺序影响缓存命中”是一致的原则。此为类比,不构成算法与工程的直接依赖。

追问#

追问:为什么交换之后后缀一定还是降序?如果这一点不成立,反转就得不到最小后缀。

回答要点:

  1. 设 j 是后缀中最右的满足 a[j] > a[i] 的位置,则 a[j+1] <= a[i]——否则 j+1 也满足,j 就不是最右。
  2. 同时后缀本身非递增,所以 a[j-1] >= a[j] > a[i]。
  3. 合起来:a[j-1] > a[i] >= a[j+1],接缝两侧仍满足非递增,因此整个后缀保持降序。
  4. 正因为仍降序,反转后才一定是升序,也就是该后缀的最小排列;若这一步不成立,整个”最小增量”论证都会崩塌。
  5. 这个论证依赖”选最右的 j”,也正是为什么必须选”最小的更大者”而不是任意更大元素——两者是同一个条件的两面。

延伸资料#

按优先级整理,均为本轮实际抓取核验的一手来源。

主题资料类型核验状态
垃圾回收Go 官方《A Guide to the Go Garbage Collector》(go.dev/doc/gc-guide,文中声明描述对象为 Go 1.19)官方文档已核验:目标堆公式、GOGC 权衡结论、软内存限制与 thrashing、GC CPU 约 50% 上限、5-10% 余量建议、堆 profile 四视图
垃圾回收(对照)Oracle《HotSpot Virtual Machine Garbage Collection Tuning Guide, Release 21》G1 章节官方文档已核验:“G1 is the default collector.”、分代定义、暂停目标非实时保证、Full GC 回退。未取到 MaxGCPauseMillis 默认值,未引用(未核验)
MCP 扩展成熟度MCP 官方《Extensions Overview》(modelcontextprotocol.io/extensions)官方文档已核验:三阶段表与各阶段”实现者能指望什么”、Experimental 四条 ground rules(含 pin 到 tag/commit)、Beta 破坏性变更与 revision 要求、晋升门禁、晋升核心后的共存与退场、默认关闭与显式 opt-in
MCP 扩展生命周期规范SEP-3392: Extension Lifecycle(PR #3392,2026-10-29 合并,Status: Final)标准规范依据本目录 题库配置.md 2026-11-02 变更记录与官方扩展页指向,本轮未直接抓取原文
技能规范Agent Skills 规范站(agentskills.io/specification)官方规范数值(name ≤ 64 / description ≤ 1024 / 三层披露量级 / 引用一层深)沿用 2026-10-14 核验记录,本轮未重复抓取
备份与 PITRPostgreSQL 官方文档《Continuous Archiving and Point-in-Time Recovery》(当前版本第 25.3 节)官方文档已核验:连续 WAL 要求、停止点必须晚于 pg_backup_stop、命名还原点与时间戳的可用性限定、时间线、归档命令不得覆盖已有文件、restore_command 返回约定、配置文件不在 WAL 中、pg_dump 不可用于 WAL 回放
LeetCode 31LeetCode 中英文题目页(leetcode.cn / leetcode.com,题号 31)官方题目页已核验:三示例、两约束一致,英文页通过率 46.5%;两页均未渲染难度标签,难度 medium 未直接核验

标记为未核验、不得作为事实引用的内容:MaxGCPauseMillis 默认值;PostgreSQL pg_stat_archiver 视图字段;LeetCode 46 / 60 页面正文;Agent Skills 规范数值(沿用旧记录)。


当日重点#

  1. 保证的强度比保证的名字更重要。 扩展的 Experimental / Beta / Stable 三阶段,本质是一份”你能指望什么”的分级表:Experimental 一栏官方写的是 “Nothing. The design may change or disappear.”;Beta 是 “Real usage is welcome. Breaking changes are documented.”;Stable 才是 “Behavior is fixed within a protocol revision.” 判断一个依赖能不能上生产,先看阶段,再看功能。
  2. 标识符不承担版本职责,判定必须可计算。 扩展的阶段变化不改标识符,破坏性变更也不再更换标识符;唯一被规范化的可判定信号是 settings 里的 revision 字符串。凡是”看着对就放行”的地方,都要改成 FAIL-CLOSED 的显式比对。
  3. GC 只保证不可达内存被回收。 存活集是程序的属性,不是 GC 的旋钮;GOGC 交换的是 CPU 与内存,GOMEMLIMIT 是软限制且会为防 thrashing 主动越限。内存问题先看分配速率与运行时看不见的部分,再看堆占用。
  4. 备份的存在不等于可恢复。 PITR 的下界由基础备份结束时刻、WAL 连续性与时间线共同决定;监控对象必须是”链条完整且可用”而不是”任务执行过”,演练必须跑业务校验并计时。
  5. 技能是知识的分发单元,不是能力也不是安全边界。 三层渐进式披露让常驻成本只由元数据承担;description 是路由质量的决定因素;包里的 scripts/ 一旦执行即等价于任意代码执行。
  6. 降级与修复都优先”定向”而非”整体”。 扩展不可用按矩阵定向降级,写路径不自动降级;数据误操作用影子实例加定向回灌,而不是整库回退。两者的共同点是:把影响集界定清楚,比把系统倒回去更重要。

本次运行信息#

配置读取状态。 成功。文件 题库配置.md,version 8,last_maintained: 2026-11-02,文件修改时间 2026-11-02 10:37(由周维护产生,本次未修改该文件,本文件 frontmatter 记 question_bank_version: 8)。配置变更记录明确”下次每日出题(2026-11-02 当日任务若尚未执行则即时生效,否则自 2026-11-03 起)使用版本 8 配置”,本次为该版本生效后首次每日出题。

  • 启用中的基础题类型:13 个,全部 active;无暂停类型。
  • 观察中的新技术:8 项,均未标记”允许试学”,本次未从中出题。
  • 配额:非 Agent 基础 1、Agent 基础 1、Agent 场景 1、非 Agent 场景 1、LeetCode 1。

历史读取范围。 读取 题目索引.md(90 条 / indexed_runs: 16)、精选/README.md(4 条)、README.md。未逐字读取更早的训练正文。 目标目录与其父目录均无 AGENTS.md / CLAUDE.md。

去重结果。 30 天窗口为 2026-10-03 至 2026-11-02,窗口内 16 次训练共 90 题,逐条比对题目标题、一级类型、二级主题、核心考点、预期结论、场景约束与故障现象、LeetCode 题号、语义相似度,结论如下:

本题相近历史题目区分依据
Q-20261102-B01(GC)2026-10-29 B01 内存模型与内存可见性;2026-10-27 B01 事件循环与 I/O 模型前者考”顺序由谁建立”,后者考”并发与阻塞隔离”;本题考”回收的判定依据与 CPU/内存交换比、软限制为何会越限”
Q-20261102-B02(Skills over MCP)2026-10-14 B04 Agent Skills 三层披露与上下文成本;2026-10-20 B02 AGENTS.md 作用域;2026-10-30 B02 提示缓存前缀布局10-14 考单技能的成本与安全边界,本题考分发通道(本地目录 vs MCP 资源)的取舍与 Beta 阶段的兼容性保证,属新增二级主题
Q-20261102-S01(扩展接入评审)2026-10-26 S01 MCP 攻击面评审;2026-10-28 S01 MCP Tasks;2026-10-30 S01 工具清单缓存与变更通知那三题分别考”漏洞类别收敛""异步任务句柄与轮询""清单缓存一致性”;本题考可选协议能力的成熟度判读、pin 策略、revision 错配检测与升级门禁,是 v8 新增场景类型的首次出题
Q-20261102-S02(备份与 PITR)2026-10-15 S02 读写分离与读己之所写;2026-10-26 S02 分片键倾斜;2026-10-27 S02 事务性发件箱均为复制/投递/分布类问题;本题考备份链条的可恢复性边界与演练机制,窗口内无同类场景题
Q-20261102-L01(31)已用题号 146、3、215、739、875、124、207、142、547、560、39、1109、435、208、92、51631 未出现;算法模式为”原地变换 + 从右找单调性断点”,与最近的区间 DP、链表反转、Trie、贪心均不同

标注:工具与互操作协议在最近 5 次 Agent 场景题中出现 4 次(10-26、10-28、10-30、11-02)。 本次仍采用该类型,原因是 v8 配置新增的场景类型属于该一级类型且本次为其首次出题;四题的核心考点互不相同(见上表)。下次 S01 建议轮换到 AI Agent 核心、Agent 评测与可观测性或 RAG 与搜索,避免该类型继续密集出现。

覆盖变化。 上下文工程与能力封装 5 → 6(缺口”Skills over MCP 与本地技能目录取舍”已补);编程语言与运行时 5 → 6(缺口”GC 与性能分析”已补);工具与互操作协议 7 → 8;数据库与存储 8 → 9。

下一周期优先方向。 RAG 与搜索(5,缺口为 RAG 评测设计)、AI 安全与治理(4)、LLM 应用工程(4)、Agent 评测与可观测性(4,可转入二级主题轮换:成本与延迟的按任务归因工程、公开榜单污染);操作系统与网络(7,缺口为 CPU 与内存性能分析);数据库与存储(9,缺口为执行计划回归的上线门禁、分区表统计盲区)。

本次参考的历史题目或文件。 题目索引.md(90 条)、精选/README.md、README.md、题库配置.md。未读取任何训练文件正文;未修改任何历史文件。

到期精选提醒。 Q-20261012-B02(建议复习日 2026-10-19)、Q-20261013-B03(2026-10-20)、Q-20261015-B03(2026-10-23)、Q-20261015-S01(2026-10-24)均已过建议复习日,且精选 README 未记录复习完成。本次未自动执行任何精选或复习动作——精选只在收到明确指令时执行。

保存状态。 本文件写入本地 Obsidian 目录成功;题目索引.md 已同步更新。未做远端同步验证,不得认为已同步。

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