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 |
| L01 | LeetCode | 栈与队列(单调栈) | 下一个更大元素的等待距离 | 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. 考察点
- 配额语义:cgroup 的 CPU bandwidth control 约束的是”每个周期内可消耗的 CPU 时间总量”,不是”可以并行跑几个线程”,更不是”独占 N 个核”。
- 限流与争抢的区分:throttling 期间线程被移出可运行队列、不消耗配额,因此表现为延迟突刺 + CPU 使用率封顶,这跟 CPU 争抢(排队等待)的曲线形状完全不同。
- 并发度错配:应用按宿主机核数推导线程池 / 进程池 / 连接数,而容器内实际配额可能只有 1—2 核。
- 观测归因:能否说出正确的指标(
container_cpu_cfs_throttled_seconds_total、throttled periods 占比),以及”CPU 不高但延迟高”这一判据。 - 与 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. 常见误区或错误用法
- “CPU 使用率没打满就没有 CPU 问题”——限流的典型特征恰恰是 CPU 用不满。
- 把
limits.cpu: 4当成”可以并行 4 个满速线程且无代价”——4 个线程会在每周期的前 1/4 时间烧完预算。 - 用
os.cpu_count()/nproc决定容器内并发度——读到的是宿主机拓扑。 - 只看平均 CPU 判断容量——限流的伤害在 P99 与周期性抖动上,平均值会把信号抹平。
- 用”加超时 + 加重试”掩盖限流——重试会再消耗一次配额,形成”限流→超时→重试→更严重限流”的正反馈,这是最常见的恶化路径。
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,你还能做什么?
回答要点:
- 削峰(首选):限制同时进入 CPU 密集路径的并发数(信号量),让消耗曲线变平,而不是让它突发。总 CPU 时间不变,但摊平后不会在周期内被烧完,因此不触发限流。这是成本为零的处置。
- 调整 period 而不是调 quota(已核验,来源:Linux 内核文档 CFS Bandwidth Control)。这是一个常被忽略的杠杆:周期越长,突发能力越强。同样的 1 核平均带宽,
quota=250ms / period=250ms比100ms / 100ms允许的连续突发时间更长;反之短周期换来的是更一致的延迟,代价是突发能力降低。所以如果瓶颈是”短暂突发被拦腰截断”而非”总带宽不足”,可以在保持平均配额不变的前提下放大 period。另外内核还有cpu.cfs_burst_us(cgroup v1),允许累积未使用的配额用于未来突发,默认 0(不累积)。 - 降单位成本:CPU 密集路径本身优化(换更快的解析库、批处理合并调用、减少序列化/反序列化、把纯 Python 循环换成向量化)。
- 迁移而非扩容:把该路径拆到独立 Deployment,用独立 quota 甚至按需 Job,避免与在线路径争抢同一预算。
- 接受并显式化:如果限流不可避免,就在 SLO 上分层——单独定该接口的 P99 目标,并在客户端设置合理 deadline,避免级联。
- 反模式要主动点名:把
limits直接去掉是最省事的”解法”,但它把问题从”可观测的限流”变成”不可控的节点超卖”,会让整个节点的邻居一起抖动,通常不可接受。同理,加重试会再次消耗配额,形成正反馈,绝不能作为解法。
补充一个容易答错的细节:限流不仅来自本层级自己的配额,父级 cgroup 配额耗尽同样会让子级被限流(即使子级自己还有余量)。所以在 Kubernetes 上排查时,不能只看 Pod 层指标,还要看节点与上层 QoS 层级的情况。
B02 API 与接口设计:兼容性、幂等与错误契约 Q-20261014-B02
1. 考察点
- 两个兼容方向:向后兼容(新服务端 + 老客户端)与向前兼容(老服务端 + 新客户端)——后者在滚动发布期间是硬性要求,最容易被忽略。
- 破坏性变更的完整清单:不只是”删字段”。
- 幂等语义契约:
Idempotency-Key、安全方法与幂等方法、重试红利由谁承担。 - 错误契约:稳定的机器可读错误码 vs 可变的消息、4xx/5xx 的可重试性、错误码不能靠字符串匹配。
- 分页契约:offset 分页在并发写入下的漂移、游标分页要求稳定且唯一的排序键。
2. 准确定义与适用边界
破坏性变更是指:已有的、符合契约的调用方,在新版本下会出现行为改变或失败。判据不是”服务端代码改动大小”,而是老调用方能否继续正常工作。
一份务实的破坏性变更清单:
- 删除或重命名字段、改变字段类型、改变单位或精度。
- 收窄取值范围或增加新的必填参数(放宽是兼容的,收窄不是)。
- 改变默认值语义(老客户端依赖旧默认值的路径会静默改变行为,这是最危险的一类,因为它不报错)。
- 改变错误码语义或把细分错误合并成泛化错误。
- 改变分页语义(如把 offset 翻页改成游标翻页、或改变默认排序)。
- 同步改异步,或改变超时语义。
- 扩展枚举值——对未做”未知值兜底”的客户端是破坏性的(老客户端穷举 switch 会直接抛异常)。
适用边界:兼容性契约约束的是跨进程边界。进程内的重构不算破坏性变更;内部服务与小流量服务可以承受更高的协调成本,但公开 API 与多方调用的中间层服务必须严格遵守。
3. 简洁示例
把 user.status: "active" | "disabled" 改成 user.active: boolean——字段名与类型同时变,是破坏性的。合规的演进路径是三阶段:
- Expand:新增
active字段,继续写status,两个字段同时返回。老客户端读status不受影响。 - Migrate:推动所有已知调用方切换到
active,用网关日志统计status字段的读取来源与量级,作为下线的准入条件。 - Contract:读取来源归零后再移除
status;若无法确认,保留字段但标记为 deprecated 并写清移除时间。
对比一个”看似安全其实危险”的例子:给订单状态枚举增加 REFUNDING。服务端认为这是 additive,但老客户端的状态机 switch 没有 default 分支,直接抛异常或展示为空——所以枚举扩展必须配套约定”未知值容忍”:客户端必须能安全处理不认识的值(跳过、展示为”处理中”、或按最邻近语义降级)。
4. 与相近概念的区别
- 向后兼容 vs 向前兼容:向后兼容是”新服务端不破坏老客户端”,向前兼容是”老服务端能容忍新客户端的请求”。滚动发布期间新旧版本必然共存,所以两个方向都要满足,这也是为什么”先部署服务端再部署客户端”的假设在持续部署下不成立。
- 兼容性 vs 一致性:兼容性是契约层面的演进能力,一致性是数据层面的正确性。双写期间两者都要处理,但手段不同。
- 幂等 vs 去重:幂等是接口契约(同样请求执行多次与一次效果相同),去重是实现手段(幂等键表、唯一索引)。没有幂等契约的去重会漏掉跨节点的并发重复。
- 版本化 vs 兼容演进:版本化是”承认不兼容并存多份契约”,兼容演进是”保持单一契约”。版本化不是免死金牌——多版本并存意味着安全补丁面、测试用例、监控口径全部翻倍。
5. 在 AI Agent 系统中的典型应用
工具的 JSON Schema 就是 API 契约,而模型是一个”不受你控制的调用方”,这三点决定了它比普通 API 更严格:
- 模型不会读变更日志。字段改名或删除,模型仍然按旧 schema 生成参数,失败表现为”工具调用参数校验失败”,而模型侧的应对常常是幻觉出别的参数名——错误会自我放大。
- 只能 additive:工具 schema 的演进应坚持”只增不删、必填只减不增、枚举扩展配默认值”,并在 description 里写清废弃字段。
- 错误契约要机器可读且可行动:返回
{"error_code": "INVALID_DATE_RANGE", "retryable": false, "hint": "..."}让模型能自我修正;只返回 “参数错误” 会让模型重试同样的参数,浪费轮次。 - 幂等对 Agent 尤为重要:Agent 的重试由模型触发、可能来自不同轮次甚至不同会话,没有
Idempotency-Key就无从去重,副作用会重复发生(下两次单、发两封邮件)。
6. 常见误区或错误用法
- “只加字段就一定安全”——严格 schema 校验的客户端(如开启了
additionalProperties: false)会拒绝未知字段。 - 把错误消息当契约——消息文案国际化或改写后,靠字符串匹配的客户端全部失效。错误码必须是稳定枚举。
- 用 POST 表达一切——丢失了幂等与缓存语义,也让重试责任落到调用方猜测上。
- 把版本化当万能解——
v1/v2并存的成本是长期的,且老版本往往成为安全债的藏身处。 - 忽略滚动发布窗口——只验证”新服务端 + 新客户端”,没验证混合状态,这正是线上事故的常见触发点。
7. 面试现场参考回答(1-2 分钟)
破坏性变更的判据不是改动大小,而是”符合旧契约的调用方在新版本下还能不能正常工作”。除了删字段、改类型这些明显的,有几类容易被漏掉:收窄取值范围、增加必填参数、改变默认值语义、合并或改变错误码、改变分页与排序、以及扩展枚举值——如果客户端穷举 switch 没有兜底,加一个枚举值就是破坏性的。
演进上我用 expand-migrate-contract 三阶段:先加新结构并保持旧结构可用(这一步要满足双向兼容,因为滚动发布期间新旧版本共存),再推动调用方迁移并用网关侧的字段访问日志作为下线准入,最后才移除旧结构。枚举扩展要配套”未知值容忍”约定。
配套契约还有三条:错误必须是稳定的机器可读错误码加上
retryable标记,不能让调用方靠字符串匹配;写接口要有Idempotency-Key,把重试红利放在服务端而不是让每个调用方自己实现去重;分页优先用不透明游标并要求稳定唯一的排序键,offset 分页在有并发写入时会漂移。版本化我会当最后手段,因为多版本并存意味着测试、监控和安全补丁面全部翻倍。
8. 面试官追问及回答要点
追问:不允许任何破坏性变更,但业务上必须”重命名一个字段”,怎么做?
回答要点:
- 不要原地重命名。新增目标字段,双写双读,旧字段 deprecated 但保留,用访问日志确认迁移完成后再考虑移除。
- 写侧:新字段为唯一真源,旧字段由兼容层派生(可由服务端写入或由响应序列化层生成),避免双写不一致。
- 读侧:明确优先级——新字段存在则用新字段,否则回落到旧字段。回落路径要有监控计数,回落数归零才是移除条件。
- 约束放宽而非收窄:如果必须删旧字段,先在响应里把它标记为 deprecated 并加
Sunset/Deprecation头,给明确的移除时间点。 - 对 Agent 工具 schema 的额外一点:模型不会读你的 deprecation 通知,所以要么永久保留旧字段作为别名,要么在 description 中把新字段描述得足够强以覆盖旧字段的触发条件,并保留一次参数校验层的自动改写(把旧参数名映射到新参数名),而不是直接报错。
B03 Planning / ReAct / Reflection 与确定性 Workflow 的边界 Q-20261014-B03
1. 考察点
- ReAct 的本质与代价:推理与行动交错,每步一次模型调用,轨迹长度随步数线性增长,错误沿轨迹累积。
- Planning 的价值与失效模式:计划漂移(plan drift)、何时触发重规划。
- Reflection 的收益边界:反馈信号必须是可验证的外部信号,而不是模型的自我感觉。
- 确定性 Workflow / 状态机:何时必须用它,而不是”让模型灵活一点”。
- 混合编排与护栏:最大步数、预算、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预算、timeoutdeadline、每步动作白名单。这三个缺一不可——只限制步数不限制 token,一次超长输出仍然能打穿预算。 - 反射点的位置:放在会产生不可逆副作用的动作之前,而不是之后。执行前审查是可撤销的,执行后审查只能补偿。
- 可观测性要求:轨迹必须记录每一步的输入摘要、模型输出、工具结果、耗时与 token。没有轨迹就无法区分”模型选错了工具”与”工具返回了错误结果”,两者的修复方向完全不同。
- 确定性兜底:金额计算、权限判定、状态机跃迁合法性校验必须放在模型之外的代码里。模型可以”建议”,不能”决定”。
6. 常见误区或错误用法
- “ReAct 步数越多越聪明”——步数与成本、延迟、错误累积都成正比。真正的优化方向是提高单步信息密度(更好的工具、更好的检索),而不是增加步数。
- 用 Reflection 修复事实性错误——没有外部证据的自我批判只是自我说服,还可能把正确答案改错。
- 把本可以确定性实现的逻辑放进模型——金额四舍五入、权限校验、日期解析。这类逻辑一旦由模型承担,就无法给出单测覆盖与审计结论。
- 没有最大步数与预算——故障表现通常是”用户等了 3 分钟然后超时”,且账单已经产生了。
- 计划一次性生成后从不重规划——尤其在会修改外部状态的任务里,这是 plan drift 的直接成因。
- 把 Reflection 当成永动的自我改进循环——没有终止条件和收敛判据的反思会无限自我否定。
7. 面试现场参考回答(1-2 分钟)
这四种不是并列的产品选项,而是同一条”自主度”轴上的不同位置,加上不同的控制流形态。选哪一种,取决于这个任务的步骤能不能预先枚举、以及某一步做错的代价有多大。
我的默认判断顺序是这样的:凡是涉及权限、金额、不可逆副作用、合规留痕的步骤,先用确定性 Workflow 固定下来,模型只在这些节点之间承担开放决策,比如从发票里提取字段、判断用户意图分类。理由是这些步骤错了要担责,而概率性组件无法给出确定性的保证。
对于确实无法预先枚举步骤的任务,比如分析财报找出毛利率下滑的原因,我用 ReAct,但必须配三个护栏:最大步数、token 预算、整体 deadline,缺一个都不行。如果有明确的全局结构,就先让模型出计划,人在执行前可以审查和修改——但一定要有重规划触发点,否则会出现计划漂移:执行改变了世界状态,原计划的前提不成立,系统却继续沿着过期计划走。
Reflection 的边界我要特别强调:它只在有外部可验证反馈时才有效——测试失败、schema 校验失败、检索到反例。没有新信息的”请检查你的回答有没有问题”不是反思,只是重新采样加自我说服,只会增加成本还可能把对的改错。另外反思点要放在不可逆动作之前,执行后的审查只能做补偿,成本高得多。
8. 面试官追问及回答要点
追问:具体到一个流程里的某个步骤,你怎么判断它应该”固化成代码”还是”交给模型”?
回答要点:
- 错判代价:错误是否可逆、是否有经济或合规后果。不可逆 + 有后果 → 代码。
- 正确性是否可判定:如果能写一个校验器(单测、schema、规则)在运行时判定对错,模型可用(因为错误会被确定性机制捕获);不能判定的开放判断 → 模型,但要有人工兜底。
- 输入空间是否闭合:输入枚举有限且规则稳定(如状态码映射、税率表)→ 代码,且要放进配置而非提示词;输入自由度高、需要语义理解(如从非结构化文本抽取原因)→ 模型。
- 变更频率与成本:规则每周变一次且由非工程人员维护 → 用配置/DSL + 模型做语义层,而不是硬编码也不是纯提示词。
- 延迟与成本约束:高频路径上的每一步模型调用都会乘以 QPS,通常要把高频的简单判断下沉到代码。
- 明确的反例:不要为了”更智能”把字符串格式校验、数值比较放进模型;也不要为了”确定性”让代码去理解用户为什么不满。
B04 Agent Skills 的渐进式披露与上下文成本 Q-20261014-B04
1. 考察点
- SKILL.md 的结构与 frontmatter 硬约束(字段名、长度、命名、与目录名的关系)。
- 渐进式披露的三层结构与各层 token 量级。
- 上下文成本的真实来源,以及如何为一支技能目录做预算。
- Skill 与 Tool / MCP / 子 Agent / 记忆的区别(这是最常被答混的地方)。
- 为什么 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 | 否 | 空格分隔的预批准工具串;实验性,各实现支持度不同 |
渐进式披露三层(官方给出的量级):
- 元数据层(约 100 tokens/技能):所有技能的
name+description在启动时全量加载,用于让模型判断”要不要用这个技能”。 - 指令层(官方建议 SKILL.md 正文 < 5000 tokens、文件 < 500 行):技能被激活后整份 body 加载。注意官方原话是”一旦决定激活,会加载整个文件”——所以 SKILL.md 的长度直接等于激活成本。
- 资源层(按需):
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. 常见误区或错误用法
- 把 Skill 当成安全边界——Skill 是提示词层面的指令,模型可以忽略、可以被注入内容误导、也不被运行时强制。授权边界必须放在模型之外(工具网关、策略执行点),不能依赖”我在 SKILL.md 里写了不要删文件”。
- description 写得像标题——失去”何时用”的信息,模型无法判断触发条件。
- 把全部细节塞进 SKILL.md——破坏了渐进式披露,激活成本线性上升,也让最重要的步骤淹没在细节里。
- 多层嵌套引用——官方要求引用保持一层深,深层链条会让模型读不到关键文件,表现为”技能明明写了但模型不照做”。
- 忽略
allowed-tools的实验性质——该字段各实现支持度不一,不能当作权限控制的正式机制使用。 - 用 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,怎么处理?
回答要点:
- 先算清楚账:对比总成本。20k tokens 相对于 200k 上下文约占 10%,是否真的构成瓶颈要用数据判断,不要凭直觉优化。真正的浪费往往来自误激活的 body。
- 分层路由:按领域把技能分组,先让路由层根据意图选组,只把该组的元数据放进上下文。这是技能版的两级索引。
- 裁剪需求侧:200 个技能中很多可能服务的是不同场景(不同产品线、不同工作流),应当按 Agent 类型裁剪可见技能集,而不是所有 Agent 共享一个全量目录。
- 压缩 description:在不损失”何时用”信息的前提下压缩到最小必要(官方上限是 1024 字符,但通常远不需要用满)。注意删关键词会直接降低召回。
- 治理手段:定期清理零命中技能(用激活日志做依据,而不是凭印象)。
- 明确不要做的事:不要为了省 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. 核心考点
- 无状态协议下租户上下文的传递位置与可信性——为什么不能让模型声明自己是哪个租户。
- 纵深隔离层次:网关身份 → 工具服务强制校验 → 存储行级安全 → 检索层预过滤 → 缓存键维度。
- 向量检索过滤的正确位置:预过滤(pre-filter)与后过滤(post-filter)在 ANN 场景下的取舍,以及”先召回后过滤”为什么既是质量问题也是安全问题。
- Hidden Context Exposure:工具响应、检索文档、记忆、应用状态都在机密性范围内;模型输出就是泄漏面。
- 审计与归因:跨租户访问尝试的检出与告警,而不是只在泄漏发生后追溯。
3. 作答框架(3-5 分钟可复用)
- 先给结论:这是数据平面的隔离缺失,不是身份认证问题;入口身份正确不代表查询受限。
- 分层定位:沿”身份 → 工具调用 → 检索 → 缓存 → 生成”五段逐层给出判定指标。
- 给出隔离重建方案:每一层的强制点放在哪里,为什么放在那里。
- 明确取舍:延迟预算怎么分配(≤50ms 要落在哪些操作上)、缓存命中率损失多少、哪些租户走硬隔离。
- 失败路径与兜底:校验失败时 FAIL-CLOSED 还是 FAIL-OPEN,各自的业务影响。
- 观测与验证:上线前怎么证明隔离生效(不是靠”没再发生泄漏”)。
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 过滤,且做结果归属断言 |
| ⑤ 生成 | 无泄漏检测 | 输出侧的跨租户实体检测 + 引用溯源强制 |
核心执行流程:
- 签发租户绑定令牌:入口认证后签发短 TTL(如 15 分钟)令牌,
tenant_id为签名 Claim,不可被客户端篡改。租户上下文不由模型生成,只能从令牌派生——这是最关键的一条原则。 - 在无状态 MCP 中传递租户上下文:2026-07-28 规范移除协议级会话后,状态不能由服务端保存,因此租户上下文必须每请求自描述。放在 HTTP 头(用于网关路由与审计)与
_meta(用于跨服务追踪与溯源)两处,各自承担不同职责。工具侧不信任任何一方自行声明的租户 ID,只认网关签名下来的 Claim。 - 工具服务侧的强制校验:每个 MCP 工具在处理前做两步——(a) 校验令牌签名与
tenant_id;(b) 查询执行时把tenant_id作为必要条件注入(不是可选过滤器)。返回结果再逐条断言归属,发现不属于该租户的记录直接丢弃并触发告警。 - 检索层:把 tenant 过滤从应用层下推到检索层。
- 结果归属断言:所有跨服务返回的数据在进入上下文之前做一次统一的归属校验(统一的
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-filter | 3—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. 常见误区
- 在提示词里写”不要泄露其他租户数据”——概率性约束不是访问控制,且不产生审计记录。这是把最后一个防线交给了模型。
- 只在入口做校验——入口日志会一直显示正确,泄漏却持续发生。修复必须下推到数据访问的每一层。
- 忽略语义缓存——共享缓存键没有租户维度时,它是一条绕过所有权限逻辑的数据通道,而且是最难发现的一条。
- 用 post-filter 后再靠”结果再检查一遍”解决——这把同一份数据在上下文里放了两次,且检查本身也是可能被绕过的第二实现;正确做法是 pre-filter + 统一归属断言。
- 把身份=用户身份——Agent 的身份是独立的非人类身份,代持令牌必须有自己的权限范围与审计标识,否则归因链断在”是系统做的”。
12. 三层能力划分(本题核心,必须说清)
- 模型本身的概率性能力:生成回答、判断是否引用、表面上的”不泄露”倾向。绝不能作为安全边界——它不可验证、不产生审计、会随模型版本漂移。
- Agent 编排与状态能力:决定调用哪些工具与检索、携带什么租户 Claim、上下文里最终留下什么。它能保证”流程上要求携带租户上下文”,但它自己也可能出错(编程错误会漏掉过滤条件)。
- 传统软件系统的确定性能力:令牌验签、归属断言、缓存键构造、pre-filter 执行、行级安全、审计落库。隔离的唯一可靠落点在这里——它的失败模式是可预测的拒绝服务,而不是静默泄漏。
13. 30-60 分钟小型实践
写一个最小复现与修复:
- 用 SQLite 建一张跨租户的
documents(id, tenant_id, content),插入两个租户各 20 条数据。 - 实现一个”错误版”检索:
SELECT * FROM documents ORDER BY bm25(content) LIMIT 5(不带 tenant 过滤),复现跨租户泄漏。 - 实现一个统一数据访问层
fetch_docs(query, tenant_ctx),强制注入tenant_id,并加assert_tenant(rows, tenant_id)。 - 实现一个语义缓存,第一版 key 只用
hash(query),复现跨租户缓存命中;改成hash(tenant_id, permission_tags, query)后验证不再命中。 - 写 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. 核心考点
- expand-migrate-contract 三阶段,以及每阶段为什么必须可逆。
- PostgreSQL DDL 的锁行为:哪些操作是元数据变更、哪些会重写表、哪些会全表扫描。
- 双写与回填的一致性:回填不能覆盖更新的行;并发写入的竞态。
- 批量回填的节流与复制延迟控制:为什么不能用
OFFSET,以及如何自适应节流。 - 回滚点与 contract 阶段的不可逆性。
3. 作答框架(3-5 分钟可复用)
- 结论先行:用 expand-migrate-contract,任何阶段都不做”删除列”的不可逆动作,直到确认无读取来源。
- 先讲锁:哪些 DDL 会锁表、用什么参数(
lock_timeout)兜住。 - 再讲一致性:双写顺序、回填与在线写入的冲突判据。
- 然后讲节奏:批量大小、keyset 分页、自适应节流、复制延迟闭环。
- 最后讲回滚与观测:每阶段的回滚点、触发阈值、验证方法。
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. 常见误区
- 用一次
UPDATE全表回填:长事务、复制延迟爆表、失败全回滚。 - 用
OFFSET分页做批量:后段批次线性退化,越跑越慢,最后看起来像”数据库变慢了”。 - 回填条件不带版本比对:用旧数据覆盖在线新写入,产生静默数据丢失——这类错误不会报错,只会让用户发现”我改的地址怎么又变回去了”。
- 在事务块里跑
CREATE INDEX CONCURRENTLY:它不允许在事务块中执行,强行写会报错;且失败会留下 INVALID 索引需要清理。 - 过早物理删列:contract 阶段最危险的动作是
DROP COLUMN,它是唯一不可逆的一步,应该独立排期并有明确的准入证据。
12. 30-60 分钟小型实践
用 Docker 起一个 PostgreSQL 15:
- 建
orders(id, address text),用generate_series灌 200 万行随机地址文本。 - 加 6 个结构化列(可空),设置
lock_timeout = '3s'观察 DDL 行为;对province建索引时用CONCURRENTLY,并观察它能否在事务块中执行。 - 写一个 keyset 分页的回填脚本(批 1000,独立提交),打印进度与速率。
- 模拟并发写入:另一个进程随机更新某些行的
address与updated_at,验证你的回填条件是否会覆盖新值。 - 实现一个一致性校验查询,统计新列为空的行数与解析失败数。
- 用
pg_stat_replication观察复制延迟(单实例可改为观察 WAL 生成量pg_current_wal_lsn()差值)。
L01 LeetCode 739:每日温度 Q-20261014-L01
1. 基本信息
| 项 | 内容 |
|---|---|
| 题目 ID | Q-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^530 <= 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. 关键边界情况
- n = 1:返回
[0]。两个版本都天然正确。 - 严格递减:
[90,80,70,60]→[0,0,0,0]。栈增长到长度 n,是空间最坏情况。 - 严格递增:
[30,40,50,60]→[1,1,1,0]。每次都把栈清空,while 循环总计执行 n-1 次。 - 全部相等:
[30,30,30]→[0,0,0]。必须注意”更高”是严格大于,相等不算,因此从左往右版本用<是正确的;若误写成<=会把相等的日子错误结算。 - 重复温度:
[70,70,71]→[2,1,0]。两个 70 都在栈中,被 71 依次结算。 - 最坏规模: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) 结构。识别信号有三个:
- 问题要求为每个元素找一侧最近的 / 第一个满足某单调条件的元素;
- 候选集合天然具有单调性(反证法可证明);
- 每个元素只会被结算一次,可以”用完即弃”。
这个”待结算队列 + 单调性保证队列有序”的模式同样适用于:滑动窗口最大值(239,单调队列)、柱状图最大矩形(84)、接雨水(42)、股票价格跨度(901)。
与 AI Agent 工程的关系:联系较弱,不宜强行类比。真正有价值的是同一个工程直觉——用”尚未解决的有序待办队列”替代”每来一个事件就全量扫描”,这在 Agent 侧的等待队列、超时扫描、滑动窗口限流(把 O(n) 的窗口重算换成增量结算)中是同一套思路。这里不做进一步牵强映射。
三、延伸资料
以下资料按可信度排序,标注核验状态。
操作系统与网络(B01)
- 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)、层级限流的两种触发方式,以及”周期越长突发能力越强、周期越短延迟越一致”的明确表述。 - Kubernetes 官方文档 — Resource Management for Pods and Containers:
resources.limits.cpu与requests的语义与 QoS 分类。本轮未核验具体页面,引用时请以当前版本文档为准。
软件工程与平台工程(B02 / S02)
- PostgreSQL 官方文档 — ALTER TABLE 与 CREATE INDEX CONCURRENTLY,
lock_timeout参数说明。本轮未核验具体页面;其中”加带非 volatile 默认值的列只改元数据”这一行为与版本相关,实际使用请以对应版本的官方 Release Notes 为准,不要凭记忆引用。 - Google AIP(API Improvement Proposals)中关于兼容性与分页的约定:属于可信技术组织的公开规范,本轮未核验具体条目编号。
Agent Skills(B04)
- Agent Skills 官方规范 — https://agentskills.io/specification — 已核验(2026-10-14)。本页 B04 中所有字段名、字符上限、命名约束、三层渐进式披露的 token 量级与”一层深”引用建议均出自此页。
- Agent Skills 参考实现与校验库 — https://github.com/agentskills/agentskills/tree/main/skills-ref — 官方规范页指向的校验工具,本轮未核验仓库当前状态。
Multi-tenancy / 安全(S01)
- Model Context Protocol 官方规范与扩展总览(2026-07-28 版本及 changelog):无状态核心、
_meta追踪上下文传播、Tasks、授权扩展。来源沿用题库配置.mdv4 变更记录中已标注的官方来源,本轮未重复核验。
未核验说明:上述标记为”未核验”的条目仅作为延伸阅读方向列出,不构成本文件的事实引用。需要作为面试论据使用时,请先自行核验。
四、当日重点与复习建议
今日重点(按优先级)
- 隔离边界必须落在确定性机制上,不能落在提示词里(S01 + B04 共同指向)。Skill 是行为规范不是安全边界;“不要泄露其他租户数据”写在提示词里不产生任何可验证保证,也不产生审计记录。这是今天最重要的一条。
- “入口正确”不等于”数据受限”(S01)。网关身份正确、MCP 头正确,但检索和缓存没有租户维度,泄漏照样发生。修复要下推到每一次数据访问的最内侧,并且各层独立 enforce。
- 语义缓存的键必须包含租户与权限维度(S01)。这是本题中最容易被忽略、也是成本最低(零延迟)的修复项。
- CPU 配额是带宽约束,不是并行度约束(B01)。“CPU 用不满但 P99 周期性抖动”是限流的指纹;处置的第一优先级是基于配额推导并发度,而不是加重试。
- 自主度是可调的连续谱,不是产品选项(B03)。权限、金额、不可逆副作用必须固化成代码;Reflection 只在有外部可验证反馈时有效。
- 契约演进的三阶段与双向兼容(B02 / S02)。滚动发布期间新旧版本共存,所以向前兼容和向后兼容都要满足;contract 阶段的物理删列是唯一不可逆动作,必须独立排期。
- 单调栈的本质是”待结算队列 + 单调性保证有序”(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 题 |
历史读取范围
题目索引.md(19 条记录,覆盖 2026-10-10 至 2026-10-13)— 已读。精选/README.md(2 条精选)— 已读,确认今日无到期复习题。- 最近 30 天训练文件(3 份)— 通过索引 + 标题结构核查,确认无同题 / 换皮 / 考点过度集中。
- 最近一次周复盘 — 不存在(累计生成 4 次,未达 7 次阈值)。
- 更早的完整正文 — 未读取(索引信息充足,无需回溯)。
去重结果
强去重(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不代表已作答或已掌握。