阿里巴巴 · 淘天

AI 应用开发一面面经

完整信息

27 条问法 · 27 道题

本篇目录

1. 做一下自我介绍

2. RAG 分块策略应该怎么选,真正影响效果的核心因素是什么

分块策略不是越细越好,也不是统一按 token 长度切就行。真正影响效果的是 chunk 是否保留了完整语义单元、是否带有足够的定位信息、是否能在召回后被模型直接利用。像制度文档、规则文档、FAQ、表格和多层级说明,最佳切法差异很大。比较稳的方式通常是结构优先、语义补充、长度兜底,也就是先尊重标题、段落、表格、列表,再在超长片段内部做语义切分。这样做的目标不是生成整齐块,而是让每个 chunk 自带最小可回答上下文。

3. 怎么评估 RAG 的效果,除了准确率还能看什么

RAG 效果不能只看最终回答对不对,因为那样很难归因。一般要拆成召回层、重排层和生成层分别评估。召回层看 recall@k、命中覆盖率、是否把关键证据找回来;重排层看关键 chunk 是否能稳定进入前几位;生成层则看引用准确率、幻觉率、答案完整性和用户采纳率。线上还要关注延迟、token 成本、缓存命中率和 bad case 类型分布,不然系统可能答得准,但慢得不可用,或者成本高得不可持续。

4. 开发 RAG 系统时你遇到过最难处理的问题是什么

最难的问题通常不是 embedding 选型,而是知识源本身不干净。比如同一个规则有多个版本、历史文档互相冲突、标题命名不统一、表格和正文语义断裂,这种情况下召回出来的东西看起来都相关,但模型很容易拼出一个貌似合理却不可靠的答案。后面一般要补版本治理、文档标准化、引用约束和冲突检测,而不是继续盲调 topK 或 prompt。RAG 系统做深了之后,核心问题经常会从“模型够不够强”转成“知识底座是否可治理”。

5. MySQL 的索引为什么普遍用 B+ 树,而不是跳表、红黑树或者哈希结构

数据库索引选 B+ 树,本质上是磁盘和页访问模型决定的。红黑树虽然查找复杂度也是对数级,但树高更高,磁盘随机 IO 会明显更多;哈希适合等值查询,却不适合范围查询和排序;跳表虽然在内存里表现不错,但在数据库页组织和磁盘预读模型下没有 B+ 树稳定。B+ 树的优势在于非叶子节点只存键和指针,单页能容纳更多分支,树高更低,叶子节点还天然有序,范围扫描和顺序读都更友好。数据库索引设计真正看重的是综合访问成本,而不是某个理论查找复杂度。

6. 操作系统里的堆和栈如果放到工程问题里理解,最值得讲的点是什么

最值得讲的不是“栈向下增长、堆向上增长”这类概念,而是它们背后的分配模型和性能语义。栈由编译器和调用约定管理,分配释放都很快,适合短生命周期、大小相对确定的数据;堆更灵活,但需要显式或隐式管理,容易带来碎片、逃逸、GC 压力或锁竞争。真实系统里很多性能问题和内存问题,本质上都能追到对象分配方式,比如频繁创建大对象导致 GC 抖动,或者线程栈过大导致进程能开的线程数受限。理解堆栈的关键是它如何影响时延、并发和资源边界。

7. 常见排序算法如果不想答成背诵题,应该怎么讲

排序算法更适合从稳定性、最坏复杂度、空间复杂度和适用场景来讲。比如快排平均快但最坏会退化,不稳定,适合一般内存排序;归并稳定,适合链表和外部排序,但需要额外空间;堆排最坏复杂度稳定,但缓存局部性差,常数也不小;插入排序在近乎有序的小数据集上反而很好用。面试里真正能拉开差距的是你知道这些算法为什么在工程里会被组合使用,比如 Java 和 C++ 标准库为什么不会只押宝一种排序。

