面灵AI→

百度 AIGC 多模态智能体算法工程师二面:工具路由、MCP 与两道手撕

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

《面试题目》

二面

  1. 个人背景与实习经历
  2. 介绍实习
  3. Agent 理解、问题处理与 Tool 定位
    • 如何理解 Agent,开发中会遇到哪些问题
    • 前端手动入口和 Agent Chat 入口如何分工
    • 散功能属于 Skill 还是 Tool
  4. 大规模工具的识别与路由
    • 当前约十个工具时如何识别正确 Tool
    • 扩展到几百或几千个工具时如何保证命中率
  5. MCP 与普通 API 的差异
    • MCP 与普通 API 调用有什么区别
    • 为什么 Agent 生态开始广泛使用 MCP
  6. 长上下文与记忆管理
    • 持续对话导致上下文变长时如何处理
    • 短期上下文和长期摘要如何分层
  7. 多模态与模型后训练经验
  8. 当前实习状态
  9. 到岗时间
  10. 实习地点偏好
  11. Coding:滑动窗口最大值
  12. Coding:和为 K 的连续子数组个数
    • 如何求和为 K 的连续子数组个数
    • 如何从枚举前缀和优化到哈希表
    • 现场实现是否通过检查
  13. Java 与 Go 的使用情况
  14. Go Channel
  15. ArrayList 与 LinkedList
    • ArrayList 和 LinkedList 的底层结构有什么区别
    • 二者查询复杂度和适用场景有什么差异
  16. 反问环节

《参考解析》

  1. 工具从上十个扩到成百上千,怎么保证路由命中:把「把全部工具塞进 prompt 让模型挑」当成不可扩展的起点,然后给分层方案。第一层是工具检索——给每个工具写清名字、描述、参数 schema 与使用场景,向量化后先按用户意图召回 top-k(十几个)候选,再让模型在这十几个里选,这本质上是把推荐/检索的召回-精排思路搬到工具选择上;第二层是分组与优先级——按业务域分组,先做域路由再选工具,并对常用工具做热点直连;第三层是schema 瘦身与消歧——描述要写「什么时候用」和「什么时候别用」,参数枚举化,名称不要同义重复(get_user 与 query_user_info 并存必然选错);最后必须有离线评测集:把真实会话里「意图 → 正确工具」的样本积累成回归集,每次改描述或换模型都跑一遍命中率,否则你无法知道几百个工具下到底掉了多少。可以补一句工程上的兜底:命中不了就问用户澄清或转人工,而不是硬选一个工具乱调。

  2. MCP 与普通 API 的差异,以及生态为什么转向它:普通 API 是人写代码去调的——接口地址、鉴权方式、参数格式每家有各自的约定,接入成本全在客户端。MCP(Model Context Protocol)把「模型怎么发现和调用外部能力」标准化了:服务端声明自己提供哪些工具、资源与提示模板及其 schema,客户端(宿主)用统一协议发现、调用并回传结果,鉴权与生命周期也有统一约定。所以它解决的是 M×N 的适配问题——以前 M 个模型宿主对接 N 个工具要写 M×N 份集成,协议化之后变成 M+N。生态转向它的现实原因是:工具一次开发可以被多个客户端复用;权限、审计、用户确认这些安全能力可以收敛到协议层实现,而不是每个集成各写一遍;以及本地与远程工具可以用同一种方式接入(本地文件、数据库、SaaS 都能挂)。答题时要能补上差异的另一面:MCP 并不改变工具本身的语义,也不解决模型选错工具的问题,传输与鉴权仍要按各自场景设计。

  3. 长上下文与记忆分层:先拆「上下文为什么会变长」——历史对话、工具返回的大块内容、检索到的文档。处理手段分四类:滑动窗口只保留最近 N 轮;滚动摘要把更早的历史压成一段,但摘要只压闲聊和已完成的中间过程,硬约束与关键实体(金额、时间、单号、用户显式要求)永远保留原文;工具返回按相关性裁剪、长文档改走检索而不是整段塞;再给上下文分 token 预算,超预算时按优先级丢最不重要的部分。分层上,短期上下文是当前会话的窗口加摘要,长期记忆是跨会话持久化的用户画像、偏好与历史结论——写入走结构化抽取 + 去重 + 冲突处理(新覆盖旧并保留时间与来源),并设 TTL。判断做得好不好不要凭感觉,要看压缩之后任务成功率有没有下降,所以要留固定用例集做回归。

  4. 滑动窗口最大值:最优解是单调队列,O(n)。做法是维护一个存下标的双端队列,队列里的元素对应值单调递减——遍历时先把队尾所有比当前值小的元素弹出(它们不可能再成为任何窗口的最大值),再把当前下标入队;接着检查队首下标是否已经滑出窗口左边界,是则弹出;当遍历到第 k 个元素之后,队首对应的值就是当前窗口的最大值。两个常见错误:队列里存值而不是下标(无法判断过期),以及在弹出队首时写成 while 跨越了多个窗口(应该是一次判断,因为每次只滑动一格)。也可以用大顶堆做 O(n log n) 的写法,但要主动说明「堆里会有过期元素需要懒删除」,并明确单调队列才是这道题想要的答案。

  5. 和为 K 的连续子数组个数:用前缀和 + 哈希表,O(n)。定义 pre[i] 为前 i 个元素之和,一段 [j+1, i] 的和等于 pre[i] - pre[j],等于 K 就等价于 pre[j] = pre[i] - K。于是遍历时一边累加前缀和,一边在哈希表里查「有多少个历史前缀和等于 当前前缀和 - K」,把个数累加进答案,再把当前前缀和计数加一;初始化时要先放入 {0: 1},代表空前缀,否则以第一个元素开头、和正好为 K 的子数组会被漏掉。这题的加分点在于主动指出「元素含负数,所以不能用滑动窗口」——窗口和不再单调,双指针会失效,这正是它和「和为正数的连续子数组」的区分点。追问「要输出具体子数组」时,把哈希表的值从计数改成下标列表即可。

  6. 集合与并发的延伸题:ArrayList 底层是动态数组,按下标访问 O(1)、尾部插入摊还 O(1)、中间插入删除要搬元素 O(n),扩容按 1.5 倍增长并有拷贝成本,读多写少、需要随机访问时选它;LinkedList 底层是双向链表,头尾插入删除 O(1),但按下标访问要遍历 O(n),而且每个节点多两个指针、缓存局部性差,所以实际项目里「用 LinkedList 做队列」的场景通常会被 ArrayDeque 取代。真正的高频考点是:LinkedList 实现 Deque 才让它勉强有存在感,同时 ArrayList 的扩容与 subList 视图的坑(视图改了会影响原列表、原列表结构变化后视图失效)常被追问。Go Channel 方面要能讲清它是 goroutine 间通信的管道,分无缓冲(发送与接收必须同时就绪,天然同步)与有缓冲(缓冲区满才阻塞),关闭后仍可读剩余数据、读空返回零值与 ok=false,向已关闭的 channel 发送会 panic,以及「用 channel 传数据、用 mutex 保护状态」这条常见的使用边界;select 配合 default 可以实现非阻塞尝试,配合 context.Done() 可以实现超时与取消。