使用agentscope自动注册agent应用到nacos以及对a2a协议的思考

参考资料

本文主要记录了在测试agent自动注册nacos过程中对a2a的一些思考,可能存在一些理解的偏差,请审慎自主思考阅读。

借助A2A 对接能力可以实现像调用本地工具一样自然地调用远端 A2A Agent,实现跨语言、跨框架的协同。agentscope能够将应用包装成符合 A2A 规范的服务端点。

这里使用agentscope runtime启动agent,并同时注册在nacos中,如下示例

# 创建 Nacos Registry 实例
registry = NacosRegistry(
    nacos_client_config=ClientConfigBuilder()
    .server_address("localhost:8848")
    # 其他可选配置项
    .build()
)
agent_app = AgentApp(
    app_name="TourGuideAgent",
    app_description="专业导游智能体,提供旅游咨询、路线规划和景点介绍服务",
    # 在 a2a_config 中配置 registry 和技能
    a2a_config=AgentCardWithRuntimeConfig(
        port=8091,
        registry=registry,
        agent_card={
            "skills": [
                AgentSkill(
                    id="attraction_recommendation",
                    name="景点推荐",
                    description="根据旅游目的地和时间,推荐当地热门景点和小众景点",
                    inputModes=["text"],
                    outputModes=["text"],
                    tags=["景点", "推荐", "旅游"]
                ),
                AgentSkill(
                    id="travel_consultation",
                    name="旅游咨询",
                    description="解答关于交通、住宿、美食、天气等旅游相关问题",
                    inputModes=["text"],
                    outputModes=["text"],
                    tags=["旅游咨询", "交通", "住宿", "美食"]
                )
            ]
        }
    ),
)

@agent_app.query(framework="agentscope")
async def query_func(
    self,
    msgs,
    request: AgentRequest = None,
    **kwargs,
):
  
    # 创建ReAct智能体实例
    agent = ReActAgent(
        name="TourGuide",
        model=OpenAIChatModel(
            model_name="gpt-4o-mini",
            api_key="sk-uzpq0u0n5FN14HorW45hUw",
            client_kwargs={"base_url": "http://172.31.14.46:4000"},
            stream=True,  # 启用流式响应
        ),
        sys_prompt="你是一位专业的导游,熟悉各地的旅游景点、历史文化和风土人情。请为游客提供详细的旅游咨询、路线规划和景点介绍。回答时要热情友好,信息准确,并尽可能提供实用的旅游建议。",
        memory=InMemoryMemory(),
        formatter=OpenAIChatFormatter(),
    )
    agent.set_console_output_enabled(enabled=False)


    # 执行智能体并流式返回消息
    async for msg, last in stream_printing_messages(
        agents=[agent],
        coroutine_task=agent(msgs),
    ):
        yield msg, last

if __name__ == "__main__":
    import asyncio
    async def main():
        result = await agent_app.deploy(
            LocalDeployManager(host="0.0.0.0", port=8091), mode=DeploymentMode.DAEMON_THREAD
        )
        stop_event = asyncio.Event()
        await stop_event.wait()

    asyncio.run(main())

启动服务后,可以观察到nacos中已经自动注册了a2a服务

image-20260122230110461

使用如下客户端请求

async def main() -> None:
    # 1. 从 Nacos 获取 Agent Card
    agent_card = await NacosAgentCardResolver(
        remote_agent_name="TourGuideAgent",
        nacos_client_config=ClientConfig(
            server_addresses="http://127.0.0.1:8848",
        ),
    ).get_agent_card()
    print(f"获取到 Agent Card: {agent_card.name}")
    
    # 2. 调用 agent 获取回复
    agent = A2AAgent(agent_card=agent_card)

    msg = Msg(
        name="user",
        content="告诉我亚马逊有哪些计算服务?",
        role="user",
    )
    await agent(msg)

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

这个时候我希望了解下获取a2a card已经agent之间通信的细节,于是通过抓包的方式来检查。

由于已经将agent card注册在nacos中,并且在客户端中通过NacosAgentCardResolver检索,因此客户端能够通过nacos获取a2a card信息,类似如下内容

$ curl localhost:8091/.well-known/agent-card.json

