13257 字
66 分钟
2026-10-30 AI Agent 面试训练
AI Agent 面试训练
2026-10-30

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

今日学习主题#

任何保证都附带成立条件;条件不满足时的退化形态必须预先设计并可观测。

今天的四道工程题共用同一条主线,但落点完全不同:

  • 多数派保证的来源是两个集合必然相交 —— 一旦凑不齐多数派,系统必须退化成”停止进展”,而不是”继续写”。
  • 提示缓存保证便宜的前提是前缀逐字节相同 —— 一旦前缀变化,它退化为”功能完全正确、成本回到基线”,而不是”报错”。
  • 变更通知保证新鲜的前提是订阅关系还活着 —— 一旦通知丢失,必须有版本号比对兜底,而不是永久陈旧。
  • 重试保证最终成功的前提是错误属于可重试类 —— 一旦遇到永久失败,必须退化为”进死信 + 人工”,而不是无限循环。

把”退化形态”写清楚,是区分中高级工程师与只会背结论的工程师的分水岭。

题型分布#

组数量题目
非 AI Agent 基础定义题1B01
AI Agent / LLM 基础定义题1B02
AI Agent 真实研发场景题1S01
非 AI Agent 真实研发场景题1S02
LeetCode1L01

题目速览#

编号题目名称题型一级类型难度一句话考察点
B01共识与选主:多数派交集、任期与日志匹配为什么能防脑裂非 Agent 基础分布式系统基础题(中高)多数派保证成立的三条前提,以及凑不齐多数派时它退化成什么
B02提示缓存:命中条件、前缀布局与命中率的工程度量Agent 基础上下文工程与能力封装(兼 LLM 应用工程)基础题(中高)缓存的是 KV 张量而非文本,命中要求已渲染前缀逐字节相同
S01工具能力清单的缓存、变更通知与失效补偿Agent 场景工具与互操作协议(兼 AI 安全与治理)场景题(高)通知是加速器不是一致性来源;版本号 + 对账才是保证
S02一条永远失败的消息拖垮整条履约链路非 Agent 场景分布式系统(兼软件工程与平台工程)场景题(高)失败分类、重试预算、毒丸通道隔离与幂等下沉
L01516. 最长回文子序列LeetCode区间动态规划medium区间 DP 的定义、填表顺序与 s[i]==s[j] 的两种转移

B01 · 非 AI Agent 基础定义题#

  • 题目 ID:Q-20261030-B01
  • 一级类型:分布式系统
  • 二级主题:一致性、可用性与多数派;共识与选主的判定依据
  • 分类标签:分布式系统、共识算法、Raft、多数派、任期、日志匹配、CAP

题目#

你们有一个基于领导人复制状态机(如 Raft)的一致性服务,承担分布式锁与关键配置的分发,5 个节点跨 3 个可用区部署。一次网络抖动后,监控的某个时刻显示有两个节点都处于 leader 状态,值班同学担心”脑裂”,问数据是不是已经写坏了。

请回答下面四个问题,并说明每项结论成立的条件:

  1. 什么是共识算法中的”多数派(quorum)“?为什么”任意两个多数派必然相交”这一条集合性质,就能保证同一个任期内不可能选出两个 leader?
  2. 任期(term)在其中起什么作用?一个节点收到来自更小任期或更大任期的 RPC 时分别应该怎么做?
  3. 如果旧 leader 被隔离在少数派分区中继续收到写请求,会发生什么?它本地的日志最终会怎样?
  4. 一个新 leader 刚当选时,能不能因为”某条旧任期的日志已经存在于多数派”就把它判定为已提交?为什么?

先思考#

  1. 先不要想 Raft 的细节,只想集合:5 个节点里”多数派”最少是几个?任意两个这样的集合,交集至少有几个元素?把结论写成不等式。
  2. 再想”投票”这件事:一个节点在一个任期内最多投几票?如果两个候选人争夺同一批选票,谁会先拿到?这条规则和上一条集合性质是如何配合的?
  3. 然后想”写成功”的判定点:leader 收到写请求后,什么时候才可以对客户端返回成功?这个判定依赖的是”自己写好了”、“多数派写好了”还是”过了一段时间”?
  4. 最后想”旧 leader”:它在少数派分区里能不能凑齐多数派的回复?如果不能,它的写请求最终结果是什么?它的日志里会留下什么痕迹?恢复网络后这些痕迹怎么处理?

参考答案#

结论:共识不靠”检测脑裂”来避免双写,而靠一条结构性不变量 —— 任意两个多数派集合必然相交,且每个节点每个任期最多投出一票,因此同一任期不可能选出两个 leader。少数派分区里的旧 leader 凑不齐多数派,它的写永远不会进入”已提交”状态,所以真正不会发生的是”坏数据被提交”;会发生的是它在本地留下未提交日志,恢复后被新 leader 覆盖回滚。这条保证的代价是:少数派一侧停止对外提供写服务。

定义:多数派(quorum)是集群成员集合中元素个数超过半数的子集,5 节点时为 3。基于领导人的共识算法把两条判定都建立在多数派之上:选主要拿到多数派的选票,提交要把日志复制到多数派。Raft 论文把这组保证写成五个性质(Figure 3):Election Safety(一个任期至多一个 leader)、Leader Append-Only(leader 只追加不覆写)、Log Matching(两条日志在相同下标处任期相同,则其之前所有条目完全一致)、Leader Completeness(某条目在某任期被提交,则所有更高任期的 leader 都包含它)、State Machine Safety(任何服务器在某下标应用了某条目,不会有其他服务器在同一下标应用不同条目)。

适用边界(成立条件):这三条缺一不可。

  • 集合条件:多数派大小取 ⌈(n+1)/2⌉,因此任意两个多数派的交集大小至少为 2⌈(n+1)/2⌉ − n > 0。这条是纯集合性质,与网络、时钟、线程调度完全无关。
  • 投票条件:每个服务器在一个任期内最多投一票,先到先得。论文原文:“Each server will vote for at most one candidate in a given term, on a first-come-first-served basis”,并由它推出选举安全性:“The majority rule ensures that at most one candidate can win the election for a particular term”。
  • 日志条件:候选人必须联系到多数派才可能当选,且它的日志必须至少与该多数派中任一成员的日志一样新,否则投票方拒绝。判定方式是”先比最后一条日志的任期,任期相同再比长度”。

任期的作用是单调递增的逻辑时钟。论文原文:“Terms act as a logical clock in Raft, and they allow servers to detect obsolete information such as stale leaders.” 收到更大任期 RPC 的节点会更新自己的任期并退回 follower;收到更小任期 RPC 的节点直接拒绝并保持自己的状态。因此旧 leader 一旦重新连上集群,会立即因为遇到更高任期而退位。

示例:5 节点 A/B/C/D/E,A 是 term 3 的 leader。网络分裂为 {A, B} 与 {C, D, E}。

  • A 侧:A 收到写请求,只能复制到 B,计数 2/5,不满足多数派 ⇒ 不推进 commitIndex,不向客户端返回成功。A 本地日志留下了若干未提交条目。
  • C/D/E 侧:选举超时陆续触发,C 拿到 C、D、E 三票,成为 term 4 的 leader,它的写请求拿到 3/5 即可提交。
  • 恢复后:A 收到 term 4 的 AppendEntries,退为 follower,自己那些未提交条目被 leader 的日志强制对齐(冲突处被删除并覆盖)。整个过程里,没有任何一个已提交条目丢失或被改写。

相近概念区别:

  • 多数派 quorum(防脑裂)vs 租约 lease(减少读延迟):前者是结构性保证,不依赖时钟;后者依赖时钟偏差上界,论文明确指出依赖 lease 会让安全性依赖时序(“this would rely on timing for safety (it assumes bounded clock skew)”)。租约只能用于优化可用性,不能用来证明正确性。
  • “两个节点自称 leader” vs “两个 leader 都能提交写入”:前者在跨任期场景下确实会出现(旧 leader 还不知道自己已被罢免),后者永远不会出现。
  • 奇数节点 vs 偶数节点:3 节点与 4 节点的多数派分别是 2 与 3,两者都只能容忍 1 个节点故障,4 节点反而更贵且无收益。

典型应用(通用工程):这条推导可以直接迁移到任何用多数派做判定的地方 —— 分布式锁服务的选主、配置中心的一致性写入、分片控制器的单点收敛、元数据服务的成员管理。面试时更有价值的表述是:先说 n=2f+1 能容忍 f 个故障,再说”扩容到偶数节点不会提升容错能力”,最后补一句”成员变更必须两阶段,否则在配置切换的瞬间会出现新旧配置两个独立多数派”(论文用 joint consensus 解决)。

面试现场回答(1 至 2 分钟):

“看到两个 leader 不用先慌,先确认它们各自处于哪个任期、各自能联系到几个节点。同一任期不可能选出两个 leader,因为每个节点每任期只投一票,而任意两个多数派必然相交 —— 5 节点里多数派是 3,两个 3 的集合至少有 1 个公共成员,这个公共成员不可能同时投给两个人。跨任期出现两个 leader 是正常的:旧 leader 被隔离在少数派,它还没发现自己已被罢免。关键是它能不能写入 —— 不能,因为它凑不齐多数派复制,写请求不会提交,客户端不会收到成功。它在本地留下的未提交日志,恢复网络后会被新 leader 覆盖回滚。所以真正要担心的不是写,而是读:如果有人允许从 leader 本地状态直接读,就会读到未提交数据,这才是脏数据来源。新 leader 也不能因为某条旧任期的日志已经在多数派上就判定它已提交,它必须先在自己任期内提交一条条目(实践上是 no-op),靠 Log Matching 性质间接提交之前的条目。最后补一句时间假设:安全性不依赖时序,但可用性依赖 —— 广播时间要远小于选举超时,选举超时要远小于单机平均故障间隔。”

关键点#

  1. 防脑裂靠集合交集,不靠检测。不需要”检测到有两个 leader”这个动作,正确性由”每任期一票 + 多数派相交”结构性保证。
  2. 提交判定点是多数派复制完成,不是 leader 本地落盘,也不是时间过去多久。少数派侧的写请求最终一定失败或超时,不会返回成功。
  3. 未提交条目会被回滚,这是设计好的收敛机制;真正的高危点是”读” —— leader 处理只读请求前必须与多数派交换心跳确认自己未被罢免,否则会读到本地未提交状态。
  4. 时间假设只影响可用性不影响安全性:broadcastTime ≪ electionTimeout ≪ MTBF。选举超时通常取 10ms–500ms(论文给出的广播时间量级为 0.5ms–20ms)。
  5. 成员变更必须两阶段。直接从旧配置切到新配置会在切换瞬间产生新旧两个互不相交的多数派,论文用 joint consensus(旧 ∪ 新 同时需要多数派)解决。
  6. 偶数节点没有额外容错:4 节点与 3 节点同样只容忍 1 个故障,却要多付一台机器的成本。

常见误区#

  1. “两个节点同时自称 leader,数据一定写坏了” —— 错。跨任期出现两个 leader 是正常状态,旧 leader 属于少数派,无法提交任何写入。真正会出问题的是读:若从 leader 本地未提交日志读,就会读到永远不会被提交的数据。这也是为什么只读请求也要走多数派确认。
  2. “心跳和租约保证同一时刻只有一个 leader” —— 错。租约依赖有界的时钟偏差,是降低读延迟的优化手段;论文明确说明依赖 lease 会让安全性依赖时序。把租约当正确性依据,会在时钟跳变或长时间 GC 暂停时出事故。
  3. “leader 把条目写进本地日志就等于写成功了” —— 错。提交的判定发生在多数派复制完成之后;在此之前返回客户端成功,等价于承诺了一个之后可能被回滚的结果。同理,新 leader 也不能用”旧条目已在多数派”来提交它,必须先提交自己任期内的条目。

追问#

追问:新 leader 刚当选时,它知道自己拥有全部已提交条目,但不知道”哪些”是已提交的。它能直接用”当前日志长度”来判断吗?如果不能,工程上正确的做法是什么?

回答要点:不能。Leader Completeness 只保证它包含了所有已提交条目,不告诉它边界在哪。两个原因:一是同一长度的日志在不同节点上内容可能不同(未提交条目可能不同);二是如果对旧任期条目按多数派计数来提交,会出现论文 Figure 8 的场景 —— 一个旧任期的条目已存于多数派,但仍可能被未来的 leader 覆盖。Raft 的做法是:每个 leader 在任期开始时提交一条空白的 no-op 条目;一旦这条当前任期的条目按多数派复制被判定提交,依据 Log Matching Property,它之前的所有条目都被间接提交。工程上这有两个可迁移的推论:第一,“提交”永远要以当前权威任期的判定为准,不能复用上一任权威的计数;第二,新权威上台后需要一次显式的”确立权威动作”,在此之前不要对外给出任何已提交的承诺。

延伸学习#

  • Raft 官方论文《In Search of an Understandable Consensus Algorithm (Extended Version)》,Diego Ongaro、John Ousterhout,Stanford(raft.github.io/raft.pdf)。重点读 Figure 3 的五个性质、§5.2 Leader election、§5.4 Safety(含 5.4.1 选举限制与 5.4.2 旧任期条目的提交)、§5.6 Timing and availability、§6 Cluster membership changes。
  • Raft 官网交互式可视化(raft.github.io),用来把”任期推进 / 日志复制 / 分区恢复”的过程在脑子里跑一遍;纸面结论容易背,时序过程容易错。

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

  • 题目 ID:Q-20261030-B02
  • 一级类型:上下文工程与能力封装(兼 LLM 应用工程)
  • 二级主题:上下文压缩、摘要与缓存断点;提示缓存的命中条件、前缀布局与工程度量
  • 分类标签:AI-Agent、面试训练、上下文工程与能力封装、LLM应用工程、提示缓存、Prefix-Caching、成本归因

题目#

你负责的 Agent 服务每天调用大模型约 40 万次。每次请求中,系统提示与工具定义合计约 6000 tokens,历史对话平均 4000 tokens,本轮动态内容(用户消息 + 检索片段)约 1500 tokens。你启用了提示缓存(Prompt Caching / Prefix Caching),期望成本下降 50% 以上,实际上线一周后只降了 8%,而且波动很大,白天高峰时几乎为零。

请回答:

  1. 提示缓存究竟缓存了什么?一次请求要命中缓存,必须满足哪些条件?
  2. 为什么”内容看起来一样”却命中不了?请至少给出三种具体成因。
  3. 命中率应该用什么口径度量?为什么”请求级命中率”会骗人?
  4. 缓存完全不命中时,系统退化成什么?成本与延迟分别如何变化?有没有”比不缓存更差”的情况?

先思考#

  1. 先想缓存的对象:如果缓存的是文本,那么”语义相同”就应该能命中;如果缓存的是模型计算出的中间张量,那么”两个请求必须相同”意味着什么级别的相同?是字节级、token 级还是语义级?
  2. 再想顺序:系统提示、工具定义、历史对话、本轮用户输入,谁应该排在最前面?如果每轮都在中间插入一个时间戳或检索片段,前缀的哪一段会变?
  3. 然后想度量:命中率应该按”命中的请求数 / 总请求数”还是”命中 token 数 / 输入 token 数”?哪一个和账单直接成正比?
  4. 最后想”退化”:完全不命中时,除了多花钱,还有没有更糟的事?第一次写入缓存的价格和不缓存相比是高还是低?

参考答案#

结论:提示缓存缓存的是模型处理前缀时计算出的 KV 张量,不是文本;命中条件是已渲染前缀从第一个 token 起到断点处逐字节完全相同。因此它首先是一个上下文布局问题,不是一个开关问题。它降低的是成本与首 token 延迟,不改变输出内容;命中率必须用 token 加权口径度量,并且要先把”写入比不缓存更贵”这一笔摊掉再谈收益。

定义(以两家官方表述为准):

  • Anthropic:“Prompt caching references the entire prompt: tools, system, and messages (in that order), up to and including the block designated with cache_control.” 以及”Cache writes occur only at your breakpoint. Marking a block with cache_control writes exactly one cache entry: a hash of the prefix ending at that block.”
  • OpenAI:“The prompt cache stores key-value (KV) tensors, not the tokens themselves.” 以及”Cache reuse requires the entire rendered prefix to match. If content or a relevant setting changes before a breakpoint, the prefix after that change cannot match the existing cache entry.”

适用边界(四条硬条件):

  1. 长度门槛。Anthropic 主流新模型最小可缓存前缀为 512 tokens(部分模型为 1024 / 2048 / 4096);OpenAI GPT-5.6 及以后为 1024 visible input tokens。低于门槛不缓存也不报错 —— 官方原文:“Any requests to cache fewer than this number of tokens are processed without caching, and no error is returned.” 这一条决定了”命中率莫名是 0”的第一类成因:前缀太短。
  2. 前缀逐字节相同,且必须从开头起一致。任何位于断点处或断点之前的 block 变化,都会改变累积哈希,使整段失效。
  3. 三层顺序固定:tools → system → messages。改工具定义会让整条缓存失效 —— “Modifying tool definitions (names, descriptions, parameters) invalidates the entire cache”。其他会改变前缀的设置还包括模型、tool_choice、图片增删、thinking 配置、speed / effort 等,各家文档都给了失效矩阵。
  4. 时间与路由条件。Anthropic 默认 5 分钟 TTL,可选 1 小时;且 TTL 计时从写入或读取该条目的请求开始算,生成响应本身消耗的时间也计入生命周期(“if a response takes 4 minutes to stream, a follow-up request that reuses the same cached prefix must start within about 1 minute of that response completing”)。OpenAI 的缓存状态保存在单机上,“traffic above 15 requests per minute can lead to overflow routing”,路由不到持有缓存的机器就命中不了 —— 这一条解释了”白天高峰命中率反而归零”。

