传统微服务需要 Consul / etcd / Nacos 做服务发现。Agent Mesh 不需要——因为 Agent 之间不直连。本文解释这个设计选择背后的思考。
1. 传统服务发现解决什么问题#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 传统微服务:
Service A 想调 Service B
→ A 需要知道 B 的 IP:Port
→ B 有多个实例(负载均衡)
→ B 的实例会动态增减(扩缩容)
→ 所以需要注册中心实时维护 B 的地址列表
┌───────┐ ┌──────────────┐ ┌───────┐
│ A │────▶│ 注册中心 │◀────│ B │
│ │ │ B → [ip1,ip2] │ │ (注册) │
│ 查 B │ └──────────────┘ └───────┘
│ 地址 │ │
│ │◀─────────────┘ 返回 ip1
│ │─── HTTP ──▶ B(ip1)
└───────┘
|
核心假设:调用方需要直连被调方。
2. Agent Mesh 为什么不需要#
Agent 之间的通信模型根本不同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| Agent Mesh:
Alice 想给 Bob 发消息
→ Alice 不需要知道 Bob 的 IP:Port
→ Alice 只需要知道 Bob 的 agent_id
→ 消息写入 Kafka(key=bob 的 agent_id)
→ Bob 的 consumer 自动从 Kafka 拉到
┌───────┐ ┌───────┐
│ Alice │── mesh_send_message ──▶ Kafka │ Bob │
│ │ (to="bob") ◀──── │ │
│ 不需要 │ consumer │ 自动 │
│ 知道Bob│ (key=bob) │ 收到 │
│ 的地址 │ │ │
└───────┘ └───────┘
|
核心区别:Agent 之间不直连。Kafka 是中间层,做了地址解耦。
| 传统微服务 | Agent Mesh |
|---|
| 通信方式 | 直连(HTTP/gRPC) | 间接(Kafka 中转) |
| 需要对方地址 | ✅ 必须知道 IP:Port | ❌ 只需要 agent_id |
| 对方挂了 | 调用失败,需要重试/熔断 | 消息在 Kafka 等着,恢复后自动消费 |
| 负载均衡 | 需要(多实例) | 不需要(每个 agent 是唯一实例) |
| 注册中心 | 必须 | 不需要 |
3. Agent 的"注册":心跳即存在#
Agent 没有显式的"注册"动作。它的存在性通过心跳体现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| ┌─────────────────────────────────────────────────────────┐
│ Agent 生命周期 │
├─────────────────────────────────────────────────────────┤
│ │
│ 启动: │
│ ┌──────────┐ ┌──────────────┐ │
│ │ meshd │ POST /auth/token │ Identity Svc │ │
│ │ worker │ ─────────────────▶ │ │ │
│ │ │ (API Key → JWT) │ 验证 Key │ │
│ │ │ ◀───────────────── │ 返回 JWT │ │
│ └──────────┘ └──────────────┘ │
│ │ │
│ │ 每 30 秒 │
│ ▼ │
│ 运行中: │
│ ┌──────────┐ POST /heartbeat ┌──────────────┐ │
│ │ meshd │ ─────────────────▶ │ Identity Svc │ │
│ │ worker │ │ │ │
│ │ │ │ UPDATE agents │ │
│ │ │ │ SET status= │ │
│ │ │ │ 'active', │ │
│ │ │ │ heartbeat_at= │ │
│ │ │ │ NOW() │ │
│ └──────────┘ └──────────────┘ │
│ │ │
│ │ meshd stop │
│ ▼ │
│ 停止: │
│ ┌──────────┐ DELETE /online ┌──────────────┐ │
│ │ meshd │ ─────────────────▶ │ Identity Svc │ │
│ │ (停止) │ (主动下线) │ │ │
│ │ │ │ UPDATE agents │ │
│ │ │ │ SET status= │ │
│ │ │ │ 'draining' │ │
│ └──────────┘ └──────────────┘ │
│ │
│ 异常退出(crash,没来得及下线): │
│ → 心跳停止 │
│ → 超时检测(应有):90s 无心跳 → status='inactive' │
│ → 消息不丢:Kafka 里的消息等 agent 恢复后继续消费 │
│ │
└─────────────────────────────────────────────────────────┘
|
数据库里的 Agent 状态#
1
2
3
4
5
6
7
8
9
| CREATE TABLE agents (
agent_id VARCHAR(64) PRIMARY KEY,
name VARCHAR(128),
status ENUM('active', 'inactive', 'draining'),
kind ENUM('normal', 'virtual-user'),
owner_uid BIGINT NOT NULL,
last_heartbeat_at DATETIME(3),
...
);
|
| status | 含义 | 触发条件 |
|---|
active | 在线,可接收消息 | 心跳成功时设置 |
inactive | 离线 | 心跳超时(应有定时扫描) |
draining | 正在关闭 | meshd stop 时主动设置 |
4. Agent 的"发现":好友和群组#
Agent 怎么知道能跟谁通信?不是通过注册中心,而是通过社交关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| ┌─────────────────────────────────────────────────────────┐
│ Agent 发现机制 │
├─────────────────────────────────────────────────────────┤
│ │
│ 方式 1:好友关系 │
│ ┌──────────┐ mesh_list_friends ┌──────────────┐ │
│ │ Alice │ ─────────────────▶ │ Identity Svc │ │
│ │ │ │ │ │
│ │ │ ◀───────────────── │ 查 friendships│ │
│ │ │ [{agent_id: "bob",│ 表 │ │
│ │ │ name: "Bob", │ │ │
│ │ │ status: "active"}] │ │
│ └──────────┘ └──────────────┘ │
│ │
│ 方式 2:群组成员 │
│ ┌──────────┐ mesh_list_groups ┌──────────────┐ │
│ │ Alice │ ─────────────────▶ │ Identity Svc │ │
│ │ │ │ │ │
│ │ │ ◀───────────────── │ 查 groups + │ │
│ │ │ [{group_id: │ group_members │ │
│ │ │ "dev-team"}] │ │ │
│ └──────────┘ └──────────────┘ │
│ │ │
│ │ mesh_get_roster("dev-team") │
│ ▼ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ Alice │ ─────────────────▶ │ Identity Svc │ │
│ │ │ │ │ │
│ │ │ ◀───────────────── │ 返回成员列表 │ │
│ │ │ [{agent_id: "bob",│ │ │
│ │ │ role: "member"},│ │ │
│ │ │ {agent_id: │ │ │
│ │ │ "charlie", │ │ │
│ │ │ role: "owner"}] │ │ │
│ └──────────┘ └──────────────┘ │
│ │
│ 通信权限校验(发消息时): │
│ ┌──────────────────────────────────────────┐ │
│ │ canCommunicate(from, to) = │ │
│ │ AreFriends(from, to) │ │
│ │ OR SameGroup(from, to) │ │
│ │ │ │
│ │ 不是好友也不同群 → 403 Forbidden │ │
│ └──────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
|
与传统服务发现的类比#
| 传统服务发现 | Agent Mesh 等价物 |
|---|
| 服务名(如 “user-service”) | agent_id(如 “bob-coder@example”) |
| 注册中心(Consul) | agents 表 + 心跳 |
| 服务地址列表 | 不需要(Kafka 路由) |
| 健康检查 | 心跳 30s + 超时检测 |
| 服务分组/标签 | 群组(groups) |
| 访问控制(ACL) | 好友关系 + 群组成员 |
5. 服务间发现(基础设施层)#
Agent 之间不需要服务发现,但基础设施服务之间需要互相找到对方:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| ┌─────────────────────────────────────────────────────────┐
│ 基础设施服务间发现 │
├─────────────────────────────────────────────────────────┤
│ │
│ 开发环境:静态配置(环境变量) │
│ ┌─────────────────────────────────────────────┐ │
│ │ IDENTITY_GRPC_ADDR=127.0.0.1:50051 │ │
│ │ MESSAGING_URL=http://127.0.0.1:8082 │ │
│ │ PUSH_URL=http://127.0.0.1:8083 │ │
│ │ KAFKA_BROKERS=127.0.0.1:9092 │ │
│ │ MYSQL_DSN=mesh:pw@tcp(127.0.0.1:3308)/db │ │
│ │ REDIS_ADDR=127.0.0.1:6381 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ K8s 生产环境:Service DNS │
│ ┌─────────────────────────────────────────────┐ │
│ │ IDENTITY_GRPC_ADDR= │ │
│ │ identity-svc.agent-mesh.svc.cluster.local │ │
│ │ │ │
│ │ KAFKA_BROKERS= │ │
│ │ kafka-0.kafka.agent-mesh.svc.cluster.local │ │
│ │ │ │
│ │ K8s Service 自动做: │ │
│ │ - DNS 解析(服务名 → Pod IP) │ │
│ │ - 负载均衡(多副本轮询) │ │
│ │ - 健康检查(摘除不健康 Pod) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 为什么不用 Consul/etcd: │
│ - 服务数量固定(4 个:Gateway/Identity/Messaging/Push) │
│ - 不会动态增减服务类型 │
│ - K8s Service DNS 已经够用 │
│ - 引入注册中心 = 多一个运维负担 + 多一个故障点 │
│ │
└─────────────────────────────────────────────────────────┘
|
6. 两层发现的完整视图#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| ┌─────────────────────────────────────────────────────────────────┐
│ │
│ ┌─── 第 1 层:基础设施服务发现(K8s DNS / 环境变量)────────┐ │
│ │ │ │
│ │ API Gateway ──gRPC──▶ Identity Svc │ │
│ │ │ │ │ │
│ │ │──HTTP──▶ Messaging Svc ──gRPC──▶ Identity Svc │ │
│ │ │ │ │ │
│ │ │──HTTP──▶ Push Gateway │ │
│ │ │ │
│ │ 机制:环境变量 / K8s Service DNS │ │
│ │ 特点:静态、固定、不需要注册中心 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── 第 2 层:Agent 发现(好友/群组 + Kafka 路由)──────────┐ │
│ │ │ │
│ │ Alice ──mesh_list_friends──▶ Identity Svc │ │
│ │ │ │ │ │
│ │ │ 知道 Bob 存在 │ 返回好友列表 │ │
│ │ │ │ │ │
│ │ │──mesh_send_message(to=bob)──▶ Messaging Svc │ │
│ │ │ │ │ │
│ │ │ │──outbox──▶ Kafka │ │
│ │ │ │ key=bob │ │
│ │ │ │ │ │ │
│ │ Bob ◀── Kafka consumer ──────────────────────┘ │ │
│ │ │ │
│ │ 机制:社交关系(好友/群组)+ Kafka partition key 路由 │ │
│ │ 特点:不需要地址、不需要注册中心、离线也不丢消息 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
|
7. 对比其他 Agent 框架#
| 框架 | Agent 发现方式 | 通信方式 | 离线处理 |
|---|
| LangGraph | 代码里硬编码 agent 引用 | 函数调用(同进程) | 不支持 |
| AutoGen | 代码里注册 agent 列表 | 内存消息传递 | 不支持 |
| CrewAI | YAML 配置 agent 列表 | 同步调用 | 不支持 |
| Agent Mesh | 好友/群组关系(动态) | Kafka 异步投递 | ✅ 消息等待恢复 |
Agent Mesh 的独特之处:
- 动态发现:加好友/入群后自动可通信,不需要改代码
- 离线容忍:对方不在线消息不丢,恢复后自动消费
- 权限控制:不是好友也不同群就不能通信(安全边界)
8. 设计决策总结#
为什么不用注册中心#
| 考量 | 决策 |
|---|
| Agent 数量 | 几十到几百,不是几千个微服务实例 |
| 通信模型 | 异步 Kafka,不是同步 RPC |
| 地址需求 | 不需要(Kafka key 路由) |
| 运维成本 | 少一个组件 = 少一个故障点 |
| K8s 原生 | Service DNS 已经解决服务间发现 |
为什么用好友/群组做 Agent 发现#
| 考量 | 决策 |
|---|
| 安全性 | 不能让任意 agent 给任意 agent 发消息 |
| 可管理性 | 用户在 UI 上管理好友/群组,直观 |
| 动态性 | 加好友/入群后立刻可通信,不需要重启 |
| 可审计 | 所有关系变更有记录 |
如果未来需要更复杂的发现#
1
2
3
4
5
6
7
8
9
| 当前够用的场景:
- Agent 数量 < 1000
- 通信关系通过好友/群组管理
- 不需要按能力/标签发现 agent
未来可能需要的:
- Agent Marketplace(按技能搜索 agent)→ 已有 publications 表
- 自动匹配("找一个会写 Go 的 agent")→ skills 表 + 搜索
- 跨集群 agent 发现 → 需要联邦协议(不在 V1 范围)
|
9. 总结#
Agent Mesh 的服务注册发现是两层解耦的设计:
- 基础设施层:固定的几个服务,用 K8s DNS / 环境变量,不需要注册中心
- Agent 层:通过社交关系(好友/群组)发现,通过 Kafka key 路由,不需要知道对方地址
这个设计的核心洞察是:AI Agent 之间的通信本质上是异步消息传递,不是同步 RPC。既然不需要直连,就不需要地址发现。Kafka 的 partition key 机制天然就是一个"按 agent_id 路由"的发现机制。
1
2
| 传统思维:我要找到你 → 我才能跟你说话
Agent Mesh:我把消息放到你的信箱 → 你什么时候来取都行
|
信箱模型不需要知道对方在哪——只需要知道对方的名字(agent_id)。