17094 字
85 分钟
2026-10-29 AI Agent 面试训练
AI Agent 面试训练
2026-10-29

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

今日学习主题#

显式建立的东西才有保证。

今天五道题共用一条主线:顺序、关系、证据与代价,凡是靠”看起来应该成立”的默认行为、相似性或直觉来兜底的,都会在最不需要失败的时候失败。

  • 内存里没有同步边,就没有顺序保证——代码写的先后不等于执行的可见顺序。
  • 语料里没有显式边,就没有多跳关系——相似度给不出连接。
  • 模型没有可校验的证据,就不算知道——自信不是证据。
  • 指标上每加一个维度,成本是乘积而不是加法——维度是显式创建的序列。
  • 链表上不显式保存区间前驱,就接不回去——边界要靠结构消除而不是靠特判。

题型分布#

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

题目速览#

编号题目 ID题目名称题型一级类型难度一句话考察点
B01Q-20261029-B01并发代码”看起来按顺序写”,为什么没有顺序保证非 Agent 基础编程语言与运行时基础题(中高)happens-before 由同步边建立,不是由代码书写顺序建立
B02Q-20261029-B02什么时候”结构”比”相似度”更值钱:Graph RAG 的适用边界Agent 基础RAG 与搜索基础题(中高)图索引解决连接与整体概括,不解决单跳精确召回
S01Q-20261029-S01知识库没有答案时 Agent 编造答案并附错引用Agent 场景AI Agent 核心场景题(高)把”不确定”从提示词形容词升级为一等状态 + 模型之外的证据闸门
S02Q-20261029-S02为了定位一个问题加了一个标签,监控系统被打挂非 Agent 场景软件工程与平台工程场景题(高)每个 label 组合都是一条新时间序列,维度成本是乘积
L01Q-20261029-L0192. 反转链表 IILeetCode链表medium区间反转的三个指针不变式与虚拟头节点

B01 · 非 AI Agent 基础定义题#

题目 ID:Q-20261029-B01 一级类型:编程语言与运行时 二级主题:内存模型、内存可见性与并发原语的同步语义 分类:非 Agent 基础 标签:AI-Agent、面试训练、编程语言与运行时、内存模型、数据竞争、并发原语

题目#

一个 Go 服务里有两个 goroutine:写侧先给一个结构体的 payload 字段赋值,再把 ready 置为 true;读侧在一个循环里判断 ready,一旦为真就去读 payload。两个字段都是普通字段,没有任何锁、channel 或原子操作。

上线后偶发:读侧看到 ready == true,但 payload 仍是零值或半个旧值,频率为万分之一量级,只在 ARM 机器上出现,x86 上压测 48 小时不复现。

请回答:

  1. 这是什么性质的问题,为什么不改代码”再跑一次”就消失?
  2. 需要建立什么,才能让”写 payload 在写 ready 之前”这个顺序对读侧成立?
  3. sync/atomic、互斥锁、channel 各自建立的是哪一条同步边?
  4. 把 ready 改成原子变量,是否就足以让 payload 安全?

先思考#

  1. 先区分三件事:原子性(操作不可分割)、可见性(一个 goroutine 的写何时能被另一个看到)、有序性(两个写之间有没有顺序)。这三者是正交的。
  2. 再问:内存模型承诺的是”同步边建立的顺序”,还是”墙钟上的先后”?
  3. 然后想:编译器和 CPU 各会在什么前提下重排?单线程下合法的重排,在并发下为什么不再合法?
  4. 最后问:如果只对 ready 用原子操作,而 payload 仍是普通写读,读侧能否”借助” ready 的原子读建立顺序?条件是什么?

参考答案#

结论:这是一个数据竞争(data race),不是”偶发的时序 bug”。Go 内存模型只保证由同步边建立的顺序;两个 goroutine 之间没有任何同步操作时,“写 payload 在前、写 ready 在后”只对写侧自己的程序顺序成立,对读侧不成立。修复方式不是加 sleep 或重试,而是引入一条从写侧到读侧的 happens-before 边。

定义。按 Go 官方内存模型(go.dev/ref/mem,版本为 June 6, 2022):

  • sequenced before 是单个 goroutine 内由语言规范规定的偏序,即我们平时说的”代码书写顺序”。
  • synchronized before 是同步内存操作之间的偏序,例如一次 Unlock 与随后的 Lock、一次 channel 发送与对应的接收完成。
  • happens before 是这两者并集的传递闭包。

关键一句是:无数据竞争时,程序表现得好像所有 goroutine 被复用到单个处理器上,这个性质叫 DRF-SC(data-race-free programs execute in a sequentially consistent manner)。注意它的前提是”无数据竞争”——一旦有竞争,顺序一致的直觉全部失效。

适用边界。内存模型承诺的是同步边建立的顺序,不承诺墙钟先后,也不承诺竞争程序的行为。官方对竞争程序只给出有限保证:对不超过机器字长的读,必须观测到某个真实写(不能凭空产生值);但对超过一个机器字长的读,实现可以把它拆成若干个字长操作、以未指定顺序执行,此时竞争可能读到”不对应任何单次写”的不一致值,进而导致任意内存损坏。这就解释了”半个旧值”。

简洁示例。

// 有竞争:ready 与 payload 之间没有同步边
go func() { payload = compute(); ready = true }()
go func() { for !ready { runtime.Gosched() }; use(payload) }()
  • 用 channel 建立边:写侧 payload = compute(); c <- struct{}{};读侧 <-c; use(payload)。官方规则是”a send on a channel is synchronized before the completion of the corresponding receive from that channel”,于是 payload 的写 happens before 其读。
  • 用互斥锁建立边:官方规则是第 n 次 Unlock synchronized before 第 m 次 Lock 返回(n < m)。
  • 用原子变量做发布(release/acquire):写侧 payload = compute(); atomic.StoreInt32(&ready, 1);读侧 if atomic.LoadInt32(&ready) == 1 { use(payload) }。这是成立的,但成立条件是读侧的原子读观测到了写侧的那个原子写:所有原子操作表现得如同按某个顺序一致的次序执行,因此观测到 1 就意味着观测到了那次 store,于是经传递闭包得到 happens before。

与相近概念的区别。

  • 原子性 vs 可见性 vs 有序性:三者正交。原子只管”这一步不可分割”,不自动给别的变量建立顺序。
  • 原子操作 vs 互斥锁:原子操作保护单个变量的读-改-写;互斥锁保护跨多个变量的不变式。计数、标志发布用原子;“账户余额与流水一致”这类不变式必须用锁。
  • 单进程 happens-before vs 分布式 happens-before:两者名字相同、含义不同。单进程内有共享内存,问题来自编译器与 CPU 的重排,靠锁/channel/原子建立边即可;分布式下没有共享内存,只能靠消息传递的传递闭包,且逻辑时钟的先后反向不成立。这是 2026-10-22 B01 的主题,不要混为一谈。

典型应用(通用工程)。配置热更新的标志位与双缓冲指针交换、优雅关闭的停止标志、并发计数器与限流器、缓存条目发布、sync.Once 做单例初始化(官方保证:单次 f() 的完成 synchronized before 任何 once.Do(f) 的返回)。

面试现场回答(1–2 分钟)。

这是数据竞争,不是偶发时序问题。Go 内存模型里的 happens before 是”程序顺序”和”同步边”的传递闭包;两个 goroutine 之间没有任何同步操作时,写侧的代码顺序对读侧不成立,编译器和 CPU 都可能重排,多字数据上的竞争还能读到不对应任何单次写的值。修复要引入同步边:channel 发送对应接收完成、第 n 次 Unlock 先于第 m 次 Lock 返回、或者用原子变量做 release/acquire 发布——用原子时条件是读侧确实观测到了那次原子写。要注意原子只保护单个变量,跨变量的不变式必须用锁。验证上不能靠压测,x86 的强内存模型会掩盖问题,要用 go build -race 加 ARM 环境跑。

