Skip to content

[A2A] getDefaultAgent fallback 遍历 map 返回非确定性 Agent,多 Agent 时路由随机化 #79

Description

@BaSui01

问题摘要

getDefaultAgent 在 DefaultAgentID 未配置或未找到时,遍历 s.agents map 返回"第一个"条目。Go 的 map 迭代顺序是有意随机的(每次运行不同),因此对 getDefaultAgent 的重复调用可能返回不同的 Agent,导致 To 字段为空的消息路由不稳定。

环境信息

  • 仓库: BaSui01/agentflow
  • 分支: dev
  • 文件: agent/execution/protocol/a2a/server_agent.go
  • 关键位置: 第 125-142 行 (getDefaultAgent) + 第 136-139 行 (fallback)
  • 调用方: server_handler.go:80 (AgentCard discovery) + server_handler.go:295 (message routing)

Bug 详细分析

非确定性的 fallback 逻辑

// server_agent.go:125-142
func (s *HTTPServer) getDefaultAgent() (Agent, error) {
    s.agentsMu.RLock()
    defer s.agentsMu.RUnlock()

    // 首先尝试默认代理ID
    if s.config.DefaultAgentID != "" {
        if ag, ok := s.agents[s.config.DefaultAgentID]; ok {
            return ag, nil           // 已配置 → 确定 ✅
        }
    }

    // 返回第一个可用的代理
    for _, ag := range s.agents {   // map 遍历顺序是随机的!
        return ag, nil               // 每次可能返回不同的 Agent ❌
    }

    return nil, ErrAgentNotFound
}

Go 语言规范明确规定:map 的迭代顺序未定义,且每次运行有意不同(runtime 在 map 迭代器中使用随机种子)。当你注册了 3 个 Agent [A, B, C] 且未设 DefaultAgentID 时:

调用 1: 遍历 → B → 返回 B
调用 2: 遍历 → A → 返回 A  (不同!)
调用 3: 遍历 → C → 返回 C  (又不同!)

两个受影响的路由点

1. Agent Card Discovery(server_handler.go:68-93)

func (s *HTTPServer) handleAgentCardDiscovery(w http.ResponseWriter, r *http.Request) {
    agentID := r.URL.Query().Get("agent_id")
    if agentID != "" {
        card, err = s.GetAgentCard(agentID)   // 明确指定 → 确定 ✅
    } else {
        ag, agErr := s.getDefaultAgent()       // 无指定 → 随机!❌
        card, err = s.GetAgentCard(ag.ID())
    }
}

客户端 GET /.well-known/agent.json(不传 agent_id 参数)时,每次返回的可能是不同 Agent 的卡片。

2. 消息路由(server_handler.go:291-317)

func (s *HTTPServer) routeMessage(msg *A2AMessage) (Agent, error) {
    agentID := strings.TrimSpace(msg.To)
    if agentID == "" {
        return s.getDefaultAgent()    // To 为空 → 随机路由!❌
    }
    // ...
}

当客户端发送 To: "" 的消息时,同一个客户端的不同请求可能被路由到不同 Agent。

实际影响场景

假设服务器注册了 3 个 Agent:math-agent(计算)、weather-agent(天气)、translate-agent(翻译):

  1. 客户端 A 发送 {To: "", Payload: {query: "1+1"}} → 随机路由到 weather-agent → 无法处理计算结果
  2. 重试同样请求 → 随机路由到 math-agent → 成功

这导致:

  • 不可复现的行为:同一请求的结果取决于路由到的 Agent
  • 调试困难:开发者难以复现问题
  • 负载不均:可能总是选中同一个 Agent

复现步骤

func TestGetDefaultAgent_Deterministic(t *testing.T) {
    server := NewHTTPServer(Config{BaseURL: "http://localhost:8080"}, nil)

    // 注册 3 个 Agent,不设 DefaultAgentID
    for i := 0; i < 3; i++ {
        server.RegisterAgent(newMockAgent(fmt.Sprintf("agent-%d", i), "test"))
    }

    // 多次调用 getDefaultAgent
    seen := make(map[string]int)
    for i := 0; i < 100; i++ {
        ag, err := server.getDefaultAgent()
        require.NoError(t, err)
        seen[ag.ID()]++
    }

    t.Logf("agent distribution: %v", seen)
    // 预期:固定返回同一个 Agent
    // 实际:每个 Agent 都有不同的计数(非确定性分布)
}

影响范围

维度 评估
严重度 中 — 不导致数据丢失,但路由不确定导致行为不可复现
影响面 注册多个 Agent 但未配置 DefaultAgentID 的 A2A 部署
触发条件 msg.To 为空 或 agent card discovery 无 agent_id 参数
后果 1) 消息随机路由 2) 结果不可复现 3) 跨 Agent 能力不匹配

修复建议

方案一:要求显式配置 DefaultAgentID(推荐)

当存在多个 Agent 且未配置 DefaultAgentID 时,返回错误而非随机选取:

func (s *HTTPServer) getDefaultAgent() (Agent, error) {
    s.agentsMu.RLock()
    defer s.agentsMu.RUnlock()

    // 首先尝试默认代理ID
    if s.config.DefaultAgentID != "" {
        if ag, ok := s.agents[s.config.DefaultAgentID]; ok {
            return ag, nil
        }
    }

    // 只有一个 Agent → 它就是默认的
    if len(s.agents) == 1 {
        for _, ag := range s.agents {
            return ag, nil
        }
    }

    // 多个 Agent 但没有配置默认 → 返回错误
    if len(s.agents) > 1 {
        return nil, fmt.Errorf(
            "multiple agents registered (%d) but no default agent configured; set DefaultAgentID in server config",
            len(s.agents),
        )
    }

    return nil, ErrAgentNotFound
}

方案二:确定性选择(排序后取第一个)

// 返回第一个可用的代理(按 ID 排序保证确定性)
agentIDs := make([]string, 0, len(s.agents))
for id := range s.agents {
    agentIDs = append(agentIDs, id)
}
sort.Strings(agentIDs)
return s.agents[agentIDs[0]], nil

方案二兼容性好(不改变 API 行为),但不能解决"随机路由到不适合的 Agent"的根本问题——因为那个 Agent 仍然不是调用方期望的。

推荐方案一:显式错误优于静默不确定性。用户要么配置 DefaultAgentID,要么在只有一个 Agent 时自动使用它。

验证清单

  • 单元测试:单个 Agent 且无 DefaultAgentID → 返回该 Agent
  • 单元测试:多个 Agent 且无 DefaultAgentID → 返回错误(方案一)或返回固定 Agent(方案二)
  • 单元测试:DefaultAgentID 已配置 → 返回指定 Agent
  • 单元测试:DefaultAgentID 指向不存在的 Agent → 不会随机 fallback,返回错误或 nil
  • 连续 100 次调用返回相同结果(验证确定性)
  • go test -race ./agent/execution/protocol/a2a/... PASS

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions