17399 字
87 分钟
2026-11-09 AI Agent 面试训练
AI Agent 面试训练
2026-11-09

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

今日学习主题#

代价转移:每一次压缩、共享与去中心化,都会把代价挪到别处,也必须在新位置重新定价。

存储引擎把随机写换成后台合并带宽,量化把显存换成功率位宽与算子覆盖,跨组织协作把信任换成可验证的凭证与合同,分布式 ID 把全局唯一换成”时钟 + 租约”这两个外部假设。今天四道非算法题的共同追问是同一句:你把代价挪到了哪里,那里有人看着吗?

题目目录#

编号题目题型一级类型难度
B01写快了,读和空间还了吗:B-tree 与 LSM-tree 的三种放大非 Agent 基础数据库与存储基础题(中高)
B02从 16 位到 4 位:量化省下的到底是显存还是时间Agent 基础AI 与大模型基础基础题(中高)
S01对方 Agent 说自己能做什么:跨组织协作的信任锚点与故障隔离Agent 场景AI 安全与治理(兼工具与互操作协议)场景题(高)
S02时钟回拨了 3.2 秒:分布式 ID 的唯一性锚点与号段兜底非 Agent 场景分布式系统(兼数据库与存储)场景题(高)
L0154. 螺旋矩阵:边界收缩与四条边的收尾判定LeetCode数组与字符串(矩阵模拟)medium

题目速览#

  • B01 非 Agent 基础 · 数据库与存储 · 基础题(中高)——B-tree 与 LSM-tree 的读写路径差异,以及读放大、写放大、空间放大分别从哪里来。
  • B02 Agent 基础 · AI 与大模型基础 · 基础题(中高)——权重-only 与权重+激活量化的收益差别,异常值为什么逼出混合精度,精度评测为什么不能只看困惑度。
  • S01 Agent 场景 · AI 安全与治理(兼工具与互操作协议)· 场景题(高)——三家供应商的 Agent 通过 A2A 协作,能力自述与实际不符、重试造成重复下单、审计无法归因,如何重建信任边界与舱壁。
  • S02 非 Agent 场景 · 分布式系统(兼数据库与存储)· 场景题(高)——NTP 回拨 3.2 秒导致发号拒绝与下游漏单,如何在唯一性、单调性与可用性之间定价。
  • L01 LeetCode · 数组与字符串(矩阵模拟)· medium——螺旋遍历的四条边收缩,以及最后一行/最后一列的重复访问判定。

B01#

  • 题目 ID:Q-20261109-B01
  • 编号:B01
  • 题型:非 Agent 基础定义题
  • 一级类型:数据库与存储
  • 二级主题:存储引擎与索引结构(B-tree / LSM-tree 的放大与选型)
  • 难度:基础题(中高)
  • 标签:数据库与存储、存储引擎、LSM-tree、B-tree、写放大、Compaction

题目#

同一个业务要落两个库:一个是订单库,单行频繁更新、按主键点查与少量范围查询、需要事务;一个是设备指标库,每秒 80 万条追加写入、按时间范围扫描、几乎不更新、允许 TTL 删除。团队里有人说”LSM 写入快,两个都用 LSM 就行”,也有人说”B-tree 更成熟,两个都用 B-tree”。

请说明:B-tree(含 B+tree)与 LSM-tree 在写路径上的根本差异是什么?读放大、写放大、空间放大分别从哪里产生?在什么条件下”LSM 写更快”这句话不成立?

先思考#

  1. 先问自己:一次 UPDATE 在 B-tree 里发生了什么?在 LSM 里又发生了什么?两者谁做了”原地”操作,谁做了”追加”操作?
  2. 再问:追加写省掉的随机 I/O,被哪一类后台工作还了回去?这类工作的资源来自哪里?
  3. 最后问:如果读请求要跨多个层级找同一个键,靠什么把无效查找挡掉?挡不掉的时候,代价体现为什么指标?

参考答案#

结论先说:B-tree 用”原地更新 + 一次定位”换取确定的读成本,代价是随机写与页分裂;LSM-tree 用”顺序追加 + 后台合并”换取写吞吐,代价是把成本转移到读放大、空间放大和 compaction 占用的磁盘带宽上。所以”LSM 写更快”只在写放大可被后台合并消化、且读路径能靠布隆过滤器与缓存挡住多层查找时成立。

定义与适用边界。B-tree 是平衡多路搜索树,B+tree 把数据集中在叶子层并用叶子链表串起范围扫描,内部节点只存键。更新是原地修改页,页满则分裂,因此写路径包含随机读页、修改、写回,并伴随 WAL 保证崩溃恢复。LSM-tree 把写入先进内存 memtable,写满后顺序刷成 L0 的 SST 文件,后台 compaction 把重叠的 SST 逐层合并成更大的有序文件;删除与更新都表现为追加一条新记录(墓碑或新版本),旧版本在合并时才真正消失。

三种放大的来源。读放大:一次点查可能要依次查 memtable、L0 的多个重叠文件、L1 及更深层,层数越多、L0 文件越多,查找次数越多;布隆过滤器只能排除”键不存在”,对存在的键仍需逐层定位。写放大:同一份数据在生命周期内被 compaction 反复读写多次,一次用户写入最终可能变成数倍的磁盘写入。空间放大:旧版本与墓碑在被合并前一直占空间,叠加多层冗余副本,磁盘占用可以是逻辑数据量的若干倍。RocksDB 官方文档的说法是:LSM 的整体写吞吐直接取决于 compaction 能跑多快;Level 风格压缩优化的是空间放大,Universal 风格优化的是写放大但读放大与空间放大更高——三种放大不可能同时最优,压缩策略就是在它们之间选一个点。

为什么订单库该选 B-tree。订单是”读多、点查、要事务、要确定的尾延迟”。B-tree 的读成本与树高线性相关,一次点查的路径长度可预测,配合缓冲池命中后延迟稳定;而 LSM 的点查尾延迟受 compaction 抖动影响,一次大的 compaction 会抢走磁盘带宽,表现为 P99 毛刺。事务与唯一约束在 B-tree 实现上也是原生能力。

为什么指标库该选 LSM。80 万条/秒是写主导负载,且按时间范围扫描、几乎不更新。LSM 把随机写变成顺序写,天然适配这种 append-only 形态;TTL 删除在 LSM 上是廉价的(墓碑 + compaction 丢弃整段旧数据),而在 B-tree 上是昂贵的逐行删除加索引维护。

“LSM 写更快”失效的三种条件。第一,写放大已经吃满磁盘带宽:当 compaction 跟不上写入速率,L0 文件堆积,写入会被反压甚至 stall,此时实测写吞吐反而低于 B-tree。第二,工作集小、更新集中在少数热行:B-tree 的更新全部命中缓冲池,几乎没有随机 I/O,LSM 却要持续产生新版本并反复合并同一批热键。第三,读多写少且要求稳定尾延迟:LSM 的读放大与 compaction 抖动会直接体现在 P99 上,收益为负。

面试现场回答(1 至 2 分钟)。B-tree 是原地更新,一次写要定位页并可能分裂,读路径确定;LSM 是追加写加后台合并,写吞吐高,但把成本转移到读放大、空间放大和 compaction 的磁盘带宽上。所以选型看三件事:读写比、是否需要确定的尾延迟、以及更新是否集中。订单这种点查加事务的场景用 B-tree;高吞吐追加、按时间范围扫描、可 TTL 的场景用 LSM。判断”LSM 更快”是否成立,要同时看 L0 文件数、compaction pending bytes 和写入 stall 次数,只测平均写吞吐会得出错误结论。

追问#

追问:如果指标库既要高写入,又要按设备 ID 点查最近 1 小时的数据,怎么办?

