16313 字
82 分钟
2026-11-13 AI Agent 面试训练
AI Agent 面试训练
2026-11-13

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

今日学习主题:跨越边界的次数决定成本与正确性。数据从磁盘到网卡、工具描述从自然语言到 JSON 参数、模型输出到用户屏幕、请求从路由层到 64 个分片、元素从数组到哈希集合——每一次跨越边界都要付一次代价,而且往往在边界上新增一条正确性约束。设计不是消除跨界,而是选择在哪一层跨、跨几次、以及跨过去之后谁对一致性负责。

题目目录:

编号题目 ID类型一级类型二级主题题目名称难度一句话考察点
B01Q-20261113-B01非 Agent 基础操作系统与网络I/O 路径、拷贝次数与零拷贝机制零拷贝省掉的是哪几次拷贝:一次 read/write 的成本账基础题(中高)拷贝次数与上下文切换分开数,零拷贝引入的新约束
B02Q-20261113-B02Agent 基础工具与互操作协议工具发现、选择与参数校验工具描述是接口契约还是提示词:180 个工具后的选择失效基础题(中高)工具定义的双角色、选择退化为检索、校验必须在执行侧
S01Q-20261113-S01Agent 场景AI Agent 核心(兼属 LLM 应用工程)Agent 产品体验与失败反馈:流式输出与工具调用交错用户已经看到的那句话被推翻了:流式输出与工具调用的状态一致性场景题(高)草稿层与提交层分离、事件日志为唯一事实、中断与更正语义
S02Q-20261113-S02非 Agent 场景数据库与存储(兼属分布式系统)分库分表后的跨分片查询、聚合与分页64 个分片查一次报表要 4.8 秒:跨分片查询与部分结果语义场景题(高)异构索引、两阶段聚合、keyset 分页、缺片必须失败而非静默
L01Q-20261113-L01LeetCode哈希表(集合)+ 摊还分析连续序列的起点判定128. 最长连续序列medium用集合做 O(1) 成员判定,用”只在起点启动”把 O(n²) 摊还成 O(n)

配置读取状态:成功读取 题库配置.md,版本 v8(last_maintained: 2026-11-02)。无暂停类型;「观察中的新技术」8 项均未标记「允许试学」,本轮不出题。13 个基础题类型与场景题类型全部启用,5 道固定配额(非 Agent 基础 1、Agent 基础 1、Agent 场景 1、非 Agent 场景 1、LeetCode 1)不变。

去重结果:索引窗口为 2026-08-13 至 2026-11-13(运行日前推 3 个日历月,含起止日期),窗口内最早条目为 2026-10-10,本轮无需剪除任何索引行。对比 题目索引.md 135 条记录与最近 30 天正文:零拷贝 / I/O 拷贝路径、工具选择与描述工程、流式输出与工具调用交错、跨分片查询聚合、LeetCode 128 均无同题、换皮题或同题号重复。需说明的邻近题目与区分点:2026-11-11 B01 是 SSE 的协议面(传输格式、Last-Event-ID、代理缓冲),本题 S01 是应用层的状态一致性(已展示内容是否构成承诺),两者不重叠;2026-10-26 S02 是分片键倾斜与在线重分布,本题 S02 是分片之上的跨片读路径;2026-10-14 B04 是 Agent Skills 的三层渐进式披露,本题 B02 的对象是工具定义本身(name/description/inputSchema 的双角色)。


B01 · 零拷贝省掉的是哪几次拷贝:一次 read/write 的成本账#

题目 ID:Q-20261113-B01 | 分类:基础(非 Agent) | 一级类型:操作系统与网络 | 二级主题:I/O 路径、拷贝次数与零拷贝机制

题目#

你的固件分发服务用 read() + write() 循环把 2 GB 镜像文件写到 socket。高峰期 CPU 的 sys 占比 65%、吞吐卡在 1.2 GB/s,而磁盘 IOPS 与网卡带宽都还有余量。同事提议改成 sendfile() 做零拷贝。

请回答:一次 read() + write() 把文件内容送到 socket,数据一共发生几次拷贝、几次用户态/内核态切换?这些拷贝里哪些是 CPU 参与的、哪些是 DMA 做的?sendfile() 具体省掉了哪几次、代价是什么?“零拷贝”这个名字在哪些条件下会失效(例如走 HTTPS、需要在传输中改写内容、源是一个 socket)?splice() 与 mmap() 方案又各自换到了什么位置?

先思考#

  1. 文件内容从磁盘到网卡,物理上必须经过哪些内存区域?哪些搬运只能由 CPU 指令完成,哪些可以交给 DMA 引擎?
  2. 一次系统调用会切换几次特权级?read() 和 write() 各算几次?这和”拷贝次数”是同一个指标吗?
  3. 如果内核能把数据直接从 page cache 送到 socket 缓冲区,应用层就再也拿不到这段字节了。哪些常见需求正好依赖”应用层能拿到字节”?
  4. 你的瓶颈真的是拷贝吗?sys 高还可能来自哪些地方?先确认什么再改架构?

参考答案#

结论:read() + write() 把文件送到 socket 的经典路径是 4 次拷贝 + 4 次上下文切换:其中 2 次是 DMA(磁盘 → page cache、socket 缓冲区 → 网卡),2 次是 CPU 参与的拷贝(page cache → 用户缓冲区、用户缓冲区 → socket 缓冲区)。sendfile() 把 CPU 拷贝降为 0、切换降为 2 次,但它换来一条新的正确性约束——传输期间这段文件内容不能被修改;并且在需要 TLS 或需要改写内容时,“零拷贝”会退回用户态路径。

适用边界(什么时候这条路成立):收益出现在三个条件同时满足时——大文件、内容不需要在应用层被检查或改写、读取基本顺序且 page cache 命中率可接受。反过来,小文件(每次调用开销占比高)、随机读(page cache 命中差,缺页把省下的拷贝又还回去)、需要压缩/转码/加水印/内容审计的场景,零拷贝的收益会明显缩水甚至为负。

简洁示例:一个 2 GB 镜像,用 read/write 时 CPU 需要搬 2 GB(进用户态)+ 2 GB(出用户态),加上 4 次特权级切换;用 sendfile(out_fd, in_fd, &offset, count) 时,内核在自己的地址空间里把 page cache 的页挂到 socket 发送队列上,CPU 不再逐字节搬运,切换只有 2 次(进入 sendfile、返回)。手册对机制的表述很直接:sendfile() 的复制”在内核内完成,因此比 read(2) 与 write(2) 的组合更高效,后者需要把数据传入和传出用户空间”。

与相近概念的区别:

  • sendfile():要求 in_fd 支持 mmap 类操作(不能是 socket);Linux 2.6.33 之前 out_fd 必须是 socket,之后可以是任意文件。它适合”文件 → socket / 文件 → 文件”,不能 socket → socket。
  • splice():要求两端至少一端是 pipe,因此可以 socket → pipe → socket,是代理/转发场景的通用零拷贝手段;sendfile() 在 out_fd 是 pipe 时(Linux 5.12 起)本身也会退化为 splice() 及其限制。
  • mmap() + write():把 page cache 直接映射进用户空间,省掉”page cache → 用户缓冲区”这一次 CPU 拷贝,但仍要一次 CPU 拷贝进 socket 缓冲区,且有小文件缺页开销、映射期间文件被截断会触发 SIGBUS 的问题。

典型工程应用(通用工程):静态文件服务与 CDN 回源、日志与备份传输、消息中间件投递(把日志段文件直接推给消费者)、反向代理转发。选型时的判据不是”哪个更快”,而是”这条路上一共跨了几次用户态边界、每次跨界是否必要”。

