面灵AI→

腾讯 WXG 后台开发一面:两道 Hot100 变体题与项目连环追问

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

面试时长 1 小时 40 分钟。开场两道手撕,之后全程问项目,没有八股。

一、手撕(Hot100 上位变体)

  1. 课程表:原题只要求输出 boolean(能否完成所有依赖任务),这里要求输出所有可能的拓扑序列。
  2. 解压字符串:原题只有字母、数字和 [ ],这里多了一个 | 需要处理。

二、面试系统项目:并发、幂等与一致性

  1. 同一个用户、同一道题,重复提交会被拒绝吗?底层是怎么做的?
  2. 拿到租约的节点挂了之后,租约怎么处理?分布式 single-flight 场景下如何防止旧节点双写?
  3. 模型调用成功、拿到结果,但结果没有写库,会出现哪些场景?分别怎么处理,有什么风险?
  4. 长会话的运行态重建(会话恢复)是怎么实现的?降级策略是什么?
  5. 面试流程的状态机如何设计?状态、合法迁移、特殊处理分别是什么?
  6. 追问裁决用了 LiteFlow 规则引擎,完整链路和降级逻辑是怎样的?
  7. 分布式锁和各类防重分别应对什么业务场景?四类锁/机制分别是什么?
  8. 项目的冷热数据怎么划分?哪些字段属于热快照、哪些属于冷快照?

三、面试系统项目:选型与架构

  1. 为什么选择 MongoDB?MySQL、Redis、Mongo 三者的职责分工是什么?
  2. 整体技术选型是什么?这套选型的设计考量?整个项目最难解决的问题是什么,难在哪?
  3. 单机版 single-flight 如何实现?核心结构和流程,有什么固有局限?
  4. 为什么不依靠 Redis 高可用做容灾,而是用单机 single-flight?Redis HA 和 single-flight 是替代关系吗?项目实际的容灾手段是什么?
  5. 模型接入层能否兼容多模型?兼容的边界在哪?两套 Agent 模型的接入方式有什么区别?

四、上线风险评估与性能瓶颈

  1. 现有实现上线高并发会暴露哪些 P0、P1 问题?对应的改进方案是什么?
  2. 这个项目真实的瓶颈在哪里?哪些不是瓶颈?

五、CodingAgent 项目

  1. CodingAgent 是什么项目?整体四层架构、核心契约、主要特性是什么?
  2. CodingAgent 对比 Claude Code 有哪些核心差异?设计背后的动机是什么?
  3. CodingAgent 的四层上下文压缩机制 L1–L4 分别做什么?哪些内容必须保留?触发条件是什么?压缩的铁律是什么?
  4. CodingAgent 的长期记忆召回如何实现?有没有用向量检索?四条记忆通道各自的优缺点?

六、Agent 记忆体系横向对比

  1. Claude Code 的长期记忆体系如何实现(CLAUDE.md、auto-memory、会话压缩)?
  2. Codex CLI 和 Claude Code 对比:两者分层记忆的实现与设计分歧在哪?

七、大模型底层原理

  1. 抛开 Agent 应用,大模型底层推理的完整工作流程是什么?核心组件有哪些?训练与推理的区别?为什么会产生幻觉?入门学习路线是什么?

《参考解析》

「输出所有拓扑序列」考的是你有没有真正理解拓扑排序的状态空间。 原题(Kahn 算法)只需要判断入度能否全部清零;改成枚举所有序列,就不能在「删一条边」的贪心上走到底,而要回溯:每轮从所有入度为 0 的节点里挑一个、记录到路径、把它的出边入度减一、递归、再整段恢复(入度加回、路径弹出)。终止条件是路径长度等于节点数,此时把路径拷一份进结果。三个必答的细节:一是恢复现场必须完整(入度数组和路径都要还原,漏一个就漏解);二是入度为 0 的候选要按固定顺序遍历才能得到确定性输出,否则同一批解的顺序会抖动;三是复杂度是指数级,节点多的时候要提前说明「这是枚举问题,必须剪枝或加限制」。如果面试官追问「有环怎么办」,答案是路径长度到不了 n,此时判定为无解(Kahn 里就是入度非零的节点还存在)。