回答要点:这是”写主导 + 点查尾延迟”的混合负载。三种处理:一是键设计成 (device_id, timestamp) 前缀,让同一设备的近期数据在 SST 中物理相邻,点查退化为小范围扫描;二是给点查路径加一层读缓存(近期热设备),把 LSM 的多层查找挡在缓存之外;三是接受读放大,用 compaction 策略换取更少的层数(例如减少层数、增大每层容量),代价是写放大上升。关键是把”点查延迟”和”写入吞吐”分别给出 SLO,再用 compaction 参数在这两个 SLO 之间找可行点,而不是追求单一指标最优。

关键点#

  1. B-tree 的核心成本是”定位 + 原地写”,LSM 的核心成本是”追加 + 合并”;两者的成本项不同,不能只比较写入那条路径。
  2. 三种放大互相牵制:优化写放大通常抬高读放大或空间放大,压缩策略的选择本质是在三者间取点。
  3. LSM 的写吞吐上限由 compaction 带宽决定,不由写入路径本身决定;写入 stall 是它失效的直接信号。
  4. 布隆过滤器只解决”键不存在”的无效查找,对存在的键和范围扫描没有帮助。
  5. 选型判据是读写比、尾延迟确定性、更新局部性、删除语义(TTL 批量删除 vs 逐行删除)。

常见误区#

  1. “LSM 顺序写所以一定比 B-tree 快”:错在把用户写入路径等同于全链路成本。compaction 是同一份数据的多次重写,写放大到 10 倍以上时,顺序写的优势会被合并带宽吃掉,实测吞吐可能低于 B-tree。
  2. “SSD 随机写便宜,所以 B-tree 的随机写不是问题”:错在忽略写放大与寿命。SSD 的随机写仍要走 FTL 的垃圾回收,长期高随机写会放大写放大并加速磨损;同时 B-tree 的页分裂带来额外的空间碎片。
  3. “compaction 是后台任务,不影响在线请求”:错在资源是共享的。compaction 与前台读共享磁盘带宽与 page cache,大合并期间读 P99 会明显抬升;这也是很多 LSM 系统把 compaction 限速、并单独监控 pending compaction bytes 的原因。

延伸学习#

  1. RocksDB 官方 Wiki《RocksDB Overview》(github.com/facebook/rocksdb/wiki/RocksDB-Overview):memtable → L0 → 逐层合并的写路径、Level 与 Universal 压缩在空间放大与写放大之间的取舍、写吞吐取决于 compaction 速度。
  2. O’Neil 等《The Log-Structured Merge-Tree (LSM-Tree)》(Acta Informatica, 1996):LSM 的原始论文,用于理解”多级合并”的成本模型;具体页码与公式本轮未逐条核验。

B02#

  • 题目 ID:Q-20261109-B02
  • 编号:B02
  • 题型:Agent 基础定义题
  • 一级类型:AI 与大模型基础
  • 二级主题:量化(权重/激活位宽、量化粒度、异常值与精度评测)
  • 难度:基础题(中高)
  • 标签:AI-与大模型基础、量化、W8A8、W4A16、混合精度、推理成本

题目#

某 Agent 产品的工具调用模型从 FP16 换成了 4 位权重量化版本,显存占用从 140 GB 降到 40 GB,团队据此认为”推理成本降了 3.5 倍、延迟也会同步下降”。上线后编码类任务的结构化输出成功率从 99.2% 降到 96.4%,长上下文场景的错误率更高,而单请求延迟只降了 12%。

请说明:量化究竟改变了什么、没有改变什么?为什么”权重-only 量化”在某些负载上几乎不提速?判断一个量化版本能不能上线,应该看哪些指标?

先思考#

  1. 先分清两个资源:显存容量(放得下多少参数、多少 KV Cache)与显存带宽 / 算力(每个 token 要搬多少字节、做多少次乘加)。解码阶段与预填阶段谁更受限于哪一个?
  2. 再想异常值:如果某几维激活值的量级是其他维的上千倍,把它们一起映射到 4 位整数会发生什么?业界的应对手段是什么?
  3. 最后想评测口径:困惑度(perplexity)不涨,能不能说明结构化输出不会退化?

参考答案#

结论先说:量化改变的是”每个参数占多少位”,因此直接降低显存容量与权重搬运字节数,但不改变模型的推理语义——它引入的误差是不可逆的,且收益强依赖算子支持与负载形态。权重-only 量化在解码阶段收益明显、在大 batch 预填阶段几乎无效;能否上线不能看困惑度,要看任务级指标(含结构化输出合法率、长上下文与工具参数正确率)与真实延迟分布。

定义与适用边界。量化是把浮点张量映射为低位宽表示:x ≈ s · (q − z),其中 s 是缩放因子、z 是零点(对称量化时为零)。按对象分为权重-only(如 W4A16、W8A16)与权重+激活(如 W8A8);按粒度分为 per-tensor、per-channel、per-group(如 group_size=128)。粒度越细,误差越小,但元数据与算子开销越大。量化的边界是:它只是数值近似,不增加也不减少模型的知识;如果任务本身处在模型能力边缘(复杂代码生成、多步工具参数拼装),量化误差会把”勉强正确”推过临界点。

为什么收益要分开算。解码阶段每生成一个 token 都要把全部权重从显存搬到计算单元,是访存受限:权重从 16 位降到 4 位,搬运量降到约 1/4,延迟随之下降,这是权重-only 量化的主要收益。预填阶段处理的是整段输入,矩阵乘的算术强度高,是计算受限:权重位宽下降但不改计算精度(仍反量化为 16 位做乘加),算力收益接近零。这就是”单请求延迟只降 12%“的典型成因——负载里预填占比高、或并发大导致计算成为瓶颈。反过来,W8A8 才有可能真正用上低位宽整数算力,但需要硬件与 kernel 支持。

异常值为什么逼出混合精度。Transformer 的激活里存在系统性的极端大值通道,这些通道对注意力和预测贡献很大。若把它们和其他通道一起按同一 scale 量化到 8 位甚至 4 位,误差会破坏模型行为。LLM.int8() 的做法是向量级量化(每个内积独立归一化常数)加混合精度分解:把异常值维度单独抽出来用 16 位计算,其余 99.9% 以上的数值仍在 8 位下计算,从而在 175B 规模上做到”内存减半、性能不降”。这条经验的关键不是”8 位够用”,而是”必须为异常值留一条高精度旁路”。

在 Agent 系统中的应用。量化模型在 Agent 里承担的是”决策与参数生成”:输出要被 schema 校验、被工具网关消费。误差在这里的放大路径很具体——少一个必填字段、把枚举值写成近义字符串、把数值参数的单位写错。因此量化上线必须配套三件事:严格 schema 校验(约束采样或服务端校验)、关键路径的修复式重试、以及按工具维度的参数正确率监控(不是整体成功率)。另外,KV Cache 是否量化是独立决策:它影响长上下文容量与带宽,但对短对话几乎无收益,且更容易损伤长距离注意力。

评测口径。至少四层:一是困惑度/负对数似然(便宜但只反映语言建模);二是任务级断言指标(结构化输出合法率、工具选择准确率、参数完全匹配率);三是分层切片(长上下文、代码、多语言、数值推理);四是真实延迟与吞吐分布(TTFT、TPOT、P99,按预填/解码比例分桶)。任何一层单独通过都不构成上线依据。

面试现场回答(1 至 2 分钟)。量化是把权重或激活映射到更低位数,省的是显存容量和权重搬运字节,不省”推理正确性”。解码是访存受限,所以权重-only 量化能明显降延迟;预填是计算受限,权重-only 几乎不提速,要大 batch 才有收益得靠 W8A8。误差的关键来源是激活里的异常值通道,主流做法是给它们留高精度旁路,比如 LLM.int8() 的混合精度分解。上线判据不能只看困惑度,要看结构化输出合法率、工具参数完全匹配率、长上下文切片,以及按预填/解码分桶的延迟分布。在 Agent 里我还会把 schema 校验和修复式重试当作量化版本的必备配套。

追问#

追问:既然 4 位量化误差更大,为什么很多团队仍然选 4 位而不是 8 位?