关键点#

  1. happens before = (sequenced before ∪ synchronized before)的传递闭包;DRF-SC 的前提是”无数据竞争”。
  2. 竞争的定义是”一个写与另一个读或写并发,且涉及的访问不全是 sync/atomic 的原子访问”。
  3. 超过机器字长的竞争读可能得到不对应任何单次写的值,官方明确说可能进一步导致任意内存损坏。
  4. 原子操作之间满足同步顺序,整体表现为某个顺序一致次序,语义等同 C++ 的 sequentially consistent atomics 与 Java 的 volatile;但这不等于”给任意其他字段自动建立顺序”。
  5. x86 压测不复现不代表正确;go build -race 与弱内存模型平台(ARM)才是判据。

常见误区#

  1. “我加了 atomic 所以整个结构体安全”。错误。原子只保证那一个变量的操作不可分割且参与全局顺序,跨字段不变式仍需要锁,否则第二个字段依然竞争。
  2. “测试没复现就没问题”。错误。竞争依赖具体时序、编译产物与 CPU 内存模型;x86 的 TSO 会掩盖大量问题,换 ARM 或换优化级别才暴露。正确判据是竞态检测工具加弱内存模型压测。
  3. “用了锁就不用懂内存模型”。错误。锁建立的边是”Unlock 先于后续 Lock 返回”,如果只有一侧加锁、或读写路径用了不同的锁、或用 TryLock 失败后继续访问(官方明确:失败的 TryLock 没有任何同步效果),边就没有建立。

追问#

问:把 ready 改成 atomic.Bool,payload 仍是普通字段,这样够吗?如果 payload 本身是一个会被继续修改的结构体呢?

答:第一问够,但要说清条件——读侧的原子读必须观测到写侧那次原子写,此时由 sequenced before 与 synchronized before 的传递闭包,payload 的写 happens before 其读。第二问不够:发布之后若写侧继续修改 payload,而读侧无锁读取,就重新构成竞争。正确做法是”发布即冻结”:发布之后对象不可变;若必须可变,则所有访问路径都要走同一把锁,或改用 channel 传递所有权。这个区分是面试里最能拉开差距的地方。

延伸学习#

  • Go 官方《The Go Memory Model》:https://go.dev/ref/mem (版本 June 6, 2022,已核验;含 happens before 的形式化定义、DRF-SC、数据竞争后果、channel / mutex / Once / atomic 的同步规则)
  • 实践方向:在 x86 与 ARM(或 GOARCH=arm64 模拟环境)上各跑一遍上述有竞争代码,再用 go build -race 复现并修复,观察 -race 报出的栈与修复前后差异。

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

题目 ID:Q-20261029-B02 一级类型:RAG 与搜索 二级主题:Graph RAG 与多跳检索 分类:Agent 基础 标签:AI-Agent、面试训练、RAG与搜索、GraphRAG、多跳检索、知识图谱

题目#

一个企业内部知识助手已经上线半年,基于标准的向量相似度检索(baseline RAG)+ 重排。业务方提出两类新需求,用现有方案效果都很差:

  • 一类是”连点成线”型:例如”过去半年因同一根因反复出现的故障有哪些,它们共同依赖哪个组件?”
  • 一类是”整体概括”型:例如”这批 4000 份事故复盘报告里,主要的风险主题是哪几个?”

团队有人提议上 Graph RAG。请你回答:

  1. Graph RAG 与 baseline RAG 的根本差别是什么?图索引里到底多了什么?
  2. 这两类问题里,哪些真的需要图,哪些只是召回没调好?
  3. 图索引的成本发生在哪个阶段?语料每天变更时怎么办?
  4. 图上检索会不会引入新的失效模式?

先思考#

  1. 先想清楚向量检索给出的到底是什么:它给的是”与查询语义相近的片段”,它能否回答”两个实体之间有没有关系”?
  2. 再区分三件不同的事:召回质量差(该来的没来)、排序差(来了但排在后面)、关系缺失(来了但没人把它们连起来)。这三者修法不同。
  3. 然后问成本放在哪:把连接成本放在索引期(预建图 + 预生成摘要),还是放在查询期(迭代多跳检索)?
  4. 最后问:图是源数据的第几手派生?源数据变了,它怎么跟上?

参考答案#

结论:Graph RAG 解决的不是”召回不准”,而是”片段之间没有结构,无法连接与整体概括”。它把连接成本从查询期前移到索引期,换来全局性与多样性上的提升;但它是派生视图,会漂移、会重建、会传播抽取错误,且对简单的事实查找没有收益,仍然应该回退到 baseline RAG。

定义。Graph RAG 是一种结构化、层次化的检索增强方法:先从原始文本抽取实体、关系与关键声明构建知识图,再对图做层次社区聚类,并自底向上为每个社区预生成摘要;查询时用这些结构为模型上下文提供素材。Microsoft GraphRAG 官方文档原文把它定义为”a structured, hierarchical approach to Retrieval Augmented Generation (RAG), as opposed to naive semantic-search approaches using plain text snippets”。

适用边界。官方明确给出了 baseline RAG 力所不及的两类问题,正好对应题目中的两类需求:

  • 需要”连点成线”时:原文 “Baseline RAG struggles to connect the dots. This happens when answering a question requires traversing disparate pieces of information through their shared attributes in order to provide new synthesized insights.”
  • 需要整体理解大规模语料或单个大文档时:原文 “Baseline RAG performs poorly when being asked to holistically understand summarized semantic concepts over large data collections or even singular large documents.”

边界也很清楚:官方的查询引擎保留了 Basic Search,明确用于”those times when your query is best answered by baseline RAG (standard top-k vector search)“。也就是说,条款号查询、单跳事实查找、强精确匹配的场景,图不会带来收益,只会增加成本与不确定性。论文侧的证据同样限定在”1 million token 范围的全局 sensemaking 问题”上,作者报告的提升集中在答案的全面性(comprehensiveness)与多样性(diversity),不是单跳准确率。

简洁示例。问”这批事故复盘里主要的风险主题是哪几个?”

  • baseline RAG:取与问题最相似的 k 个片段,模型只能看到局部,答案覆盖不全且高度依赖这 k 个片段碰巧代表哪些主题。
  • Graph RAG:先用层次社区摘要做全局检索——每个社区摘要独立生成一个局部回答,再把所有局部回答汇总成最终回答(map-reduce 形态)。这正是论文提出的机制:“each community summary is used to generate a partial response, before all partial responses are again summarized in a final response to the user.”

与相近概念的区别。

  • Graph RAG vs 混合检索 / 重排:后者在”片段集合”内部改善召回与排序,不引入实体间关系。召回差先查切片与混合检索,别急着上图。
  • Graph RAG vs 迭代式多跳检索:两者都追求多跳,但成本位置不同。多跳检索在查询期反复检索,延迟与成本随跳数线性增长;Graph RAG 把连接预建进索引,查询期便宜,代价是索引期贵且需要随语料更新。
  • 图索引 vs 长期记忆:两者都是派生结构,都必须与源数据版本绑定、都要有重建与对账。这一点与 2026-10-19 S01(知识更新滞后导致引用废止条款)是同一类问题,只是派生结构从向量索引换成了图。

