19950 字
100 分钟
2026-10-28 AI Agent 面试训练

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

今日学习主题#

隐形成本的显性化:估算、等待、失败与资源占用,都要有账可查。

五道题共用一条主线。系统里那些”看起来成功""看起来很快""看起来均匀”的东西,代价往往被转移到了另一个环节——只有把账本摆到台面上,问题才第一次变得可见。

题目隐性成本藏在哪显性化的手段
B01 执行计划回归优化器的估算错误被转移到查询执行EXPLAIN ANALYZE 的 estimated vs actual
B02 推理延迟与成本批处理带来的尾延迟、多轮调用带来的成本放大拆成 TTFT / TPOT,归因到 task 与 tenant
S01 MCP Tasks同步语义下”超时”被当成”未执行”,代价是重复触发句柄 + 状态机 + 幂等键
S02 分片上传未完成的分片持续计费生命周期规则 + 应用层台账
L01 Trie前缀共享的指针开销,以及”精确 vs 容错”的取舍逐字符路径的确定性判定

一句话记住它:凡是”自动”完成的机制,都要问清三件事——判据是什么、代价由谁承担、上界在哪里。

题型分布#

  • 非 AI Agent 基础定义题:1 道(B01)
  • AI Agent / LLM 基础定义题:1 道(B02)
  • AI Agent 真实研发场景题:1 道(S01)
  • 非 AI Agent 真实研发场景题:1 道(S02)
  • LeetCode:1 道(L01)

题目速览#

编号题目 ID题目名称题型一级类型难度一句话考察点
B01Q-20261028-B01索引还在,为什么计划变了非 Agent 基础数据库与存储基础题(中高)优化器是代价估算器,计划回归的根因是统计信息失真而不是索引失效
B02Q-20261028-B02推理延迟应该拆成几段Agent 基础LLM 应用工程基础题(中高)TTFT 与 TPOT 的瓶颈不同,批处理以单用户尾延迟换吞吐
S01Q-20261028-S01长耗时工具调用改为 MCP TasksAgent 场景工具与互操作协议场景题(高)把执行与等待解耦,句柄是唯一引用,取消是协作式的
S02Q-20261028-S02分片全 200,附件却打不开非 Agent 场景软件工程与平台工程场景题(高)完成的判定点是服务端合并,孤儿分片是持续计费的负资产
L01Q-20261028-L01208. 实现 Trie(前缀树)LeetCodeTriemedium逐字符路径的确定性检索,以及前缀共享的空间取舍

B01 · 索引还在,为什么计划变了#

  • 题目 ID:Q-20261028-B01
  • 题型:非 Agent 基础定义题
  • 一级类型:数据库与存储
  • 二级主题:查询优化器统计信息、索引选择与执行计划回归
  • 标签:数据库与存储、查询优化、统计信息、执行计划回归、EXPLAIN ANALYZE、PostgreSQL
  • 难度:基础题(中高)

题目#

线上一条报表查询,昨天 P95 是 20 毫秒,今天 P95 是 20 秒。DBA 排查后告诉你:“SQL 文本没变,索引都还在,数据量级也没跳变,只是没走那个索引了。”

请你回答:

  1. 为什么在数据没有结构性变化的情况下,优化器会改选一个差得很多的计划?
  2. 你会按什么顺序定位,每一步用什么手段验证?
  3. 为什么”加一个新索引”和”强制走索引”经常都不是正确答案?

先思考#

  1. 优化器选择计划时,输入里哪些是测量值,哪些是估算值?
  2. 如果估算行数是 100、实际行数是 100 万,计划会往哪个方向偏?
  3. “索引存在”和”索引会被选中”之间隔了几层判定?
  4. 同一条 SQL 在同一个库里,是否可能同时存在两套计划?由什么参数控制?
  5. 如果问题出在两列相关(例如 status 与 tenant_id),单列统计能不能表达这种相关?

参考答案#

结论:执行计划回归几乎总是”估算失真”而不是”索引失效”。 优化器是代价估算器,它唯一的分布事实来源是统计信息;统计信息陈旧、抽样精度不足、多列相关性无法表达、以及预编译计划在 generic 与 custom 之间切换,都会让同一条 SQL 在数据分布变化后仍按旧分布选计划。定位顺序必须是”先证明计划变了 → 定位估算偏差在哪一层 → 补统计或改写法”,直接加索引或强制计划都属于掩盖症状。

定义:查询优化器做的是基于代价(cost)的搜索,代价由两部分合成——代价常数与选择性估算。前者是配置项(seq_page_cost 默认 1.0、random_page_cost 默认 4.0、cpu_tuple_cost 默认 0.01、cpu_index_tuple_cost 默认 0.005);后者来自统计信息。PostgreSQL 官方文档对这些代价常数的定性很关键,原文是:

“The cost variables described in this section are measured on an arbitrary scale. Only their relative values matter, hence scaling them all up or down by the same factor will result in no change in the planner’s choices.”

也就是说,整个体系里没有一个”绝对正确的毫秒数”,只有相对比较。代价常数错了会让选择系统性偏移,但它们通常不是”昨天好今天坏”的原因——变化来自选择性估算那一侧。

适用边界:这套机制成立于”统计信息能代表当前数据分布”的前提上,而这个前提天然是弱的。官方原话:

“Entries in pg_statistic are updated by the ANALYZE and VACUUM ANALYZE commands, and are always approximate even when freshly updated.”

注意”even when freshly updated”——精度本来就是概率性的,不是精确值。同样地,pg_class.reltuples 与 relpages 也不是实时值:

“For efficiency reasons, reltuples and relpages are not updated on-the-fly, and so they usually contain somewhat out-of-date values. They are updated by VACUUM, ANALYZE, and a few DDL commands such as CREATE INDEX.”

简洁示例(按排查顺序):

-- 1) 先证明计划变了,并量化估算偏差
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT ... ;
-- 关键:比较每个节点 rows= 的 estimated 与 actual rows=... loops=...
-- 估算 100 / 实际 1000000 就是强信号
-- 2) 看统计信息本身是否还能代表分布
SELECT attname, n_distinct, null_frac, correlation,
array_length(most_common_vals, 1) AS mcv_len
FROM pg_stats
WHERE tablename = 'report_facts';
-- 3) 提高统计精度(全局或针对列)
ALTER TABLE report_facts ALTER COLUMN status SET STATISTICS 500;
ANALYZE report_facts;
-- 4) 只有多列相关时才需要扩展统计
CREATE STATISTICS stts_facts (dependencies, ndistinct, mcv)
ON status, tenant_id FROM report_facts;
ANALYZE report_facts;

相近概念的区别:

  • 统计信息 vs 索引:索引是访问路径(access path),统计信息是选择哪条路径的判据。索引存在不等于会被选中;优化器认为走索引的估算代价更高时,就会选顺序扫描或另一个索引。判定”索引是否可用”要看计划,不是看 \d。

  • 估算行数 vs 实际行数:EXPLAIN 只给估算,EXPLAIN ANALYZE 才给实际。两者相差一个数量级以上,通常就是计划回归的根因信号——它是唯一能同时证明”计划变了”和”为什么变了”的证据。

  • ANALYZE vs VACUUM:VACUUM 回收空间、更新可见性映射;ANALYZE 才更新 pg_statistic。VACUUM 也会更新 reltuples / relpages,但那是粗粒度计数,不含分布形状。

  • custom plan vs generic plan:预编译语句(显式 PREPARE、PL/pgSQL、以及多数驱动的预处理路径)会在执行时选择计划形态。官方定义:

    “Custom plans are made afresh for each execution using its specific set of parameter values, while generic plans do not rely on the parameter values and can be re-used across executions. Thus, use of a generic plan saves planning time, but if the ideal plan depends strongly on the parameter values then a generic plan may be inefficient.”

    若参数分布高度倾斜(有的租户命中 10 行、有的命中 100 万行),切到 generic 就会对某一端灾难性。

  • 单列统计 vs 扩展统计:单列统计按定义无法表达跨列关系,官方明确点出这是慢查询的常见成因:

    “It is common to see slow queries running bad execution plans because multiple columns used in the query clauses are correlated. The planner normally assumes that multiple conditions are independent of each other, an assumption that does not hold when column values are correlated.”

典型应用(通用工程):任何”数据量连续增长、某天突然出现慢查询”的场景,都应该把执行计划纳入回归门禁——因为 schema 没变不代表计划没变。发布流程里可以对核心查询保存计划快照(EXPLAIN (FORMAT JSON)),在 CI 中对估算行数做断言;上线后用 auto_explain 记录实际执行计划,把”估算/实际偏离倍数”作为一个持续观测的指标。

面试现场回答(约 90 秒):

“我先确认是不是计划变了:同一条 SQL 抓 EXPLAIN (ANALYZE, BUFFERS),比较每个节点 rows 的估算值和实际值。如果估算 100 行、实际 100 万行,那就是估算失真——优化器按 100 行去选,大概率选了嵌套循环或某个看起来很便宜的索引,实际却要扫百万行。接下来定位失真来源,按三层查。第一层看 pg_class 的 reltuples / relpages 是否严重滞后,官方文档说这两个字段不是实时更新的,靠 VACUUM、ANALYZE 和 CREATE INDEX 更新,未全表扫描时是增量近似值。第二层看 pg_stats 里的 n_distinct、null_frac 和 most_common_vals 长度——默认统计目标是 100 条,分布不规则时会低估,可以按列用 ALTER TABLE SET STATISTICS 提高。第三层看是不是多列相关,因为优化器默认假设多个条件相互独立,官方明确说列相关时这个假设不成立,需要 CREATE STATISTICS 建扩展统计对象。还有一个常被忽略的:预编译语句的 generic plan 与 custom plan 切换,参数倾斜时会固定到一套对所有参数都次优的计划,可以用 plan_cache_mode 来判定。处置顺序上我先补统计再谈其他。加索引只是增加候选路径,如果估算本身错了,新索引同样不会被选中;enable_seqscan=off 这类强制手段属于临时方案,官方文档的原话是把这些方法当作 temporary solution,更好的做法是调代价常数、手动 ANALYZE、提高统计目标。”