回答要点:因为目标函数不同。选 8 位通常是为了”精度几乎无损地省一半显存”,选 4 位是为了”把模型塞进单卡/端侧,或把单位 token 的权重带宽再压一半”,此时接受精度损失是明确的产品决策。工程上的判断顺序是:先看是否被显存容量卡住(放不下就谈不上延迟),再看是否被带宽卡住(解码受限则 4 位有直接收益),最后看任务对误差的敏感度(结构化输出、数值计算、长上下文最敏感)。如果 4 位在关键切片上掉点超过阈值,可退到混合方案:敏感层保持高精度、其余层 4 位,或用 4 位权重 + 更高精度激活。

关键点#

  1. 量化省的是位宽带来的容量与带宽,不是计算量;收益方向由负载是访存受限还是计算受限决定。
  2. 权重-only 量化对解码有效,对预填和大 batch 计算几乎无效;W8A8 才可能带来算力收益,但依赖 kernel 与硬件。
  3. 异常值通道是量化误差的主要来源,“混合精度旁路”是主流对策,不能简单归结为”8 位足够”。
  4. 量化粒度和 group size 是精度与开销的调节旋钮,越细越准但元数据与算子开销越大。
  5. 评测必须分层:语言建模指标、任务级断言、敏感切片、真实延迟分布,缺一层就会漏掉上线风险。

常见误区#

  1. “4 位就是 4 倍加速”:错在混淆容量与速度。位宽降 4 倍只意味着权重体积降 4 倍;实际延迟取决于是否访存受限、是否有对应的低位宽 kernel、以及反量化开销,常见结果是延迟改善远小于 4 倍。
  2. “困惑度没掉就能上线”:错在指标与任务不匹配。困惑度是平均意义上的语言建模损失,对”少一个 JSON 字段”这类离散错误不敏感,而后者在 Agent 里会直接变成工具调用失败。
  3. “量化是纯推理侧改动,不影响系统设计”:错在忽略了误差的下游放大。量化后的模型更需要服务端 schema 校验、修复式重试与按工具维度的监控,这些是系统设计的一部分,不是模型侧可以单独承担的责任。

延伸学习#

  1. Dettmers 等《LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale》(arXiv:2208.07339,NeurIPS 2022):向量级量化 + 混合精度分解,异常值维度用 16 位、其余 99.9% 以上数值用 8 位,175B 规模内存减半且性能不降。
  2. Hugging Face Transformers 文档的 Quantization 概览(huggingface.co/docs/transformers/main/en/quantization/overview):bitsandbytes、GPTQ、AWQ 等方案的定位与适用场景;本轮仅核验了页面存在与主题范围,具体 API 细节未逐条核验。

S01#

  • 题目 ID:Q-20261109-S01
  • 编号:S01
  • 题型:AI Agent 真实研发场景题
  • 一级类型:AI 安全与治理(兼工具与互操作协议)
  • 二级主题:跨组织多 Agent 协作的信任边界、审计与故障隔离
  • 难度:场景题(高)
  • 标签:AI-安全与治理、工具与互操作协议、A2A、Agent-Card、跨组织信任、故障隔离、审计归因

题目#

业务背景:一家工业设备厂商的采购 Agent 负责询价、比价与下单准备。它不直接对接供应商系统,而是通过 A2A 与三家外部 Agent 协作:供应商 A 的报价 Agent、供应商 B 的合同条款 Agent、物流商 C 的运力 Agent。采购 Agent 只做编排与决策,价格与条款由对方 Agent 返回。

系统规模:日均 1.2 万次协作任务,峰值 40 次/秒;单任务平均跨 2.3 个外部 Agent,最长的合同评审链跨 3 家、含 7 次往返;外部 Agent 由对方独立运维,版本与可用性不受本方控制。

当前架构:本方通过一个统一的 Agent 网关出网,网关持有与每家共享的 API Key;对方的 Agent Card 由对方托管在公开 URL;本方在接入时人工阅读卡片、把”能力清单”抄进自己的提示词与路由表;任务状态存在本方会话里,跨组织调用是同步 HTTP 等待;审计只记录”网关向某 URL 发起了请求”。

目标:在不要求对方改造协议核心的前提下,把跨组织协作做成可验证、可归因、可隔离、可终止的形态;单家外部 Agent 故障时,本方任务不能整体挂死。

限制条件:一是跨组织数据最小化,采购价与设备清单属于敏感信息,不能整包透传给非必要方;二是本方合规要求”任何影响下单的决策必须能定位到具体执行方与凭证”;三是对方不接受为本方单独做版本分支;四是端到端 P95 目标 8 秒,合同评审链允许放宽到 60 秒。

故障现象:近两周出现三类问题。第一,供应商 B 的 Agent Card 声称支持”批量条款生成”,实际在对方接口变更后返回的字段结构变了,本方解析失败,编排层按”可重试”处理,连续重试 5 次,导致 B 侧生成了 3 份重复的报价单草案,其中一份被人工误发。第二,物流 C 的 Agent 一次超时 18 分钟,本方同步等待,会话全部挂起,前端显示”处理中”,用户重复提交。第三,事后对账时,本方只能证明”网关调用过 B 的端点”,无法证明是哪一次任务、哪个凭证、以什么参数生成了那份草案。

需要取舍的问题:信任强度与接入成本之间如何取点(让对方提供签名卡片与入驻核验,还是继续靠人工阅读自述);同步等待与异步任务句柄之间如何选;跨组织追踪是共享 trace 还是各自审计后对齐;授权是按任务一次性给足,还是逐步最小化。

先思考#

  1. 先分清”能力声明”与”能力事实”:Agent Card 是对方自述的清单,谁能为它的真实性背书?本方在什么时点、用什么手段把自述变成事实?
  2. 再分清”调用失败”与”结果不可用”:字段结构变化是协议层错误还是语义层变化?把它归入”可重试”会发生什么?
  3. 最后分清”谁的账”:一份被误发的报价单,责任链上有几个主体?审计要记录哪些字段才能把决策落到具体主体与凭证上?

参考答案#

结论先说:这个问题的核心不是”接不接 A2A”,而是把信任从”阅读对方的自述”改为”验证对方的能力事实”,并把一次跨组织协作变成有边界、有凭证、有终态的任务对象。具体做法是四层:身份与凭证层(双向认证 + 按任务签发、可撤销的委托凭证)、契约层(能力探测走运行时发现而非卡片抄写、结果为带版本与幂等键的结构化对象)、隔离层(舱壁、超时分层、异步句柄与预算)、审计层(以任务 ID + 凭证指纹 + 归一化参数为三元组的决策台账)。四层里没有一层依赖”对方 Agent 说得对”。

作答框架(3 至 5 分钟)。第一分钟定边界:谁对什么负责、这次协作的承诺是什么(唯一、及时、可归因)。第二分钟给结构:身份与凭证 → 契约与版本 → 隔离与预算 → 审计与对账,四层各一句话。第三分钟讲失效:声明漂移、超时无界、归因缺口分别怎么被这四层挡住。第四分钟讲取舍与验证:信任强度换接入成本、同步换异步,各自的指标是什么。最后一句收在”哪一层是确定性的、哪一层允许概率性失败”。

问题拆解。三类故障对应三个不同的失效面。第一类是声明漂移:卡片是接入时抄下来的静态文本,对方接口变更后卡片没变或本方没重新拉取,能力事实与声明脱节;更根本的是,卡片本身只是”自描述清单”,A2A 规范明确它是 agent 的自我描述(identity、capabilities、skills、security requirements),签名(JWS,RFC 7515 格式)是可选字段,签名只证明”这份卡片由某私钥签发”,不证明”对方真的具备该能力、也不会作恶”。第二类是等待无界:同步 HTTP 把本方会话寿命绑在对方响应上,对方 18 分钟不返回,本方就挂 18 分钟;重试又把”等待”放大成”重复副作用”。第三类是归因缺口:审计只到端点级别,缺任务与凭证维度,一旦发生争议无法证明”谁用什么参数做了什么”。

核心方案。

