面灵AI→

蚂蚁集团 AI Agent 开发面经:记忆设计、多 Agent 协作与工具调用

时间
2026-10
来源
牛客网

《面试题目》

  1. 介绍自己,重点说明 Agent 相关技术经历。
  2. 挑选一个代表性 Agent 项目,说明业务方案与落地效果。
  3. 说说 Agent 记忆用 Markdown 维护的原因,对比它和 RAG 的差异。
  4. 多 Agent 协作相比 Workflow,优势体现在哪里?
  5. 无人工审核场景,如何保障 Agent 自动执行稳定?
  6. 讲解大模型工具调用原理,以及工具的选型思路。
  7. MCP 是什么,调用失败时重试、降级方案如何设计?
  8. 中心调度 Agent 怎样完成任务拆解、分发与结果汇总?
  9. 说说 KV Cache 在大模型推理中起到的作用。
  10. MySQL 索引优先选用 B+ 树的原因,对比 B 树的区别。
  11. Python GIL 是什么,解释多线程不能跑满多核的原因。
  12. 手撕:基于数组实现栈,实现 O(1) 获取最小值,兼顾线程安全。

《参考解析》

Markdown 记忆与 RAG 的分工:Markdown 维护的是「工作记忆」——目标、计划、已确认的事实、用户偏好、待办,它需要可读、可改、可审计,模型每一轮都能看到完整且一致的当前状态;RAG 维护的是「外部知识」——文档、历史案例、规范,按需检索,不常驻上下文。用 Markdown 的理由是:结构自由、人和模型都能直接读写、diff 友好、出问题能定位是哪一行写错了;缺点是容量有限、需要人工或模型维护一致性、并发写会冲突。RAG 的优点是容量大、更新独立、检索成本可控;缺点是对切分和召回质量敏感,检索不到就等于没有。实践上两者组合:常驻的短记忆用 Markdown/结构化 JSON,长尾知识走 RAG,检索结果只在命中时注入,并在记忆里记下「引用过哪一段」。

多 Agent 与 Workflow 的边界:Workflow 是预定义的 DAG,步骤、分支、失败处理都写死,优点是可控、可测、成本可预测;多 Agent 是让模型动态决定「下一步找谁、做什么」,优点是能处理开放任务和意外情况,缺点是路径不可预测、成本与延迟波动大、调试困难。真正的选型标准是「任务空间是否可枚举」:流程稳定、合规要求高的场景用 Workflow,把模型限制在单个节点的判断上;任务开放、需要探索和反复试错的场景才上多 Agent。很多生产系统是混合的——外层 Workflow 管阶段与审批,内层 Agent 管具体执行。

无人审核下的稳定性:把「稳定」拆成可执行的几层。① 输入输出约束:结构化输出、schema 校验、工具参数校验,非法输出直接重试或降级;② 权限与边界:工具白名单、最小权限、危险操作(转账、删除、发布)强制人工确认或双人复核;③ 幂等与补偿:每个动作带幂等键,失败可重放,副作用可回滚;④ 预算与熔断:最大轮次、token/费用上限、超时、连续失败熔断;⑤ 评估与灰度:离线评测集 + 线上小流量灰度 + 关键指标监控(成功率、人工接管率、平均轮次);⑥ 兜底:模型连续失败时转人工或转规则引擎,且要把失败样本回流成回归用例。核心原则是「模型负责判断,程序负责确定性约束」,不要把安全寄希望于提示词。

工具调用原理与选型:模型本身不能执行代码,工具调用是「模型生成结构化意图 → 运行时校验并执行 → 结果回填上下文」的闭环。请求里带工具的名称、描述、参数 JSON Schema,模型输出 tool_call(名称 + 参数),运行时按 schema 校验、做权限检查、执行、把结果作为一条工具消息追加,再进入下一轮,直到模型给出最终回答。选型看四点:能力是否覆盖真实需求(别为了用工具而用)、输入输出是否稳定可校验、失败是否可观测可重试、权限与成本是否可控。工具描述本身就是提示词,名称和参数要写得让模型不容易选错;工具太多会稀释选择准确率,按场景分组或做二级路由。

