多Agent系统的通信协议:事件总线、消息格式与服务发现的标准化设计

一、Agent间通信的根本挑战

多Agent系统区别于传统微服务的关键在于:Agent不仅是请求的被动响应者,更是具有自主决策能力的协作节点。一个Agent可能根据任务进展主动向其他Agent发起通信、委托子任务或广播状态变更。这要求通信协议在可靠性、可追溯性和语义表达力上超越传统的REST/gRPC。

二、消息格式的标准化——CloudEvents与自定义选择

消息格式的选择取决于互通性需求。如果多Agent系统涉及与外部系统的集成,CloudEvents是一个被广泛采纳的标准。如果只在内部Agent间通信,更紧凑的自定义格式可以减少序列化开销。

// CloudEvents规范的消息结构
// 符合 CNCF CloudEvents v1.0 规范
type CloudEvent struct {
    // 必填字段
    ID              string `json:"id"`               // 唯一事件标识
    Source          string `json:"source"`            // 事件来源(Agent标识)
    SpecVersion     string `json:"specversion"`       // CloudEvents版本:"1.0"
    Type            string `json:"type"`              // 事件类型:如 "task.assigned"
    
    // 可选字段
    DataContentType string      `json:"datacontenttype,omitempty"` // "application/json"
    DataSchema      string      `json:"dataschema,omitempty"`       // JSON Schema URI
    Subject         string      `json:"subject,omitempty"`          // 事件主题
    Time            time.Time   `json:"time,omitempty"`             // 事件发生时间
    Data            interface{} `json:"data,omitempty"`             // 事件载荷
    
    // 扩展上下文属性(Agent系统特有)
    TaskID          string `json:"taskid,omitempty"`
    AgentRole       string `json:"agentrole,omitempty"`       // planner/executor/reviewer
    CorrelationID   string `json:"correlationid,omitempty"`   // 关联ID,用于追踪调用链
    Priority        int    `json:"priority,omitempty"`        // 0-10,10最高
    TTLSeconds      int    `json:"ttlseconds,omitempty"`      // 消息存活时间
}

// Agent间内部消息协议(比CloudEvents更紧凑,适合高吞吐场景)
type AgentMessage struct {
    Header  MessageHeader
    Payload []byte
}

type MessageHeader struct {
    Version    uint8   // 协议版本
    Type       uint8   // 消息类型(0=Event, 1=Request, 2=Response, 3=Stream)
    Flags      uint8   // bit0=compressed, bit1=encrypted, bit2=priority
    MessageID  uint64  // 单调递增消息ID
    Timestamp  int64   // Unix毫秒时间戳
    Source     uint32  // Agent ID(内部数字标识)
    Target     uint32  // 目标Agent ID(0=广播)
    BodyLength uint32  // Payload长度
}

事件总线的发布-订阅与请求-响应模式

事件总线是多Agent系统的通信中枢。它需要同时支持两种通信模式:发布-订阅(一对多,异步)和请求-响应(一对一,同步或异步)。

package eventbus

import (
    "context"
    "sync"
    "time"
)

// Agent通信接口
type AgentCommunicator interface {
    // 发布-订阅:广播事件
    Publish(ctx context.Context, topic string, event *CloudEvent) error
    Subscribe(ctx context.Context, topic string, handler EventHandler) (Subscription, error)
    
    // 请求-响应:点对点通信
    Request(ctx context.Context, targetAgentID string, req interface{}) (interface{}, error)
    RegisterHandler(method string, handler RequestHandler) error
}

type EventHandler func(ctx context.Context, event *CloudEvent) error
type RequestHandler func(ctx context.Context, payload []byte) ([]byte, error)
type Subscription interface {
    Unsubscribe() error
    Topic() string
}

// NATS JetStream 实现(支撑高吞吐与持久化)
type NATSEventBus struct {
    conn           *nats.Conn
    js             nats.JetStreamContext
    subscriptions  map[string][]*nats.Subscription
    requestHandlers map[string]RequestHandler
    mu             sync.RWMutex
}

func NewNATSEventBus(natsURL string) (*NATSEventBus, error) {
    nc, err := nats.Connect(natsURL,
        nats.ReconnectWait(2*time.Second),
        nats.MaxReconnects(-1),
        nats.DisconnectErrHandler(func(nc *nats.Conn, err error) {
            // 断连告警
        }),
    )
    if err != nil {
        return nil, fmt.Errorf("nats connect: %w", err)
    }
    
    js, err := nc.JetStream()
    if err != nil {
        return nil, fmt.Errorf("jetstream init: %w", err)
    }
    
    return &NATSEventBus{
        conn:            nc,
        js:              js,
        subscriptions:   make(map[string][]*nats.Subscription),
        requestHandlers: make(map[string]RequestHandler),
    }, nil
}

