LLM 面试备考
首页/系统设计

设计题 08:高并发高可用的 Agent 平台

权重:★★★ 形式:45 分钟系统设计轮
素材来源:Own — QueWI 的 AI 网关(限流 / 熔断 / 降级 / 路由)与 Race Bot 的 retry-and-fix;其余为《深入理解 AI Infra》第 11 章与《深入理解 AI Agent》第 10 章的知识框架,非一手实测
相关知识点:06-agent-infra/10-agent-runtime.md(本篇的知识底座:并发容量 / 隔离边界 / 环境生命周期 / 抢占 / 恢复与持久性语义)、06-agent-infra/07-distributed-inference.md §6(实例级启动积压与部分故障恢复)、06-agent-infra/gateway-routing.md 与 05-routing-gateway.md §4.7(网关自身的高可用)、04-agent-architecture/13-multi-agent-coordination.md(多 Agent 时的冲突治理)、07-single-agent.md(单体 Agent 的循环控制)、06-agent-observability.md(失败排查)


1. 题干#

「我们要做一个 Agent 平台 —— 业务方把他们的 Agent 接进来,用户提任务,平台负责把任务跑完。现在每天几万次任务,高峰期老出问题:环境建不出来、任务卡住没人知道、有时候跑了两遍扣了两次钱。你来设计这个平台,要扛得住高峰、还要高可用。」

题干的模糊点比 07-single-agent 更多:它没说 Agent 是谁写的、工具是谁实现、任务有多长、有没有副作用、SLA 是多少、是否多租户。这些不是题面省略,是刻意的 —— 因为「平台」这个词本身就是最大的模糊源:它可能指"给自家一个 Agent 用的执行沙箱",也可能指"给几十个业务方用的多租户运行时",两者的设计几乎没有交集。

⚠️ 讲话口径:这道题的框架来自教材(《AI Infra》第 11 章、《AI Agent》第 10 章)与工程常识。答题用「如果让我设计,我会先算哪三笔账」而不是「我们平台就是这么搭的」—— 追问两层容量与恢复细节就会露底。你真正做过的是 QueWI 的网关那一层(限流 / 熔断 / 降级 / 路由),那一段可以讲成一手经验,其余要标成知识。


2. 第一步:澄清(这一轮最重要的动作)#

不要一上来画「接入层 → 编排层 → 执行层 → 存储层」的四层图。 开场第一句应该是:

「这个平台的设计跨度很大 —— 我先确认几件事,因为**「给一个内部 Agent 用」和「给几十个业务方用」是两套完全不同的系统**,问错了后面全白搭。」

必问清单(按对方案的影响排序)#

# 问题 为什么它会改变方案
1 单租户还是多租户?业务方能不能上自己的代码 / 工具? 单租户 → 隔离与配额可以简单很多;多租户 + 用户代码 → 隔离边界必须抬到 microVM 级、配额与计费必须按租户切,整个执行平面的设计重心都变了
2 工具是谁实现的?有没有副作用? 只读工具 → 可以随意并行、重试、缓存;能付款 / 发消息 / 改生产库 → 必须幂等 + 确认 + 补偿,「跑了两遍扣两次钱」正是这一条没做
3 任务有多长?秒级还是小时级? 秒级 → 同步请求响应模型就够;小时级 → 需要任务状态机 + 持久化 + 断点恢复 + 异步查询接口,这是「任务卡住没人知道」的根因
4 能不能中断 / 抢占?任务失败了要不要接着跑? 决定要不要 checkpoint、要不要 WAL、要不要把「一轮」做成事务
5 SLA 是什么?高峰是什么形态? 「扛得住高峰」得先定义高峰:是均匀 3 倍,还是瞬时突发 10 倍? 前者靠扩容,后者靠预留容量 + 排队 + 降级
6 要不要跑多 Agent 的协作任务? 要 → 还得加冲突治理(虚拟文件系统四区域、乐观锁、结算点);不要 → 这一整块可以先不做
7 成本归属谁?按什么计费? 决定埋点粒度(要归到租户 / 任务 / 尝试),也决定失败尝试算谁的
8 平台是控制面还是数据面?要不要 SLO 分级? 免费任务可以被抢占、付费任务不能 —— 这直接决定队列与优先级设计

面试技巧:先问 1 和 3。 这两问决定你后面讲的是「一个执行沙箱服务」还是「一个有状态的任务平台」。能在第 3 分钟说出「这题的核心不是并发,是"有状态任务"和"多租户隔离"」,比画出四层架构图都加分。