“零拷贝”的两个真实陷阱:

  1. 它没有消除数据移动。DMA 仍然在搬,只是 CPU 不再参与逐字节拷贝。网卡支持 scatter-gather DMA 时,“socket 缓冲区 → 网卡”这一步也可以省掉,此时是 2 次 DMA、0 次 CPU 拷贝——但”零”指的是 CPU 拷贝为零,不是”数据不动”。
  2. 它引入新的正确性约束。手册明确要求:如果 out_fd 支持零拷贝,调用方必须保证这段文件内容在对端读取完之前不被修改。这意味着原地改写、truncate、复用文件做滚动日志都会破坏已排队的引用——用 sendfile 发一个正在被追加写的日志文件,尾部可能发出去的是被覆盖后的内容。这是”省掉拷贝”换来的耦合,必须显式管理(不可变文件、写时复制、或发送前先快照)。

面试现场回答:传统 read/write 是 4 次拷贝(2 次 DMA + 2 次 CPU)加 4 次上下文切换;sendfile 把 CPU 拷贝降到 0、切换降到 2 次,前提是源必须是可 mmap 的文件、不需要在应用层看或改这些字节。它不适用于 TLS(加密要在用户态做,除非用内核 TLS)、需要改写内容、或源是 socket 的场景;后者要用 splice 经 pipe 转。另外上线前要先确认瓶颈到底是不是拷贝——sys 高也可能是协议栈开销、锁竞争或小包造成的,这时候换零拷贝不会有明显收益。

追问#

追问:这个服务前面挂了 HTTPS,sendfile 还能用吗?如果不能,你会怎么改?

回答要点:传统”用户态 TLS 库”路径下不能——加密必须发生在用户态缓冲区,数据被迫回到用户空间,零拷贝的前提(数据始终留在内核)被打破。三条可选路径:一是 TLS 在入口层(LB / 网关)终止,内网段走明文 sendfile,把加密成本移出业务进程;二是采用内核 TLS(kTLS),把对称加密下沉到内核 socket 层,使 sendfile 仍可在核内完成(需确认内核与 OpenSSL 版本支持,属于需要实测的能力,不能假设);三是接受 read/write + 用户态加密,此时优化方向改为增大缓冲区、合并系统调用(readv/writev)、复用连接而不是追求零拷贝。判断依据是”加密在哪一层做”,而不是”有没有 HTTPS”。

关键点#

  1. 拷贝次数与上下文切换是两个独立指标,要分开数:read/write 是 4 拷贝 / 4 切换,sendfile 是 2 DMA(+可选 1 CPU)/ 2 切换。
  2. 零拷贝消除的是 CPU 拷贝,DMA 搬运依然存在;“零”指的是用户态不参与。
  3. 零拷贝带来新的不变量:传输中的文件区间在消费完成前不可变。滚动日志、原地改写、truncate 都会破坏它。
  4. 适用条件是大文件 + 无需改写 + 顺序读;小文件、随机读、需压缩或审计时收益退化。
  5. 优化前先判定瓶颈归属:sys 高可能来自拷贝,也可能来自协议栈、小包、锁竞争;用 strace -c、perf top(看 copy_user_* 类符号占比)、pidstat 区分后再决定是否改造。

常见误区#

  1. 把零拷贝理解为”数据不移动”:物理搬运仍由 DMA 完成,省掉的是 CPU 逐字节拷贝与两次特权级切换;用这个误解做容量估算会得出错误结论。
  2. 认为 sendfile 是通用文件传输优化:它要求源可 mmap(不能是 socket),且不适用于需要 TLS 或内容改写的链路;socket → socket 转发必须用 splice 经 pipe。
  3. 只改 API 不看瓶颈:如果 sys 高其实是小包导致的协议栈开销或锁竞争,换 sendfile 收益接近零,还可能因为绕过了应用层缓冲而失去批量发送的机会(需要配合 TCP_CORK 才能减少小包)。

延伸学习#

  1. Linux man-pages sendfile(2)(内核内复制、in_fd 必须支持 mmap、out_fd 的历史限制、零拷贝下”传输区间不可修改”的要求、EINVAL/ENOSYS 时回退 read/write 的建议):https://man7.org/linux/man-pages/man2/sendfile.2.html (已核验,2026-11-13)
  2. Linux man-pages splice(2)(以 pipe 为中介的通用零拷贝,socket ↔ pipe ↔ socket 的实现路径):https://man7.org/linux/man-pages/man2/splice.2.html (方向性推荐,本轮未逐字核验)

B02 · 工具描述是接口契约还是提示词:180 个工具后的选择失效#

题目 ID:Q-20261113-B02 | 分类:基础(Agent) | 一级类型:工具与互操作协议 | 二级主题:工具发现、选择与参数校验

题目#

一个企业内部的 Agent 平台,工具数量从 42 个增长到 180 个(含 MCP 服务接入的工具)。最近一个季度:工具选择错误率从 3% 升到 14%,其中约六成是”选了功能相近的另一个工具”(search_docs / docs_search / kb_lookup 三个并存);参数错误集中在日期格式与时区、以及 ID 传成了名称。团队想把所有工具描述写得更详细来解决。

请回答:工具定义(name / description / inputSchema)在系统中实际承担了哪两个角色?这两个角色的保证强度有什么本质差别?“把描述写详细”能修好哪些失败、修不好哪些?参数校验应该放在哪一侧、为什么?工具集规模增长到几百个之后,应该做哪些结构性改造?

先思考#

  1. 同一份声明,模型读它是”自然语言线索”,程序读它是”校验依据”。这两次使用分别发生在什么时候、失败后果有什么不同?
  2. 三个功能相近的工具同时出现在清单里,模型凭什么区分?如果描述里没有判别特征,它会靠什么猜?
  3. 如果一个参数在 schema 里是合法的 string(格式正确但语义错误,比如 2026-13-01 或把时区写成 Asia/Shangai),谁的职责拦住它?
  4. 工具从 40 个到 180 个,改变的是”模型更笨了”还是”任务性质变了”?如果是后者,应该在哪一层做优化?

参考答案#

定义:工具定义是一份双角色声明。对模型,它是按工具切片的微型提示——description 提供”这个工具做什么、什么时候该用、什么时候不该用”的判别特征,参与”选哪个工具”的条件分布;对系统,它是接口契约——name 是调度键,inputSchema 是校验与执行的依据。协议层也是这么定位的:MCP 规范把每个工具描述为”由名称唯一标识,并带有描述其 schema 的元数据”,同时明确工具是 model-controlled 的(模型基于上下文自行发现与调用),而服务端 MUST 校验所有工具输入。

两个角色的保证强度不同,这是整道题的核心:模型侧是概率的——描述写得再清楚,也只是提高了”被正确选中”的概率,采样、上下文长度、工具在清单中的位置都会影响结果;执行侧是确定的——inputSchema 校验不通过就必须拒绝执行,没有任何”这次算了”的空间。把这两件事混为一谈,就会得出”描述写严格一点就不会误调用”这种错误结论。

适用边界:描述工程能修的是”选择歧义”——命名相近、缺少使用时机说明、缺少反例(“不要用它查工单”)、参数含义不明。它修不了四类问题:能力不存在(该加工具或该拒绝)、权限不足(该在执行侧判定)、参数语义无法从文本推断(该用 enum 收敛取值或提供示例)、以及工具数量超过选择分辨率(该做裁剪与分层,而不是继续加形容词)。

