阿里巴巴淘天AI应用研发一面:Skill/MCP/Harness与缓存一致性
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 手撕一:最长有效括号。
- 手撕二:岛屿数量,先用 DFS 再用 BFS 各写一遍。(追问:矩阵很大时 DFS 递归会不会栈溢出?)
- 做一下自我介绍,重点讲一个你主导的 Agent 项目。
- 你这个 CR Agent 具体解决什么问题?
- 为什么用 Agent,不用固定 Workflow?
- Workflow 和自治 Agent 的区别是什么?什么场景更适合 Workflow?
- ReAct 和 Plan-and-Execute 怎么选?
- Skill、MCP、Function Calling 三者区别是什么?
- 你项目里的 Skill 怎么设计?输入输出契约是什么?
- Skill 怎么渐进式加载?工具特别多怎么办?
- MCP 获取工具列表的完整流程讲一下。
- 如果有 100 个 MCP 工具,Agent 怎么选?怎么保证稳定性?
- 你怎么理解 Harness?Harness 和 Agent 框架的关系是什么?
- 看过 DeepSeek Harness 吗?和 Claude Code 有什么区别?
- Agent 上下文超限怎么处理?滑动窗口和摘要压缩各自牺牲了什么?
- 工具调用失败怎么兜底?重试策略怎么设计?
- 多 Agent 写同一个文件怎么避免冲突?
- Agent 长任务跑失败,怎么定位是模型、检索还是工具的问题?
- 长任务从 5 分钟变成 20 分钟,怎么分析延迟?
- 线上高并发场景,Agent 服务怎么限流、降级、熔断?
- RAG 召回高但答案不准,怎么排查?
- 文档切分策略怎么选?Markdown 没标题怎么切?
- BM25 和向量检索怎么融合?RRF 的 k 值怎么设?
- Java 线程池核心参数怎么配?IO 密集和 CPU 密集分别怎么设?
- ThreadLocal 原理,为什么会内存泄漏?
- MySQL 深分页为什么越往后越慢?怎么优化?
- Redis 缓存击穿、穿透、雪崩分别怎么解决?
- 先更新数据库再删缓存,删缓存失败了怎么办?
- 如果消息队列重试删除缓存,但旧值又写回来了,怎么保证一致性?
- AI Coding:写一个商品计价系统,优惠策略可以随时更新,你会怎么设计?
《参考解析》
最长有效括号:栈做法最稳。栈底先压一个 −1 当哨兵,遍历字符串:遇到 ( 就把下标入栈;遇到 ) 先弹栈,如果此时栈空说明这个右括号没得匹配,把当前下标压进去当作新的哨兵;栈不空时,当前下标减栈顶就是「以当前字符结尾的最长有效长度」,全程取最大。也可以用 DP:dp[i] 表示以 i 结尾的最长有效括号长度,s[i] 是 ) 时看 s[i-1]:若是 ( 则 dp[i] = dp[i-2] + 2;若是 ) 则先跳到 i - dp[i-1] - 1,那里是 ( 才能接上,dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2]。两种都是 O(n) 时间,栈版 O(n) 空间、DP 版 O(n) 空间但常数更大。
岛屿数量:遍历矩阵,遇到 '1' 就把岛屿计数加一,然后把这整片连通区域「淹掉」(改成 '0')。DFS 用递归向四方向扩散,代码短但递归深度等于连通块大小,超大矩阵会栈溢出,所以生产上更稳的是 BFS 队列或显式栈的迭代式 DFS。BFS 把起点入队后层序扩散,同样在出队时把 '0' 写回以防重复入队。两者都是 O(mn)。网格类题目要注意边界判断写在扩散前,而不是在递归入口——后者会因为越界访问直接崩。
Skill、MCP、Function Calling 的分工:Function Calling 是模型侧能力,模型输出结构化的调用意图(工具名 + 参数),由运行时解析并执行,它不解决工具怎么被发现和管理。MCP 是工具集成的标准化协议,规定工具如何暴露、如何列举、如何调用、参数如何描述,价值在于一个 Server 写一次、所有支持 MCP 的客户端都能用,把 M×N 的适配矩阵降成 M+N。Skill 是面向业务场景的能力封装,一个 Skill 可以编排多个 Tool 调用,再叠加 prompt 模板、业务规则与前后处理——「查天气」是 Tool,「规划行程」是 Skill。三者的层次关系是:Function Calling 是底层机制,MCP 是接入标准,Skill 是业务封装。答题时能把这层关系讲清,比背概念名词更得分。
Skill 契约与渐进式加载:契约至少要含 name、description、inputSchema、execute 四件套,其中 inputSchema 用 JSON Schema 写清参数名、类型、是否必填、枚举取值,输出统一成 {status, data, error, metadata} 的形状,再加超时与重试配置。契约的核心要求是稳定:一旦发布参数名就不能随意改,要改就带版本号并保留旧参数的兼容映射——把 query 重命名成 search_query 这类改动会把所有调用方一起打挂。工具多了以后不能把全部 Schema 塞进 prompt:第一层只加载 Skill 索引(名称 + 一句话描述,控制在几千 token),模型判断需要哪类之后再按需加载该类下完整 Schema;再往上可以给每个 Skill 的简介做 embedding,用 query 做向量召回只加载 Top‑20,一千个工具也只需几千 token 的上下文。这是「工具规模上去后 prompt 装不下」的标准解法。
100 个 MCP 工具怎么选:分层路由。第一层按类别粗筛(检索类、数据库类、通知类……),第二层在候选类别内用向量相似度对工具描述做精排,取 Top‑5 加载完整描述。稳定性靠三道校验:调用前做参数校验,格式或枚举不合法直接拦截,不进模型循环;调用后做结果校验,返回异常结构就重试或切换备选工具;再加工具级熔断,某个工具连续失败就临时摘掉,避免它反复把整个 Agent 拖进超时。全量加载 100 个工具会使 prompt 爆炸并显著降低选对率,分层路由通常能把工具选择准确率从六七成提到九成。
Harness 与 Agent 框架:Harness 指 Agent 的运行时,管的是执行循环、工具注册与调用、会话与状态管理、异常处理、上下文拼装这一层「怎么跑起来」。Agent 框架的外延更大,往往等于 Harness 加开发脚手架加部署工具链。两者是包含关系而不是同义词。观察 Harness 这层是否成熟,看它能不能替换模型适配器、工具注册表、会话存储这些组件而不改动业务代码——插件化程度决定了换模型、换工具集要付多少代价。
上下文超限的两条路:滑动窗口保留最近 N 轮,实现简单、延迟低,代价是丢早期信息,长任务里早期的关键约束会被挤掉;摘要压缩把历史压成摘要,能保住主线,代价是细节丢失且多一次生成开销,而「摘要把关键数字压没了」是真实事故。工程上的折中是结构化摘要——把摘要拆成实体、事件、结论、待办四类字段分别存,实体和结论保留原文而不是转述,事件与待办用短句概括,检索时按需回捞原文。另外长 query 本身也要走摘要或分段检索,否则单条输入就能把窗口吃掉。
多 Agent 写同一文件:可选分布式锁或乐观锁。Agent 场景里更推荐乐观锁:文件带版本号,写之前校验版本,不匹配就重新读取后重试(限定重试次数)。理由是分布式锁怕持锁方挂掉导致死锁,需要额外的锁超时与续期机制,而 Agent 的失败率本来就高。更细的粒度是按区域分段加锁,让多个 Agent 改同一文件的不同片段。文件系统之外还要注意「同一份状态被两个 Agent 分别写回」的问题,那本质上是并发写覆盖,靠版本号或串行化写通道解决。
长任务失败的归因:靠分段日志加决策链路追踪。每个步骤记四件事:输入状态、决策内容、备选方案、最终选择,并打上统一的 trace_id。复盘时按 trace_id 回放,如果模型的 Thought 本身不合理,是模型或提示的问题;如果检索回来的片段与问题不相关,是检索侧的问题;如果工具返回错误或脏数据,是工具侧的问题——最后一种最隐蔽,模型基于脏数据做出「看起来合理」的错误决策,光看最终输出根本查不出来,所以工具结果必须先校验再进上下文。
延迟劣化的排查:先分段计时,把任务拆成模型调用、检索、工具、上下文构建四段,各记 P50/P95/P99,瓶颈立刻显形。常见根因按频率排:上下文越来越长导致推理时间线性增长(最常见);上游模型限流或模型版本切换导致单次调用变慢;向量库索引膨胀导致检索变慢;下游被调服务自己慢。前两者的解法是上下文压缩与请求级的超时/降级,后两者要靠缓存与容量治理。
RAG 召回高但答案不准:说明检索这一环基本达标,问题在上下文构建或生成。按顺序排查:一,看 Rerank 后的 Top‑K 里到底有没有正确答案——有时候是精排把对的排掉了;二,看上下文拼接顺序,关键片段是否落在中段被 Lost in the Middle 吃掉,把最相关的挪到首尾;三,看 prompt 模板有没有明确「只依据给定资料作答、无依据要说明」;四,换模型或调低温度对比;五,看多篇召回内容是否存在互相冲突的说法,有冲突就要加冲突检测或时间/来源优先级。经验的判据是:如果人在同样这几段资料下能答对而模型答错,那就是生成侧问题;如果人看了也答不出,那还是检索质量问题,别在 prompt 上白费功夫。
线程池参数:核心线程数按任务性质定——CPU 密集型取「核数 + 1」,IO 密集型取「核数 × 2」起步(具体还要看阻塞系数,即 核数 × (1 + 等待时间/计算时间));最大线程数一般取核心的 2~3 倍;队列必须有界,无界队列会在流量尖峰时堆到 OOM;拒绝策略优先 CallerRunsPolicy,让提交任务的线程自己执行形成背压,比直接丢弃更安全。线程池不能各业务各建一个,要按业务隔离并统一监控活跃线程数、队列长度、拒绝次数。
ThreadLocal 内存泄漏:每个线程内部有一个 ThreadLocalMap,key 是 ThreadLocal 对象的弱引用,value 是强引用。线程池复用线程时,如果 ThreadLocal 用完不 remove,key 会被 GC 回收成 null,但 value 仍被 Entry 强引用着,于是这块内存永远无法回收,堆积起来就是 OOM。解法只有一个:在 finally 里显式 remove,通常用 Filter 或拦截器统一做。用 InheritableThreadLocal 或线程池传递上下文时尤其要小心,因为线程生命周期远超单次请求。
MySQL 深分页:LIMIT 100000, 10 的语义是先取出前 100010 行再丢掉前 100000 行,扫描量随 offset 线性增长,所以越翻越慢,而且只有当查询本身能走索引、需要在索引上回表时这个代价才最明显。三条优化路径:游标分页——记住上一页最后一条的主键,下一页用 WHERE id > last_id ORDER BY id LIMIT 10,把 O(offset) 变成 O(1);覆盖索引——先只查主键再回表,减少回表次数;业务上限制最大可翻页数,大多数列表页用户根本翻不到那么深。
缓存穿透、击穿、雪崩:穿透是查一个数据库里也不存在的 key,请求每次都穿过缓存打到库上;解法是布隆过滤器提前拦截,或把空结果也缓存起来并设很短的 TTL。击穿是某个热点 key 恰好失效的瞬间,大量并发同时打到库上;解法是互斥锁(只放一个线程去回源,其余等待)或逻辑过期(不设物理 TTL,value 里带逻辑过期时间,过期后异步刷新,读到的仍是旧值)。雪崩是大量 key 同时失效或缓存集群整体故障;解法是给过期时间加随机抖动打散、缓存集群做高可用、本地缓存兜底、入口限流熔断。三者的共同后果都是数据库压力骤增,但成因不同,解法不能互换——拿雪崩的方案去治穿透(加随机过期)完全无效。
删缓存失败与旧值回写:删缓存失败会让缓存长期停留旧值,补偿手段按可靠性排序是:订阅 binlog 在数据变更后触发删除(最彻底,不依赖应用层重试成功)、消息队列异步重试删除、设短 TTL 兜底。更难的是并发写入场景:线程 A 更新了库,线程 B 在此之前读到旧值并在 A 删缓存之后把旧值写回缓存,结果缓存里是旧值而库里是新值。对策是延迟双删(更新库后删一次,延迟数百毫秒再删一次,盖住这个窗口)、缓存值带版本号(写回时校验版本,旧版本直接丢弃)、或让同一数据的读写走同一队列串行化。要说明的边界是:这些都只能把不一致窗口压到毫秒级,做不到强一致;要强一致就得上分布式锁或干脆不缓存,代价是吞吐。
商品计价系统的策略化设计:用策略模式加配置化。每种优惠规则实现统一接口(适用条件、计算方式、优先级、是否可叠加),策略的元数据与参数存数据库或配置中心,运行时动态加载;计价流程是「基础价 → 按优先级依次应用策略链 → 输出最终价」,叠加与互斥关系由规则引擎控制,避免把互斥逻辑写死在各个策略里。关键约束是钱的计算不能浮点:金额一律用整数分或 BigDecimal,每步都要有精度舍入策略;策略变更要有生效时间与灰度,改完必须能回放历史订单做回归,否则运营改一条规则就可能全站多打或者少收钱。AI 可以帮忙生成策略类骨架,但边界条件与金额校验必须人来定。