欢迎光临
我们一直在努力

第21章 Streaming & 实时 Agent 架构 —— 让用户不用等待的 Agent

综合来源:

  • Claude Code h2A 机制 (Ch8)
  • Event-Driven Agent Architecture (Sandipan Haldar, 2025)
  • A2A Streaming Protocol Design
  • 腾讯云 Agent 实时推理设计

21.1 为什么 Agent 需要实时架构?

传统「请求-响应」模式的问题:用户 → 发送消息 → [等待 30 秒…] → 收到完整回复

Agent 执行过程中:

  • 用户不知道 Agent 在做什么
  • 用户发现方向不对,但只能等 Agent 完成再纠正
  • 每个步骤都是「黑盒」

实时架构的目标:

用户 → 发送消息 → 实时看到 Agent 的思考过程 →中途可以修正方向 → 最终得到结果

核心能力:

  • 流式输出:LLM 每生成一个 token 就推送给用户
  • 中途介入:用户可以在 Agent 执行中途发送消息
  • 工具调用可见:用户能看到 Agent 在调什么工具
  • 可中断:用户可以随时停止 Agent 的任务

这是 Claude Code h2A 机制(Ch8)和 Ch13 SSE/WebSocket 的「为什么」解释 —— 本章讲架构设计层面。

21.2 事件驱动架构 (EDA) —— Agent 系统的骨架

核心思想:

传统架构:线性调用 → 阻塞等待 → 同步返回

事件驱动:发布事件 → 异步处理 → 流式推送

Agent 的关键事件类型:

📊 架构示意

┌─────────────────────────────────────────────┐
│ Agent 事件体系 │
├──────────────┬──────────────────────────────┤
│ 事件类型 │ 说明 │
├──────────────┼──────────────────────────────┤
│ user_input │ 用户发送消息 │
│ thinking │ Agent 思考中 │
│ token_stream │ LLM 流式输出的一个 token │
│ tool_call │ Agent 决定调用工具 │
│ tool_result │ 工具执行完成 │
│ interrupt │ 用户中途介入/停止 │
│ error │ 发生错误 │
│ done │ Agent 任务完成 │
└──────────────┴──────────────────────────────┘

关键组件:

EventBus:事件总线,所有组件通过它通信

→ 解耦 LLM 调用、工具执行、UI 更新

StateManager:状态管理器,唯一数据源

→ 记录 Agent 当前步骤、对话历史、工具执行状态

StreamBridge:流式桥接

→ 将 LLM 的 streaming 输出桥接到前端

21.3 EventBus 模式 —— Agent 架构的核心

问题:Agent 的多个组件(LLM 调用 / 工具执行 / 前端 UI)

怎么高效通信而不互相耦合?

方案:EventBus —— 发布-订阅模式

📊 架构示意

┌──────────────────────────────────────────────────┐
│ EventBus │
│ (消息中枢) │
│ │
│ pub/sub pub/sub pub/sub │
│ token_stream tool_call interrupt │
│ ↕ ↕ ↕ │
│ ┌────────┐ ┌──────────┐ ┌──────────┐ │
│ │ LLM │ │ Tool │ │ User │ │
│ │ Service│ │ Executor │ │ Interface│ │
│ └────────┘ └──────────┘ └──────────┘ │
└──────────────────────────────────────────────────┘

EventBus 的优势:

  • 解耦:LLM 不需要知道 UI 的存在
  • 可测试:可以 Mock EventBus 单独测每个组件
  • 可扩展:新组件只需订阅感兴趣的事件
  • 可观测:所有事件都可以记录(天然 Tracing)

类比:

就像办公室里的广播系统 ——

没人需要知道「谁在听」,只需要「把消息广播出去」。

📝 对应的代码实现

subscribepublishget_historyreduceshould_interruptget_status_displayAgentEventEventBusAgentStateManager

import asyncio
import time
from typing import Any, Callable, Optional
from collections import defaultdict
from dataclasses import dataclass, field

class="d">@dataclass
class AgentEvent:
"""Agent 事件基类。"""
event_type: str
data: Any = None
timestamp: float = field(default_factory=time.time)
session_id: str = "default"

class EventBus:
"""Agent 事件总线 —— 发布-订阅模式的核心。

所有 Agent 组件通过 EventBus 通信,不需要直接引用对方。
这是 Event-Driven Agent 架构的基础设施。

特性:
1. 支持多个订阅者监听同一事件类型
2. 支持通配符订阅 (*)
3. 记录事件历史(用于调试和 Tracing)
4. 异步处理(不阻塞发布者)
"""

