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:

  1. GitHub 发送 pull_request.opened webhook
  2. Event Router 匹配规则:repo=X, files_changed 包含 .py
  3. Dispatcher 启动 Code Review Agent,注入 PR diff 作为上下文
  4. Agent 执行审查,结果写回 PR comment

监控告警 → 触发诊断 Agent:

  1. Prometheus 触发 alert:某服务 P99 延迟超过 5s
  2. Event Router 判断严重级别,选择对应 Agent
  3. Dispatcher 注入告警详情 + 最近 15 分钟日志
  4. Agent 执行诊断链路,输出根因分析报告

定时任务 → 触发数据分析 Agent:

  1. Cron 每天 9:00 产生 daily_report 事件
  2. Agent 拉取前一天的业务数据
  3. 生成分析报告,推送到 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

选择依据

决策树:

  1. 工具是否执行用户提供的代码? → 是:microVM
  2. 工具是否有网络或文件系统副作用? → 是:容器级
  3. 工具是否为纯计算/只读查询? → 是: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,本文按自己的结构与示例重新整理)