基于 Agent-to-Agent 协议实现研究员与作者 Agent 的去中心化协作机制
- 作者

- 姓名
- Nino
- 职业
- Senior Tech Editor
当今绝大多数智能体(AI Agent)编排演示都存在一个隐蔽的架构局限:它们将多个 Agent 强行绑定在单个框架内部(例如 LangChain、LangGraph 或 Agno)。这种模式虽然适合构建单体应用,但无法解决真正的智能体互操作问题(Agent Interoperability)——不同团队在不同语言、不同运行时上开发的独立 Agent,究竟该如何进行标准的、无侵入的跨平台协作?
Agent-to-Agent (A2A) 协议(agent2agent.dev)正是为此而生的开放、中立标准。你可以将它理解为“Agent 领域的 HTTP 协议”。它定义了自包含的 Agent 名片发现机制、任务生命周期模型,以及基于 JSON-RPC 2.0 和 Server-Sent Events (SSE) 的消息传输标准。在构建高性能的多智能体协作架构时,开发者通常依赖于像 n1n.ai 这样稳定且高速的 LLM API 聚合平台,为高并发的代理任务提供强力的底层模型支持。
本文将手把手带你使用纯 Node.js/TypeScript 与 React 构建一个轻量且完整的 A2A 协议实现:一个具备真实 Web 检索能力的 Researcher Agent(研究员),与一个支持逐 Token 实时流式输出 Markdown 文稿的 Writer Agent(作者),并通过浏览器客户端完成无框架依赖的任务交接(Handoff)。
1. 架构设计与 A2A 协议核心机制
A2A 协议将 Agent 间的通信规约分解为以下三个关键层次:
- Agent Card 发现机制:每个符合 A2A 规范的 Agent 都在
GET /.well-known/agent.json或通过agent/getCard方法公开其能力名片,声明其支持的 Skill、输入/输出格式及能力参数(如是否支持 Streaming)。 - 任务生命周期状态机:任务具备明确的状态变化:
submitted(已提交)→working(处理中)→completed(已完成),以及failed(失败)、canceled(已取消)、rejected(拒绝)与input-required(需补充输入)。 - 双模传输协议:同步任务通过 JSON-RPC 2.0 的
message/send发送;长耗时或流式任务通过 SSE 的message/stream订阅 incremental 输出。
系统三节点拓扑架构如下:
┌────────────────────────────────────────────────────────────┐
│ 前端浏览器客户端 (Vite + React) │
│ A2A Client 调度器 │
│ 主题输入 · 配置面板 · JSON-RPC 观察时间线 · Markdown 预览 │
└───────────────┬──────────────────────────────┬─────────────┘
│ JSON-RPC 2.0 over HTTP (CORS) │
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ Researcher Agent │ │ Writer Agent │
│ (Port 3001) │ │ (Port 3002) │
│ streaming: false │ │ streaming: true │
│ message/send │ │ message/stream │
│ ├─ Tavily 搜索引擎 │ │ └─ LLM Token Stream│
│ └─ LLM 汇总生成 │ │ │
└────────────────────┘ └────────────────────┘
时序交互图
浏览器客户端 (5173) 研究员 Agent (3001) 作者 Agent (3002)
│ │ │
│────── agent/getCard ──────>│ │
│<───── 名片响应 (无流式) ─────│ │
│ │
│───────────────────────── agent/getCard ─────────────────>│
│<──────────────────────── 名片响应 (支持流式) ──────────────│
│ │
│────── message/send { topic } ───────────────────────────>│
│<───── Task Completed { 报告内容, 真实来源 URL } ──────────│
│ │
│───────────────────────── message/stream { 报告内容 } ---->│
│<──────────────────────── SSE: working 状态 ──────────────│
│<──────────────────────── SSE: message/part (逐块输出) ───│
│<──────────────────────── SSE: message/complete ──────────│
2. 定义标准 Agent 名片(Agent Card)
Agent 名片是实现去中心化智能体发现的基础。Writer Agent 在名片中明确标明了其具备 capabilities.streaming = true,这为客户端选择调用 message/send 还是 message/stream 提供了契约依据。
Writer Agent Card 数据结构
{
"name": "Writer Agent",
"description": "将研究报告转化为结构化文档,支持逐 Token 流式输出。",
"url": "http://localhost:3002",
"version": "0.1.0",
"skills": [
{
"id": "structured-drafting",
"name": "Structured Drafting",
"description": "根据研究资料生成结构良好的 Markdown 初稿"
}
],
"capabilities": {
"streaming": true
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"]
}
在跨团队协作多智能体系统时,为了避免不同模型提供商接口兼容性带来的繁琐适配,企业级团队常通过 n1n.ai 统一接入各类 LLM API,使得不论底层的模型是 DeepSeek-V3、Claude 3.5 Sonnet 还是 OpenAI o3,都能保证流畅一致的 API 交互与极低的传输延迟。
3. 实现 Researcher Agent(同步任务处理)
Researcher Agent 负责处理同步交互模式。收到主题后,它会使用大语言模型生成 2–3 个针对性的搜索关键词,调用 Tavily Search API 获取实时网络数据,去重后生成带有引用序号的总结报告。
核心实现代码 (researcher-agent/src/index.ts)
import express, { Request, Response } from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
interface JSONRPCRequest {
jsonrpc: string;
method: string;
params: any;
id: string | number;
}
app.post('/jsonrpc', async (req: Request, res: Response) => {
const { jsonrpc, method, params, id } = req.body as JSONRPCRequest;
if (method === 'agent/getCard') {
return res.json({
jsonrpc: '2.0',
id,
result: {
name: 'Researcher',
capabilities: { streaming: false },
skills: [{ id: 'web-search', name: 'Web Research' }]
}
});
}
if (method === 'message/send') {
const { topic, apiKey } = params;
// 1. LLM 自动扩展搜索关键词
const queries = await generateSearchQueries(topic, apiKey);
// 2. 调用 Tavily 搜索 API 获取真实网页数据
const sources = await executeTavilySearch(queries);
// 3. LLM 结构化总结并标注引用
const summary = await synthesizeFindings(topic, sources, apiKey);
return res.json({
jsonrpc: '2.0',
id,
result: {
taskId: crypto.randomUUID(),
status: { state: 'completed' },
message: {
role: 'agent',
parts: [{ kind: 'text', text: summary }]
},
sources: sources.map(s => ({ title: s.title, url: s.url }))
}
});
}
return res.status(404).json({ error: 'Method not found' });
});
诚实原则:容错与降级设计
在开发 Agent 协作流程时,严禁伪造参考数据。如果用户未配置 Tavily 密钥,Researcher 不应伪造检索链接,而是明确告知客户端:
if (!tavilyKey) \{
return \{
taskId: crypto.randomUUID(),
status: \{ state: 'completed' \},
message: \{
role: 'agent',
parts: [\{ kind: 'text', text: "⚠️ 提示:未配置实时搜索密钥,输出结果完全基于模型已有知识库。