def __init__(self):
self._subscribers: dict[str, list[Callable]] = defaultdict(list)
self._history: list[AgentEvent] = []
self._max_history = 1000

def subscribe(self, event_type: str,
callback: Callable[[AgentEvent], None]):
"""订阅事件类型。

Args:
event_type: 事件类型(支持 '*' 通配符)。
callback: 事件处理回调函数。
"""
self._subscribers[event_type].append(callback)

def publish(self, event: AgentEvent):
"""发布事件(同步)。

所有匹配的订阅者都会被通知。
"""
self._history.append(event)
if len(self._history) > self._max_history:
self._history = self._history[-self._max_history:]

# 精确匹配
for callback in self._subscribers.get(event.event_type, []):
callback(event)
# 通配符匹配
for callback in self._subscribers.get("*", []):
callback(event)

async def publish_async(self, event: AgentEvent):
"""异步发布事件(用于需要 await 的回调)。"""
self._history.append(event)
tasks = []
for callback in self._subscribers.get(event.event_type, []):
if asyncio.iscoroutinefunction(callback):
tasks.append(asyncio.create_task(callback(event)))
else:
callback(event)
for callback in self._subscribers.get("*", []):
if asyncio.iscoroutinefunction(callback):
tasks.append(asyncio.create_task(callback(event)))
else:
callback(event)
if tasks:
await asyncio.gather(*tasks)

def get_history(self, event_type: str = None,
limit: int = 50) -> list[AgentEvent]:
"""获取事件历史(用于调试)。"""
if event_type:
filtered = [e for e in self._history
if e.event_type == event_type]
return filtered[-limit:]
return self._history[-limit:]

class AgentStateManager:
"""Agent 状态管理器 —— 单一数据源。

使用纯函数(Reducer)更新状态,确保状态变更可预测。
"""

def __init__(self, session_id: str):
self.session_id = session_id
self.state = {
"status": "idle", # idle/thinking/acting/done/interrupted
"current_step": 0,
"tool_calls": [],
"last_tool_result": None,
"messages": [],
"error": None,
"interrupt_requested": False,
}

def reduce(self, event: AgentEvent):
"""纯函数式状态更新(Reducer 模式)。

根据事件类型更新状态,没有副作用。
"""
etype = event.event_type

if etype == "thinking":
self.state["status"] = "thinking"
self.state["current_step"] += 1

elif etype == "tool_call_start":
self.state["status"] = "acting"
self.state["tool_calls"].append({
"tool": event.data.get("tool_name"),
"args": event.data.get("args"),
"status": "pending",
"started_at": event.timestamp,
})

elif etype == "tool_call_end":
if self.state["tool_calls"]:
self.state["tool_calls"][-1]["status"] = "done"
self.state["tool_calls"][-1]["result"] = event.data
self.state["last_tool_result"] = event.data

elif etype == "user_interrupt":
self.state["interrupt_requested"] = True
self.state["status"] = "interrupted"

elif etype == "resume":
self.state["interrupt_requested"] = False
self.state["status"] = "thinking"

elif etype == "done":
self.state["status"] = "done"

elif etype == "error":
self.state["status"] = "error"
self.state["error"] = event.data

def should_interrupt(self) -> bool:
"""检查是否需要中断。"""
return self.state["interrupt_requested"]

def get_status_display(self) -> dict:
"""获取当前状态摘要(用于 UI 展示)。"""
return {
"status": self.state["status"],
"step": self.state["current_step"],
"active_tools": [t["tool"] for t in self.state["tool_calls"]
if t["status"] == "pending"],
}

21.4 动态中断 (Dynamic Interrupt) —— Agent 交互的质变

这是 Agent 从「自动化脚本」升级到「交互式伙伴」的关键能力。

传统模式:提交任务 → 等完成 → 看结果 → (不满意)→ 重新提交

中断模式:提交任务 → 观察进行中 → 途中发现方向不对 → 立即纠正

架构实现:

  • EventBus 监听 interrupt 事件
  • Agent 循环每步检查 should_interrupt()
  • 如果中断 → 暂停执行 → 等待用户新指令
  • 用户发送新指令 → resume 事件 → 继续执行

和 Claude Code h2A 的关系(Ch8):

h2A 是「双缓冲异步队列」

