面灵AI→

恒生电子AI开发AI面试:闭包、缓存三兄弟与LangGraph选型

轮次
AI面试
时间
2026-09
来源
牛客网

《面试题目》

  1. 请简单做个自我介绍,包括个人基本信息、教育背景,重点聊一下你是怎么理解这个岗位的。
  2. 你当初是因为什么契机决定投递这个 AI 开发工程师岗位的?
  3. 请解释 Python 中闭包的概念,它的形成需要满足哪些条件?
  4. 在外函数里定义内函数,内函数使用外函数变量,外函数执行结束返回内函数,外层变量为什么不会被回收?
  5. 闭包会带来内存泄漏,实际开发中你会用什么方法解决闭包导致的内存问题?
  6. 分别说明缓存穿透、缓存击穿、缓存雪崩是什么,对应的解决方案有哪些?
  7. 分布式场景下,本地锁限制并发为什么会失效?
  8. 为什么永不过期的热点 key 方案相比分布式锁,有更低的一致性风险?
  9. 大模型稳定输出结构化 JSON,经常出现多余文字、格式非法,你会用哪些手段提升结构化输出稳定性与可解析性?
  10. 输出 JSON 缺失必填字段你会怎么处理?
  11. 把解析错误返回给大模型重生成的纠错方案,在生产环境会带来哪些额外问题?如何应对?
  12. RAG 系统支持多轮对话问答,需要解决哪些问题?
  13. 摘要压缩把冷门技术细节遗漏了该怎么处理?
  14. 多轮历史对话检索与 RAG 外部知识库检索,在排序逻辑上有什么核心差异?
  15. Agent 和 Workflow 的区别是什么?分别适合什么场景?
  16. 系统既要灵活处理,又要求所有动作符合合规、不能越权,你会怎么选择技术方案?
  17. 大模型给出规则之外的处理方案,如何区分是合理创新还是违规越权?
  18. 分享一个你做过最具技术挑战的大模型应用项目:解决什么问题、核心难点、如何攻克。
  19. 筛选有效测试用例,用什么指标、方法判断生成用例有效,相比人工手写效率更高?
  20. 有没有遇到方案理论可行但落地后效果不达预期,推翻原有思路调整方案的情况?
  21. 如何排查锁定根因是流程设计问题,而不是 prompt 写法、模型版本等其他因素?
  22. 生成内容不符合领域常识,与成本、延迟之间如何取舍?
  23. 解决测试用例质量差、不可复现的最关键设计是什么?为什么没有选择其他技术路线?
  24. 聊一个你熟悉的 AI 应用框架:解决什么问题、设计巧思、落地使用场景。
  25. 为什么选 LangGraph,不选 LangChain 或者其他编排框架?
  26. LangGraph 的 Checkpoint 和状态持久化,在你的项目中实际解决了什么具体问题?
  27. LangGraph 有什么短板?

《参考解析》

Python 闭包的形成条件与变量为什么不被回收:闭包要成立需要三个条件:存在嵌套函数(外函数里定义内函数)、内函数引用了外函数的局部变量(自由变量)、外函数把内函数作为返回值返回(或传出到外部作用域)。变量不被回收的原因是 CPython 在编译期就把自由变量识别出来了,并把它们从外函数的栈帧里搬进一个「cell 对象」,内函数的 __closure__ 持有对这些 cell 的引用,而 cell 又持有值——所以即使外函数的栈帧已经销毁,只要内函数对象还活着,这些值就有引用链可达,GC 不会回收。可以用 func.__closure__ 与 func.__code__.co_freevars 直观地看到这一点。要注意延迟绑定的经典坑:循环里创建的多个闭包共享同一个变量(应该用默认参数 lambda x=i: ... 或工厂函数固定每次的值)。

