面灵AI→

淘天供应链AI应用研发一面:Skill编排、提效证明与消息幂等

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

《面试题目》

  1. 选一个真实的 bad case,描述一下整个过程:遇到了什么问题?你的 skill 做了什么判断、调到了什么工具、输出了什么?然后你怎么优化的,优化完产生了什么变化?
  2. 不同的 skill 之间,大模型怎么会选哪个 skill?提示词怎么写?
  3. skill 调用什么工具?(答:没有工具,只有 py 脚本)脚本怎么写?
  4. 怎么证明 skill 真的提升了效率?
  5. 怎么做回归测评集?
  6. agent 要调用一些工具或脚本,目前是串行的,怎么判断并且改成并行?
  7. 有什么办法能让 AI 看代码的时候看得更准?代码库规模很大,怎么确认 AI 为什么找不准?
  8. 知识库怎么更新?需要人 review 吗?
  9. 用户提供了一个非常明确的错误,用向量检索反而可能查不到,你觉得是为什么?然后怎么改进?
  10. 消息队列重复消息怎么解决(幂等 id,给出伪代码)?能不能用布隆过滤器?
  11. 同时消费一个消息的时候并发了怎么办?一个消息同时投递到了 A 和 B 两台机器上你怎么办?
  12. 实习与项目拷打。

《参考解析》

多 Skill 之间怎么路由、提示词怎么写:路由不是玄学,是可以工程化的。主流三层:第一层用规则做硬路由(调用来源、用户角色、显式指令),命中就直接进对应 skill,不消耗模型判断;第二层是把 skill 的「名称 + 一句话适用场景 + 一句明确的不适用场景」作为工具清单注入模型,让模型选——提示词的关键不在堆形容词,而在于给出可区分的边界:每个 skill 描述里都要写清「什么时候用我、什么时候不要用我」,因为模型最容易在语义相近的两个 skill 之间选错;第三层是 skill 数量超过几十个之后改用向量召回,给每个 skill 的简介做 embedding,用用户 query 召回 Top-N 再把完整 Schema 注入。提示词里还要有兜底指令:不确定时先澄清而不是硬选,以及选定后要说明理由(便于事后归因)。评价路由好不好,要用一个「query → 正确 skill」的标注集算准确率,并单独看「相近 skill 之间的混淆矩阵」,那才是重灾区。

怎么证明 skill 真的提升了效率:必须给出对照与口径,不能只说「感觉快了」。可用的三类证据:一是时间维度,同一批真实任务在「有 skill 辅助」与「纯人工」两组下的平均耗时、P95 耗时、以及完成率,最好有线上埋点而不是回忆;二是结果质量,错误率、返工率、以及需要人工介入的比例;三是成本维度,单位任务的 token 消耗与模型调用次数。采样上要注意任务难度要可比(按任务类型分层抽样,而不是随便挑几个),样本量要够(几个 case 说明不了问题)。如果实在拿不到对照数据,退而求其次的做法是记录「人工写一段脚本 / 查一次日志」的基线耗时,再和 skill 端到端耗时对比,并把假设明确写在结论旁边。面试官想听的是「你怎么定义提升、怎么排除其他解释」,不是某个百分数。

回归测评集怎么建:核心是分层 + 冻结 + 与线上闭环。分层上按用途分四类:冒烟集(十几条主干路径,秒级反馈,每次改动都跑)、回归集(覆盖历史 bug 与边界条件,一有线上事故就补一条)、对抗集(诱导走错路径、超长输入、参数注入)、以及真实长尾集(从线上采样并脱敏)。每条用例要带三样东西:输入、期望结果的判定方式(能用代码断言的绝不靠模型打分,比如文件是否生成、脚本退出码、字段是否齐全)、以及可接受的耗时与成本上限。冻结的意思是测试集版本化管理,调 prompt 时不能顺手改测试集,否则就是在过拟合;同时要留一部分「只在发布前跑」的封存集来衡量真实泛化。闭环则是每次线上出现 bad case 就回填一条,并定期复盘「哪类问题反复出现」——那类问题往往不是模型能力问题,而是流程或工具设计的缺口。