在 Agent 系统中的应用。图检索应当作为 Agent 的一个工具纳入工具路由,而不是替换掉所有检索:按问题形态路由到 Global / Local / DRIFT / Basic 四种模式之一,官方对它们的定位分别是整体性问题、特定实体扩展、实体扩展加社区上下文、以及回退 baseline。工程上还有三条硬要求:

  1. 溯源锚点必须落到源文本单元。官方流程里 TextUnits 的作用之一就是”provide fine-grained references in our outputs”,答案里的每个结论都要能回指到具体文本单元,不能只回指到图节点。
  2. 图是派生视图,必须版本化。查询响应要能标注”基于哪个版本的图”,并在图上标注源语料的知识截止版本。
  3. 降级路径要显式。图构建滞后、抽取失败或索引不可用时,应回退 baseline RAG 并在答案里标注证据等级,而不是让 Agent 拿着陈旧图静默作答。

面试现场回答(1–2 分钟)。

先判断问题类型,不要一上来就上图。向量检索给的是语义相近的片段,它不提供实体之间的关系,所以”连点成线”和”整体概括”这两类问题会失败——这也是官方点名的 baseline RAG 的两个短板。Graph RAG 的做法是先用模型从语料抽实体、关系和声明建图,再用层次聚类做社区,自底向上生成社区摘要;查询时全局性问题走社区摘要的 map-reduce,局部性问题从实体向邻居扩展。它换来的是全面性和多样性,论文在百万 token 规模的全局问题上验证了这个收益,但对简单的事实查找没收益,官方自己仍保留回退到 top-k 的 Basic Search。代价要讲清楚:索引期贵,语料一变图和社区摘要就漂移,必须有版本绑定、增量重建和影响集失效;抽取错误会沿边传播,同名实体消歧失败会连错边。所以它应该作为 Agent 工具箱里的一个工具按问题形态路由,答案必须回指到源文本单元,且要有降级路径。

关键点#

  1. 图解决的是”关系与整体概括”,不是”召回精度”;召回差先查切片、混合检索与重排。
  2. 成本前移:连接成本从查询期搬到索引期(抽取 + Leiden 层次聚类 + 社区摘要),查询期便宜但索引期贵且需随语料更新。
  3. 官方保留 Basic Search 用于”最适合 top-k 的问题”,说明图不是通用替代。
  4. 图是派生视图,必须与源语料版本绑定并具备增量重建与对账;滞后会静默错误。
  5. 答案必须回指到 TextUnit 级锚点,不能只引用图节点或社区摘要。

常见误区#

  1. “图能解决召回问题”。错误。图改善的是连接与整体概括。该来的片段没来,是切片、嵌入或混合检索的问题,上图只会让索引更贵、错误面更大。
  2. “建一次图可以长期用”。错误。语料变更后实体与社区都会漂移,社区摘要是一级近似会丢失细节;没有版本绑定与增量重建机制时,Agent 会拿着旧图自信作答,属于静默失效。
  3. “图越大越全越好”。错误。抽取错误沿边传播,同名词消歧失败会连错边;成本随语料规模增长,而收益集中在特定的问题类型上,应当按问题形态路由而不是全局铺开。

追问#

问:语料每天有约 2% 的文档更新,图谱怎么处理?查询时要不要标注版本?

答:三层。第一层是脏标记——文档变更只标记受影响的 TextUnit 与其实体、边、所属社区,不整体重建;社区摘要按社区脏标记重算,因为自底向上生成,父级社区的脏会向上传播。第二层是版本——每次图构建产出图版本号,并绑定源语料的快照版本,查询响应里带上”基于图版本 X / 语料知识截止 Y”。第三层是兜底——未纳入本次图构建的变更部分,回退 baseline RAG 检索,并在答案里标注证据来源与证据等级不同。评测上要有”图索引滞后 + 源数据已更新”的专项回归集,否则这类失效不会出现在常规指标里。

延伸学习#

  • 《From Local to Global: A Graph RAG Approach to Query-Focused Summarization》,arXiv:2404.16130(Darren Edge 等,2024-04-24 提交 v1 / 2025-02-19 v2,已核验;含”两阶段建图 + 社区摘要 map-reduce”机制与百万 token 规模全局问题上的全面性、多样性结论)
  • Microsoft GraphRAG 官方文档首页:https://microsoft.github.io/graphrag/ (已核验;含 Graph Index 流程、四种查询模式 Global / Local / DRIFT / Basic 的定位,以及”何时回退 baseline RAG”的明确表述。注意:该页未给出索引或查询的成本数据,成本结论不可引用)

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

题目 ID:Q-20261029-S01 一级类型:AI Agent 核心(兼属 AI 安全与治理、Agent 评测与可观测性) 二级主题:失败反馈、能力边界的诚实表达与人工接管 场景类型:Agent 产品体验与失败反馈(首次使用) 分类:Agent 场景 标签:AI-Agent、面试训练、AI-Agent核心、失败反馈、证据闸门、引用溯源、人工接管、可观测性

题目#

业务背景:企业客服工单 Agent,面向 2.1 万家企业客户的 IT 支持场景。日均 3.5 万会话,注册工具 42 个,RAG 知识库 12 万篇文档(含历史版本与已废止版本,仅有 63% 标注了生效期)。

当前架构:意图路由 → 规划器(LLM,ReAct 循环)→ 工具调用 / RAG 检索 → 答案生成 → 引用附件 → 结束。答案质量唯一的线上指标是”会话结束满意度”,抽样率 3%,滞后 24 小时。

目标(一个季度):把”自信但错误的答案”从 6.4% 降到 1% 以下;自助解决率下降不超过 2 个百分点;转人工率上升控制在 3 个百分点以内;P95 延迟劣化不超过 15%。

限制条件:

  • 已灰度验证过两种”朴素方案”均不可用:全局降低采样温度(解决率打平但用户觉得答案变蠢)、在提示词里加”不确定就说明”(解决率掉 15 个百分点,因为模型把大量可答问题也判为不确定)。
  • 知识库无法在一个季度内补齐,缺文档是长期事实。
  • 人工坐席每班次 40 人,容量有硬上限。
  • 合规要求:任何涉及金额、SLA、合同条款的结论必须可溯源到具体文档条款。

已知现象:

  • 人工抽样 800 通会话:知识库无答案的问题占 9.1%,其中 71% 被自信作答;这些答案里 61% 的引用指向已废止文档版本或根本不存在的锚点。
  • 转人工平均发生在第 4.6 轮;转人工时 38% 的上下文包缺失”已尝试过哪些工具、结果如何”。
  • 用户在同一会话中重复提供同一单号/序列号的比例为 23%。
  • 投诉集中在两类表述:“说得很肯定但是错的”和”同一个信息问了我三遍”。

请给出方案:如何设计,才能让这个 Agent 在”不知道”的时候诚实地说不知道,而不是编造?

先思考#

  1. 先问一个反问:什么叫”不知道”?它应该由模型自己判定,还是由系统判定?如果把判定交给模型,你用什么指标观测它判得准不准?
  2. 再看失败的三类不同根因:模型凭空生成(无证据)、引用不可溯源(证据与答案脱节)、状态缺失(重复追问、升级太晚)。它们的修复层级一样吗?
  3. 然后想:如果给答案加上一道”必须有证据才能输出”的闸门,哪些问题会被误伤?怎么把误伤控制在 2 个百分点以内?
  4. 最后问:转人工不是失败,但转人工要带什么?如果人工侧拿到的是一段聊天记录而不是诊断包,人工一次解决率会怎样?

参考答案#

结论:不要试图让模型”更诚实”,要把”不确定”从提示词里的形容词升级为一等输出状态,并把它的判定从模型内部搬到模型之外的确定性证据闸门。答案有三态——answer(附已校验引用)、clarify(只问确实缺失的槽位)、escalate(带完整诊断包转人工);三态之间的切换由确定性规则与证据校验决定,模型只负责措辞与相关性判断。同时必须建立一个”无答案集”作为回归门禁,否则你根本看不见自己有没有改善。

