23827 字
119 分钟
2026-10-14 AI Agent 面试训练

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

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

今日主题:能力边界与隔离边界。今天 7 道题串一条主线——系统里哪些约束必须由确定性机制保证,哪些才能交给概率性组件。B01 讲容器 CPU 配额这类”看不见的硬约束”如何伪装成延迟问题;B02 讲契约(API)是唯一能对抗不可控调用方的手段;B03 讲 Agent 的自主度应该在哪里收紧;B04 讲能力封装的上下文成本与它不是安全边界;S01 把这几条合起来,落在多租户隔离这个必须靠确定性机制兜底的场景;S02 是纯通用工程侧的对称问题——在不停机的前提下改变系统契约;L01 用单调栈训练”把 O(n²) 的暴力扫描压缩成一次有序扫描”的思维。

题型分布(依据 题库配置.md v4):

编号题型一级类型二级主题题目 ID
B01基础(非 Agent)操作系统与网络容器 CPU 配额、CPU 限流与并发度错配Q-20261014-B01
B02基础(非 Agent)软件工程与平台工程API 与接口设计:兼容性、幂等与错误契约Q-20261014-B02
B03基础(Agent)AI Agent 核心Planning / ReAct / Reflection 与确定性 Workflow 的边界Q-20261014-B03
B04基础(Agent)上下文工程与能力封装Agent Skills 规范、渐进式披露与上下文成本Q-20261014-B04
S01场景(Agent)AI 安全与治理 / 工具与互操作协议多租户隔离、Hidden Context Exposure 与租户上下文传递Q-20261014-S01
S02场景(非 Agent)数据库与存储 / 软件工程与平台工程零停机 schema 变更:双写、回填与回滚Q-20261014-S02
L01LeetCode栈与队列(单调栈)下一个更大元素的等待距离Q-20261014-L01

配置与去重状态:配置 v4(last_reviewed: 2026-10-12)读取成功,无暂停类型,观察中的新技术未标记”允许试学”,本次未从中出题。去重窗口 30 天(2026-09-14 至 2026-10-14),覆盖 2026-10-10、10-12、10-13 三份训练文件。本次首次覆盖的一级类型:操作系统与网络、软件工程与平台工程(此前索引中标注”尚未覆盖”的两个类型)。今日无复习题——两道精选题的建议复习日分别为 2026-10-19 与 2026-10-20,尚未到期。


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

B01|基础定义题(非 Agent)|操作系统与网络|Q-20261014-B01#

容器 CPU 配额与 CPU 限流:一个服务 CPU 使用率只有 0.6 核,P99 却周期性地抖到 2 秒,可能是什么原因?请解释容器 CPU 配额(CFS bandwidth control)的语义、限流与 CPU 争抢的区别,以及在 Agent 系统中为什么会放大这个问题。

B02|基础定义题(非 Agent)|软件工程与平台工程|Q-20261014-B02#

什么是一次 API 的破坏性变更?在不允许破坏性变更的前提下,如何演进一个已经有多方调用的接口?请说明兼容性方向、幂等语义、错误契约和分页契约的设计要点。

B03|基础定义题(Agent)|AI Agent 核心|Q-20261014-B03#

ReAct、Planning、Reflection 与确定性 Workflow 各自的适用边界是什么?什么时候你不应该让模型自己生成计划?请说明每种模式的失败模式与控制手段。

B04|基础定义题(Agent)|上下文工程与能力封装|Q-20261014-B04#

Agent Skills 的渐进式披露具体分几层,各层的上下文成本是多少?Skills 与 Tool / MCP / 子 Agent / 记忆的区别是什么?为什么说 Skill 不是安全边界?

S01|真实研发场景题(Agent)|AI 安全与治理 · 工具与互操作协议|Q-20261014-S01#

多租户 Agent 平台出现跨租户数据泄漏:约 800 个企业租户共享一套 Agent 运行时、共享 MCP 工具服务(工单 / CRM / 文档库)、共享向量库与模型网关。日活会话 2 万,峰值 QPS 300,Agent 单次会话平均 6 次工具调用、3 次检索。

现象:租户 A 的用户在一次”帮我查上次签的框架协议付款条件”的对话里,收到了租户 B 的合同条款片段。事后链路追踪显示:入口网关拿到的租户身份是 A,MCP 工具请求头里的租户标识也是 A,但向量检索返回了 B 的文档 ID,且 LLM 语义缓存命中了一条由 B 触发的缓存项。

目标:在 P95 延迟增加不超过 50ms、不为每个租户单独部署(成本约束)、保留共享缓存收益的前提下,重建租户隔离边界,并满足合规审计要求(谁在什么时候以什么身份访问了哪个租户的数据)。

已知约束:MCP 2026-07-28 规范已移除协议级会话,租户上下文必须每请求自描述;其中约 60 个金融与医疗租户要求跨租户完全不可见;向量库已按 collection 混放历史数据,短期无法重新分片。

请给出定位思路、隔离方案、取舍理由与失败路径。

S02|真实研发场景题(非 Agent)|数据库与存储 · 软件工程与平台工程|Q-20261014-S02#

零停机 schema 变更:订单服务使用 PostgreSQL 15,主库 1 主 2 从 + 1 个离线分析从库,orders 表 8000 万行、日增 25 万行、峰值写入 1200 TPS。现在要把单列 address text 拆成结构化的 receiver_name / phone / province / city / district / address_detail,并让新代码只读写结构化列。

要求:全程零停机、任意阶段可回滚、不允许出现超过 5 秒的表级锁、回填期间主库写入不能失败、从库复制延迟不得超过 30 秒。

请给出分阶段方案、双写与回填的一致性处理、锁与长事务风险控制、回滚点与观测指标。

L01|LeetCode|栈与队列(单调栈)|Q-20261014-L01#

739. 每日温度 / Daily Temperatures(Medium):给定整数数组 temperatures,返回数组 answer,其中 answer[i] 是在第 i 天之后需要等待多少天才能遇到更高的温度;若之后不会再升高则为 0。


以下为完整答案与解析。建议先独立作答,再对照。


B01 容器 CPU 配额与 CPU 限流:Q-20261014-B01#

1. 考察点#

  1. 配额语义:cgroup 的 CPU bandwidth control 约束的是”每个周期内可消耗的 CPU 时间总量”,不是”可以并行跑几个线程”,更不是”独占 N 个核”。
  2. 限流与争抢的区分:throttling 期间线程被移出可运行队列、不消耗配额,因此表现为延迟突刺 + CPU 使用率封顶,这跟 CPU 争抢(排队等待)的曲线形状完全不同。
  3. 并发度错配:应用按宿主机核数推导线程池 / 进程池 / 连接数,而容器内实际配额可能只有 1—2 核。
  4. 观测归因:能否说出正确的指标(container_cpu_cfs_throttled_seconds_total、throttled periods 占比),以及”CPU 不高但延迟高”这一判据。
  5. 与 Agent 系统的连接:沙箱执行与批处理 worker 共享配额时,超时判定的可信度问题。

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

定义:Linux CFS bandwidth control 按周期给 cgroup 分配 CPU 时间预算。cgroup v1 用 cpu.cfs_quota_us / cpu.cfs_period_us,cgroup v2 用 cpu.max(形如 100000 100000)。Kubernetes 的 resources.limits.cpu: 1 翻译成每 100ms 周期 100ms 的 CPU 时间。当 cgroup 在一个周期内耗尽预算,内核会把该组所有可运行线程挂起,直到下一个周期重新充能,这段被挂起的时间就是 throttling。

三个必须说清的边界:

  • 它是带宽约束,不是并行度约束。1 核配额可以由 1 个线程跑满 100ms,也可以由 64 个线程在 1.5ms 内烧完然后被挂起 98.5ms。后者对外表现为”服务周期性假死”,而平均 CPU 使用率只有 0.6 核左右。
  • 它不提供任何下限保证。limits 只约束上限;requests 才决定调度装箱与 QoS 等级(Guaranteed / Burstable / BestEffort)。只设 requests 不限流风险,但节点超卖时会被挤压。
  • 它只管 CPU 时间。内存超限是 OOM kill(进程终止,不可恢复),I/O 用 io.max,三者机制不同,不能混为一谈,也不能用”加内存”解决限流。

3. 简洁示例#

宿主机 64 核,Pod limits.cpu: 1。Python 服务启动时 ThreadPoolExecutor(max_workers=os.cpu_count()) 得到 64 个线程(容器内 os.cpu_count() 读到的是宿主机核数,不感知 cgroup 配额)。日常 I/O 等待为主,线程大部分时间在阻塞,运行良好。某批任务触发一次 CPU 密集的文档解析前处理,64 个线程的 CPU 需求在 20ms 内吃满 100ms 预算,随后 80ms 全部线程被限流。

结果:container_cpu_usage_seconds_total 的均值约 0.6 核,看似健康;container_cpu_cfs_throttled_seconds_total 增速约 0.8 秒/秒,throttled periods 占比接近 80%;HTTP P99 从 80ms 涨到 1.9s,且抖动呈严格周期性(与 100ms 周期对齐),不是随机长尾。

4. 与相近概念的区别#

概念机制典型指标特征处置
CPU 限流(throttling)配额耗尽,线程被强制挂起CPU 曲线锯齿并封顶在 limits;throttled 秒数陡增降并发度、减少 CPU 密集路径、或提高 limits
CPU 争抢(oversubscription)可运行但排队等核run queue 变长、调度延迟上升;CPU 使用率高降密度、调 requests、亲和性
OOM kill内存超限,进程被杀容器重启、Exit 137提内存 limit 或降内存占用
背压 / 队列积压下游慢导致排队队列长度与等待时间增长,CPU 通常不封顶限流、扩容、异步化

limits vs requests:limits 决定上限与限流,requests 决定调度与 QoS。生产上常见的错误是只设 limits 不设 requests,导致 Pod 落到 BestEffort,节点压力时被优先驱逐。

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

  • 沙箱执行:Agent 生成的代码在受限容器里跑,给 0.5 核是常见配置。如果沙箱内的脚本起多进程编译或做向量计算,会瞬间烧完预算。此时”执行超时”必须区分两种语义:真死循环 vs 被限流的正常计算。判据就是 throttled 指标——否则你会错误地把超时时间从 10s 调到 60s,问题依旧。
  • 混部:在线 Agent 循环(延迟敏感、I/O 密集)与离线 embedding 批处理(CPU 密集)共享同一个 quota 时,批处理的突发会把在线请求的 P99 打穿。处置是把批处理放到独立 quota 的 Pool,而不是靠优先级调度(优先级无法对抗硬配额)。
  • 并发度配置:Agent 运行中同时调用多个工具的协程上限、连接池大小,都应基于容器配额推导,而不是 os.cpu_count()。Python 3.13 提供了 os.process_cpu_count(),但它反映的是 CPU 亲和性,仍然不感知 cgroup quota,不要误用。

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

  1. “CPU 使用率没打满就没有 CPU 问题”——限流的典型特征恰恰是 CPU 用不满。
  2. 把 limits.cpu: 4 当成”可以并行 4 个满速线程且无代价”——4 个线程会在每周期的前 1/4 时间烧完预算。
  3. 用 os.cpu_count() / nproc 决定容器内并发度——读到的是宿主机拓扑。
  4. 只看平均 CPU 判断容量——限流的伤害在 P99 与周期性抖动上,平均值会把信号抹平。
  5. 用”加超时 + 加重试”掩盖限流——重试会再消耗一次配额,形成”限流→超时→重试→更严重限流”的正反馈,这是最常见的恶化路径。

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