3. 参考架构#

假设澄清后得到:多租户、业务方接入自己的 Agent 与工具、有副作用、任务分钟到小时级、可中断可恢复、高峰是瞬时突发、要按租户计费、暂时不做多 Agent。核心是把控制面和执行面分开(这是全篇的主线):

┌─ 接入与控制面(无状态,可水平扩)──────────────────────┐
│  接入网关:鉴权 · 租户配额(RPM/TPM/并发)· 限流 · 计费埋点 │
│  任务 API:提交 / 查询 / 取消  (异步,返回 task_id)      │
│  编排器:读任务状态机 → 决定下一步 → 派发    ← 无状态      │
└───────────────────────────────────────────────────────┘
        ↓ 派发(消息)                ↑ 状态回写
┌─ 状态存储(有状态,是真正的关键路径)──────────────────┐
│  任务状态机:已提交/进行中/需要输入/已完成/失败            │
│  每轮事务记录(WAL)· checkpoint · 「连续保存到哪一轮」    │
└───────────────────────────────────────────────────────┘
        ↓
┌─ 工具执行面(有状态,资源受限,是真正的瓶颈)──────────────┐
│  调度器:power-of-k-choices · 本地补记 · 节点自查准入      │
│  沙箱池:进程 / 容器 / microVM(按任务选)                │
│    四区域文件系统:私有工作区 · 共享空间 · 外部挂载 · 内置资源│
│  队列与优先级:在线(不可抢占) / 离线(可抢占)            │
└───────────────────────────────────────────────────────┘
        ↓ 模型调用
┌─ 模型服务面 ────────────────────────────────────────────┐
│  (复用 design/05 的网关)路由 · 前缀缓存亲和 · 限流 · 降级 │
└───────────────────────────────────────────────────────┘

这张图上有三个判断值得单独说(都是与「直觉版架构」的分歧点):

  1. 控制面必须无状态。 编排器一有状态就成了单点,扩不出去、挂一台就丢任务。状态全部外置到状态存储,编排器变成「读状态 → 决策 → 派发」的纯函数,可以随时增减副本。
  2. 真正的瓶颈在执行面,不在控制面。 控制面加机器很容易,但沙箱内存是硬约束 —— 详见 §4.1。
  3. 状态存储是新的关键路径。 它一旦不可用,整个平台「不知道任务跑到哪了」。它比编排器更需要高可用。

4. 关键决策与权衡#

4.1 并发容量:先算三类资源,找出最先撞线的那一个#

不要凭感觉说「加机器」。 用一个关系式把三类资源都算出来:

资源需求率=到达率×每任务平均资源需求\text{资源需求率}=\text{到达率}\times\text{每任务平均资源需求}

以教材的教学平台为参照(每秒 10 项任务,每项 3 轮,每轮模型 9 秒 + 工具 1 秒,工具主机 48 核 / 358 GiB):

资源 需求 容量 占用率
CPU 核 平均 30 核 48 核 62.5% ✅
模型并发调用 平均 270 次 320 槽 约 84% ✅
环境内存 平均 600 GiB 358 GiB 约 168% ❌

结论:先撞的是环境内存,不是 CPU —— 而且 CPU 还空着 18 核,加 CPU 核解决不了这个问题。358 GiB 最多同时保留约 178 个 2 GiB 环境,每个要占 30 秒,每秒最多开始约 5.9 项任务,其余只能排队。

一个必须主动说出来的反直觉推论:把模型调用从 9 秒延长到 12 秒,CPU 需求一点没变,内存需求却从 600 涨到 780 GiB、模型并发从 270 涨到 360(超过 320 的槽) —— 因为任务变长了,环境要保留更久。模型变慢会同时推高工具平台的内存需求和自身的并发压力。

生产规模上,超卖是常态而不是异常。 DeepSeek 的 DSec 沙箱平台按一周运行记录:约九成容器和 microVM 平均只用到所申请 CPU 的 5% 以内(agent 装依赖、起服务、改的文件在模型生成期间一直留着),所以平台超卖,单节点曾稳定跑 3,200 个容器或 800 个 microVM。密度上限由两件事决定:① 等待期间一直占用的内存,② 挤在同一物理核上的任务相互拖慢。

4.2 隔离边界:按任务选,不是越强越好#