身份与凭证上,出网调用改为双向认证(mTLS 或 OAuth2 client credentials + 短期令牌),并以每任务一个委托凭证的方式传递:凭证里带 task_id、允许调用的能力白名单、参数约束(例如只允许查询本任务涉及的料号)、有效期与最大调用次数。这样”授权”从”网关有一个 Key 能调所有东西”收敛为”这次任务只能做这几件事”,且撤销是按任务粒度生效。

契约上,接入时不再把卡片能力抄进提示词,而是连接后调用运行时的能力发现接口,以运行时值为准,并把卡片仅作为”预连接线索”(预连接阶段用来决定要不要连,不用于路由与授权)。对每次调用的响应做强校验:schema 版本号、必填字段、枚举取值、金额与单位;校验失败按”语义错误”处理,不进入通用重试,而是走”降级 + 人工可见的失败终态”。

隔离上,把每个外部 Agent 当成独立的故障域:独立连接池与并发上限(舱壁)、独立超时(单跳 3 秒、整链 60 秒预算)、独立熔断(按错误类型分桶),长耗时调用改为异步任务句柄(提交返回 task 句柄,轮询或回调,带截止时间),本方会话不再被对方响应时间绑架。重试只对”明确未执行”的错误进行(连接失败、限流、幂等键可证明的重复请求),对”可能已执行”的超时只做查询不做重发。

审计上,每次跨组织调用写一条台账:task_id、caller_agent、callee_agent、凭证指纹、归一化后的参数摘要、schema 版本、结果状态(成功 / 语义失败 / 未决)、是否降级。归一化函数必须与执行侧共用同一个实现,否则”审计记录的对象”和”实际生效的对象”会分叉。

执行流程(一次合同评审链)。编排层生成 task_id 与幂等键 → 按数据最小化原则裁剪上下文(只给 B 需要的条款片段,不给整包报价)→ 申请该任务的委托凭证 → 调用 B 的条款生成(异步句柄)→ 轮询并校验 schema 与版本 → 结果写入本方任务状态(不是直接写业务库)→ 需要 C 的运力时,用新的凭证调用 C,不复用 B 的凭证 → 全链完成或到达截止时间后收敛为终态(成功 / 部分成功 / 失败待人工),并把终态与台账一起落库。

技术选型与依据。身份:优先 mTLS(传输层双向认证,不依赖应用层自述)+ 短期委托令牌(能力与参数约束);备选是 OIDC 联邦 + 令牌交换,代价是引入对端 IdP 的可用性依赖。契约:运行时能力发现 + 服务端 schema 强校验;备选是”接入时生成客户端 stub 并锁版本”,代价是对端升级即失效。异步:任务句柄 + 轮询(间隔按状态分级、带抖动);备选是回调(webhook),代价是本方要暴露可被外部访问的端点,安全面扩大。审计:本方统一台账 + 对端台账按 task_id 对齐;备选是共享 trace(W3C traceparent 透传),代价是跨组织暴露内部调用结构,需先做采样与字段脱敏。

方案取舍。信任强度 vs 接入成本:要求签名卡片 + 入驻核验(对方提供 JWKS、本方缓存并按 kid 轮换)会把新供应商接入从半天拉长到一周,但能把”卡片伪造”和”卡片被中间人替换”这两类风险降到可控;对低风险、低金额的供应商可以先走”人工阅读 + 运行时校验”,把签名作为二期。同步 vs 异步:同步实现简单、链路短,但把会话寿命外包给对方;异步多一次状态机与轮询成本,换来可取消、可恢复、可设预算。共享 trace vs 各自对齐:共享 trace 排障快,但跨组织会泄露内部结构;建议只传 task_id 与最小关联字段,trace 留在各自域内。授权粒度:一次性给足减少往返,但一旦凭证泄露影响面大;按任务最小化增加一次签发往返(可缓存),建议默认最小化、对高频只读能力设短 TTL 缓存。

失败路径与边界。对方超时:区分”未执行”(连接失败、409 冲突、明确的限流)与”可能已执行”(读超时、5xx)——前者可重试,后者只查询。对方返回结构变化:schema 校验失败即终态失败,不重试,触发”能力降级 + 供应商告警”。对方返回语义错误(金额单位从元变分):归一化与量纲校验必须在网关做,且校验对象等于生效对象。凭证过期或撤销:调用立即失败并 fail-closed,不带旧凭证重试。任务终态收敛:任何路径都必须落到成功/部分成功/失败待人工三者之一,禁止”永久处理中”。重复副作用:幂等键由本方生成并放在请求头,对方不回显也要在台账记录,用于事后去重与对账。

超时、重试、幂等、恢复。超时三层:连接超时(1 秒)、单跳读超时(3 秒)、整链预算(60 秒),预算向下传递而不是每跳各自重置。重试:指数退避 + 抖动,次数上限 2 次、总时长不超过剩余预算,且必须携带同一幂等键。幂等:本方侧以 task_id + step_id + 参数指纹 为唯一键,先占位后回填;对方侧若支持幂等键则透传,不支持则由本方台账在提交前查重。恢复:任务状态持久化,进程重启后按台账重建;处于”可能已执行”的步骤进入核对态(向对方查询该幂等键的结果),核对超时则转人工。

安全、权限、成本、延迟、可靠性。安全:数据最小化(按能力裁剪上下文)、参数级约束、出网白名单(只允许访问已注册的对端域名,重定向不跟随)、响应内容不直接进入提示词(先落到结构化字段再注入,降低注入面)。权限:委托凭证只覆盖本任务所需能力,禁止转发给第三方(凭证绑定 aud)。成本:按供应商维度统计调用次数、token 与失败成本,单任务设调用预算。延迟:P95 8 秒目标下,同步链最多 2 跳,超过则转异步;对端慢时快速失败优于长等待。可靠性:每家一套舱壁与熔断,单家故障时降级为”仅用其余供应商”,并在结果里显式标注”未覆盖 B 的条款”,而不是静默给出不完整结论。

评测指标与观测。契约层:schema 校验通过率、能力声明漂移数(运行时发现与卡片不一致的项数)、按供应商的错误类型分布。协作层:任务成功率、部分成功率、人工接管率、平均跳数、P95 端到端时延、单任务调用次数分布。信任层:凭证签发/撤销时延、越权拒绝次数、签名校验失败次数。审计层:台账完整率(缺 task_id / 凭证指纹 / schema 版本的比例)、争议可定位率(抽样人工复核)。测试:契约测试(对端 schema 快照回放)、故障注入(超时、结构变更、返回乱码、凭证过期)、混沌演练(单家全挂)。上线观测:按供应商 × 错误类型 × 是否降级的分桶看板,任何一家的语义失败率突增都要能独立告警。

三层能力归属。模型能力:从条款文本里抽取关键义务、判断是否需要补条款——这部分本质是概率性的,必须允许失败并给降级路径。Agent 编排能力:任务分解、跨组织链路的顺序与并行、失败后的重规划。确定性系统:凭证签发与校验、schema 校验、幂等台账、超时与预算、熔断、审计落库——这些不能交给模型判断,也不能用提示词约束代替。

追问#

追问一:对方坚持只提供一张公开的 Agent Card,不做签名也不做入驻核验,你还会接吗?

回答要点:会接,但要把这类供应商放到”受限档”。判据是三条:一是数据敏感度(只给它非敏感输入,价格与设备清单裁剪到最小必要);二是金额与副作用(只允许”生成草案”,不允许直接下单,最终动作留在本方确定性流程里);三是可观测性(本方侧强校验 + 台账 + 独立熔断,异常时自动降级)。同时明确写下”卡片不构成安全依据”这一前提,把卡片只用于预连接发现,路由与授权全部以运行时发现与本地白名单为准。换句话说,信任强度可以降,但验证责任不能转移给对方。

追问二:如果对方 Agent 也是有自主规划的 Agent,同一个 task_id 两次调用返回不同结果,你怎么处理?