CPU 使用率低但 P99 周期性抖动,首先怀疑容器 CPU 限流。CFS bandwidth control 给的是每周期的 CPU 时间预算,比如 limits 设为 1 核就是每 100ms 只有 100ms 的 CPU 时间。它是带宽约束不是并行度约束:如果应用按宿主机核数起了 64 个线程,一次 CPU 密集的突发会在几十毫秒内把整个周期的预算烧完,剩下的时间所有线程被内核挂起,这就是 throttling。它的指标特征是 CPU 曲线封顶并呈锯齿、container_cpu_cfs_throttled_seconds_total 陡增,而平均 CPU 看起来很健康。

排查上我会先看 throttled 指标和抖动周期是否与 CFS period 对齐;定位后,处置优先级是:第一,把并发度从”宿主机核数”改成基于容器配额推导,比如显式配置成配额核数的 1—2 倍而不是 64;第二,把 CPU 密集路径从在线服务剥离到独立的 quota pool,因为混部时批处理的突发会直接打穿在线延迟;第三,确认 requests 是否设置,只设 limits 会让 Pod 落到 BestEffort,节点压力时被优先驱逐。

需要强调的是不会用加重试来解决——重试会再次消耗配额,让限流更严重。

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

追问:如果确认是 throttling,但成本约束不允许提高 limits.cpu,你还能做什么?

回答要点:

  1. 削峰(首选):限制同时进入 CPU 密集路径的并发数(信号量),让消耗曲线变平,而不是让它突发。总 CPU 时间不变,但摊平后不会在周期内被烧完,因此不触发限流。这是成本为零的处置。
  2. 调整 period 而不是调 quota(已核验,来源:Linux 内核文档 CFS Bandwidth Control)。这是一个常被忽略的杠杆:周期越长,突发能力越强。同样的 1 核平均带宽,quota=250ms / period=250ms 比 100ms / 100ms 允许的连续突发时间更长;反之短周期换来的是更一致的延迟,代价是突发能力降低。所以如果瓶颈是”短暂突发被拦腰截断”而非”总带宽不足”,可以在保持平均配额不变的前提下放大 period。另外内核还有 cpu.cfs_burst_us(cgroup v1),允许累积未使用的配额用于未来突发,默认 0(不累积)。
  3. 降单位成本:CPU 密集路径本身优化(换更快的解析库、批处理合并调用、减少序列化/反序列化、把纯 Python 循环换成向量化)。
  4. 迁移而非扩容:把该路径拆到独立 Deployment,用独立 quota 甚至按需 Job,避免与在线路径争抢同一预算。
  5. 接受并显式化:如果限流不可避免,就在 SLO 上分层——单独定该接口的 P99 目标,并在客户端设置合理 deadline,避免级联。
  6. 反模式要主动点名:把 limits 直接去掉是最省事的”解法”,但它把问题从”可观测的限流”变成”不可控的节点超卖”,会让整个节点的邻居一起抖动,通常不可接受。同理,加重试会再次消耗配额,形成正反馈,绝不能作为解法。

补充一个容易答错的细节:限流不仅来自本层级自己的配额,父级 cgroup 配额耗尽同样会让子级被限流(即使子级自己还有余量)。所以在 Kubernetes 上排查时,不能只看 Pod 层指标,还要看节点与上层 QoS 层级的情况。


B02 API 与接口设计:兼容性、幂等与错误契约 Q-20261014-B02#

1. 考察点#

  1. 两个兼容方向:向后兼容(新服务端 + 老客户端)与向前兼容(老服务端 + 新客户端)——后者在滚动发布期间是硬性要求,最容易被忽略。
  2. 破坏性变更的完整清单:不只是”删字段”。
  3. 幂等语义契约:Idempotency-Key、安全方法与幂等方法、重试红利由谁承担。
  4. 错误契约:稳定的机器可读错误码 vs 可变的消息、4xx/5xx 的可重试性、错误码不能靠字符串匹配。
  5. 分页契约:offset 分页在并发写入下的漂移、游标分页要求稳定且唯一的排序键。

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

破坏性变更是指:已有的、符合契约的调用方,在新版本下会出现行为改变或失败。判据不是”服务端代码改动大小”,而是老调用方能否继续正常工作。

一份务实的破坏性变更清单:

  • 删除或重命名字段、改变字段类型、改变单位或精度。
  • 收窄取值范围或增加新的必填参数(放宽是兼容的,收窄不是)。
  • 改变默认值语义(老客户端依赖旧默认值的路径会静默改变行为,这是最危险的一类,因为它不报错)。
  • 改变错误码语义或把细分错误合并成泛化错误。
  • 改变分页语义(如把 offset 翻页改成游标翻页、或改变默认排序)。
  • 同步改异步,或改变超时语义。
  • 扩展枚举值——对未做”未知值兜底”的客户端是破坏性的(老客户端穷举 switch 会直接抛异常)。

适用边界:兼容性契约约束的是跨进程边界。进程内的重构不算破坏性变更;内部服务与小流量服务可以承受更高的协调成本,但公开 API 与多方调用的中间层服务必须严格遵守。

3. 简洁示例#

把 user.status: "active" | "disabled" 改成 user.active: boolean——字段名与类型同时变,是破坏性的。合规的演进路径是三阶段:

  1. Expand:新增 active 字段,继续写 status,两个字段同时返回。老客户端读 status 不受影响。
  2. Migrate:推动所有已知调用方切换到 active,用网关日志统计 status 字段的读取来源与量级,作为下线的准入条件。
  3. Contract:读取来源归零后再移除 status;若无法确认,保留字段但标记为 deprecated 并写清移除时间。

对比一个”看似安全其实危险”的例子:给订单状态枚举增加 REFUNDING。服务端认为这是 additive,但老客户端的状态机 switch 没有 default 分支,直接抛异常或展示为空——所以枚举扩展必须配套约定”未知值容忍”:客户端必须能安全处理不认识的值(跳过、展示为”处理中”、或按最邻近语义降级)。

4. 与相近概念的区别#

  • 向后兼容 vs 向前兼容:向后兼容是”新服务端不破坏老客户端”,向前兼容是”老服务端能容忍新客户端的请求”。滚动发布期间新旧版本必然共存,所以两个方向都要满足,这也是为什么”先部署服务端再部署客户端”的假设在持续部署下不成立。
  • 兼容性 vs 一致性:兼容性是契约层面的演进能力,一致性是数据层面的正确性。双写期间两者都要处理,但手段不同。
  • 幂等 vs 去重:幂等是接口契约(同样请求执行多次与一次效果相同),去重是实现手段(幂等键表、唯一索引)。没有幂等契约的去重会漏掉跨节点的并发重复。
  • 版本化 vs 兼容演进:版本化是”承认不兼容并存多份契约”,兼容演进是”保持单一契约”。版本化不是免死金牌——多版本并存意味着安全补丁面、测试用例、监控口径全部翻倍。

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

工具的 JSON Schema 就是 API 契约,而模型是一个”不受你控制的调用方”,这三点决定了它比普通 API 更严格:

  1. 模型不会读变更日志。字段改名或删除,模型仍然按旧 schema 生成参数,失败表现为”工具调用参数校验失败”,而模型侧的应对常常是幻觉出别的参数名——错误会自我放大。
  2. 只能 additive:工具 schema 的演进应坚持”只增不删、必填只减不增、枚举扩展配默认值”,并在 description 里写清废弃字段。
  3. 错误契约要机器可读且可行动:返回 {"error_code": "INVALID_DATE_RANGE", "retryable": false, "hint": "..."} 让模型能自我修正;只返回 “参数错误” 会让模型重试同样的参数,浪费轮次。
  4. 幂等对 Agent 尤为重要:Agent 的重试由模型触发、可能来自不同轮次甚至不同会话,没有 Idempotency-Key 就无从去重,副作用会重复发生(下两次单、发两封邮件)。

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

  1. “只加字段就一定安全”——严格 schema 校验的客户端(如开启了 additionalProperties: false)会拒绝未知字段。
  2. 把错误消息当契约——消息文案国际化或改写后,靠字符串匹配的客户端全部失效。错误码必须是稳定枚举。
  3. 用 POST 表达一切——丢失了幂等与缓存语义,也让重试责任落到调用方猜测上。
  4. 把版本化当万能解——v1/v2 并存的成本是长期的,且老版本往往成为安全债的藏身处。
  5. 忽略滚动发布窗口——只验证”新服务端 + 新客户端”,没验证混合状态,这正是线上事故的常见触发点。

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

破坏性变更的判据不是改动大小,而是”符合旧契约的调用方在新版本下还能不能正常工作”。除了删字段、改类型这些明显的,有几类容易被漏掉:收窄取值范围、增加必填参数、改变默认值语义、合并或改变错误码、改变分页与排序、以及扩展枚举值——如果客户端穷举 switch 没有兜底,加一个枚举值就是破坏性的。

演进上我用 expand-migrate-contract 三阶段:先加新结构并保持旧结构可用(这一步要满足双向兼容,因为滚动发布期间新旧版本共存),再推动调用方迁移并用网关侧的字段访问日志作为下线准入,最后才移除旧结构。枚举扩展要配套”未知值容忍”约定。

配套契约还有三条:错误必须是稳定的机器可读错误码加上 retryable 标记,不能让调用方靠字符串匹配;写接口要有 Idempotency-Key,把重试红利放在服务端而不是让每个调用方自己实现去重;分页优先用不透明游标并要求稳定唯一的排序键,offset 分页在有并发写入时会漂移。

版本化我会当最后手段,因为多版本并存意味着测试、监控和安全补丁面全部翻倍。

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

追问:不允许任何破坏性变更,但业务上必须”重命名一个字段”,怎么做?