解压字符串多一个 | 的变体,考的是「递归下降 + 分隔符语义」。 原题(如 3[a2[c]])的骨架是:遇到数字读完整数、遇到 [ 递归解析直到 ]、遇到字母直接追加。多出来的 | 通常表示「多选一」或者「分段拼接」,处理方式取决于它的语义——如果 | 是并列分隔([a|b|c] 表示任选其一或并列),那它比 ] 低一级,解析到它时既不能结束当前递归层级、也不能把它当普通字符吞掉,正确写法是给每一层维护一个「当前片段」和一个「已完成片段列表」,遇到 | 把当前片段推入列表并重置,遇到 ] 把列表连同当前片段一起交给上层处理。这三层嵌套(数字/括号/分隔符)正好是「优先级」问题,先想清楚优先级再写代码比边写边试靠谱得多。分点说清「数字只修饰紧跟的 [」「| 的优先级低于 [」这两条,面试官就知道你不是背答案。

重复提交的幂等要答出「在哪一层拦」,而不是「加个锁」。 四层各司其职:接入层用「用户 id + 题目 id + 请求指纹」做去重键,短时间内重复的请求直接返回上次的结果(幂等表 + 分布式锁);业务层用唯一索引兜底(同一用户同一题目同一轮次只允许一条进行中的记录),让数据库做最终裁判;状态机层检查状态是否允许这次迁移(已经是「已提交」就不能再提交);模型层用一次性的 requestId 保证同一次调用只产生一份结果。要说明为什么不能只靠一层:只有锁的话,锁过期与进程崩溃会让重复进来;只有唯一索引的话,冲突会以异常形式打到用户面前。

租约(lease)与 single-flight 的双写问题,本质是「旧主以为自己还是主」。 租约是带过期时间的所有权凭证,节点拿到租约才能执行任务,执行期间需要续约(心跳)。节点挂掉后租约自然过期,新节点可以抢到,于是出现窗口:旧节点其实只是网络卡住了、进程还活着,恢复后继续写库——这就是旧主双写。防法有两种主流的:一是 fencing token(栅栏令牌),每次发放租约时令牌单调递增,所有写操作带上令牌,存储侧只接受比已见令牌更大的写,旧主的写会被拒绝;二是写前校验租约仍然有效(带条件的 CAS:「如果我的租约还没过期才写」),代价是多一次校验且仍有极小窗口。工程上更彻底的做法是把「所有权」下沉到存储侧(比如 Mongo 的 findOneAndUpdate 带 lease 条件),让判断和写入在同一次原子操作里完成。

「模型调用成功但结果没写库」是分布式系统里的经典半成功,要按「谁重试、能不能重试」分类。 场景一:调用成功、写库失败(DB 抖动/超时)——这是可重试的,用本地消息表或「先落一条 pending 记录再更新」把结果兜住,重试时必须幂等(用 requestId 做唯一键)。场景二:调用成功、进程在写库前崩溃——靠启动时扫 pending 记录恢复,所以 pending 必须在调用之前就写下去。场景三:调用超时但上游其实成功了——最麻烦,重试会产生两次真实调用,成本翻倍且可能状态不一致,解法是让上游支持「按 requestId 查询结果」的接口,或在调用前就分配好幂等键由上游去重。风险统一收口成一句:所有「先调用后落库」的链路都要有补偿,补偿的键必须是调用前就生成的。

长会话的运行态重建,考的是「哪些必须能从存储里恢复」。 运行态通常包括:会话状态机当前状态、已完成的轮次、当前轮的上下文(历史消息、简历、岗位信息)、以及连接层的临时状态(SSE 连接、音频流的偏移)。重建的做法是前四类全部持久化,连接建立时按会话 id 拉取快照并在服务端恢复上下文;临时状态不可恢复,靠客户端重连 + 服务端从最后一个完整消息边界续发。降级策略要分层:模型不可用时降级到「只读历史 + 提示稍后重试」,存储不可用时降级到「内存态继续跑当前轮、后台补偿落库」,长连接不可用时降级到「短轮询」——每一层降级都要有明确的用户可见提示,不能静默。

面试状态机的设计要点是「状态少、迁移严、副作用集中」。 状态建议是:待开始 → 进行中(含子状态:等待用户作答、模型生成中)→ 已暂停 → 已结束 → 已归档。合法迁移写成一个显式的表,任何不在表里的迁移直接拒绝并打日志——这一条能挡掉绝大多数并发导致的错乱。特殊处理有三处:一是「模型生成中」是唯一允许写入部分结果的子状态,用户此时提交要排队;二是超时(比如用户十分钟没说话)应迁移到「已暂停」而不是直接结束,允许恢复;三是幂等的结束操作(重复点结束只生效一次)。把状态机画成图和表各一份,面试时直接说「我们是表驱动的」,比口头描述流程有说服力。

LiteFlow 这类规则引擎的链路要讲清「决策在哪、执行在哪」。 典型链路是:输入(模型输出 + 上下文)→ 规则前置校验(结构合法性)→ 规则链编排(LiteFlow 的 chain 定义「先判什么、再判什么」)→ 逐节点执行(每个节点是一个独立组件,可单独测试)→ 汇总裁决结果 → 落库与埋点。降级逻辑必须显式设计:规则引擎本身异常时降级到「保守裁决」(比如一律追问、不自动通过);单个节点超时时跳过该节点并记录;规则版本要能灰度(按用户或会话分流到新旧两套规则),出问题能一键切回。这题的核心是让面试官看到你把「业务规则」和「代码流程」解耦了,而不是写了 500 行 if-else。

四类锁/机制的分工,要按「防什么」来分,不是按「用什么实现」来分。 常见的四类是:互斥锁(同一时刻只允许一个执行者,比如同一会话只允许一个生成任务);幂等锁/去重键(防重复提交,短时间内同键只处理一次,通常是 setnx + 短 TTL);租约锁(带过期与续约,用于长时间任务的所有权,比如 single-flight 的主节点);乐观锁/版本号(防并发覆盖,读时带版本、写时校验版本,冲突就重试)。这四者的区别在于「持有时间」和「冲突时的策略」:互斥锁短持有、冲突排队;幂等锁短持有、冲突直接返回;租约锁长持有、要续约与栅栏;乐观锁不持有锁、冲突重试。答清这张表,后面所有追问都能挂上去。

冷热数据划分的判据是「读的频率」和「是否参与在线决策」。 热快照是当前会话运行必须的:会话状态、当前轮的上下文摘要、用户与岗位的关键字段、未完成的工具调用与中间产物——它要能被一次读取拿到、放在 Redis 或文档库的热集合里。冷快照是一次性的历史:完整对话记录、音频文件、逐轮评分明细、报告全文——放在对象存储或归档表,只在回放和统计时按需拉取。划分错的两个典型后果:把全量对话放热路径,导致每次读写都很大、延迟抖动;把运行必需的状态只放冷路径,恢复会话时要拼多次查询、超时。

「为什么选 MongoDB」要答成「三类存储各自擅长什么」。 MySQL 承担强事务与关系一致性(订单、账户、需要 join 与唯一约束的业务);Redis 承担高频读写与临时状态(会话缓存、限流计数、分布式锁、热快照);MongoDB 承担「结构不固定、按文档整体读写」的数据(面试会话的消息流、模型返回的嵌套结构、报告 JSON)——它免去了为每种结构建表与迁移的代价,单文档原子性又能保证「一条会话消息连同它的元数据一起写成功」。反面也要说:Mongo 不适合强事务跨文档、不适合复杂 join、不适合需要严格约束的核心账务。这套「按数据形状选存储」的说法,比「Mongo 性能好」专业得多。

单机 single-flight 与 Redis HA 不是替代关系,是两层不同的东西。 single-flight 解决的是「同一时刻同一个 key 只让一个请求真正执行」,它的价值在于省掉重复的昂贵计算(重复调模型),单机实现就是一个 ConcurrentHashMap<key, Future> + 第一个请求创建 Future、后到的请求复用。Redis HA 解决的是「这个缓存/锁服务本身不能挂」,是可用性问题。所以项目里用单机 single-flight 的理由通常是:调用方就在同一个进程内(会话落在同一台机器上),跨机去重没有必要,反而增加一次网络往返和锁租约的复杂度。它的固有局限必须能说出来:进程重启丢状态、多实例之间无法去重、单机成为该 key 的瓶颈。容灾手段则是另外一套:会话粘性(同一会话路由到同一实例)+ 实例故障时会话迁移重建 + 幂等键兜底(即使重复执行也不会产生两份结果)。

多模型兼容的边界要讲成「契约在哪一层统一」。 能统一的:请求与响应的结构(统一成内部 message/tool 格式)、流式事件的类型(文本增量、工具调用增量、结束原因)、错误码与重试语义、计费口径。不能强求统一的:各家对工具调用的参数格式、思考/推理字段的暴露方式、上下文窗口与截断策略、特有的能力(某家支持 prompt cache、某家支持结构化输出)。所以接入层通常做成「适配器 + 能力声明」:每个 provider 一个 adapter 负责协议转换,同时声明自己支持哪些能力(流式、工具、缓存、结构化输出),上层逻辑只依赖声明,不依赖具体厂商。两套 Agent 接入方式(一套走标准 function calling、一套走自定义的文本协议解析)本质区别就在这里:前者契约由厂商保证,后者契约要自己解析和校验,后者的鲁棒性成本高得多。

上线风险的 P0/P1 划分要给出「影响面 + 触发条件 + 兜底」。 P0(上线即事故):连接数打满(每会话一条长连接,没有上限与排队)、模型渠道单一(某家挂了全线不可用)、上下文无上限(长会话把 token 预算吃穿、成本失控)、幂等缺失(重复提交产生双份计费)。P1(高峰期劣化):单机 single-flight 让热点会话压在一台上、Mongo 热集合无限增长、日志与埋点同步写阻塞主流程、重试没有退避导致雪崩。每一条都要配一个具体动作:连接数上限 + 拒绝策略、多渠道兜底与降级文案、token 预算与压缩、幂等键、热点打散、TTL 与归档、异步日志、指数退避 + 抖动。

「真实瓶颈在哪、哪些不是」是反直觉题,答错会显得没做过压测。 常见的假瓶颈:模型推理(它慢但你控制不了,属于外部依赖,不是你的系统瓶颈)、数据库 CPU(如果索引对,读写量远达不到瓶颈)、网络带宽(文本流很小)。真实瓶颈通常在这几处:长连接数量受文件描述符与内存限制、单会话内的串行等待(用户不说话时连接占着但不能复用)、上下文拼装与 JSON 序列化的 CPU、以及最容易被忽略的「同步写库/写日志」。判断方法只有一个:先加埋点分清各段耗时(P50/P95),再压测验证,别靠猜。

CodingAgent 四层架构与核心契约,答的是「层与层之间靠什么解耦」。 常见四层:交互层(CLI/IDE,负责展示、打断、权限确认)、编排层(任务规划、循环控制、终止条件)、能力层(工具:读写文件、执行命令、检索代码、调外部服务)、模型层(选模型、参数、重试与兜底)。核心契约有两份:工具契约(名称、参数 schema、返回值结构、是否需要用户确认、是否幂等)和任务契约(目标、验收标准、可用工具白名单、上下文引用)。四层压缩(L1–L4)通常按「越靠后越激进」设计:L1 只裁剪冗长的工具输出(保留首尾与错误行)、L2 对早期对话做结构化摘要、L3 把已完成任务的中间产物替换成引用(只留结论与位置)、L4 跨会话落盘成长期记忆并在需要时召回。必须保留的东西是固定的三类:system 与硬约束、当前任务的验收标准与未完成待办、已确认的事实(尤其是用户明确说过的偏好与禁止项)。 压缩的铁律是「可追溯」——任何被压掉的内容都要能在需要时按引用取回,且摘要必须标注它是摘要而不是原文。

长期记忆的四条通道,按「谁写、何时读」区分最清楚。 ① 显式规则文件(CLAUDE.md 这类,人写、每次全量注入,优点是可靠可控,缺点是占上下文、要人维护);② 自动记忆(agent 自己在会话中判断「这条值得记」并写入,优点是无需人工,缺点是会记错、需要去重与淘汰);③ 会话摘要(会话结束时压缩成一段,跨会话按项目召回);④ 检索式记忆(把历史片段向量化,按当前任务检索 top-k,优点是容量无限,缺点是召回不准、且难保证「一定想起来」)。是否用向量检索取决于「记忆量」和「查询的模糊程度」:几十条规则用全量注入就够,几千条历史才需要向量检索。四条通道的实际工程难点是冲突消解——同一个事实被记了两次且不一致时,以谁为准,这需要显式的时间戳与来源优先级。

Claude Code 的记忆体系可以按「三层 + 一个文件」讲。 一个文件是 CLAUDE.md(项目根/用户目录/子目录多级,进会话时按层级拼进 system,是唯一「确定性」的记忆);三层是:会话内的上下文(随对话增长,靠压缩控制)、auto-memory(模型在会话中主动写下的笔记,落在本地文件,下次会话按需加载)、以及压缩后的会话摘要(把长对话折叠成结构化摘要续上)。它的设计取向很明确:优先确定性、优先用户可控——CLAUDE.md 是人写的、不会被模型改坏;自动记忆是辅助,宁可少记也不乱记。

Codex CLI 与 Claude Code 的分歧在「记忆的写入权归谁」。 Claude Code 更偏「人定规则、模型遵守」,Codex 更偏「模型自己维护工作记忆」——它更依赖会话内的任务状态与工具产物的重新读取,跨会话的持久记忆做得更轻。这个分歧的根源是产品定位:一个面向长项目的持续协作(所以要有稳定的项目规则),一个面向「给一个任务、跑完就结束」(所以更看重当前上下文的完整与工具的可重放)。回答时不要评谁更好,要说清「各自的场景决定了各自的选择」。

大模型推理的完整流程,按「一次前向」讲最清楚。 输入文本 → tokenizer 切成 token id → embedding 查表得到向量 → 加位置编码 → N 层 Transformer(每层:LayerNorm → 多头自注意力 → 残差 → LayerNorm → FFN/ MoE → 残差)→ 最后一层输出 hidden state → 接 lm_head(与 embedding 共享权重)得到词表维度 logits → 采样(temperature / top-k / top-p / repetition penalty)选出下一个 token → 拼回序列继续,直到遇到结束符或长度上限。核心组件就是 tokenizer、embedding、注意力、FFN、Norm、lm_head、KV Cache。训练与推理的区别:训练是并行处理整段序列、有反向传播与梯度更新、要算 loss(下一 token 的交叉熵);推理是自回归逐 token 生成、没有梯度、靠 KV Cache 避免重复计算,因此瓶颈从算力变成显存带宽。

幻觉的成因要落到「训练目标」和「解码过程」两层。 训练层:模型学的是「给定前缀下一个 token 的概率分布」,目标是「像人写的文本」,不是「说真话」,所以流畅与正确在目标函数里没有区别;训练数据里的事实性错误、知识截止、长尾知识覆盖不足,都会让模型在没把握时也给出高置信度的输出。解码层:采样引入随机性、beam 与温度会放大「看起来合理」的路径;上下文里没有依据时,模型会用先验补全。工程上的缓解手段:检索增强(把依据放进上下文并强制引用)、工具调用(让模型去查而不是猜)、结构化输出 + 校验(先约束格式再验证内容)、自我一致性(多次采样投票)、以及最实用的「让模型标注不确定」。要能说清幻觉不可能被完全消除,只能被约束在可验证的环节里。

(关于这场面试的节奏:1 小时 40 分钟、两道手撕加 22 个项目追问,强度很大,但它们全部围绕「你自己写过的系统」展开,没有一道八股。这类面试的准备方式不是刷题库,而是提前把自己的项目写成「设计文档 + 故障清单」——每个模块能说清契约、边界、失败模式与兜底,追问再深也落在这个框架里。)