简洁示例:把 kb_lookup 重命名为 search_internal_kb,描述从”查询知识库”改为”检索内部知识库(含历史工单与运维手册),用于回答内部流程与故障处置问题;不要用它查产品对外文档”,并给 search_docs 补上”仅检索当前租户的对外产品文档”。这三处改动提供的是判别特征,不是更长的说明书——选择错误率下降的原因是可以被解释的(相似工具的负例被显式写出),而不是”描述变长了”。

与相近概念的区别:

  • 与 Prompt Engineering:提示词是全局指令,作用于整段对话;工具描述是按工具分片的提示,只在工具进入清单(或被检索出来)时消耗 token。工具越多,每条描述分到的注意力越少,这是规模增长带来的结构性变化。
  • 与 Function Calling / Tool Calling 协议:协议保证”调用被表达成结构化 JSON”,不保证选对了工具、也不保证参数语义正确。协议是搬运层,不是判定层。
  • 与 structured outputs 的约束解码:约束解码保证输出符合 schema 形状,与”这个值在业务上是否合理”无关(status="pending" 合法但该订单早已关闭)。
  • 与 MCP 的关系:tools/list 负责发现(支持分页与缓存,且服务端可通过 subscriptions/listen 发 notifications/tools/list_changed),描述与 inputSchema 仍是同样的双角色;但服务端可以随时改描述,所以客户端侧必须把”工具清单”当作需要版本对账的外部输入,而不是编译期常量。

在 Agent 系统中的典型应用:工具规模上来后,“选工具”在工程上退化为一个检索/分类问题——先按意图或权限把候选集从 180 缩到 8~12 个,再让模型在候选集内选。常见做法是:按命名空间分组(crm.* / billing.* / ops.*)、合并同义工具、给参数加 enum 与格式约束、在描述里写明”何时用 / 何时不用”、执行前用 inputSchema + 业务规则双重校验、对结果做 schema 校验后再回灌给模型(规范建议客户端在传给 LLM 前校验工具结果)。描述变更应与接口变更同级对待:进版本、跑选择准确率评测、灰度。

面试现场回答:工具定义同时是给模型看的提示和给系统用的契约,但前者是概率的、后者必须是确定的。描述写详细只能修选择歧义,修不了能力缺失、权限、语义歧义和”工具太多”;参数校验必须在执行侧,因为模型输出的合法性是概率事件,而 schema 与业务规则是唯一能 fail-closed 的地方。规模到几百个时,正确动作是结构性改造:命名空间分组、同义工具合并、按意图/权限预筛候选集、参数用枚举收敛、清单版本对账,而不是继续给每条描述加字数。

追问#

追问:180 个工具里,你怎么判断哪些该合并、哪些该下线、哪些该藏起来按需加载?

回答要点:看数据不看直觉。先拉调用日志做混淆矩阵——被高频互相误选的工具对(A 被误选成 B)优先处理:若两者语义重叠则合并;若语义不同则补判别特征(改名 + 负例描述)。再看调用分布:长期零调用、或已被上游确定性流程替代的工具直接下线,减少候选集基数。最后看使用场景的聚集性:只在少数流程中出现的工具按命名空间分组、按会话意图加载(渐进披露),把”清单长度”从全局常量变成会话变量。三步的顺序很重要:先合并(减少歧义源)、再下线(减少基数)、最后做按需加载(控制上下文成本)。

关键点#

  1. 工具定义是双角色声明:模型侧是提示(概率),系统侧是契约(确定)。混用两者的保证强度是最常见的判断错误。
  2. 选择失败的主因是”缺少判别特征”和”同义工具并存”,不是描述不够长;更长的描述会挤占注意力并抬高上下文成本。
  3. 参数校验必须在执行侧:inputSchema 挡形状,业务规则挡语义,两者都不能由模型”承诺”。
  4. 工具规模增长使选择退化为检索问题:候选集裁剪、命名空间、渐进披露是结构性解法。
  5. 工具清单是外部可变输入(MCP 服务端可随时改),必须版本对账;描述变更等同接口变更,要评测与灰度。

常见误区#

  1. 把描述写成产品文档:单条描述几百字,工具一多就挤爆上下文,且关键判别信息被稀释;有效做法是”一句话做什么 + 何时用 + 何时不用 + 一个反例”。
  2. 认为 schema 严格就等于语义正确:{"date": "2026-02-30"} 形状合法但语义非法;{"user_id": "张三"} 类型合法但指向错误对象。这类错误只能由执行侧的业务校验拦住。
  3. 用”工具越多能力越强”做扩张理由:能力上限提高的同时,选择准确率与上下文成本同时恶化;工具数是一个需要预算约束的资源,不是越多越好。

延伸学习#

  1. MCP 规范 Tools 页(工具由 name 唯一标识并带 schema 元数据、工具是 model-controlled、服务端 MUST 校验输入、客户端 SHOULD 在传给 LLM 前校验结果、tools/list 的分页与 list_changed 通知):https://modelcontextprotocol.io/specification/2026-07-28/server/tools (已核验,2026-11-13)
  2. 同一页的 Tool 数据类型与 outputSchema 一节(结构化结果与 structuredContent 的校验路径),可作为”契约在输出侧同样成立”的对照阅读:同上链接(已核验)

S01 · 用户已经看到的那句话被推翻了:流式输出与工具调用的状态一致性#

题目 ID:Q-20261113-S01 | 分类:场景(Agent) | 一级类型:AI Agent 核心(兼属 LLM 应用工程) | 二级主题:Agent 产品体验与失败反馈 · 流式输出与工具调用交错

题目#

业务背景:一个面向企业运营人员的分析助手。用户提问”分析上周退款率上升的原因,并给出处置建议”,Agent 会先输出一段分析文字,期间调用 3~4 个数据工具(订单、退款、物流),最后给出结论与建议。前端通过 SSE 逐字渲染。

系统规模:12,000 DAU,日均 40 万条消息,平均每条消息 3.2 次工具调用,首字 P95 为 900 ms,单条消息输出 token 中位数 1,400。

当前数据流:模型流式输出 → 网关透传 SSE → 前端逐字渲染并拼接本地字符串 → 消息结束后整段回写数据库。工具调用时,前端把流式到达的 tool_call 参数(部分 JSON)直接渲染成一张”正在查询”卡片。

目标:在不牺牲首字体验的前提下,消除”已展示内容被推翻”和”多端不一致”,并让消息可审计。

限制条件:首字 P95 必须保持在 1 s 以内;消息内容需要可回溯(合规要求:能还原”用户当时看到了什么”);计费按输出 token;不允许为了正确性改成”整段生成完再返回”。

已知现象(上线三个月):

  1. 用户先看到”退款率上升主要来自华东仓”,随后工具返回的数据不支持该结论,模型改口”主要来自华南”。用户截图投诉”AI 前后矛盾”。
  2. 前端偶发渲染出半截 JSON 卡片,字段名可见、值为空。
  3. 用户点”停止”后消息卡在”生成中”;重新进入会话看到两条内容相近的回答。
  4. 同一会话在两个浏览器打开,一端显示已完成的结论,另一端仍显示”思考中”。

需要取舍的问题:流式体验与”已展示即承诺”之间怎么划线?工具调用期间是否应该暂停展示?失败后是”重写”还是”追加更正”?

先思考#

  1. 模型的 token 是按顺序生成的,它在输出第一句话时并不知道后面的工具会返回什么。这个物理事实决定了”已展示内容”的性质是什么?
  2. 如果把”用户在屏幕上看到的字符串”当作事实来源,会同时引出哪两类问题(一致性、审计)?
  3. 消息从”生成中”到”已完成”,中间应该经过几个状态?“停止”和”失败”是同一个状态吗?
  4. 哪些内容一旦说出口就必须保留(可更正不可删除)?哪些内容在工具结果回来之前本来就不该当结论?

参考答案#

结论:把消息拆成草稿层(provisional)与提交层(committed),以服务端事件日志为唯一事实、前端只做投影。工具结果尚未返回时产出的内容一律进草稿层,不作为结论展示;提交层的内容只允许追加更正,不允许删除或覆盖。这样既不牺牲首字,也不产生”两套事实”。

问题拆解(四层,失败要按层归因):

层失效机制对应现象
模型层token 顺序生成,先说的话无法预知后到的工具结果;无法自我撤回现象 1 的根源
编排层没有区分”计划性表述”与”结论性表述”,工具调用边界上没有暂停点现象 1、2
传输层SSE 事件没有序号、断线无游标可续、tool_call 增量被当作完整对象渲染现象 2、4
存储/UI 层客户端本地拼接字符串当事实、消息状态机缺 interrupted 态、回写覆盖而非幂等 upsert现象 3、4

核心方案:

  1. 事件日志作为唯一事实。每条消息一个 message_id,事件带单调 seq:block_start(kind=plan|answer)、delta、tool_call_start、tool_call_args_done、tool_result、block_end、message_end、interrupted、correction。写入语义是”先落库(或同事务写 outbox)再推送”,推送失败不影响事实。
  2. 草稿层 / 提交层分离。answer 块在依赖的工具结果全部返回前标记 provisional,UI 用”正在核对数据”样式呈现且不计入消息正文;block_end 且依赖满足后转 committed。合规要求的”用户当时看到了什么”由事件日志 + provisional 标记共同回答,而不是靠前端截图。
  3. 先计划后结论(编排层硬约束)。工具调用之前只允许输出计划性文字(“我将核对退款明细与物流时效”),不允许出现数字结论;这条通过工具调用顺序策略 + 提示词双重约束,但最终兜底靠呈现层——即使模型违规,provisional 也不会被当成结论展示。
  4. 工具调用参数不流式渲染。tool_call_start 时前端只显示”正在查询退款明细”,tool_call_args_done 后再一次性成型展示;这直接消除现象 2。
  5. 停止 = 协作式取消,不等于删除。客户端发 cancel,服务端把消息置 interrupted 并落事件,保留已生成的 token 与已发生费用;消息状态机必须有超时巡检,禁止永久停留在”生成中”。interrupted、failed、cancelled 是三个不同状态,语义分别是”用户主动停”、“执行出错”、“系统侧撤销”。
  6. 幂等写入 + 游标续传。客户端重试/重连按 (message_id, seq) upsert,杜绝现象 3 的重复消息;断线用最后收到的 seq(或事件 ID)续传,超出保留窗口则拉取消息快照而非重放全量。
  7. 多端一致靠投影。客户端不再本地拼接,而是”读事件日志 → 渲染投影”;committed 版本对同一 message_id 唯一,provisional 允许多端各自刷新,因此两端差异只可能出现在草稿层,不会出现”一端有结论一端没有”。

执行流程(一次含工具调用的回答):

  1. 收到用户输入,创建 message_id,写入 message_start。
  2. 模型流式输出计划块 → 服务端逐条追加 delta,标记 provisional,同时推送。
  3. 检测到工具调用意图 → tool_call_start,计划块 block_end。
  4. 工具参数完整后 tool_call_args_done,执行工具(带超时、错误结构化、幂等键)。
  5. tool_result 回灌模型,进入答案块;答案块在最后一个依赖工具返回前保持 provisional。
  6. 答案块结束 → block_end → 提交为 committed → message_end → 异步触发计费与审计归档。

技术选型与依据:

  • 传输仍用 SSE(单向、天然适配”服务端推 token”),但事件必须自描述且带序号;不建议为此引入 WebSocket——双向需求(取消、追问)可以用独立 HTTP 接口解决,避免长连接状态管理成本(这是与 2026-11-11 B01 协议面结论一致的复用)。
  • 事实存储用追加式事件表(message_id, seq, type, payload, created_at)+ 唯一约束 (message_id, seq);不选”直接存最终文本”,因为它无法回答”用户当时看到了什么”,也无法支持更正链。
  • 投影可以缓存(Redis)但缓存键必须含 message_id + 已提交版本号,避免脏读。
  • provisional 的判定放在编排层(确定性代码),不交给模型自评”这句话是不是结论”。

方案取舍:

  • 流式 vs 一致:完全一致的做法是”结论不流式、只流计划”,体验最好但不牺牲正确性;折中是”计划流式 + 答案块整体提交”,代价是最长 1~2 s 的答案延迟。如果业务要求”绝不出现被推翻的结论”,唯一可行解就是把结论从流式路径上摘出去。
  • 重写 vs 更正:重写对用户友好但对审计不友好(历史被覆盖);更正保留链路但视觉上”打补丁”。合规场景选更正,消费级场景可以选”重写 + 保留内部审计日志”。
  • 工具调用期间暂停展示:暂停会拉长体感时间(用户在等一个空白屏),不暂停则要求 provisional 样式足够明确。建议暂停”结论块”、继续流”计划块”,两者兼得。

失败路径与边界条件:

  • 工具超时或报错:该块标 failed,把结构化错误(error_class、retryable)回灌模型,由模型决定”降级回答”或”说明无法核实”;不得让模型把超时当作”没有异常”。
  • 模型在 provisional 里已经说了结论、后续数据不支持:走 correction 事件,追加”更正:上周退款率上升主要来自华南仓(华东仓为次要因素)“,原文保留;用户可见更正标记。
  • 断线:按 seq 续传;若服务端已 committed 而客户端仍显示 provisional,客户端以快照为准(提交层优先级高于草稿层)。
  • 用户在工具执行中取消:工具调用不强制中断(可能已产生副作用),但结果不再回灌;消息置 interrupted,费用照计。
  • 消息超长被截断:block_end(reason=length) 显式标注,禁止静默截断(否则用户以为结论完整)。

超时、重试、幂等、恢复:

  • 单次工具调用超时 < 整条消息预算;重试只对 retryable 类错误且总预算有限,重试不重发已成功写的副作用。
  • 事件写入用 (message_id, seq) 唯一约束实现幂等;重复推送由客户端按 seq 去重。
  • 服务于生成中途崩溃:从最后一条事件恢复——已 committed 的块不再重算,provisional 块标记 aborted 并在用户重试时重建,避免”半句话被当成完整回答”。

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

  • 安全:事件日志含用户数据,需要按租户分区与保留期策略;工具结果回灌前做 schema 校验与敏感字段脱敏(规范建议客户端在传给 LLM 前校验结果)。
  • 成本:provisional → committed 的改写会重复生成 token;把”计划块”限制在固定长度(如 ≤120 token)可以把浪费控制在上界。
  • 延迟:首字来自计划块(短),答案块整体提交让 P95 略升,需在 SLA 中分开声明”首字延迟”与”结论延迟”。
  • 可靠性:订阅推送与事件写入解耦(outbox),推送失败可重放;前端渲染必须能处理乱序(按 seq 排序缓冲)。

验证指标:首字 P95、结论块延迟 P95、provisional → committed 改写率、用户标记”前后矛盾”的比例、中断后重进会话状态一致率(抽检 200 条)、重复消息率、单消息工具调用数分布、取消后仍计费 token 占比、事件缺口率(seq 连续性)。