回答要点:

  1. 不要原地重命名。新增目标字段,双写双读,旧字段 deprecated 但保留,用访问日志确认迁移完成后再考虑移除。
  2. 写侧:新字段为唯一真源,旧字段由兼容层派生(可由服务端写入或由响应序列化层生成),避免双写不一致。
  3. 读侧:明确优先级——新字段存在则用新字段,否则回落到旧字段。回落路径要有监控计数,回落数归零才是移除条件。
  4. 约束放宽而非收窄:如果必须删旧字段,先在响应里把它标记为 deprecated 并加 Sunset / Deprecation 头,给明确的移除时间点。
  5. 对 Agent 工具 schema 的额外一点:模型不会读你的 deprecation 通知,所以要么永久保留旧字段作为别名,要么在 description 中把新字段描述得足够强以覆盖旧字段的触发条件,并保留一次参数校验层的自动改写(把旧参数名映射到新参数名),而不是直接报错。

B03 Planning / ReAct / Reflection 与确定性 Workflow 的边界 Q-20261014-B03#

1. 考察点#

  1. ReAct 的本质与代价:推理与行动交错,每步一次模型调用,轨迹长度随步数线性增长,错误沿轨迹累积。
  2. Planning 的价值与失效模式:计划漂移(plan drift)、何时触发重规划。
  3. Reflection 的收益边界:反馈信号必须是可验证的外部信号,而不是模型的自我感觉。
  4. 确定性 Workflow / 状态机:何时必须用它,而不是”让模型灵活一点”。
  5. 混合编排与护栏:最大步数、预算、leroy Jenkins 式无限重试的防护。

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

ReAct(Reasoning + Acting):模型交替输出思考与动作,动作结果回填上下文后再次调用模型,直到产出最终答案。它的力量来自”每一步都看到真实世界的反馈”;代价是步数 × 单次调用的全部上下文的成本与延迟,以及任何一步的错误都会被后续步骤当作事实继承。

适用:步骤数不可预知、每步都需要外部信息决定下一步,例如故障排查、开放域调研。

Planning(先计划后执行):先由模型产出任务分解,再逐步执行。它的力量是全局结构与可审查性(人可以在执行前看到并修改计划);失效模式是计划漂移——执行改变了世界状态后,原计划的前提不再成立,而系统没有重规划,于是沿着过期计划越走越远。

适用:步骤之间有明确依赖、可以预先枚举的任务;且必须有”重规划触发点”(每完成 N 步、每遇到意外结果、每执行完一个会产生副作用的动作后)。

Reflection / self-critique:让模型审视自己的产出并改进。它的收益严格依赖于是否存在可验证的反馈信号:

  • 有效:代码生成 + 单元测试失败信息、SQL + 执行报错、结构化输出 + schema 校验失败、事实陈述 + 检索到矛盾来源。这类反馈来自外部确定性系统。
  • 无效:让模型判断”这个回答是否准确""这个方案是否合理”而没有外部证据。这不是反思,是重新采样,模型会倾向于自我说服,且常常把正确的改错。

确定性 Workflow / 状态机:控制流由代码定义,模型只在特定节点承担”开放决策”(分类、提取、生成)。它可预测、可测试、可审计、成本可控。凡涉及权限、金额、不可逆副作用、合规留痕的步骤,都应固化成确定性节点。

适用边界总结:这是一个自主度连续谱,不是二选一。正确的问法不是”用 Agent 还是用 Workflow”,而是”这个子步骤的判断,模型做错了的代价有多大、能不能被确定性机制兜住”。

3. 简洁示例#

不该让模型自己规划的例子:报销审批。金额是否超阈值、审批链该走几级、能否自动通过——这些规则明确、错了要担责。正确做法是:用 Workflow 定义节点(提交 → 规则校验 → 分级路由 → 人工审批 → 支付),模型只负责从发票图片提取结构化字段(提取本身就是可抽取 admit 错误的任务)。如果让 ReAct Agent 自己决定”我要不要调用支付工具”,你就把金额上限的强制力交给了概率。

该让模型规划的例子:“这份季度财报里,导致毛利率下滑的主要因素是什么”。子问题无法预先枚举(要对比哪些科目、要不要查上期、要不要看行业数据),必须依赖中间结果决定下一步。这里用 ReAct + 有限步数(如 12 步)+ 预算上限,并要求每步引用来源。

4. 与相近概念的区别#

  • ReAct vs Function / Tool Calling:后者是能力接口(模型请求执行工具的协议),前者是控制流模式。Tool Calling 是在 ReAct、Planning、Workflow 里都能用的执行手段,二者不在同一层。
  • Planning vs 一次性任务分解 prompt:真正的 Planning 有独立的计划数据结构、可被人工修改、并在执行中被检查与重规划;一次性分解只是把”列出步骤”塞进同一次生成里,没有重规划入口。
  • Reflection vs 重试:没有新信息的重试不产生新结果,只是重复采样。反思必须伴随新的观测(校验器输出、工具报错、检索到的反例),否则它退化成”用同样的输入多摇几次骰子”,只增加成本。
  • Workflow vs Agent:不是产品类别的对立,而是同一条自主度轴上的位置。成熟系统的形态通常是 Workflow 骨架 + 局部 Agent 化,而不是全 Agent。
  • Reflection vs 投票 / self-consistency:后者是多路采样后聚合,利用的是多样性;前者是串行改进,利用的是反馈。两者对付的错误类型不同。

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

  • 护栏配置:max_steps(如 15)、max_tokens 预算、timeout deadline、每步动作白名单。这三个缺一不可——只限制步数不限制 token,一次超长输出仍然能打穿预算。
  • 反射点的位置:放在会产生不可逆副作用的动作之前,而不是之后。执行前审查是可撤销的,执行后审查只能补偿。
  • 可观测性要求:轨迹必须记录每一步的输入摘要、模型输出、工具结果、耗时与 token。没有轨迹就无法区分”模型选错了工具”与”工具返回了错误结果”,两者的修复方向完全不同。
  • 确定性兜底:金额计算、权限判定、状态机跃迁合法性校验必须放在模型之外的代码里。模型可以”建议”,不能”决定”。

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

  1. “ReAct 步数越多越聪明”——步数与成本、延迟、错误累积都成正比。真正的优化方向是提高单步信息密度(更好的工具、更好的检索),而不是增加步数。
  2. 用 Reflection 修复事实性错误——没有外部证据的自我批判只是自我说服,还可能把正确答案改错。
  3. 把本可以确定性实现的逻辑放进模型——金额四舍五入、权限校验、日期解析。这类逻辑一旦由模型承担,就无法给出单测覆盖与审计结论。
  4. 没有最大步数与预算——故障表现通常是”用户等了 3 分钟然后超时”,且账单已经产生了。
  5. 计划一次性生成后从不重规划——尤其在会修改外部状态的任务里,这是 plan drift 的直接成因。
  6. 把 Reflection 当成永动的自我改进循环——没有终止条件和收敛判据的反思会无限自我否定。

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

这四种不是并列的产品选项,而是同一条”自主度”轴上的不同位置,加上不同的控制流形态。选哪一种,取决于这个任务的步骤能不能预先枚举、以及某一步做错的代价有多大。

我的默认判断顺序是这样的:凡是涉及权限、金额、不可逆副作用、合规留痕的步骤,先用确定性 Workflow 固定下来,模型只在这些节点之间承担开放决策,比如从发票里提取字段、判断用户意图分类。理由是这些步骤错了要担责,而概率性组件无法给出确定性的保证。

对于确实无法预先枚举步骤的任务,比如分析财报找出毛利率下滑的原因,我用 ReAct,但必须配三个护栏:最大步数、token 预算、整体 deadline,缺一个都不行。如果有明确的全局结构,就先让模型出计划,人在执行前可以审查和修改——但一定要有重规划触发点,否则会出现计划漂移:执行改变了世界状态,原计划的前提不成立,系统却继续沿着过期计划走。

Reflection 的边界我要特别强调:它只在有外部可验证反馈时才有效——测试失败、schema 校验失败、检索到反例。没有新信息的”请检查你的回答有没有问题”不是反思,只是重新采样加自我说服,只会增加成本还可能把对的改错。另外反思点要放在不可逆动作之前,执行后的审查只能做补偿,成本高得多。

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

追问:具体到一个流程里的某个步骤,你怎么判断它应该”固化成代码”还是”交给模型”?

回答要点:

  1. 错判代价:错误是否可逆、是否有经济或合规后果。不可逆 + 有后果 → 代码。
  2. 正确性是否可判定:如果能写一个校验器(单测、schema、规则)在运行时判定对错,模型可用(因为错误会被确定性机制捕获);不能判定的开放判断 → 模型,但要有人工兜底。
  3. 输入空间是否闭合:输入枚举有限且规则稳定(如状态码映射、税率表)→ 代码,且要放进配置而非提示词;输入自由度高、需要语义理解(如从非结构化文本抽取原因)→ 模型。
  4. 变更频率与成本:规则每周变一次且由非工程人员维护 → 用配置/DSL + 模型做语义层,而不是硬编码也不是纯提示词。
  5. 延迟与成本约束:高频路径上的每一步模型调用都会乘以 QPS,通常要把高频的简单判断下沉到代码。
  6. 明确的反例:不要为了”更智能”把字符串格式校验、数值比较放进模型;也不要为了”确定性”让代码去理解用户为什么不满。

B04 Agent Skills 的渐进式披露与上下文成本 Q-20261014-B04#

1. 考察点#

  1. SKILL.md 的结构与 frontmatter 硬约束(字段名、长度、命名、与目录名的关系)。
  2. 渐进式披露的三层结构与各层 token 量级。
  3. 上下文成本的真实来源,以及如何为一支技能目录做预算。
  4. Skill 与 Tool / MCP / 子 Agent / 记忆的区别(这是最常被答混的地方)。
  5. 为什么 Skill 不是安全边界,以及由此引出的供应链风险。

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

Agent Skills 是一个开放格式:一个技能就是一个目录,至少包含 SKILL.md,可选 scripts/、references/、assets/。SKILL.md 由 YAML frontmatter 加 Markdown 正文组成。

frontmatter 约束(已核验,来源 agentskills.io 官方规范):

字段必需约束
name是1—64 字符;仅小写字母、数字、连字符;不能以连字符开头或结尾;不能有连续连字符;必须匹配父目录名
description是1—1024 字符,非空;必须同时说明”做什么”和”什么时候用”,并包含有助于识别任务的具体关键词
license否许可证名或指向打包的许可证文件
compatibility否≤500 字符,说明环境要求(目标产品、系统包、网络访问);大多数技能不需要
metadata否任意键值映射,建议键名加前缀避免冲突
allowed-tools否空格分隔的预批准工具串;实验性,各实现支持度不同