串行调用怎么判断该改成并行、怎么改:先做依赖分析,把调用序列画成 DAG:有数据依赖的(后一个的入参来自前一个的输出)必须串行,只有控制依赖或完全独立的可以并行。判断手段很直接——看每个步骤的入参里有没有引用前面步骤的输出,没有就是可并行的候选。改造上分两种:如果这些调用是本地脚本,用进程池或异步任务并发执行,注意设置并发上限(别把下游打爆)、单步超时、以及部分失败的处理策略(全部成功才继续,还是允许降级);如果调用的是远程服务,用异步客户端(CompletableFuture、asyncio、Promise.all)并发发起再统一收口,同时对同一个下游加信号量限流。改造后要能报出收益:总耗时从「各步之和」变成「最慢一步 + 汇总开销」,这一步最好有改造前后的实测对比。容易忽略的坑是并发后的结果合并顺序(并行完成顺序不等于业务顺序,要按 key 归位)与幂等性(并发重试同一个写操作可能重复生效)。

怎么让 AI 看代码看得更准:核心结论是「不要指望模型读完整个仓库,要帮它缩小范围」。可行手段有四类。一是检索层:用代码库自己的符号表做精确检索(函数名、类名、调用关系),而不是纯语义向量检索——代码里的标识符是精确匹配最有效的抓手,向量检索适合补充「语义相近但命名不同」的场景,两者混合。二是上下文层:给模型喂的不是整个文件,而是「目标函数 + 它的调用方与被调用方 + 相关类型定义 + 最近的相关提交」,这种基于依赖图的切片(类似给代码做 context 裁剪)能显著提高定位准确率。三是索引层:建调用图与符号索引(tree-sitter、LSP、ctags 都能产出),让「找不准」这件事变成可观测的——记录每次检索的召回结果与最终是否命中,没有这个日志就无法归因。四是任务层:把大任务拆成小问题(先定位再修改),并要求模型给出它依据的文件与行号,人类一眼就能判断它有没有找错地方。判断「为什么找不准」的方法是分层排查:是检索没召回(索引问题)、召回了但排序不对(排序问题)、还是召回正确但上下文给得太少被截断(上下文预算问题)——这三类的修法完全不同。

知识库怎么更新、要不要人 review:按内容的风险等级分档,不要一刀切。低风险、可自动校验的内容(API 文档、代码注释、已知问题清单)可以走全自动管道,但必须有两个硬约束:一是来源可追溯(每块内容带原始出处与更新时间),二是更新要幂等且可回滚(新版本进库后旧版本保留一段时间,出问题能一键回退)。高风险内容(对外口径、财务数字、法务条款、用户隐私相关)必须人工 review,而且 review 要审「变更点」而不是审全文——否则很快就没人认真看了。工程上还要做三件事:定期对知识库做一致性巡检(同一问题在不同文档里答案冲突要报出来)、对久未更新且被高频命中的内容做时效提醒、以及给检索结果带时间戳让模型和用户都知道这是哪个版本的知识。判断「要不要人 review」的标准可以简化为:出错后的代价是否可逆——可逆的自动化,不可逆的人工把关。

用户给了明确报错,向量检索为什么反而查不到:这是向量检索的典型失效模式,原因有好几层。第一,报错文本是「精确符号」(错误码、异常类名、堆栈路径),它的语义信息密度极低但字面要求极高——embedding 模型倾向于把「相似结构」的文本拉近(不同错误码的堆栈长得很像),于是把真正匹配的那条挤出了 Top-K。第二,术语鸿沟:用户复述错误时用的词与知识库原始文档的措辞不一致(用户说「连接超时」,文档写「read timeout after 30s」),纯向量反而比关键词更不稳。第三,长文本稀释:报错被塞进一大段描述里,向量表示被无关内容平均掉。改进方向很明确:混合检索——把错误码、异常类名、文件路径这种精确标识符单独抽出来走关键词或精确匹配通道,向量通道只管语义描述,两路结果用 RRF 融合;同时建一个「错误码/异常类名到文档」的映射索引,命中就直接置顶。另外可以做查询侧处理:从用户输入里抽实体(错误码、模块名)作为结构化过滤条件先缩小范围,再做语义检索。要记住的原则是「精确的归精确、语义的归语义」,把两类信息混在一个向量里必然两头不讨好。