示例:题目里那个只降 8% 的系统,典型成因是三类叠加。

  • 布局错误:把检索片段或时间戳放在系统提示之后、工具定义之前,前缀每次都变,命中率约等于 0,而每一次未命中还要再付一次写入溢价。
  • 工具列表不稳定:按租户过滤后重新生成 tools,或每次请求重排顺序。Anthropic 明确”改工具名、描述、参数会让整条缓存失效”;OpenAI 也把 tools(“Changes tool names, descriptions, schemas, ordering”)列为影响前缀的首要设置。
  • 逐轮改写历史:把上一轮消息”扩展”成新内容而不是追加新消息。OpenAI 原文指出,这样”the old endpoint is now inside a message, rather than at its end”,后续请求就无法复用;正确做法是在允许时保留原消息并追加新消息,或把可复用文本放在独立 block 并在其后放显式断点。

相近概念区别:

  • 提示缓存 vs KV Cache:KV Cache 是单次请求内解码阶段对已算注意力状态的复用,目标是避免重复计算已生成 token;提示缓存是跨请求对前缀 KV 状态的复用,目标是避免重复 prefill。两者都叫”缓存 KV”,但作用域、失效条件与收益口径完全不同。
  • 提示缓存 vs 语义缓存:语义缓存按相似度命中,直接返回上一次的答案,会引入陈旧、跨租户泄漏与”相似但不等价”的错误;提示缓存要求逐字节相同,命中的只是注意力状态,模型仍然会重新生成输出 —— 官方明确”Prompt caching has no effect on output token generation”。语义缓存是正确性风险,提示缓存不是。
  • 提示缓存 vs 批处理:批处理用时效换折扣(分钟到小时级延迟,价格折扣);提示缓存用布局灵活性换前缀复用(延迟与成本同时下降,但要求前缀稳定)。两者可以叠加,但叠加后要注意 TTL 与批处理窗口的关系。
  • 提示缓存 vs 上下文压缩:压缩会重写历史,OpenAI 明确”Summarization, compaction, or context truncation can change the prefix and reset cache reuse”,且可能从第一个被改动的 token 起就失效。压缩与缓存是相互拉扯的两个手段,需要一起设计保留集与断点。

在 Agent 系统中的典型应用:

  • 断点位置:把系统提示 + 工具定义 + 稳定的领域说明放在最前,断点打在”静态前缀的末尾”,动态内容(用户消息、检索片段、时间戳)放后面。Anthropic 的原话是”place the breakpoint at the end of the static prefix, not on the varying block”。
  • 多轮对话:用自动缓存让断点随对话自动前移,或保证每轮新增 block 数不超过回看窗口(Anthropic 为 20 个 block,连续 tool_use 或连续 tool_result 各只算一个位置);超出就在靠近上次写入的位置再加断点(最多 4 个显式断点)。OpenAI 侧同样建议”保留原始消息并追加新消息”,而不是改写已有消息。
  • 工具列表稳定:OpenAI 给出的工程建议非常直接 —— 用 tool_choice: "none" 或 allowed_tools 来限制可调用工具,而不要移除工具定义(“Use allowed_tools to restrict which tools are callable while keeping the supplied tools list stable”)。MCP 侧也有对应约束:规范建议服务端以确定性顺序返回 tools/list,以启用客户端缓存并提升提示缓存命中率。
  • 并行分支与子 Agent:共享同一前缀的分支都能受益;不同分支的中间结果不要塞回共享前缀,否则每次分支推进都会击穿其他分支的缓存。
  • 度量与归因:Anthropic 用 cache_creation_input_tokens / cache_read_input_tokens / input_tokens;OpenAI 用 input_tokens_details.cached_tokens 与 cache_write_tokens(GPT-5.6+ 还能按 ttl_30m_tokens / ttl_12h_tokens 拆分写入)。要按 task / tenant / step 归因,并同时记录”命中率”与”命中长度”。

面试现场回答(1 至 2 分钟):

“提示缓存缓存的是模型处理前缀算出的 KV 张量,不是文本。所以命中的前提是已渲染前缀从第一个 token 到断点完全一致,顺序是 tools 到 system 到 messages。这决定了它首先是个上下文布局问题,不是开个开关的问题。它有三条硬边界:第一是长度门槛,主流模型 512 或 1024 tokens,低于门槛不缓存也不报错,所以命中率莫名是零时先查前缀够不够长;第二是时间边界,Anthropic 默认五分钟 TTL,而且计时从请求开始算,生成响应本身也消耗生命周期,长任务很容易在下一轮开始前就过期;第三是路由边界,OpenAI 的缓存状态在单机上,每分钟超过 15 次请求会溢出路由,所以白天高峰命中率反而可能归零。它退化时是纯成本与延迟问题 —— 不命中就退回全量 prefill,输出完全不变;但第一次写入比不缓存贵 25%,长时间 TTL 更贵,所以低频复用是净亏损。工程上我做三件事:断点打在静态前缀末尾、工具列表和顺序保持稳定、用 token 加权命中率而不是请求级命中率来度量,并按任务和租户归因。”

关键点#

  1. 缓存对象是 KV 张量,不是文本;命中条件是”已渲染前缀逐字节相同且从开头起一致”,语义相近毫无意义。
  2. 顺序敏感:tools → system → messages;改工具定义(名字、描述、参数、顺序)会让整条缓存失效,因为 tools 排在最前。
  3. 写比不缓存贵,命中才便宜:Anthropic 5 分钟写入 1.25×、1 小时 2×,命中 0.1×(部分模型 0.05× / 0.025×);OpenAI 30 分钟写入 1.25×、12 小时 2.5×,命中 0.1×(个别模型 0.05×)。低频复用是净亏损。
  4. TTL 从请求开始计时,生成响应本身消耗生命周期;两家都没有提供手动清除缓存的接口,只能等过期或改前缀。
  5. 度量必须是 token 加权命中率,并按 task / tenant / step 归因;请求级命中率会严重高估收益。