三层能力归属:

  • 模型:token 生成顺序、是否遵循”先计划后结论”、是否并行发起工具调用——概率能力,只能提高概率。
  • Agent 编排:块划分、暂停点、工具边界的错误回灌、取消语义、provisional 判定——这是本题的主战场,属于确定性代码。
  • 传统软件:事件日志、唯一约束、状态机、投影渲染、outbox、审计保留——提供”事实只有一个”的保证。三者缺一:只靠模型会出现前后矛盾,只靠编排会出现多端不一致,只靠存储则无法定义”什么是结论”。

追问#

追问 1:为什么不干脆等答案完整生成后再一次性返回?

回答要点:会牺牲首字体感,在”用户在等一个空白屏”的场景里退订率明显上升;但这不是非黑即白——可以把流式的对象从”结论”换成”计划与进度”(“正在核对退款明细 → 已核对 3/4”),既保留感知延迟优势,又让结论走”整体提交”路径。真正要避免的是”用流式输出赌模型不会改口”,那等于把正确性押在采样上。

追问 2:工具失败后,已经流出去的内容算不算对用户的承诺?

回答要点:取决于它落在哪一层。provisional 内容不算承诺(UI 明确标”核对中”),committed 内容算承诺且只能追加更正、不能删除;这是把”承诺”从时间维度(说出口就算)改为状态维度(提交才算)的关键设计。审计上,“用户当时看到了什么”由事件日志回答,包括 provisional 标记与被更正前的原文,这样既不欺骗用户也不丢失历史。

追问 3:两个客户端同时在线,其中一个点了”停止”,另一个应该看到什么?

回答要点:以服务端状态为准。cancel 是服务端状态变更(interrupted + 事件落库),另一个客户端通过续传/推送收到 interrupted 事件后把本地 provisional 置灰并停止渲染。写路径必须是单写者:按 message_id 分区串行处理命令,避免两个客户端的 cancel 与 retry 交叉导致消息状态跳变。客户端永远不产生事实,只投影事实——这条约束一旦破,多端一致性就无法靠工程手段保证。

关键点#

  1. 模型的顺序生成决定了”已展示内容”天然是概率性的中间态,把它当承诺是设计错误,不是模型缺陷。
  2. 草稿层 / 提交层的划分位置决定了一切:它必须由编排层的确定性代码判定,不能交给模型自评。
  3. 事实来源只能是服务端事件日志(带 seq),客户端本地拼接字符串是最常见的一致性污染源。
  4. interrupted / failed / cancelled 是三个状态;“停止”不等于”什么都没发生”(副作用与费用已产生)。
  5. 结论块整体提交、计划块流式输出,是在不牺牲首字体验下消除”前后矛盾”的最小改动。

常见误区#

  1. 把问题当成纯前端渲染 bug:半截 JSON、重复消息只是表征;根因是缺少事件序号与状态机,前端只是在替后端背锅。
  2. 用”重新生成”覆盖已展示内容:用户体验上像”AI 失忆”,合规上直接销毁了”用户当时看到了什么”的证据;正确做法是追加更正并保留链路。
  3. 让模型自己判断”这句话是不是结论”:这是把确定性判定交给概率组件,模型在压力下会把任何句子都认成结论;判定必须在编排层按块类型与依赖关系计算。

小型实践#

用 FastAPI + SSE 写一个 30~60 分钟的最小版本:

  1. 建表 events(message_id, seq, type, payload),加唯一约束 (message_id, seq);实现 append_event 幂等写入。
  2. 两个并发客户端对同一 message_id 发 cancel 与 retry,观察是否需要按 message_id 加单写者(用 SELECT ... FOR UPDATE 或行级串行队列验证)。
  3. 模拟工具超时:第一个工具返回 upstream_timeout,验证 provisional 块不被提交、failed 事件被正确处理。
  4. 前端(或 curl)断线后按 seq 续传,验证不产生重复消息、且 committed 版本唯一。
  5. 人为构造”先给结论后被打脸”的脚本,验证 correction 事件生成且原文保留。

延伸学习#

  1. MDN Server-sent events(text/event-stream、event / data / id / retry 字段与断线重连语义,事件 id 作为续传锚点):https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events (该页已于 2026-11-11 核验;本轮未重新打开)
  2. MCP 规范 Tools 页安全建议(“客户端 SHOULD 在传给 LLM 前校验工具结果”、“服务端 MUST 校验所有输入”),作为”事件回灌也要校验”的依据:https://modelcontextprotocol.io/specification/2026-07-28/server/tools (已核验,2026-11-13)

S02 · 64 个分片查一次报表要 4.8 秒:跨分片查询与部分结果语义#

题目 ID:Q-20261113-S02 | 分类:场景(非 Agent) | 一级类型:数据库与存储(兼属分布式系统) | 二级主题:分库分表后的跨分片查询、聚合与分页

题目#

业务背景:电商订单库按 user_id 哈希分成 64 个物理分片,共 8.2 亿行。运营后台需要三种查询:(a)按时间范围 + 订单状态筛选明细并分页浏览;(b)按商家维度聚合 GMV 与订单数;(c)展示”共 N 条”用于翻页。

系统规模:峰值写入 3,500 TPS,读侧后台查询 QPS 约 60(但每条查询要扇出到全部分片),单分片约 1,300 万行,订单表宽 42 列。

当前架构:后台服务直接向 64 个分片并发下发同一条 SQL,在应用内归并结果、用 OFFSET 实现分页。没有任何读模型。

目标:把跨分片查询的 P95 从 4.8 s 降到 2 s 以内(第 200 页这类深分页目前是 12 s),并保证聚合口径与财务对账一致。

限制条件:不能改写入路径的分片键(user_id 是点查与事务的主键);不允许为了报表再加一整套在线事务库;聚合结果可以延迟,但不能错。

已知现象:

  1. 深分页越翻越慢,第 200 页耗时 12 s,且翻页过程中偶发”同一行出现两次”。
  2. 某次一个分片超时,应用把其余 63 片的聚合结果直接返回,报表显示 GMV 比财务低 1.6%,无人发现。
  3. 分片间数据分布不均(大商家集中在少数 user_id 段),最慢分片决定了整体延迟。
  4. 同一时间范围重复查询两次,总数偶发差几十条(期间有新订单写入)。

需要取舍的问题:实时性与成本、精确总数与延迟、读模型冗余与一致性,三者怎么选?

先思考#

  1. 按 user_id 分片之后,“按时间 + 状态查”这个访问路径在物理上发生了什么?为什么再快的单机索引也救不了它?
  2. 聚合是求和运算,缺一个分片意味着什么?为什么它和”明细少一页”的严重性不同?
  3. OFFSET 10000 在每个分片上分别意味着什么?为什么它在大偏移下必然变慢、且可能重复?
  4. 读模型是从哪里来的?它的滞后应该暴露给谁、以什么形式暴露?

参考答案#

结论:跨分片查询不应该继续在”分片之上”做扇出,而应该在分片旁边建一个按查询维度组织的读模型(异构索引/宽表),把”跨片归并”从查询时搬到写入时;聚合用预聚合 + 增量补偿;分页改成 keyset;缺片时必须失败而不是返回部分和。

问题拆解:

  • 现象 1(深分页 + 重复):OFFSET 在 64 个分片上各扫 offset+limit 行再归并,代价随偏移线性增长;同时因为各分片在读的时刻不同(无统一快照),归并时同一行可能在不同批次被取到两次。
  • 现象 2(静默缺片):聚合是求和,缺一片得到的是”系统性偏低”的看似合法的数字,比报错危险得多——这是本题最重要的一条工程判断。
  • 现象 3(长尾分片):LIMIT 语义要求”全局前 N”,必须等最慢分片返回,因此整体延迟由 max 决定而非 avg。
  • 现象 4(总数漂移):总数来自多次独立读,期间有新写入;在无快照隔离的跨片查询里,“总数”天然只是一个时间点估计。