追问#

追问:autovacuum 一直在跑,为什么统计信息还会陈旧到引发计划回归?

回答要点:因为 autovacuum 判断是否需要 ANALYZE 的判据是”变更元组数”,而不是”分布是否改变”。官方给出的公式是 analyze threshold = analyze base threshold + analyze scale factor * number of tuples,默认 autovacuum_analyze_threshold = 50、autovacuum_analyze_scale_factor = 0.1——也就是说,一张 1 亿行的表要变更约 1000 万行才触发一次 ANALYZE。更关键的是官方这一句:

“The daemon schedules ANALYZE strictly as a function of the number of rows inserted or updated; it has no knowledge of whether that will lead to meaningful statistical changes.”

两个推论:其一,如果新增的 1000 万行全部集中在 status='pending' 上,分布已经完全变了,但只要未达阈值就不会分析;其二,一张表频繁更新但分布稳定时,反会被反复分析。另外两个结构性盲区:分区父表不直接存元组,autovacuum 不会对其自动 ANALYZE,需要手动;临时表 autovacuum 无法访问,只能靠会话内 SQL。

关键点#

  1. 优化器是代价估算器,不是执行反馈器。PostgreSQL 没有自适应查询优化——计划一旦选定就执行到底,不会因为发现估算错了而中途改。
  2. EXPLAIN ANALYZE 的 estimated vs actual 行数比是首要判据;相差一个数量级以上基本可以锁定估算失真。
  3. 统计信息”即使刚更新过也是近似值”,默认统计目标 100 条 MCV 与直方图边界;这是精度上界,不是保证。
  4. 独立性假设在多列相关时结构性失效,单列统计无法表达,必须靠 CREATE STATISTICS 扩展统计。
  5. 预编译计划的 generic / custom 切换会造成”同一条 SQL 两套计划”,这是参数倾斜场景下的高频陷阱。

常见误区#

  1. “索引还在,所以计划不会变。” 索引只是候选路径,选择依据是代价估算。估算一变,同一个索引可能被弃用,也可能改走另一个更差的索引。判定索引是否生效要看执行计划,不是看索引是否存在。
  2. “慢了就加索引 / 强制走索引。” 官方文档把 enable_* 系列参数明确称为 temporary solution。估算错误时新索引同样不会被选中;而 enable_seqscan=off 这类开关是会话级或全局的,会把问题平移到别处,且在下一次分布变化后彻底失控。
  3. “autovacuum 开着,统计就是新的。” 触发判据是变更元组数,不是分布是否改变。把”autovacuum 正常运行”当作”统计信息新鲜”是一个类别错误。
  4. “看平均响应时间就够了。” 计划回归的典型形态是双峰:多数请求走老计划很快,少数走新计划极慢。均值几乎不动,只有 P95 / P99 会暴露——这与 B02 的分位数论证是同一个道理。

延伸学习#


B02 · 推理延迟应该拆成几段#

  • 题目 ID:Q-20261028-B02
  • 题型:AI Agent / LLM 基础定义题
  • 一级类型:LLM 应用工程
  • 二级主题:缓存、批处理、延迟与成本优化 —— 推理延迟构成(TTFT / TPOT / E2E)、流式输出与成本归因
  • 标签:LLM 应用工程、推理延迟、TTFT、TPOT、批处理、成本归因
  • 难度:基础题(中高)

题目#

你们的 Agent 服务上线两周后,产品同学反馈”变慢了”。你打开监控面板,发现上游只暴露了两个指标:avg_latency(所有请求的平均端到端耗时)和 total_tokens(当日总 token 数)。

请回答:

  1. LLM 推理的延迟应该拆成哪几段?每一段的物理瓶颈分别是什么?
  2. 为什么平均值和总 token 数都不足以定位”变慢了”这个问题?你需要哪些指标?
  3. 批处理(batching)在什么条件下是净收益,什么条件下是负收益?
  4. “成本”应该归因到哪个粒度才具备行动含义?

先思考#

  1. 用户感知的”快”,到底对应哪一段?是首字节还是总时长?
  2. 如果输出长度翻倍,首 token 时间会变吗?端到端时间会怎么变?
  3. 批处理提高了 GPU 利用率,为什么单个用户的请求反而可能变慢?
  4. 一个 Agent 任务包含 12 次模型调用,单次调用 P50 下降 20%,任务级 P50 会下降多少?
  5. “总 token 涨了 30%“这句话,能不能告诉你是哪个租户、哪类任务、哪一步造成的?

参考答案#

结论:延迟必须拆成 TTFT(首 token 时间)与 TPOT(每输出 token 时间)两段,因为它们的瓶颈物理不同、优化手段不同、用户感知也不同。平均值和总 token 都无法定位问题——前者掩盖双峰与长尾,后者掩盖结构。批处理是以单用户尾延迟换吞吐,只有在”吞吐是约束且延迟预算仍有余量”时才是净收益。成本必须归因到任务与租户粒度,否则”成本涨了”这句话没有任何行动含义。

定义(以下四条均出自 MosaicML 工程团队公开的性能工程博客,已核验):

  • TTFT(Time To First Token):用户输入查询后多久开始看到模型输出。原文:

    “How quickly users start seeing the model’s output after entering their query. Low waiting times for a response are essential in real-time interactions, but less important in offline workloads. This metric is driven by the time required to process the prompt and then generate the first output token.”

  • TPOT(Time Per Output Token):为每个查询用户生成一个输出 token 的时间。原文给了直观换算:

    “a TPOT of 100 milliseconds/tok would be 10 tokens per second per user, or ~450 words per minute, which is faster than a typical person can read.”

  • Latency:

    “latency = (TTFT) + (TPOT) * (the number of tokens to be generated)”

  • Throughput:推理服务跨所有用户与请求每秒生成的输出 token 数。

适用边界:这套分解只覆盖单次生成调用。Agent 的一次任务通常包含 N 次调用(规划、工具选择、消化工具结果、总结),此时用户感知的延迟是 Σ(TTFT_i + TPOT_i × out_i) + Σ工具等待,而这个求和的方差会累积——所以单次调用 P50 改善 20%,任务级 P50 的改善既不是 20% 也不一定是改善,取决于哪一步在长尾上。这也是 Agent 与聊天机器人在性能工程上最大的分野。

简洁示例:3000 token 的上下文,期望输出 400 token。

  • TTFT ≈ prefill 3000 token 的时间(还叠加了批处理中的排队等待与 KV cache 分配)
  • E2E ≈ TTFT + 400 × TPOT
  • 若 TTFT = 1.2s、TPOT = 25ms,则 E2E ≈ 11.2s。流式让用户 1.2s 后开始读,但总时长不变。
  • 若把批大小从 4 提到 32,吞吐可能提升数倍,但每个用户的 TPOT 会变差。原文:

    “if we process 16 user queries concurrently, we’ll have higher throughput compared to running the queries sequentially, but we’ll take longer to generate output tokens for each user.”

相近概念的区别:

  • 吞吐 vs 延迟:吞吐是系统侧指标(全体的 tokens/s),延迟是用户侧指标。二者通过批处理耦合,优化其中一个通常伤害另一个。把吞吐当健康指标、把延迟当结果指标,是绝大多数”监控看着挺好但用户在骂”的根源。

  • TTFT 的瓶颈 vs TPOT 的瓶颈:原文:

    “Generating the first token is typically compute-bound, while subsequent decoding is memory-bound operation.”

    所以 prefill 主要受算力与批并行度影响,decode 主要受显存带宽与 KV cache 影响;两者的优化手段完全不同(前者靠并行与算子,后者靠访存效率、量化、KV cache 管理)。

  • 流式(streaming) vs 更快:流式只改变信息到达的时间分布,不改变 token 总数与总时长。它改善的是”感知等待”,对 E2E 零贡献。用流式去满足一个 E2E 的 SLO 是无效的。

  • 单次调用成本 vs 任务成本:Agent 的任务成本 = Σ 各次调用的 input + output token,且上下文在多轮间重复计费(除非命中前缀缓存)。只看”总 token”无法回答”哪个租户在亏钱""哪次改 prompt 让成本翻倍”。

  • P50 vs P99:批处理与排队引入的是尾延迟。均值与 P50 在大批场景下常常毫无变化,只有 P95 / P99 会动——这与 B01 里”计划回归表现为双峰”是同一类观测陷阱。

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

  1. 分层 SLO:交互型 Agent 用 TTFT 定 SLO(用户何时开始看到东西),批量 / 后台型用吞吐与单位成本定 SLO。混在一路服务里就必须分级排队。
  2. Agent 特有的放大效应:一轮对话可能触发 5–20 次模型调用,每次都重付一次 TTFT。因此”减少调用轮次、缩短系统提示、提高前缀缓存命中”对任务级延迟的影响,往往大于”换一个更快的模型”——这是 Agent 性能工程最容易被忽略的杠杆。
  3. 成本归因:按 trace_id → task_id → tenant_id → step 四个粒度记录 input / output token 与 TTFT / TPOT,才能回答”哪个租户在亏钱""哪次改动让成本翻倍”。
  4. 缓存布局:前缀缓存命中直接降低 TTFT 与 prefill 成本,因此上下文中稳定不变的部分必须前置。这是上下文工程与成本工程的交叉点(关联 2026-10-19 B03 提示缓存断点)。
  5. 降级:延迟预算紧张时,限制输出长度直接线性影响 E2E 的后半段;对低优先级租户降级到更小模型则同时影响两端。