EventBus 模式是 h2A 的「通用化实现」

关键在于中断的「粒度」:

  • 太粗:等当前工具调完才知道 → 延迟大
  • 太细:每收到一个 token 就检查 → 开销大
  • 最优:每个 Agent 步骤结束时检查(Claude Code 的做法)

📝 对应的代码实现

request_interruptresumeInterruptibleAgent

import asyncio
import time
from typing import Any, Callable, Optional
from collections import defaultdict
from dataclasses import dataclass, field

class InterruptibleAgent:
"""可中断 Agent —— 演示动态中断的完整流程。

结合 EventBus + StateManager + 模拟 LLM。
"""

def __init__(self, session_id: str = "demo"):
self.bus = EventBus()
self.state = AgentStateManager(session_id)
self.cancelled = False

# 注册事件处理
self.bus.subscribe("user_interrupt",
lambda e: self.state.reduce(e))
self.bus.subscribe("resume",
lambda e: self.state.reduce(e))

def request_interrupt(self, reason: str = ""):
"""用户请求中断。"""
self.bus.publish(AgentEvent(
"user_interrupt",
{"reason": reason},
session_id=self.state.session_id,
))
print(f" ⏸️ 中断请求: {reason}")

def resume(self, new_instruction: str = ""):
"""用户恢复执行(可带新指令)。"""
self.bus.publish(AgentEvent(
"resume",
{"new_instruction": new_instruction},
session_id=self.state.session_id,
))
print(f" ▶️ 恢复执行" +
(f"(新指令: {new_instruction})" if new_instruction else ""))

async def run(self, task: str, max_steps: int = 10):
"""模拟 Agent 执行(可被中断)。"""
print(f" 🎯 任务: {task}")

for step in range(1, max_steps + 1):
# 第1步:检查中断
if self.state.should_interrupt():
print(f" ⏸️ Agent 在第 {step} 步暂停,等待用户…")
yield {"type": "interrupted", "step": step}
return
if self.cancelled:
yield {"type": "cancelled", "step": step}
return

# 第2步:思考
self.bus.publish(AgentEvent("thinking"))
yield {"type": "thinking", "step": step,
"message": f"正在分析第 {step} 步…"}
await asyncio.sleep(0.3)

# 第3步:模拟工具调用
tool_name = f"tool_{step}"
self.bus.publish(AgentEvent(
"tool_call_start",
{"tool_name": tool_name, "args": {"step": step}},
))
yield {"type": "tool_call", "step": step,
"tool": tool_name}
await asyncio.sleep(0.3)

# 第4步:工具结果
self.bus.publish(AgentEvent(
"tool_call_end",
f"工具 {tool_name} 执行成功",
))
yield {"type": "tool_result", "step": step,
"result": f"{tool_name} 完成"}
await asyncio.sleep(0.2)

# 完成
self.bus.publish(AgentEvent("done"))
yield {"type": "done", "step": max_steps + 1,
"message": "所有步骤完成"}

21.5 背压控制 (Backpressure) —— 生产环境的隐性需求

场景:

LLM 的 token 生成速度快于前端的渲染速度

→ 前端缓冲区满了 → 如果不处理 → 丢数据或 OOM

解决方案:

  • 缓冲区控制

前端维护一个固定大小的缓冲队列

满了就暂停 SSE 流

  • 自适应降频

如果前端处理变慢 → 减少推送频率

从「每 token 推送」降级到「每 5 token 推送」

  • 生产者-消费者模式

生产者(LLM)→ 队列 → 消费者(前端)

队列有容量上限,满了暂停生产者

这就是 Claude Code h2A 的「智能背压控制」

(Ch8 提到:吞吐量 > 10,000 消息/秒 + 智能背压)。

📝 对应的代码实现

statsdemo_event_driven_agentBackpressureController

import asyncio
import time
from typing import Any, Callable, Optional
from collections import defaultdict
from dataclasses import dataclass, field

class BackpressureController:
"""背压控制器 —— 防止生产者速度超过消费者。

生产者-消费者队列,内置流控机制。
"""

def __init__(self, max_size: int = 100):
self.queue = asyncio.Queue(maxsize=max_size)
self.dropped = 0 # 丢弃的事件数
self.total = 0

async def produce(self, event: AgentEvent) -> bool:
"""生产者:发布事件。

Returns:
是否成功入队。
"""
self.total += 1
try:
self.queue.put_nowait(event)
return True
except asyncio.QueueFull:
self.dropped += 1
return False