回答要点:先把”不确定”显式化。第一,幂等键覆盖到”结果版本”:同一幂等键只接受一个终态结果,后续不同结果视为冲突,进入人工核对而不是覆盖。第二,要求对方在结果里带 result_id 与生成时间,本方台账记录”哪个结果被采用、为什么”。第三,对影响下单的链路,把”多次运行一致性”作为验收指标:同一输入重复 5 次,关键字段(金额、账期、违约条款)的一致率必须达到阈值,达不到就降级为”人工确认”。第四,编排层不要用”再调一次看看”来做决策,重复调用只用于核对,不用于择优。

关键点#

  1. Agent Card 是自述,签名只证明来源不证明能力;路由与授权必须以运行时发现和本地白名单为准。
  2. 能力声明漂移是跨组织协作的头号故障源,schema 版本与运行时发现要比”接入时抄写的清单”更权威。
  3. “可能已执行”的超时只能查询不能重发,否则重试会把等待放大成重复副作用。
  4. 委托凭证应按任务签发、绑定 aud、可撤销,且不得跨任务复用。
  5. 审计必须落到 task_id + 凭证指纹 + 归一化参数 + schema 版本,否则争议时无法归因。

常见误区#

  1. “对方有 Agent Card 且带签名,就等于可信”:错在混淆”来源可验证”与”行为可信”。签名只说明卡片由某私钥签发,能力是否存在、是否稳定、是否会越权,都要靠运行时探测 + 本方校验 + 舱壁来兜。
  2. “超时就重试,反正有幂等键”:错在把幂等键当成万能。对方若不实现幂等(跨组织时这是常态),重试就是重复执行;即便实现,“可能已执行”的重试也会制造额外的账单与噪音,正确做法是先查询再决定。
  3. “把三家的调用都放进一个同步链路,简单”:错在故障域合并。同步链把本方会话寿命绑定到最慢的一方,任一家变慢就全链挂起;舱壁与异步句柄的成本远低于一次全站不可用的代价。
  4. “审计记录 URL 就够了”:错在缺主体与参数维度。URL 只能证明”调用过”,不能证明”谁、以什么凭证、用什么参数”产生了那份被误发的草案。

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

写一个最小可运行的”跨组织调用网关”原型:用 Python 起两个 FastAPI 服务模拟供应商 B 与本方网关。要求实现四件事——(1)网关为每个任务签发一个带 task_id、能力白名单、exp 的短期令牌,B 侧校验 aud 与能力;(2)B 侧提供一个”可注入故障”的开关,能返回结构变更(少字段)、慢响应(18 秒)、重复副作用(同一幂等键生成两份草案);(3)网关侧实现 schema 校验 + 三类错误分流(可重试 / 语义失败 / 未决),并对”可能已执行”只查询不重发;(4)落一张台账表(SQLite 即可),字段含 task_id、凭证指纹、参数摘要、schema 版本、终态。跑通后统计:注入 20 次结构变更,重复副作用是否为 0;注入 5 次慢响应,会话是否仍能在 60 秒内收敛为终态。

延伸学习#

  1. A2A 协议规范(a2a-protocol.org/latest/specification/,v1.0.0):AgentCard 定义(self-describing manifest,含 capabilities、skills、securitySchemes、securityRequirements)、signatures 为可选的 JSON Web Signature(RFC 7515 格式)、SecurityScheme 支持 apiKey / HTTP auth / OAuth2 / OIDC / mTLS;规范同时要求客户端在调用可选能力前 SHOULD 先检查 Agent Card。注意:本次抓取的页面未包含签名验证与密钥发现(如 JWKS)章节,也未出现”签名不构成信任”的显式表述,相关结论属工程判断,不当作规范原文引用。
  2. RFC 8693《OAuth 2.0 Token Exchange》:§1.1 区分 delegation 与 impersonation;§4.1 act 声明与”接收方只应依据顶层 claims 与 current actor 做访问控制”。注意:RFC 全文并未规定”派生令牌不得比原始令牌更特权”,该约束需由本方策略强制。

S02#

  • 题目 ID:Q-20261109-S02
  • 编号:S02
  • 题型:非 AI Agent 真实研发场景题
  • 一级类型:分布式系统(兼数据库与存储)
  • 二级主题:分布式 ID、时钟回拨与发号器可用性
  • 难度:场景题(高)
  • 标签:分布式系统、分布式ID、Snowflake、时钟回拨、号段模式、租约、单调性

题目#

业务背景:一个电商订单服务需要为每笔订单生成全局唯一、趋势递增的主键,要求写入时不产生额外往返(下单链路的 P99 已卡在 120 毫秒),因此采用本地发号(Snowflake 类:41 位毫秒时间戳 + 10 位机器号 + 12 位序列号)。

系统规模:日均订单事件 2.8 亿,峰值写入 4.6 万/秒,发号集群 40 个实例、跨两个可用区;下游有 6 个系统用订单号做增量拉取与对账。

当前架构:机器号(workerId)由运维在配置文件里静态分配,扩容时人工排号;时间戳取本机 CLOCK_REALTIME,NTP 由基础设施统一同步;发号逻辑在进程内完成,无外部依赖;订单表主键即订单号,另有唯一索引兜底;下游的增量拉取任务用”上次最大订单号”作为水位,每分钟查一次 id > 水位。

目标:订单号全局唯一、趋势递增、发号可用性不低于 99.99%(月度不可用不超过约 4.3 分钟)、下游增量拉取不漏单、时钟异常时不允许产生重复订单号。

限制条件:不能停服改造;已有 12 亿存量订单号为 19 位十进制;下游 6 个系统短期无法全部改造;跨可用区网络往返约 2 毫秒,跨区强一致写入约 8 毫秒;运维不接受”每天手工排机器号”的流程。

故障现象:上周 NTP 校正时,两台实例的 CLOCK_REALTIME 回拨了 3.2 秒。发号器检测到回拨后直接抛异常,持续 40 秒拒绝发号,上游重试后仍有 12% 的下单请求失败。另有一次扩容,两台新实例被分到同一个 workerId,在 17 毫秒内产生了 3 个重复订单号,靠唯一索引挡下,但其中一个请求已进入支付流程,产生了人工对账工单。更隐蔽的是:某天回拨修复后,下游增量拉取少拉了 1,842 条订单——因为 ID 回拨期间生成的值小于水位。

需要取舍的问题:严格单调与可用性之间如何定价(回拨时等待、拒绝,还是切换发号源);本地发号与集中号段服务之间如何选;workerId 用静态分配还是租约 + epoch;下游的”ID 单调即可增量”这一假设要不要拆掉。

先思考#

  1. 先问:订单号到底承诺了哪几件事?“唯一”、“趋势递增”、“严格单调”、“连续无空洞”,这四件事哪些是业务真的需要,哪些只是实现带来的副作用?
  2. 再问:回拨发生时,除了”报错”和”照常发号”,还有第三种选择吗?如果让 ID 的某几位承载”发号纪元”,会发生什么?
  3. 最后问:下游用 ID 做水位,依赖的是”ID 单调”这个实现细节;如果这个假设被打破,谁该改?

参考答案#

结论先说:这类故障的根因不是 NTP,而是把”全局唯一”这个强承诺挂在了”时钟单调”这个弱假设上,同时让下游把”ID 趋势递增”当成”事件顺序”来用。处置分三步:短期止血(回拨容忍窗口 + 拒绝发号改为有限等待 + workerId 冲突检测),中期改造(workerId 改租约 + epoch,ID 里显式承载纪元,发号退化为”唯一但不保证严格单调”),长期纠偏(下游水位改用 (updated_at, id) 复合游标或显式递增序列,拆掉对 ID 单调性的隐式依赖)。

作答框架(3 至 5 分钟)。先给一句判断(问题在假设而不在组件)→ 把故障拆成四个失效面(唯一、可用、顺序、可运维)→ 每个失效面给一个确定性机制(租约 + epoch、分级容忍、业务顺序字段、自动排号)→ 说清取舍(严格单调换可用性、本地发号换中心依赖)→ 收在验证指标与演练方式上。