隔离强度是一根连续的轴,代价随强度上升:进程(独立地址空间,共享宿主内核)→ 容器(加命名空间 + 文件系统视图 + 资源控制,仍共享内核)→ microVM(自带内核,经虚拟硬件隔离,精简虚拟设备降开销)→ 完整虚拟机(完整商用 OS、可图形渲染)。

判据是按任务类型分档(DSec 的做法):判题 / 编译这类短小无状态任务 → 预先创建、反复使用的容器;代码仓库级任务 → 容器(快、密度高);安全相关任务 → microVM;需要完整 OS(如 Android)或图形渲染 → 完整虚拟机。注意容器本身通常也跑在 QEMU 里(容器共享宿主内核,外面再套一层更安全)。

如果问的是多租户平台,这一段是全篇最重要的地方。 因为它意味着不能因为"图快"就全用容器 —— 隔离与否是一条产品级的信任边界。

沙箱要防三个方向,中间那一层最容易被忽略:

方向 手段
沙箱 ↔ 沙箱 虚拟化 + 资源配额
沙箱内 agent ↔ 平台自己的代理进程 AppArmor(按程序限制可访问的文件与 socket)—— agent 以 root 运行也越不了权
沙箱 ↔ 外部网络 eBPF 逐包白名单,只放行本任务需要的软件源

为什么中间那层是重点?因为 agent 会主动找捷径。 DSec 记录到的做法:向沙箱内平台代理进程的 socket 发伪造请求、翻查代理日志找残留的答案、改写 /bin/bash 篡改后续命令。也有无意的破坏:递归 grep 读到 /proc 下某文件触发内核缺陷整机崩溃;执行 yes 命令让平台逐字记录输出、攒下数十 GB。要诚实地补一句边界:这些手段只能堵住已经发现的捷径,单看最终输出判断不了 agent 是否按预期完成了任务。

4.3 环境生命周期:这是「环境建不出来」的正解#

四种做法,取舍可以当场算:

做法 何时用 代价
始终驻留 轮间状态无法落盘 内存按整任务时长计费(教材算例里正是它导致 600 GiB)
暂停 / 恢复 间隔远大于保存时间的场景 保存耗时随内存容量增长
每轮重建 轮间状态能完整落盘 每次付创建开销(但很便宜)
提前准备 下一轮工具类型可预测 预测错误要取消重来

判据一:暂停什么时候划算。

pM M G>Ctransitionp_M\,M\,G>C_{\mathrm{transition}}

E2B 口径下暂停每 GiB 约 4 秒、恢复约 1 秒。2 GiB 环境保存要 8 秒,而模型等待只有 9 秒 → 整个间隔被保存恢复占满,一点没省;512 MiB 环境约 2 秒保存完,9 秒间隔能释放 6 秒,才开始有收益。反过来,RL 训练里 GPU 作业被抢占后要等一到三小时 —— DSec 上一个作业最多可申请 32K 个沙箱(按 2 GiB 算约 70 TB),但保存加恢复仍只要 9 秒,暂停两小时,内存空出来的时间是保存恢复期的约 800 倍。所以作业一被抢占就主动暂停全部沙箱。

判据二:每轮重建能省多少。 前提是下一轮要用的状态已全部写入持久存储。此时每轮只在「准备 2 秒 + 执行 1 秒」保留环境:

60 GiB⋅秒 ⟶ 3×(2+1)×2=18 GiB⋅秒60\ \text{GiB·秒}\ \longrightarrow\ 3\times(2+1)\times2=18\ \text{GiB·秒}

平台平均内存 600 → 180 GiB,代价是每秒创建 30 个环境、平均多用 3 个 CPU 核(30 → 33;Firecracker 论文报告单主机每秒最多创建 150 个 microVM,仍有余量)。用少量创建开销换回 420 GiB 的等待期驻留 —— 这就是"环境建不出来"的最直接解法。

⚠️ 但这条路的边界必须说清楚:它成立的前提是轮间状态能完整落盘。模型装了依赖、起了数据库或后台服务时就不成立 —— 只保存文件,重建出来的环境和原来不同。判别就是一句话:轮间状态能否完整写进持久存储。

判据三:提前准备的收益与过度准备的代价。

E[W]=P−hmin⁡(P,L)E[W]=P-h\min(P,L)

P=2P=2 秒、命中率 h=0.75h=0.75:不提前等 2 秒;提前 1 秒 → 1.25 秒;提前 2 秒 → 0.5 秒;提前 3 秒仍是 0.5 秒,却让正确分支多占 1.5 GiB·秒。能在调用到达前准备好就够了,更早只会白占内存 —— 实测里「一开始就准备」比「提前 50 ms」的累计内存占用高约 24 倍。