面试现场回答(约 100 秒):

“我会先把 latency 这一个词拆掉。LLM 推理的延迟由 TTFT 和 TPOT 两段构成,端到端约等于 TTFT 加上 TPOT 乘以输出 token 数。这两段的物理瓶颈不一样:生成第一个 token 通常是计算密集的 prompt 处理阶段,后续每个 token 是访存密集的 decode 阶段,所以优化手段也不同——前者靠算力与批处理并行,后者靠显存带宽、KV cache 与量化。只看平均值会漏掉两件事:一是批处理带来的尾延迟,Mosaic 那篇工程博客明确说并发处理 16 个请求吞吐更高,但每个用户拿到输出 token 会更慢;二是双峰,缓存命中与未命中是两个完全不同的分布,均值几乎是常数。所以我的观测口径一定分 TTFT 与 TPOT 各自的 P50 / P95 / P99,再加上每任务的总 token 与每任务的调用次数。批处理方面,它是拿单用户尾延迟换吞吐,只有当吞吐是瓶颈、且延迟 SLO 还有余量时才是净收益;在线交互场景我用连续批处理而不是静态大批,离线批处理场景才接受延迟换成本。成本维度上,Agent 和聊天机器人最大的区别是一轮任务会有 N 次模型调用,上下文还会被重复送入,所以成本必须归因到 trace、task、tenant 与步骤四个粒度,否则’总成本涨了’这句话没有任何行动含义。”

追问#

追问:你把批大小调大之后吞吐翻倍了,但用户投诉反而变多。为什么?怎么权衡?

回答要点:这正是吞吐与 TPOT 的取舍——批越大,同一批内所有请求共享算力,单位时间总 token 更多,但每个用户分到的每 token 速度更慢;同时排队等待时间计入 TTFT。权衡方式分四步:其一,先明确这一路请求的 SLO 属于交互型还是吞吐型;其二,给批大小设上界(按 KV cache 水位而非单纯按显存容量),并把”排队等待时间”从 TTFT 里单独拆出来观测;其三,按关键性分级,高优先级用小批或优先队列,低优先级用大批;其四,用”满足 SLO 前提下的最大吞吐”而不是”最大吞吐”作为优化目标——后者在数学上会把系统推向不可用的边界。补充一点:批内长请求会拖长短请求的完成时间(队头阻塞),这是连续批处理要解决的问题,而不是靠调大批大小能解决的。

关键点#

  1. E2E ≈ TTFT + TPOT × 输出长度;两段瓶颈不同(compute-bound vs memory-bound),不能混为一谈。
  2. 批处理以单用户 TPOT 换吞吐;净收益取决于 SLO 类型与延迟余量,不是”越大越好”。
  3. 均值与总 token 都会掩盖结构;必须分位数 + 分层(task / tenant / step)归因。
  4. 流式只改感知,不减总量;用流式满足 E2E SLO 是无效的。
  5. Agent 场景下成本与延迟的主乘数是每任务的模型调用次数与上下文复用率,而不是单次调用的快慢。

常见误区#

  1. “GPU 利用率高 = 服务健康。” 利用率是时间占比,不反映排队与尾延迟;高利用率常常正是大排队的成因。判定健康度要看排队等待时长与 TPOT 的分位数(关联 2026-10-23 S01 的容量规划场景)。
  2. “开了流式就变快了。” 流式只让首字节更早到达,token 总数与总时长不变。用户”感觉快”不等于 SLO 达标,也不等于成本下降。
  3. “总 token 涨了 30%,去优化 prompt 就行。” 必须先按租户 / 任务 / 步骤下钻。可能是某个接入方改了调用方式,也可能是 Agent 陷入了重试循环。不下钻的归因等于没归因——与 S01 里”重复触发必须靠幂等键度量”是同一个道理。
  4. “批越大越划算。” 批大小的上界由 KV cache 显存决定,且批内长请求会造成队头阻塞。静态大批只适用于请求长度均匀的离线场景。

延伸学习#

  • MosaicML / Databricks 工程博客《LLM Inference Performance Engineering: Best Practices》(2023-10-12):https://www.databricks.com/blog/llm-inference-performance-engineering-best-practices (已核验:TTFT / TPOT / Latency / Throughput 四指标定义与换算、吞吐与 TPOT 的取舍原文、“Generating the first token is typically compute-bound, while subsequent decoding is memory-bound operation”、连续批处理的适用建议)
  • arXiv:2211.05102《Efficiently Scaling Transformer Inference》(Pope、Douglas、Chowdhery、Devlin、Bradbury、Levskaya、Heek、Xiao、Agrawal、Dean,2022-11-09 提交,cs.LG / cs.CL):摘要给出”a low-batch-size latency of 29ms per token during generation (using int8 weight quantization) and a 76% MFU during large-batch-size processing of input tokens” —— 可作为 prefill 与 decode 两阶段效率差异的量化佐证(已核验)

S01 · 长耗时工具调用改造:从同步超时到 MCP Tasks#

  • 题目 ID:Q-20261028-S01
  • 题型:AI Agent 真实研发场景题
  • 场景题类型:长耗时工具调用改为 MCP Tasks(持久句柄、轮询节奏、中途人工输入与协作式取消)
  • 一级类型:工具与互操作协议(兼属分布式系统、AI Agent 核心)
  • 二级主题:MCP Tasks 状态机、句柄生命周期、协作式取消、幂等派发
  • 标签:工具与互操作协议、MCP-Tasks、分布式系统、幂等、协作式取消、人工介入
  • 难度:场景题(高)

题目#

业务背景:某企业内部研发 Agent 平台,把研发流程工具(触发 CI 流水线、批量数据回填、跨仓库重构、生成并发布报表)通过 MCP 暴露给 Agent。

系统规模:42 个工具,日均 8 万次工具调用。其中 6 个工具 P50 耗时 90 秒、P99 耗时 15 分钟。单次 Agent 会话平均 6 次工具调用,Agent 并发峰值 300。

当前架构:Agent 运行时 → 统一 MCP 网关 → 各工具服务。所有 tools/call 都是同步语义:网关 HTTP 超时 60 秒;Agent 侧把超时按”工具失败”处理,并重试一次。

目标:支持最长 30 分钟的工具执行;客户端重启或断线后仍能取回结果;需要中途确认的工具不要让确认请求石沉大海。

限制条件:

  • 不能要求所有工具服务自己实现任务系统(多数是包装既有异步 API 的薄层)。
  • 必须向后兼容不支持 Tasks 的客户端。
  • 网关与 Agent 之间已有分布式追踪上下文传播。

故障现象:

  1. 长工具 P99 频繁超时,Agent 把超时当成”工具没生效”并重试,导致同一 commit 的流水线被触发 3 次。
  2. 客户端断线重连后无法找回结果,只能重新发起,成本翻倍。
  3. 有 2 个工具需要中途向用户确认(“这个迁移会影响 12 个下游,是否继续”)。当前实现是在工具内部阻塞等待 HTTP 回调,网关超时后确认请求丢失,任务卡死。

需要取舍的问题:把同步调用改为 Tasks 持久句柄会引入轮询开销、句柄生命周期管理、结果保留成本,以及”重提 / 取消 / 续跑”三种语义的选择。请给出改造方案。

先思考#

  1. “超时”在语义上是几态?把它降成”失败”会破坏什么前提?
  2. 要让断线后能找回结果,最小需要持久化什么?
  3. 服务端返回句柄之后崩溃了,客户端会看到什么?谁来负责”有句柄但执行没发生”?
  4. 取消请求发出后,服务端其实已经产生了副作用,系统该怎么表达这件事?
  5. 客户端不支持 Tasks 时,服务端应该返回什么才不会重演今天的故障?

参考答案#

先给结论:不要试图靠”把超时调大”来解决,而要把”执行”与”等待”解耦成两个独立生命周期。 服务端在任务被持久化创建之后立即返回 CreateTaskResult(resultType: "task")与 taskId;客户端按 pollIntervalMs 轮询 tasks/get 直到终端态;中途需要输入时进入 input_required,客户端用 tasks/update 回填;取消走 tasks/cancel,但必须按协作式设计——服务端只确认收到意图,任务仍可能走到 completed。重复触发不能靠 Agent 自律,必须在网关用幂等键收敛。

问题拆解:三个故障其实是同一个根因的三个投影——同步语义把”网络等待”当成了”执行状态”。

  • 重试重复触发:超时是三态(成功 / 失败 / 未知),网关把它降成了”失败”。而重试的前提是可重试性判定,这个判定今天完全缺失。
  • 断线丢结果:结果的引用只存在于连接上下文里。连接一断,引用消失,即使执行仍在继续。
  • 中途确认丢失:把”人机交互”塞进同步请求,等于让人类的响应时间去跟 HTTP 超时赛跑——这场比赛人类必输。

MCP 官方对 Tasks 的定位恰好对应这三点:“MCP Tasks let servers return a durable handle instead of blocking, so clients can poll for progress, provide input when needed, and retrieve the final result after reconnecting.”