问题拆解。四个失效面要分开看。第一,唯一性失效:workerId 重复 + 同一毫秒内序列号重复,直接产生重复 ID;这是最严重的一类,因为唯一索引只能挡住写入,挡不住”已进入下游流程”的重复。第二,可用性失效:回拨检测后无条件抛异常,把”时钟异常”升级为”服务不可用”,40 秒即超过月度预算的 15%。第三,顺序性失效:回拨期间生成的 ID 小于此前水位,下游按 id > 水位 拉取必然漏单。第四,可运维性失效:静态排号在弹性扩缩容下不可持续,人是最不可靠的一环。

核心方案。

唯一性上,把”机器号”从静态配置改成租约 + epoch:实例启动时向租约服务(etcd / 数据库租约表)申请 workerId,租约带 TTL 与单调递增的 epoch;ID 布局里划出若干位给 epoch(或把 epoch 并入高位),确保”同一个 workerId 的不同纪元”生成的 ID 不可能相同。租约续期失败即停止发号(fail-closed),续期成功后 epoch 递增。这样即使两台实例短暂共用同一 workerId,只要 epoch 不同,ID 就不冲突。

可用性上,把回拨处理从”抛异常”改为分级容忍:回拨幅度小于阈值(例如 5 秒)时,等待时钟追上上一次发号时间,等待期间用尽当前毫秒的序列号余量并小幅阻塞(可控的延迟惩罚);超过阈值时,切换到备用发号源(号段模式,从中心服务预取一段 ID,本地发放),并告警。号段模式的价值在于它不依赖时钟,代价是多一次中心依赖与”段内 ID 不连续”。

顺序性上,明确区分两件事:ID 的”趋势递增”是发号器的实现特征,不是业务语义。订单的业务顺序应当由 (created_at, id) 复合游标或独立的单调序列(如按分区的 seq)来表达;下游增量拉取改为复合水位(updated_at > t OR (updated_at = t AND id > last_id)),并对”回拨窗口内的补偿扫描”给出兜底(按时间区间重扫最近 N 分钟)。

执行流程(一次发号)。读当前毫秒 → 与上次发号时间比较:若前进,序列号归零;若相同,序列号 +1 并检查是否溢出;若回拨,进入分级容忍分支 → 组装 ID(epoch + 时间 + workerId + 序列)→ 写入本地 last_timestamp → 返回。启动流程:申请租约(带 TTL 与 epoch)→ 校验 workerId 唯一性(租约服务侧用 CAS 抢占)→ 校准时钟偏移(读一次基准时间,记录本机偏移的上界)→ 进入可发号状态。降级流程:租约续期失败或时钟异常超阈值 → 切号段源 → 号段余量低于阈值时异步预取下一段(双 buffer),取号失败则按”拒绝新请求 + 已有请求继续”的优先级处理。

技术选型与依据。发号方案:Snowflake 类本地发号(低延迟、无外部依赖)为主,号段模式为降级通道;备选是纯号段模式(中心服务 + 双 buffer),代价是中心依赖与段边界的不连续。workerId 分配:etcd 租约(TTL 短、续期快、天然 CAS)为主;备选是数据库租约表(简单、可审计,但续期是写操作,量大时成为热点)。时钟:使用单调时钟(CLOCK_MONOTONIC)度量”经过时间”,用实时时钟只做偏移校准;备选是完全依赖 NTP 的正常运行(不可接受,因为 NTP 本身会跳变)。ID 表达:保留 19 位十进制兼容存量,epoch 与 workerId 位宽重新分配(例如压缩时间位、扩大机器位),位宽调整必须一次性完成并写入文档,否则新旧 ID 无法比较大小。

方案取舍。严格单调 vs 可用性:严格单调意味着回拨时必须等待或拒绝,可用性受损;放弃严格单调(允许小幅乱序)能换来可用性,但下游必须改水位。建议选后者,并把”允许的最大乱序窗口”(例如 5 秒)写成 SLA 的一部分。本地发号 vs 集中号段:本地发号延迟低(微秒级)、无中心依赖,但依赖时钟与 workerId 正确性;号段模式不依赖时钟、天然支持弹性扩容,但每次取段有中心往返(2 至 8 毫秒)且段边界不连续。workerId 静态 vs 租约:静态简单但扩容易冲突;租约引入一个弱依赖,换来确定性。ID 位宽:扩大机器位提升扩容上限,压缩时间位缩短寿命(Sonyflake 默认 39 位时间、10 毫秒单位,寿命 174 年;经典 Snowflake 41 位毫秒约 69 年),这是”寿命 vs 扩容上限”的显式取舍。

失败路径与边界。时钟大幅回拨(分钟级):号段模式接管,若号段服务不可用则拒绝发号并快速失败(优于产生重复)。时钟快速前进(NTP 大步跳):可能瞬间耗尽当前毫秒序列号,按”下一毫秒”处理并记录跳变幅度。租约服务不可用:已持有租约的实例可继续发号到 TTL 到期,到期即停(这是有意的:宁可短暂不可用,也不产生重复)。双实例同 workerId:epoch 不同则 ID 不同;若 epoch 也相同(租约服务脑裂),唯一索引兜底并触发对账。号段耗尽:拒绝新请求,已有请求继续;预取失败按剩余量做准入。存量 ID 兼容:新布局必须保证与 19 位十进制存量可比,或明确”跨代 ID 不比较大小”,下游按代际分区处理。

超时、重试、幂等、恢复。发号本身不重试(重试会产生第二个 ID,语义错误);上层若需重试,必须携带业务幂等键,由订单表唯一约束保证”同一业务请求只落一单”。租约续期重试:指数退避 + 抖动,重试期间继续用当前租约(不提前放弃),续期失败次数达到阈值即进入停止发号状态。号段预取重试:后台重试,不阻塞前台;连续失败触发告警与降级。恢复:实例重启后重新申请租约(epoch 递增),不沿用旧 last_timestamp;进程内 last_timestamp 只用于同一次运行内的单调性,不持久化(持久化反而会在时钟回拨时造成”未来时间”污染)。

性能、可用性与验证指标。性能:发号是纯内存操作,单实例 12 位序列号意味着每毫秒最多 4096 个,4.6 万/秒需要约 12 个实例同时工作(有余量),瓶颈在租约续期与号段预取,不在发号本身;号段模式下一次取段可覆盖数秒流量,中心 QPS 被摊薄。可用性:月度不可用预算 4.3 分钟,需要把”时钟异常”从”直接拒绝”改为”有限等待 + 切换源”,并为租约服务设置独立 SLO。验证指标:重复 ID 数(应为 0,按天对账)、发号 P99 延迟、回拨事件次数与持续时长、号段剩余量水位、租约续期失败率、下游增量拉取的漏单数(按时间区间抽样比对)、跨代 ID 比较异常数。测试:时钟回拨注入(1 秒 / 5 秒 / 60 秒三档)、workerId 冲突注入、租约服务宕机演练、号段服务不可用演练、双可用区切换演练。

三层边界说明。本题与模型无关:唯一性由”租约 + epoch + 唯一索引”这类确定性机制保证,不依赖任何概率性组件;顺序性由业务字段(时间戳 + 序列)表达,不依赖 ID 的数值单调。把”ID 单调”当成”事件顺序”,是把实现细节写进了契约,代价会在时钟异常那天一次性付清。

追问#

追问一:为什么不让发号器在回拨时直接”借用未来时间”(把时间戳继续往前推)?

回答要点:这在短期可行,但有三个后患。第一,ID 里的时间戳会系统性超前于真实时间,任何”按 ID 反解时间”的下游(分库分表路由、按时间归档、审计)都会得到错误时间;第二,超前量会累积,一旦超过位宽可表示范围或超过下一个真实时间点,后续 ID 会与”未来”冲突;第三,它掩盖了时钟不可信这一事实,让运维失去修复动力。更稳妥的做法是:允许小幅”逻辑时间”超前(在文档中声明为逻辑时钟),并对超前量设上限与告警;大幅回拨走号段模式,让”不依赖时钟”成为显式的降级路径。