4.4 队列、优先级与抢占:突发、物理核、以及两笔账#

排队时间不由总工作量决定,而由到达方式决定。 十项各 1 秒的工作:均匀到达 → 零排队;同时到达 → 执行仍只要 10 秒,平均等待 4.5 秒。所以从平均需求推响应时间,必须知道高峰的形态(这正是澄清第 5 问要问的)。

突发怎么摊到多台节点: 难点是调度器看到的负载视图天然过时(定期汇总),都挑"最闲的"会让同批请求挤到同一台。三招:① power-of-k-choices(随机抽几个节点、选最闲的),同时做的决定自然分散;② 调度实例把刚放下、还没进汇总的沙箱补记到本地视图;③ 最终接不接收由节点自查本地容量决定。全局视图可以过时,不超容量由节点保证,且调度器不需要持久状态、可随时增减。

优先级:硬件上不一定分得开。 把尽力而为(BE)任务放进 SCHED_IDLE,理应"在线任务能跑就让"。但开了超线程后,一个物理核的两个硬件线程共用执行单元和一级缓存,两种任务仍可能落在同一物理核上。DSec 实测:BE 占节点一半容量时,在线任务每步耗时增加约 45%,单靠 SCHED_IDLE 几乎没改善;再加 core scheduling(同一物理核只跑同一组任务)降到约 17%。教材平台干脆关掉超线程 —— 换来干净隔离,代价是可调度逻辑核少一半。

抢占要算两笔账: ① 交还延迟 —— 离线任务保存状态可能要 30 秒,而在线任务只肯等 2 秒,所以光设优先级没用,必须另行预留能在 2 秒内给出的资源;② 未保存进度 —— 被抢占后要重做多少,取决于上一个 checkpoint 之后的积累。

4.5 高可用:控制面、执行面、状态存储,三处分开设计#

这是「高可用」在平台语境下的正解 —— 不是笼统说"多副本",而是按三处分别说:

层 可用性做法 关键点
接入 / 控制面 无状态多副本、跨可用区;配置中心故障时回落到「最后一次已知良好配置」;路由决策本地化 与 05-routing-gateway.md §4.7 同理:这一层挂了 = 全部任务挂了。还要有安全默认(如统一走稳定档位)而不是直接报错
状态存储 多副本 + 持久化 + 定期备份 新的关键路径 —— 它挂了整个平台「不知道任务跑到哪了」。比编排器更需要高可用
执行面(沙箱) 节点自查准入(宁可拒绝也不超卖);部分故障隔离;作业被抢占时主动暂停沙箱而不是销毁 单节点故障只影响它上面的沙箱,其他节点与完整副本继续服务 —— 这是多副本在容错维度的价值

一个容易漏的点:故障恢复本身要算时间。 实例从停止状态启动期间,新请求照常到达,就绪时积压约 λTs\lambda T_s;就绪后按净消化能力 μ−λ\mu-\lambda 排空,Tdrain=λTs/(μ−λ)T_{drain}=\lambda T_s/(\mu-\lambda)(推导见 06-agent-infra/07-distributed-inference.md §6)。多出来的算力常常是用来消化启动积压的,不是用来提高稳态吞吐的 —— 说得出这一句,就超过了大多数答案。

4.6 失败恢复与持久性语义:这才是「跑两遍扣两次钱」的正解#

「任务卡住没人知道」和「跑两遍扣两次钱」看似两个问题,根源是同一个:没有把「一轮」做成有记录的事务。

① 先分清两种失败。 控制器没收到工具完成的确认时,无法区分「没做完」和「做完了但确认丢了」。正确动作不是重试,而是先查询已持久化的结果、核对操作标识与输入文件版本:有结果就接着走,没有才判断能不能重试。「消息去重」只防"同一结果传两次";「实际执行两次」只能靠持久化结果 + 目标系统的幂等协议 —— 这就是"扣两次钱"的位置。

② 定义清楚持久性语义。 操作系统早就区分「写入完成」和「已经持久化」(write() 返回 ≠ 落盘,要 fsync() 并检查)。平台同样要明确规定:向调用方报告「完成」时,保证的是"答案已生成",还是"消息、工具结果和恢复位置都已保存"?而且只对聊天日志 fsync() 不够 —— 恢复所需信息可能分散在日志、工作目录和外部数据库三处,控制器要核对一致。「保存成功」还分等级:本地页缓存 / 本地 fsync() / 云端持久化确认,能扛的故障完全不同。

