快手 C++开发 三面:P99 抖动定位与多工具并发聚合
- 轮次
- 三面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 自我介绍。
- 讲一下这个 Agent 项目。
- 主要看哪些指标?
- 如果 Agent 的 P99 延迟突然上涨,但 CPU、内存都正常,你会怎么定位?
- 怎么去找 case 看的?
- 如果 Agent 需要同时调用多个工具,怎么设计并发执行和结果聚合?
- 做的时候遇到什么难点,怎么处理的?
- C++ 高并发服务中,为什么使用 shared_ptr 仍然可能发生数据竞争和性能问题?
《参考解析》
项目介绍要按「问题—链路—取舍—结果」讲,而不是报技术栈。 面向企业内部研发的知识检索 Agent,解决的是接口文档、历史故障和技术方案散落在多个系统、研发反复切换平台的问题。链路可以概括成:请求进来先做会话与权限校验,Planner 判断要调用哪些工具,Executor 做参数校验、权限、超时和结果处理,检索与数据库查询并行或串行执行,最后聚合结果交给模型生成带引用的回答。技术选型上,C++17 加 Drogon 承担核心后端,MySQL 存会话、任务状态和工具调用记录,Redis 缓存热点文档元数据和短期上下文,知识库服务提供 Embedding、向量检索与重排;长任务通过 MQ 交给 Worker,低延迟强依赖的检索保留同步调用。真正值得讲的是为什么这样切:哪些步骤必须同步、状态为什么要持久化、引用为什么要单独建模。
指标要分业务、链路、成本三层。 业务层看任务成功率、有效引用率、人工负反馈率、用户是否继续追问或复制答案;链路层看各阶段 P95/P99、工具调用成功率、超时率、重试率、检索命中率、模型首 token 与总生成时间;成本层看单位任务 token、模型调用次数和缓存命中率。指标要能按入口、租户、模型版本、知识库版本分层,否则平均值会把长尾盖住。还要设护栏,比如错误率、越权访问、过期文档引用,这些指标变差时不能因为任务成功率没掉就忽略。
P99 上涨而 CPU、内存正常,说明瓶颈大概率在等待而不是计算。 先用 Trace 把请求拆成 Session Load、Memory Retrieval、LLM Planning、Tool Execution(向量检索、数据库查询)、LLM Generation 几段,重点区分实际执行时间和排队等待时间:数据库查询本身 10ms、连接池等待 800ms,这种情况下继续优化 SQL 没有收益,应该查连接池占用和慢请求。如果延迟集中在模型侧,再拆连接建立、首 token 和完整生成,判断是服务排队还是输出 token 变多;如果集中在 MQ,就看 Consumer Lag、消息等待、Worker 并发和重试次数。C++ 服务还要查线程池任务饥饿:Worker 长期阻塞在 Future 或同步 I/O 上,CPU 不高但请求队列持续积压,表现就是 P99 先恶化。
找 case 不能只随机抽用户问题,要按失败原因分层。 数据来源至少有三路:用户负反馈、任务执行日志、自动化指标异常。比如某次任务连续调用同一个检索工具三次仍没有有效引用,就值得回放,判断是 Planner 规划错误、检索结果不相关,还是工具返回格式不对。把 case 分成模型规划错误、工具参数错误、知识检索不相关、引用来源错误、任务超时、多轮上下文丢失、工具重复调用、下游服务异常几类,每类对应不同的优化手段:检索问题看切分、Embedding、召回和 Rerank,参数问题看 Schema 和结构化输出,超时看链路和重试策略。还要单独维护 Hard Case,比如多跳检索、过期文档、相似接口名、信息不完整和工具异常恢复,放进固定回归集,每次改 Prompt、模型或检索策略都重跑,防止修一类问题引入另一类退化。
多工具并发的前提是依赖分析,不是所有 Tool Call 都能一起发。 查询接口文档和历史故障记录互不依赖,可以并行;第二个工具的参数依赖第一个工具返回的接口 ID,就必须串行。工程上把工具调用组织成 DAG,Planner 产出节点和依赖边,执行器按拓扑序调度,能并行的节点同时发起,聚合节点等依赖完成。C++ 里可以用协程、Future 或异步 I/O,具体取决于底层 RPC 客户端是否真正异步。聚合时要处理部分失败:设统一 Deadline,一个成功一个超时,独立检索任务可以返回部分结果并标注缺失,涉及资金、权限和数据修改的操作则要更严格的语义,不能拿部分成功当成功。还要有全局和工具级并发上限,否则一个 Agent 同时触发几十个调用,可能瞬间打爆数据库连接池。
高并发下 shared_ptr 的问题,一半是数据竞争,一半是性能。 数据竞争方面,控制块的引用计数是原子的,多个 shared_ptr 副本并发复制、析构不会把计数改坏;但被管理对象本身没有保护,多个线程同时对同一个 vector 写仍然会竞争,同一个 shared_ptr 实例被多线程无同步读写也是未定义行为,需要外部锁或 C++20 的 std::atomic<std::shared_ptr<T>>。性能方面,每次复制和销毁都要对同一个控制块做原子读改写,热点对象会让多个 CPU 核争抢同一缓存行,产生 cache line ping-pong;控制块和对象分开分配还会带来额外分配与缓存不友好。优化方向是减少不必要的共享所有权,能用 unique_ptr 或引用就用,创建时用 make_shared 合并分配,把共享指针限制在真正跨线程传递的边界,读多写少的场景考虑侵入式引用计数或对象池。
长链路可靠性是这类项目最难的工程点。 模型可能输出非法参数,下游工具可能超时,检索结果可能不相关,部分任务还要跨多个请求持续执行,所以要有显式任务状态机并持久化到 MySQL:哪些步骤可重规划、哪些完成后不能重复执行、失败是可重试还是致命、恢复时从哪里继续。再配以 Trace ID 为核心的链路追踪,记录模型调用、工具参数、耗时、检索结果和状态迁移,才能定位一次失败发生在模型决策、知识检索还是工具执行阶段。这些机制不直接提升回答质量,但决定了系统能不能在真实流量下稳定交付。