常见误区#

  1. “在系统提示里加个时间戳或请求 ID 不影响缓存” —— 错。任何位于断点之前的变化都会改变累积哈希,导致整段失效,并且你会为每一次失效再付一次写入溢价。正确做法是把变化内容放在断点之后,或放在独立的后置 block。
  2. “命中率高就等于省钱” —— 错。两层错误:一是命中率应按 token 加权,只缓存了 512 token 的短前缀而 6000 token 的动态部分每次都重算时,请求级命中率接近 100% 而账单几乎没变;二是必须先摊掉写入溢价,一次写入加一次完全复用的成本约为 1.35 倍,比两次全量的 2 倍便宜,但如果写完就没人再用,就是 1.25 倍纯亏。
  3. “提示缓存会改变模型输出,要当心正确性” —— 错。官方明确说明它不影响输出 token 的生成,只是跳过前缀的重复计算。真正可能改变输出的是你为了迁就缓存而调整的上下文布局(例如把动态内容挪到后面、把检索片段截断),那是布局决策的影响,不是缓存机制的影响。

追问#

追问:Agent 每轮的检索片段都不同,而系统提示和工具定义是稳定的,断点应该打在哪里?如果某一轮产生了 30 个 block(大量并行工具调用加结果),回看窗口会不会失效?失效了怎么办?

回答要点:断点打在静态前缀的末尾 —— 也就是系统提示与工具定义之后、第一条动态消息之前,绝不要打在会变的 block 上。Anthropic 的回看窗口是 20 个 block,但连续的 tool_use 只算一个位置、连续的 tool_result 也只算一个位置,所以”一轮里有很多并行工具调用”本身不一定会撑爆窗口;真正会撑爆的是每轮产生超过 20 个可计位置的混合内容。失效时有两个办法:一是在靠近上次写入的位置再加一个断点(最多 4 个显式断点),这样第二个断点会开启第二段回看;二是改用自动缓存,让断点随对话自动前移到最后一个可缓存 block。同时要注意,动态检索片段必须放在断点之后,并且在同一个请求内保持确定性排序 —— 排序不稳定等于每次都改前缀。

延伸学习#

  • Anthropic 官方《Prompt caching》(platform.claude.com/docs/en/build-with-claude/prompt-caching):命中三原则(写入只在断点、读取向后找先前写入、回看窗口 20 block)、失效矩阵、TTL 计时口径、用量字段。
  • OpenAI 官方《Prompt caching》指南(developers.openai.com/api/docs/guides/prompt-caching):前缀哈希路由与 15 rpm 溢出、最小可缓存长度、context_management(压缩)对复用的破坏、“用 allowed_tools 而不是删工具定义”这条工程建议。

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

  • 题目 ID:Q-20261030-S01
  • 一级类型:工具与互操作协议(兼 AI 安全与治理、上下文工程与能力封装)
  • 二级主题:工具能力清单的缓存、变更通知与失效补偿(resources/subscribe 与 HTTP GET 迁移到 subscriptions/listen)
  • 分类标签:AI-Agent、面试训练、工具与互操作协议、MCP、能力清单版本化、提示缓存、多租户隔离

背景、目标、约束与已知现象#

业务背景:公司为内部 3000 名员工提供统一 Agent 平台。Agent 通过统一网关接入 40 多个 MCP 工具服务,工具总数约 420 个。网关是 12 副本的无状态服务,为降低延迟与成本,网关在进程内缓存每个工具服务的 tools/list 结果,并把工具定义拼进模型请求的 tools 字段(合计约 9000 tokens),依赖提示缓存降低 prefill 成本。** MCP 规范 2026-07-28 修订后,原先用于服务端变更通知的 HTTP GET 端点与 resources/subscribe / resources/unsubscribe 已被 subscriptions/listen 取代。**

当前数据流:客户端请求 → 网关(进程内缓存 tools/list,TTL 5 分钟,并按 tenant 裁剪工具子集)→ MCP 工具服务。变更通知原先依赖自研 HTTP GET 长轮询端点。

目标:完成迁移后,任何一个工具服务的能力变更要在 30 秒内被全部 12 个网关副本看到,且不得因迁移本身造成工具调用失败率上升。

限制条件:

  • 工具服务由不同团队维护,其中 7 个明确暂不支持新的订阅机制。
  • 网关不能为每个请求回源 tools/list(主链路 P99 延迟预算 1.5s,单次回源平均 400ms)。
  • 工具定义是共享资产,不能跨租户泄漏;部分工具只对特定租户可见。
  • 回滚必须能在 5 分钟内完成,且不依赖工具服务侧回滚。

已知现象(迁移后一周):

  1. 某工具服务新增了一个工具,15 分钟后仍有 Agent 报告”没有这个工具”,网关缓存始终没有失效。
  2. 另一个工具服务修改了 inputSchema 的字段类型后,短时间内参数校验失败率上升,同时提示缓存命中率从 62% 掉到 11%。
  3. 某个网关副本重启后,它之前订阅过的工具服务不再推送变更,且系统里没有任何报错。
  4. 一次工具服务滚动发布期间,网关收到同一条变更通知 6 次,触发了 6 次全量刷新。

需要取舍的问题:通知延迟 vs 回源成本;会话一致性的新鲜度 vs 在途执行的稳定性;工具定义的快速迭代 vs 提示缓存命中率。

先思考#

  1. 先区分”三种不一致”:缓存过期(TTL 自然到期)、缓存未失效(通知丢了)、缓存被污染(刷新拿到了错误版本)。这三种各自的判定指标分别是什么?能不能只用一种机制覆盖三种?
  2. 再想订阅的语义:订阅关系保存在哪里?副本重启后它还活着吗?不支持订阅的那 7 个服务该怎么办?
  3. 然后想”通知”与”拉取”的分工:通知能不能作为一致性的唯一来源?如果必须在两者之上再加一个东西,那个东西应该是什么形态(版本号?时间戳?哈希?)
  4. 最后回到提示缓存:工具定义在这次命中率崩塌中扮演什么角色?如果业务确实必须改工具定义,怎样把代价摊平?