MCP 与失败降级:MCP 是模型与外部工具/数据源之间的标准化协议(JSON-RPC 底座,server 暴露 tools、resources、prompts),解决的是「M 个客户端 × N 个工具」的适配问题,让工具实现一次、多个客户端复用。失败处理分四层:超时与重试(只重试幂等操作,指数退避加随机抖动,设置最大次数)、熔断(连续失败打开断路器,避免拖垮上游)、降级(换备用工具、读缓存、返回结构化错误让模型改走别的路径)、可观测(记录工具名、参数摘要、耗时、错误码,按工具统计成功率)。写操作必须带幂等键,重试前先查状态,不能让模型「以为失败了」就重复执行。

中心调度 Agent 的拆解与汇总:先做任务规划——把目标拆成有依赖关系的子任务(可以用计划树或 DAG),标注输入、输出、验收标准;再按能力路由到子 Agent,能并行的并行、有依赖的串行;每个子任务的结果以结构化摘要回传,中心 Agent 负责合并、去重、冲突消解和最终验收。难点在「汇总」:不同子 Agent 可能给出矛盾结论,要有优先级规则(证据强度、来源可信度)或让模型做一次带证据的裁决;还要控制上下文膨胀,子任务只回传结论与引用,不回传全部过程。失败处理要能局部重跑,而不是整条链路重来。

KV Cache:Transformer 推理分 prefill(处理输入、生成 KV)和 decode(逐 token 生成)两阶段。自回归生成时,之前 token 的 Key/Value 在每个注意力层都要重复使用,KV Cache 把它们缓存下来,decode 每步只算新 token 的 K/V,把复杂度从「每步重算整个序列」降到「每步算一个 token」,显著降低延迟和算力。代价是显存占用随序列长度线性增长,所以有 PagedAttention、量化和前缀缓存等优化;多轮对话中相同前缀可以复用缓存,这是提升首 token 延迟的关键手段之一。

B+ 树与 B 树:B+ 树非叶子节点只存索引键、不存数据,同样大小的页能容纳更多键,扇出更大、树更矮,查一次磁盘 I/O 更少;数据全部在叶子节点且叶子之间用链表相连,范围查询和顺序扫描只需顺着叶子链走,不用反复回溯;所有查询路径长度一致,性能稳定。B 树每个节点都存数据,扇出小、树更高,范围查询需要中序遍历、跨节点跳转多。数据库以页为单位读写磁盘,B+ 树的设计正好匹配「减少 I/O 次数 + 支持范围扫描」这两个需求,所以 InnoDB 选它。

Python GIL:CPython 的解释器锁保证同一时刻只有一个线程执行 Python 字节码,所以多线程在 CPU 密集任务上不能跑满多核,只能交替执行。GIL 的存在主要是为了简化 CPython 的内存管理与 C 扩展的线程安全。I/O 密集任务在阻塞时会释放 GIL,多线程仍然有效;CPU 密集要用多进程(multiprocessing)或把热点下沉到 C 扩展/NumPy(它们在计算时释放 GIL)。Python 3.13 起有实验性的 free-threaded 构建,但生态兼容仍需时间。

手撕:O(1) 取最小值的线程安全栈:核心是「辅助栈存当前最小值」,每个元素入栈时把 min(当前值, 当前最小值) 压入辅助栈,出栈时两栈同步弹出,getMin() 直接读辅助栈顶。线程安全最直接是加锁(synchronized 或 ReentrantLock),把 push/pop/getMin 都包住;如果追求更高并发可以用 CAS + 原子引用,但「主栈与辅助栈必须一起更新」这件事用锁更简单可靠。要注意空栈时 getMin() 的行为(抛异常或返回哨兵),以及 Integer 比较要用 equals 而不是 ==。

class MinStack {
    private final Deque<Integer> data = new ArrayDeque<>();
    private final Deque<Integer> mins = new ArrayDeque<>();
    private final ReentrantLock lock = new ReentrantLock();

    public void push(int x) {
        lock.lock();
        try {
            data.push(x);
            mins.push(mins.isEmpty() ? x : Math.min(x, mins.peek()));
        } finally {
            lock.unlock();
        }
    }

    public int pop() {
        lock.lock();
        try {
            if (data.isEmpty()) throw new NoSuchElementException();
            mins.pop();
            return data.pop();
        } finally {
            lock.unlock();
        }
    }

    public int getMin() {
        lock.lock();
        try {
            if (mins.isEmpty()) throw new NoSuchElementException();
            return mins.peek();
        } finally {
            lock.unlock();
        }
    }
}