③ 保存频率是一笔显式权衡。 100 个请求、每个 200 ms、提交固定开销 8 ms、每条记录 2 ms:

保存方式 保存共用时 无故障总用时 故障后最多重做
每请求提交一次 100(8+2)=1000100(8+2)=1000 ms 21 s 0.2 s
每 10 个请求提交 10(8+10×2)=28010(8+10\times2)=280 ms 20.28 s 2 s

比较时除了完成时间,还要记录「已返回但未保存」的请求数,并核对重启后实际能恢复到哪。

④ 异步上传有一个隐蔽缺口(多副本恢复时最致命)。 每轮先写本地、后台异步上传(失败不重试也不中止):三轮本地都全,但第二轮上传失败、第三轮成功 → 云端只连续完整到第一轮。换台机器仅凭云端恢复就撞上缺口。所以云端要记的不是"收到的最后一轮",而是"从起点连续保存到了哪一轮"。

⑤ 把一轮做成事务,但要知道它的边界。 借鉴数据库:把一轮「模型决策 — 工具执行 — 保存结果」当整体提交,全部保存成功才记「已提交」,重启后从最后一次完整提交接着跑(这是 Agent 事务 / ACID 的方向)。但硬边界是:若这一轮还调了外部服务,仅在本地记「已提交」保证不了外部操作恰好执行一次 —— 那一侧必须靠操作标识 / 幂等接口 / 补偿。

⑥ 成本按「成功任务」算,并用恢复树预估。 C‾q=Call/Nq\overline C_q=C_{\mathrm{all}}/N_q(分子含失败尝试的全部支出)。教材的恢复树算例:首次 80% 直接成功 + 12% 局部修复 + 其余升级 → 成功率从 80% 提到约 99.7%,每项成功成本从 0.0125 美元升到约 0.0146 美元(按时 0.0153)。该不该付,取决于业务对「按时成功率」的门槛,而不是单次价格 —— 这也回答澄清第 7 问「失败尝试算谁的」。

4.7 如果要做多 Agent 协作:加一层冲突治理#

多 Agent 不是并发问题,是"有状态协作"问题 —— 完整机制见 04-agent-architecture/13-multi-agent-coordination.md,这里只列平台必须提供的那几件东西:

  • 数据平面:四区域虚拟文件系统(私有工作区 / 共享空间 / 外部挂载 / 内置只读)。冲突与信息泄露,大多源于把本应隔离的区域混置 —— 平台应该在架构层面提供这四类区域,而不是让业务方自由发挥。
  • 控制平面:消息(带信封)· 状态查询(进度文件 + 消息,而不是轮询接口)· 终止(优雅 → 强制,且沿创建关系级联,防孤儿)· 资源调度(步数 / token 预算、并发上限、抢占)。
  • 冲突治理三层:同文件 → 乐观锁;跨文件语义冲突 → 工作副本隔离(Git 分支 / worktree)或收敛写权限;并行收口 → 结算点必须是「第一个已验证成功」,且 settle_once 幂等(否则两个几乎同时的成功事件会触发两次汇总)。
  • 跨租户协作才需要 A2A:MCP 管 Agent ↔ 工具,A2A 管 Agent ↔ Agent;同一租户内部用消息总线即可,别为了"标准"提前上 A2A。

5. 容量与成本直觉#

容量:先算三类资源的需求率,找最先撞线的那个。 教学平台的参照口径是 CPU 62.5% / 模型并发 84% / 环境内存 168% —— 卡住的是环境内存。要给出结论性的量级判断,而不是「加机器」:

  • 先算环境数量(Little 定律):N‾env=λenvT‾env\overline N_{\mathrm{env}}=\lambda_{\mathrm{env}}\overline T_{\mathrm{env}} —— 每秒 10 项任务、每项占环境 30 秒 → 平均 300 个环境、600 GiB。
  • 反推能接多少:358 GiB ÷ 2 GiB ÷ 30 秒 → 每秒约 5.9 项。
  • 改环境策略后:每轮重建 → 平均内存 600 → 180 GiB,代价 CPU 30 → 33 核、每秒多创建 30 个环境。