闭包导致的内存问题怎么解决:闭包本身不是「泄漏」,但容易造成对象生命周期被意外拉长——典型场景是把闭包注册成回调或事件监听后忘记注销,于是内函数一直持有外函数的大对象(大数组、数据库连接、整个请求上下文),导致它们无法释放。治理手段分四类:一是缩小捕获范围,闭包里只引用真正需要的小变量,不要把整个大对象捕获进来(必要时在闭包创建前先把它拆开或置空);二是显式注销,所有注册过的回调/监听在不再需要时反注册,weakref(弱引用)是更彻底的方案——让闭包只持有弱引用,对象无人引用时自然回收;三是用上下文管理器(with)或 finally 保证资源释放,不依赖 GC 时机;四是把长生命周期改为显式传参,避免闭包成为隐式的状态容器。排查上,gc.get_referrers 与 objgraph 可以定位到底是谁在引用对象,tracemalloc 能看出增长的对象类型。

缓存三兄弟:穿透是查一个缓存和数据库里都不存在的 key,请求每次都穿过缓存直击数据库,常见于恶意攻击或无效 ID 查询;解法是布隆过滤器提前拦截(注意假阳性)、空值也缓存但设很短 TTL、以及入口参数校验。击穿是某个热点 key 在失效的那一瞬间,大量并发同时回源;解法是互斥锁(只放一个线程回源、其余等待)或逻辑过期(不设物理 TTL,value 内带逻辑过期时间,过期后异步刷新,读到的仍是旧值)。雪崩是大量 key 同时失效或缓存集群整体故障;解法是过期时间加随机抖动、集群高可用(主从加哨兵或 Cluster)、多级缓存(本地缓存兜底)、以及入口限流熔断。三者的共同后果都是数据库压力骤增,但成因不同,方案不能互换——用过期的随机抖动治穿透完全无效。

本地锁在分布式下为什么失效:synchronized、ReentrantLock 这类锁的作用域是单个 JVM 进程,锁的是本进程内的对象监视器或 AQS 队列。分布式部署后同一个业务请求可能落到不同实例上,各实例的锁互不可见,于是不同实例的线程可以同时进入临界区——本地锁完全失效。要跨实例互斥必须用分布式锁(Redis 的 SETNX 加过期时间与 Lua 释放、ZooKeeper 的临时顺序节点、或数据库的唯一索引与行锁)。补充两个容易被追问的点:分布式锁必须解决「锁过期而业务没执行完」(看门狗续期)与「释放别人的锁」(value 存唯一标识并在释放时校验)两个问题;另外如果只是想让同一用户的请求串行,更轻的替代方案是按用户 ID 哈希路由到同一实例,用本地锁就够。

为什么「永不过期的热点 key」一致性风险更低:因为永不过期意味着缓存里不会出现「key 缺失」的窗口,所有读请求都被缓存接住,不存在「失效瞬间并发回源」的击穿路径,也就不会出现多个线程同时读库、各自写缓存导致的短暂不一致。它的更新方式改成「写请求主动更新缓存」或「后台异步刷新」,读路径永远命中。相比之下分布式锁方案在锁过期、锁获取失败降级、以及锁续期失败时都有窗口,窗口内会出现部分线程绕过锁直接读旧值,所以一致性风险在链路上更多。代价是永不过期需要主动维护(数据变更必须同步更新缓存,漏更新就会长期脏数据),并且要防止内存被冷 key 占满——通常配合「逻辑过期 + 后台刷新」与 LRU 淘汰一起用。回答时要说清这是「用运维复杂度换一致性风险」的取舍,不是一个绝对更优的方案。