func (bus *NATSEventBus) Publish(
    ctx context.Context, topic string, event *CloudEvent,
) error {
    data, err := json.Marshal(event)
    if err != nil {
        return fmt.Errorf("marshal event: %w", err)
    }
    
    // JetStream发布保证至少一次投递
    _, err = bus.js.Publish(topic, data,
        nats.MsgId(event.ID),
        nats.ExpectStream("agent-events"),
    )
    return err
}

func (bus *NATSEventBus) Subscribe(
    ctx context.Context, topic string, handler EventHandler,
) (Subscription, error) {
    sub, err := bus.js.Subscribe(topic, func(msg *nats.Msg) {
        var event CloudEvent
        if err := json.Unmarshal(msg.Data, &event); err != nil {
            // 无效消息,确认消费(避免死信堆积)
            msg.Ack()
            return
        }
        
        if err := handler(ctx, &event); err != nil {
            // 处理失败:Nak触发重投
            msg.Nak()
            return
        }
        msg.Ack()
    }, nats.ManualAck(),
       nats.Durable(fmt.Sprintf("agent-%s", topic)),
    )
    if err != nil {
        return nil, fmt.Errorf("subscribe: %w", err)
    }
    
    bus.mu.Lock()
    bus.subscriptions[topic] = append(bus.subscriptions[topic], sub)
    bus.mu.Unlock()
    
    return &natsSubscription{sub: sub, topic: topic}, nil
}

// 请求-响应模式:带超时和重试
func (bus *NATSEventBus) Request(
    ctx context.Context, targetMethod string, payload []byte,
) ([]byte, error) {
    // 使用NATS的Request-Reply模式
    msg, err := bus.conn.RequestWithContext(ctx, targetMethod, payload)
    if err != nil {
        return nil, fmt.Errorf("request %s: %w", targetMethod, err)
    }
    return msg.Data, nil
}

三、服务发现与健康检查机制

多Agent系统中的服务发现需要感知Agent的能力而不仅仅是IP地址。Agent注册时需要声明自己的能力清单、当前负载和状态。

// Agent注册信息
type AgentRegistration struct {
    AgentID       string            `json:"agent_id"`
    AgentType     string            `json:"agent_type"`     // "planner" / "executor" / "reviewer"
    Capabilities  []string          `json:"capabilities"`    // ["code_gen", "code_review", "test_gen"]
    Status        AgentStatus       `json:"status"`          // IDLE / BUSY / DRAINING
    Address       string            `json:"address"`         // gRPC地址
    Metadata      map[string]string `json:"metadata"`        // 扩展属性
    MaxConcurrency int              `json:"max_concurrency"`
    CurrentLoad    int              `json:"current_load"`
    LastHeartbeat  time.Time        `json:"last_heartbeat"`
}

type AgentStatus string

const (
    AgentStatusIdle     AgentStatus = "IDLE"
    AgentStatusBusy     AgentStatus = "BUSY"
    AgentStatusDraining AgentStatus = "DRAINING"
)

// 服务注册与发现接口
type ServiceRegistry interface {
    Register(ctx context.Context, info AgentRegistration) error
    Deregister(ctx context.Context, agentID string) error
    Discover(ctx context.Context, filters ...AgentFilter) ([]AgentRegistration, error)
    Heartbeat(ctx context.Context, agentID string) error
}

type AgentFilter func(AgentRegistration) bool

func ByCapability(cap string) AgentFilter {
    return func(a AgentRegistration) bool {
        for _, c := range a.Capabilities {
            if c == cap {
                return true
            }
        }
        return false
    }
}

func ByType(agentType string) AgentFilter {
    return func(a AgentRegistration) bool {
        return a.AgentType == agentType && a.Status != AgentStatusDraining
    }
}

func ByAvailability() AgentFilter {
    return func(a AgentRegistration) bool {
        return a.Status == AgentStatusIdle &&
            a.CurrentLoad < a.MaxConcurrency
    }
}

// etcd实现的注册中心
type EtcdRegistry struct {
    client     *clientv3.Client
    leaseID    clientv3.LeaseID
    selfInfo   AgentRegistration
    ttlSeconds int64
}

func (r *EtcdRegistry) Heartbeat(ctx context.Context, agentID string) error {
    _, err := r.client.KeepAliveOnce(ctx, r.leaseID)
    if err != nil {
        // 心跳失败:重新注册
        return r.Register(ctx, r.selfInfo)
    }
    return nil
}