问题拆解。三类失败对应三个不同层,不能混为一谈:

  1. 无证据仍生成——知识库确实没有,模型用自己的先验补齐。这是生成侧问题,必须在输出前拦截。
  2. 引用不可溯源——引用编号指向废止版本或不存在的锚点。这不是模型问题,是校验缺失:引用有效性是可断言的事实,应由确定性程序判定。
  3. 状态缺失——重复追问已提供的单号、升级过晚、上下文包残缺。这是编排层的状态管理问题,与模型能力无关。

核心方案。

  • 输出契约三态化:把 answer / clarify / escalate 写进 Agent 的结构化输出契约,每态有固定字段。answer 必须带 citations[](每项含文档 ID、版本号、锚点、片段 ID);clarify 必须带 missing_slots[];escalate 必须带 handoff_package。
  • 证据闸门(确定性层,位于模型之外)。答案放行需要同时满足:① 至少一个检索片段且重排分过阈值;② 片段所属文档为当前生效版本(废止版本直接判不通过);③ 答案中每个引用编号与片段一一对应,且锚点在文档中真实存在;④ 金额、SLA、条款类断言与片段内容可对齐(用蕴含判定,判定不通过即降级)。
  • 槽位状态机:已收集字段写入结构化会话状态(单号、序列号、时间窗、产品版本)。提问前先查状态,已存在的字段禁止再问。这是确定性逻辑,不依赖模型”记得住”。
  • 升级触发改三条:证据不足且属高风险域、连续 N 轮无进展、用户显式要求人工。命中任一即升级。升级时自动生成诊断包:会话摘要、已收集槽位、已尝试工具与结果、失败原因分类、建议的下一步。
  • 无答案集(unanswerable set):构造一批知识库确实无法回答、但与真实问题分布相近的样本,作为回归门禁。这是整个方案里最重要的一件东西——没有它,改善不可度量。

执行流程。

  1. 用户问题 → 槽位抽取 → 结构化状态写入。
  2. 检索(向量 + 稀疏混合,租户与生效期前置过滤)→ 重排。
  3. 证据校验(确定性):片段阈值 → 版本有效 → 锚点存在 → 断言对齐。
  4. 通过 → 生成带引用答案 → 引用二次校验 → 输出 answer。
  5. 不通过 → 一次定向改写重检索(更换查询表达、放宽到相邻文档)→ 仍不通过 → 按风险分级走 clarify 或 escalate。
  6. 全链路记录:证据判定结果、降级标记、最终态、引用校验通过率。

技术选型与依据。

  • 规则 + 重排分阈值起步,而不是上判定模型。理由:可解释、可回滚、无需训练数据,能在一周内上线并拿到第一版 abstention 精度数据。
  • 蕴含对齐(NLI)作为第二道,只用于金额/SLA/条款类高风险断言,控制成本与误伤面。
  • judge 只用于措辞与有用性,不用于”有没有编造”。理由:可断言的事实不要交给概率组件判定——这是 2026-10-15 B03 已经确立的原则。
  • 诊断包自动生成用模型,字段校验用程序:摘要与建议下一步交给模型,包内字段完整性由 schema 校验强制。

方案取舍。

  • 覆盖 vs 准确:不用单一阈值全局拒答,按风险域分级——金额/SLA/合规类严格(无有效证据即升级),一般咨询类宽松(无证据可给方向性建议但须显式标注”未查到依据”)。
  • 自动化 vs 人工成本:升级率上升是预期代价,用诊断包质量换人工一次解决率,把净人工成本压住。
  • 延迟 vs 证据充分性:追加一次校验与一次改写重检索约增加 300–600 ms,但只在证据不足的分支触发(预计 20–30% 会话),对 P95 的净影响可以通过并行校验进一步压缩。
  • 通用措辞 vs 场景化表达:clarify 只问缺失槽位而不是”请您提供更多信息”,escalate 明确告知已转人工与预期等待,避免用户重复等待。

失败路径、边界条件与恢复。

  • 检索超时 → 视为无证据,走向 clarify 或 escalate,绝不降级为”凭先验作答”。这是 fail-closed。
  • 证据/校验服务不可用 → 降级为规则-only 校验(片段阈值 + 版本 + 锚点存在),并在响应上打 degraded 标记纳入观测;高风险域仍 fail-closed 到升级。
  • 引用校验失败 → 允许一次”改写重生成”,第二次失败直接降级,不允许无限重试(重试预算 1)。
  • 人工队列满 → 转异步工单并给用户明确回执与工单号,而不是让 Agent 硬答。
  • 边界:多轮会话中早期已给出的答案与新证据冲突时必须重校验;知识库版本切换窗口存在双版本,检索须按生效期而非按最新;一条用户输入含多个问题时按问题分态输出,不合并成一个 answer;寒暄与低风险确认不受证据闸门约束。
  • 超时、重试、幂等:每次检索携带 deadline 与预算;工具调用按参数指纹幂等;升级动作幂等(同一会话同一原因只升级一次,避免重复建单)。
  • 恢复:会话检查点持久化,人工接管后 Agent 可从检查点继续;人工结论回流知识库时走写入闸门并带 provenance,避免把个案结论当通用知识灌回去。

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

  • 安全:高风险结论禁止无证据输出;检索前置租户过滤,引用不得暴露租户外文档;诊断包脱敏后再给人工;审计记录证据判定结果而不仅是最终文本,否则事后无法区分”判错了”和”没判”。
  • 成本:每会话预计增加 0.2–0.4 次检索 + 1 次校验调用,按 3.5 万会话/日估算增量;人工成本增量 = 升级率增量 × 单次人工成本,需与坐席容量对表。
  • 延迟:条件性增加 300–600 ms,仅证据不足分支触发;用并行校验压缩,并用”仅在风险域启用 NLI”控制长尾。
  • 可靠性:三层降级矩阵——证据服务不可用走规则-only;检索不可用走升级;人工不可用走异步工单。任一层都不得回退到”让模型自由发挥”。

验证指标与上线观测。

  • 核心门禁:无答案集准确率(unanswerable accuracy,要求 ≥ 95% 正确走向 clarify/escalate)、可答集解决率(下降 ≤ 2pp)、引用有效率(锚点存在且版本有效,目标 ≥ 99%)。
  • 运营指标:abstention rate 与 abstention precision(真无答案占比)、升级率、人工一次解决率、用户重复提供率、投诉率、P95 延迟、单会话成本。
  • 观测分桶:按意图、租户、知识版本、风险域分桶看差异——整体指标会掩盖”某个意图类型被闸门误伤”。
  • 上线方式:5% → 20% → 50% 灰度,守护指标(解决率、投诉率、P95)自动回滚。

三层能力归属(本题关键,必须说清)。

  • 模型的概率性能力:生成措辞、判断片段与问题的相关性、判断用户意图、生成诊断包摘要。这些允许出错,且有降级路径。
  • Agent 编排能力(确定性):三态状态机、轮次预算、升级触发条件、工具调用顺序、重试与降级策略、会话检查点。这些必须是代码,不能交给模型”自觉”。
  • 传统软件系统的确定性能力:引用锚点存在性与版本有效性校验、槽位去重、租户过滤与权限、审计存储、指标与告警、人工队列与工单。这些是闸门本体。

回答顺序建议(3–5 分钟口述):先给结论(把不确定做成一等状态 + 模型之外的证据闸门)→ 拆三类失败 → 讲三态契约与证据闸门的四条判据 → 讲降级与失败路径的 fail-closed → 讲无答案集这个门禁指标 → 点明三层能力归属。