async def consume(self) -> AgentEvent:
"""消费者:取出事件。"""
return await self.queue.get()

def stats(self) -> dict:
"""背压统计。"""
return {
"queue_size": self.queue.qsize(),
"total_produced": self.total,
"dropped": self.dropped,
"drop_rate": self.dropped / max(self.total, 1),
}

def demo_event_driven_agent():
"""演示事件驱动 Agent 的完整流程。"""
print("=" * 60)
print(" 事件驱动 + 动态中断 演示")
print("=" * 60)

bus = EventBus()
state = AgentStateManager("demo_session")

# 注册事件处理器
event_log = []
bus.subscribe("*", lambda e: event_log.append(
f"[{e.event_type}] {e.data}"
))

bus.subscribe("thinking",
lambda e: state.reduce(e))
bus.subscribe("tool_call_start",
lambda e: state.reduce(e))
bus.subscribe("tool_call_end",
lambda e: state.reduce(e))

# 模拟 Agent 执行流程
print("\\n 🔄 模拟 Agent 执行…")
steps = [
("thinking", "正在分析任务…"),
("tool_call_start", {"tool_name": "search", "args": {"q": "AI"}}),
("tool_call_end", "找到 5 条结果"),
("thinking", "正在整理答案…"),
("user_interrupt", "等等,换一个方向!"),
]

for etype, data in steps:
event = AgentEvent(etype, data, session_id="demo_session")
bus.publish(event)
print(f" 📡 事件: {etype} → 状态: {state.get_status_display()['status']}")

print(f"\\n 📋 事件历史 ({len(event_log)} 条):")
for entry in event_log:
print(f" {entry}")

# 演示中断
print(f"\\n ⚡ 中断检测: {state.should_interrupt()}")

21.6 本章总结

核心要点回顾:

  • Event-Driven = Agent 实时交互的基础
  • EventBus 解耦各组件
  • StateManager 做唯一数据源
  • 事件驱动使中断、流式输出、工具调用可见成为可能
  • 动态中断
  • 用户可以在 Agent 执行中途「注入」新指令
  • 核心机制:EventBus + should_interrupt() 检查
  • 粒度控制:每步结束时检查(最优平衡)
  • 背压控制
  • 生产者(LLM)vs 消费者(前端)速度不匹配
  • 解决方案:有界队列 + 自适应降频
  • 这是从 Demo 到 Production 的关键一步
  • 架构和 Ch8/Ch13 的关系
  • Ch8 Claude Code h2A:一种具体的实现
  • Ch13 SSE/WebSocket:传输层实现
  • Ch21 EventBus:架构层抽象(本章)

面试速记:

"Agent 怎么让用户可以中途介入?"

→ Event-Driven 架构 + EventBus

→ Agent 每步检查 interrupt 信号

→ h2A 双缓冲队列是 Claude Code 的具体实现

→ 关键是中断粒度:每步结束时检查

📝 对应的代码实现

import asyncio
import time
from typing import Any, Callable, Optional
from collections import defaultdict
from dataclasses import dataclass, field

if __name__ == "__main__":
print("╔══════════════════════════════════════════════════════╗")
print("║ 第21章:Streaming & 实时 Agent 架构 ║")
print("║ EventBus · 动态中断 · 背压控制 · 状态机 ║")
print("╚══════════════════════════════════════════════════════╝")

demo_event_driven_agent()

print("\\n▶ Agent 关键事件类型")
print("-" * 50)
events = [
("user_input", "用户发送消息"),
("thinking", "Agent 思考中"),
("token_stream", "LLM 流式 token"),
("tool_call", "Agent 调用工具"),
("tool_result", "工具返回结果"),
("interrupt", "用户中途介入/停止"),
("error", "错误发生"),
("done", "任务完成"),
]
for etype, desc in events:
print(f" {etype:15s} → {desc}")

print("\\n▶ 实时架构 Checklist")
print("-" * 50)
items = [
"EventBus 解耦组件通信",
"StateManager 单一数据源(纯函数 Reducer)",
"每步结束检查中断信号",
"背压控制防止生产者超过消费者",
"事件历史用于 Tracing 和调试",
]
for item in items:
print(f" ✅ {item}")

print("\\n✅ 第21章完成!")


赞(0)
未经允许不得转载:171主机测评 » 第21章 Streaming & 实时 Agent 架构 —— 让用户不用等待的 Agent
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址