核心方案(三段):

  1. 解耦:长耗时工具在 MCP 层改为返回任务句柄。句柄是唯一引用,任务状态由服务端持久化,连接不再是状态的容器。
  2. 收敛:在统一 MCP 网关上做”任务语义适配层”,工具服务本身不必实现任务系统——网关负责生成 taskId、持久化状态、按 pollIntervalMs 应答、把 input_required 转成一次人工审批单。关键约束:任务必须在返回响应之前持久创建,官方原文是 “The task is durably created before the response is sent.” 否则客户端拿到句柄后服务端崩溃,就会出现”有句柄、无执行”的孤儿态,且客户端会一直轮询一个永不前进的任务。
  3. 兜底:副作用的幂等由执行侧保证(幂等键 + 唯一约束抢占);取消只是”意图”,重提只是”查询”,结果有 TTL 与成本上限。

执行流程(严格按序):

  1. 能力协商:客户端在每个请求的 _meta 里声明 io.modelcontextprotocol/clientCapabilities.extensions["io.modelcontextprotocol/tasks"] = {};服务端在 server/discover 响应的 capabilities.extensions 里声明同一扩展。
  2. 创建任务:服务端判断该请求将长耗时——注意这是服务端决定的,官方原话 “Server-directed. The server decides per-request whether to create a task.” 返回 CreateTaskResult:含 resultType: "task"、taskId、初始 status、ttlMs、pollIntervalMs。
  3. 持久化句柄:客户端必须持久存储 taskId,这是官方对客户端的第 5 条要求:“Store task IDs durably so polling can resume after a client crash or restart.”
  4. 轮询:客户端调 tasks/get,遵守 pollIntervalMs,直到状态进入 completed / failed / cancelled(这三个是终端态,官方明确 “completed, failed, and cancelled are terminal — once reached, the task’s state does not change.”)。
  5. 中途输入:若状态为 input_required,响应里带 inputRequests map;客户端呈现给用户或模型,用 tasks/update 提交 inputResponses。服务端对未知或已满足的 key 应忽略(“Ignore responses for unknown or already-satisfied keys.”)。
  6. 取消:客户端随时可发 tasks/cancel;服务端返回空结果确认,但不保证停止工作。
  7. 取结果:completed 时 result 字段就是原本同步会返回的内容;failed 时 error 字段是 JSON-RPC 错误。返回形状与原同步调用一致,这是让上层改动最小的关键。

技术选型与选择依据:

  • 采用 MCP Tasks 官方扩展,而不是自造协议。理由:状态机、客户端与服务端职责、能力协商方式都已有标准定义,跨团队与跨厂商互操作成本最低。它是”扩展”而非核心,官方明确 “Extensions are always disabled by default and require explicit opt-in from the developer.” 这正好符合”默认不改变既有行为”的迁移要求。
  • 轮询 vs 通知:默认轮询,通知加速。官方说明:“Servers can push status updates via notifications/tasks. Clients opt into these through the subscriptions/listen mechanism. Each notification carries the full task state, eliminating the need for an extra tasks/get round-trip.”取舍:通知需要订阅通道,且存在漏通知与乱序风险;因此正确组合是通知加速 + 轮询兜底,而不是二选一。
  • 网关侧适配 vs 各工具服务各自实现:选网关适配。理由有三:工具服务数量多、实现能力参差;任务状态需要统一的可观测与审计;网关是唯一同时能看到 trace、权限与副作用的层。备选方案是只改造 6 个长耗时工具,代价是两套语义并存、客户端需要知道”哪些工具是异步的”——这在工具数继续增长时会失控。
  • 幂等键设计:idempotency_key = hash(tool_name, 规范化后的参数, 用户/租户, 业务意图窗口),在网关侧用唯一约束抢占。规范化必须由服务端执行并与执行侧共用同一函数,否则”看起来一样的参数”会生成两个键。

方案取舍:

  • 轮询节奏 vs 延迟 vs 成本:短间隔感知快,但 300 并发 × 每 2 秒轮询 = 网关 150 QPS 的纯查询负载;长间隔省资源但结果延迟。做法是按状态分级:working 用服务端建议的 pollIntervalMs 并允许指数退避到上限(任务反正不会更快);input_required 立即唤醒(它在等人,不在等算力,退避毫无意义)。
  • 单 Agent 长等 vs 多 Agent 并行:长任务期间可以让 Agent 挂起或去做别的。挂起 + 检查点更省 token,但需要恢复点;继续推进则占用上下文且可能基于不完整信息过早下结论。取舍依据是任务的可分解性:可分解就并行,不可分解就挂起。
  • 协作式取消 vs 强制终止:官方明确 “Cancellation is cooperative — the server acknowledges the intent but is not obligated to stop the work.” 因此”取消成功”不能成为风控断言;真正的止损必须在执行侧(幂等键、写闸门、已发生副作用的补偿)。
  • 实时处理 vs 异步处理:交互型工具(秒级)保持同步,避免为 200ms 的调用引入句柄生命周期成本。迁移判据是 P90 是否明显超过网关超时——只对这类工具启用 Tasks。

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

  • 三层超时必须分开:网关 HTTP 超时(对 Tasks 只作用于单次请求,不作用于任务本身)、任务总时限(超过则置 failed 并触发补偿)、客户端轮询 deadline(到点后不是重试,而是转人工或告知用户”仍在进行”)。
  • 句柄丢失:客户端持久化 taskId 是主路径;网关另提供”按幂等键反查最近任务”的能力,让 Agent 在丢句柄时用同一参数找回——这依赖”同参数 + 同意图 = 同一任务”的幂等契约。
  • 孤儿任务:服务端创建任务后、真正派发前崩溃,任务停在 working 且永不前进。兜底是任务记录带 dispatched_at,由扫描器把”创建超过 N 分钟未成功派发”的任务重新派发或置 failed。
  • 结果过期:ttlMs 到期后结果不可取。取舍是存储成本 vs 客户端回取成功率:终端态结果至少保留一个业务周期(如 7 天),并让客户端在 TTL 临近前取回并落自己的库。
  • 重试的正确位置:只有”尚未派发”的状态才可安全重提;一旦派发过,重提必须走幂等键返回原任务,而不是新建。这是”超时三态”在 Tasks 语义下的落地方式。
  • 恢复流程:Agent 会话崩溃后恢复时,先枚举”我名下未终结的 taskId”——working 的继续轮询,input_required 的重新向用户发起确认,已进入终端的直接取结果。
  • 取消后仍完成:任务可能以 completed 终结,官方原话 “the task may still reach a non-cancelled terminal status.” 因此 UI 与审计都不能把 cancelled 当作”未发生”。

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

  • 权限:tasks/get、tasks/update、tasks/cancel 都必须校验发起主体是否为最初发起者或同一租户。taskId 是类 bearer 的引用,泄露即等于可读取结果,必须做归属校验而不是凭 ID 直接返回。
  • 人工输入的安全:input_required 携带的内容可能含敏感信息(待执行的 SQL、受影响的下游清单)。审批界面必须展示规范化后的参数,而不是模型的自由文本——这与 2026-10-27 S01 的审批令牌绑定操作指纹是同一条原则。
  • 成本:轮询是纯开销,需要有配额与上限(最大轮询次数、最长等待时间);任务状态存储与结果保留按 TTL 设计并设总量上限。
  • 延迟:TTFT 类指标不适用于任务。应改为”提交 → 首个状态可见”与”提交 → 终端态”两个分位数,并按工具类型分桶。
  • 可靠性:任务存储应独立于工具服务,写入与派发解耦,避免”工具服务挂了导致任务状态丢失”。

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

  • 指标:重复触发率(同一幂等键被多次派发的比例,目标 0);孤儿任务数(创建未派发且超龄);轮询放大倍率(轮询 QPS / 在途任务数);input_required 的平均人工响应时长与超时率;任务级 P50 / P95 完成时长;TTL 到期前未取回的结果数。
  • 测试:注入”返回句柄后立即杀掉服务端”验证孤儿检测;注入”客户端断线 5 分钟后重连”验证恢复;对同一参数并发提交 5 次验证只派发一次;取消后强制工具继续执行,验证审计与补偿。
  • 上线:按工具维度灰度(先迁 P99 最长的那一个),双轨运行(同步路径保留给不支持 Tasks 的客户端),灰度期对比重复触发率与任务完成率两项指标。

三层能力归属(必须区分):

  • 模型的能力(概率性):决定调用哪个工具、填什么参数;把 input_required 的请求翻译成人类可读的确认文案;解释 failed 的原因并决定下一步。模型不负责状态持久化,也不负责判定”是否真的执行过”。
  • Agent 编排的能力:持有 taskId、按节奏轮询、在 input_required 时挂起并保留检查点、会话恢复后重新接管未完成的任务。
  • 传统软件系统的能力(确定性):任务状态机的持久化与迁移、幂等派发、TTL 与清理、权限与归属校验、审计记录、孤儿检测与补偿。“是否执行过”这个问题只能由这一层回答。