渐进式披露三层(官方给出的量级):

  1. 元数据层(约 100 tokens/技能):所有技能的 name + description 在启动时全量加载,用于让模型判断”要不要用这个技能”。
  2. 指令层(官方建议 SKILL.md 正文 < 5000 tokens、文件 < 500 行):技能被激活后整份 body 加载。注意官方原话是”一旦决定激活,会加载整个文件”——所以 SKILL.md 的长度直接等于激活成本。
  3. 资源层(按需):scripts/、references/、assets/ 中的文件只在需要时读取。官方要求引用层级保持一层深,避免深层嵌套引用链导致模型读不到关键文件。

适用边界:Skill 承载的是程序性知识(how to),是加在模型上的行为约束与操作步骤。它不返回结果、不产生副作用、不提供能力。这一点决定了它不能替代工具。

3. 简洁示例#

一个合规的 PDF 技能与它的成本模型:

pdf-processing/
├── SKILL.md # name: pdf-processing; description: 提取 PDF 文本与表格…
├── references/
│ └── FORMS.md # 表单字段格式,仅处理表单时读取
├── scripts/
│ └── extract.py # 具体提取脚本,按需读取后执行
└── assets/
└── template.json # 输出模板

成本计算:假设目录里挂了 40 个技能。启动时常驻 = 40 × ~100 tokens ≈ 4k tokens,这是固定税。用户请求触发 PDF 技能后,加载 SKILL.md 正文 ≈ 2k tokens。只有确需表单字段规范时才读 FORMS.md ≈ 1.5k tokens。

关键推论:真正昂贵的是 body 与误加载的 references,不是元数据。所以优化顺序是——先压 SKILL.md 正文(超过 500 行的必须拆分),再保证 references 粒度足够小且被明确指路(写明”处理表单时才读 references/FORMS.md”),最后才考虑减少技能数量。直接砍元数据省下的 token 最少,却牺牲了发现准确率,通常是最后一个选项。

4. 与相近概念的区别#

概念承载内容是否返回结果是否副作用类比
Skill程序性知识(怎么做)否,它改变模型的行为分布否(但脚本执行有)员工手册 / SOP
Tool / MCP Server能力执行(做什么)是,返回结构化结果可能(写操作)API / 微服务
子 Agent独立执行单元 + 自己的上下文返回最终结果摘要可能外包给同事
记忆 / Memory关于用户与历史的事实(是什么)读取时进入上下文否客户档案
Prompt 模板单次任务的输入格式——一次性表单

最容易混淆的是 Skill vs Tool:Skill 告诉你”报销要走哪几步、哪些票能报”,Tool 是”把这笔钱打出去”。把两者混在一起的结果是:用 Skill 去做本该落地的执行(不可靠、不可审计),或用 Tool 描述去做本该是行为规范的事(每次都要重复粘贴 SOP,浪费上下文)。

Skills over MCP 的取舍:技能指令可以经 MCP 作为资源分发,好处是集中更新、统一治理、客户端零部署;代价是每次获取需要协议往返、服务端不可用时技能不可用、且引入了对第三方服务端的运行时依赖。本地技能目录则离线可用、无往返延迟、可由版本控制系统 review,但更新依赖客户端同步。选择依据是:技能是否需要跨客户端强制一致更新(合规、风控类答案是需要),以及是否能接受服务端依赖(沙箱化 agent 往往不接受)。

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

  • 上下文预算的第一性原理:常驻元数据是”目录索引”,必须廉价但足够可区分;body 是”操作手册”,必须完整但不过载;references 是”附录”,必须按需。
  • 发现准确率取决于 description 质量:官方明确区分了好例子(说明做什么 + 何时用 + 具体关键词)与坏例子(“Helps with PDFs.”)。description 写得太泛会误触发,写得太窄会漏触发,两者都直接削弱整套机制。
  • 与 Rightsizing 的关系:技能数量增长时,先用路由(按意图先选技能组,再在该组内加载)而不是全量平铺。
  • 供应链安全:技能目录里的 scripts/ 一旦被模型执行,等价于执行任意代码。第三方技能必须做来源验证、代码 review、最小权限运行环境,并记录执行审计。

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

  1. 把 Skill 当成安全边界——Skill 是提示词层面的指令,模型可以忽略、可以被注入内容误导、也不被运行时强制。授权边界必须放在模型之外(工具网关、策略执行点),不能依赖”我在 SKILL.md 里写了不要删文件”。
  2. description 写得像标题——失去”何时用”的信息,模型无法判断触发条件。
  3. 把全部细节塞进 SKILL.md——破坏了渐进式披露,激活成本线性上升,也让最重要的步骤淹没在细节里。
  4. 多层嵌套引用——官方要求引用保持一层深,深层链条会让模型读不到关键文件,表现为”技能明明写了但模型不照做”。
  5. 忽略 allowed-tools 的实验性质——该字段各实现支持度不一,不能当作权限控制的正式机制使用。
  6. 用 Skill 替代确定性校验——本该由代码校验的输出格式,写进 SKILL.md 只是把强约束降级为弱建议。

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

Skill 是一个以 SKILL.md 为核心的目录格式,承载的是程序性知识——也就是”怎么做”,而不是”做什么”。它不返回结果、不产生副作用,这是它和工具最本质的区别:工具是能力执行,Skill 是行为规范。

它的核心机制是渐进式披露三层。第一层是元数据,name 加 description,启动时全量加载,每个技能大约 100 tokens;第二层是 SKILL.md 正文,技能被激活后整份加载,官方建议控制在 5000 tokens、500 行以内;第三层是 scripts、references、assets,按需读取。这里有个反直觉的点:技能数量增长时,最贵的不是元数据,而是 body 和被错误加载的 references,所以优化顺序是先压 SKILL.md、再拆细 references、最后才考虑砍技能数量。

description 的写法决定了发现准确率,官方要求它同时说明”做什么”和”什么时候用”,并带上具体关键词,写成标题式的短语会导致误触发或漏触发。

最后一点必须强调:Skill 不是安全边界。它是提示词层面的指令,模型可以忽略,也可能被注入内容带偏。授权和校验要放在模型之外的工具网关与策略点;而 skills 目录里的 scripts 一旦被执行,等价于执行任意代码,第三方技能必须做来源验证和运行时审计。

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

追问:目录里挂了 200 个技能,光元数据就 20k tokens,怎么处理?

回答要点:

  1. 先算清楚账:对比总成本。20k tokens 相对于 200k 上下文约占 10%,是否真的构成瓶颈要用数据判断,不要凭直觉优化。真正的浪费往往来自误激活的 body。
  2. 分层路由:按领域把技能分组,先让路由层根据意图选组,只把该组的元数据放进上下文。这是技能版的两级索引。
  3. 裁剪需求侧:200 个技能中很多可能服务的是不同场景(不同产品线、不同工作流),应当按 Agent 类型裁剪可见技能集,而不是所有 Agent 共享一个全量目录。
  4. 压缩 description:在不损失”何时用”信息的前提下压缩到最小必要(官方上限是 1024 字符,但通常远不需要用满)。注意删关键词会直接降低召回。
  5. 治理手段:定期清理零命中技能(用激活日志做依据,而不是凭印象)。
  6. 明确不要做的事:不要为了省 token 把多条元数据合并成一条”超级技能”——那会一次性拉取大量无关正文,反而更贵。

S01 场景题(Agent):多租户隔离失效与重建 Q-20261014-S01#

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

  • 背景:SaaS 多租户 Agent 平台,800 个企业租户,共享 Agent 运行时、共享 MCP 工具服务(工单 / CRM / 文档库)、共享向量库与模型网关。日活会话 2 万,峰值 QPS 300,单次会话平均 6 次工具调用、3 次检索。
  • 已知现象:租户 A 的用户收到了租户 B 的合同条款片段。链路追踪显示入口网关租户身份是 A、MCP 请求头租户标识也是 A,但向量检索返回了 B 的文档 ID,且 LLM 语义缓存命中了一条由 B 触发的缓存项。
  • 目标:P95 延迟增加 ≤ 50ms;不为每个租户单独部署;保留共享缓存收益;满足合规审计(谁在什么时候以什么身份访问了哪个租户的数据)。
  • 约束:MCP 2026-07-28 规范已移除协议级会话,租户上下文必须每请求自描述;60 个金融 / 医疗租户要求跨租户完全不可见;向量库历史数据混放,短期无法重新分片。

2. 核心考点#

  1. 无状态协议下租户上下文的传递位置与可信性——为什么不能让模型声明自己是哪个租户。
  2. 纵深隔离层次:网关身份 → 工具服务强制校验 → 存储行级安全 → 检索层预过滤 → 缓存键维度。
  3. 向量检索过滤的正确位置:预过滤(pre-filter)与后过滤(post-filter)在 ANN 场景下的取舍,以及”先召回后过滤”为什么既是质量问题也是安全问题。
  4. Hidden Context Exposure:工具响应、检索文档、记忆、应用状态都在机密性范围内;模型输出就是泄漏面。
  5. 审计与归因:跨租户访问尝试的检出与告警,而不是只在泄漏发生后追溯。

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

  1. 先给结论:这是数据平面的隔离缺失,不是身份认证问题;入口身份正确不代表查询受限。
  2. 分层定位:沿”身份 → 工具调用 → 检索 → 缓存 → 生成”五段逐层给出判定指标。
  3. 给出隔离重建方案:每一层的强制点放在哪里,为什么放在那里。
  4. 明确取舍:延迟预算怎么分配(≤50ms 要落在哪些操作上)、缓存命中率损失多少、哪些租户走硬隔离。
  5. 失败路径与兜底:校验失败时 FAIL-CLOSED 还是 FAIL-OPEN,各自的业务影响。
  6. 观测与验证:上线前怎么证明隔离生效(不是靠”没再发生泄漏”)。

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

结论:入口身份正确但检索返回了别人家的数据,这说明租户约束没有被下推到数据平面。网关验了身份,却没有把这份约束强制传导到检索查询和缓存键上——中间任何一个环节的过滤缺失都会造成泄漏,而入口日志看起来永远是干净的。所以修复不是在网关再加一层校验,而是要在每一次数据访问的最内侧强制带上租户约束,并且让缓存、检索、工具三类访问各自独立地 enforce,不依赖上游”已经过滤过”。

第二件事是把语义缓存的键从”提示词哈希”改成”(租户, 权限标签, 提示词) 哈希”。这条最容易被忽略:缓存是共享的,一旦键里没有租户维度,它就是一条跨租户的数据通道,而且这条通道完全绕过了所有检索与权限逻辑。

第三,泄漏点之所以从”合同条款”暴露出来,是因为模型把检索到的内容说了出去——这正是 Hidden Context Exposure 的表现:检索文档、工具响应、记忆、应用状态都是机密数据,它们的边界不在提示词里,而在访问控制里。

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

拆解:把一次会话的数据访问拆成五段,每段都要有独立的租户约束:

段失效点修复后的强制点
① 入口身份已正确签发租户绑定的短期令牌,含 tenant_id、user_id、agent_id、权限标签
② LLM 网关语义缓存键无租户缓存键 = hash(tenant_id, permission_tags, model, prompt)
③ MCP 工具调用头里有租户但工具服务未校验工具服务独立校验租户并对结果做归属断言
④ 向量 / 全文检索无 pre-filter 或过滤发生在 ANN 之后检索层强制 tenant 过滤,且做结果归属断言
⑤ 生成无泄漏检测输出侧的跨租户实体检测 + 引用溯源强制

核心执行流程:

  1. 签发租户绑定令牌:入口认证后签发短 TTL(如 15 分钟)令牌,tenant_id 为签名 Claim,不可被客户端篡改。租户上下文不由模型生成,只能从令牌派生——这是最关键的一条原则。
  2. 在无状态 MCP 中传递租户上下文:2026-07-28 规范移除协议级会话后,状态不能由服务端保存,因此租户上下文必须每请求自描述。放在 HTTP 头(用于网关路由与审计)与 _meta(用于跨服务追踪与溯源)两处,各自承担不同职责。工具侧不信任任何一方自行声明的租户 ID,只认网关签名下来的 Claim。
  3. 工具服务侧的强制校验:每个 MCP 工具在处理前做两步——(a) 校验令牌签名与 tenant_id;(b) 查询执行时把 tenant_id 作为必要条件注入(不是可选过滤器)。返回结果再逐条断言归属,发现不属于该租户的记录直接丢弃并触发告警。
  4. 检索层:把 tenant 过滤从应用层下推到检索层。
  5. 结果归属断言:所有跨服务返回的数据在进入上下文之前做一次统一的归属校验(统一的 assert_tenant(items, tenant_id)),作为最后一道确定性闸门。

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

向量检索的过滤位置(本题的核心技术取舍):

  • Post-filter(先 ANN 召回后过滤):默认做法,实现最便宜。问题是被过滤掉的名额已经消耗在别的租户的文档上,会导致有效召回数不足;更严重的是,正确率低时工程上常见的”补偿”做法是放宽过滤或扩大 top-k,这会直接重新打开泄漏窗口。这是本题既有质量问题又有安全问题的根因。
  • Pre-filter(带 tenant 条件的 ANN):租户维度基数高(800 个)时,HNSW 的图遍历会被过滤条件切断,召回质量下降,需要配合按租户分区或带过滤能力的索引参数。
  • Namespace / 分区隔离:把每个租户放到独立 collection 或 partition。隔离性最强、性能最好,但 800 个租户会产生大量小索引,元数据与内存开销显著。
  • 混合方案(推荐):对 60 个金融 / 医疗租户使用独立 namespace 硬隔离;对其余租户使用共享 collection + pre-filter + 结果归属断言双保险。这样把最高的隔离强度花在最需要的地方,而不是一刀切地全量拆索引(这也回应了”不为每个租户单独部署”的成本约束)。

延迟预算分配(P95 + 50ms 的账):

操作预算说明
令牌校验与 Claim 解析< 1ms本地验签,不查库
租户要使用 pre-filter3—10ms依赖向量库的过滤能力,需实测
结果归属断言1—3ms内存态批量比对,通常可忽略
缓存键加租户维度~0ms只是哈希输入变化,不增加延迟
输出侧泄漏检测(抽样)0—20ms见下

输出侧检测如果全量做一次额外的模型调用,成本与延迟都不可接受。做法是只对高风险租户全量做基于规则的实体匹配(检测回答中出现的外部租户专有名词、文档 ID、合同编号),其余租户抽样 5% 做离线检测——这既是可用性取舍,也是诚实说明”第二道防线的覆盖率不是 100%”。

备选方案及不选的理由:

  • 每租户独立部署:隔离最强,但 800 套部署的容量利用率与运维成本不可接受。
  • 仅靠数据库行级安全(RLS):对 PostgreSQL 有效,但对向量库与语义缓存无效,无法覆盖全部数据平面。
  • 在提示词里写”不要泄露其他租户的信息”:这是最危险的错误。它是概率性约束,不是访问控制,且不产生可审计记录。

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

  • 令牌过期或验签失败:FAIL-CLOSED,中断会话并返回可操作的错误。绝不能”降级为不校验”继续服务。
  • 租户过滤条件缺失(编程错误):由统一的数据访问层强制注入租户条件;工具与检索入口不允许绕过该层。用 lint / 单测 / 目录服务三层保证,而不是靠 review。
  • pre-filter 导致召回不足:不要通过放宽租户过滤来补偿。正确处置是扩大 ef_search / top-k 后再过滤,或调整索引参数——任何以”放宽租户条件”换召回的改动都必须被代码评审规则直接禁止。
  • 向量库超时:不属于可重试的情形只有一种——响应体里已经带了数据。空结果或连接超时可以重试,但要带幂等键;重试不得改变租户 Claim。
  • 服务间 Claim 不一致:当作安全事件立即中断并告警,不要取”多数”或”取第一个”。
  • 缓存命中但不能验证归属:视为未命中,且在审计日志中记录该 key 为可疑。
  • 恢复:泄漏事件后必须做的是按 tenant_id 反查该时段所有访问记录,产出受影响数据清单,而不是只修一条链路。

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

  • 权限:遵循最小代理原则——Agent 不以用户身份也不以平台超管身份访问工具,而是持有租户绑定的受限令牌,权限范围显式枚举(哪些工具、哪些数据类、只读还是可写)。这与 2026-10-10 Q-20261010-B05 的原则一致,本题是那条原则在数据平面的落地。
  • 安全:真正的边界是代码里的访问控制,不是提示词。泄漏面包括检索文档、工具响应、记忆、应用状态——四者都要纳入同一套机密性分级。
  • 成本:共享向量的 FTE 与存储成本不变;增加的是 60 个硬隔离租户的独立索引开销与输出检测的计算。相比全量拆索引节省明显。
  • 延迟:主要增量来自 pre-filter 与高风险租户的输出检测;缓存键改造零延迟成本,是性价比最高的一项。
  • 可靠性:归属断言是同步的确定性逻辑,不引入新的外部依赖;它的失败模式是”拒绝服务”而非”泄漏”,这是正确的失败方向。

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

  • 评测集:构建跨租户对抗集——为每对易混租户构造”本租户没有、邻居租户有”的文档与查询,断言检索返回为空且回答不引用外部文档 ID。这类用例必须进 CI,而不是只在泄漏事后补一次。
  • 上线前验证:混沌测试——随机把某次请求的 tenant_id 换成另一个,验证每层(工具、检索、缓存、生成)至少有一处拒绝或过滤。只靠端到端”看起来没泄漏”无法证明隔离生效。
  • 线上观测指标:跨租户归属断言触发数(期望恒为 0,非 0 立即告警)、缓存跨租户命中率(期望 0)、pre-filter 前后召回数比、按租户的检索延迟分布、按 agent_id 的访问审计完整率。
  • 回归防线:任何修改检索或缓存的 PR,必须跑跨租户对抗集才能合并。

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

追问一:为什么不直接在网关加一层严格的租户校验就够了?

回答要点:① 网关只看到入口请求,看不到工具服务内部是否按租户过滤,也决定不了向量库的 ANN 遍历路径;② 缓存命中会绕过网关之后的所有逻辑——本题中缓存漏了租户维度,就是最好的反证;③ 纵深防御的意义在于任何单层失效都不应导致泄漏,单点校验做不到这一点;④ 网关也管不了内部服务之间的调用(东西向流量)。

追问二:pre-filter 让召回质量变差了,业务方投诉答案变差,怎么办?

回答要点:① 绝不通过放宽租户过滤来提升召回——这是用安全换指标,属于必须禁止的改法;② 正确手段是提高 ef_search / 候选集再过滤、调整索引构建参数、或改进 embedding 与查询改写;③ 若确实存在结构性冲突,把该租户升级到独立 namespace(本题已为高风险租户准备的通道);④ 用数据说话:分别测”召回质量指标”与”泄漏断言次数”,让取舍可见,而不是口头争论。

追问三:60 个金融 / 医疗租户要求”完全不可见”,怎么证明给审计方看?

回答要点:① 架构证据:独立 namespace / 独立加密密钥,跨租户代码路径不存在(而不是”被过滤”);② 运行时证据:该租户的访问日志中不存在任何跨租户归属断言;③ 测试证据:对抗集在 CI 中作为阻塞项,附最近 N 次执行结果;④ 失败方向证据:说明系统在异常时是 FAIL-CLOSED;⑤ 诚实边界:说明共享控制平面仍有共用的日志基础设施,需靠字段级脱敏与访问审计来兜住——不要声称”物理隔离”如果没有做到。

11. 常见误区#

  1. 在提示词里写”不要泄露其他租户数据”——概率性约束不是访问控制,且不产生审计记录。这是把最后一个防线交给了模型。
  2. 只在入口做校验——入口日志会一直显示正确,泄漏却持续发生。修复必须下推到数据访问的每一层。
  3. 忽略语义缓存——共享缓存键没有租户维度时,它是一条绕过所有权限逻辑的数据通道,而且是最难发现的一条。
  4. 用 post-filter 后再靠”结果再检查一遍”解决——这把同一份数据在上下文里放了两次,且检查本身也是可能被绕过的第二实现;正确做法是 pre-filter + 统一归属断言。
  5. 把身份=用户身份——Agent 的身份是独立的非人类身份,代持令牌必须有自己的权限范围与审计标识,否则归因链断在”是系统做的”。

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

  • 模型本身的概率性能力:生成回答、判断是否引用、表面上的”不泄露”倾向。绝不能作为安全边界——它不可验证、不产生审计、会随模型版本漂移。
  • Agent 编排与状态能力:决定调用哪些工具与检索、携带什么租户 Claim、上下文里最终留下什么。它能保证”流程上要求携带租户上下文”,但它自己也可能出错(编程错误会漏掉过滤条件)。
  • 传统软件系统的确定性能力:令牌验签、归属断言、缓存键构造、pre-filter 执行、行级安全、审计落库。隔离的唯一可靠落点在这里——它的失败模式是可预测的拒绝服务,而不是静默泄漏。

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

写一个最小复现与修复:

  1. 用 SQLite 建一张跨租户的 documents(id, tenant_id, content),插入两个租户各 20 条数据。
  2. 实现一个”错误版”检索:SELECT * FROM documents ORDER BY bm25(content) LIMIT 5(不带 tenant 过滤),复现跨租户泄漏。
  3. 实现一个统一数据访问层 fetch_docs(query, tenant_ctx),强制注入 tenant_id,并加 assert_tenant(rows, tenant_id)。
  4. 实现一个语义缓存,第一版 key 只用 hash(query),复现跨租户缓存命中;改成 hash(tenant_id, permission_tags, query) 后验证不再命中。
  5. 写 5 条跨租户对抗用例,作为回归测试运行。