追问二:下游 6 个系统短期无法改造,怎么保证不漏单?

回答要点:分三层兜底。第一层,短期在发号侧保证”回拨窗口内生成的 ID 不小于此前水位”——具体做法是回拨期间使用号段模式,号段 ID 从中心按块分配、块内单调且块间按中心顺序递增,只要中心水位单调,回拨窗口内的 ID 就不会低于旧水位。第二层,在拉取侧加”时间维度补偿”:除 id > 水位 外,追加”按 updated_at 重扫最近 10 分钟”的补偿任务,用业务时间去覆盖 ID 乱序。第三层,做日终对账:按订单表全量快照与下游落库数比对,差异进人工队列。同时把”下游依赖 ID 单调”登记为技术债,给出改造时间表——不改也能活,但每次时钟事件都会放大对账成本。

关键点#

  1. 唯一性必须由”租约 + epoch + 唯一索引”三层保证,不能只靠”workerId 不重复”这一人工假设。
  2. 时钟回拨的正确处置是分级:小幅等待、大幅切换号段,而不是无条件抛异常。
  3. 号段模式的价值是”不依赖时钟”,代价是中心依赖与段内不连续;它是降级通道,不是默认路径。
  4. “ID 趋势递增”是发号器特征,“事件顺序”是业务语义;下游用水位拉取是在消费实现细节。
  5. 位宽分配是寿命与扩容上限的取舍,调整必须一次性完成并写入契约,否则新旧 ID 不可比。

常见误区#

  1. “回拨时抛异常最安全”:错在把”不产生重复”与”不接受请求”等同。40 秒拒绝发号会直接击穿可用性预算,正确做法是分级容忍 + 切源,“不产生重复”由 epoch 与唯一索引保证,不需要靠停服。
  2. “workerId 配好了就不会冲突”:错在低估弹性扩缩容。人工排号在扩容、重建、跨区漂移时必然出现复用;租约 + epoch 的成本很低,收益是消除一整类生产事故。
  3. “订单号递增,所以增量拉取用 id > 水位 就行”:错在把实现细节当契约。只要发生一次时钟事件或切到号段模式,ID 就不再严格单调,漏单是必然结果;复合游标或补偿扫描才是可交付的方案。

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

实现一个可注入时钟的 Snowflake 发号器并做三组实验:(1)写一个 Clock 接口,支持正常推进、回拨 1 秒 / 5 秒 / 60 秒;(2)实现分级策略——回拨 ≤ 5 秒时等待并消耗序列余量,> 5 秒时切到本地模拟的号段源(预置 1000 个 ID 的段,双 buffer 预取);(3)用 8 线程并发压 10 万次发号,统计重复数、P99 延迟、切源次数。再写一个”下游消费者”,分别用 id > 水位 与 (updated_at, id) 复合游标拉取,对比回拨场景下的漏单数。目标是在回拨 60 秒的用例中做到重复数为 0、漏单数为 0,并记录切源带来的额外延迟。

延伸学习#

  1. Sonyflake 官方仓库(github.com/sony/sonyflake):默认位分配为 39 位时间(10 毫秒单位)、8 位序列、16 位机器号,寿命 174 年、单实例每 10 毫秒最多 256 个 ID;机器号默认取私有 IP 低 16 位,这正是不做唯一性校验就会冲突的典型来源;位宽与时间单位均可配置,时间耗尽后 NextID 返回错误。
  2. PostgreSQL 18 官方文档第 19 章(运行时配置)与第 24 章(日常维护):用于支撑”唯一索引兜底”与”按时间戳补偿重扫”的可行性判断;具体参数(如 idle_in_transaction_session_timeout)本轮未逐条复核,引用前请以官方文档为准。

L01#

  • 题目 ID:Q-20261109-L01
  • 编号:L01
  • 题型:LeetCode
  • 一级类型:数组与字符串(矩阵模拟)
  • 二级主题:边界收缩与四条边的收尾判定
  • 难度:medium
  • LeetCode 题号:54
  • 标签:LeetCode、矩阵模拟、边界收缩、数组与字符串

题目#

给定一个 m x n 的矩阵 matrix,请按照顺时针螺旋顺序返回矩阵中的所有元素。要求额外空间为 O(1)(不计输出数组)。

输入:matrix,二维整数数组,1 <= m, n <= 10。 输出:按螺旋顺序排列的一维数组。

约束

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 10
  • -100 <= matrix[i][j] <= 100

示例

  • 示例 1(官方):matrix = [[1,2,3],[4,5,6],[7,8,9]] → [1,2,3,6,9,8,7,4,5]
  • 示例 2(官方):matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] → [1,2,3,4,8,12,11,10,9,5,6,7]

先思考#

  1. 把矩阵看成”四条边 + 一个内层矩阵”。每绕完一圈,剩下的还是一个矩阵,能不能用四个下标 top / bottom / left / right 描述它?
  2. 绕圈时四条边的遍历方向分别是什么?每条边遍历完后,对应的边界应该怎么移动?
  3. 最容易错的是收尾:当只剩一行或一列时,第二、第四条边的遍历会不会把已经访问过的元素再加一遍?用什么条件避免?

参考答案#

题号与名称:54. 螺旋矩阵(Spiral Matrix),难度 medium。 官方链接:https://leetcode.com/problems/spiral-matrix/ ;中文站 https://leetcode.cn/problems/spiral-matrix/ 。 题目描述:按顺时针螺旋顺序遍历矩阵全部元素,返回一维数组。 输入输出:输入 m x n 整数矩阵,输出长度为 m*n 的一维数组。 约束与示例:见上,1 <= m, n <= 10;两个官方示例分别是 3×3 与 3×4。

解题思路(边界收缩)。维护四个边界 top, bottom, left, right,每一轮按”上边 → 右边 → 下边 → 左边”的顺序遍历当前环,然后收缩边界。关键在两次条件判断:遍历下边之前检查 top <= bottom(否则只剩一行时,下边会重复访问上边已经走过的元素);遍历左边之前检查 left <= right(否则只剩一列时,左边会重复访问右边已经走过的元素)。

正确性说明。每一轮开始时,[top, bottom] × [left, right] 恰好是尚未访问的矩形区域。四条边分别覆盖该区域的第 top 行、第 right 列、第 bottom 行、第 left 列,且四条边的元素互不重叠(在 top < bottom 且 left < right 时)或在条件判断后不重叠(单行/单列时)。每轮结束后矩形严格缩小(top+1、right-1、bottom-1、left+1),因此循环终止时所有元素恰好被访问一次,顺序与螺旋顺序一致。

时间复杂度:O(mn),每个元素访问一次。 空间复杂度:O(1) 额外空间(输出数组不计)。

Python 参考代码

from typing import List
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if not matrix or not matrix[0]:
return []
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
res: List[int] = []
while top <= bottom and left <= right:
# 上边:从左到右
for c in range(left, right + 1):
res.append(matrix[top][c])
top += 1
# 右边:从上到下
for r in range(top, bottom + 1):
res.append(matrix[r][right])
right -= 1
# 下边:从右到左(仅在仍存在有效行时)
if top <= bottom:
for c in range(right, left - 1, -1):
res.append(matrix[bottom][c])
bottom -= 1
# 左边:从下到上(仅在仍存在有效列时)
if left <= right:
for r in range(bottom, top - 1, -1):
res.append(matrix[r][left])
left += 1
return res

关键边界情况

  1. 单行:[[1,2,3]] —— 上边遍历完后 top > bottom,下边必须跳过。
  2. 单列:[[1],[2],[3]] —— 上边只取一个元素、右边取剩余元素后 left > right,左边必须跳过。
  3. 单元素:[[7]] —— 上边取完即结束。
  4. 非方阵:3×4 与 4×3 都要覆盖,边界收缩依赖 m 与 n 分别计算。
  5. 负值元素:题目允许 -100,不影响逻辑。