{"additionalInterfaces":[],"capabilities":{"pushNotifications":false,"stateTransitionHistory":false,"streaming":false},"defaultInputModes":["text"],"defaultOutputModes":["text"],"description":"专业导游智能体,提供旅游咨询、路线规划和景点介绍服务","name":"TourGuideAgent","preferredTransport":"JSONRPC","protocolVersion":"0.3.0","skills":[{"description":"根据旅游目的地和时间,推荐当地热门景点和小众景点","id":"attraction_recommendation","inputModes":["text"],"name":"景点推荐","outputModes":["text"],"tags":["景点","推荐","旅游"]},{"description":"解答关于交通、住宿、美食、天气等旅游相关问题","id":"travel_consultation","inputModes":["text"],"name":"旅游咨询","outputModes":["text"],"tags":["旅游咨询","交通","住宿","美食"]}],"url":"http://172.31.14.46:8091/a2a","version":"1.0.0"}

然而在操作过程中发现无法获取到访问8848端口的请求,

  1. 程序成功运行并获取到了 a2a card,但是抓包只看到 8091 端口的流量
  2. 但是程序访问了 Nacos,不存在缓存机制(已经重启了客户端)

通过strace我们发现client实际访问nacos使用的是9848端口

# strace -f -e trace=network python agentscope/a2a/client.py
[pid 1146518] connect(12, {sa_family=AF_INET, sin_port=htons(9848), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
[pid 1146520] connect(12, {sa_family=AF_INET6, sin6_port=htons(9848), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_scope_id=0}, 28) = -1 EINPROGRESS (Operation now in progress)
[pid 1146518] connect(13, {sa_family=AF_INET, sin_port=htons(9848), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
[pid 1146519] connect(13, {sa_family=AF_INET6, sin6_port=htons(9848), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_scope_id=0}, 28) = -1 EINPROGRESS (Operation now in progress)
[pid 1146475] connect(14, {sa_family=AF_INET, sin_port=htons(8091), sin_addr=inet_addr("172.31.14.46")}, 16) = -1 EINPROGRESS (Operation now in progress)

Nacos 的 gRPC 端口通常是 9848,也就是说客户端Nacos SDK 使用了 gRPC 协议来通信,具体代码入口为。关于这一点的说明见,https://nacos.io/blog/faq/nacos-user-question-history15295/

Nacos 从 2.0 版本开始引入了 gRPC 协议作为客户端与服务器之间的通信方式。Nacos 2.0 引入 gRPC 的主要目的是为了提升性能和扩展性

# agentscope/a2a/_nacos_resolver.py
client = await NacosAIService.create_ai_service(...)
return await client.get_agent_card(...)

# v2/nacos/ai/nacos_ai_service.py
self.grpc_client_proxy = AIGRPCClientProxy(...)
return await self.grpc_client_proxy.get_agent_card(...)

继续查看抓包结果,追踪HTTP流结果如下

image-20260122233546447

可见客户端向agent的/a2a发起post请求

{
    "id": "8725cad3-0d83-48d2-bed3-da456fca651e",
    "jsonrpc": "2.0",
    "method": "message/send",
    "params": {
        "configuration": {
            "acceptedOutputModes": [],
            "blocking": true
        },
        "message": {
            "kind": "message",
            "messageId": "3fef013a-842c-462c-9be6-d5bdf23a4b28",
            "parts": [
                {
                    "kind": "text",
                    "text": "告诉我亚马逊有哪些计算服务?"
                }
            ],
            "role": "user"
        }
    }
}

响应内容如下,响应内容和ADK的示例还是有区别的,可能是由于A2A协议本身也在变动中。

{
    "id": "8725cad3-0d83-48d2-bed3-da456fca651e",
    "jsonrpc": "2.0",
    "result": {
        "kind": "message",
        "messageId": "msg_46df2210-d0f8-463c-a108-ae69bd62c2b5",
        "parts": [
            {
                "kind": "text",
                "text": "您提到服您推荐一个适合初学者的“第一个 AWS 计算项目”?😊"
            }
        ],
        "role": "agent"
    }
}

这里有一个问题,a2a调用并没有提到需要使用那些技能,这一点和mcp tool中通过明确的schema定义支持的工具是有区别的,我理解由于a2a本身是供agent之间相互调用的协议,因此a2a card中携带的内容实际上起到的作用类似于对agent的描述。

所以,a2a更像是意图委托,Client并不关心 Server 具体用了什么函数和方法,只是发送一个自然语言的任务。Remote Agent 接到任务后,自己进行推理,决定是否需要使用其他工具来完成这个任务。

所以当我们将remote agent的系统提示定义为一个技术专家,但是skill中指定导游的技能时,会得到以下回复。因为skill只决定了client agent是否会发送相关的请求,而remote agent的输出并不会受到skill的影响。

image-20260122235413288

posted @ 2026-01-22 23:58  zhaojie10  阅读(1)  评论(0)    收藏  举报