参考答案#

结论:工具能力清单是一个带版本号的派生视图,必须用”服务端权威版本号 + 客户端缓存 + 变更通知(加速)+ 周期性版本比对(兜底)“这四件套来治理。通知只能当加速器,绝不能当一致性来源 —— 通知会丢、会重、订阅会静默死掉。同时,工具定义位于模型请求前缀的第一段,它的字节级稳定性直接决定提示缓存命中率,因此”改不改工具定义”要当成一次成本与安全联合评审,而不是随手发布。

一、问题拆解:四种现象对应四个不同的缺口。

  1. 通知丢失叠加长 TTL(现象 1):网关 5 分钟 TTL + 通知链路无可靠性保证 ⇒ 最坏陈旧窗口远超 30 秒目标,而且系统里没有任何”陈旧”的可观测信号。这是典型的”失效没有被观测,于是等于没有失效机制”。
  2. 刷新竞态与工具变更的成本外部性(现象 2):12 个副本各自独立刷新,不同副本在不同时刻持有新旧两版 schema,同一会话的请求落在不同副本上就会看到不同的工具定义,表现为参数校验失败率上升。同时,tools 排在 system 与 messages 之前,改工具定义会让整条前缀失效 —— Anthropic 官方原话是”Modifying tool definitions (names, descriptions, parameters) invalidates the entire cache”。命中率 62% 掉到 11% 正是这一条被触发,而不是某种偶发抖动。
  3. 订阅状态未持久化、未观测(现象 3):订阅是一条有状态的租约。副本重启后它不会自动重建,而”订阅是否存活”没有被做成可观测契约,于是静默失效。
  4. 至少一次投递导致重复(现象 4):通知天然允许重复,且这里有 12 个副本扇出。没有幂等去重时,一次实际变更被放大为 6 次回源与 6 次全量替换。

二、核心方案:四件套。

  1. 版本化能力清单。每个工具服务的工具清单带一个不可变 capabilityVersion(内容哈希或单调序号)。网关缓存以 (serviceId, tenantScope, capabilityVersion) 为键;任何判定都以版本号为准,而不是以时间为准。
  2. 确定性缓存边界。2026-07-28 规范已为这件事提供了原生字段:tools/list、prompts/list、resources/list 等结果需携带 ttlMs(新鲜度提示,毫秒)与 cacheScope(public 或 private,决定是否允许共享中间层缓存响应)。网关应优先遵循服务端给出的 ttlMs,而不是自己硬编码 5 分钟 —— 让声明新鲜度的一方来定义失效时间,这正是该字段的设计意图。
  3. 变更通知作为加速器。迁移到 subscriptions/listen:它是一条长生命周期的 POST 响应流,取代了原来的 HTTP GET 端点与 resources/subscribe / resources/unsubscribe;客户端显式 opt-in 到指定变更类型(toolsListChanged、promptsListChanged、resourcesListChanged、resourceSubscriptions),服务端确认并给通知打上 io.modelcontextprotocol/subscriptionId,这个标识正是幂等去重与归因的抓手。
  4. 版本比对兜底。通知只触发”尽快重新比对”;真正的判定是拿本地 capabilityVersion 与服务端当前版本比较,不一致才真正刷新。这一条把”通知丢失”的后果从”永久陈旧”降级为”最多多等一个比对周期”,是整套方案里最关键的一步。注意规范要求属同一请求作用域的通知(如 notifications/progress)继续走各自请求的响应流,不要混进 subscriptions/listen 这条流。

三、关键执行流程:

  1. 启动:从持久化的订阅台账加载 (serviceId, 订阅类型, 上次版本, 最后通知时间) → 建立 subscriptions/listen 长连接 → 立即做一次全量版本比对(冷启动补偿)→ 比对完成前就绪探针不通过,不接流量。
  2. 收到 toolsListChanged:查台账,若该 (serviceId, subscriptionId) 已处理过则直接丢弃(幂等)→ 否则标记该服务为 stale → 进入刷新队列,队列使用 3 秒合并窗口并按 serviceId 去重。
  3. 刷新:按 serviceId 做 singleflight 合并回源 → 拉取 tools/list → 做 schema 与语义校验 → 计算新版本号 → 原子替换进程内缓存(整棵子树替换,不做原地修改)→ 更新台账。
  4. 会话绑定快照:一个会话在执行期间绑定一个不可变的能力快照,工具清单的变更不穿透在途执行;新会话才使用新快照。这是把”新鲜度”与”在途稳定性”解耦的关键设计。
  5. 周期性对账:每 60 秒对所有服务做一次低成本版本比对(只取版本号,不取全量清单),把”本地版本 ≠ 服务端版本”做成一项持续存在的时间序列。这是发现”订阅静默死亡”的唯一可靠手段。

四、技术选型与依据:

  • 通知机制选 subscriptions/listen:规范已移除 HTTP GET 端点与 resources/subscribe,旧机制不再存在;新机制把四类变更统一到一条流并带 subscriptionId,便于去重与归因。
  • 版本来源选服务端声明的 ttlMs + server/discover:规范规定服务端必须实现 server/discover 来广告支持的协议版本、能力与身份,因此它是连接前的权威版本源;ttlMs 则是服务端声明的新鲜度,比客户端硬编码更符合”谁定义谁负责”。
  • 循环读取等措施:cacheScope: public 的能力清单可在网关层跨会话共享;按租户裁剪的子集必须标记为 private,且缓存键必须包含租户维度,禁止跨租户复用。

备选方案与取舍:

方案收益代价
A. 纯轮询(不用通知)实现最简单,无订阅状态管理,天然不会”订阅静默死亡”陈旧窗口等于轮询周期;40 多个服务高频轮询的回源压力大
B. 纯推送(不做版本比对)变更传播延迟最低通知丢失即永久陈旧,且无法发现订阅已死
C. 推送 + 版本比对 + 周期对账(推荐)延迟接近 B,正确性由版本号与对账保证,双机制互为兜底多一份台账与一条比对链路,实现复杂度中等

五、失败路径、边界条件、超时、重试、幂等与恢复:

  • 通知流断开:退化为”周期对账 + TTL 到期刷新”。重连必须带指数退避与抖动,避免 12 个副本同时重连把工具服务打爆;“订阅存活副本数”必须纳入健康检查与告警。
  • 通知重复:按 (serviceId, subscriptionId) 与 capabilityVersion 去重;本地版本已满足的通知不再触发刷新。
  • 通知乱序:以版本号单调递增为准,丢弃比本地更旧的版本;出现版本回退要单独告警,因为它通常意味着服务端回滚或缓存串了。
  • 回源失败:保留旧快照继续服务(fail-static),标记 stale 并告警;连续失败 N 次后把该服务标记为降级,从模型可见工具集中移除,并由确定性代码给出”该能力暂不可用”的提示,而不是让模型自行解释。
  • 回源超时:回源必须有独立超时(如 800ms)且不占用主请求预算;超时不等于失败,按”未确认”处理并留待下一轮比对。
  • 重启恢复:订阅台账持久化,冷启动先全量比对再放流量。
  • 回滚:保留最近 N 个能力快照,回滚就是把当前指针指回上一个快照(秒级),不依赖工具服务回滚,满足 5 分钟窗口。

六、安全、权限、成本、延迟与可靠性:

  • 安全:工具定义按租户过滤,缓存键必须含 tenant;标记为 public 的响应不得包含租户私有工具。订阅通道是读通道,订阅本身不等于授权 —— 工具调用时的授权判定必须留在工具网关这一确定性层,不能放进提示词,也不能因为”收到了订阅确认”就认为有权访问。
  • 成本:工具定义的字节稳定性直接决定提示缓存命中率。新工具优先以”新服务 / 新命名空间”方式上线,减少对既有前缀的改动;确需修改既有定义时,走”新旧快照并存 + 会话绑定 + 灰度”来摊平冲击。另外,规范要求服务端以确定性顺序返回 tools/list,目的正是启用客户端缓存并提升提示缓存命中率 —— 这是一个协议层为成本优化让路的明确例子。
  • 延迟:单次回源 400ms 不能落在主链路上 ⇒ singleflight + 异步刷新 + 快照原子替换;主链路 P99 预算内只允许缓存读。
  • 可靠性 SLI:对账不一致率、订阅存活副本数、快照切换频率、回源失败率、变更传播 P99。

七、验证指标、测试方法与上线观测:

  • 陈旧度:max(本地快照年龄)、对账不一致率(目标恒为 0,持续两个周期不为 0 即告警)、变更传播 P99(目标小于 30 秒)。
  • 提示缓存:token 加权命中率 cache_read / (cache_read + cache_creation + input),按 step / tenant 归因;工具定义变更单独立项,变更前给出”预计命中率损失”估算。
  • 调用质量:按工具与版本分组的参数校验失败率、工具选择错误率、“找不到该工具”类失败。
  • 成本:回源次数每分钟、写缓存与读缓存 token 比。
  • 上线观测:先灰度 1 个网关副本 + 2 个低频工具服务,对比新旧两套的陈旧窗口与命中率;守护窗口 30 分钟,命中率跌破基线 20% 或校验失败率翻倍则自动回滚到上一快照。故障注入用例:杀掉订阅流、拒绝单点回源、注入重复通知、注入顺序颠倒的通知。

八、三层能力归属(必须讲清楚):

  • 模型能力(概率性):在给定的工具集合中选择工具、填充参数、理解工具描述。它既不负责判断工具清单是否最新,也不知道缓存是否过期,更不应被要求”发现通知丢了”。
  • Agent 编排能力:决定本次请求携带哪些工具(工具集装配)、会话与能力快照的绑定、在目标工具不可用时改走降级路径或升级到人工介入。
  • 确定性系统能力(传统软件):能力版本号与新鲜度声明、订阅台账的持久化与重建、幂等去重与乱序保护、singleflight 回源、快照原子替换与秒级回滚、按租户过滤与授权判定、周期对账与告警。一致性只能由这一层保证;一致性永远不能寄托在模型”注意到”上。

关键点#

  1. 通知是加速器,不是一致性来源。一致性由”版本号 + 周期对账”提供;没有对账,通知丢失就等价于永久陈旧且无人知晓。
  2. 订阅是一条有状态的租约:必须持久化、可重建、可观测存活;副本重启后必须补偿一次全量版本比对才能接流量。
  3. 能力快照不可变且与会话绑定。这样”变更传播延迟”与”在途执行被签名变更打断”这两个问题被彻底解耦。
  4. 工具定义位于模型请求前缀第一段,它的字节稳定性直接决定提示缓存命中率;改一次定义等于整条缓存失效,这是一笔必须被计入变更评审的外部成本。
  5. 不一致时应 fail-closed 到”拒绝调用并给出确定性提示”,而不是用旧 schema 强行调用,也不是让模型自己决定怎么办。

常见误区#

  1. “有了变更通知就不需要 TTL 和对账了” —— 错。通知会丢、会重复、订阅会静默死亡。没有版本号与对账,丢失的通知没有任何机制能发现,陈旧会被永久化,而且系统会在很长一段时间里”看起来正常”。
  2. “工具定义是小改动,随时可以发” —— 错。它是模型请求前缀的第一段,一次改动会击穿全部提示缓存,成本与首 token 延迟同时恶化;同时不同副本刷新时序不一致会造成跨副本 schema 漂移,表现为参数校验失败。它应按”低频、高影响资产”走评审、灰度与可回滚流程。
  3. “缓存命中率掉了就加大 TTL” —— 错。TTL 只影响陈旧窗口,不解决”前缀变了导致必然失效”。而且更长的 TTL 会同时放大一致性风险与安全风险:已下线或已收紧权限的工具仍在模型可见范围内,等于把一个过期的能力清单暴露给模型。

