快手 C++开发 二面:哪些 Agent 步骤不该走 MQ
- 轮次
- 二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 自我介绍。
- 你这个项目是网上找的吗?
- 在京东干了点什么?
- 主流程通过 MQ 触发,这里为什么不也这么做?
- C++ 中 Reactor 和 Proactor 模型的区别是什么?为什么 Linux 高并发服务普遍采用 Reactor?
- 有没有参考其它 Agent 做法?
- 如果 MQ 消费者处理成功,但 ACK 失败,怎么避免同一个 Agent 任务重复执行?
- 这个 Agent 技术方案上是怎么设计的?
- C++ 线程池如何设计任务队列?在高并发场景下怎么避免锁竞争和任务堆积?
- 上线效果如何?有没有什么评测体系?
《参考解析》
不能因为主流程用了 MQ,就要求所有步骤都走 MQ。 消息队列适合耗时较长、允许排队、能独立重试的任务,比如离线文档索引、批量 Embedding、日志处理;这些工作延迟几秒不影响用户体验。但 Agent 的很多子步骤是强依赖的:模型决定调用检索工具后,必须拿到结果才能继续生成,如果每个短步骤都经过 MQ,反而增加序列化、网络传输、Broker 排队和 Consumer 调度的开销。判断标准可以写成两句话:低延迟、强依赖的内部调用走同步;跨生命周期、需要缓冲或独立恢复的执行边界走 MQ。还要注意 MQ 不天然保证数据库状态与消息投递的原子性,任务记录已提交但消息发送失败,就会出现「有任务没人执行」,这类场景要用 Transactional Outbox,在同一事务里写任务和待发事件,再由独立发布器投递。
Reactor 和 Proactor 的区别在「通知什么」。 Reactor 是就绪通知:内核告诉应用某个文件描述符可读或可写,应用再主动调用 read/write 完成数据传输,Linux 下基于 epoll 的服务器通常就是这种模型,epoll_wait 返回事件后交给 Event Handler 做非阻塞读写。Proactor 是完成通知:应用提前提交 I/O 请求,底层完成操作后再通知应用处理结果。关键差别是 Reactor 通知「可以做」,Proactor 通知「已经做完」。Linux 的 io_uring 可以支持更接近完成驱动的异步编程,但具体行为取决于操作类型和内核实现,不能简单理解为所有操作都完全异步。epoll 被广泛采用,是因为接口成熟、生态完善、能高效管理大量连接,避免 select 每次扫描整个集合;但 epoll 本身不解决业务回调阻塞,某个回调里做耗时数据库查询,照样会卡住 Event Loop,所以工程上还要划分 I/O 线程和业务线程。
ACK 丢失是至少一次投递的必然结果,解法是让消费幂等而不是期待只投一次。 消费者处理完业务后 ACK 丢失,Broker 会认为没消费成功并重新投递;如果任务会创建资源或调用收费接口,重复执行就有真实副作用。做法是给每个 Agent 任务分配全局唯一 Task ID,执行器先查持久化状态,已完成的任务直接确认、不再执行;状态推进用条件更新,比如 UPDATE agent_task SET status = 'RUNNING' WHERE task_id = ? AND status = 'PENDING',通过受影响行数判断当前 Worker 是否抢到执行权。只有 RUNNING 还不够,Worker 可能执行到一半崩溃,所以还要租约、超时恢复、重试次数上限和执行版本号,防止旧 Worker 恢复后覆盖新结果。对外部有副作用的工具调用,把 Task ID 和 Step ID 组合成幂等键,让下游一起参与去重;对账和补偿也要有,因为 ACK 与业务执行本来就不能简单等价。
Agent 技术方案的核心是把 Planner、Executor 和 State Store 解耦。 Planner 只根据用户目标、当前状态和工具返回决定下一步;Executor 负责真实工具调用;State Store 保存任务状态和执行历史。这样模型不直接持有数据库连接,也不拥有任意执行系统命令的能力。工具层用统一的 JSON Schema 描述参数类型、必填字段和允许值,模型返回的 Tool Call 必须通过校验才进入执行器;执行器里再放限流、重试、超时和权限控制。无副作用的查询工具可以按错误类型自动重试,修改数据的工具必须用幂等键或人工确认。上下文也不能无限追加历史,系统要按任务状态裁剪,保留目标、关键决策和当前结果,避免工具返回的大量原始数据占满窗口。评估时不能只看回答质量,还要看工具调用成功率、任务完成率、平均步数和单位任务成本。
线程池真正危险的问题往往不是锁,而是无界队列。 简单线程池是固定 Worker 加一个共享队列,提交任务时加锁入队、唤醒消费者;高并发下共享队列会成为竞争热点,可以用每线程本地队列加 Work Stealing,空闲 Worker 从别人队列偷任务,减少集中式锁竞争并改善负载均衡。但任务到达速率长期高于处理速率时,积压会持续增加,加线程也不一定有效,因为瓶颈可能在数据库连接池或下游模型接口。工程上要用有界队列、明确的拒绝策略和任务优先级,并隔离 I/O 密集与 CPU 密集任务;还要避免在线程池内部同步等待同一个线程池的其他任务,Worker 数量有限时这种嵌套等待会导致线程饥饿死锁。
上线评测要区分离线、灰度和线上三层。 离线构造覆盖真实意图的数据集,包含单次检索、多工具组合、信息不足、工具超时和错误恢复等场景;每条样本不只有问题,还要有预期引用文档、允许调用的工具和结果约束。线上关注任务成功率、有效引用率、人工负反馈率、P95 延迟、工具错误率和单位任务成本;一个回答读起来流畅但引用了错误版本的接口文档,不能算成功。发布时灰度扩量,与基线版本比较,并区分模型、提示词、检索策略各自的影响。LLM Judge 可以做粗筛,但要用人工标注校准,报告一致率和置信区间,否则评测本身会变成新的误差来源。