追问#

  1. 客户端不支持 Tasks 时怎么办? 服务端在返回 CreateTaskResult 之前必须校验客户端能力,官方原文 “Never return a task to a client that did not declare support.” 对不支持的客户端走原同步路径,但必须给它一个语义正确的超时响应:不是”失败”,而是带明确错误分类(超时 + 操作可能仍在进行 + 幂等键),并让网关侧把这类请求的超时容忍提高到任务时限。同时把”不支持 Tasks 的客户端占比”做成指标,作为推动升级的依据。
  2. input_required 时用户一直不响应怎么办? 必须有 deadline:超过阈值置 failed(或转入离线审批队列)。关键取舍是这一步的走向必须是**“不做”而不是”默认继续”**——默认继续等于把沉默当授权。同时要单独观测审批疲劳(连续通过率、平均响应时长),连续通过率接近 100% 说明审批已经流于形式。
  3. 任务结果很大(几百 MB)怎么取? 结果体不应塞进 tasks/get 响应;应返回指向对象存储的短期签名引用,并让引用的有效期与结果 TTL 对齐,引用还要带校验和。这样同时也复用了 S02 讨论的那一套对象生命周期治理。

关键点#

  1. 超时是三态,不是”未执行”。同步语义下把它降成失败,必然导致重复触发副作用。
  2. 句柄(taskId)是唯一引用,任务是持久对象,连接不是状态容器;任务必须在响应发出前持久创建。
  3. 取消是协作式的,cancelled 不等于”未发生”;审计与风控不能依赖它。
  4. 幂等必须由”参数 + 意图”的指纹承担,taskId 只保证”能找到这一次”。
  5. 通知加速、轮询兜底,二者不互换。

常见误区#

  1. “把网关超时从 60 秒调到 30 分钟就好了。” 超时只解决等待,不解决断线丢结果与确认丢失;而且长连接会占满连接池(池容量 = 并发 × 占用时长),改一个超时会把故障平移到连接池耗尽。
  2. “客户端收到 cancelled 就等于没执行。” 取消是协作式的,服务端可能已经完成。把取消当作风控断言会在审计上留下错误结论,而在退款、发布这类场景里这个结论是要负责的。
  3. “有了 taskId 就不用幂等了。” taskId 只保证”能找到这一次”;Agent 重试时若重新提交,会创建一个新任务。幂等必须由内容指纹承担。
  4. “有通知就可以不轮询了。” 通知会漏、会乱序、依赖订阅通道可用性。通知是优化手段,轮询是正确性保障,二者的地位不对等。

延伸学习#

  • MCP 官方扩展《Tasks》:https://modelcontextprotocol.io/extensions/tasks/overview (已核验:五状态定义与终端态、pollIntervalMs 与 ttlMs、协作式取消原文、inputRequests / inputResponses 机制、server-directed 判定、任务须在响应前持久创建、客户端须持久存储 task ID)
  • MCP 官方《Extensions Overview》:https://modelcontextprotocol.io/extensions/overview (已核验:扩展标识符格式 {vendor-prefix}/{extension-name}、“Extensions are always disabled by default and require explicit opt-in”、能力协商的 _meta 位置与 server/discover 响应、优雅降级要求)

S02 · 分片全部 200,附件却打不开#

  • 题目 ID:Q-20261028-S02
  • 题型:非 AI Agent 真实研发场景题(通用软件工程能力)
  • 场景题类型:数据一致性、资源生命周期与失败语义(非 Agent,核心方案不依赖模型或 Agent)
  • 一级类型:软件工程与平台工程(兼属分布式系统、数据库与存储)
  • 二级主题:分片上传、断点续传、孤儿资源回收与跨系统原子性
  • 标签:分片上传、对象存储、幂等、孤儿资源、预签名 URL、断点续传
  • 难度:场景题(高)

题目#

业务背景:某 SaaS 产品支持用户上传附件(设计稿、数据集、录屏),单文件最大 5 GiB。

系统规模:日均 40 万次上传,平均 120 MB,P99 1.2 GB。上传完成后下游有 6 个消费服务(病毒扫描、转码、缩略图、全文索引、合规留存、通知)。

当前架构:浏览器 → 应用服务(Go)→ 对象存储(S3 兼容)。旧实现是应用服务代理流式转发;新实现改为浏览器用预签名 URL 直传对象存储,走分片上传(multipart upload)。上传完成后写一条 attachments 记录(status = uploaded),并投递一条”待扫描”消息。

目标:上传成功率 ≥ 99.9%;大文件支持断点续传;不产生无法回收的存储成本。

限制条件:

  • 不能把对象存储的长期凭证下发给浏览器。
  • 弱网与移动端占比 35%。
  • 已有对象存储是 S3 兼容,可配置为任意厂商实现,但语义以 S3 为准。

故障现象:

  1. 账单里对象存储容量每月涨 15%,但业务侧统计的附件总量只涨 4%。差额来自未完成的上传残留。
  2. 用户反馈”上传完成但附件打不开”:分片全部返回 200,但 complete 请求失败,对象从未生成。
  3. 同一文件重复上传会产生多条 attachments 记录与多份副本。

需要取舍的问题:并发分片数 vs 弱网成功率 vs 客户端资源占用;直传 vs 代理;立即合并 vs 延迟合并;孤儿清理的激进程度 vs 误删正在进行的上传。

先思考#

  1. 客户端拿到所有分片的 200 之后,能否认为”上传完成”?
  2. 一个未完成的上传,什么时候才停止计费?判据在谁手里?
  3. 用户刷新页面后,系统凭什么知道哪些分片已经在服务端了?
  4. 预签名 URL 泄露会造成什么?最小权限应该限定到什么粒度?
  5. 同名文件重复上传,应在哪一层判定”这是同一个文件”?文件名能不能作为判据?

参考答案#

先给结论:把”上传完成”的定义从客户端视角改为服务端视角——只有当合并(CompleteMultipartUpload)成功、对象可被读取、并且业务记录与对象校验和对齐之后,才算完成。 未完成的分片是真实的持续计费资源,必须靠生命周期规则 + 应用层台账双重回收;重复上传用内容指纹在应用层收敛,而不是指望对象存储去重。

问题拆解:三个故障同样是”判定点错位”的三个投影。

  • 成本差额 = 孤儿分片(已 initiate 但从未 complete 或 abort)。AWS 文档明确:

    “After you initiate a multipart upload and upload one or more parts, you must either complete or stop the multipart upload to stop incurring charges for storage of the uploaded parts.”

  • “分片全 200 但打不开” = 客户端把”分片成功”当成了”对象存在”。对象是在 complete 时才被构造的:

    “Upon receiving the complete multipart upload request, Amazon S3 constructs the object from the uploaded parts.”

  • 重复副本 = 缺少内容级幂等。uploadId 只标识一次上传会话,不是内容身份;把它当去重依据会漏掉”用户重开页面重传同一文件”这一最常见的情形。

核心方案:

  1. 两阶段 + 业务台账:attachments 记录在建会话时就创建(status = uploading),保存 upload_id、内容指纹(SHA-256)、expected size、分片大小、已完成分片列表。只有当 complete 成功且 HEAD 校验通过后才置 uploaded。台账先行的好处是:任何孤儿都能被”台账里有、对象里没有”或”对象里有、台账没有”这两种对账之一捕获。

  2. 完整请求必须带清单。AWS 文档:

    “Your complete multipart upload request must include the upload ID and a list of part numbers and their corresponding ETag values.”

    所以客户端必须把每个分片返回的 ETag 记录下来——这既是合并的输入,也是断点续传的元数据来源。由服务端发起 complete(而不是把凭证给客户端)是关键设计:客户端只提交清单,服务端校验后才调 complete。

  3. 孤儿回收双保险:桶级生命周期规则 AbortIncompleteMultipartUpload(清理无主与超龄的未完成上传)+ 应用层扫描(按台账比对并主动 abort)。生命周期规则是兜底,应用层台账才能给出”为什么残留”的可观测性——没有台账,你只能看到容量在涨,看不到是谁在涨。

  4. 内容级幂等:客户端先算文件 SHA-256(或按分片算指纹树),init 时带上;后端按 (tenant_id, sha256, size) 查已有记录——命中 uploaded 直接返回”秒传”,命中 uploading 则返回已有 upload_id 与已完成分片用于续传。

执行流程(顺序不能换):

  1. 客户端计算内容指纹 → 请求应用服务 POST /attachments:init(指纹、大小、类型、文件名)。

  2. 应用服务做权限与配额校验 → 查幂等(命中 uploaded 直接返回;命中 uploading 返回已有 upload_id 与已完成分片)→ 调 CreateMultipartUpload → 台账落 uploading(含 upload_id 与 expires_at)→ 返回 upload_id 与已完成分片列表。

  3. 客户端按需请求预签名分片 URL(逐片或批量短时),带 partNumber;并发上传分片,弱网下调低并发、增大重试次数。

  4. 每片成功 → 记录 partNumber → ETag 并上报(或由客户端保留,complete 时提交)。

  5. 全部分片完成 → 客户端请求 POST /attachments:complete(upload_id + 分片清单)→ 应用服务在服务端调 CompleteMultipartUpload(可与 ListParts 交叉核对)→ HEAD 校验 size 与 checksum → 置 uploaded → 投递”待扫描”消息。

  6. 失败或放弃 → AbortMultipartUpload。这里有一个容易被忽略的顺序要求:

    “To make sure you free all storage consumed by all parts, you must stop a multipart upload only after all part uploads have completed.”

    即:先停客户端在途分片,再发 abort,否则在途分片仍会写入并残留。