关键点#

  1. 判定必须在模型之外。可断言的事实(锚点是否存在、版本是否生效)不要用模型或 judge 判定。
  2. 无答案集是门禁,不是可选项。没有它,任何”幻觉下降”的说法都不可验证。
  3. 证据不足必须 fail-closed 到 clarify/escalate,绝不能 fail-open 到生成。
  4. 重复追问是状态问题(编排层),不是记忆问题(模型层),修法完全不同。
  5. 审计要记录判定结果本身,不然后续无法区分”判错”与”没判”。

常见误区#

  1. 在提示词里加”不确定就说明”。错误。这把判定交给了概率组件:既没有可校验的判据,也没有可观测的中间结果,而且题目已经给出数据——这样干解决率掉 15 个百分点,因为它同时打掉了大量可答问题。
  2. 只看整体满意度或整体成功率。错误。无答案样本只占 9%,会被整体指标淹没;必须单独建 unanswerable 集,并按意图与租户分桶。
  3. 把 abstain 做成单一阈值。错误。误伤集中发生在特定意图上,应按风险域分级并分桶观测,否则要么误伤过大、要么闸门形同虚设。
  4. 引用只校验”有没有”而不校验”对不对”。错误。题目数据里 61% 的失败引用正是锚点不存在或版本已废止,这是可断言的确定性事实,是收益最大、成本最低的一道闸门。

追问#

追问 1(验证):你怎么证明下降的是”编造”而不是”有用回答”?

答:双集对照。无答案集上看”正确走向 clarify/escalate”的比例,可答集上看解决率与用户评分;两个集合必须同源同分布,否则无答案集会被做成”明显答不上来的怪问题”从而虚高。再加一层:对 abstain 样本做人工复核抽样,算 abstention precision,看被拦下的里面有多少是真无答案。三者一起才构成证据链。

追问 2(容量):如果闸门上线后升级率涨了 8 个百分点,超出坐席容量,怎么办?

答:四条并行。① 阈值按风险域收敛,只保留高风险域的严格拦截;② 把溢出部分转异步工单而不是实时排队;③ 用诊断包质量提升人工一次解决率来换吞吐(38% 缺失工具记录是这个环节最大的浪费);④ 承认这是知识缺口的代价,回到长期路径——按升级原因聚类补齐知识库的前 N 个主题。这里要说明的是:不要让”人工容量不够”倒逼你放宽闸门,那是把风险从成本侧转移回用户侧。

追问 3(能力边界):模型能不能自己判断”我不知道”?这在业界有研究吗,能不能直接用?

答:有,但不能单独用。Anthropic 团队的《Language Models (Mostly) Know What They Know》(arXiv:2207.05221)研究了模型预测 “P(IK)“(我知道答案的概率)的能力:模型在预测 P(IK) 上表现不错,能在一定程度上跨任务泛化,但在新任务上 P(IK) 的标定(calibration)会变差;另外在有相关材料进入上下文、或有解题提示时,P(IK) 会适当地上升。这恰好说明了两件事:一是自评可以作为特征参与闸门(例如与检索分做加权),二是不能作为唯一闸门,因为标定会随任务漂移,而漂移是静默的。工程上的做法是:把模型自评与外部门禁的判定差异做成一个观测指标——差异率持续上升就是自评失标的信号。

延伸学习#

  • 《Language Models (Mostly) Know What They Know》,arXiv:2207.05221(Saurav Kadavath 等,2022-07-11 提交 v1 / 2022-11-21 v4,已核验;含 P(True) 自评、P(IK) 预测与”新任务上 P(IK) 标定变差”的结论)
  • 自建实践方向(无外部链接):从真实会话里构造 100 条样本、其中 30 条确认无答案的 unanswerable 集,作为回归门禁纳入 CI。这是本题最值得落到自己项目里的产物。

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

取 60 条真实会话(其中 20 条人工确认知识库无答案),实现一个最小证据闸门:片段重排分阈值 + 文档版本有效 + 锚点存在性校验,三判据任一不过即走向 clarify/escalate。统计三件事:无答案集上的正确走向率、可答集上的误伤率、以及阈值从 0.5 到 0.9 扫描时这两条曲线的交点。把这条曲线画出来,它就是”准确 vs 覆盖”取舍的可视化答案。


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

题目 ID:Q-20261029-S02 一级类型:软件工程与平台工程(兼属分布式系统) 二级主题:可观测性数据治理——指标基数、采样策略与标签预算 分类:非 Agent 场景 标签:AI-Agent、面试训练、软件工程与平台工程、可观测性、指标基数、采样、Prometheus

题目#

业务背景:多租户 SaaS 平台,320 个 Pod,单实例 Prometheus + 远端存储,日志走采集 agent(与业务 Pod 同机部署)+ 集中式日志平台,链路追踪采样率默认 1%。

当前架构:业务 Pod 暴露 /metrics → Prometheus 抓取 → 本地 TSDB + remote write 到远端存储 → Grafana 与告警规则;日志由采集 agent 尾随容器标准输出;追踪由 SDK 上报到 collector。

事件经过:为了定位”某个客户反馈某个接口变慢”,一位工程师做了一次”很自然”的变更,三件事一起上线:

  1. 给 http_request_duration_seconds 增加了 user_id 标签(平台注册用户约 2000 万,日活 60 万)。
  2. 把追踪采样率从 1% 调到 100%,理由是”先全量采一段时间,查完再关”。
  3. 日志级别从 INFO 提到 DEBUG,并全量打印 trace_id,理由是”方便串起来查”。

故障现象:

  • 活跃时间序列从 180 万涨到 3900 万;Prometheus 内存从 9 GB 涨到 74 GB;抓取超时率 12%。
  • 告警大面积失灵——因为告警规则本身依赖抓取,抓取挂了告警就不会触发,故障期间是”监控盲飞”状态。
  • 远端存储写入账单月增 6.8 倍。
  • 日志量从 1.2 TB/日涨到 14 TB/日,采集 agent CPU 打满,同机业务 Pod 的 P99 随之恶化。
  • 讽刺的是最初的目标没达成:在 Grafana 里按 user_id 查询需要扫 120 万条序列,查询超时。

目标:48 小时内恢复监控可用性;长期把可观测性总成本控制在营收的 1.5% 以内,同时保留”定位单个用户单次请求”的能力。

限制条件:合规要求指标留存 6 个月,不能停监控;业务方改代码需要 2 周发版周期,不能指望立即全面下标签;团队只有 1 名 SRE 与 2 名后端。

取舍要求:请就”维度丰富度 vs 成本""全量追踪 vs 采样""指标 / 日志 / 追踪的职责边界""排障便利 vs 监控系统自身稳定性”给出明确选择。

先思考#

  1. 先算一笔账:user_id 这一个标签,为什么能把 180 万序列推到 3900 万?请把它写成”基数相乘”的形式。
  2. 再问:为什么”加个标签”不是让查询变慢一点,而是把整个监控系统打挂?中间经历了哪几级放大?
  3. 然后想:工程师真正的诉求是”定位单个用户的单次请求”。指标是不是承载这个诉求的正确载体?
  4. 最后问:这类变更应该在哪个环节被拦住?告警失效说明监控系统的可用性依赖链是什么?

参考答案#

结论:这不是”监控变慢”,是基数爆炸引发的级联故障,且告警依赖抓取,所以故障期间失去了发现故障的能力。止血要靠写入侧的标签裁剪与采样回退(小时级),根治要靠把高基数维度移出指标、下沉到追踪与日志,并用标签预算 + CI 门禁把”新增 label”当成容量变更来管理(周级)。

