9.23 恒生电子 AI 应用开发面试:从 Python 装饰器、联合索引到 RAG 评测与 Agent 规划
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 先简单做个自我介绍,重点讲讲你怎么理解 AI 应用开发这个岗位。
- 你平时最常用的 AI 编码工具是什么?
- 谈谈你对 Python 装饰器的理解,核心作用是什么?举一个实际项目里用装饰器的场景,说明它解决了什么问题?
- 如果要给一个带位置参数、关键字参数和返回值的业务函数加一个统计执行耗时的装饰器,怎么处理参数传递和返回值透传?
- 这里统计的到底是哪一种时间?
perf_counter和process_time有什么区别? - Python 里实现装饰器时,能自动保留被装饰函数源信息(函数名、文档字符串)的标准库装饰器是什么?
- 创建联合索引时字段的放置顺序会不会影响查询效率?为什么?
- 有一个联合索引 (a,b,c),查询条件是 b=1 AND c=2,这个查询能用到这个联合索引吗?
- 联合索引在 B+ 树中存储时按什么规则组织键值顺序,从而支撑最左匹配?
- 让大模型稳定输出结构化数据(比如 JSON),你会用哪些手段提升稳定性和可解析性?
- 提示词里已经明确要求只输出 JSON,模型仍然在前后附带自然语言说明,怎么处理?
- 模型输出的 JSON 本身有转义错误、括号不匹配,正则也截不出合法片段,怎么处理?
- RAG、传统检索加生成、微调三种方案的本质区别是什么?分别在什么场景下选用?
- 企业内部有大量表述差异但高度相关的文档(比如不同部门对同一业务流程的不同口径),传统检索加生成会出现什么核心问题?
- 微调会从根本上改变大模型的哪些固有能力边界?
- 开发 AI Agent 的过程中,你总结过哪些常用的设计模式?分别适合解决什么类型的问题?
- 遇到需要动态调整全局目标、初始计划很快失效的开放场景,你会选哪种模式组合?
- 通过反思判断是否需要重规划,怎么设计反思环节的判定标准,避免频繁无意义地推翻原有计划?
- 分享一个你做过的最具技术挑战的大模型应用项目:它要解决什么问题,核心难点在哪,你又是怎么攻克的?
- 最初只有 50 条产品给的黄金数据来构建知识库,当时是怎么测量并确认回答准确性不达标的?
- 最初测召回时,20 条召回结果里会漏掉正确的文档片段,当时怎么一步步排查定位出漏召的根因?
- 优化切分策略的过程中,有没有原本判断有效、实测后效果不达预期被推翻的方案?
- 切分优化后召回率提升了,最终怎么判断整个客服系统的回答效果真的变好,而不只是召回指标好看?
- 为了降低 TTFT 替换了豆包 1.8 flash 模型、还做了 KV 缓存优化,在响应速度、回答准确性和调用成本之间做了哪些关键取舍?
- 聊一个你熟悉或自研的 AI 应用框架:它核心解决什么问题,实现上有哪些技术设计上的巧思,你在什么场景里真正用它落地过?
- 用点和边的图结构替代 LangChain 的链式调用之后,这个框架里最核心的节点抽象是怎么设计的?
- 所有节点都围绕共享 state 读写,这种全局共享状态相比每个节点独立传参,实际带来了哪些额外的代价?
- 实际用这个框架落地工作流编排时,遇到过最棘手的一个和它设计特性相关的坑是什么?
《参考解析》
装饰器与计时契约:装饰器是接收一个可调用对象、返回另一个对象的函数,@dec 就是 f = dec(f);带参数的装饰器多一层工厂,多层叠加从下往上应用、调用时从外往里进入,鉴权、缓存放在哪一层会直接改变语义。计时装饰器要盯四件事:参数用 *args, **kwargs 原样转发、结果原样 return;时钟用 time.perf_counter(),单调且分辨率高,time.process_time() 量的是进程消耗的 CPU 时间,I/O 等待和调度让出都不计入,拿它当用户等待时长会把长尾抹平;记录放 finally,成功、抛异常、取消都落一条,异常继续向上抛;同步与协程分开包装,同步包装器套 async def,量到的只是协程对象创建的时间,要用 inspect.iscoroutinefunction 分流并在异步分支里 await。functools.wraps 把 __name__、__doc__、__dict__ 拷过去并设置 __wrapped__,让 inspect.signature 能穿透回原签名,否则日志里全叫 wrapper;但 wraps 只管元数据,参数透传、异步语义、异常传播仍要包装器自己写对。
联合索引的列顺序与最左匹配:二级索引的叶子存「索引列 + 主键」,按索引列依次做字典序比较:先比 a,a 相同再比 b,都相同才比 c。所以 (a,b,c) 对 a 全局有序、对 b 只在同一个 a 内有序、对 c 只在同一个 a、b 内有序。最左匹配不是优化器额外加的规则,而是这棵树只能给出这样的连续区间。a=? AND b=? 能精确定位;a=? AND b>? AND c=? 里 c 落在范围之后,收窄不了扫描范围,但仍可做索引条件下推在索引层过滤、少回表,查询列都在索引里就是覆盖索引。b=1 AND c=2 缺前导列 a,前缀定位失效,但不等于一定用不上:可能走全索引扫描(覆盖查询时扫索引比扫全表便宜)、MySQL 8.0.13 之后的 Skip Scan(前导列基数低才划算),也可能直接全表扫描,看执行计划才算数。列顺序的原则是等值条件在前、范围与排序在后:工单表 (tenant_id, status, created_at) 支撑「某租户某状态最近 20 条」,前两列定区间、第三列反向扫到 LIMIT 就停,SELECT id, created_at 还被索引覆盖。「选择性最高的列排第一」并不严谨,要优化的是高频访问路径的整体代价,候选方案用真实数据分布下的 EXPLAIN 比。
结构化输出:四层防线与脏输出:把「模型能不能吐合法 JSON」当成一个问题会漏掉大半故障,实际要拆成生成协议、语法、结构、业务语义四层。协议层优先用供应商的 schema 约束输出或工具参数 schema,不能只写一句「只输出 JSON」——JSON mode 通常只保证是合法 JSON,schema 约束才约束字段。语法层用严格解析:json.loads 默认接受 NaN、Infinity,用 parse_constant 拒掉;重复键默认后者覆盖前者,用 object_pairs_hook 查重;parse_float=Decimal 让金额不掉进二进制浮点。结构层用 Pydantic 校验类型、必填、枚举和多余字段。业务层才决定能不能执行:权限、实体是否存在、金额能否与订单对上、证据是否支撑结论——币种错了、订单号是别的租户的,格式再对也是事故。
模型在 JSON 前后加解释,第一反应不该是写正则。贪婪地取第一个 { 到最后一个 },碰到嵌套、字符串里的大括号或两个候选对象,就会把错位的片段当成结果。顺序是:先严格解析整段响应;失败后只剥离协议定义过的外层围栏;确实要从说明文字里捞,就用 JSONDecoder().raw_decode() 解出一个完整顶层对象并拿到结束下标,跳过已解析区间继续找,根类型限定 object;两个候选都合 schema 就重生成或报歧义。括号不匹配、非法转义先看是不是被截断——finish_reason=length、连接中断、工具流没结束都算没生成完,得补协议或重发,不能拿格式修复掩盖上游故障;真语法错误交给有重试上限和 deadline 的修复步骤,修完关键字段要和请求、数据库对照,补一个负号就能改掉金额。禁止 eval,ast.literal_eval 解的是 Python 字面量。解析、schema、业务三类错误分开统计,集中在某个模型或长度区间就修根因。
RAG、微调与词汇鸿沟:这三个词常被并列成三选一,其实维度不同。RAG 是把外部证据检索进上下文再生成,「传统检索 + 生成」只要真用了检索到的证据,本来就是 RAG 的一种,BM25 还是向量只决定检索组件。微调改的是参数,改的是给定输入下输出的分布:领域表达、格式遵循、工具选择、分类边界能明显改善;但它不是可精确更新的知识库,改一条制度要重训、也无法保证只动这一条;也不突破架构限制,上下文窗口、模态、外部权限都不是 SFT 能给的,更不会因为微调就不幻觉。选型先看失败类型:缺新事实、要出处、要按权限过滤,矛盾在检索质量和索引新鲜度;材料给对了模型仍不守规则、格式跑偏、路由选错,才轮到微调。「先提示词、再 RAG、最后微调」只是成本优先的排查顺序。
词汇鸿沟是纯词面检索的典型失效:A 部门写「客户风险等级复核」,B 部门写「投资者适当性重新评估」,说的是同一件事,重合词却几乎没有,正确文档进不了上下文,生成器只能给出不完整答案、误拒答,或者用参数记忆编一段很顺的内容。改法是领域同义词与别名规范化、受控查询改写、BM25 与稠密召回混合再重排;但代码、编号、条款号仍是稀疏检索更强,不能为了上向量把词项通道删掉。更要小心口径差异有时不是同义改写而是规则本身不同(适用对象、地区、时间都不一样),这时要在元数据里带部门、适用范围和生效时间,按权威性和适用性选,选不出来就问,不能把两套制度揉成一段顺滑的答案。
Agent 的滚动规划与重规划触发器:Plan-and-Execute 在开放场景的死法很固定:计划一生成就过期,后面全在硬执行。可行的是分层:上层只维护用户授权的目标、硬约束和里程碑,中层保留「近期几步」的计划,下层用 ReAct 处理局部不确定性,新观察进来只改近期计划,里程碑变化才回到上层。这里必须切开两件事:手段失效(工具报错、资源变了)可以自己换路;目标本身变了(新证据说明做不到、用户改需求)只能上报或请用户确认。Agent 不能因为网页里一句话就改掉目标和权限边界,提示注入正是从这里进来的。状态里存目标版本、约束、计划版本、已验证的里程碑、未完成依赖、预算和失败证据;重规划时作废失效部分、保留已完成成果,还要防止被取消的旧任务把延迟结果写回来。
反思环节最容易退化成每步都自我怀疑一遍,代价是延迟翻倍、计划反复推翻。判据该围绕可验证的偏差:硬条件直接触发——权限不满足、关键依赖失败、必守约束被违反、用户改了目标;软条件才需要冷却和滞回——连续若干步没有新增有效证据、同一工具同样参数反复失败、剩余预算走不完当前路径。阈值靠离线回放和线上灰度定,不能把模型自报的「信心 0.8」当概率。执行层记行动签名和产物摘要来识别原地打转;对比「不反思 / 每步反思 / 事件触发反思」三组的成功率、耗时、成本,再看无效重规划率和该重规划却没重规划的比例。
50 条样本能证明什么,漏召怎么查:先问清这 50 条是什么:知识条目、FAQ,还是带正确答案和证据的评测题;同一批数据既拿来建库又拿来测原句,分数一定虚高,反复用它挑切分和提示之后,它也不再是独立测试集。50 条独立评测题里错一题就是 2 个百分点,40/50 看着是 80%,Wilson 95% 区间大约 67%~89%——「80 分」和「70 分」在这批样本上区分不开。所以「不达标」要对照事先定好的业务门槛、高风险错误数和区间,而不是盯点估计;这 50 条的作用是发现问题、攒出第一版回归集。
漏召排查按链路分层,先把 query、索引快照、embedding 版本、切分配置固定住,否则一边改一边查得不出可复现结论。顺序是:证据在库里吗(文档入库没有、表格解析有没有丢、chunk 里到底有没有答案);过滤有没有误杀(权限、业务标签、生效时间,权限不能为了提召回而绕过);查询与表征有没有错位(同义词、缩写、embedding 输入模板、超长截断);近似检索是不是漏的(小集上用精确向量检索对照,再调 ANN 参数);融合、重排、TopK 在哪一步把证据挤掉。还要区分 Hit@K 和 Recall@K:前者只问前 K 有没有至少一条相关,后者问已标注证据覆盖了多少。一道题要同时看「适用对象、额度上限、例外条件」三段,命中一个 Hit@5 就是 1,Recall@5 只有 1/3,只看前者会把召回算得比实际好。
切分实验最常被推翻的假设是「块越大上下文越完整」。块变大会把无关主题带进来、稀释向量表示;调大 overlap 同理,边界断裂缓解了,重复候选却占满 TopK。实验要固定 token 预算再比。判断系统有没有真的变好不能看召回:固定模型和上下文预算做配对比较,统计「旧对新对 / 旧错新对 / 旧对新错 / 旧错新错」四格,净改善是(旧错新对 − 旧对新错)/ N;二元评分用 McNemar,连续评分用配对 bootstrap;严重回归单独拦截。上线走影子流量,看有效解决率、重复咨询、转人工率和负反馈——转人工率下降不一定好,系统把答错标成已解决时指标会变漂亮但伤害更大。
TTFT 与缓存的取舍:用户感知的首字时间含网关、排队、检索、重排、prefill 和网络回传,各段有重叠,把各段 p95 相加当端到端 p95 是错的;换一个 prefill 更快的模型只改善计算段,瓶颈在重排或队列时几乎没有感知收益。缓存要分单次生成的 KV Cache、跨请求的 prefix cache 和整段答案缓存,前缀缓存只省命中前缀那部分 prefill、不加速后续 decode。真成本按「总调用费用 ÷ 达标任务数」算,低价模型频繁失败再升级反而更贵。
LangGraph:节点契约、共享状态与恢复重放:图框架的价值是把「模型这一步做什么」和「系统何时继续、暂停、重试、结束」拆开。以 StateGraph 为例,节点是读 state、返回部分更新的可调用对象,边和条件边负责调度,节点不需要知道全局后面还有什么。关键在「返回部分更新」这条契约:只返回自己负责的字段,不要把整个旧 state 原样返回,否则并行分支会互相覆盖。字段合并由 reducer 决定,没声明 reducer 的按覆盖语义走,并行写同一个标量就是最后写入者赢;声明了 reducer 的(按稳定 ID 合并、冲突报错)才有可预期语义。checkpointer 支持中断、恢复和时间旅行,但它只保证流程走到哪,不保证外部副作用恰好一次。
共享状态省掉了传一大串参数的样板代码,代价是隐式依赖:节点读写哪些字段、字段何时有效,从签名上看不出来,schema 容易长成什么都往里塞的大对象。并行分支还有合并成本,列表追加会重复;状态越大,序列化、checkpoint 存储和恢复越贵,图片、文档、连接对象、密钥都不该塞进去。版本演进也容易忽略:流程中断几天再恢复,schema 和节点逻辑已经换过两版,旧 checkpoint 会对不上。
最容易踩的坑是「中断恢复导致副作用重复」:节点里先调接口创建工单、再 interrupt() 等审批,恢复时框架从节点开头重新执行,创建动作又跑一遍。修法是把审批挪到副作用之前;给写操作一个稳定的幂等键(业务动作加已确认的参数版本,不要每次重试重新生成随机键),服务端用唯一约束原子记录;超时后不确定成功没有,先查业务状态再决定重发。状态可设计成 PENDING_APPROVAL → APPROVED → RUNNING → SUCCEEDED / FAILED / UNKNOWN,UNKNOWN 表示客户端不知道结果,不能当失败处理。验证靠注入故障:创建成功后 checkpoint 前崩溃、恢复请求重复提交、审批取消、超时但服务端其实成功——确认最后只有一个业务结果。流程可恢复、状态可持久、业务恰好一次是三个不同的问题。