三协议合一实战:用MCP+A2A+ANP搭建一个智能研究助手

三协议合一实战:用MCP+A2A+ANP搭建一个智能研究助手

写在前面

MCP、A2A、ANP三个协议我们都分别讲过了------MCP管工具接入,A2A管Agent间协作,ANP管服务发现。但分开讲总觉得不够直观,很多人问:"这三个协议放在一起到底怎么用?能不能给一个完整的例子?"

今天这篇文章就来满足这个需求。我会带大家从零搭建一个"智能研究助手"系统------用户输入一个研究主题,系统自动完成搜索资料、阅读网页、整合信息、撰写报告的全流程。整个系统会同时用到MCP、A2A、ANP三个协议,让你直观看到它们是怎么协同工作的。

这个项目我自己花了一个周末做出来,虽然不算复杂,但跑通的那一刻还是挺有成就感的。希望这篇文章能帮你把三个协议的知识串起来。

一、项目目标和系统架构

先明确一下我们要做什么。智能研究助手的核心功能是:

用户输入一个研究主题(比如"大模型推理优化的最新进展"),系统自动:

  1. 搜索相关资料
  2. 阅读搜索到的网页
  3. 整合信息,撰写一份结构化的研究报告

为了实现这个功能,我们设计四个Agent:

  • 主控Agent(Orchestrator):接收用户请求,分解任务,协调其他Agent,汇总最终结果
  • 搜索Agent(Searcher):负责搜索相关资料,通过MCP调用搜索引擎工具
  • 阅读Agent(Reader):负责抓取和阅读网页内容,通过MCP调用网页抓取工具
  • 写作Agent(Writer):负责整合资料,撰写研究报告

三个协议在系统中的分工:

复制代码
用户 → 主控Agent
         │
         │  ANP:发现搜索/阅读/写作Agent的地址
         ▼
    ┌────┴────┐
    ▼         ▼         ▼
 搜索Agent  阅读Agent  写作Agent
    │         │
    │ MCP     │ MCP
    ▼         ▼
 搜索引擎   网页抓取
  • ANP:主控Agent通过ANP发现其他三个Agent的地址,不需要硬编码
  • A2A:主控Agent通过A2A跟其他三个Agent通信,委托任务、获取结果
  • MCP:搜索Agent和阅读Agent通过MCP调用外部工具(搜索引擎、网页抓取)

你看,三个协议各司其职,组合起来就是一个完整的多Agent系统。

二、第一步:搭建ANP注册中心

老规矩,先把基础设施搭起来。注册中心是整个系统的"通讯录",所有Agent启动时都来这里注册。

python 复制代码
# registry.py
from helloagents.anp import ANPRegistry

registry = ANPRegistry(
    host="0.0.0.0",
    port=9000,
    health_check_interval=15,
    expire_after=45
)

if __name__ == "__main__":
    print("ANP注册中心已启动,端口9000")
    registry.run()

就这么简单。运行起来之后,所有Agent都可以注册到这里。

三、第二步:实现MCP工具服务器

搜索Agent和阅读Agent需要调用外部工具。我们用MCP来封装这些工具。

搜索工具MCP服务器

python 复制代码
# search_mcp_server.py
from mcp.server import Server
from mcp.types import Tool, TextContent
import asyncio

server = Server("search-tools")

@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="web_search",
            description="搜索互联网,返回相关网页的标题、链接和摘要",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索关键词"},
                    "num_results": {"type": "integer", "description": "返回结果数量", "default": 5}
                },
                "required": ["query"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name, arguments):
    if name == "web_search":
        query = arguments["query"]
        num = arguments.get("num_results", 5)
        # 实际项目中这里调用真实的搜索API(如Bing、Google)
        # 这里用模拟数据
        results = [
            {"title": f"{query}相关文章1", "url": "https://example.com/1", "snippet": "这是第一篇文章的摘要..."},
            {"title": f"{query}相关文章2", "url": "https://example.com/2", "snippet": "这是第二篇文章的摘要..."},
            {"title": f"{query}相关文章3", "url": "https://example.com/3", "snippet": "这是第三篇文章的摘要..."},
        ][:num]
        return [TextContent(type="text", text=str(results))]
    return [TextContent(type="text", text="未知工具")]