8. HashMap 的底层实现原理如果往深了问,你会怎么回答

HashMap 的核心不只是数组加链表或红黑树,而是如何在时间、空间和冲突概率之间做平衡。put 时先通过 hash 扰动减少高位信息浪费,再定位桶位;桶内冲突少时用链表,冲突严重且达到阈值时树化,避免极端退化;扩容时通过容量翻倍和位运算优化重定位,减少重新计算成本。真正值得讲的是为什么容量最好是 2 的幂、为什么树化前还有最小容量限制、为什么高并发下不能直接拿 HashMap 共享用。这些点比“数组链表红黑树”更像真正理解底层。

int index = (n - 1) & hash;

9. 做多智能体项目的背景一般是什么,为什么很多系统最后还是回到单 Agent 加工具

多智能体通常出现在任务天然可分工的场景,比如规划、检索、执行、审计是不同职责,而且每一步都需要不同的上下文和约束。它的价值在于把复杂任务拆成可观测、可验证的子流程,而不是让一个 Agent 在长链路里自由发挥。但很多系统最后回到单 Agent 加工具,是因为多智能体会显著增加上下文传递成本、状态管理复杂度和调试难度。如果任务本身并没有明确角色边界,多智能体反而只是把一个问题拆成多个不稳定问题。

10. AI Coding 里让你写一个 Skill 来解决业务需求,你会怎么设计

Skill 本质上不是一段 prompt,而是一个带能力边界、输入输出协议和失败语义的执行单元。设计时我通常先收窄职责,只做一件可验证的事,比如“生成接口测试样例”或者“扫描仓库里某类风险调用”。然后定义严格的输入 schema、结果格式、超时策略、错误码和回滚方式,最后再决定内部要不要调用模型、规则引擎或代码检索。这样 Skill 才能接到真实工作流里,而不是停留在一次性问答。

{
  "name": "generate_api_testcases",
  "input": {
    "apiSpec": "OpenAPI内容",
    "riskLevel": "high"
  },
  "output": {
    "cases": []
  }
}

11. 一个 Skill 写完之后,通常应该怎么优化,而不是只改 prompt

优化 Skill 不能只盯 prompt wording,更关键的是把不稳定来源拆开。先看输入是不是太宽,是否需要做预处理和参数标准化;再看是否有可规则化的部分,能用规则就不要全交给模型;然后看输出是否可校验,能不能加 schema 校验、引用检查、静态规则或回归测试。很多 Skill 表现不稳定,本质上是职责过大、上下文污染或输出缺乏约束,不是模型“今天状态不好”。

12. 如果让你聊一下 base 地以及开发方向选择,你会怎么回答得更像工程师

这个问题不适合答成“哪里都可以”。更合理的讲法是把地点选择和业务密度、技术复杂度、成长曲线联系起来。如果某个 base 的业务更贴近核心链路、技术治理更重、系统复杂度更高,那对后端和 AI 应用落地都会更有锻炼价值。开发方向上,我更倾向于能同时接触系统基础设施和应用落地的岗位,因为只做上层编排会缺底层稳定性视角,只做底层又容易离真实业务效果太远。这样回答会显得你是在考虑成长路径,而不是随意投递。

13. 如果一个 RAG 系统召回看起来还行,但最终回答总是答偏,最可能的问题在哪

这种情况很多时候不是召回错了,而是上下文构造错了。比如召回回来的 chunk 顺序不合理、不同版本文档混在一起、关键证据被长噪声淹没、引用粒度太粗,模型虽然“看到了”正确信息,但实际上无法稳定利用。另一个常见问题是重排目标和最终回答目标不一致,重排可能更偏语义相关,但回答需要的是强证据和高精度约束。真正排查时不能只看 topK 命中,而要检查模型实际吃进去的上下文长什么样。

14. 如果知识库中文档频繁更新,你会怎么处理索引更新和查询一致性