消息重复与幂等(伪代码):重复消息是至少一次投递语义的必然结果,只能靠消费端幂等解决,不能指望 MQ 不重发。

# 方案一:唯一键去重表(强一致,落库)
def consume(msg):
    key = msg.biz_key              # 业务唯一键,如 order_id + 事件类型
    # 在同一个事务里先占位再执行业务
    begin()
    rows = insert_ignore("dedup", key=key, created_at=now())
    if rows == 0:                  # 已经处理过
        commit(); return ACK
    do_business(msg)               # 业务写入与去重记录同事务,保证原子
    commit()
    return ACK

# 方案二:Redis SETNX(高性能,允许极小概率漏判,需配 TTL)
def consume_fast(msg):
    key = "dedup:" + msg.biz_key
    if not redis.set(key, "1", nx=True, ex=7*24*3600):
        return ACK                 # 已处理
    try:
        do_business(msg)
    except Exception:
        redis.delete(key)          # 业务失败要释放,允许重试
        raise
    return ACK

要点:去重键必须来自业务语义(业务 ID + 事件类型),不能用消息 ID——重发时消息 ID 通常会变;去重记录的写入必须和业务写入在同一个事务里,否则会出现「去重记了但业务没做」的漏洞;用 Redis 时 TTL 要覆盖 MQ 的最大重试窗口,且失败要释放占位。

能不能用布隆过滤器:可以,但只适合作为「前置快速拦截」而不是唯一防线。布隆过滤器空间效率高、查询快,适合用来挡住大量历史重复(比如按天滚动的窗口),但它有两个硬伤:一是存在假阳性——判断「存在」时可能是误判,会把正常的新消息误杀,所以它只能用来判断「一定没见过」(返回不存在时才能确信是新的),不能用来确信「见过」;二是无法删除(除非用计数布隆过滤器),而消息去重往往需要按时间窗口滑出。所以生产上的组合是:布隆过滤器挡在前面做快速否定,真正的一致性保证仍落在数据库唯一索引或 Redis 幂等键上。

同一条消息被并发消费怎么办:分两个层面。同一分区内的并发:MQ 通常保证分区内顺序消费,只要不主动并发处理同一分区就不会出现——所以基础解法是「按业务键哈希到同一分区 + 单分区单消费者」。跨分区或跨实例的并发(同一条消息被投到 A 和 B 两台机器):这本质上就是幂等问题,解法是让去重键的操作具有原子性与互斥性——数据库唯一索引的 INSERT ... ON DUPLICATE KEY 或 INSERT IGNORE,天然让先到者成功、后到者影响行数为 0;或者 Redis 的 SETNX 做分布式占位。要注意的是「先查再写」的两步做法在高并发下必然失败(查的时候都没有,然后都去写),一定要用数据库或 Redis 的原子操作。另外还有两个配套设计:处理时间较长的业务可以先占位再异步执行(避免占位锁持有太久),以及给去重记录加状态机(处理中/已完成),避免「占了位但业务崩了」导致消息永久丢失——处理中的记录要有超时回补机制。

一个真实 bad case 该怎么讲:推荐的叙述结构是「现象 → 定位 → 判断依据 → 改动 → 验证」。现象要具体到可复现的输入与错误输出;定位要说明你怎么缩小范围(看哪一段日志、排除了哪些可能),这一步最能体现工程能力;判断依据要落到 skill 内部的具体决策点(是哪一步的检索、哪个工具的返回、还是提示词的某句约束导致了错误选择);改动要说明为什么选这个改法而不是别的(比如是收紧工具边界而不是换模型,因为问题出在工具描述有歧义);验证要有数据——同一批 case 上的错误率变化、以及没有引入新的回归。面试官最在意的往往不是 case 有多难,而是你把「怎么发现的」讲得是否清楚、以及改完之后怎么防止同类问题再发生。