S02 场景题(非 Agent):零停机 schema 变更 Q-20261014-S02#

题型说明:本题为通用工程场景。核心方案是数据库迁移、双写与发布编排,不依赖模型或 Agent 的任何能力。三层能力划分在此不适用(不存在概率性能力参与决策),因此本节不列该小节。

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

  • 背景:订单服务,PostgreSQL 15,1 主 2 从 + 1 个离线分析从库。orders 表 8000 万行、日增 25 万行、峰值写入 1200 TPS。
  • 目标:把单列 address text 拆成 receiver_name / phone / province / city / district / address_detail,新代码只读写结构化列。
  • 约束:全程零停机;任意阶段可回滚;不允许出现超过 5 秒的表级锁;回填期间主库写入不能失败;从库复制延迟 ≤ 30 秒。

2. 核心考点#

  1. expand-migrate-contract 三阶段,以及每阶段为什么必须可逆。
  2. PostgreSQL DDL 的锁行为:哪些操作是元数据变更、哪些会重写表、哪些会全表扫描。
  3. 双写与回填的一致性:回填不能覆盖更新的行;并发写入的竞态。
  4. 批量回填的节流与复制延迟控制:为什么不能用 OFFSET,以及如何自适应节流。
  5. 回滚点与 contract 阶段的不可逆性。

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

  1. 结论先行:用 expand-migrate-contract,任何阶段都不做”删除列”的不可逆动作,直到确认无读取来源。
  2. 先讲锁:哪些 DDL 会锁表、用什么参数(lock_timeout)兜住。
  3. 再讲一致性:双写顺序、回填与在线写入的冲突判据。
  4. 然后讲节奏:批量大小、keyset 分页、自适应节流、复制延迟闭环。
  5. 最后讲回滚与观测:每阶段的回滚点、触发阈值、验证方法。

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

结论:用 expand-migrate-contract 三阶段,全程不做破坏性 DDL。核心三条纪律:第一,加列只允许”可空无默认值”或”非 volatile 常量默认值”的元数据级变更,避免触发全表重写;第二,回填用 keyset 分页 + 小批提交 + 自适应节流,把复制延迟作为反馈信号;第三,contract 阶段只做逻辑下线,物理删列放到最后且单独排期,因为删列是不可回滚的。

我先把地址拆分的特殊点说清楚:这次变更有数据转换逻辑(把自由文本解析成结构化字段),而解析规则可能出错。所以不能”加列 + 回填 + 直接切读”,必须保留 address 原文作为回滚依据,且回填结果需要可校验、可重新解析——这一点是它区别于”单纯加一列”的关键。

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

阶段划分

阶段动作可逆性
0. 准备加新列(可空);部署解析函数;加索引(CONCURRENTLY)完全可逆(DROP COLUMN,但注意其本身的锁)
1. Expand新代码双写(写新旧两套),读仍走旧列可回滚到旧版本代码
2. Backfill后台回填历史 8000 万行可停止、可重跑(幂等)
3. Verify一致性校验:抽样 + 全量计数比对校验失败则停留在本阶段
4. Migrate灰度切读:先 1% → 10% → 50% → 100%,读新列,回落到旧列每个比例都是回滚点
5. Contract停止写旧列,旧列逻辑下线(保留物理列)逻辑可逆;物理删列不可逆

关键技术点

① 加列为什么必须小心:在 PostgreSQL 中,加一个带非 volatile 常量默认值的列只改元数据,不重写表(11 及以后版本的行为);但如果默认值是 volatile 表达式、或者需要计算,就会触发全表重写,在 8000 万行的表上是分钟到小时级的表级锁。所以纪律是:先加可空列,再单独回填默认值,绝不在业务高峰期加带计算默认值的列。

② 索引必须 CONCURRENTLY:CREATE INDEX 默认拿 SHARE 锁阻塞写入;CREATE INDEX CONCURRENTLY 不阻塞写入,代价是不能在事务块里执行、失败会留下 INVALID 索引需要清理、且耗时更长。

③ 每次 DDL 都要设 lock_timeout:这是本题”不允许超过 5 秒表级锁”的工程实现方式——不是靠估算,而是靠让数据库自己拒绝:SET lock_timeout = '3s' 后执行 DDL,拿不到锁就失败退出重试。这是失败方向正确的设计:宁可 DDL 失败,不要锁住全表写入。

④ 回填的一致性(本题最容易答错的地方):

回填与在线写入会撞车。如果回填线程读到旧值、解析、写回,而期间用户已经改了地址,回填就会用旧数据覆盖新数据。

正确做法是让回填不覆盖已由新路径写入的行。两种实现:

  • 判据一(推荐):回填时带条件 WHERE structured_updated_at IS NULL OR structured_updated_at < updated_at,即”新列比旧列更旧才回填”。这需要一个 updated_at(或版本号)作为可比对的时钟;要求更新时同时维护它。
  • 判据二:回填用 INSERT ... ON CONFLICT DO NOTHING 只补空值,把”修正”交给在线写入路径自己完成。更简单,但对”解析规则修好后需要重跑”的场景不够灵活。

⑤ 回填的节流:

  • 分页方式:绝不能用 LIMIT ... OFFSET——OFFSET 会随偏移线性扫描,8000 万行的表上后段批次会退化。用 keyset 分页:WHERE id > :last_id ORDER BY id LIMIT 1000。
  • 批次与提交:每批 500—2000 行,每批独立提交。长事务会持有锁、阻止 autovacuum、并让 WAL 累积。
  • 自适应节流:以复制延迟为反馈信号——延迟 > 10s 降速(减小批次 + 增加 sleep),> 20s 暂停,< 5s 恢复。这是满足”复制延迟 ≤ 30 秒”硬约束的唯一可靠方式,靠人工调参做不到。
  • WAL 与 autovacuum:回填会产生大量 WAL 与死元组;要提前确认磁盘水位与 autovacuum 能否跟上,必要时对回填表临时调高 autovacuum 参数。

⑥ 解析质量问题:地址解析会有失败样本(海外地址、异常格式)。回填要记录解析失败率与失败样本 ID,而不是静默写入 NULL。失败率超过阈值(如 2%)应暂停并人工检查规则——否则你会在切读后才发现大量订单地址为空。

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

方案 A:原地加列 + 双写 + 回填(推荐)

  • 依据:无需额外存储翻倍;回滚点密集;可灰度。
  • 代价:回填期间主库有额外写入压力;需要维护双写逻辑一段时间。

方案 B:建新表 + 触发器同步 + 切换

  • 适用:拆分幅度大、原表结构严重不合理时。
  • 代价:触发器增加每次写入的延迟;切换后的表名/视图兼容层更复杂;回滚要反向同步。本题拆分幅度不足以支撑这个复杂度。

方案 C:逻辑复制 / 外部工具(如 pgroll、gh-ost 类工具的 PG 等价物)

  • 依据:把回填与同步自动化,减少手写逻辑出错。
  • 取舍:引入新的运维组件与依赖;团队不熟悉时故障排查成本更高。可以作为提高效率的选择,但面试中应该说清它替代的是哪个环节,而不是当作”不用懂原理的黑盒”。

不选的做法:pg_dump 停机迁移(违反零停机);单条 UPDATE 全表回填(长事务 + 复制延迟爆表);ALTER TABLE ... ALTER COLUMN TYPE(重写表 + 长时间锁)。

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

  • DDL 锁等待超时:lock_timeout 触发即失败退出,不要重试太频繁(会堆积锁请求队列)。退避重试,并在低峰期执行。
  • 回填中断:因为用 keyset 分页且每批提交,中断后从 last_id 继续即可,天然幂等——前提是回填写入本身是幂等的(见上文判据)。
  • 回填覆盖新数据:由 structured_updated_at < updated_at 条件阻止;这是必须写进代码的约束,不能依赖”顺序上不会发生”。
  • 解析失败:不写 NULL 而写”解析失败”标记并保留原文,保证后续可重跑;失败率超阈值暂停。
  • 复制延迟超限:回填自动暂停;若持续超限,说明节流参数过激进或 WAL 生成过快,需降速而不是提高延迟阈值。
  • 切读后发现问题:回滚路径是把读切回旧列(配置开关),旧列在双写期间一直是最新的,所以读切换可以瞬间回滚。这正是双写的价值。
  • 幂等:回填任务需要记录 checkpoint(last_id、已处理数、失败数),支持重启续跑与进度可观测。

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

  • 安全与权限:地址属于 PII。回填脚本与分析从库都会接触全量地址;要确保回填任务使用最小权限账号(只允许更新目标列),分析从库的访问要有脱敏策略;回填日志不得打印地址原文。
  • 成本:额外的列会增加行宽与存储(8000 万行 × 6 列),索引也会增大;回填期间的 IO 与 WAL 是主要增量成本。
  • 延迟:在线写入路径只多一次列写入,增量在微秒级;回填导致的复制延迟是主要风险,由自适应节流控制。
  • 可靠性:每个阶段都有明确回滚点;唯一不可逆的是物理 DROP COLUMN,因此被刻意推迟到独立排期,且在此之前应先确认无任何读取来源(通过查询日志统计或改列名观察错误率)。

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

  • 前置验证:在与生产同规格的影子库上跑完整回填,实测耗时、WAL 增量与复制延迟曲线,用实测数据定批次大小,不靠猜。
  • 一致性校验:
    • 全量计数:新列非空行数 vs 总行数(应相等)。
    • 抽样比对:随机抽样 1 万行,比对结构化字段拼接后的语义与原 address 的一致性。
    • 反向校验:新写入的行必须双列一致(用一个后台校验任务持续比对,产出不一致计数)。
  • 上线观测:回填进度(已处理 / 总数 / 速率)、解析失败率、复制延迟、主库 IOPS 与 WAL 生成速率、锁等待事件数、不一致计数。
  • 准入条件:只有”不一致计数 = 0 且解析失败率 < 阈值”才允许进入切读阶段;切读按 1% → 10% → 50% → 100% 灰度,每个比例观察错误率与业务指标。

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

追问一:为什么回填一定要分批提交,不能一个大事务一次性跑完?

回答要点:① 长事务持有快照,阻止 autovacuum 清理死元组,导致表膨胀;② 长事务期间产生的 WAL 无法被回收,可能撑爆磁盘;③ 从库要等事务提交才能应用,复制延迟会直接超过 30 秒约束;④ 失败要全部回滚,已做的工作全部作废;⑤ 长事务持有的锁窗口更长,与其他 DDL / 查询冲突概率上升。