问题拆解。五个层面,各自独立修复:

  1. 基数爆炸:Prometheus 的数据模型明确——“Every time series is uniquely identified by its metric name and optional key-value pairs called labels”,并且”the change of any label’s value, including adding or removing labels, will create a new time series”。序列数是各标签基数(近似)相乘,不是相加。
  2. 采样失控:100% 采样把追踪从”抽样观测”变成”全量数据管道”,采集、传输、存储三段同时放大。
  3. 采集侧资源争抢:日志采集 agent 与业务同机且无资源上限,CPU 打满直接伤害业务,这是部署形态问题。
  4. 查询侧放大:高基数让单次查询需要扫描的序列数暴涨,Grafana 超时,原始诉求落空。
  5. 治理缺位:没有标签准入、没有基数预算、没有变更门禁,所以这次变更一路绿灯上线。

核心方案(分三阶段)。

  • 止血(小时级,不动业务代码):在 Prometheus 抓取配置里用 metric_relabel_configs 在写入前丢弃 user_id 标签——写入侧裁剪比事后删除便宜得多,且不占用 TSDB 资源;把追踪采样率降回 1%,日志级别回 INFO。这三件事都不依赖业务发版。
  • 恢复(天级):观察 head 序列数与内存回落曲线,确认抓取成功率恢复 100%;提前估算 WAL 回放时间(重启可能耗时几十分钟),必要时按代价评估是否清理 head block——这一步要显式权衡”丢失近期数据”与”恢复时长”。
  • 治理(周级):标签白名单 + 每指标序列数预算;预聚合 recording rules 保留 tenant_tier / route / method / status 这类有界维度;高基数明细下沉到追踪与结构化日志,用 exemplar 把指标分位桶与具体 trace 关联;新增 label 进 CI 门禁,要求声明基数上界。

执行流程。先用 topk 类查询定位贡献序列最多的指标与标签(不要凭感觉)→ 在写入侧加 metric_relabel_configs drop → 观察内存、抓取成功率、抓取耗时 → 恢复告警并做一次合成告警演练确认真的会响 → 再上治理机制。

技术选型与依据。

  • drop vs 聚合:drop 最便宜,但要保留的洞察改用 recording rule 聚合到有界维度(租户等级 + 路由 + 状态码),基数可控且查询快。
  • 头部采样 vs 尾部采样:尾部采样能在总样本量可控的前提下保留错误与慢请求,比”全量采一段时间”理性得多;代价是 collector 需要缓冲窗口内的全部 span,内存与时延要评估。
  • 指标 / 日志 / 追踪的职责边界:指标回答”有多少、多快、成功率多少”——低基数、可聚合、可告警;追踪回答”这一个请求为什么慢”——高基数、单条、按需;日志回答”具体发生了什么”——高基数但必须结构化且量可控。三者的桥是 exemplar,不是”在指标上加 user_id”。
  • 采集 agent 部署形态:必须设 CPU/内存上限,并与业务 Pod 资源隔离(独立 DaemonSet 资源配额),避免观测系统反噬业务。

方案取舍。

  • 维度丰富度 vs 成本:选成本,但用 exemplar + 追踪补回单用户能力。指标只保留有界维度,这是唯一能同时满足”可聚合告警”与”成本可控”的组合。
  • 全量追踪 vs 采样:采样,但改成尾部采样(保留异常与慢请求 + 固定比例基线),并把采样决策集中到 collector 而不是应用侧。
  • 排障便利 vs 监控系统稳定性:稳定性优先。监控系统不可用时排障能力是 0,不是”打折”。
  • 动态 DEBUG 的需求:改为按租户 / 时间窗动态开启的采样调试能力(有 TTL、有审批),而不是常驻全量。

失败路径、边界条件与恢复。

  • drop 标签会让既有看板与告警失效:改之前必须盘点依赖(哪些面板/规则用了 user_id),改之后一并修复,并在灰度期并行新旧两套面板。
  • 重启 Prometheus 的 WAL 回放可能几十分钟:先在预发演练,再决定是否需要清理 head block,并显式告知”这一步会丢近期数据”。
  • 远端存储仍按序列计费:drop 之后要确认远端同步生效,否则本地降了账单没降。
  • remote write 重试会在写入失败时放大流量:需要背压与队列上限,而不是无限重试。
  • 边界条件:租户维度的基数是否有界(3000 个租户可接受,2000 万 user_id 不可接受);更要关注的是 churn(序列流失率)——高频新建又消失的序列,对 TSDB 的代价往往高于同等数量的稳态序列;另外官方明确”Labels with an empty label value are considered equivalent to labels that do not exist”,空值标签不会额外计费但会让聚合语义混乱。
  • 超时与幂等:抓取超时会级联触发告警失灵,超时阈值与重试策略要成对设计;relabel 配置变更必须版本化、可幂等应用、可一键回滚。
  • 恢复:Prometheus 配置纳入版本管理,灰度到单个 scrape job 验证后再全量;保留 last-known-good 配置,回滚只需切回版本。

性能与可用性。单点 Prometheus 是本次故障的放大器——长期应做按功能/团队分片,或改用能承受更高基数的远端存储;查询侧设时间范围与序列数上限,避免单条查询拖垮实例。采集链路必须与业务做资源隔离。

验证指标与上线观测。

  • 恢复判据:活跃序列数、抓取成功率(目标 100%)与抓取耗时、Prometheus 内存 RSS、查询 P95、告警漏报数(用合成告警演练验证,而不是看规则是否”存在”)。
  • 长期观测:基数 Top-N 指标周报、序列 churn rate、新增高基数标签告警、远端写入字节/月、日志量/日、采集 agent CPU、可观测性总成本占营收比。
  • 门禁:新增 label 的 PR 必须声明基数上界并自动校验;超过预算的合入直接拒绝。

回答顺序建议(3–5 分钟口述):先定性(基数爆炸 + 告警依赖抓取导致盲飞)→ 引用数据模型的”新序列”规则说明为什么是乘积 → 分三阶段给方案(写入侧裁剪止血 / 恢复验证 / 预算与门禁治理)→ 讲清指标/日志/追踪的职责边界与 exemplar 桥接 → 给出验证指标与门禁。

关键点#

  1. 每个 label 取值组合都是一条独立时间序列;序列数是各维度基数的乘积,不是加法。这是全部故障的根。
  2. 告警依赖抓取,抓取被打挂 = 失去发现故障的能力,这是”监控系统自伤”最危险的部分。
  3. 高基数明细属于追踪与日志,不属于指标;桥接手段是 exemplar,不是加标签。
  4. 序列 churn(新建与消失速率)的代价常被低估,稳态基数不是唯一成本。
  5. 治理必须前置到准入:高基数一旦写入,历史数据与看板依赖同步形成,删除成本远高于准入成本。

常见误区#

  1. “监控越细越好”。错误。维度是乘积成本,一个无界维度就能让序列数涨一个数量级以上;可观测性的价值在于”关键时刻能查到”,不在于”平时存得多”。
  2. “先加着,出问题再删”。错误。序列一旦写入,TSDB 的索引与内存占用、远端账单、看板与告警依赖已经同步形成;删除要付出停机与回滚代价,远高于准入评审的成本。正确做法是把新增 label 当作容量变更走门禁。
  3. “采样率调 100% 排查更快”。错误。题目里已经证明:全量采样让查询扫 120 万序列从而超时,排障反而更慢;正确做法是尾部采样保留异常与慢请求,加固定比例基线,把总量压在可控范围。

追问#

追问 1(机制):为什么加一个标签不是”慢一点”,而是把整个系统打挂?中间有哪几级放大?

答:四级放大。序列数从 180 万到 3900 万是第一级(乘积);内存与倒排索引按序列规模增长,是第二级;抓取每个目标要序列化与写入的数据量暴涨导致抓取超时,是第三级;告警规则依赖抓取结果做求值,抓取失败使告警不触发,是第四级——这一级让故障从”性能问题”升级为”观测能力丧失”。所以恢复顺序必须是先裁剪写入侧把序列数降下来,而不是先扩容内存。