提升结构化 JSON 输出的稳定性:分四层。第一层用模型侧能力:优先走厂商原生的结构化输出/工具调用(response_format、JSON Schema 约束、function calling),它们比在 prompt 里写「请只输出 JSON」可靠得多;同时把温度调低。第二层在提示词里给约束与示例:明确给出目标 JSON 的 schema、给出一个完整正例与一个「错误示范」(带多余解释文字的反例),并显式要求「不要输出 markdown 代码块、不要输出任何解释」。第三层在工程上做容错解析:用能容忍前后噪声的解析方式(先提取第一个 { 到最后一个 } 的子串、或正则先剥掉 ```json 包裹)、对常见错误做修复(尾随逗号、单引号、未转义的换行);解析失败的次数与类型要埋点,否则无法知道提示词改动是否有效。第四层是兜底:设重试上限,重试仍失败则降级到「用自然语言回答 + 人工处理」或返回结构化错误,绝不能把未校验的内容直接当结构化数据用。要强调的是「容错解析不是替模型擦屁股的理由」——如果某个字段长期解析失败,那是 schema 设计或模型能力问题,应该在提示词与字段设计上解决。

缺失必填字段怎么处理:区分三种情况。一是字段能从上下文推导(比如 currency 默认与用户地区一致),就按业务规则补默认值,但必须在返回体里标记「该字段为系统补全」并记录日志,方便审计。二是缺的是关键字段(金额、账号、目标对象),绝不能猜——直接把缺失信息与具体缺失字段回给模型,让它基于已有上下文补齐或向用户发起澄清;向用户澄清时要问得具体(一次只问最关键的一项),而不是把一张表丢给用户填。三是缺字段源于输入本身不全,那就应该在流程更早的地方拦住(入口做参数校验),而不是等到模型生成完再补救。工程上要把「必填字段清单」定义成单一数据源(JSON Schema 或 Pydantic 模型),生成与校验共用同一份定义,避免两边不一致。

解析错误回灌重生成在生产环境的问题:四个典型问题。一是成本与延迟放大:每次重生成都是一次完整的模型调用,失败率高时会成倍推高 token 成本与 P95 延迟,而且重生成不一定成功,容易形成长尾。二是重试风暴:大量请求同时重试可能触发上游限流,反而让整体成功率下降——所以要给重试加全局限流与指数退避。三是错误信息泄漏与提示注入:把原始错误与上下文一起回灌,可能把内部字段名、堆栈甚至其他用户数据带进 prompt,需要对回灌内容做裁剪与脱敏。四是幂等与副作用:如果这是带工具调用或写操作的链路,重生成必须保证之前的动作不重复执行(用 request_id 做幂等)。应对上还有一个更根本的改法:把「先让模型自由生成再校验」换成「用结构化输出约束 + 更小的生成单元」(拆成多次小生成、每次只产出可校验的片段),从源头上减少解析失败,而不是事后重试。

RAG 支持多轮对话要解决什么:五件事。一是查询改写(query rewriting):用户的后续提问常含指代(「它的价格呢」),必须结合对话历史改写成自包含的检索 query,否则检索必然跑偏。二是上下文预算分配:对话历史、检索到的知识、系统指令三方争抢 token,要定策略(历史做摘要、知识按相关性截断)。三是历史检索与知识检索的融合排序:历史对话和外部知识是两类不同性质的证据,得分不可直接比较(见下一问)。四是意图分流:判断这一轮到底需要检索外部知识,还是直接基于历史就能回答(比如「你刚才说的第二点再展开」),无差别检索会引入噪声。五是状态一致性:用户在多轮中修正过前提(「不是 A 公司,是 B 公司」),后续检索必须采用修正后的前提,这要求有一个显式维护的对话状态,而不是靠模型从长历史里自己找。

历史对话检索与知识库检索的排序差异:核心差异在「相关性之外还要考虑什么」。知识库检索是「找事实」,排序主要由语义相关性加质量信号(来源权威性、时效性、是否被引用过)决定,且同一份知识对所有用户都一样。历史对话检索是「找回上下文」,排序要额外考虑:时间近因性(越近的轮次越相关,通常要有时间衰减)、说话人(用户自己说过的话与助理说过的话权重不同)、话题连续性(与当前话题同一段的优先)、以及未被后续轮次否定的有效性(被修正过的旧信息要降权甚至排除)。所以历史检索更适合用「时间衰减 + 话题分段 + 显式状态」的混合策略,而不是纯向量相似度——纯粹按语义相似度找回历史,很容易把「上一轮用户否定的那个错误方案」重新捞出来当作依据。

Agent 与 Workflow 的边界:Workflow 是人预先编排好的确定性流程,步骤、分支、重试规则写在代码里,路径可预测、可测试、成本可控,缺点是无法应对未预料输入。Agent 由模型自己决定下一步调什么工具、以什么顺序,灵活、能处理长尾与开放式任务,代价是路径不可预测、成本与延迟波动大、评测与调试困难。适用场景的判断标准很实用:任务步骤能被明确写出且覆盖绝大多数情况,就用 Workflow;长尾分支多到无法穷举、或必须根据中间结果决定下一步,才用 Agent。工程上成熟的做法是「Workflow 骨架 + Agent 填充」——外层用确定性流程控制关键节点、审批与回滚,把自由度限制在局部(只让模型决定调哪个检索工具、要不要再查一次)。

灵活性与合规怎么兼顾:答案是「把自由度限制在可验证的范围内」,而不是在灵活与合规之间二选一。具体做法:把系统拆成「模型自由决策的部分」与「确定性执行的部分」,模型只输出意图与参数(结构化),实际动作由受控的审计过的执行层完成;给每个动作定义权限边界与参数范围,越界直接拒绝并记录,不允许模型绕过(这是代码层的硬约束,不是 prompt 里的软约束);高风险动作(写数据、对外发送、涉及资金)走人工审批或二次确认,低风险动作放行;全链路记录谁能触发什么动作、模型给了什么建议、最终执行了什么。这样灵活性体现在「模型可以选择用哪种方式达成目标」,合规体现在「能做什么由权限系统决定」——两者不冲突。

怎么区分合理创新与违规越权:核心是「有没有越出被授权的范围」,而不是「方案是否在规则清单里」。判断可以分三步:第一,看动作是否超出权限与数据边界(访问了不该访问的数据、执行了不该执行的操作)——这类一律是越权,不管动机多合理;第二,看是否违反了明示的硬约束(合规红线、合规口径、金额与审批流程),违反即违规;第三,在上述边界内,如果方案确实比既有规则更优,那就当创新对待,但必须满足「可解释、可回滚、可审计」三个条件:模型要给出理由与依据,动作要能撤销,过程要留痕。落地机制上,把「规则之外的处理方案」统一送进沙箱或建议态(生成方案但不直接执行),由人或策略引擎评估后再放行——这比试图在 prompt 里教会模型「什么是创新什么是越权」可靠得多。

怎么定位根因是流程设计问题:做变量隔离实验。第一步把问题分类:同一个 bad case 换更长的 prompt、更好的模型、不同温度,如果失败模式完全不变,基本可以排除「prompt 写法」与「模型版本」这两类解释。第二步做分层归因:把链路拆成检索、上下文构建、决策、工具调用四段,看失败发生在哪一段——如果错误决策的输入本身是正确的检索结果与完整上下文,那就是流程(决策规则或步骤设计)的问题。第三步看分布而不是个案:如果同类失败在不同模型、不同提示词下都稳定复现且集中在某一步,那就是结构性问题;如果只在特定模型上出现,那才是模型问题。第四步做对照修复:只改流程(不动 prompt 与模型)看是否解决,这是最终确认。要提醒的是这类排查必须有完整的链路日志与固定的回归集,否则每次都在重新猜。

领域常识、成本、延迟怎么取舍:把三者放在同一把尺子上衡量才有意义——最实用的是按「错误的代价」分层。高风险场景(涉及金额、合规、对外承诺)用「慢而准」的路径:更强的模型、检索加事实核查、必要时人工确认,接受更高的成本与延迟;低风险场景(内部辅助、草稿生成、探索性问答)用「快而糙」的路径:小模型加缓存、减少检索与校验环节,牺牲一点质量换吞吐。中间地带用分级策略:先用便宜模型试答,加一个轻量的一致性校验(关键实体与数字是否与检索原文一致),不通过再升级到大模型重跑——这就是常见的「模型级联」思路。另外要固化几条不该被成本妥协的底线:引用必须真实(不能因为省钱省略检索)、金额与权限相关字段必须有确定性校验、生成内容必须带可追溯的来源。

测试用例质量差、不可复现的最关键设计:最关键的是「把生成与执行验证闭环接起来」。用例质量差的根因是模型在「凭空写」——没有被测对象的精确定义、没有执行反馈,于是产出一堆看起来合理但跑不起来或断言不成立的用例。可复现则需要确定性:同一输入必须产出同一结果,这要求固定随机种子、固定模型的快照版本、固定依赖环境(容器化),并把用例的输入数据版本化。落到设计上:一,从结构化的被测对象定义(OpenAPI/类型定义/状态机)出发生成用例,而不是从自然语言需求自由发挥;二,生成的用例必须能直接执行(生成可跑的脚本),执行结果回灌给模型做修正,形成闭环;三,把「用例有效性」定义为可度量指标——新增有效缺陷数、重复率、误报率、可自动化率,用这些指标驱动迭代而不是靠人肉抽查。至于技术路线为什么不选别的:模板化生成覆盖率不足、纯人工写成本高且难以覆盖边界、纯 UI 录制脆弱且难维护,所以选了「结构化定义 + 模型生成 + 执行验证闭环」这一条。

为什么选 LangGraph:LangGraph 把 Agent 建模成状态图——节点是函数(调模型、调工具、处理数据),边是转移条件,状态在节点之间显式传递,因而支持条件分支、循环、持久化检查点、人工介入与时间旅行回放。选它的理由是可测试、可中断、可恢复:对比 LangChain 那种「while 循环里让模型自由发挥」的 AgentExecutor,状态可见、控制流可审计,非常适合有审批环节或长流程的业务。与其他方案的对比:AutoGen 偏多 Agent 对话编排、适合研究型协作但流程控制弱;CrewAI 是角色化团队抽象、上手快但对复杂控制流表达不如图;LlamaIndex 强在数据与检索抽象;直接手写状态机最灵活但要把检查点、重试、中断恢复全部自己实现,成本高且容易出错。选型的判断维度是控制流复杂度、状态持久化需求、是否需要人工介入、可观测性生态、以及是否绑定单一模型厂商。

Checkpoint 与状态持久化实际解决了什么:三类真实问题。一是长流程的中断恢复:一次任务可能包含几十个步骤、跨越分钟甚至小时,中途进程重启或部署发版时,靠检查点可以从最后一个节点继续,而不是从头重跑(这直接决定了成本与成功率)。二是人工介入(human-in-the-loop):高风险动作前把状态存下来并挂起,等人审批后从该点恢复继续执行,这在没有持久化的情况下无法实现,因为内存里的执行上下文会丢。三是时间旅行与调试:每个检查点都是一份可回溯的状态快照,能定位「哪一步的输入开始变坏」,也能用同一份状态复现问题——这对 Agent 这种路径不确定的系统几乎是必需品。设计上要注意:状态里不要塞不可序列化的对象(连接、文件句柄),要控制快照大小与保留策略(否则存储成本膨胀),以及要保证从检查点恢复的幂等性(已执行过的副作用不能重放)。

LangGraph 的短板:一是抽象与学习成本:把流程写成图比写顺序代码更重,简单场景属于过度设计,团队要理解状态、归约(reducer)、边条件这些概念。二是调试体验:图结构在节点多了之后阅读困难,出错时的堆栈与状态变化需要额外的可观测性建设(LangSmith 之类)才好看清。三是版本迭代快、API 有破坏性变更,升级需要仔细评估。四是状态管理有坑:并发分支写同一个状态字段时需要显式的归约函数,否则会互相覆盖;状态里放大对象会拖慢检查点。五是生态绑定:用深了以后与 LangChain 生态耦合,切换成本不低。六是它解决的是「编排」这一层,模型选型、检索质量、工具可靠性这些真正决定效果的部分它都帮不上——所以「选 LangGraph 就万事大吉」是典型的误判。