func (r *EtcdRegistry) Discover(
    ctx context.Context, filters ...AgentFilter,
) ([]AgentRegistration, error) {
    resp, err := r.client.Get(ctx, "/agents/",
        clientv3.WithPrefix(),
    )
    if err != nil {
        return nil, fmt.Errorf("etcd get agents: %w", err)
    }
    
    var agents []AgentRegistration
    for _, kv := range resp.Kvs {
        var agent AgentRegistration
        if err := json.Unmarshal(kv.Value, &agent); err != nil {
            continue  // 跳过格式错误的数据
        }
        
        // 应用过滤器
        match := true
        for _, f := range filters {
            if !f(agent) {
                match = false
                break
            }
        }
        if match {
            agents = append(agents, agent)
        }
    }
    
    return agents, nil
}

四、通信的超时重试与幂等性

多Agent系统中,通信失败是常态而非异常。Agent可能正在处理长时间任务、正在重启或网络抖动。超时和重试策略需要配合幂等性设计。

// 重试策略:指数退避 + 抖动
type RetryPolicy struct {
    MaxRetries    int           // 最大重试次数
    InitialDelay  time.Duration // 初始延迟
    MaxDelay      time.Duration // 最大延迟上限
    Multiplier    float64       // 退避乘数
    JitterFactor  float64       // 抖动因子 (0.0 ~ 1.0)
}

func DefaultRetryPolicy() RetryPolicy {
    return RetryPolicy{
        MaxRetries:   3,
        InitialDelay: 100 * time.Millisecond,
        MaxDelay:     5 * time.Second,
        Multiplier:   2.0,
        JitterFactor: 0.3,
    }
}

func (p RetryPolicy) Execute(
    ctx context.Context,
    operation func(context.Context) error,
	isIdempotent func(error) bool,
) error {
    var lastErr error
    delay := p.InitialDelay
    
    for attempt := 0; attempt <= p.MaxRetries; attempt++ {
        if attempt > 0 {
            // 检查是否应该重试
            if lastErr != nil && !isIdempotent(lastErr) {
                return fmt.Errorf("non-idempotent error, not retrying: %w", lastErr)
            }
            
            // 带抖动的指数退避
            jitter := time.Duration(float64(delay) * p.JitterFactor * 
                (float64(time.Now().UnixNano()%100) / 100.0))
            sleepDuration := delay + jitter
            if sleepDuration > p.MaxDelay {
                sleepDuration = p.MaxDelay
            }
            
            select {
            case <-ctx.Done():
                return ctx.Err()
            case <-time.After(sleepDuration):
            }
            
            delay = time.Duration(float64(delay) * p.Multiplier)
        }
        
        lastErr = operation(ctx)
        if lastErr == nil {
            return nil
        }
    }
    
    return fmt.Errorf("all %d retries exhausted: %w", p.MaxRetries, lastErr)
}

// 幂等性保障:基于消息ID的去重
type IdempotencyGuard struct {
    processed sync.Map // messageID → result
    ttl       time.Duration
}

func (g *IdempotencyGuard) Process(
    messageID string,
    handler func() (interface{}, error),
) (interface{}, error) {
    // 检查是否已处理
    if result, ok := g.processed.Load(messageID); ok {
        return result, nil  // 幂等返回
    }
    
    result, err := handler()
    if err != nil {
        return nil, err
    }
    
    g.processed.Store(messageID, result)
    
    // TTL后自动清理,防止内存泄漏
    time.AfterFunc(g.ttl, func() {
        g.processed.Delete(messageID)
    })
    
    return result, nil
}

幂等性的关键设计决策是去重窗口(TTL)的大小。窗口太小可能导致重复执行,窗口太大会积累太多已处理消息的记录。合理的TTL应大于消息的最大重试时间窗口——通常设置为重试次数 × 最大延迟的2-3倍。

五、总结

多Agent系统的通信协议需要同时支持发布-订阅、请求-响应和流式三种模式。CloudEvents提供了标准化的消息格式,与外部系统集成时有天然优势;内部通信可以使用更紧凑的自定义格式。事件总线的选型上,NATS的JetStream在性能、持久化和复杂度之间取得了好的平衡——它不像Kafka那样需要复杂的基础设施,又比Redis Streams提供更强的持久化保证。服务发现需要从IP级别升级到能力级别——Agent注册的是能力清单和当前负载,其他Agent基于能力而非地址进行发现。超时重试必须和幂等性配套设计,幂等的实现成本远低于处理重复执行引发的一致性问题的成本。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