核心方案(三条独立但互补的改造):

  1. 读模型(异构索引):用 CDC 订阅 binlog,构建按 (merchant_id, created_at) 与 (status, created_at) 组织的宽表(只含报表需要的 8~10 列)。明细查询走读模型,原分片只承担 user_id 点查与事务。
  2. 聚合分层:日粒度预聚合表(merchant_id, stat_date, gmv, order_cnt)+ 当日增量(从 CDC 流实时累加)。查询”上周”= 7 天预聚合 + 当日增量,避免每次全量扫明细。
  3. 分页改 keyset:以 (created_at, order_id) 为游标,WHERE (created_at, order_id) > (:t, :id) ORDER BY created_at, order_id LIMIT n;游标对客户端不透明(base64),保证不跳行、不重复。
  4. 总数分级:普通浏览用”约 N 条”(预聚合或近似计数);财务/导出场景走精确计数接口(异步任务 + 结果快照 + as_of 时间戳),并明确”截至 T 时刻”。

执行流程(明细查询):

  1. 校验时间范围与状态(限制最大跨度,如 31 天,防止全表扫)。
  2. 路由到读模型(按 merchant_id 或全局二级索引),命中 (status, created_at) 复合索引。
  3. keyset 分页取 limit+1 行判断 has_more,返回游标而非页码。
  4. 返回前附加 data_as_of(读模型已追到的水位),前端显式展示”数据截至 xx”。
  5. 若读模型 lag 超过阈值(如 60 s),降级为”提示稍后重试”或回退到分片扇出(但扇出必须限制分片数与总预算)。

执行流程(聚合查询):

  1. 解析时间范围 → 拆成”预聚合区间 + 增量区间”。
  2. 预聚合区间直接查汇总表;增量区间走 CDC 累积表或小范围明细聚合。
  3. 对多个数据源的结果做完整性校验:期望的分片/分区数 vs 实际返回数,不一致则整体失败。
  4. 写结果缓存,缓存键 = (维度, 时间范围, 水位),水位变化即失效,不依赖 TTL。

技术选型与依据:

  • 读模型存储:优先”同库不同表”或独立只读库(复用 SQL、口径一致、运维简单);数据量与检索复杂度上来后再考虑 ES(倒排适合多条件筛选,但写放大与一致性维护成本高,且不能作为对账权威源)。
  • CDC:binlog 订阅(Debezium 类)优于双写——双写会引入”写成功但消息丢”的不一致,而 CDC 的位点可重放。
  • 是否引入 OLAP 列存:如果聚合维度稳定、数据量到十亿级,列存 + 预聚合是长期解;但本题的限制条件(不加一整套在线库)意味着先做”读模型 + 预聚合”的轻量版。

方案取舍:

  • 实时性 vs 成本:CDC 读模型的延迟通常在秒级,财务口径要求”错不过夜”即可满足;若要求”毫秒级一致”,只能回到扇出,延迟与成本同时上升——这是必须让业务方明确选择的点。
  • 精确总数 vs 延迟:精确计数在 8 亿行上必然是一次重查询;产品层接受”约”是成本最低的解法,财务侧用异步精确任务。
  • 读模型冗余 vs 一致性:冗余带来存储与维护成本,但把”跨片归并”从每次查询摊销到每次写入,是唯一能在 P95 上取得数量级改善的路径。

失败路径与边界条件:

  • 分片/读模型超时:聚合必须整体失败(返回错误码 + 缺失范围),不得返回部分和;明细可返回部分结果但必须带 partial=true 与缺失分片列表,且禁用导出。
  • 重试放大:64 片 × 3 次重试 = 192 次请求,必须设总预算(如总 deadline 优先于单次重试),并对慢片做有限 hedge(同一请求发两份,先到先用),hedge 需限速避免雪崩。
  • 游标越界/数据被删:keyset 游标指向的行已不存在时仍能继续(比较的是值不是行存在性),这是 keyset 相对 offset 的额外好处。
  • 时间范围跨越预聚合与明细边界:边界日必须只算一次,避免重复计数(用半开区间 [start, end) 统一)。
  • 时钟与业务日:按业务时区(如 Asia/Shanghai)切日,不能用 UTC,否则跨日聚合会错位——这是对账差异的经典来源。

超时、重试、幂等、恢复:

  • 读路径天然幂等;写读模型必须按 (order_id, version) 幂等 upsert,重放 CDC 不产生重复计数。
  • 回填任务按 (created_at, order_id) 位点续跑,失败可断点重来;回填期间读模型标记”回填中”,避免把不完整数据当完整。
  • 恢复:CDC 位点落后时先追赶再切读;切换读模型用双读比对(新旧结果差异率 < 阈值才切)。

性能、可用性、验证指标:

  • 性能:P95 目标 2 s 需要同时满足”单次查询扫描行数有上界”(索引 + 时间范围限制)与”不等待最慢分片”(读模型单点查询);扇出模式下延迟由 max 决定,这条物理约束无法用调参绕过。
  • 可用性:读模型故障时后台查询不可用但交易不受影响(故障域隔离);读模型与主库分离部署,避免报表拖垮在线库。
  • 指标:跨片查询 P95/P99、深分页占比、读模型 lag(P95)、聚合与财务差异率(按天,目标 0)、partial 返回占比、缓存命中率、单查询扫描行数分布、慢分片 TOP-N。

三层归属说明:本题为非 Agent 场景,方案完全由确定性系统提供——CDC 位点、唯一约束、keyset 游标、完整性校验都是可断言的机制,不涉及模型判断。

追问#

追问 1:为什么不直接把 ES 当成主查询引擎,省掉读模型?

回答要点:ES 能解决”多条件筛选 + 检索”,但解决不了三件事:一是权威性——对账口径必须以事务库为准,ES 只能做派生视图;二是写放大与成本——8 亿行、42 列全量同步的存储与索引开销远高于只同步 8~10 列的宽表;三是一致性维护——ES 的 refresh 语义与 CDC 位点需要额外对账机制。合理分工是:交易库是权威源,读模型/ES 是派生视图且必须可重建,对账以权威源为准。

追问 2:缺一个分片时,为什么不能”先返回再补”?

回答要点:因为聚合是求和,缺片得到的是一个看起来正常的偏小值,没有任何异常信号,用户会据此做决策;而”少一页明细”至少用户能感知到不完整。工程原则是:不完整的结果必须自证不完整(错误码或 partial 标记),而不是让调用方去猜。这条原则在报表、账单、监控聚合里都成立——静默的部分结果是最危险的一类 bug。

追问 3:总数偶发差几十条,你怎么向业务解释并修掉?

回答要点:先解释清楚”总数”在跨片无快照查询下的语义——它是多次独立读的拼接,期间有新写入就必然漂移;这不是 bug 而是语义缺失。修法是给总数一个明确的时点语义:要么用预聚合的”截至 T 时刻”计数(可复现),要么用异步精确任务产出带 as_of 的快照。禁止的做法是”两次查询之间取平均”或”用明细条数反推”——那只是把不确定性藏起来。

关键点#

  1. 分片键决定访问路径:按 user_id 分片后,“按时间 + 状态”在物理上就是全片扫描,索引救不了路由。
  2. 跨片查询的延迟由最慢分片决定(LIMIT 语义要求全局有序),所以优化方向是减少扇出,而不是优化单分片 SQL。
  3. 聚合缺片必须失败:求和类运算的部分结果没有异常信号,静默偏差比报错危险。
  4. OFFSET 深分页在大偏移下既慢又不稳定(无统一快照 → 重复/跳行),keyset 是唯一可扩展的分页方式。
  5. 读模型是派生视图,必须可重建、可对账,且要暴露 lag 与 as_of,不能假装自己实时。