追问二:切读到新列之后,怎么保证”旧列真的没人读了”?

回答要点:① 不要靠”我记得没有”——用证据:查询日志 / 代码静态扫描 / 网关字段访问统计;② 更可靠的做法是先停止写旧列(或写入哨兵值),观察是否有读取报错或异常,这是最直接的访问探测;③ 用视图或改名做过渡(把旧列重命名为 address_deprecated,读取方会立刻失败并暴露出来,比静默删除安全);④ 确认归零并保留一个观察期后,才在独立变更里做物理 DROP COLUMN。

追问三:如果回填到一半发现解析规则有 bug,已经写入了 3000 万行错误数据,怎么办?

回答要点:① 立即暂停回填,不要继续扩大污染面;② 双写期间旧列仍是真源,所以不需要回滚线上数据——修复规则后对已回填范围重跑即可,前提是新列有可比对的版本/时间戳;③ 重跑必须是幂等的:用”新列版本 < 规则版本”作为重跑条件,避免二次污染;④ 切读绝不能在这种状态下进行,这正是一致性校验作为准入条件的意义;⑤ 事后要补一个规则变更的评测流程:规则上线前先在样本集上跑成功率,而不是直接上生产。

11. 常见误区#

  1. 用一次 UPDATE 全表回填:长事务、复制延迟爆表、失败全回滚。
  2. 用 OFFSET 分页做批量:后段批次线性退化,越跑越慢,最后看起来像”数据库变慢了”。
  3. 回填条件不带版本比对:用旧数据覆盖在线新写入,产生静默数据丢失——这类错误不会报错,只会让用户发现”我改的地址怎么又变回去了”。
  4. 在事务块里跑 CREATE INDEX CONCURRENTLY:它不允许在事务块中执行,强行写会报错;且失败会留下 INVALID 索引需要清理。
  5. 过早物理删列:contract 阶段最危险的动作是 DROP COLUMN,它是唯一不可逆的一步,应该独立排期并有明确的准入证据。

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

用 Docker 起一个 PostgreSQL 15:

  1. 建 orders(id, address text),用 generate_series 灌 200 万行随机地址文本。
  2. 加 6 个结构化列(可空),设置 lock_timeout = '3s' 观察 DDL 行为;对 province 建索引时用 CONCURRENTLY,并观察它能否在事务块中执行。
  3. 写一个 keyset 分页的回填脚本(批 1000,独立提交),打印进度与速率。
  4. 模拟并发写入:另一个进程随机更新某些行的 address 与 updated_at,验证你的回填条件是否会覆盖新值。
  5. 实现一个一致性校验查询,统计新列为空的行数与解析失败数。
  6. 用 pg_stat_replication 观察复制延迟(单实例可改为观察 WAL 生成量 pg_current_wal_lsn() 差值)。

L01 LeetCode 739:每日温度 Q-20261014-L01#

1. 基本信息#

项内容
题目 IDQ-20261014-L01
题号739
中文名每日温度
英文名Daily Temperatures
难度Medium
模式单调栈(栈与队列)
中文链接https://leetcode.cn/problems/daily-temperatures/
英文链接https://leetcode.com/problems/daily-temperatures/
链接核验已核验(2026-10-14 同时访问中英文页面,题号、标题、示例与约束一致)
状态首次出现,非复习题(历史已用题号:146、3、215)

2. 题目描述#

给定整数数组 temperatures,表示每天的温度。返回数组 answer,其中 answer[i] 是在第 i 天之后,需要等待多少天才能遇到更高的温度。若之后都不会升高,则 answer[i] = 0。

输入 / 输出 / 约束

  • 输入:List[int] temperatures
  • 输出:List[int],与输入等长
  • 1 <= temperatures.length <= 10^5
  • 30 <= temperatures[i] <= 100

示例

输入输出
[73,74,75,71,69,72,76,73][1,1,4,2,1,1,0,0]
[30,40,50,60][1,1,1,0]
[30,60,90][1,1,0]

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

单调栈(monotonic stack)。核心识别特征:题目为每个元素寻找”某一侧第一个满足单调性条件的元素”(这里是右侧第一个严格更大的元素),并且要求结果与”距离”相关。

这类题的通用形态覆盖了「下一个更大元素」(496)、「下一个更大元素 II」(503)、「每日温度」(739)、「商品价格折扣」(1475)、「柱状图最大矩形」(84)等。

4. 渐进式提示(不直接给答案)#

提示一:先想清楚暴力法为什么是 O(n²)。对于第 i 天,暴力是向右线性扫描直到遇到更高温度。哪些信息被反复扫描了?

提示二:如果对于某一天 i,我们已经知道它右边若干个”还没找到更高温度”的日子,当第 j 天(j > i)的温度比它们都高时会发生什么?这些”悬而未决”的日子能不能被一次性结算?

提示三:维护一个栈,栈里放的是下标而不是温度,并且让栈中下标对应的温度保持单调递减。当新来的温度 t 高于栈顶对应温度时,栈顶那天的答案就是今天——把它弹出并结算,重复这个过程直到栈空或栈顶温度不低于 t。想清楚:为什么被弹出的元素永远不会再用到?为什么剩下的元素仍然保持单调递减?

5. 直观方案及其局限#

暴力法:对每个 i,从 i+1 向右扫描直到 temperatures[j] > temperatures[i],答案 j - i。

def dailyTemperatures(temperatures):
n = len(temperatures)
ans = [0] * n
for i in range(n):
for j in range(i + 1, n):
if temperatures[j] > temperatures[i]:
ans[i] = j - i
break
return ans

局限:时间复杂度 O(n²)。在 n = 10^5 且严格递减的最坏输入下(如 [100,99,98,...]),需要约 5×10^9 次比较,必然超时。空间 O(1)(不含输出)——这是它唯一的优点。

关键观察:暴力法把”第 i+1 到 j-1 天都不够高”这个信息扫描了一遍又一遍,而它对后续每一天的判断都是有用的。

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

第一步:把”悬而未决”的日子显式化。
从左到右遍历。遍历到第 i 天时,有一些更早的日子还在”等待”一个更高的温度。这些日子不是任意集合——它们的温度必然是单调递减的。为什么?假设第 a 天早于第 b 天,且 temp[a] <= temp[b],那么当第 b 天出现时,它对第 a 天来说已经是一个更高的温度,第 a 天早就被结算了,不可能还在等待。反证成立:等待队列里的温度严格递减。

第二步:新的一天能结算谁?
第 i 天温度 t。等待队列(栈)顶部是最近一个还在等待的日子。如果 t > temp[栈顶],那么第 i 天就是栈顶那天等待的”第一个更高温度”——因为栈顶与第 i 天之间的所有日子温度都不高于栈顶(否则栈顶早就被结算了)。于是结算 answer[栈顶] = i - 栈顶,弹出。重复这个过程——每次弹出都可能让新的栈顶暴露出来,而它同样在第 i 天被结算。

第三步:什么时候停?
当栈空,或 temp[栈顶] >= t。此时第 i 天对栈顶不构成”更高温度”,于是把 i 压入栈。压入后栈的单调递减性质仍然成立(因为 t <= temp[栈顶])。

第四步:剩下的元素怎么办?
遍历结束后栈中剩余的下标,对应那些”永远等不到更高温度”的日子。它们的答案保持初始值 0——这正是初始化为 0 的原因,不需要额外处理。

7. 正确性说明#

不变量:遍历到第 i 天之前(处理第 i 天之前),栈中的下标严格递增(按入栈顺序),且对应的温度严格递减;并且栈中每个下标 k 都满足:k 与 i 之间不存在温度高于 temp[k] 的日子。

  • 初始:栈空,不变量平凡成立。
  • 保持:处理第 i 天时,while 循环不断弹出栈顶 k 并令 answer[k] = i - k。由不变量,k 与 i 之间不存在更高的温度,因此 i 是 k 右侧第一个更高温度,赋值正确。循环结束条件保证 temp[栈顶] >= temp[i](或栈空),压入 i 后温度单调递减保持;而”栈顶与 i 之间无更高温度”这个性质对新栈顶也成立(因为所有介于其间、温度高于新栈顶的日子,若存在,会让新栈顶在更早的时候就被弹出)。
  • 终止:遍历结束后,栈中剩余下标在右侧没有更高温度(否则会被后续某天弹出),答案保持 0,正确。

每个下标恰好入栈一次、出栈一次,因此总操作是 O(n) 而不是 O(n²)——这是均摊分析的关键,也是面试官最想听到的一句话。

8. 复杂度#

  • 时间复杂度:O(n)。每个下标最多入栈一次、出栈一次,while 循环的总执行次数上界为 n。
  • 空间复杂度:O(n)(栈 + 输出数组)。最坏情况(严格递减)栈深度为 n。不计输出数组则为 O(n) 的栈空间。

9. Python 参考代码#

from typing import List
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
"""单调栈:栈中下标对应的温度严格递减。"""
n = len(temperatures)
answer = [0] * n # 默认 0,正好覆盖"等不到更高温度"的情形
stack: List[int] = [] # 存下标,不存温度
for i, t in enumerate(temperatures):
# 当前温度是栈顶那些天等待的"第一个更高温度"
while stack and temperatures[stack[-1]] < t:
j = stack.pop()
answer[j] = i - j
stack.append(i)
return answer

从右往左的等价写法(便于理解”未被结算的元素”这一视角,同样 O(n)):

class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
answer = [0] * n
stack: List[int] = [] # 右侧候选,温度单调递增(自栈顶向下)
for i in range(n - 1, -1, -1):
t = temperatures[i]
# 弹出所有不比 t 更高的候选:它们永远不可能成为 i 的答案
while stack and temperatures[stack[-1]] <= t:
stack.pop()
if stack:
answer[i] = stack[-1] - i
stack.append(i)
return answer

注意这里的弹出条件是 <=(而不是 <):右侧温度等于 t 的日子对第 i 天没有价值——它不够高,而且它会挡住更远但更低的候选。从左往右版本之所以用 <,是因为那里比较的是”栈顶是否低于当前”,语义方向不同,不要机械套用。

10. 关键边界情况#

  1. n = 1:返回 [0]。两个版本都天然正确。
  2. 严格递减:[90,80,70,60] → [0,0,0,0]。栈增长到长度 n,是空间最坏情况。
  3. 严格递增:[30,40,50,60] → [1,1,1,0]。每次都把栈清空,while 循环总计执行 n-1 次。
  4. 全部相等:[30,30,30] → [0,0,0]。必须注意”更高”是严格大于,相等不算,因此从左往右版本用 < 是正确的;若误写成 <= 会把相等的日子错误结算。
  5. 重复温度:[70,70,71] → [2,1,0]。两个 70 都在栈中,被 71 依次结算。
  6. 最坏规模:n = 10^5,暴力法必然超时,O(n) 解法与 Python 的 append/pop 常数足够通过。