比较稳的方式是增量更新加版本隔离。先对文档结构做 diff,只重建受影响的 chunk 和向量;再用版本号或影子索引隔离新旧数据,等新版本索引构建完成后再切流。查询时要保证同一次请求看到的是同一版本数据,不能一半查到旧 chunk,一半查到新 chunk。真正难点不在“怎么更新 embedding”,而在如何控制更新延迟、构建成本和查询期的一致性。

class IndexDoc {
    String docId;
    long version;
    String chunkId;
    float[] vector;
}

15. 混合检索里,为什么很多时候不是“召回越多越好”

召回越多并不一定更好,因为噪声会跟着一起放大。尤其在长上下文场景下,模型并不是对所有召回内容一视同仁,靠后的内容很可能被忽略,而前面如果塞进太多弱相关片段,反而会挤掉关键证据。混合检索真正要解决的是“在有限上下文里放入最有决策价值的信息”,而不是机械地堆 topK。工程上更常见的做法是多路召回后精排,再结合 chunk 合并、去重和证据覆盖做上下文组装。

16. 为什么有些业务明明用了缓存,系统还是扛不住高并发

因为缓存只能解决一部分读压力,不能自动解决热点倾斜、穿透、雪崩和一致性问题。比如一个热点 key 命中虽高,但所有流量都打在单个节点上,仍然可能把 Redis 或网络打满;缓存失效瞬间大量请求回源,会把数据库击穿;还有些链路虽然命中缓存,但后面仍然要调多个下游,整体 RT 依旧上不去。真正能扛住高并发的是缓存、限流、异步化、预热和数据模型一起配合,而不是“上 Redis 就完了”。

17. 如果一个系统里既有数据库事务,又有消息通知,你怎么保证状态一致性

比较常见的工程解法是本地消息表或者事务消息。核心思路是不要把“更新业务数据”和“发消息”分成两个彼此独立、没有约束的步骤,否则任何一步失败都会造成状态断层。本地消息表的好处是通用,业务数据和待发消息可以落在同一个事务里,后续异步扫描发送;事务消息则更实时,但依赖中间件能力。真正上线时,一致性方案要和幂等、重试和补偿配套使用,否则只是把问题延后。

18. 如果一个多智能体系统开始频繁出现工具误调、死循环和结果漂移,你会先改哪里

我不会先改模型,而是先收紧执行边界。先把工具注册做白名单和参数 schema 校验,再把代理之间的状态流转显式化,限制每个步骤允许做的事;然后补最大调用步数、超时和人工接管条件,避免无限循环。对于结果漂移,还要检查中间结果是不是自然语言太多,最好改成结构化传递。多智能体系统真正稳定下来,靠的通常不是更强模型,而是更严格的流程约束。

19. 为什么数据库索引命中了,SQL 还是可能很慢

命中索引不等于访问成本低。可能是索引选择性很差,虽然走了索引,但回表了大量数据;也可能是 where 用上了索引,order by 或 group by 却额外触发 filesort 和 temporary;还有可能是联合索引只利用了前缀部分,后续过滤仍然代价很高。线上经常见到 explain 里 key 不为空,开发就觉得没问题了,但真正影响性能的是扫描行数、回表次数和额外排序聚合成本。

20. 如果让你设计一个可观测性体系来排查 AI 应用线上问题,你会优先打哪些点

我会优先把请求级 trace、检索命中明细、模型输入输出摘要、工具调用日志和版本信息打通。因为 AI 应用问题大多不是单点错误,而是召回、重排、上下文组装、模型调用、工具执行中某一环偏了。没有链路级日志,只看最终答案根本无法归因。除此之外,还要记录 token 消耗、缓存命中率、超时原因、重试次数和 bad case 样本,不然只能知道“今天效果变差了”,但不知道到底是模型换了、知识库变了还是流量形态变了。