测试用例

  1. [[1,2,3],[4,5,6],[7,8,9]] → [1,2,3,6,9,8,7,4,5]
  2. [[1,2,3,4],[5,6,7,8],[9,10,11,12]] → [1,2,3,4,8,12,11,10,9,5,6,7]
  3. [[1,2,3]] → [1,2,3]
  4. [[1],[2],[3]] → [1,2,3]
  5. [[7]] → [7]

追问#

面试官变体:如果要求”逆时针”或者”从内向外”输出呢?

回答要点:逆时针只需把四条边的顺序改为”上 → 左 → 下 → 右”,并把每条边的方向反转;从内向外则可以先用同一套边界收缩把元素按环分组(记录每环的起止下标),再逆序输出各环,或改用”从中心开始按层扩张”的写法。真正可迁移的是**用四个下标描述”剩余区域”**这一建模方式,方向与顺序只是参数。

关键点#

  1. 用四个边界描述”尚未访问的矩形”,是本题唯一需要记住的状态。
  2. 两条 if 是正确性的关键:单行时跳下边、单列时跳左边。
  3. 边界收缩发生在整条边遍历之后,顺序不能提前。
  4. 复杂度由元素个数决定,不存在”提前终止”的优化空间。
  5. 非方阵必须分别用 m、n 初始化 bottom 与 right,这是最常见的下标错误来源。

常见误区#

  1. 不判断就写下边和左边:单行输入 [[1,2,3]] 会输出 [1,2,3,2,1] 之类的重复序列,因为下边又走了一遍同一行。
  2. 用”已访问标记”代替边界:能过题但空间变 O(mn),且在要求 O(1) 额外空间的追问下不成立;原地标记还会破坏输入。
  3. 把 bottom 初始化为 len(matrix[0]) - 1:行列不等时直接越界或漏元素;bottom 对应行数,right 对应列数,不能混用。

延伸学习#

  1. LeetCode 54 官方题面(https://leetcode.com/problems/spiral-matrix/):两示例与四条约束已核验;本轮中英文页面均未渲染难度标签(连续第 7 次),medium 为按标准分类记录,未直接核验。
  2. 同族练习:LeetCode 59 螺旋矩阵 II(按同样边界收缩生成矩阵)、LeetCode 885 螺旋矩阵 III(按方向向量与步长模拟),用于把”边界建模”迁移到”生成”与”游走”两类题型。

延伸资料#

  1. RocksDB Wiki《RocksDB Overview》(https://github.com/facebook/rocksdb/wiki/RocksDB-Overview)——LSM 写路径与 compaction 成本模型的一手材料,适合配合 B01 阅读;官方文档,无需额外核验。
  2. Dettmers 等《LLM.int8()》(https://arxiv.org/abs/2208.07339,NeurIPS 2022)——理解”异常值 + 混合精度分解”为何是低位宽量化的前提;arXiv 摘要与提交历史本轮已核验。
  3. A2A 协议规范 v1.0.0(https://a2a-protocol.org/latest/specification/)——AgentCard、signatures(JWS)、securitySchemes 的字段定义;签名验证与密钥发现章节本轮未取到,相关工程结论已在 S01 中标注为判断而非规范原文。
  4. OWASP《Server Side Request Forgery Prevention Cheat Sheet》(https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html)——今天的题目未直接使用,但它是”地址约束必须按归一化后的生效对象判定”这一原则最完整的清单(IPv4-mapped、NAT64、重定向逐跳、DNS rebinding),建议作为 S01 出网白名单的补充阅读。
  5. Sonyflake 官方仓库(https://github.com/sony/sonyflake)——位分配、寿命与机器号默认值的可验证来源;S02 中的冲突场景即来自其”默认取私有 IP 低 16 位”的设计。

当日重点#

  1. 放大是守恒的:存储引擎的三种放大、量化的精度损失、跨组织协作的信任成本、分布式 ID 的时钟依赖,都是同一个结构——优化把代价挪到了另一层,那一层如果没有指标,就等于没有代价。
  2. 自述不等于事实:Agent Card、能力清单、运维配置里的 workerId、下游假设的”ID 单调”,都是”声明”。工程上要把声明与运行时事实分开,并让授权、路由、水位这类关键决策只依赖事实。
  3. 唯一性与顺序性是两件事:前者靠租约、epoch、唯一索引这类确定性机制;后者要靠业务字段表达。把两者混在一个自增 ID 上,时钟异常那天会同时失效。
  4. 降级通道要预先存在:号段模式之于 Snowflake、异步句柄之于同步调用、人工接管之于自动化链路——降级不是故障时才设计的东西。
  5. 评测要打在敏感切片上:困惑度之于量化、平均写吞吐之于 LSM、整体成功率之于跨组织协作,都是”平均值掩盖尾部”的同类错误。

运行信息(不发送)#

  • 配置读取状态:成功。题库配置.md 版本 v8(last_maintained: 2026-11-02),无暂停类型;观察中的 8 项新技术均未标记「允许试学」,本轮未从中出题。本轮未修改配置文件。
  • 题型分布核验:非 Agent 基础 1(B01)、Agent 基础 1(B02)、Agent 场景 1(S01)、非 Agent 场景 1(S02)、LeetCode 1(L01),合计 5 道,符合配置 v8 的每日结构。
  • 历史读取:题目索引.md(115 条 / indexed_runs: 21)、精选/README.md(4 条)、README.md 导航表;未通读更早正文。滚动窗口 2026-08-09 至 2026-11-09,窗口内最早条目为 2026-10-10,无需剪除。
  • 去重结果:与窗口内最近 30 天记录比对,无同题、无换皮、无重复 LeetCode 题号。B01 与 2026-10-28 B01(统计信息、索引选择与执行计划回归)区分:后者是优化器决策,本题是存储引擎结构。B02 与 2026-10-13 B04(LoRA/量化并列)、2026-11-04 B02(MoE)区分:本题只讲位宽与误差来源。S01 与 2026-11-04 S01(代持令牌与身份传播)、2026-10-20 S01(Server Card 跨组织发布)区分:本题主线是”声明漂移 + 舱壁 + 审计归因”,凭证部分只作一层。S02 与 2026-10-22 B01(时钟与因果序)、2026-10-21 S02(分布式锁与 fencing)区分:本题是发号器的唯一性锚点与下游水位假设。L01 题号 54 为窗口内首次,矩阵模拟模式亦为首次。
  • 参考的历史题目:Q-20261028-B01、Q-20261013-B04、Q-20261104-S01、Q-20261020-S01、Q-20261022-B01、Q-20261021-S02、Q-20261030-B01(Raft,用于区分”共识保证唯一顺序”与”发号器只保证唯一”)。
  • 本轮核验来源:LeetCode 54 中英文页(题面、两示例、四条约束一致,难度标签未渲染);RocksDB Overview(compaction 与三种放大的表述);LLM.int8() arXiv 摘要;OWASP SSRF Cheat Sheet;A2A 规范 v1.0.0 的 AgentCard/签名/安全方案字段(签名验证章节未取到);Sonyflake README(位分配、174 年、2^8 每 10 毫秒、机器号默认值)。
  • 未核验项:LeetCode 54 难度标签;A2A 签名验证与 JWKS 发现流程;LSM-tree 原始论文(O’Neil 1996)的具体页码与公式;Hugging Face 量化文档的 API 细节;Twitter Snowflake 原始博客(站点可能已迁移,未抓取)。
  • 精选:未收到明确意图,未执行,未自动精选。
  • 作答反馈:无提交,未评分。
  • 本地写入状态:已写入 2026-11/2026-11-09-AI-Agent-面试训练.md;远端同步未验证,不声称已同步。
2026-11-09 AI Agent 面试训练
https://vissssa.github.io/posts/ai-agent-interview-2026-11-09/
作者
Siegward
发布于
2026-11-09
许可协议
CC BY-NC-SA 4.0