追问 2(需求):业务确实需要”查某个用户的请求”,你砍掉了标签,怎么满足?

答:把诉求拆开。指标侧提供”某租户等级 + 某路由”的分位与速率,用于告警与趋势;单用户单次请求走追踪与结构化日志,可以按 user_id 精确检索;两者用 exemplar 关联——在指标的直方图桶上挂典型 trace 的 ID,从”P99 变差了”直接跳到”看这条具体请求”。如果确实需要临时的高基数下钻,用带 TTL 与审批的动态采样窗口开启,而不是常驻标签。

追问 3(治理):怎么防止再发生?

答:三件事同时做,缺一不可。① 准入:新增 label 的 PR 必须声明基数上界,CI 自动校验并在超预算时拒绝;② 预算:按指标设序列数上限,超出触发告警而不是先看账单;③ 可见性:每周出基数 Top-N 指标报告与 churn 报告,让增长在变成事故之前被看见。再加一条组织约定:观测配置的变更与业务代码同等对待,需要评审与灰度。

延伸学习#

  • Prometheus 官方文档《Data model》:https://prometheus.io/docs/concepts/data_model/ (已核验;含”Every time series is uniquely identified by its metric name and optional key-value pairs called labels”与”the change of any label’s value … will create a new time series”)
  • Prometheus 官方文档《Metric and label naming》:https://prometheus.io/docs/practices/naming/ (已核验;含明确警示 “Do not use labels to store dimensions with high cardinality (many different label values), such as user IDs, email addresses, or other unbounded sets of values”)

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

本地起一个 Prometheus 加一个自建导出器,暴露两个版本的同一指标:一个带 user_id、一个不带,构造 5 万个用户。用 PromQL 统计两边的序列数差异,观察内存占用变化;然后写一条 metric_relabel_configs 把 user_id 在写入侧 drop 掉,验证序列数回落且查询恢复。把这三组数字记下来,就是这次故障的最小复现。


L01 · LeetCode#

题目 ID:Q-20261029-L01 一级类型:链表 分类:LeetCode 标签:AI-Agent、面试训练、链表、区间反转、虚拟头节点

题目#

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回反转后的链表。

位置从 1 开始计数。要求只反转节点之间的指向关系,不允许通过修改节点值来完成。进阶:能否只用一趟扫描完成?

先思考#

  1. 如果要反转的区间从表头开始(left == 1),函数返回值会发生变化;能不能引入一个结构,让所有情况都走同一段代码、不需要特判?
  2. 反转 [left, right] 这一段时,必须记住哪两个”区间外部”的节点?区间内部需要几个指针才能完成就地反转?
  3. 采用”把 cur 后面的节点逐个摘下来插到区间头部”的做法时,需要重复多少次?这个次数和 right - left 是什么关系?
  4. 反转结束后,原来的区间头节点变成了什么?它的 next 应该指向谁?

参考答案#

题号:92 中文名:反转链表 II 英文名:Reverse Linked List II 难度:Medium(按 LeetCode 标准分类记录;本次中英文两个官方页面均未渲染难度标签,该难度未直接核验) 官方链接:

以上两个页面本次均已核验可访问,题号、标题、示例与约束完全一致;英文页显示通过率 52.7%(该数值会随时间变化,不作为稳定事实引用)。

题目描述:给定单链表头指针 head 与两个整数 left、right(left <= right),反转从位置 left 到位置 right 的节点,返回反转后的链表。

输入 / 输出:输入为链表头指针与两个 1-起始的位置下标;输出为反转后链表的头指针。

约束:

  • 链表中节点数目为 n
  • 1 <= n <= 500
  • -500 <= Node.val <= 500
  • 1 <= left <= right <= n

示例:

  • 示例 1:输入 head = [1,2,3,4,5], left = 2, right = 4,输出 [1,4,3,2,5]
  • 示例 2:输入 head = [5], left = 1, right = 1,输出 [5]

解题思路。核心矛盾是:left == 1 时返回值会变,left > 1 时不变。用虚拟头节点(dummy)把这个差异消灭在结构里——dummy.next = head,之后一律操作 dummy.next,返回 dummy.next,两种情况代码完全相同。

然后维护两个”外部”锚点:

  • pre:指向区间前一个节点,全程不动,负责把反转好的区间接回前半段;
  • cur:指向原始区间头节点,反转后它成为区间尾节点,全程不动,负责接住后半段。

区间内用”头插法”就地反转,每轮做三件事:

  1. nxt = cur.next——摘下 cur 后面的那个节点;
  2. cur.next = nxt.next——把它从原位置摘除,cur 接住它的后继;
  3. nxt.next = pre.next; pre.next = nxt——把它插到已反转区间的头部。

共执行 right - left 轮(left == right 时为 0 轮,直接返回原链表)。定位 pre 需要从头走 left - 1 步,加上区间内 right - left 轮,整个过程只向前扫描一次,满足”一趟扫描”的进阶要求。

正确性说明。定义循环不变式:第 k 轮结束后,pre.next 指向的节点序列恰好是原链表中 [left, left+k] 这一段反转后的头节点,cur 仍指向原始区间头(即当前反转区间的尾节点),且 cur.next 指向原链表中 left+k+1 位置的节点。

  • 初始(k = 0):pre.next == cur,反转区间长度为 1,不变式成立。
  • 保持:每轮把 cur 的后继 nxt 摘下插到 pre.next 之前,反转区间长度加 1,且 cur.next 顺延到下一个未处理节点,不变式保持。
  • 终止(k = right - left):反转区间恰为 [left, right],cur 为区间尾并接住 right + 1 之后的剩余部分,pre 接住反转区间头,链表完整、无节点丢失、无环。

时间复杂度:O(n)。定位 pre 走 left - 1 步,反转走 right - left 轮,总计不超过 O(right) <= O(n),且只向前扫描一趟。

空间复杂度:O(1)。只使用常数个指针。

可运行的 Python 参考代码:

from typing import Optional
class ListNode:
def __init__(self, val: int = 0, next: Optional['ListNode'] = None):
self.val = val
self.next = next
class Solution:
def reverseBetween(self, head: Optional[ListNode],
left: int, right: int) -> Optional[ListNode]:
# 虚拟头节点:统一 left == 1 与 left > 1 两种情况,消除返回值特判
dummy = ListNode(0, head)
pre = dummy
# 1) 定位到区间前一个节点
for _ in range(left - 1):
pre = pre.next
# 2) cur 固定为原始区间头,反转后成为区间尾
cur = pre.next
# 3) 头插法:把 cur 后面的节点逐个插到区间头部,共 right - left 轮
for _ in range(right - left):
nxt = cur.next
cur.next = nxt.next # 摘除 nxt
nxt.next = pre.next # nxt 接到已反转区间的头
pre.next = nxt # pre 接住新的区间头
return dummy.next

关键边界情况:

  1. left == right:循环 0 轮,直接返回原链表,不能出错。
  2. left == 1:区间从头开始,返回值改变;虚拟头节点让这段代码无需任何特判。
  3. right == n:区间包含尾节点,cur.next 会变成 None,nxt 永远非 None(因为循环轮数保证 cur 后面还有节点),不会空指针。
  4. n == 1:唯一节点,left == right == 1,直接返回。
  5. 约束 n <= 500,迭代实现无递归深度风险;若改用递归需考虑栈深度。

测试用例(以下 5 个均已在本机实际执行通过,并与暴力解对 1296 组组合做了交叉验证,全部一致):