11. 测试用例#

#输入期望输出覆盖点
1[73,74,75,71,69,72,76,73][1,1,4,2,1,1,0,0]官方示例,混合情形(含”跨 4 天”的长距离结算)
2[30,40,50,60][1,1,1,0]严格递增,每次清空栈
3[30,60,90][1,1,0]官方示例,小样本
4[90,80,70,60][0,0,0,0]严格递减,栈深度最大,答案全 0
5[70,70,71][2,1,0]重复温度,严格大于语义

附加自测:

s = Solution()
assert s.dailyTemperatures([73,74,75,71,69,72,76,73]) == [1,1,4,2,1,1,0,0]
assert s.dailyTemperatures([30,40,50,60]) == [1,1,1,0]
assert s.dailyTemperatures([30,60,90]) == [1,1,0]
assert s.dailyTemperatures([90,80,70,60]) == [0,0,0,0]
assert s.dailyTemperatures([70,70,71]) == [2,1,0]
assert s.dailyTemperatures([100]) == [0]

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

变体:把”更高温度”改成”更高 或相等 温度”(即 >=),answer 会怎么变?

回答要点:① 从左往右版本只需把 while 条件从 < 改成 <=;② 但要注意语义变化后,栈中温度变成单调不增(允许相等),原来”严格递减”的不变量需要改写为”非递增”;③ 用 [70,70,71] 举例,输出从 [2,1,0] 变成 [1,1,0];④ 更进一步的变体是「下一个更大元素 II」(503,环形数组)——那里对同一个下标要扫描两轮,栈里可以存一轮的下标而用 i % n 复用,也可以存两轮,两种写法的不变量不同,需要明确说明。

13. 可迁移的解题能力#

核心迁移能力:把”对每个元素在某一侧做线性扫描”的 O(n²) 结构,改造成”维护一个待结算队列 + 一次性批量结算”的 O(n) 结构。识别信号有三个:

  1. 问题要求为每个元素找一侧最近的 / 第一个满足某单调条件的元素;
  2. 候选集合天然具有单调性(反证法可证明);
  3. 每个元素只会被结算一次,可以”用完即弃”。

这个”待结算队列 + 单调性保证队列有序”的模式同样适用于:滑动窗口最大值(239,单调队列)、柱状图最大矩形(84)、接雨水(42)、股票价格跨度(901)。

与 AI Agent 工程的关系:联系较弱,不宜强行类比。真正有价值的是同一个工程直觉——用”尚未解决的有序待办队列”替代”每来一个事件就全量扫描”,这在 Agent 侧的等待队列、超时扫描、滑动窗口限流(把 O(n) 的窗口重算换成增量结算)中是同一套思路。这里不做进一步牵强映射。


三、延伸资料#

以下资料按可信度排序,标注核验状态。

操作系统与网络(B01)

  1. Linux Kernel Documentation — CFS Bandwidth Control:https://docs.kernel.org/scheduler/sched-bwc.html — 已核验(2026-10-14)。权威一手来源,含 quota/period 语义、cpu.stat 的五个字段(nr_periods / nr_throttled / throttled_time / nr_bursts / burst_time)、层级限流的两种触发方式,以及”周期越长突发能力越强、周期越短延迟越一致”的明确表述。
  2. Kubernetes 官方文档 — Resource Management for Pods and Containers:resources.limits.cpu 与 requests 的语义与 QoS 分类。本轮未核验具体页面,引用时请以当前版本文档为准。

软件工程与平台工程(B02 / S02)

  1. PostgreSQL 官方文档 — ALTER TABLE 与 CREATE INDEX CONCURRENTLY,lock_timeout 参数说明。本轮未核验具体页面;其中”加带非 volatile 默认值的列只改元数据”这一行为与版本相关,实际使用请以对应版本的官方 Release Notes 为准,不要凭记忆引用。
  2. Google AIP(API Improvement Proposals)中关于兼容性与分页的约定:属于可信技术组织的公开规范,本轮未核验具体条目编号。

Agent Skills(B04)

  1. Agent Skills 官方规范 — https://agentskills.io/specification — 已核验(2026-10-14)。本页 B04 中所有字段名、字符上限、命名约束、三层渐进式披露的 token 量级与”一层深”引用建议均出自此页。
  2. Agent Skills 参考实现与校验库 — https://github.com/agentskills/agentskills/tree/main/skills-ref — 官方规范页指向的校验工具,本轮未核验仓库当前状态。

Multi-tenancy / 安全(S01)

  1. Model Context Protocol 官方规范与扩展总览(2026-07-28 版本及 changelog):无状态核心、_meta 追踪上下文传播、Tasks、授权扩展。来源沿用 题库配置.md v4 变更记录中已标注的官方来源,本轮未重复核验。

未核验说明:上述标记为”未核验”的条目仅作为延伸阅读方向列出,不构成本文件的事实引用。需要作为面试论据使用时,请先自行核验。


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

今日重点(按优先级)#

  1. 隔离边界必须落在确定性机制上,不能落在提示词里(S01 + B04 共同指向)。Skill 是行为规范不是安全边界;“不要泄露其他租户数据”写在提示词里不产生任何可验证保证,也不产生审计记录。这是今天最重要的一条。
  2. “入口正确”不等于”数据受限”(S01)。网关身份正确、MCP 头正确,但检索和缓存没有租户维度,泄漏照样发生。修复要下推到每一次数据访问的最内侧,并且各层独立 enforce。
  3. 语义缓存的键必须包含租户与权限维度(S01)。这是本题中最容易被忽略、也是成本最低(零延迟)的修复项。
  4. CPU 配额是带宽约束,不是并行度约束(B01)。“CPU 用不满但 P99 周期性抖动”是限流的指纹;处置的第一优先级是基于配额推导并发度,而不是加重试。
  5. 自主度是可调的连续谱,不是产品选项(B03)。权限、金额、不可逆副作用必须固化成代码;Reflection 只在有外部可验证反馈时有效。
  6. 契约演进的三阶段与双向兼容(B02 / S02)。滚动发布期间新旧版本共存,所以向前兼容和向后兼容都要满足;contract 阶段的物理删列是唯一不可逆动作,必须独立排期。
  7. 单调栈的本质是”待结算队列 + 单调性保证有序”(L01),每个元素恰好入栈出栈各一次,这是 O(n²) → O(n) 的均摊依据。

复习建议#

  • 今日无复习题。两道精选题的建议复习日分别为 2026-10-19(Q-20261012-B02,角度从”定义边界”转为”故障定位”)与 2026-10-20(Q-20261013-B03,角度从”定义与选型”转为”故障定位”),尚未到期。
  • 建议主动关联复习(不占用今日配额,自行回顾):
    • Q-20261010-B05(最小代理原则、Agent 身份与代持令牌)→ 今日 S01 是这条原则在数据平面的落地。对照看:原则层讲”Agent 不能复用用户权限”,落地层讲”租户约束如何下推到检索与缓存”。
    • Q-20261013-B01(幂等、重试、退避与超时预算)→ 今日 S01 的”失败方向必须是拒绝服务而不是泄漏”是同一条思路在安全场景的应用。
    • Q-20261013-B03(混合检索融合与重排归因)→ 今日 S01 的 pre-filter / post-filter 取舍是同一批组件在隔离视角下的另一个切面。
  • LeetCode 轮换:已用 146(哈希表 + 双向链表)、3(滑动窗口)、215(堆 / 快速选择)、739(单调栈)。下次建议轮换:二叉树(遍历 / BST 性质)、二分查找(边界型)、基础动态规划(一维 / 背包),并避免再次使用栈与哈希表主导的模式。

五、本次运行元信息#

配置读取状态#

项结果
配置文件../题库配置.md
读取状态成功
实际版本4(last_reviewed: 2026-10-12)
暂停类型无
观察中的新技术未标记”允许试学”,本次未从中出题
使用配额非 Agent 基础 2 + Agent 基础 2 + Agent 场景 1 + 非 Agent 场景 1 + LeetCode 1 = 7 题

历史读取范围#

  1. 题目索引.md(19 条记录,覆盖 2026-10-10 至 2026-10-13)— 已读。
  2. 精选/README.md(2 条精选)— 已读,确认今日无到期复习题。
  3. 最近 30 天训练文件(3 份)— 通过索引 + 标题结构核查,确认无同题 / 换皮 / 考点过度集中。
  4. 最近一次周复盘 — 不存在(累计生成 4 次,未达 7 次阈值)。
  5. 更早的完整正文 — 未读取(索引信息充足,无需回溯)。

去重结果#

强去重(30 天窗口)全部通过:

维度检查结论
题目标题7 道题与历史 19 题无重复
一级类型首次覆盖操作系统与网络、软件工程与平台工程;其余类型均为不同二级知识点
核心考点CPU 限流 vs 异步并发(10-13 B02)机制层不同;API 契约 vs 幂等重试(10-13 B01)分属契约层与控制层
预期回答结论无重合
场景约束与故障现象跨租户泄漏(今日)vs 缓存雪崩(10-13 S02)vs 模型降级(10-13 S01)现象与根因均不同
LeetCode 题号739,历史 146 / 3 / 215,无重复
语义相似度已逐项比对,B03 与 10-10 B03(编排器-工作者)虽同属 AI Agent 核心,但二级主题(控制流模式 vs 上下文隔离)不同

标记为关联复习的题目:S01 关联 Q-20261010-B05(原则 → 落地)、Q-20261013-B03(检索质量 → 检索隔离),均已在正文中标明关联关系与角度差异。今日未生成任何”复习题”(无 31 天以上题目被改编复用)。

归档与索引#

  • 归档路径:2026-10/2026-10-14-AI-Agent-面试训练.md(相对题库根目录)
  • 写入验证:见下节
  • 索引更新:7 条记录已按题目 ID 幂等追加至 题目索引.md
  • 周期复盘:未触发(累计 4 次,阈值 7 次,预计 2026-10-16 前后达成)
  • 精选:未执行(未收到明确精选意图)

写入验证#

  • 文件存在且路径正确
  • 7 道题齐全(B01—B04、S01、S02、L01)
  • 每题答案完整(基础题 8 项要素、场景题 12 项要素、LeetCode 16 项要素)
  • LeetCode 参考代码可运行、5 个测试用例齐备
  • frontmatter 字段完整,配置版本 4
  • 未修改任何历史训练文件正文
  • 远端同步未验证——本文件仅确认写入本地 Obsidian 目录,未做任何远端同步校验

本文件由自动化任务生成于 2026-10-14(Asia/Shanghai)。题目与答案均为生成内容,status: generated 不代表已作答或已掌握。

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