21. 如果一个 Skill 很耗 token,你会怎么降本,而不是直接砍上下文

降本不能粗暴删上下文,因为很多时候删掉的是关键信息。更合理的是先做任务拆分,把不需要模型参与的部分规则化;再做上下文压缩,比如结构摘要、字段抽取、去重和引用编号化;然后复用中间结果,把稳定说明和工具定义外置,不要每次重复输入。最后再看模型分层,大任务用强模型,小任务用便宜模型。真正大的 token 开销,往往不是业务内容本身,而是重复描述和低效组织。

22. 解释一下为什么“向量检索效果不好”这句话通常不成立

因为“效果不好”不是一个单点问题。可能是分块就错了,embedding 模型和领域不匹配,query 改写不对,过滤条件过强,ANN 参数过激进,重排没配好,甚至最后的上下文组装把好结果冲掉了。向量检索只是整个链路里的一段,把所有问题都归到它头上往往不准确。真正排查这类问题,一定是逐层验证:chunk 合不合理、向量是否区分度足够、召回列表里有没有正确证据、重排是否提升还是破坏、模型最终有没有正确消费这些证据。

23. 算法题:给定一个数组,返回滑动窗口中的最大值

这题核心是单调队列。队列里存下标,并且保持对应值单调递减,这样队首永远是当前窗口最大值。每次右移窗口时,先弹出所有比当前值小的元素,再把当前下标加入队尾;同时如果队首已经滑出窗口,就把它移除。这样每个元素最多进出队列一次,整体复杂度是 O(n)。

public int[] maxSlidingWindow(int[] nums, int k) {
    if (nums == null || nums.length == 0) return new int[0];
    Deque<Integer> deque = new ArrayDeque<>();
    int[] ans = new int[nums.length - k + 1];
    int idx = 0;

    for (int i = 0; i < nums.length; i++) {
        while (!deque.isEmpty() && deque.peekFirst() <= i - k) {
            deque.pollFirst();
        }
        while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
            deque.pollLast();
        }
        deque.offerLast(i);
        if (i >= k - 1) {
            ans[idx++] = nums[deque.peekFirst()];
        }
    }
    return ans;
}

24. 算法题:设计一个支持 O(1) 获取最小值的栈

这题通常是主栈加辅助栈。主栈正常存元素,辅助栈同步维护到当前位置为止的最小值。push 时如果当前元素小于等于辅助栈栈顶,就一起压入;pop 时如果弹出的值等于辅助栈栈顶,也要同步弹出。这样 getMin 直接看辅助栈栈顶即可,所有操作都是 O(1)。

class MinStack {
    private Deque<Integer> stack = new ArrayDeque<>();
    private Deque<Integer> minStack = new ArrayDeque<>();

    public void push(int val) {
        stack.push(val);
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        }
    }

    public void pop() {
        int x = stack.pop();
        if (x == minStack.peek()) {
            minStack.pop();
        }
    }

    public int top() {
        return stack.peek();
    }

    public int getMin() {
        return minStack.peek();
    }
}

25. 算法题:二叉树的最近公共祖先怎么做

如果是普通二叉树,最经典的做法是递归。当前节点如果为空,或者等于 p、q 之一,直接返回当前节点;递归查左右子树,如果左右都非空,说明 p 和 q 分布在两边,当前节点就是最近公共祖先;如果只有一边非空,就把那一边返回。这个解法的关键是利用“自底向上汇总信息”,时间复杂度是 O(n)。

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) return root;
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    if (left != null && right != null) return root;
    return left != null ? left : right;
}

26. 反问

本篇目录
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06
    操作系统里的堆和栈如果放到工程问题里理解,最值得讲的点是什么
  7. 07
  8. 08
  9. 09
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
    如果一个 RAG 系统召回看起来还行,但最终回答总是答偏,最可能的问题在哪
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27

同作者面经

展开其余 7 篇收起其余 7 篇

相关公司