追问#

追问一(基础):那 7 个明确不支持 subscriptions/listen 的工具服务,你怎么保证它们的清单一致性?

要点:降级为纯轮询,轮询周期取其声明的 ttlMs 或配置的保底周期,并把它们单独标记为”无通知能力”,用独立的陈旧度指标观测。更进一步的收敛做法是在网关侧为这些服务做集中代理订阅:由网关内的一个逻辑订阅者(通过选主或单点租约产生)统一维持一条订阅流并扇出给所有副本,把订阅状态从 12 份收敛为 1 份,代价是这 1 份成为单点,需要租约与自动接管。

追问二(进阶):12 个副本同时收到同一条通知,如何在不引入分布式锁的前提下只回源一次?

要点:两层合并。第一层是进程内 singleflight(按 serviceId 合并),第二层是版本抢占 —— 谁先完成回源并写入新版本,后来者发现本地版本已满足就直接返回,不需要互斥。跨副本的少量冗余回源本身是幂等的(同一版本写入同一内容),可以接受,因此不需要为它引入强互斥与分布式锁。这里体现的是”用幂等换掉互斥”这一可迁移原则。

追问三(深入):工具定义变更导致提示缓存命中率从 62% 掉到 11%,你如何判断这是”必要变更的代价”还是”布局缺陷”?

要点:看失效是否集中在 tools 段,以及它在无变更窗口内是否仍然抖动。如果命中率只在工具定义真正变更的时刻塌陷、随后在一个 TTL 内恢复,那是必要变更的代价,应该用”新旧快照并存 + 会话绑定 + 灰度放量”摊平,并在变更评审里把预计的命中率损失作为成本项列出来。如果在工具定义没有任何变更的时间窗口里命中率依然大幅抖动,那就是布局缺陷 —— 常见成因是工具列表顺序不稳定、每请求重排或按租户重新生成 tools、动态内容混进了静态前缀。判据非常明确:无变更窗口内的命中率方差,它由布局决定,与业务变更无关。

延伸学习#

  • MCP 规范 2026-07-28《Key Changes》(modelcontextprotocol.io/specification/2026-07-28/changelog):SEP-2575 用 subscriptions/listen 取代 HTTP GET 端点与 resources/subscribe;SEP-2549 引入 ttlMs 与 cacheScope;以及”SHOULD return tools from tools/list in a deterministic order to enable client-side caching and improve LLM prompt cache hit rates”这一条把协议与成本直接连起来的要求。
  • Anthropic 官方《Prompt caching》的失效矩阵(platform.claude.com/docs/en/build-with-claude/prompt-caching):“Modifying tool definitions invalidates the entire cache”是本题现象 2 的直接依据。

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

  • 题目 ID:Q-20261030-S02
  • 一级类型:分布式系统(兼软件工程与平台工程)
  • 二级主题:消息队列与异步任务:可见性超时、失败分类、重试预算、毒丸隔离与死信治理
  • 分类标签:面试训练、分布式系统、软件工程与平台工程、消息队列、重试预算、毒丸消息、死信队列、幂等

背景、目标、约束与已知现象#

业务背景:电商履约系统。订单支付成功后写入一条”履约”消息,由消费者集群处理:占用库存、生成发货单、通知仓库。日峰值 180 万条,平均处理耗时 800ms,P99 6 秒。

当前架构:业务库 → 事务性发件箱 → 消息中间件(至少一次投递)→ 消费者集群(30 实例 × 16 并发)→ 下游库存 / 仓单 / 物流服务。消费成功后显式删除消息。可见性超时沿用默认值 30 秒,没有配置重试上限,也没有死信队列。

目标:在不丢消息、不重复扣减库存的前提下,30 分钟内清除积压、恢复消费延迟,并让同类问题不再拖垮整条链路。

限制条件:

  • 不能丢弃任何订单消息(涉及真实库存与资金)。
  • 下游仓单服务是外部系统,无法立刻修复。
  • 消费者不能无限扩容(下游有调用配额)。
  • 恢复时间目标 30 分钟。

已知现象(某天 14:20 起):

  1. 队列积压从 0 涨到 42 万条,消费延迟 P99 从 6 秒涨到 31 分钟。
  2. 消费者 CPU 从 35% 涨到 96%,但下游服务一切正常,QPS 反而下降。
  3. 日志里几乎全是同一条错误:某订单的收货地址含非法字符,仓单服务返回 400 INVALID_ADDRESS。
  4. 该消息被消费了约 1.7 万次,期间该订单库存被重复占用 3 次(幂等键漏掉了这个分支)。
  5. 告警被同一条错误刷屏,真正的新问题(另一服务的连接池打满)被完全淹没。

需要取舍的问题:重试次数 vs 吞吐;严格保序 vs 毒丸隔离;自动回灌 vs 人工确认;主链路延迟 vs 单条消息的端到端时延。

先思考#

  1. 先把失败分成两类:一类是”过一会儿可能自己会好”的(下游 503、网络抖动、限流),另一类是”永远不会好”的(参数非法、字段缺失、权限不足)。对这两类,重试的意义有什么本质区别?
  2. 再想队列的可见性机制:消息被消费者取走之后,它是立刻从队列里消失,还是仍在队列中只是暂时不可见?如果处理失败且没有删除,会发生什么?这解释了”1.7 万次”从哪来。
  3. 然后想”一条消息”与”整条链路”的关系:为什么一条永远失败的消息,会让另外 41 万条正常消息一起变慢?中间被争抢的稀缺资源是什么?
  4. 最后想幂等:重复投递是设计前提还是异常?去重责任应该放在消息层、业务层还是下游调用层?
2026-10-30 AI Agent 面试训练
https://vissssa.github.io/posts/ai-agent-interview-2026-10-30/
作者
Siegward
发布于
2026-10-30
许可协议
CC BY-NC-SA 4.0