常见误区#

  1. 在应用层继续做”扇出 + 归并”并期待调优见效:扇出模型下 P95 由 max 分片决定,加索引只改变常数项;真正的解是换访问路径。
  2. 缺片时返回部分结果并标”成功”:这是最危险的实现,因为调用方无法区分”真的只有这么多”和”少了一片”;必须用错误或显式 partial 表达不完整。
  3. 把 ES/读模型当权威源做对账:派生视图必然滞后且可能丢事件,对账口径必须回到事务库,否则差异无法收敛。

小型实践#

30~60 分钟:本地用 4 个 SQLite 文件模拟按 user_id 哈希分片,每片 10 万条造数。

  1. 实现”扇出 + OFFSET 分页”,测第 1 页与第 50 页耗时差,观察重复行。
  2. 改为 keyset 分页((created_at, order_id) 游标),对比耗时与正确性。
  3. 实现两阶段聚合:各片返回 (merchant_id, sum, cnt),主流程归并;随后人为让 1 片超时,验证”缺片必须失败”的分支。
  4. 加一张预聚合表(按天 + 商家),对比”预聚合 + 增量”与”全量扫描”的耗时差。
  5. 加一个 data_as_of 字段并在输出中展示,体会”语义显式化”对调用方的影响。

延伸学习#

  1. PostgreSQL 官方文档 SELECT 的 LIMIT/OFFSET 与游标相关章节(OFFSET 需要跳过并丢弃前 N 行的语义,是深分页成本的直接来源):https://www.postgresql.org/docs/current/sql-select.html (方向性推荐,本轮未逐字核验)
  2. PostgreSQL 官方文档 COPY/分区与 pg_stat_statements 方向(用于验证”单查询扫描行数”这一指标口径):https://www.postgresql.org/docs/current/pgstatstatements.html (方向性推荐,未核验)

L01 · LeetCode 128. 最长连续序列#

题目 ID:Q-20261113-L01 | 分类:LeetCode | 一级类型:哈希表(集合)+ 摊还分析 | 二级主题:连续序列的起点判定

题目#

给定一个未排序的整数数组 nums,返回其中最长连续元素序列的长度(序列中的元素不要求在数组中相邻,只要求数值连续)。要求算法时间复杂度为 O(n)。

输入:nums: int[];输出:int(最长连续序列长度,空数组返回 0)。

先思考#

  1. 如果允许排序,答案很好写——排序把”数值相邻”变成了”位置相邻”。但排序是 O(n log n),限制条件要求 O(n),说明必须换一种方式判断”x+1 是否存在”。
  2. 要做到 O(1) 判断某个值是否存在,用什么结构?代价是什么?
  3. 如果对每个元素都向上数一遍 x+1, x+2, ...,最坏会重复数多少次?什么情况下这种重复可以避免?
  4. 什么样的元素才值得作为”一段连续序列的起点”?这个判据能不能保证每个元素最多被数一次?

参考答案#

题目信息:

  • 题号:128
  • 中文名:最长连续序列
  • 英文名:Longest Consecutive Sequence
  • 难度:medium(LeetCode 页面本轮未渲染难度标签,按标准分类记录,标注”未直接核验”)
  • 官方链接:https://leetcode.com/problems/longest-consecutive-sequence/ (已核验,2026-11-13)

题目描述:给定未排序整数数组,返回最长连续元素序列的长度;要求 O(n) 时间。重复元素不影响长度(同一个值只算一次)。注意”连续”指数值连续,不是下标连续。

输入输出:输入 nums(整数数组,可有重复、可为负、可为空);输出一个整数,表示最长连续序列的长度。

约束:0 <= nums.length <= 10^5;-10^9 <= nums[i] <= 10^9。

示例(官方原题给出 3 例):

  1. nums = [100,4,200,1,3,2] → 4(最长序列为 [1,2,3,4])
  2. nums = [0,3,7,2,5,8,4,6,0,1] → 9
  3. nums = [1,0,1,2] → 3(重复的 1 只算一次)

解题思路(推导过程):

  1. 先解决”判断存在”:把所有元素放进 set,x + 1 in s 是平均 O(1)。这一步用 O(n) 空间换来 O(1) 查询,是整道题的地基。
  2. 直接对每个 x 向上数,最坏情况是数组本身是一段长序列,每个元素都要数到末尾 → O(n²)。重复劳动的来源是:一段长度为 L 的序列被它的每个元素各数了一遍。
  3. 消除重复劳动的关键观察:一段连续序列里,只有最小值满足 x - 1 不在集合中。其余元素都”有前驱”,不该由它们启动计数。
  4. 于是算法是:遍历集合中的每个 x,若 x - 1 在集合中就 continue(它不是起点);否则从 x 向上数 x+1, x+2, ... 直到断开,更新最大值。
  5. 复杂度为什么是 O(n):每个元素只会被访问两次——一次作为外层遍历的元素,一次作为某段序列内部的 while 步进;且每段序列只被它的起点数一次。所有 while 的总步数不超过 n,故总代价 O(n)(摊还分析)。

正确性说明:

  • 完备性:任取最长连续序列 S = {a, a+1, ..., a+L-1},其中 a-1 ∉ S(否则 S 不是最长)。遍历到 a 时 a-1 不在集合中,故 a 必被当作起点,while 会一路数到 a+L-1,得到长度 L。因此算法至少能得到最优长度。
  • 最优性(不会多算):while 只在集合内连续时才前进,得到的每一段都是真实存在的连续序列,长度不会超过真实最优值。
  • 去重正确:集合天然去重,示例 3 的重复 1 不重复计数。

复杂度:时间 O(n)(建集合 O(n) + 遍历 O(n) + 所有 while 合计不超过 n 步);空间 O(n)(集合存储)。

Python 参考代码:

from typing import List
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
num_set = set(nums)
best = 0
for x in num_set:
# 只从一段连续序列的最小值开始计数
if x - 1 in num_set:
continue
y = x
while y + 1 in num_set:
y += 1
best = max(best, y - x + 1)
return best

边界情况:

  • 空数组:set() 为空,循环不执行,返回 0(不要写 best = 1 作为初值)。
  • 单元素:返回 1。
  • 全重复元素(如 [7,7,7]):去重后为单元素,返回 1。
  • 负数与大范围:-10^9 与 10^9 混排时集合仍可用,注意不要用数组做桶(值域过大)。
  • 整段连续:while 会一次走完,总代价仍是 O(n)(这正是摊还分析要说明的点,面试时要主动讲)。

测试用例:

输入期望输出验证点
[100,4,200,1,3,2]4官方示例 1,基本正确性
[0,3,7,2,5,8,4,6,0,1]9官方示例 2,含重复元素
[1,0,1,2]3官方示例 3,重复值去重
[]0空输入,初值不能是 1
[-1,-2,-3,0,1]5负数与跨零连续性

面试官可能提出的变体或追问:

  • 变体 1:要求返回最长序列本身(不只是长度)。改动是记录 start = x、length = y - x + 1,最后用 range(start, start + length) 输出;复杂度不变。
  • 变体 2:数组已排序且要求 O(1) 额外空间。此时改用一次线性扫描比较相邻差值(跳过重复),时间 O(n)、空间 O(1);说明”有序”这一额外信息可以换掉集合。
  • 变体 3:数据以流的形式到达,内存放不下。此时需要外部排序或分桶(按值域分块,逐块处理后合并边界),代价从 O(n) 空间转为多趟扫描。