if __name__ == "__main__":
    asyncio.run(server.run_stdio())

网页抓取工具MCP服务器

python 复制代码
# fetch_mcp_server.py
from mcp.server import Server
from mcp.types import Tool, TextContent
import asyncio

server = Server("fetch-tools")

@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="fetch_webpage",
            description="抓取指定URL的网页内容,返回正文文本",
            inputSchema={
                "type": "object",
                "properties": {
                    "url": {"type": "string", "description": "网页URL"},
                    "max_length": {"type": "integer", "description": "最大返回字符数", "default": 5000}
                },
                "required": ["url"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name, arguments):
    if name == "fetch_webpage":
        url = arguments["url"]
        max_len = arguments.get("max_length", 5000)
        # 实际项目中这里用requests/httpx抓取真实网页
        # 这里用模拟数据
        content = f"这是从{url}抓取的网页正文内容。文章讨论了相关主题的核心观点、方法论和最新进展..."[:max_len]
        return [TextContent(type="text", text=content)]
    return [TextContent(type="text", text="未知工具")]

if __name__ == "__main__":
    asyncio.run(server.run_stdio())

这两个MCP服务器分别提供搜索和网页抓取能力。实际项目中,你可以把模拟数据换成真实的API调用。

四、第三步:实现各个Agent

搜索Agent

搜索Agent通过MCP调用搜索工具,然后把结果返回给主控Agent。

python 复制代码
# search_agent.py
from helloagents import A2AAgent, TaskResult, MCPTransport
import asyncio

class SearchAgent(A2AAgent):
    name = "搜索助手"
    description = "搜索互联网资料"
    capabilities = ["research.search"]
    
    anp_registry_url = "http://localhost:9000"
    anp_agent_host = "localhost"
    anp_agent_port = 8001
    
    async def on_start(self):
        # 连接到搜索MCP服务器
        self.search_transport = MCPTransport(command="python", args=["search_mcp_server.py"])
        await self.search_transport.connect()
    
    async def handle_task(self, task):
        query = task.input.get("query", "")
        num = task.input.get("num_results", 5)
        
        # 通过MCP调用搜索工具
        result = await self.search_transport.call_tool(
            name="web_search",
            arguments={"query": query, "num_results": num}
        )
        
        return TaskResult.success({
            "query": query,
            "results": eval(result.content[0].text)  # 实际项目中用json.loads
        })

if __name__ == "__main__":
    SearchAgent().run(host="0.0.0.0", port=8001)

阅读Agent

阅读Agent通过MCP调用网页抓取工具,读取网页内容。

python 复制代码
# reader_agent.py
from helloagents import A2AAgent, TaskResult, MCPTransport

class ReaderAgent(A2AAgent):
    name = "阅读助手"
    description = "抓取和阅读网页内容"
    capabilities = ["research.read"]
    
    anp_registry_url = "http://localhost:9000"
    anp_agent_host = "localhost"
    anp_agent_port = 8002
    
    async def on_start(self):
        self.fetch_transport = MCPTransport(command="python", args=["fetch_mcp_server.py"])
        await self.fetch_transport.connect()
    
    async def handle_task(self, task):
        urls = task.input.get("urls", [])
        contents = []
        
        for url in urls[:3]:  # 最多读3个网页,避免太长
            result = await self.fetch_transport.call_tool(
                name="fetch_webpage",
                arguments={"url": url, "max_length": 3000}
            )
            contents.append({"url": url, "content": result.content[0].text})
        
        return TaskResult.success({
            "urls": urls,
            "contents": contents,
            "summary": f"已阅读{len(contents)}个网页"
        })

if __name__ == "__main__":
    ReaderAgent().run(host="0.0.0.0", port=8002)

写作Agent

写作Agent接收资料,撰写研究报告。这个Agent不需要MCP工具,纯靠大模型能力。

python 复制代码
# writer_agent.py
from helloagents import A2AAgent, TaskResult

class WriterAgent(A2AAgent):
    name = "写作助手"
    description = "整合资料撰写研究报告"
    capabilities = ["research.write"]
    
    anp_registry_url = "http://localhost:9000"
    anp_agent_host = "localhost"
    anp_agent_port = 8003
    
    async def handle_task(self, task):
        topic = task.input.get("topic", "")
        materials = task.input.get("materials", [])
        
        # 实际项目中这里调用大模型生成报告
        # 这里用模板模拟
        report = f"""# {topic}研究报告

## 概述
本报告围绕"{topic}"主题,基于搜集到的资料进行整理和分析。

## 核心观点
1. 观点一:基于资料分析,该领域的主要发展方向是...
2. 观点二:关键技术挑战包括...
3. 观点三:未来趋势可能是...

## 资料来源
共参考了{len(materials)}份资料。

## 结论
{topic}是一个快速发展的领域,值得持续关注。
"""
        return TaskResult.success({
            "topic": topic,
            "report": report,
            "word_count": len(report)
        })

if __name__ == "__main__":
    WriterAgent().run(host="0.0.0.0", port=8003)

主控Agent

主控Agent是整个系统的核心,它通过ANP发现其他Agent,通过A2A委托任务,最后汇总结果。

python 复制代码
# orchestrator.py
from helloagents import A2AAgent, ANPClient, A2AClient, TaskResult
import asyncio

class OrchestratorAgent(A2AAgent):
    name = "研究助手"
    description = "智能研究助手,自动完成资料搜索、阅读和报告撰写"
    capabilities = ["research.plan"]
    
    anp_registry_url = "http://localhost:9000"
    
    async def on_start(self):
        self.anp = ANPClient(self.anp_registry_url)
    
    async def handle_task(self, task):
        topic = task.input.get("topic", "")
        print(f"收到研究任务:{topic}")
        
        # 第一步:通过ANP发现搜索Agent
        search_agent = await self.anp.discover_one(capability="research.search")
        print(f"发现搜索Agent:{search_agent.name}")
        
        # 第二步:委托搜索任务
        search_client = await A2AClient.connect(search_agent.address)
        search_result = await search_client.create_task_and_wait(
            input={"query": topic, "num_results": 5},
            timeout=60
        )
        await search_client.close()
        print(f"搜索完成,找到{len(search_result.output['results'])}个结果")
        
        # 第三步:通过ANP发现阅读Agent
        reader_agent = await self.anp.discover_one(capability="research.read")
        print(f"发现阅读Agent:{reader_agent.name}")
        
        # 第四步:委托阅读任务
        urls = [r["url"] for r in search_result.output["results"]]
        reader_client = await A2AClient.connect(reader_agent.address)
        reader_result = await reader_client.create_task_and_wait(
            input={"urls": urls},
            timeout=120
        )
        await reader_client.close()
        print(f"阅读完成,读取了{len(reader_result.output['contents'])}个网页")
        
        # 第五步:通过ANP发现写作Agent
        writer_agent = await self.anp.discover_one(capability="research.write")
        print(f"发现写作Agent:{writer_agent.name}")
        
        # 第六步:委托写作任务
        writer_client = await A2AClient.connect(writer_agent.address)
        writer_result = await writer_client.create_task_and_wait(
            input={
                "topic": topic,
                "materials": reader_result.output["contents"]
            },
            timeout=120
        )
        await writer_client.close()
        print("报告撰写完成")
        
        # 第七步:汇总结果
        return TaskResult.success({
            "topic": topic,
            "report": writer_result.output["report"],
            "sources": search_result.output["results"],
            "stats": {
                "search_results": len(search_result.output["results"]),
                "pages_read": len(reader_result.output["contents"]),
                "report_words": writer_result.output["word_count"]
            }
        })

if __name__ == "__main__":
    OrchestratorAgent().run(host="0.0.0.0", port=8000)

主控Agent的逻辑很清晰:发现 → 委托搜索 → 委托阅读 → 委托写作 → 汇总。每一步都通过ANP发现Agent,通过A2A委托任务。

五、运行整个系统

现在所有组件都写好了,我们按顺序启动:

bash 复制代码
# 终端1:启动注册中心
python registry.py

# 终端2:启动搜索Agent(会自动连接MCP搜索工具)
python search_agent.py

# 终端3:启动阅读Agent(会自动连接MCP抓取工具)
python reader_agent.py

# 终端4:启动写作Agent
python writer_agent.py

# 终端5:启动主控Agent
python orchestrator.py

全部启动后,你可以在注册中心查一下,确认四个Agent都注册成功了:

bash 复制代码
curl http://localhost:9000/agents

然后给主控Agent发一个任务:

bash 复制代码
curl -X POST http://localhost:8000/task \
  -H "Content-Type: application/json" \
  -d '{"input": {"topic": "大模型推理优化的最新进展"}}'

你应该能看到主控Agent的终端输出类似这样的日志:

复制代码
收到研究任务:大模型推理优化的最新进展
发现搜索Agent:搜索助手
搜索完成,找到5个结果
发现阅读Agent:阅读助手
阅读完成,读取了3个网页
发现写作Agent:写作助手
报告撰写完成

最后返回一份完整的研究报告。整个过程中,主控Agent完全不需要知道其他Agent的地址------全靠ANP自动发现;Agent之间的通信全靠A2A;工具调用全靠MCP。三个协议完美配合。

六、系统中的协议协作细节

跑通之后,我想再深入聊聊三个协议在这个系统里是怎么协作的,一些细节可能你之前没注意到。

ANP的作用:解耦地址和能力

在这个系统里,主控Agent没有硬编码任何其他Agent的地址。它只知道"我需要一个能搜索的Agent",ANP就帮它找到了。这意味着:

  • 搜索Agent换了地址,主控Agent不需要改
  • 部署多个搜索Agent实例,主控Agent自动负载均衡
  • 搜索Agent挂了,ANP自动摘除,主控Agent不会再调用它

这种解耦在系统规模变大的时候尤其重要。

A2A的作用:标准化Agent间协作

主控Agent跟其他Agent的通信全是A2A协议。不管是搜索Agent还是写作Agent,主控Agent用的都是同一套API:connect → create_task_and_wait → close。不需要为不同的Agent写不同的调用逻辑。

而且A2A的Task机制自带状态跟踪------主控Agent可以随时查询任务进度,任务失败了也有明确的错误信息。

MCP的作用:工具即插即用

搜索Agent需要搜索引擎,阅读Agent需要网页抓取。这些工具都是通过MCP接入的。如果以后想把搜索引擎从Bing换成Google,只需要换一个MCP服务器,搜索Agent的代码一行都不用改。

这就是协议的力量------每一层都可以独立替换,不影响其他层。

七、我踩的坑

做这个项目的过程中,踩了不少坑,挑几个典型的说说:

坑1:MCP服务器的stdio模式跟Agent的stdout冲突

搜索Agent本身是一个HTTP服务,会输出日志到stdout。但它连接的MCP服务器也是stdio模式,MCP协议需要用stdout传消息。结果日志混进了MCP消息,导致解析失败。

解决办法:把Agent的日志输出到stderr或者文件,不要用stdout。MCP的stdio传输对stdout是独占的。

坑2:ANP发现到的Agent可能已经挂了

有一次我把搜索Agent关了,但还没到过期时间,ANP还返回了它的地址。主控Agent连接的时候就超时了。

解决办法:客户端一定要做超时和重试。发现到的Agent不一定是活的,连接失败了就换一个,或者等ANP自动摘除后重试。

坑3:任务超时时间要设合理

阅读Agent要抓多个网页,可能比较慢。我一开始把超时设成30秒,经常超时。后来改成120秒就稳定了。不同类型的任务超时时间不一样,搜索可能30秒够了,阅读和写作可能需要更长。

坑4:大模型生成的报告可能引用不存在的资料

写作Agent拿到资料后,大模型可能会"幻觉"出一些资料里没有的内容。这个问题靠协议解决不了,需要在Prompt里强调"只能基于提供的资料撰写,不要编造",或者加一个事实核查的环节。

坑5:组件太多,调试困难

四个Agent + 两个MCP服务器 + 一个注册中心,七个进程同时跑,出了问题不知道是哪一层的锅。

解决办法:每个组件都加详细的日志,并且日志里带上请求ID,这样可以追踪一个请求在各个组件中的流转路径。

八、可以扩展的方向

这个项目只是一个最小可用版本,还有很多可以扩展的方向:

1. 并行化:目前搜索、阅读、写作是串行的。其实搜索完成后,可以并行阅读多个网页,甚至并行写作不同章节。

2. 迭代优化:写作Agent写完报告后,可以再让一个"评审Agent"审阅,提出修改意见,然后写作Agent修改,多轮迭代提升质量。

3. 缓存机制:相同的研究主题可以缓存结果,避免重复搜索和阅读。

4. 用户反馈:用户可以对报告打分和评论,系统根据反馈优化后续的搜索和写作策略。

5. 多模态:目前只处理文本,以后可以加入图片、视频、PDF等多模态资料的处理。

这些扩展都不需要改动协议层------MCP加新工具、A2A加新Agent、ANP自动发现,架构天然支持扩展。

写在最后

做完这个项目,我最大的感受是:协议让复杂系统变得可控。

如果没有这些协议,四个Agent之间的通信、工具调用、服务发现,每一处都得自己写定制化的代码,系统稍微大一点就会变成一团乱麻。有了MCP、A2A、ANP这些标准协议,每个组件只需要关注自己的业务逻辑,跟其他组件的交互全靠标准化的协议,系统就清晰多了。

这篇文章是10.4节ANP协议实战的收尾,也是整个通信协议章节的倒数第二篇。从"为什么需要协议"到"三协议合一实战",我们把Agent通信的核心内容都过了一遍。

下一篇会进入10.5节,聊聊怎么构建自定义MCP服务器------这是你把自己的工具接入Agent世界的关键技能。

你们有没有做过类似的多Agent项目?用的什么技术栈?遇到过什么有意思的问题?欢迎在评论区交流。


下一篇预告:创建你的第一个MCP服务器。我们会从零开始,手把手教你写一个功能完整的MCP服务器,把你自己的工具暴露给Agent使用。

相关推荐
大侠归来1 小时前
Redis Cluster 的 16384 个槽(Slot)是什么?
数据库·redis·缓存
Chengbei112 小时前
JsOwl|AI 赋能 JS 敏感信息扫描器,AST + 正则、source‑map 源码还原、登录态爬虫输出交互式 HTML 报告
开发语言·javascript·网络·人工智能·爬虫·网络安全·自动化
leoZ2312 小时前
第 31 篇 金融与医疗:高合规行业的 AI 产品方法论
大数据·人工智能·金融·大模型·agent
杨运交2 小时前
[079][消息模块]基于 Redis Stream 的高可靠消息队列实现——Spring Data Redis 实战解析
数据库·redis·spring
北京中科新远科技2 小时前
高速光链路故障排查实战:从DOM光功率到FEC与BER
运维·服务器·网络
xrlfreedom2 小时前
MCP 面试高频考点:把知识库 RAG 检索封装为 Tool,Docker 与结构化输出怎么配合不踩坑
docker·结构化输出·mcp
xrlfreedom2 小时前
MCP 面试高频考点:如何给 Tool 调用做身份认证、最小权限与可观测
opentelemetry·prompts·mcp
是真的小外套2 小时前
第16章:网安风险评估技术原理及应用
网络·web安全·网络安全