跳转到内容

事件驱动 Agent 信号系统

学习目标

理解 LobeHub 的 AgentSignal 事件驱动架构,包括运行时生命周期事件、渲染器和处理器管道、Redis waypoint 跨实例同步。

前置知识

本章涉及事件驱动 Agent 运行的通用原理,建议先阅读:

下文假设你已理解上述概念,直接聚焦 LobeHub 的具体实现。

项目实践

事件生命周期

AgentSignal 监听 Agent 运行时的关键事件:

事件触发时机用途
runtimeBeforeStep每步执行前注入上下文、记录指标
runtimeAfterStep每步执行后发送流式事件、更新 UI
runtimeResults运行完成处理最终结果

三层事件管道

AgentSignal 内部有三层处理管道:

Redis Waypoint 跨实例同步

在水平部署的多实例环境中,不同实例的 Agent 运行事件需要同步:

// 伪代码:Redis waypoint 存储
class RedisWaypointStore {
async storeWaypoint(operationId: string, waypoint: Waypoint): Promise<void> {
// 将 waypoint 写入 Redis Stream
await this.redis.xadd(`agent:waypoints:${operationId}`, "*", waypoint);
}
async getWaypoints(operationId: string): Promise<Waypoint[]> {
// 从 Redis Stream 读取所有 waypoint
return this.redis.xrange(`agent:waypoints:${operationId}`, "-", "+");
}
}

用途:客户端连接到任意实例时,可以通过 Redis waypoint 获取完整的 Agent 运行历史,即使该运行发生在另一个实例上。

渲染器实现

渲染器将 Agent 运行状态转换为前端可展示的事件:

// 伪代码:runtimeAfterStep 渲染器
function renderAfterStep(step: StepResult): StreamEvent[] {
const events: StreamEvent[] = [];
// 渲染工具调用
if (step.toolCalls) {
events.push({ type: "tool_call", data: step.toolCalls });
}
// 渲染文本输出
if (step.text) {
events.push({ type: "text_chunk", data: step.text });
}
// 渲染状态变更
if (step.status === "waiting_for_human") {
events.push({ type: "waiting_for_approval" });
}
return events;
}

问题与规避

问题场景规避策略
事件丢失Redis 连接断开本地缓存事件队列,重连后回传
渲染器执行顺序多个渲染器依赖特定顺序渲染器按优先级排序
waypoint 数据膨胀长时间运行的 Agent 产生大量 waypoint定期清理过期 waypoint
跨实例延迟Redis 网络延迟导致事件延迟设置合理的超时和重试

设计取舍

为什么用事件驱动而非轮询?

轮询需要客户端定期请求服务器检查状态,延迟高且浪费资源。事件驱动在状态变化时主动推送,实时性好。

代价:事件驱动需要维护 WebSocket/SSE 连接,服务器需要管理连接池。

为什么需要 Redis waypoint?

在多实例部署中,客户端可能连接到与 Agent 运行不同的实例。没有 Redis waypoint,客户端只能获取到当前实例的事件,看不到其他实例上的运行。

替代方案:数据库持久化所有事件。但数据库写入比 Redis Stream 慢,且 Redis Stream 天然支持按 operationId 分片。

参考来源

  • LobeHub AgentSignal 服务 — src/server/services/agentSignal/
  • 事件驱动运行时 — 通用事件驱动原理