技术选型与选择依据:

  • 直传 vs 代理:选直传。代理会把应用服务的带宽、连接数与内存变成上传瓶颈(1.2 GB × 并发 = 应用服务被打爆),且无法实现断点续传。代价是客户端变复杂、凭证下发面变大——用按分片、按 partNumber 粒度的短期预签名 URL 把暴露面压到最小。备选方案是代理 + 应用服务本地落盘再转存,代价是应用服务成为有状态节点,扩缩容与故障恢复都要重新设计,只在必须做同步内容审查的场景才值得。

  • 分片大小与并发度:受两条硬约束——分片数上限 10,000、单片 5 MiB–5 GiB,最后一片无最小限制(已核验)。因此分片大小必须满足 ceil(total / partSize) ≤ 10000,例如 5 GiB 文件用 1 MiB 分片会直接超限。做法是按文件大小动态推导 partSize = max(5 MiB, ceil(total / 10000)),弱网下取 8–16 MiB 以平衡重试粒度与请求数,并发 3–4。

  • 校验和:启用对象级 checksum。AWS 文档:

    “When you provide a full object checksum during a multipart upload, the AWS SDK passes the checksum to Amazon S3, and S3 validates the object integrity server-side, comparing it to the received value. Then, S3 stores the object if the values match. If the two values don’t match, Amazon S3 fails the request with a BadDigest error.”

    但要注意合并后的 ETag 不再是数据 MD5:“This ETag is not necessarily an MD5 hash of the object data.” 所以业务侧必须另存自己的校验和,不能拿 ETag 当内容指纹。

  • 对账信号:对象创建事件(服务端通知)可作为”确实生成了对象”的独立证据,与台账双向对账。

方案取舍:

  • 并发度:高并发提高吞吐,但放大弱网失败率与客户端内存 / 连接压力。弱网占比 35% 的场景下应降低并发、提高单请求超时、并对失败分片做分片级指数退避重试(而不是整文件重来)。
  • 立即合并 vs 延迟合并:立即合并让对象尽快可读,但 complete 的失败与重试都压在用户请求路径上;延迟合并(后台作业)把这段移出用户路径,代价是用户要轮询状态。取舍规则:小文件立即合并,大文件由后端作业合并——本质上是把”等待”与”执行”解耦,与 S01 的 Tasks 是同构的思路。
  • 清理的激进度:生命周期窗口越短越省钱,但可能误删慢速但合法的上传(弱网下 1.2 GB 可能要几十分钟甚至数小时)。做法是分层:生命周期窗口设为一个远大于 P99 上传时长的保守值(如 7 天)做兜底;应用层台账按”创建超过 X 小时无进展”主动 abort。两者职责不同,不能互相替代。
  • 强一致性 vs 吞吐:uploaded 状态的判定不能只看 complete 是否返回。complete 成功但业务库写入失败,会造成”有对象无记录”(漏);反过来会造成”有记录无对象”(打不开)。取舍是以业务库为唯一 source of truth,对象存储视为外部副作用,用对账任务补齐两侧。

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

  • 边界:分片号范围 1–10000,顺序由 part number 升序决定(“Amazon S3 creates an object by concatenating the parts in ascending order based on the part number”),且不要求连续。这里有一把双刃剑:

    “if you upload a new part using the same part number as a previously uploaded part, the previously uploaded part gets overwritten”

    这既提供了分片级幂等(重试同一片是安全的),也意味着”内容变了但 partNumber 相同”会静默覆盖——所以断点续传必须先校验指纹未变,再复用已完成分片。

  • 超时:预签名 URL 有效期要覆盖单片上传时间但尽量短;整体上传会话要有租约(uploading 记录带 expires_at)。

  • 重试:只重试失败分片,重试前重新校验预签名 URL 是否过期。complete 失败要按原因分类:清单不匹配 / 4xx 不可盲目重试;5xx 可重试,且 complete 本身对同一 upload_id + 同一清单是幂等的(重复 complete 得到同一对象)。

  • 幂等的两层:uploadId 保证”同一次上传会话”,(tenant, sha256, size) 保证”同一内容”。缺前者无法续传,缺后者无法秒传与去重。

  • 恢复:客户端崩溃后按指纹重新 init,后端返回已有 upload_id 与已完成分片,跳过已完成分片。注意 AWS 的提醒:

    “the returned list of parts doesn’t include parts that haven’t finished uploading.”

    所以 ListParts 只能作为交叉核对,不能作为唯一依据;权威记录是应用层台账。

  • 孤儿的三条来源与处置:用户放弃 → 客户端显式取消并 abort;complete 失败后进重试队列并最终 abort;abort 时仍有在途分片 → 先停客户端写入再 abort。

性能、可用性、安全、成本与延迟:

  • 安全:预签名 URL 是 bearer 凭证,泄露即可写。必须限定 partNumber、短有效期、固定 bucket 与 key 前缀——key 由服务端生成,不接受客户端拼路径,否则等于给了”往任意路径写”的能力(路径穿越与覆盖他人对象)。长期凭证绝不下发。另外 s3:AbortMultipartUpload 权限需单独授予:AWS 文档指出 initiator 默认拥有,但在 VPC endpoint policy 下 initiator 不自动获得该权限。
  • 成本:孤儿分片是纯粹的负资产。除了生命周期规则,还要把”在途上传量 / 已完成对象量”的比值做成告警——比值异常上升即说明清理链路失效,而不是”用户传得多了”。
  • 延迟:用户感知延迟 = 分片传输 + 合并时间;合并时间与分片数相关,所以分片不是越细越好。
  • 可用性:对象存储不可用期间,init 应快速失败并给出可重试错误,而不是让客户端白传几 GB;下游扫描与上传解耦(消息队列),上传成功不以扫描成功为前提。

验证指标与上线观测:

  • 孤儿分片容量 / 已完成对象容量(目标 < 2%);在途上传会话数与其最老年龄;分片失败率与重试成功率;断点续传命中率;complete 失败率(按错误码分类);“有对象无记录”与”有记录无对象”的对账不一致数(目标 0);秒传命中率;上传 P50 / P95 完成时长。
  • 上线方式:按文件大小灰度(先对 > 500 MB 的启用新链路),保留旧代理链路一个版本;灰度期对比成功率与上述成本比值两项。

追问#

  1. 分片上传能保证原子性吗? 在对象存储侧,complete 是原子的——对象要么不存在,要么完整构造。但”对象 + 业务记录”跨系统没有原子性。做法是以业务库为中心:先 complete 再置 uploaded,用对账补齐两侧;所有下游消费方只认 uploaded 状态,不认”分片都传完了”。
  2. 为什么不用一个请求直接传 5 GiB? 单次 PUT 在弱网下任何中断都要全量重传,且受连接稳定性与内存限制。分片把失败粒度降到单片并支持并行。AWS 给出的经验阈值是:

    “In general, when your object size reaches 100 MB, you should consider using multipart uploads instead of uploading the object in a single operation.”

  3. 用户上传的是恶意构造的文件(解压暴涨、含恶意宏)怎么办? 上传与内容处理必须解耦:uploaded 只代表”字节已到齐”,不代表”内容可用”。扫描与转码异步执行,且带沙箱、大小与时长上限;未通过扫描的对象要能删除或隔离;扫描完成前的对象不得对外可访问(私有桶 + 签名读)。

关键点#

  1. 完成点是服务端合并,不是”所有分片返回 200”。对象在 complete 时才被构造。
  2. 未完成的分片持续计费,直到 complete 或 abort;这是真实的、可被度量的负资产。
  3. 幂等有两层:uploadId(会话)与内容指纹(身份),缺一不可。
  4. 合并后的 ETag 不是数据 MD5,不能当内容指纹;业务侧必须自存校验和。
  5. 预签名 URL 是完整写凭证,key 必须服务端生成并限定前缀与 partNumber。

常见误区#

  1. “分片全部返回 200 就上传成功了。” 对象在 complete 时才被构造;缺了 complete,你只有一堆不可寻址的分片,同时还在计费。这是本题故障现象二的直接成因。
  2. “ETag 就是文件的 MD5,可以拿来做去重。” AWS 明确 “This ETag is not necessarily an MD5 hash of the object data.” 分片合并后的 ETag 是 checksum of checksums,把它当内容指纹会在去重与校验两处同时出错。
  3. “配一条生命周期规则就能自动清掉所有残留。” 它清的是”未完成且超龄”的上传;如果台账状态错乱、或分片仍被引用,规则无能为力。而且窗口设得过短会误杀慢速但合法的上传——尤其在弱网占比高的产品里。
  4. “预签名 URL 只是临时链接,权限小一点没关系。” 它是完整的写凭证。key 若由客户端提供,等于把”往任意路径写”的能力交给了浏览器。

延伸学习#

  • AWS S3 用户指南《Uploading and copying objects using multipart upload》:https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html (已核验:三步流程与 upload ID、part number 与 ETag 必须成对提交、同 partNumber 覆盖语义、计费持续到 complete/abort、ListParts 不含未完成分片、AbortIncompleteMultipartUpload 建议、先停客户端再 abort 的顺序要求、各操作所需权限)
  • AWS S3 用户指南《Amazon S3 multipart upload limits》:https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html (已核验:最大对象 48.8 TiB、最大分片数 10,000、part number 1–10000、单片 5 MiB–5 GiB、最后一片无最小限制、ListParts 每次最多返回 1000、100 MB 起建议分片)

L01 · 208. 实现 Trie(前缀树)#

  • 题目 ID:Q-20261028-L01
  • 题型:LeetCode
  • LeetCode ID:208
  • 中文名:实现 Trie(前缀树)
  • 英文名:Implement Trie (Prefix Tree)
  • 难度:medium(说明:中英文官方页面本次均未渲染难度标签,该难度按 LeetCode 官方题库的标准分类记录,未直接核验)
  • 官方链接:https://leetcode.com/problems/implement-trie-prefix-tree/ ;https://leetcode.cn/problems/implement-trie-prefix-tree/ (两个页面本次均已访问,题号、标题、示例与约束完全一致)
  • 一级类型:LeetCode — 树 / 设计(Trie)
  • 二级主题:前缀树、逐字符路径检索、前缀共享
  • 标签:Trie、设计、字符串、前缀检索、空间取舍
  • 英文页通过率:70.2%(会随时间变化)