成本:分母是成功任务数,分子是全部支出。 十项花 1 美元、五项成功 → 每完成一项实际支付 0.2 美元;只统计成功那五项的 0.5 美元会得到 0.1 美元、正好漏掉一半支出。教材的完整方案对比(快速服务 + 每轮重建环境)给出 每千项按时通过任务约 9.07 美元,其中模型费用占 98% 以上 —— 这是一个很重要的量级判断:执行环境的优化主要买的是「容量」,不是「费用」。

一个能收口的判断:如果被追问「优化环境策略能省多少钱」,诚实的答案是省不了多少钱、但能让平台装得下 —— 教材算例里每轮重建只让单位成本降低约 1.2%,而它把平均内存从 600 GiB 压到 180 GiB。把目标说对(容量 vs 成本)比报一个省的百分比更重要。


6. 面试官会怎么往下挖#

第一层:容量与瓶颈(看你有没有算过账)

  • 「高峰是 3 倍还是 10 倍?你怎么算需要多少台?」← 见 4.1:先算三类资源的需求率
  • 「CPU 还有富余,为什么环境建不出来?」← 见 4.1 / 4.3:瓶颈是内存驻留,不是 CPU
  • 「模型响应变慢,对你的工具平台有什么影响?」← 见 4.1 的反直觉推论:CPU 不变,内存和并发都涨
  • 「超卖会不会出事?」← 见 4.1:CPU 超卖可行(九成容器只用 5% 以内),但内存与同核干扰是上限

第二层:隔离与环境(看你会不会分档)

  • 「所有人都用容器行不行?」← 见 4.2:按任务分档,多租户 + 用户代码必须抬到 microVM
  • 「沙箱要防什么?」← 见 4.2:三个方向,中间那层(agent ↔ 平台代理)最容易漏;补上「单看输出判断不了是否按预期完成」
  • 「环境一直留着行不行?」← 见 4.3:判据是「轮间状态能否落盘」;能落盘就每轮重建,不能就只能压驻留
  • 「暂停沙箱能省内存吗?」← 见 4.3:判据 pMMG>Ctransitionp_M M G>C_{\mathrm{transition}},2 GiB 环境 9 秒间隔毫无节省;长抢占下几乎免费

第三层:高可用与恢复(这一层拉开差距)

  • 「控制面挂一台会怎样?」← 见 4.5:控制面无状态,状态外置;状态存储才是新的关键路径
  • 「任务卡住没人知道,你怎么发现?」← 见 4.3 / 4.6:进度文件的最后修改时间超时触发兜底;配合 06-agent-observability.md 的分段计时
  • 「扣了两次钱,问题出在哪?」← 见 4.6 这一节,是本篇最重要的一问:区分「没做完」与「做完了但确认丢了」、先查询再重试、幂等 / 操作标识 / 补偿、本地记「已提交」保证不了外部恰好一次
  • 「多副本恢复时会不会丢进度?」← 见 4.6 ④:云端要记「连续保存到哪一轮」,不是「最后一轮」
  • 「故障恢复要多久?」← 见 4.5 的提醒:Tdrain=λTs/(μ−λ)T_{drain}=\lambda T_s/(\mu-\lambda),扩容的算力常是用来消化启动积压的

第四层:取舍与不做什么(资深与初级的分界)

  • 「为什么不直接上多 Agent?」← 见 4.7:多 Agent 是"有状态协作"问题;先单 Agent(07-single-agent.md)
  • 「为什么不自己写模型路由,用现成网关?」← 复用 05-routing-gateway.md 的结论:网关是横切关注点的收拢层,但别把它做成有状态的编排引擎(会变成瓶颈与单点)
  • 「哪些东西你建议先不做?」← 多 Agent 协作、A2A、跨可用区容灾、精细计费,都可以按阶段推后;但"任务状态机 + 幂等 + 持久化"一天都不能省

7. 自测#

1. 平台方说「CPU 利用率只有 20%,但高峰期经常建不出新环境」。你怎么解释,给什么方案?

答案

先纠正前提:CPU 利用率低和生产级沙箱平台是同一个事实的两面。 DSec 按一周运行记录,约九成容器和 microVM 平均只用到所申请 CPU 的 5% 以内 —— agent 装依赖、起服务、改的文件在模型生成期间一直占着内存,CPU 大部分时间闲着。所以平台本来就超卖,单节点能跑 3,200 个容器或 800 个 microVM。

"建不出来"的瓶颈是密度上限,而密度上限由两件事决定:① 等待期间一直占用的内存,② 挤在同一物理核上的任务相互拖慢。

