Agent Basic / 17
异步 Agent 与事件驱动架构
前面的文章(05 篇 Tool Calling、09 篇 Agent Infra)描述的都是同步场景:用户发请求,Agent 调用工具,工具几秒内返回结果,Agent 生成回复。整个链路在一个 HTTP 请求或 WebSocket 帧内闭合。
但真实系统里,工具不总是秒回的。CI/CD 构建要五分钟,人工审批要半小时,大数据 ETL 要几个小时。如果 Agent 一直等着,不仅浪费算力,还可能触发超时被杀掉。
这篇文章聚焦一个核心问题:当工具执行时间远超模型推理时间时,Agent 架构需要怎样演进?
同步模型 vs 异步现实的矛盾
LLM 的训练范式是同步的:给定输入序列,预测下一个 token。Tool Calling 协议也是同步设计:模型输出 tool_call,客户端执行,把 tool_result 拼回 messages 数组,继续下一轮推理。
这套模型隐含了一个假设:工具执行是”瞬时”的。
现实打破了这个假设:
| 工具类型 | 典型耗时 | 同步等待的代价 |
|---|---|---|
| 代码搜索 / 知识库查询 | 1-5 秒 | 可以接受 |
| CI 构建 / 单元测试 | 3-15 分钟 | GPU/内存空转,连接超时 |
| 人工审批 | 10 分钟 - 数小时 | 完全不可接受 |
| 大数据处理 / 模型训练 | 数小时 - 数天 | 不可能同步等待 |
矛盾的本质:LLM 是无状态的函数调用,但异步工具要求系统有持久状态来记住”我在等什么”以及”等到了之后该做什么”。
sequenceDiagram
participant U as 用户
participant A as Agent
participant T as 长时工具
U->>A: 请求(部署到生产环境)
A->>T: 发起 CI 构建
Note over A: 同步模式:空等 10 分钟<br/>异步模式:释放资源,挂起
T-->>A: 构建完成(10 分钟后)
A->>U: 部署成功
三种异步执行模式
取消-重提交模式(Cancellation-based)
最简单的异步化方式:发起长任务后,Agent 主动挂起当前执行。任务完成后,系统重新构建 Context 并恢复推理。
flowchart TD
A[Agent 推理] --> B{工具是否为长任务?}
B -->|否| C[同步执行,拼回结果]
B -->|是| D[发起任务,获取 task_id]
D --> E[序列化当前 Context + task_id]
E --> F[释放 Agent 资源]
F --> G[等待回调/轮询]
G --> H[任务完成]
H --> I[加载 Context + 结果]
I --> J[恢复 Agent 推理]
C --> J
实现要点:
- Context 序列化:挂起前,完整保存 messages 数组、当前 system prompt、工具定义。恢复时原封不动地加载回来,对模型来说就像”刚返回了结果”。
- 幂等设计:长任务可能被重复触发(比如超时重试),工具侧必须做幂等校验。
- 超时兜底:设置最大等待时间,超时后注入
tool_result: "timeout"让模型决定下一步。
适合场景:单个长工具调用、审批流、CI 触发。
队列模式(Queue-based)
Agent 不直接调用工具,而是把任务写入队列。独立的 Worker 消费队列、执行工具、通过回调把结果送回。
flowchart LR
A[Agent] -->|enqueue| Q[(任务队列)]
Q --> W1[Worker 1]
Q --> W2[Worker 2]
Q --> W3[Worker 3]
W1 -->|callback| R[(结果存储)]
W2 -->|callback| R
W3 -->|callback| R
R -->|notify| A
适合场景:批量操作(一次发 20 个文件转换任务)、多步工作流中的并行子任务、需要限流和背压控制的高并发场景。
优势在于解耦:Agent 不需要知道 Worker 的数量、位置和负载情况。Worker 可以独立扩缩容,Queue 提供天然的背压能力。
并发模式(Parallel)
多个独立工具调用同时发出,全部完成(或部分超时)后统一处理。这不是”异步”的字面含义,但它解决的问题一样:减少总等待时间。
sequenceDiagram
participant A as Agent
participant T1 as 搜索引擎
participant T2 as 知识库
participant T3 as 数据库
A->>T1: 查询关键词
A->>T2: 向量检索
A->>T3: SQL 查询
Note over A: 并发等待,总耗时 = max(T1, T2, T3)
T2-->>A: 结果 2(1.2s)
T1-->>A: 结果 1(2.1s)
T3-->>A: 结果 3(3.5s)
A->>A: 合并结果,继续推理
实现上通常用 asyncio.gather / Promise.all 或类似机制。关键决策是超时策略:全部等齐还是”够用就走”(如 3 个源返回了 2 个就开始推理)。
三种模式对比
| 维度 | 取消-重提交 | 队列模式 | 并发模式 |
|---|---|---|---|
| 适用场景 | 单个长任务 | 批量/多步任务 | 多源信息收集 |
| 实现复杂度 | 中(需要 Context 持久化) | 高(需要队列 + Worker + 回调) | 低(语言层面原生支持) |
| 资源效率 | 高(挂起时不占资源) | 高(Worker 独立扩缩) | 中(并发等待期间占连接) |
| 故障恢复 | 需要 checkpoint | 队列天然支持重试 | 单个失败可降级 |
| 典型耗时范围 | 分钟-小时 | 分钟-天 | 秒-分钟 |
事件驱动 Agent
前面三种模式的共同点是:依然由用户请求触发。Agent 收到指令,发起异步任务,等待结果。
事件驱动 Agent 更进一步:不需要用户主动请求,外部事件本身就是触发条件。
从请求驱动到事件驱动
flowchart TD
subgraph 请求驱动
U[用户] -->|"帮我 review 这个 PR"| A1[Agent]
end
subgraph 事件驱动
E1[GitHub Webhook: PR created] --> EB[Event Bus]
E2[Prometheus Alert: CPU > 90%] --> EB
E3[Cron: 每天 9:00] --> EB
EB --> Router[Event Router]
Router --> D[Agent Dispatcher]
D --> A2[Code Review Agent]
D --> A3[诊断 Agent]
D --> A4[日报 Agent]
end
事件驱动架构的核心组件
| 组件 | 职责 | 实现选型 |
|---|---|---|
| Event Source | 产生事件(webhook、定时器、数据库 CDC) | GitHub/GitLab webhook、CloudWatch、Kafka Connect |
| Event Bus | 事件传输和持久化 | Kafka、NATS、Redis Streams、SQS |
| Event Router | 事件过滤、路由、去重 | EventBridge Rules、自定义 router |
| Agent Dispatcher | 选择 Agent 实例、注入上下文、启动执行 | 自研调度器 |
| Execution Runtime | Agent 运行环境 | Temporal Worker、K8s Job、Lambda |
典型场景
代码 PR 提交 → 自动触发 Code Review Agent:
- GitHub 发送
pull_request.openedwebhook - Event Router 匹配规则:repo=X, files_changed 包含 .py
- Dispatcher 启动 Code Review Agent,注入 PR diff 作为上下文
- Agent 执行审查,结果写回 PR comment
监控告警 → 触发诊断 Agent:
- Prometheus 触发 alert:某服务 P99 延迟超过 5s
- Event Router 判断严重级别,选择对应 Agent
- Dispatcher 注入告警详情 + 最近 15 分钟日志
- Agent 执行诊断链路,输出根因分析报告
定时任务 → 触发数据分析 Agent:
- Cron 每天 9:00 产生
daily_report事件 - Agent 拉取前一天的业务数据
- 生成分析报告,推送到 Slack/邮件
和 Workflow 编排的区别
容易混淆的是:事件驱动 Agent 和 Airflow/Temporal 这类 Workflow 编排有什么区别?
| 维度 | Workflow 编排 | 事件驱动 Agent |
|---|---|---|
| 触发方式 | 预定义的 DAG/流程图 | 响应式,事件到来才启动 |
| 执行路径 | 确定性的(分支也是预定义的) | 不确定性的(LLM 动态决策) |
| 适合什么 | 步骤固定的 ETL、审批流 | 需要推理判断的开放性任务 |
| 失败处理 | 重试固定步骤 | Agent 自主选择替代方案 |
两者不互斥。常见模式是:Workflow 编排负责宏观流程,某些需要推理的节点委托给 Agent 执行。
连续时间推理(Continuous-time Reasoning)
同步训练的 LLM 面对异步场景,有一个根本问题:它的”思考”和”等待”不能同时进行。模型要么在推理,要么在等结果,不像人类可以”边等边想”。
核心技巧:预生成后续步骤
在等待 Tool A 结果时,让模型预先规划后续动作:
Context 注入策略:
[当前状态]
- Tool A (CI 构建) 已发起,预计 8 分钟后返回
- Tool B (代码检查) 结果已返回:3 个 lint warning
[模型被要求做的事]
基于已有信息,预生成:
1. 如果 CI 成功,下一步做什么
2. 如果 CI 失败,下一步做什么
3. 当前已有的 lint warning 能否先处理
这种方式让模型在等待期间产出有价值的规划,而不是纯粹空等。
实现模式:Speculative Execution
flowchart TD
A[发起长工具 A] --> B[挂起等待]
A --> C[预推理:假设 A 成功的后续计划]
A --> D[预推理:假设 A 失败的后续计划]
B --> E{A 的实际结果}
E -->|成功| F[执行预生成的成功路径]
E -->|失败| G[执行预生成的失败路径]
C -.-> F
D -.-> G
注意:预推理的结果不一定能直接用。实际结果和预期可能有差异(比如 CI 失败的原因和预想不同),此时需要丢弃预推理结果,重新推理。
Context 中的”等待状态”注入
给模型的 system prompt 或 messages 中注入时间感知信息:
[系统状态快照 - 2025-03-15 14:35:00]
正在执行的异步任务:
1. deploy_staging (已等待 3m22s / 预计总时长 8m)
2. run_integration_tests (已等待 1m05s / 预计总时长 5m)
已完成的任务:
1. lint_check → 通过,0 errors
2. unit_test → 通过,覆盖率 87%
你可以:
- 基于已完成的结果继续规划
- 预判正在执行任务的可能结果并准备应对方案
- 如果有不依赖当前等待任务的工作,现在就执行
Safety Sidecar 模式
异步架构引入一个安全问题:如果 Agent 的决策和安全检查是串行的,延迟会翻倍。
传统串行安全检查
Agent 决策 (200ms) → 安全检查 (150ms) → 执行工具 (Xms)
总额外延迟:350ms per tool call
10 次工具调用:3.5s 额外延迟
当 Agent 一次任务调用几十个工具时,串行安全检查的累积延迟就不可忽视了。
Sidecar 并行架构
flowchart LR
subgraph 主线程
A[Agent 推理] --> B[生成 tool_call]
B --> C[执行工具]
C --> D[下一轮推理]
end
subgraph Safety Sidecar
B --> S1[Intent 分类]
B --> S2[参数风险评估]
B --> S3[上下文异常检测]
S1 --> V{汇合点}
S2 --> V
S3 --> V
end
V -->|通过| C
V -->|拦截| Block[阻断执行 + 告警]
关键设计:Agent 主线程和 Safety Sidecar 并行启动,在执行前汇合。如果 Sidecar 的判断快于工具执行的准备阶段,就不会增加额外延迟。
Sidecar 的判断维度
| 维度 | 检查内容 | 示例 |
|---|---|---|
| Intent Classification | 工具调用的意图是否和用户原始请求一致 | 用户问天气,Agent 却调用了支付接口 |
| Parameter Risk | 参数值是否在安全范围内 | 删除操作的 scope 是 “all” 而非特定 ID |
| Context Anomaly | 当前调用链是否偏离正常模式 | 前 3 次都是查询,突然变成写入操作 |
| Rate Anomaly | 调用频率是否异常 | 同一工具 1 秒内被调用 50 次 |
Sidecar 本身可以是一个轻量 LLM(如 Haiku 级别),也可以是基于规则的分类器。选择取决于延迟预算:规则引擎 <5ms,轻量 LLM 50-100ms。
工具执行的安全隔离
异步执行的工具往往涉及副作用(写文件、发请求、操作数据库)。隔离级别决定了工具”能伤害多大范围”。
三层隔离
graph TD
subgraph L1["OS 级隔离"]
F[文件系统 chroot/restrict]
N[网络 allowlist]
P[进程权限降级]
end
subgraph L2["容器级隔离"]
D[Docker namespace]
CG[cgroup 资源限制]
RO[只读文件系统]
end
subgraph L3["microVM 级隔离"]
VM[完整虚拟化]
HW[硬件级隔离]
NET[独立网络栈]
end
L1 --> L2 --> L3
style L1 fill:#e8f5e9
style L2 fill:#fff3e0
style L3 fill:#fce4ec
隔离级别对比
| 维度 | OS 级 | 容器级 | microVM 级 |
|---|---|---|---|
| 启动时间 | 无额外开销 | 100ms-1s | 1-5s |
| 内存开销 | 无 | 20-50MB | 128MB+ |
| 隔离强度 | 弱(共享内核) | 中(共享内核但 namespace 隔离) | 强(独立内核) |
| 逃逸风险 | 高 | 中(内核漏洞可逃逸) | 低 |
| 适合工具 | 只读查询、计算型工具 | 文件读写、网络请求 | 任意代码执行、不可信插件 |
| 典型实现 | seccomp, AppArmor | Docker, gVisor | Firecracker, Cloud Hypervisor |
选择依据
决策树:
- 工具是否执行用户提供的代码? → 是:microVM
- 工具是否有网络或文件系统副作用? → 是:容器级
- 工具是否为纯计算/只读查询? → 是:OS 级或无隔离
实际工程中,常见做法是对工具打”信任标签”:
tools:
- name: web_search
trust_level: high # 无副作用,OS 级即可
isolation: os
- name: run_python
trust_level: low # 执行任意代码
isolation: microvm
resource_limits:
cpu: 1 core
memory: 512MB
timeout: 30s
network: deny
- name: write_file
trust_level: medium # 有副作用但受控
isolation: container
resource_limits:
writable_paths: ["/workspace/output"]
network: allow_list: ["internal-api.company.com"]
异步系统的可观测性挑战
同步系统的 trace 是一条连续的线:请求进来,处理完成,响应出去。异步系统的 trace 被时间打断了。
问题:断裂的 Trace
sequenceDiagram
participant User
participant Agent
participant Queue
participant Worker
participant Callback
Note over User,Callback: Span 1: 发起阶段
User->>Agent: 请求
Agent->>Queue: 入队任务
Agent->>User: "已提交,请等待"
Note over User,Callback: --- 时间间隔:10 分钟 ---
Note over User,Callback: Span 2: 执行阶段
Queue->>Worker: 消费任务
Worker->>Worker: 执行
Worker->>Callback: 结果回调
Note over User,Callback: Span 3: 恢复阶段
Callback->>Agent: 注入结果
Agent->>User: 最终回复
三个 Span 在时间上可能相隔几分钟到几小时,但逻辑上是同一个任务。如果没有关联机制,调试时根本找不到它们之间的关系。
解决方案:Correlation ID
每个异步任务在发起时生成唯一的 correlation_id,贯穿整个生命周期:
{
"correlation_id": "task_abc123",
"trace_id": "trace_xyz",
"spans": [
{"phase": "initiate", "timestamp": "14:00:00", "duration": "200ms"},
{"phase": "execute", "timestamp": "14:08:32", "duration": "45s"},
{"phase": "resume", "timestamp": "14:09:17", "duration": "1.2s"}
],
"total_wall_time": "9m17s",
"total_compute_time": "46.4s"
}
区分 wall_time(用户视角的总等待时间)和 compute_time(实际消耗计算资源的时间),这对成本归因至关重要。
事件驱动系统的特有挑战
| 挑战 | 表现 | 应对 |
|---|---|---|
| Dead Letter Queue | 事件处理失败 N 次后进入 DLQ,无人处理 | DLQ 深度监控 + 告警,定期人工审查 |
| 事件乱序 | 网络延迟导致事件到达顺序和发生顺序不一致 | 事件携带时间戳 + 序列号,消费端排序 |
| 重复投递 | At-least-once 语义导致同一事件被处理多次 | 消费端幂等设计,用 event_id 去重 |
| 事件风暴 | 某个源突然产生大量事件,压垮下游 | 限流 + 背压 + 采样降级 |
| 因果链丢失 | 事件 A 触发了事件 B,但 trace 中看不出关联 | 事件中携带 caused_by 字段,构建因果图 |
监控仪表盘关键指标
对于异步 Agent 系统,以下指标是必须监控的:
异步任务维度:
- 排队深度(当前等待执行的任务数)
- 排队时长 P95(任务等了多久才被消费)
- 执行成功率(按工具类型分组)
- 端到端延迟分布(从发起到最终完成)
事件驱动维度:
- 事件吞吐量(按 source 和 type 分组)
- 事件处理延迟 P95
- DLQ 深度及增长趋势
- Agent 触发频率和成功率
小结
- LLM 的同步推理模型和真实工具的异步执行之间存在根本矛盾,需要架构层面的方案来弥合。
- 三种异步模式各有适用场景:取消-重提交适合单个长任务,队列模式适合批量操作,并发模式适合多源信息收集。
- 事件驱动 Agent 不需要用户主动请求,由外部事件(webhook、告警、定时器)触发执行,是 Agent 从被动工具走向主动服务的关键架构演进。
- 连续时间推理通过预生成后续步骤、注入等待状态,让模型在异步等待期间仍能产出价值。
- Safety Sidecar 通过并行化安全检查避免串行延迟累积,是异步 Agent 安全架构的核心模式。
- 工具隔离分三层(OS / 容器 / microVM),根据工具的信任等级和副作用范围选择。
- 异步系统的可观测性核心是 Correlation ID:把被时间打断的多个 Span 关联为同一个逻辑任务。
参考资料
- 李博杰《深入理解 AI Agent:设计原理与工程实践》,第四章(固定提交
e3883f8c,本文按自己的结构与示例重新整理)