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