三条方案,按性价比排:

  1. 压每次创建的驻留量:不做「整份复制模板」,用按需加载 + 只读页共享 + 冷页回收。教材 100 个环境的算例:整份复制约 200 GiB → 只加载访问到的内容约 50 GiB → 再共享只读内容、只留脏页约 13 GiB。DSec 再让只读镜像层走 virtio-pmem + DAX,内存峰值再降约四成。
  2. 让调度器把突发摊开:power-of-k-choices + 本地补记 + 节点自查准入。要点是承认"全局视图天然过时",靠节点自己保证不超容量。
  3. 若轮间状态能完整落盘,改成每轮重建环境:平均内存 600 → 180 GiB,代价仅每秒多创建 30 个环境、多用 3 个 CPU 核。

要提醒两点:① 如果不便重启环境(模型装了依赖、起了后台服务),方案 3 不适用,只能靠 1 和 2;② 提前整份拉取镜像反而更慢 —— 按需加载与"镜像全部预缓存"完成整批任务的时间差不多,先整份拉取要慢约 1.7 倍(镜像 4–12 GB,运行时其实只读到 4%–13%)。

2. 「跑了两遍扣了两次钱」—— 请给出完整的归因和修复方案。

答案

归因:把「没收到确认」当成了「没执行」。 控制器发起的写操作(付款)实际已经在外部系统执行成功,但确认消息丢在路上;控制器据此重试,于是同一笔付款发生两次。这是分布式系统的经典问题,但 Agent 平台里特别容易踩,因为工具调用是模型驱动的,重试往往发生在"模型自己觉得没成功"时。

修复分四层,缺一层都不完整:

  1. 先查询,再重试(顺序不能反):收到超时 / 无确认时,先查已持久化的结果、核对操作标识与输入文件版本,有结果就接续。不要直接重试。
  2. 幂等协议:不幂等的操作(付款 / 创建资源 / 发消息)必须由目标系统支持操作标识、事务或补偿。平台侧要能生成并透传一个稳定的操作标识(同一逻辑操作重试时标识不变),让对方能识别重复。读操作和幂等操作重试无妨,其余不行。
  3. 区分两种"重复":同一结果被传送两次 → 消息层去重即可(累计 CPU 时间不变);同一操作实际执行两次 → 只能靠持久化结果 + 幂等协议(会消耗两次资源)。混为一谈是常见错误。
  4. 把一轮做成事务,并认清它的边界:把「模型决策 — 工具执行 — 保存结果」当整体提交(Agent 事务 / ACID 方向)。但硬边界是:仅本地记「已提交」保证不了外部恰好执行一次 —— 外部那一侧的保证只能来自操作标识 / 幂等 / 补偿。

再加两条工程上的防线:不可逆操作走「预检—确认」两段式(先让另一个模型家族校验风险,再执行);前台 / 后台调用分开处理 —— 辅助性的后台调用失败直接放弃,否则后台重试会挤占主链路配额、形成「重试放大」(这一步的细节见 09-coding-agent.md 与 quewi-chatbot.md 的线上故障复盘)。

3. 「我们按 SLA 给任务分级:付费任务不能等,免费任务可以等。这样设计对吗?」你怎么评价?

答案

方向对,但只说了一半 —— 优先级在硬件上不一定分得开。 把免费(尽力而为,BE)任务放进 Linux 的 SCHED_IDLE 调度类,逻辑是"付费任务能跑就让"。但开了超线程(SMT)后,一个物理核有两个硬件线程、共用执行单元和一级缓存,调度器按硬件线程分配,两种任务仍可能落在同一个物理核上互相拖慢。DSec 实测:BE 占节点容量一半时,延迟敏感任务每步耗时增加约 45%,单靠 SCHED_IDLE 几乎没有改善;再给延迟敏感任务开 core scheduling(同一物理核只跑同一组任务),增幅才降到约 17%。剩下的是整机高负载降频和共用内存带宽 / 末级缓存,要隔离还得另做。

第二件没说的事:光有优先级不够,还得预留资源。 设付费任务最多等 2 秒,而免费任务保存状态要 30 秒 —— 那么这组资源即使可抢占,也要 30 秒后才交还。所以平台必须另行预留能在 2 秒内分配给付费任务的容量,否则"可抢占"只是个说法。