题目#

Trie(发音类似 “try”)或者说前缀树是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word。
  • boolean search(String word) 如果字符串 word 在前缀树中(即,在检索之前已经插入),返回 true;否则返回 false。
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix,返回 true;否则返回 false。

输入 / 输出:按操作序列调用构造器与方法,输出每个有返回值方法的返回值,构造器与 insert 输出 null。

约束:

  • 1 <= word.length, prefix.length <= 2000
  • word 和 prefix 仅由小写英文字母组成
  • insert、search、startsWith 调用次数总计不超过 3 * 10^4 次

示例:

输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True

先思考#

  1. 如果用 HashSet<String> 存所有已插入单词,startsWith 要怎么算?代价是多少?
  2. search("app") 与 startsWith("app") 都沿同一条路径走,返回结果为什么可以不同?差异应该记在哪个位置?
  3. 插入 “apple” 之后再插入 “app”,需要新建节点吗?只需要在哪里打标记?
  4. 走到某个字符发现子节点不存在,此时能立即给出什么结论?还需要继续往下走吗?
  5. 每个节点存 26 个指针,和一个只存实际出现字符的映射,空间上差在哪?字符种类很多时呢?

参考答案#

结论:用一棵”边代表字符、节点代表前缀”的多叉树。insert 沿字符逐层下沉,缺失则建节点,末端节点打 is_end 标记;search 要求路径走完且终点带 is_end,startsWith 只要求路径走完。时间复杂度与字符串长度线性相关,与已插入的单词总数无关——这是它相对哈希集合的核心优势。

题号 / 中英文名 / 难度 / 链接:见本节头部(LeetCode 208,中文名”实现 Trie(前缀树)“,英文名 “Implement Trie (Prefix Tree)“,medium)。

解题思路(推荐方案的推导):

  • 直观方案:用 HashSet<String> 存所有已插入的单词。search 是 O(1) 平均、startsWith 要遍历全部已插入单词做 startswith 判断,即 O(N × L)。在 N 很大(比如词典十万级)而查询以”前缀”为主时,这个方案直接不可用。
  • 它暴露的需求:前缀查询要求”按字符逐层收敛候选集”。哈希把整个字符串打成一个值,破坏了前缀结构;而 Trie 保留这个结构,代价是空间。
  • 推荐方案:节点存”子节点映射 + 是否是某个单词的结尾”。插入时逐字符下沉,缺失则建;查询时逐字符下沉,遇到缺失立即返回失败——这是早停,也是 Trie 比哈希快的地方(哈希要算完整串的哈希值,Trie 在第一个不匹配的字符就能停)。
  • search 与 startsWith 的唯一差别:终点节点的 is_end。插入 “apple” 后,“app” 这条路径存在但没有 is_end,所以 startsWith("app") 为真、search("app") 为假;再插入 “app” 后,只需把该节点的 is_end 置真,不新建任何节点。

正确性说明:

  • 不变式:对任意已插入的单词 w,从根沿 w 的字符逐层下行必然存在一条路径,且该路径终点 is_end = True;反之,任何 is_end = True 的节点,其根到该节点的字符序列恰好是一个已插入单词。
  • insert 维护不变式:逐字符保证路径存在,末位置 is_end = True。
  • search 判定”路径存在 且 终点 is_end”,与”该串是已插入单词”等价。
  • startsWith 判定”路径存在”,与”存在已插入单词以该串为前缀”等价(因为路径存在意味着至少有一个 is_end 节点在其子树中——这个 is_end 正是在插入时被打上的)。
  • 重复插入同一单词是幂等的:路径已存在,只是把同一个 is_end 再置一次真。

时间复杂度:设操作串长度为 L。insert / search / startsWith 均为 O(L)。注意这里没有 N(单词总数)项——这正是相对哈希集合做前缀查询的优势。

空间复杂度:O(Σ 所有插入单词的字符数),即所有节点的子节点映射条目总数。最坏情况下(无公共前缀)为 O(总字符数);有大量公共前缀时显著小于该上界。

可运行的 Python 参考代码:

from typing import Optional
class TrieNode:
__slots__ = ("children", "is_end")
def __init__(self) -> None:
self.children: dict[str, "TrieNode"] = {}
self.is_end: bool = False
class Trie:
def __init__(self) -> None:
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for ch in word:
nxt = node.children.get(ch)
if nxt is None:
nxt = TrieNode()
node.children[ch] = nxt
node = nxt
node.is_end = True
def search(self, word: str) -> bool:
node = self._walk(word)
return node is not None and node.is_end
def startsWith(self, prefix: str) -> bool:
return self._walk(prefix) is not None
def _walk(self, s: str) -> Optional[TrieNode]:
"""沿字符下沉;任一字符缺失即返回 None。"""
node = self.root
for ch in s:
nxt = node.children.get(ch)
if nxt is None:
return None
node = nxt
return node

边界情况:

  1. search 与 startsWith 的分野:只插 “apple” 时 search("app") 必须为假——漏掉 is_end 判断是本题最常见的错误。
  2. 一个单词是另一个单词的前缀:先插 “app” 再插 “apple”(或反序),两者 search 都应为真。
  3. 重复插入:同一单词插多次不应改变任何查询结果(is_end 置真是幂等操作)。
  4. 空串语义:题目约束长度 ≥ 1,但若被调用,startsWith("") 应返回真(空串是任何字符串的前缀,且路径”根”存在),search("") 应返回假(根节点的 is_end 为假,除非显式插入过空串)。这两个行为要靠”根节点不设 is_end”自然导出。
  5. 长度上界 2000:纯链式结构,递归实现会爆栈(Python 默认递归深度 1000),必须迭代实现。
  6. 字符集合:若改为大写字母、数字或 Unicode,固定长度数组(26 槽)会失效或浪费;dict 实现天然可扩展,代价是哈希开销与更大的常数。

测试用例(以下代码已在本机执行,全部通过):

# 用例 1:官方示例
ops = ["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
args = [[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
# 期望 [null, null, True, False, True, None, True]
# 用例 2:前缀存在但单词不存在 / 单词互为前缀
t = Trie()
t.insert("app"); t.insert("apple")
assert t.search("apple") is True
assert t.search("app") is True
assert t.search("appl") is False # 路径存在但无 is_end
assert t.startsWith("appl") is True
assert t.startsWith("applex") is False # 走到 'x' 无子节点,立即 False
# 用例 3:单字符与空串语义
t = Trie()
t.insert("a")
assert t.search("a") is True
assert t.startsWith("a") is True
assert t.search("") is False # 根节点 is_end 为 False
assert t.startsWith("") is True # 空串是任何字符串的前缀
# 用例 4:重复插入幂等 + 长度上界 2000
t = Trie()
w = "z" * 2000
t.insert(w); t.insert(w)
assert t.search(w) is True
assert t.startsWith("z" * 1999) is True
assert t.search("z" * 1999) is False
# 用例 5:与参考实现(集合暴力解)随机对拍 300 组,全部一致

关键点#

  1. search 与 startsWith 沿同一条路径,唯一差别是终点 is_end——漏掉这个判断是本题第一错误。
  2. 复杂度与已插入单词总数无关,只与查询串长度相关;这是相对哈希集合的根本优势所在。
  3. 任一字符缺失即可早停返回,这是 Trie 的实际性能来源,也决定了它适合”前缀过滤”而非”精确存在性”。
  4. 长度 2000 必须用迭代实现(递归会爆栈)。
  5. 子节点用 dict 而不是 26 长数组,是为了在字符集变化时仍成立;代价是常数更大。

常见误区#

  1. search 与 startsWith 写成同一段逻辑。 二者路径相同但终止条件不同;把 search 写成只判路径存在,会让 search("app") 在只插入 “apple” 时错误返回真。
  2. 认为 Trie 在所有场景都优于哈希集合。 只做精确存在性判定时,哈希的 O(1) 与极小常数通常更好;Trie 的优势只在需要前缀语义(自动补全、前缀路由、字典序枚举)时才成立。
  3. 忽略空间代价。 每个节点一个映射对象,在长词、低公共前缀的场景下内存放大明显;生产里常改用压缩前缀树(Radix Tree / Patricia Trie)把单子节点链合并成一个节点。

延伸学习#

面试官可能的变体或追问:把 startsWith 换成支持通配符的搜索——. 可以匹配任意单个字符(对应 LeetCode 211「添加与搜索单词 - 数据结构设计」)。此时不能单纯沿路径下沉,需要在遇到 . 时对该层所有子节点做 DFS 回溯;这会引出”Trie + 回溯”的组合模式,以及一个关键工程问题:如果字符集很大且通配符出现在靠前位置,搜索会退化成遍历整棵树,需要按”通配符数量与位置”决定是否走另一条检索路径。

可迁移的解题能力:“把检索结构建在数据本身的结构上”——当查询天然带有层级 / 前缀语义时,保留这个结构(树)比把它压平成一个值(哈希)更合适;反之亦然。判断依据是”查询模式是否利用了数据的结构”。这个判断在工具命名空间设计、路由表匹配、以及”确定性字符匹配 vs 语义相似度匹配”的选型里反复出现。

与 AI Agent 工程的自然联系:工具名与命令通常用点分或下划线命名空间(db.read.query、fs.write.file),其层级展开与 Trie 的逐层下沉同构——这也是”渐进式工具发现”思路的底层结构:先给前缀层,再按需要展开具体工具,避免一次性把全部工具定义塞进上下文。更重要的取舍是:Trie 提供的是零误召的确定性前缀匹配,而基于 embedding 的工具检索提供的是容错但概率性的语义匹配;前者不会召回错的工具但要求命名规范,后者能容忍用户说法多样但可能召回错误的工具。生产中常见做法是先用确定性规则(前缀 / 白名单)收敛候选集,再在候选集内做语义排序——把概率性组件的决策空间压到最小。


延伸资料#

按优先级排序(官方文档 / 规范 > 原始论文 > 官方工程博客 > 可信技术组织 > 可验证的技术文章)。本轮所有链接均在运行当日实际访问并核对了引用内容。

主题资料类型核验状态
统计信息与扩展统计PostgreSQL 官方文档《14.2. Statistics Used by the Planner》
https://www.postgresql.org/docs/current/planner-stats.html
官方文档已核验
代价常数与临时方案定性PostgreSQL 官方文档《19.7. Query Planning》
https://www.postgresql.org/docs/current/runtime-config-query.html
官方文档已核验
autovacuum 触发公式与盲区PostgreSQL 官方文档《24.1. Routine Vacuuming》
https://www.postgresql.org/docs/current/routine-vacuuming.html
官方文档已核验
autovacuum 参数默认值PostgreSQL 官方文档《19.10. Vacuuming》
https://www.postgresql.org/docs/current/runtime-config-autovacuum.html
官方文档已核验
推理延迟四指标与批处理取舍MosaicML / Databricks 工程博客《LLM Inference Performance Engineering: Best Practices》(2023-10-12)
https://www.databricks.com/blog/llm-inference-performance-engineering-best-practices
官方工程博客已核验
prefill / decode 两阶段效率差异arXiv:2211.05102《Efficiently Scaling Transformer Inference》(2022-11-09 提交)
https://arxiv.org/abs/2211.05102
原始论文已核验(仅摘要)
MCP Tasks 状态机与协作式取消MCP 官方扩展《Tasks》
https://modelcontextprotocol.io/extensions/tasks/overview
官方文档已核验
扩展协商、默认关闭与优雅降级MCP 官方《Extensions Overview》
https://modelcontextprotocol.io/extensions/overview
官方文档已核验
分片上传流程与计费语义AWS S3 用户指南《Uploading and copying objects using multipart upload》
https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html
官方文档已核验
分片上传硬性限额AWS S3 用户指南《Amazon S3 multipart upload limits》
https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
官方文档已核验
Trie 题目LeetCode 208 中英文官方页
https://leetcode.com/problems/implement-trie-prefix-tree/ 、https://leetcode.cn/problems/implement-trie-prefix-tree/
官方题库已核验(难度标签未渲染,已在正文标注)

当日重点#

  1. 优化器是代价估算器,不是执行反馈器。 计划回归先查 EXPLAIN ANALYZE 的 estimated vs actual 行数比,再按 reltuples → 单列统计 → 多列相关性 → generic/custom 计划切换四层下钻。加索引与 enable_* 都是掩盖症状。
  2. 延迟拆两段才谈得上优化。 E2E ≈ TTFT + TPOT × 输出长度,两段瓶颈分别 compute-bound 与 memory-bound。批处理以单用户尾延迟换吞吐,净收益取决于 SLO 类型。
  3. Agent 的成本与延迟主乘数是”每任务调用次数”与”上下文复用率”。 只盯”总 token”没有行动含义,必须下钻到 task / tenant / step。
  4. 超时是三态,把它降成”失败”必然导致重复触发。 Tasks 的解法是解耦执行与等待:taskId 是唯一引用,任务须在响应前持久创建,取消是协作式的,cancelled 不等于”未发生”。
  5. “完成”的判定点必须在服务端。 分片全 200 不等于对象存在;未完成的上传持续计费;孤儿资源要有生命周期规则 + 应用层台账双重回收。
  6. 贯穿五题的一条主线:凡是”自动”完成的机制,都要问清判据是什么、代价由谁承担、上界在哪里。

本次运行信息#

配置读取状态:成功。题库配置.md 读取正常,版本 7(last_maintained: 2026-10-26),文件 mtime 为 2026-10-26 10:20,系周维护所致,本次未修改(本任务只读不写)。

实际生效配置版本:question_bank_version: 7。配额为 5 道(非 Agent 基础 1、Agent 基础 1、Agent 场景 1、非 Agent 场景 1、LeetCode 1),编号 B01 / B02 / S01 / S02 / L01。

启用状态核对:13 个一级基础题类型均为 active;暂停类型为空;「观察中的新技术」8 项均未标记「允许试学」,本次未从中出题。

指令文件核对:目标目录 better/ 及其父目录 Plan/ 均无 AGENTS.md / CLAUDE.md,无额外规则需要遵守。

本次参考的历史题目来源:

  1. 题目索引.md(读取时 75 条 / indexed_runs: 13,窗口 2026-07-28 至 2026-10-27)
  2. 精选/README.md(4 条精选)
  3. README.md(导航表)
  4. 运行前已确认当日训练文件在磁盘上不存在(非重跑)

未读取更早完整题目正文(索引信息充足,无冲突)。

去重结果:本次 5 题与最近 30 天(2026-09-28 至 2026-10-27)已有题目在标题、一级类型、二级主题、核心考点、预期结论、约束与故障现象、LeetCode 题号七个维度上逐一比对,无重复、无换皮题。

  • LeetCode 去重:已用题号 146、3、215、739、875、124、207、142、547、560、39、1109、435;本次选 208,算法模式为 Trie(首次使用),与既往栈、二分、哈希、图论、并查集、前缀和、差分、回溯、区间贪心均不同。(另:LeetCode 322 曾生成后随文件删除且未登记,仍按规避处理。)
  • B01 去重:数据库与存储上一次作为基础题是 2026-10-15(事务隔离与写偏斜),本次为统计信息与执行计划回归,二级主题、考点与结论均不同。注:2026-10-19 上午曾生成过”索引与执行计划回归”基础题,但该次训练文件已删除且未登记索引,故不构成本窗口内的重复。
  • B02 去重:LLM 应用工程上一次出现是 2026-10-23 S01(GPU 排队与容量规划,场景题);本次为基础题,聚焦延迟构成、测量口径与成本归因,层次与结论不同,已在正文中标注关联。
  • S01 去重:2026-10-12 S01(MCP 扩展协商与降级)的结论是”能力协商分层 + 降级矩阵 + 灰度回滚”,其中 Tasks 仅作为降级手段之一被提及;本次以 Tasks 为主命题,结论是”解耦执行与等待 + 句柄为唯一引用 + 协作式取消 + 幂等派发”。场景题类型「长耗时工具调用改为 MCP Tasks」为首次使用。
  • S02 去重:非 Agent 场景已用类型中无”分片上传 / 断点续传 / 孤儿资源回收”,为首次使用。

覆盖情况更新(本次新增后):数据库与存储 8、LLM 应用工程 4、工具与互操作协议 6、软件工程与平台工程 6。

  • 已填补缺口:数据库与存储「索引维护与统计信息、执行计划回归」;LLM 应用工程「批处理与延迟构成(TTFT / TPOT)、成本归因工程」。
  • 下一周期优先:RAG 与搜索(4,缺口为 Graph RAG 与多跳检索、RAG 评测设计)、上下文工程与能力封装(4,缺口为 Skills over MCP 与本地技能目录取舍)、编程语言与运行时(4,缺口为内存模型与资源管理、GC 与性能分析、并发原语的内存可见性)、操作系统与网络(7,缺口为 CPU 与内存性能分析)。

LeetCode 轮换建议(下次):链表操作类、区间 DP、单调队列、Trie 变体(211 含通配符);避免连续 Trie、贪心、差分 / 前缀和、回溯、并查集、图论、DP、栈、二分主导。

周期复盘:累计第 15 次。上一份周复盘为 2026-10/2026-10-19-AI-Agent-周复盘.md(当前磁盘上不存在,git 显示已删除)。本轮自动化提示词不含周复盘章节,本次未触发。

精选与复习:本次未执行任何精选动作(未收到明确意图)。到期精选提醒:Q-20261012-B02(建议复习日 2026-10-19)、Q-20261013-B03(2026-10-20)、Q-20261015-B03(2026-10-23)、Q-20261015-S01(2026-10-24)均已过建议复习日,且 精选/README.md 未记录复习完成。需要时可明确指定题号执行。

建议复习日与转换角度(本次题目):

题目 ID建议复习日转换角度
Q-20261028-B012026-11-04从”计划回归定位”转为”分区表与继承树的统计盲区 + 上线门禁设计”
Q-20261028-B022026-11-06从”延迟构成定义”转为”给定 TTFT P95 恶化 3 倍的分层归因与处置”
Q-20261028-S012026-11-08从”Tasks 改造”转为”任务存储的可用性设计与跨组织场景下的句柄信任边界”

本地保存状态:本次已写入 2026-10/2026-10-28-AI-Agent-面试训练.md,并已更新根目录 题目索引.md(75 → 80 条)与 README.md 导航表。写入本地 Obsidian 目录不等于远端已同步,远端同步状态未做独立验证,不作任何声称。

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