用例输入left / right期望输出覆盖点
1[1,2,3,4,5]2 / 4[1,4,3,2,5]官方示例,区间在中间
2[5]1 / 1[5]官方示例,单节点
3[1,2,3]1 / 3[3,2,1]整表反转,left == 1 且 right == n
4[1,2,3,4]2 / 4[1,4,3,2]right == n,区间含尾节点
5[1,2,3,4,5]2 / 2[1,2,3,4,5]left == right,零轮反转

补充验证过的用例:[1,2] / 1 / 2 → [2,1];[1,2,3,4,5,6] / 3 / 4 → [1,2,4,3,5,6]。

面试官可能提出的变体或追问:如果要求”每 k 个节点一组翻转,不足 k 个保持原序”(LeetCode 25),你的思路还能复用吗?可以——每一组都是一次”确定区间 [left,right][left, right] 的反转”,把上面的逻辑抽成 reverse(pre, cur, k),并额外返回下一组的前驱;多出来的关键是先探测剩余节点是否够 k 个,不够则不动。追问要点:探测与反转要分开,不要边走边改导致最后一组被误翻。

本题训练的可迁移解题能力:

  1. 用结构消除特判:虚拟头节点把”返回值是否改变”的分支合并成一条路径,这是处理链表头部相关问题最稳定的手法。
  2. 区间操作的三锚点不变式:区间外前驱、区间头(反转后成尾)、区间尾后的续接,三个锚点确定了就写不出错。
  3. 把”做了几轮”与”区间长度”对齐:right - left 而不是 right - left + 1,这类差一错误在链表题里最常出现,用最小例子(left == right、n == 2)验一遍就能抓住。
  4. 一趟扫描的可行性判断:只要指针只向前推进、不回退,就是一趟,与走多少步无关。

关键点#

  1. 虚拟头节点统一 left == 1 的返回值差异,是本题最省事的一步。
  2. pre 与 cur 全程不动,只做”接住”,所有插入都发生在它们之间。
  3. 循环轮数是 right - left,不是 right - left + 1。
  4. cur 反转后成为区间尾,它的 next 自动接住后半段,不需要额外保存尾后继。

常见误区#

  1. 通过修改节点值来”反转”。错误。题目与面试惯例都要求调整指针;改值在节点被外部引用、或值不可变时直接语义错误,且掩盖了真正的指针操作能力。
  2. 把节点取出来放进数组再重连。可行但不满足空间要求,且在有外部引用的场景下会破坏引用完整性;更重要的是它绕开了本题真正考察的指针不变式。
  3. 循环轮数写成 right - left + 1 或在 left == 1 时另写一套逻辑。前者会导致 cur.next 越界,后者说明没有理解虚拟头节点的作用,代码分支一多就容易出现空指针。

延伸学习#

  • LeetCode 官方题目页(中英文,链接见上,均已核验可访问)
  • 延伸练习方向:LeetCode 25「K 个一组翻转链表」(同一区间反转原语的复用,本次未核验该页面,仅作练习方向)

延伸资料#

主题资料类型核验状态
内存模型与数据竞争《The Go Memory Model》https://go.dev/ref/mem (版本 June 6, 2022)官方规范已核验
Graph RAG 机制与结论《From Local to Global: A Graph RAG Approach to Query-Focused Summarization》arXiv:2404.16130原始论文已核验(v1 2024-04-24 / v2 2025-02-19)
Graph RAG 查询模式与回退边界Microsoft GraphRAG 官方文档 https://microsoft.github.io/graphrag/官方文档已核验(该页无成本数据)
模型自评与诚实性《Language Models (Mostly) Know What They Know》arXiv:2207.05221原始论文已核验(v1 2022-07-11 / v4 2022-11-21)
指标基数与标签设计Prometheus 官方文档《Data model》https://prometheus.io/docs/concepts/data_model/官方文档已核验
指标与标签命名实践Prometheus 官方文档《Metric and label naming》https://prometheus.io/docs/practices/naming/官方文档已核验
链表区间反转LeetCode 92 中英文官方题目页官方题目已核验(难度标签未渲染)

未核验项(不作为事实引用):LeetCode 25 页面正文;Prometheus 内存与序列数的定量换算公式;各语言(Java / C++)内存模型的具体条文,本题仅以 Go 官方文本为准。


当日重点#

  1. 顺序不是默认存在的。内存模型里 happens before 由同步边建立,代码书写顺序只对单线程成立;这是今天最容易被忽略、也最容易在生产上以”偶发”面貌出现的一类问题。
  2. 关系不是相似度的副产品。Graph RAG 的价值在于显式建边,代价是索引期成本与派生视图漂移;用之前先确认问题真的是”连接与概括”而不是”召回”。
  3. 诚实是系统属性,不是提示词属性。把”不确定”做成一等输出状态,把判定放到模型之外的确定性闸门,并配一个无答案集门禁——缺任何一环方案都不成立。
  4. 维度是乘法,不是加法。可观测性的成本模型必须显式化,且治理要前置到准入;监控系统自身不可用会让所有排障能力归零。
  5. 边界用结构消除,不用特判堆积。虚拟头节点与三个锚点不变式,是链表区间操作与很多系统边界问题的通用解法。

本次运行信息#

配置读取状态:成功。读取 ../题库配置.md,实际配置版本 7(last_maintained: 2026-10-26),文件 mtime 为 2026-10-26 10:20,本次运行未修改配置。13 个一级类型均处于 active,无暂停类型;「观察中的新技术」8 项均未标记「允许试学」,本次未从中出题。按 v7 的 5 题配额执行(B01 / B02 / S01 / S02 / L01)。

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

  1. 题目索引.md(80 条 / indexed_runs: 14)— 主去重依据。
  2. 精选/README.md(4 条精选题)— 复习节奏核对。
  3. README.md — 导航表追加位置确认。
  4. 目标目录及其父目录无 AGENTS.md / CLAUDE.md。

去重结果:本次 5 道题在最近 30 天(2026-09-29 至 2026-10-29)内无重复、无换皮题、无同一 LeetCode 题号。

  • B01「内存模型与数据竞争」为首次考察;与 2026-10-22 B01(分布式时钟与因果序)属不同层概念,已在答案中显式区分。
  • B02「Graph RAG 与多跳检索」为首次考察;与 2026-10-13 B03(混合检索与重排)、2026-10-16 B03(切片与父子块)、2026-10-23 B02(Query Rewrite)在考察点上不重合。
  • S01 首次使用场景题类型「Agent 产品体验与失败反馈」;与 2026-10-15 S01(工具调用失败语义)、2026-10-19 S01(RAG 知识更新)的故障现象与修复层级不同。
  • S02 首次考察「可观测性数据治理:指标基数与采样」;与 2026-10-22 B02(Trace/Span 数据模型)不同命题。
  • L01 使用题号 92,未在历史中出现;已规避 146、3、215、739、875、124、207、142、547、560、39、1109、435、208,以及已删除未登记的 322。

覆盖更新:编程语言与运行时 4 → 5;RAG 与搜索 4 → 5;AI Agent 核心 7 → 8;软件工程与平台工程 6 → 7。

剩余缺口(下一周期优先):上下文工程与能力封装(4,缺口为 Skills over MCP 与本地技能目录取舍、上下文写入与选择策略);编程语言与运行时(5,剩余缺口为 GC 与性能分析);操作系统与网络(7,缺口为 CPU 与内存性能分析);数据库与存储(8,缺口为执行计划回归的上线门禁设计、分区表与继承树的统计盲区);分布式系统(7,缺口为共识算法与选主、时钟不确定区间的工程落地)。

到期精选提醒: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 未记录复习完成。本次未自动执行任何精选或复习动作。

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

归档状态:本文件为本地 Obsidian 目录写入成功;未进行任何远端同步验证,不声称已同步。

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