第三件:抢占要算未保存进度。 被抢占的作业要重做多少,取决于上一个 checkpoint 之后的积累。调度器要同时看「新任务提前多久开始」「保存 / 恢复开销」「原作业增加的延迟」三笔。

还有一个公平性维度值得提:长时间看要明确按什么标准衡量分配是否公平 —— 按资源份额(如主导资源公平 DRF,取每任务占比最高的那类资源作为主导资源)、按推进速度、还是限制最长等待时间,这是三个不同的目标,先说清选哪个。

4. 平台要做多副本容灾。有同事说「每轮跑完同步上传到云端就行了」。这个方案有什么问题?

答案

前提没错,但"上传成功"和"历史完整"是两回事 —— 缺口出在异步与失败重试上。

具体缺口:如果实现是「每轮先写本地、再后台上传、上传失败不重试也不中止本地执行」,那么:三轮记录本地都完整,但第二轮上传失败、第三轮成功 → 云端只连续完整到第一轮。此时换一台机器仅凭云端恢复,就会丢第二轮(模型输出与工具结果都没有)。

根因:云端只记了「收到的最后一轮」,而恢复需要的是「从起点连续保存到了哪一轮」—— 不能因为第三轮上传成功,就把连续位置从第一轮改成第三轮。

正确做法:

  • 云端记录「连续保存位置」,而不只是最后收到的那一轮;
  • 要确认历史完整,必须补齐缺失的那一轮,或用完整的本地副本重建并核对;
  • 删除旧日志同理:删前两轮之前,必须先保存一份能恢复到第二轮结束的完整状态,并把状态与轮次一起提交;否则即使剩余日志全部读完,也应返回 incomplete,表示没有足够依据确认历史完整;
  • 如果接口允许按消息位置恢复,还要确认该位置对应一个已保存的完整轮次,并核对工作目录版本。

顺带把持久性语义讲清(这是同一条链上的另一环):平台必须明确「报告完成」保证的是什么 —— 只保证答案已生成,还是保证消息、工具结果、恢复位置都已保存?「保存成功」本身还分等级:写入本地页缓存 / 完成本地 fsync() / 收到云端持久化确认,能扛的故障完全不同(本地 fsync() 扛得住进程重启,扛不住整台机器及其磁盘不可用)。多副本容灾要的是最后一档。

5. 客户要求平台「先支持多 Agent 协作」。你会怎么回应?

答案

先问清「多 Agent 解决的是他们哪个具体问题」,再决定做不做、做到哪一层 —— 因为多 Agent 不是并发问题,是"有状态协作"问题,成本与风险都另算。

第一步:过判据。 多 Agent 唯一站得住的判据是**「协作是否引入了单个 Agent 拿不到的新信息」(执行反馈、视觉反馈、工具反馈算;同一份材料互审、自我审查不算)。如果他们的场景只是"任务多",那要的是平台的并发能力**,不是多 Agent。先单 Agent,遇具体瓶颈再拆(详见 04-agent-architecture/multi-agent.md 与 07-single-agent.md)。

第二步:如果确实要做,平台要提供的其实是三件基础设施(不是"支持多 Agent 编排"这么一句):

  1. 数据平面:四区域虚拟文件系统(私有工作区 / 共享空间 / 外部挂载 / 内置只读)。冲突与信息泄露大多源于把本应隔离的区域混置 —— 这是平台的职责,不该让业务方自由发挥。
  2. 控制平面:消息(带信封)· 状态查询(进度文件 + 消息,而不是轮询接口,还能靠"最后修改时间超时"发现卡住)· 终止(优雅 → 强制,且沿创建关系级联防孤儿)· 资源调度(步数 / token 预算、并发上限、抢占)。
  3. 冲突治理:同文件 → 乐观锁;跨文件语义冲突 → 工作副本隔离(Git 分支 / worktree)或收敛写权限;并行收口 → 结算点必须是「第一个已验证成功」,且 settle_once 幂等。

第三步:主动说不做什么。 A2A 不做 —— 它是跨信任边界的 Agent ↔ Agent 互操作协议(Agent Card / 任务状态机 / 不透明协作),同一租户内部用消息总线即可。同理,别把网关做成有状态的编排引擎(会变成瓶颈与单点)。

一个可以收口的完整回答:"我建议分三步 —— 先把任务状态机、幂等与持久化做扎实(这一天都不能省);再补数据平面与控制平面,让协作具备基础设施;最后才谈编排拓扑与跨组织协议。多 Agent 的价值判断在业务侧,平台的职责是让它出问题时可控。"