为纪念 35 周年:《Terminator 2: Judgment Day》将在全球影院重映
2026-07-13
2026-07-14 0
与传统的 Web 服务不同,AI Agent 系统的请求处理链路具有几个独特的特征,使得性能优化变得格外复杂,我们在开发业务整合Agent时,遇到了这些问题:

第一,请求生命周期极长。 一个用户请求可能触发 5~20 轮 LLM 推理循环,每轮推理又可能并行调用多个工具,端到端耗时从数秒到数分钟不等。传统 Web 服务的"请求-响应"模型下的很多优化假设在这里不再成立。
第二,资源消耗随对话轮次线性增长。 每轮推理都会往上下文中追加消息(用户输入 + AI 回复 + 工具调用结果),Prompt 体积持续膨胀,同时每轮结束还需要将完整状态持久化——这两者都会随轮次增加而恶化。
第三,冷启动问题被放大。 Agent 系统通常采用进程级隔离(每个会话一个独立进程),新会话到来时需要 fork 进程、加载模型定义、初始化沙箱环境……这一系列操作的叠加使得首条消息延迟动辄 1~3 秒。
第四,外部依赖多且不可控。 LLM API 延迟波动大(2s-15s)、沙箱容器创建成本高(5-10s)、配置中心网络抖动……系统自身的优化再好,也容易被这些外部依赖拖累。
基于此,我们对项目做了一些性能优化
Agent 子进程的启动链路通常很长:读取配置中心 → 建立数据库连接 → 初始化存储客户端 → 加载 Agent 定义 → 创建沙箱连接……这些操作串行执行,任何一环慢了都会拖累整体。
更关键的是,这些操作中有很多是非关键路径——数据库连接没建好不代表不能开始处理消息,存储客户端没初始化完也不影响前几轮推理。
核心思想是将启动过程拆分为关键路径和非关键路径:
复制代码┌─────────────────────────────────────────────┐
│ 传统模式 (串行就绪) │
│ │
│ [配置加载] → [DB连接] → [存储] → [沙箱] │
│ ↓ │
│ 发送 ready │
│ 总耗时: ~1.2s │
└─────────────────────────────────────────────┘┌─────────────────────────────────────────────┐
│ 分层就绪模式 (先跑后补) │
│ │
│ [配置快速恢复] ──→ 立即发送 ready (~50ms) │
│ ↓ ↓ │
│ [后台: DB] [后台: 存储客户端] │
│ │
│ 首条消息延迟: ~50ms (vs 1200ms) │
└─────────────────────────────────────────────┘
关键设计点:
fork 语义——子进程继承父进程的地址空间。| 指标 | 优化前 | 优化后 |
|---|---|---|
| 子进程启动到可服务时间 | ~1200ms | ~50ms |
| 首条消息 P99 延迟 | 1.5~3s | <200ms |
| 因基础设施抖动导致的启动失败率 | ~2% | 0%(重试兜底) |
Agent 的推理引擎(我们称之为 AgentLoop)是一个有状态的复杂对象,内部维护着完整的多轮对话历史、工具调用的请求-响应配对关系、运行时 Skill 激活状态等。
传统做法是每轮对话结束后销毁旧实例、创建新实例。这在逻辑上最简单,但代价是:
复制代码┌──────────────────────────────────────────┐
│ 传统模式: 每轮重建 │
│ │
│ 第1轮: new AgentLoop() → 推理 → 销毁 │
│ 第2轮: new AgentLoop() → 推理 → 销毁 │
│ 第3轮: new AgentLoop() → 推理 → 销毁 │
│ │
│ 问题: 第2轮 LLM 不知道第1轮执行了什么工具 │
└──────────────────────────────────────────┘┌──────────────────────────────────────────┐
│ 优化模式: 实例复用 │
│ │
│ 第1轮: new AgentLoop() → 推理 → 保留 │
│ 第2轮: updateContext() → 推理 → 保留 │
│ 第3轮: updateContext() → 推理 → 保留 │
│ │
│ 优势: 完整 tool_use/tool_result 配对保留 │
└──────────────────────────────────────────┘
关键设计点:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 每轮引擎重建开销 | 50~200ms | <1ms |
| LLM 重复执行工具的概率 | ~15%(3+ 轮对话) | 0% |
| 因孤儿状态导致的 API 报错 | 偶发 400 错误 | 0% |
Agent 的能力来源于 Prompt 中注入的 Skill(技能)定义。一个典型的 Skill 定义文件会引用大量子文件:表单描述、操作说明、字段约束、示例数据、城市列表……
原始方案将这些引用全部内联展开到 Prompt 中。结果是:
我们将 Skill 内容的内联过程设计为一个有损但可控的裁剪管道:
复制代码原始 Skill 定义 (SKILL.md)
│
▼
┌─ 防护层 1: 单文件上限 (16KB) ─┐
│ 超限 → 替换为 占位符 │
│ 通过 → 继续下一层 │
└──────────────┬────────────────┘
▼
┌─ 防护层 2: 总量预算 (64KB) ────┐
│ 累计超限 → 后续文件全部占位 │
│ 未超限 → 继续递归展开 │
└──────────────┬────────────────┘
▼
┌─ 防护层 3: 深度限制 (3 层) ────┐
│ 达到深度 → 停止展开 │
│ 未达到 → 递归处理子引用 │
└──────────────┬────────────────┘
▼
┌─ 防护层 4: 压缩后截断 (8K tokens)│
│ 上下文压缩后重注入时截断防膨胀 │
└──────────────┬────────────────┘
▼
最终 Prompt (~16KB)
关键设计点:
| 指标 | 优化前 | 优化后 | 降幅 |
|---|---|---|---|
| 单 Skill 激活时 Prompt 大小 | ~528KB | ~16KB | 97%↓ |
| Token 消耗估算 | ~176K tokens | ~5.3K tokens | 97%↓ |
| LLM 首个 token 延迟 | <1s | ~80%↓ |
Agent 系统中有几类重量级资源,创建成本极高但复用价值也很大:
这些资源的共同特点是:创建昂贵、有状态、可安全复用。
以沙箱为例,我们设计了类似数据库连接池的管理架构:
复制代码 acquire(sessionId)
│
┌──────────┼──────────┐
▼ ▼ ▼
┌─────────┐ ┌────────┐ ┌────────┐
│ ① 幂等 │ │ ② 软绑定│ │ ③ LRU │
│ 命中 │ │ 命中 │ │ 空闲池 │
└────┬────┘ └───┬────┘ └───┬────┘
│ │ │
▼ ▼ ▼
已持有? 上次用的还在? 有空闲的?
→ healthCheck → 复用 → healthCheck → 复用
以上都没命中?
│
▼
④ 池未满? → 创建新的
⑤ 池满了? → FIFO 排队等待
三层命中的设计哲学:
配套的生命周期保障:
光有池化还不够,还需要解决两个衍生问题:
问题 A:平台回收。云平台的容器服务通常有空闲超时机制(如 30 分钟无活动自动回收)。我们的解法是心跳保活——定期向沙箱发送轻量级心跳信号重置计时器。
问题 B:脏数据残留。沙箱被回收或失效后重建时,旧的缓存数据(已安装的包列表、上传记录等)可能导致后续操作走错路径。我们的解法是缓存三路清理——在任何销毁/失效路径上清空所有实例级缓存。
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 沙箱获取 P99 延迟 | 5~10s(新建) | <2s(池化命中) |
| 同 session 多轮复用率 | 0%(每次新建) | >60%(软绑定) |
| 因平台回收导致的重建频率 | 每 30 分钟/个 | 接近 0(心跳保活) |
Agent 的快照持久化是一个"必要之恶"——它保证了断点恢复能力,但代价高昂:
在我们的系统中,一轮推理的快照保存耗时分布如下:
| 对话轮次 | 快照大小 | 序列化 | 存储写入 | 总计 |
|---|---|---|---|---|
| 1~3 轮 | ~50KB | ~2ms | ~20ms | ~25ms |
| 4~10 轮 | ~200KB | ~8ms | ~30ms | ~40ms |
| 11~20 轮 | ~1MB | ~40ms | ~80ms | ~120ms |
| 21+ 轮 | 5MB+ | ~200ms | 200ms~2s |
对于一个 10 轮的工具调用密集型对话,累计快照开销可达 0.5~2 秒——这还不算 LLM 本身的推理时间。
核心思路是利用时间局部性——连续的多轮推理之间间隔很短(通常 <2s),没必要每轮都立即持久化:
复制代码┌─────────────────────────────────────────────┐
│ 同步模式 (当前) │
│ │
│ 第1轮推理结束 → await saveSnapshot() → 继续 │
│ 第2轮推理结束 → await saveSnapshot() → 继续 │
│ 第3轮推理结束 → await saveSnapshot() → 结束 │
│ │
│ 用户感知: 每轮多等 25~200ms │
└─────────────────────────────────────────────┘┌─────────────────────────────────────────────┐
│ Debounce 异步模式 │
│ │
│ 第1轮结束 → scheduleSave() → 不等待 → 继续 │
│ 第2轮结束 → scheduleSave() → 不等待 → 继续 │
│ (2s 窗口内只有最后一次实际写入) │
│ 第3轮结束 → flushSave() → 强制写入 → 结束 │
│ │
│ 用户感知: 每轮多等 <1ms │
└─────────────────────────────────────────────┘
关键设计点:
| 指标 | 当前 (同步) | 优化后 (debounce) |
|---|---|---|
| 单轮额外开销 | 25~2000ms | <1ms (schedule only) |
| 10 轮对话累计写入次数 | 10 次 | 1~3 次 |
| 数据安全性 | 强一致 | 最终一致性 (turn_end flush) |
Agent 系统中有两类"冷启动"场景特别影响用户体验:
这两类延迟虽然只在"第一次"出现,但第一印象决定用户体验。
Level 1 — Worker 预热池
在服务启动时或流量低谷期,提前创建 N 个空闲 Worker 进程放入池中。新会话到来时直接从池中取出一个绑定,免去 fork 开销:
复制代码传统: 用户消息 → fork Worker (~500ms) → 处理
预热: 用户消息 → 取池中 Worker (<10ms) → 处理
Level 2 — Skill 内容预加载
在 bootstrap 阶段预解析所有 Skill 定义文件并缓存结果。Worker 首次激活某个 Skill 时直接从内存读取:
复制代码传统: 首次激活 Skill → 读磁盘 → 解析 Markdown → 递归展开引用 → 缓存
预热: 首次激活 Skill → 内存缓存命中 (0ms)
关键设计点:
| 指标 | 优化前 | 优化后 (预热命中) |
|---|---|---|
| 新会话首条消息延迟 | 500ms~2s | <50ms |
| 首次 Skill 激活延迟 | 50~200ms | <5ms |
| 额外内存开销 | 0 |
当一条用户消息进入 Agent 系统,在到达推理引擎之前,需要经过一系列前置处理步骤:
复制代码用户消息
→ 安全意图检测 (~20~200ms)
→ 会话获取/创建 (~10~50ms)
→ 历史消息加载 (~10~50ms)
→ 用户画像查询 (~20~100ms)
→ 权限校验 (~5~20ms)
↓
进入推理引擎
这些步骤在传统实现中是严格串行的——上一步完成后才开始下一步。但仔细观察会发现:其中很多步骤之间没有数据依赖关系。安全检测不依赖会话是否已创建,历史消息加载不依赖画像是否已查到。
串行执行的总耗时 = 各步骤耗时之和 = 65~420ms
如果这些无依赖的步骤能并行执行,总耗时 = 最慢的那一步 ≈ 200ms(通常是安全检测)
核心思路是对请求编排阶段的所有操作进行依赖分析,将无依赖的操作打包为并行批次:
复制代码┌─────────────────────────────────────────────┐
│ 传统模式: 严格串行 │
│ │
│ [安全检测] ─→ [会话] ─→ [历史] ─→ [画像] │
│ 200ms 50ms 50ms 100ms │
│ 总计: ~400ms │
└─────────────────────────────────────────────┘┌─────────────────────────────────────────────┐
│ 优化模式: 分批并行 │
│ │
│ Batch 1 (并行, 无互相依赖): │
│ ├─ [安全检测] ──── 200ms │
│ ├─ [会话获取] ──── 50ms │
│ ├─ [历史消息] ──── 50ms │
│ └─ [用户画像] ──── 100ms │
│ ↑ 取 max │
│ Batch 1 耗时: ~200ms │
│ │ │
│ Batch 2 (依赖 Batch 1 结果): │
│ └─ [权限校验 + 最终组装] ── 20ms │
│ │
│ 总计: ~220ms (省 45%) │
└─────────────────────────────────────────────┘
关键设计点:
这种并行化思路不仅适用于请求编排阶段,还可以推广到:
| 场景 | 可并行的操作 | 预期收益 |
|---|---|---|
| 配置预加载 | 60+ 个配置项从远程中心拉取 | N×RTT → ~1×RTT |
| 工具结果收集 | 多个独立工具同时执行 | 受限于最慢的工具 |
| 多模态内容处理 | 文本/图片/文件同时解析和嵌入 | 总耗时取 max |
| 指标 | 优化前 (串行) | 优化后 (并行) |
|---|---|---|
| 请求编排阶段耗时 | ~400ms | ~220ms |
| 配置批量加载耗时 (60项) | ~1000ms | ~80ms |
| 正常请求的处理延迟改善 | — | ~45%↓ |
以上 7 个方向并非互相独立,它们覆盖了 Agent 系统生命周期的不同阶段:
复制代码┌─────────────────────────────────────────────────────────┐
│ Agent 请求生命周期 │
│ │
│ ┌──────────┐ │
│ │ 启动阶段 │ ← 方向一: 分层就绪 │
│ │ (冷启动) │ 方向六: Worker 预热池 │
│ └─────┬─────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ 编排阶段 │ ← 方向七: 并行化编排 │
│ │ (前置处理) │ │
│ └─────┬─────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ 推理阶段 │ ← 方向二: 引擎复用 │
│ │ (多轮循环) │ 方向三: Prompt 裁剪 │
│ │ │ 方向四: 资源池化 (沙箱) │
│ └─────┬─────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ 持久化 │ ← 方向五: 异步 debounce 写入 │
│ │ (每轮结束) │ │
│ └──────────┘ │
│ │
│ 贯穿全程: │
│ ├─ 方向六: Skill 预加载 │
│ └─ 方向七: 配置批量并行加载 │
└─────────────────────────────────────────────────────────┘
| 优化方向 | 核心收益 | 实施复杂度 | 风险等级 |
|---|---|---|---|
| 分层就绪 | 启动加速 96% | 低 | 低 |
| 引擎复用 | 消除重复工具调用 + 每轮省 50~200ms | 低 | 低 |
| Prompt 裁剪 | Token 消耗降 97% | 中 | 低 |
| 资源池化 | 重量级资源获取从秒级降至亚秒 | 高 | 中 |
| 异步持久化 | 单轮省 200ms~2s | 中 | 低 |
| 预热预分配 | 首条消息 <50ms | 中 | 中 |
| 并行编排 | 请求编排阶段省 45% | 低 | 低 |
感谢你能看到这里,本文分享了关于Agent项目性能优化的一些思考,如果你也有类似的开发经历,欢迎交流~~~