Files
beaver_project/app-instance/backend/A2A_Multiagent_change.md
2026-03-13 16:40:08 +08:00

754 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# A2A Multi-Agent 改造方案
## 1. 需求目标
当前 `spawn`/`sub-agent` 只有一种执行方式: 创建一个本地后台 subagent 去完成任务。
这次需求要改成:
1. 调用 `sub-agent` 时,不一定新建本地 subagent。
2. 先从“已添加的 Agent”里找可用目标。
3. 再从 skills 中声明的 `agent cards` 里找可用目标。
4. 通过 A2A 协议把任务发给对应 agent。
5. 支持一个任务发给多个 agent形成 `agent group`,最后回到主 agent 汇总。
6. 保持现有 `spawn(task, label)` 兼容,不破坏已有行为。
结论先说:
- 最合适的做法不是继续把能力堆进 `SubagentManager`
- 应该把“本地 subagent 执行”升级为“统一委派层”。
- `spawn` 工具继续保留,但语义从“创建 subagent”扩展为“委派给合适的 agent / agent group”。
## 2. 当前代码现状
### 2.1 当前触发链路
现有链路很单一:
1. `AgentLoop` 初始化 `SubagentManager`
- 位置: `nanobot/agent/loop.py:88-114`
2. `AgentLoop._register_default_tools()` 注册 `SpawnTool`
- 位置: `nanobot/agent/loop.py:116-138`
3. LLM 调用 `spawn(task, label)`
4. `SpawnTool.execute()` 直接转发给 `SubagentManager.spawn()`
- 位置: `nanobot/agent/tools/spawn.py:67-76`
5. `SubagentManager.spawn()` 创建本地 asyncio 后台任务
- 位置: `nanobot/agent/subagent.py:64-93`
6. `_run_subagent()` 用一个受限工具集运行本地子代理
- 位置: `nanobot/agent/subagent.py:95-195`
7. `_announce_result()` 把结果包装成 `channel="system"` 的消息回投主消息总线
- 位置: `nanobot/agent/subagent.py:197-230`
8. `AgentLoop._process_message()` 接到 `system` 消息,再整理成用户可见回复
- 位置: `nanobot/agent/loop.py:331-347`
### 2.2 当前已经有但没接入调度链路的能力
仓库里已经有两类“候选 agent 信息”,但没有进入实际调度:
1. Plugin agents
- `PluginLoader.find_agent()` 已能找 agent
- 位置: `nanobot/agent/plugins.py:83-91`
- `build_agents_summary()` 也已能汇总 agent 信息
- 位置: `nanobot/agent/plugins.py:100-121`
- 但当前 `AgentLoop` / `ContextBuilder` 并没有用它做调度
2. Skills
- `SkillsLoader` 已能枚举 / 读取 skill
- 位置: `nanobot/agent/skills.py:32-249`
- 但 skill 目前只被当作 prompt 资源,不会暴露成“可路由 agent”
### 2.3 当前缺口
当前缺少这几层:
1. 统一的 `Agent Registry`
2. A2A `agent card` 发现与缓存
3. A2A client 调用层
4. 统一的委派器,负责在“本地 subagent / plugin agent / skill agent card / agent group”之间做路由
5. group 级别的状态管理和结果聚合
## 3. 推荐总方案
推荐采用“保留 `spawn` 工具名,重构内部执行层”的方案。
### 3.1 核心思路
把当前:
- `SpawnTool -> SubagentManager -> 本地 subagent`
改成:
- `SpawnTool -> DelegationManager -> AgentResolver -> Executor(local/plugin/a2a/group)`
也就是:
1. `spawn` 不再等价于“必须创建 subagent”。
2. `spawn` 变成“委派任务”。
3. 真正执行方式由委派层动态决定。
### 3.2 为什么这样最合适
如果直接继续扩 `SubagentManager`,很快会出现这些问题:
1. 一个类同时负责本地 LLM 运行、A2A 网络调用、agent card 发现、group 并发、结果聚合。
2. 后续要支持 plugin agent、本地 named agent、A2A streaming 时会越来越乱。
3. 当前 `SubagentManager` 的职责本来就已经比较明确: “本地后台 subagent 执行器”。
所以更合理的拆法是:
1. `SubagentManager` 保留或下沉为 `LocalSubagentExecutor`
2. 新增 `DelegationManager` 作为统一入口
3. 新增 `AgentRegistry` / `AgentResolver`
4. 新增 `A2AClient`
## 4. 推荐模块拆分
### 4.1 新增 `DelegationManager`
建议新文件:
- `nanobot/agent/delegation.py`
职责:
1. 接收 `spawn` 请求
2. 根据参数和任务内容选择目标 agent
3. 决定执行方式
4. 对 group 做并发调度
5. 统一把结果回投主消息总线
建议接口:
```python
class DelegationManager:
async def dispatch(
self,
task: str,
label: str | None = None,
target: str | None = None,
targets: list[str] | None = None,
strategy: str = "auto",
origin_channel: str = "cli",
origin_chat_id: str = "direct",
) -> str: ...
```
### 4.2 保留本地执行器
当前 `nanobot/agent/subagent.py``_run_subagent()` 逻辑可以保留,但角色改为:
- `LocalSubagentExecutor`
也可以第一版不重命名文件,只把里面逻辑拆成:
1. `spawn_local()`
2. `_run_local_subagent()`
3. `_announce_local_result()`
这样可以最小改动落地。
### 4.3 新增 `AgentRegistry`
建议新文件:
- `nanobot/agent/agent_registry.py`
职责:
1. 汇总所有可调度 agent
2. 统一输出规范化 descriptor
3. 维护优先级和去重逻辑
统一后的 agent 来源:
1. workspace 中“已添加的 agent”
2. plugin agents
3. skill frontmatter 里声明的 `agent_cards`
4. 必要时 fallback 到本地 `local-subagent`
建议统一 descriptor:
```python
@dataclass
class AgentDescriptor:
id: str
name: str
description: str
source: str # workspace | plugin | skill | builtin
kind: str # local_prompt | a2a_remote | local_fallback
protocol: str | None # a2a | None
plugin_name: str | None = None
skill_name: str | None = None
model: str | None = None
endpoint: str | None = None
card_url: str | None = None
tags: list[str] = field(default_factory=list)
capabilities: dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
```
### 4.4 新增 A2A client 层
建议新目录:
- `nanobot/a2a/client.py`
- `nanobot/a2a/cards.py`
- `nanobot/a2a/models.py`
职责:
1. 获取 agent card
2. 解析 card 能力
3. 对远端 agent 发 JSON-RPC 请求
4. 处理同步返回 / task 轮询 / streaming 兼容
## 5. 代码插入点
## 5.1 `nanobot/agent/loop.py`
### 插入点 A: `__init__`
当前:
- `self.subagents = SubagentManager(...)`
- 位置: `nanobot/agent/loop.py:88-102`
建议改成:
1. 初始化 `PluginLoader`
2. 初始化 `AgentRegistry`
3. 初始化 `DelegationManager`
4. `DelegationManager` 内部持有 `LocalSubagentExecutor` / `A2AExecutor`
推荐形态:
```python
self.plugins = PluginLoader(workspace)
self.agent_registry = AgentRegistry(workspace, plugins=self.plugins, ...)
self.delegation = DelegationManager(
provider=provider,
workspace=workspace,
bus=bus,
registry=self.agent_registry,
...
)
```
### 插入点 B: `_register_default_tools`
当前:
- 注册 `SpawnTool(manager=self.subagents)`
- 位置: `nanobot/agent/loop.py:130-134`
建议改成:
```python
self.tools.register(SpawnTool(manager=self.delegation))
```
### 插入点 C: `_set_tool_context`
当前会给 `spawn` 工具写 origin context:
- 位置: `nanobot/agent/loop.py:165-192`
这里逻辑可以继续保留,不需要大改,因为 A2A / group 结果最终也要回到原会话。
## 5.2 `nanobot/agent/tools/spawn.py`
当前 `SpawnTool` 参数只有:
- `task`
- `label`
位置:
- schema: `nanobot/agent/tools/spawn.py:49-65`
- execute: `nanobot/agent/tools/spawn.py:67-76`
建议扩成:
```python
{
"task": "string",
"label": "string?",
"target": "string?",
"targets": "string[]?",
"strategy": "auto|local|plugin|a2a|group"
}
```
兼容规则:
1. 老调用只传 `task/label` 时,等价于 `strategy="auto"`
2. `target` 表示单目标
3. `targets` 表示 group
4. `strategy="local"` 强制走本地 subagent
5. `strategy="a2a"` 强制只找 A2A 目标
## 5.3 `nanobot/agent/context.py`
当前 prompt 中只注入:
1. bootstrap
2. memory
3. skills summary
位置:
- `build_system_prompt()`: `nanobot/agent/context.py:38-76`
建议新增一段:
- `# Available Agents`
`AgentRegistry.build_agents_summary()` 生成,内容只放:
1. agent id / name
2. 简短 description
3. source
4. protocol
5. 是否支持 group / streaming
目标是让主 agent 知道:
1. 当前有哪些现成 agent 可用
2. 什么时候应该 `spawn(target=...)`
3. 哪些是 skill 暴露出来的 A2A agent
## 5.4 `nanobot/agent/skills.py`
这是 skill agent cards 的关键入口。
当前 skill frontmatter 已支持 `metadata` 字段,并会解析其中的 JSON:
- `_parse_nanobot_metadata()`: `nanobot/agent/skills.py:190-196`
- `_get_skill_meta()`: `nanobot/agent/skills.py:209-212`
最推荐的做法不是去扫 `SKILL.md` 正文里的自由文本,而是约定 skill frontmatter 的 `metadata.nanobot.agent_cards`
建议新增:
```python
def list_skill_agent_cards(self) -> list[dict[str, Any]]: ...
```
推荐 skill 写法:
```md
---
name: github-research
description: GitHub research helper
metadata: '{"nanobot":{"agent_cards":[{"id":"repo-analyst","url":"https://example.com/.well-known/agent-card","tags":["github","research"],"auth_env":"REPO_AGENT_TOKEN"}]}}'
---
```
为什么推荐这样做:
1. 当前 frontmatter 解析已经存在
2. 不需要引入新的 skill 文件格式
3. 不需要解析自由文本
4. skill 打包/上传链路也不需要大改
## 5.5 `nanobot/agent/plugins.py`
当前 plugin agents 已能加载:
- `find_agent()`: `nanobot/agent/plugins.py:83-91`
- `_load_agents()`: `nanobot/agent/plugins.py:210-229`
建议:
1. `AgentRegistry` 直接复用 `PluginLoader`
2. plugin agent 作为“本地可执行 agent”来源之一
这里不建议把 plugin agent 强行转成 A2A。
更合理的处理是:
1. plugin agent 本地执行
2. skill agent cards 远程 A2A 调用
3. workspace 手动添加的 agent 也可走 A2A
## 5.6 `nanobot/config/schema.py`
当前 `ToolsConfig` 只有:
- `web`
- `exec`
- `restrict_to_workspace`
- `mcp_servers`
位置:
- `nanobot/config/schema.py:337-347`
建议新增:
```python
class A2AConfig(Base):
enabled: bool = True
timeout_seconds: int = 30
poll_interval_seconds: int = 2
card_cache_ttl_seconds: int = 300
max_parallel_agents: int = 4
allow_skill_cards: bool = True
allow_workspace_agents: bool = True
allowed_hosts: list[str] = Field(default_factory=list)
```
然后挂到:
```python
class ToolsConfig(Base):
...
a2a: A2AConfig = Field(default_factory=A2AConfig)
```
## 5.7 `nanobot/web/server.py`
当前 web API 有:
- `/api/skills`
- `/api/plugins`
位置:
- skills: `nanobot/web/server.py:702-843`
- plugins: `nanobot/web/server.py:1000-1037`
建议新增:
1. `GET /api/agents`
- 返回统一后的 agent registry
2. `POST /api/agents`
- 添加 workspace agent card
3. `DELETE /api/agents/{id}`
- 删除 workspace agent
4. `POST /api/agents/refresh`
- 刷新 card cache
这样“已添加的 Agent”才有明确的持久化来源。
## 6. 推荐的数据来源优先级
为了行为稳定,推荐 resolver 按以下优先级匹配:
1. workspace 手动添加的 agent
2. plugin agents
3. skill metadata 里的 agent cards
4. fallback 到本地 subagent
原因:
1. workspace 手动添加通常是用户明确希望接入的 agent
2. plugin agent 是本地稳定能力
3. skill card 往往是外部资源,可信度和可用性最弱
4. 本地 subagent 最后兜底,保证老行为不失效
## 7. A2A 协议接入建议
## 7.1 Agent Card 发现
建议支持 3 种入口:
1. 显式 `card_url`
2. `base_url + /.well-known/agent-card`
3. fallback `base_url + /.well-known/agent.json`
这样做的原因是:
1. 当前 A2A 文档和样例在 card 路径上存在新旧写法并存
2. 兼容性会更好
## 7.2 RPC 调用兼容层
建议客户端优先尝试:
1. `tasks/send`
2. 不支持时 fallback `message/send`
后续可选支持:
1. `tasks/sendSubscribe`
2. `message/sendStream`
3. `tasks/get`
4. `tasks/cancel`
推荐第一期先做:
1. 非流式发任务
2. 如果返回 `Task` 状态不是最终态,就轮询 `tasks/get`
这样能最小代价先打通。
## 7.3 发送给远端 agent 的上下文范围
不要把主会话完整 history 直接发给远端 agent。
建议第一版只发送:
1. 任务目标
2. 必要的结构化说明
3. 主 agent 整理好的最小上下文
原因:
1. 当前本地 subagent 也不共享主会话历史
2. 外部 A2A agent 不可信时,最小化数据泄漏面
3. 避免 token 膨胀
## 8. agent group 设计
## 8.1 什么时候触发 group
建议第一版只支持两种触发:
1. 用户明确指定多个 agent
2. LLM 在工具调用里显式传 `targets=[...]`
不建议第一版做“自动拆成多个 agent 并行”的强自动化。
原因:
1. 容易失控
2. 很难解释为什么调了这些 agent
3. 对成本和网络调用不可控
## 8.2 group 执行链路
推荐链路:
1. `SpawnTool.execute()` 收到 `targets`
2. `DelegationManager.dispatch()` 创建 `group_run_id`
3. `AgentRegistry` 解析出每个 target 的 descriptor
4. 按 executor 类型并发执行
5. `asyncio.gather(..., return_exceptions=True)` 收集结果
6. 统一做 group aggregation
7. `_announce_group_result()` 回投主消息总线
8. 主 agent 再生成最终用户回复
## 8.3 group 结果聚合
建议 group 执行器输出结构化结果:
```python
@dataclass
class AgentRunResult:
agent_id: str
status: str # ok | error | timeout | cancelled
summary: str
raw: dict[str, Any] | None = None
```
group 最终回投内容建议类似:
```text
[Agent group 'repo-check' completed]
Members:
- researcher: ok
- reviewer: ok
- planner: error
Results:
...
Summarize this naturally for the user. Mention disagreements if any.
```
这样能继续复用当前 `system -> main agent -> user` 的输出模式。
## 9. 推荐触发方式
## 9.1 用户显式触发
用户说法示例:
1. “把这个任务交给 `github-reviewer`
2. “让 `researcher``reviewer` 一起处理”
3. “如果有现成 agent 就不要新建 subagent”
这时主 agent 应调用:
```json
{
"task": "...",
"target": "github-reviewer"
}
```
或者:
```json
{
"task": "...",
"targets": ["researcher", "reviewer"],
"strategy": "group"
}
```
## 9.2 模型自主触发
当主 agent 判断:
1. 任务独立可并行
2. 已有 agent 专长明显更匹配
3. 任务耗时长,适合后台执行
则调用 `spawn`,但不再默认认为一定是“新建本地 subagent”。
## 9.3 自动回退
如果没有找到匹配 agent:
1. `strategy=auto` -> fallback 本地 subagent
2. `strategy=a2a` -> 直接返回未找到
3. `strategy=group` 且部分目标不存在 -> 明确报错或只跑已解析目标,建议第一版严格报错
## 10. workspace 中“已添加 agent”的建议存储
建议新增:
- `workspace/agents/registry.json`
示例:
```json
[
{
"id": "github-reviewer",
"name": "GitHub Reviewer",
"description": "Review GitHub repository changes",
"protocol": "a2a",
"base_url": "https://reviewer.example.com/a2a",
"card_url": "https://reviewer.example.com/.well-known/agent-card",
"auth_env": "GITHUB_REVIEWER_TOKEN",
"enabled": true,
"tags": ["github", "review"]
}
]
```
为什么不用直接塞进 `config.json`:
1. 这是 workspace 维度资源,不是全局运行参数
2. web API 做增删改查更方便
3. 不要求用户每次改 agent 都改配置再重启
## 11. 推荐实施顺序
### Phase 1: 打通单 agent 路由
目标:
1. 引入 `AgentRegistry`
2. `spawn` 支持 `target`
3. 支持 workspace agent 和 skill agent card
4. 支持 A2A 单点调用
5. 找不到时 fallback 本地 subagent
### Phase 2: 接入 plugin agent 本地执行
目标:
1. plugin agent 进入统一 registry
2. plugin agent 可作为 `target`
3. 本地 prompt-based agent 与 A2A remote agent 共存
### Phase 3: group 并发和聚合
目标:
1. `targets=[...]`
2. 并发执行
3. group 级状态跟踪
4. 聚合后回投主 agent
### Phase 4: web 管理接口
目标:
1. `/api/agents`
2. 添加 / 删除 / 刷新 agent
3. 前端展示 unified registry
## 12. 兼容性要求
这次改造一定要保留以下兼容性:
1. 旧的 `spawn(task, label)` 调用仍然可用
2. 没有 A2A agent 时,行为和现在一致
3. skill 没写 `agent_cards`skill 仍只是普通 skill
4. plugin agent 不参与调度时,现有 plugin 机制不受影响
## 13. 风险点
### 13.1 A2A 规范新旧写法并存
从当前公开文档和样例看,存在这些并行写法:
1. card 路径: `/.well-known/agent-card``/.well-known/agent.json`
2. RPC 方法: `tasks/send``message/send`
所以客户端必须做兼容适配,不能写死一种。
### 13.2 外部 agent 的安全边界
需要限制:
1. 白名单 host
2. 超时
3. card cache TTL
4. skill card 是否允许自动启用
### 13.3 远端 agent 无法直接访问本地 workspace
这意味着:
1. 不能把“去读本地文件然后处理”原样发给远端 A2A agent
2. 主 agent 需要先整理出必要上下文
3. 第一版最好只做文本级委派
## 14. 我建议的落地结论
如果要控制改动面,又要保证后续可扩展,推荐最终采用下面这个结构:
```text
AgentLoop
-> SpawnTool
-> DelegationManager
-> AgentRegistry / AgentResolver
-> LocalSubagentExecutor
-> PluginAgentExecutor
-> A2AExecutor
-> AgentGroupExecutor
-> announce_result() -> MessageBus(system) -> AgentLoop -> user
```
也就是说:
1. `spawn` 工具保留
2. `SubagentManager` 不再是唯一执行器
3. `DelegationManager` 成为真正总入口
4. skills 里的 `agent_cards` 用 frontmatter metadata 承载
5. workspace agent 单独持久化
6. group 通过并发 executor + 汇总消息实现
这是当前仓库里最稳妥、最符合现有架构的改法。
## 15. 外部参考
以下是我写这个方案时核对的 A2A 资料:
1. A2A Protocol Development Guide: https://a2aprotocol.ai/docs/guide/a2a-typescript-guide.html
2. Python A2A Tutorial: https://a2aprotocol.ai/docs/guide/python-a2a-tutorial-20250513
注意:
1. 当前公开文档里既能看到 `tasks/send`,也能看到 `message/send`
2. agent card 路径也能看到 `agent-card``agent.json` 两种写法
3. 所以实现时建议做兼容层,不要只押一种命名