可迁移的解题能力:一是”用哈希集合把存在性判定降到 O(1)”,这是大量”找配对/找连续/找重复”题目的通用起手式;二是”只在起点启动”这种消除重复枚举的思路,配合摊还分析给出严格上界——同类思想出现在滑动窗口的左边界单调性、单调栈的均摊 O(n)、并查集的路径压缩里;三是”用额外信息换掉一种资源”(有序换空间、集合换时间),是复杂度取舍的标准表达。

与 AI Agent 工程的自然联系(仅在存在自然联系时说明):本题的”只从起点扩展、其余跳过”与 Agent 里的”只对候选集中的锚点做扩展”同构——检索后先判定哪些候选值得深入(有前驱/有上下文支撑),避免对全部候选逐个展开导致成本爆炸。

追问#

追问:如果面试官说”你的循环看起来是嵌套的,怎么证明不是 O(n²)”,你怎么答?

回答要点:用摊还论证而不是”看起来”。把代价拆成两部分:外层循环遍历集合恰好 n 次;内层 while 的每一次前进都消耗一个新的、此前未被任何 while 访问过的元素(因为只有当 y+1 在集合中才前进,而每段序列只会被它的起点访问一次)。因此所有 while 的总步数不超过集合大小 n,总代价 O(n)。关键前提是”只在 x-1 不存在时启动”,去掉这个判断就退化为 O(n²)——面试时要主动指出这个前提,它才是复杂度成立的真正原因。

关键点#

  1. set 把”存在性判定”降到平均 O(1),代价是 O(n) 空间——这是本题唯一允许的”用空间换时间”。
  2. 起点判据 x - 1 not in set 是复杂度成立的核心,不是可选优化。
  3. 摊还论证的落脚点:每段序列只被数一次,所有 while 步数合计不超过 n。
  4. 集合天然去重,重复元素不影响结果;空数组必须返回 0,初值不能设为 1。
  5. 值域大(±10^9)时不能开桶数组,只能用哈希集合。

常见误区#

  1. 排序后扫描:正确但 O(n log n),直接违反题目的时间要求;面试中可以作为”基线方案”提出,但必须说明为什么不满足约束。
  2. 对每个元素都向上数,不做起点判定:最坏 O(n²)(全数组连续时),且在很多平台上会超时;这是最常见的”看起来对”的错误。
  3. 用 list 做成员判断:x - 1 in nums 是 O(n),整体退化到 O(n²) 甚至更差;必须显式 set(nums)。

延伸学习#

  1. LeetCode 128 官方题目页(示例、约束与 O(n) 要求):https://leetcode.com/problems/longest-consecutive-sequence/ (已核验,2026-11-13)
  2. LeetCode 官方题解页(哈希集合 + 起点判定的标准证明,可对照阅读摊还分析部分):https://leetcode.cn/problems/longest-consecutive-sequence/solution/ (仅核验题目页存在,题解正文本轮未核验)

延伸资料(当日重要主题)#

  1. Linux man-pages sendfile(2):https://man7.org/linux/man-pages/man2/sendfile.2.html (已核验,2026-11-13;内核内复制、in_fd 必须支持 mmap、零拷贝下区间不可修改)
  2. MCP 规范 Tools 页(2026-07-28 版本):https://modelcontextprotocol.io/specification/2026-07-28/server/tools (已核验,2026-11-13;工具是 model-controlled、服务端 MUST 校验输入、客户端 SHOULD 校验结果、tools/list 分页与 list_changed)
  3. MDN Using server-sent events:https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events (2026-11-11 核验过,本轮未重新打开;事件字段与断线重连语义)
  4. PostgreSQL 官方文档 SELECT(LIMIT/OFFSET 语义):https://www.postgresql.org/docs/current/sql-select.html (方向性推荐,本轮未逐字核验)
  5. LeetCode 128 官方页面:https://leetcode.com/problems/longest-consecutive-sequence/ (已核验,2026-11-13)

当日重点#

  1. 拷贝次数与上下文切换要分开数:read/write 是 4 拷贝 / 4 切换,sendfile 是 2 DMA / 2 切换;零拷贝消掉的是 CPU 拷贝,并顺手引入”传输期间区间不可变”这条新约束。
  2. 工具定义是双角色声明:对模型是概率性的提示,对系统是确定性的契约。描述越长不等于选得越准,缺少判别特征和同义工具并存才是主因;参数语义校验只能落在执行侧。
  3. “已展示”不等于”已承诺”:草稿层 / 提交层分离把承诺从时间维度改成状态维度;事实只能来自服务端事件日志,客户端只做投影。
  4. 不完整的结果必须自证不完整:求和类聚合缺片得到的是”看起来正常的偏小值”,必须整体失败;明细可降级但必须带 partial 与缺失范围。
  5. 消除重复枚举是 O(n) 的来源:128 的起点判据与摊还论证,和滑窗左边界单调、单调栈均摊是同一类思维工具。

运行信息#

  • 本次参考的历史题目或文件:题目索引.md(135 条,窗口 2026-08-13 至 2026-11-13,indexed_runs: 25);精选/README.md(4 条,未变动);README.md(导航表);2026-11/2026-11-12-AI-Agent-面试训练.md(主线衔接与 LeetCode 轮换建议);2026-11/2026-11-11-AI-Agent-面试训练.md(SSE 协议面结论,本轮 S01 复用并区分为应用层状态一致性);2026-11/2026-11-10-AI-Agent-面试训练.md(LeetCode 轮换记录)。未通读更早训练文件正文。
  • 配置读取状态:成功,实际配置版本 v8(last_maintained: 2026-11-02);无暂停类型;观察中 8 项未启用,本轮无「允许试学」项。
  • 去重结果:最近 30 天(2026-10-14 至 2026-11-13)无同题、换皮题或同一 LeetCode 题号;本轮选题响应上轮标注的覆盖优先级(工具与互操作协议规模治理、AI Agent 核心的编排层一致性、数据库跨分片读路径),并执行上轮 LeetCode 轮换建议(哈希表去重变体方向,改用 128;上轮建议的 232/946/94/144 未选,因本轮需要 O(n) 复杂度论证类题目)。
  • 链接核验:sendfile(2)、MCP Tools 页、LeetCode 128 于 2026-11-13 实际访问核验;LeetCode 128 页面本轮仍未渲染难度标签(连续第 10 次),medium 按标准分类记录并已在题目标注”未直接核验”;MDN SSE 页沿用 2026-11-11 核验结论(本轮未重新打开);splice(2)、PostgreSQL SELECT、pg_stat_statements、LC 128 题解正文为方向性推荐,均已标注未核验。
  • 覆盖变化:操作系统与网络 10→11、工具与互操作协议 8→9、AI Agent 核心 9→10、数据库与存储 13→14。下一周期优先:上下文工程与能力封装(7)、Agent 评测与可观测性(7)、编程语言与运行时(7)、RAG 与搜索(8,转二级轮换)。
  • 代码验证:L01 参考代码通过 6 组定向用例(含官方 3 例、空数组、单元素、负数跨零)+ 3000 组随机对拍(对照排序去重后线性扫描的独立实现),10 万元素乱序输入耗时 0.008 s。
  • 本文件由每日出题任务自动生成;本轮未执行精选、未修改任何历史训练文件正文、未修改 题库配置.md;仅报告本地保存结果,未验证也未声称任何远端同步状态。
2026-11-13 AI Agent 面试训练
https://vissssa.github.io/posts/ai-agent-interview-2026-11-13/
作者
Siegward
发布于
2026-11-13
许可协议
CC BY-NC-SA 4.0