腾讯 WXG 后台开发一面:两道 Hot100 变体题与项目连环追问
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
面试时长 1 小时 40 分钟。开场两道手撕,之后全程问项目,没有八股。
一、手撕(Hot100 上位变体)
- 课程表:原题只要求输出 boolean(能否完成所有依赖任务),这里要求输出所有可能的拓扑序列。
- 解压字符串:原题只有字母、数字和
[],这里多了一个|需要处理。
二、面试系统项目:并发、幂等与一致性
- 同一个用户、同一道题,重复提交会被拒绝吗?底层是怎么做的?
- 拿到租约的节点挂了之后,租约怎么处理?分布式 single-flight 场景下如何防止旧节点双写?
- 模型调用成功、拿到结果,但结果没有写库,会出现哪些场景?分别怎么处理,有什么风险?
- 长会话的运行态重建(会话恢复)是怎么实现的?降级策略是什么?
- 面试流程的状态机如何设计?状态、合法迁移、特殊处理分别是什么?
- 追问裁决用了 LiteFlow 规则引擎,完整链路和降级逻辑是怎样的?
- 分布式锁和各类防重分别应对什么业务场景?四类锁/机制分别是什么?
- 项目的冷热数据怎么划分?哪些字段属于热快照、哪些属于冷快照?
三、面试系统项目:选型与架构
- 为什么选择 MongoDB?MySQL、Redis、Mongo 三者的职责分工是什么?
- 整体技术选型是什么?这套选型的设计考量?整个项目最难解决的问题是什么,难在哪?
- 单机版 single-flight 如何实现?核心结构和流程,有什么固有局限?
- 为什么不依靠 Redis 高可用做容灾,而是用单机 single-flight?Redis HA 和 single-flight 是替代关系吗?项目实际的容灾手段是什么?
- 模型接入层能否兼容多模型?兼容的边界在哪?两套 Agent 模型的接入方式有什么区别?
四、上线风险评估与性能瓶颈
- 现有实现上线高并发会暴露哪些 P0、P1 问题?对应的改进方案是什么?
- 这个项目真实的瓶颈在哪里?哪些不是瓶颈?
五、CodingAgent 项目
- CodingAgent 是什么项目?整体四层架构、核心契约、主要特性是什么?
- CodingAgent 对比 Claude Code 有哪些核心差异?设计背后的动机是什么?
- CodingAgent 的四层上下文压缩机制 L1–L4 分别做什么?哪些内容必须保留?触发条件是什么?压缩的铁律是什么?
- CodingAgent 的长期记忆召回如何实现?有没有用向量检索?四条记忆通道各自的优缺点?
六、Agent 记忆体系横向对比
- Claude Code 的长期记忆体系如何实现(CLAUDE.md、auto-memory、会话压缩)?
- Codex CLI 和 Claude Code 对比:两者分层记忆的实现与设计分歧在哪?
七、大模型底层原理
- 抛开 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 个项目追问,强度很大,但它们全部围绕「你自己写过的系统」展开,没有一道八股。这类面试的准备方式不是刷题库,而是提前把自己的项目写成「设计文档 + 故障清单」——每个模块能说清契约、边界、失败模式与兜底,追问再深也落在这个框架里。)