
开头Agent正在互相调用——这句话放在三年前大概还只是科幻片里的桥段放在今天已经是各大Agent框架的基本操作。LangChain里的AgentExecutor、CrewAI的Crew、AutoGen的GroupChat哪一个不是让几个Agent在你设定的剧本里你一言我一语地配合完成任务但如果你把视野从一个Python进程里跑三个Agent挪到两个不同公司的Agent隔着公网协作你会立刻撞上一堵墙它们之间既不知道对方能干什么也不知道怎么把一个活儿交出去更不知道活儿干到一半出了岔子该找谁。A2A 1.0Agent2Agent Protocol 1.0就是冲着这堵墙来的。这个协议由Google在2025年提出并开源联合了Salesforce、SAP、Cohere等一批厂商目标是把Agent之间的协作从框架内的私有方言升级成跨系统、跨组织、跨信任边界的通用协议。我看不少团队拿到A2A之后的第一反应是这不就是让Agent调Agent的API吗老套。乍一听确实没什么新鲜的但真正把规范读一遍、再动手搭一个跨Agent协作场景之后你会发现它真正解决的问题不是怎么调用而是协作边界——能力怎么声明、任务怎么交接、状态怎么跟踪、安全怎么兜底。这些才是两个Agent在并不互相了解的前提下敢把活儿交给对方的底气。这篇就聊聊我对A2A 1.0的理解、拆解它的四个核心概念再附上我实际落地时踩过的坑和排查思路。1. 从能互相调用到敢互相调用协作边界是怎么浮出水面的1.1 框架内的Agent协作其实早就实现了Agent互相调用这件事放在今天真不算新鲜。你用LangGraph或LangChain定义几个Agent节点让一个Agent的输出作为另一个Agent的输入用CrewAI把不同角色、不同目标的Agent组织成一支小队让它们按流程协作或者在AutoGen里建一个GroupChat让几个Agent像群聊一样讨论决策。这些都是非常成熟的多Agent协作形态市面上教程一抓一大把。但注意这类协作有一个共同前提所有Agent都在同一个框架、同一个进程、同一套代码里活着。它们的通信方式说白了就是函数调用加内存共享它们的记忆和上下文全都挤在同一个上下文窗口里它们的行为剧本由你一个人写在编排配置里。本质上这些Agent是一个公司里的同事共享同一个老板、同一套人事制度。这种模式解决的是任务编排问题把一个大任务切成几块分给几个专职Agent像生产线一样运转。它很高效但它是单系统内部的协作边界问题并不突出。真正麻烦的是当边界往外扩一层之后发生的事这也是我在实际项目里反复撞墙的地方。1.2 跨系统、跨厂商协作时三个边界问题躲不开一旦你想让别的系统、别的团队甚至别的公司的Agent参与协作事情立刻变味。我在项目里遇到的三个最典型的边界问题这里逐个说清楚。第一是能力边界。我这边有一个能理解用户意图的Agent想调用对方系统的能力可是对方的系统不是一个Agent而是一堆API。谁能准确告诉我的Agent你能做什么、输入是什么、返回什么如果对方也是一个Agent它的能力可能还是动态变化的又该怎么同步没有能力边界上的明确声明协作就只能靠人肉写对接文档这种文档维护成本高还总是过期。第二是状态与所有权边界。普通API调用的逻辑是请求发出、响应返回然后就没了。但Agent之间的协作往往是一个长任务比如分析本周销售数据并生成一份PPT这个任务可能跑几十秒也可能跑几分钟中途还可能要回来跟发起方要补充资料。那么问题来了这个任务现在处于什么状态由谁负责推进谁有权取消算完成的标准是什么这些都需要在一开始就说清楚否则出了岔子就只能互相扯皮。第三是安全与信任边界。跨组织的Agent协作本质上是在公网上交换数据。对方会不会乱动我的数据对方返回的结果我能不能信我发给对方的数据万一泄露了谁负责这些话题一点都不浪漫但它们是决定敢不敢让Agent互call的底层问题。我们常说的协作边界其实就是这三件事能力边界、状态与所有权边界、信任与安全边界。A2A 1.0的价值恰恰就是把这三个边界在协议层面定义清楚。理解了这三个边界你再看MCP和A2A的关系也会清晰很多MCP解决的是Agent怎么去够到外部工具和数据源A2A解决的是Agent与Agent之间怎么建立外交关系、怎么交办业务两者是不同层级的事很多人一上来就把它们搞混了。2. A2A 1.0拆解四个核心概念值得吃透在动手写代码之前建议先把A2A 1.0的四个核心概念过一遍。理解了它们你再看协议文档就不会晕Agent Card、Task、Message、Artifact。2.1 Agent Card把我能做什么变成机器可读的简历A2A 1.0里每个Agent都要提供一个Agent Card说白了就是这个Agent的公开简历同时也是机器可读的能力契约。Agent Card通常放在/.well-known/agent-card这个标准路径下方便任何对端过来查档案。我贴一个精简示例方便你直观感受{ name: inventory-agent, description: 查询商品库存数量与状态的智能体, url: https://inventory.example.com/a2a, version: 1.0.0, capabilities: { streaming: true, pushNotifications: false }, skills: [ { id: check-stock, name: check_stock, description: 输入SKU编号返回该商品的实时库存数量, inputModes: [text], outputModes: [text, json] } ], security: { authentication: bearer } }这里有几个字段值得展开。name和description是给协作方看的描述要具体到输入什么、输出什么千万别写能处理各种复杂任务这种空话因为卡片不是给人看的海报主要是给别的Agent的模型看的描述越具体对方选型越准确误调用的概率越低。url是A2A请求的入口地址类似API网关。skills是核心它声明了Agent能干什么、输入输出是什么格式。capabilities声明的是协议层能力比如支不支持流式输出、支不支持服务端推送这样客户端可以决定用轮询还是SSE。security则声明了安全策略。你可以把Agent Card理解成Agent界的OpenAPI文档但它比OpenAPI多了一层策略意味它不仅告诉你接口长什么样还告诉你这个Agent在外交场合怎么打交道。这里面的每一项都是在定义能力边界。2.2 Task从调用函数升级为交办任务A2A里最核心的抽象是Task。传统API里你调用一个函数做完了就完了Agent协作里你是在把一个任务交出去然后像盯项目进度一样跟进它。Task有完整的生命周期状态规范里定义得很明确submitted任务已提交等待处理workingAgent正在执行中input-requiredAgent在执行中发现信息不足等待发起方补充completed任务完成交付Artifactfailed任务执行失败带具体错误信息canceled任务被取消任务的流转路径大致是这样从submitted进入working正常情况一路走到completed中间如果信息不足会退回input-required挂起等待发生错误则进入failed发起方或承接方都可以在某些条件下将任务置为canceled。这里input-required这个状态特别值得一提。它是Agent协作区别于传统API调用最明显的地方。传统接口打电话过去话筒里传来的永远是请一次性说完你的需求但Agent协作中执行方干到一半可能会回头说你给的参数不够再补两个维度。这种双向对话机制让Agent之间的协作真正具备了边做边沟通的能力而不是一步错、整盘崩。2.3 Message与Artifact过程对话和最终交付要分开任务执行过程中双方靠Message交换内容。Message包含role和parts可以是一段文本也可以是一个文件。整个交互就像聊天记录一样逐轮累积。而Artifact是任务执行完之后的交付物可能是一份报告、一个数据分析结果、一个文件。它和Message的区别在语义上Message是过程性的交流Artifact是结果性的交付。把它们分开设计是为了避免把聊天记录和交付成果混在一起。客户端收到Message可以流式展示给用户收到Artifact则可以落盘保存或直接喂给下一个环节。这一点在工程上很实用。比如数据分析Agent跑完任务中间过程看起来是我正在加载第2张表最终交付是一张CSV文件。协议约定就是过程中的寒暄用Message成果用Artifact。边界清晰下游处理就简单。2.4 传输层为什么选HTTP而不是消息队列A2A 1.0的传输层采用JSON-RPC 2.0 over HTTP(S)长任务的实时更新用SSEServer-Sent Events也可以选Webhook推送。很多人第一反应是Agent之间这么高频交互为什么不用Kafka或者MQTT答案很实在。HTTP是互联网的基础设施任何一个能开端口、能上公网的服务都能跑不需要额外部署一个broker也不用担心企业防火墙对私有协议的各种拦截。对A2A的目标场景——跨组织、跨技术栈的Agent协作——HTTP是最短路径。消息队列在高吞吐上确实更强但它引入了部署和运维的额外负担还要求所有参与方都兼容同一套消息基础设施。A2A 1.0优先要解决的是能不能互通而不是每秒能扛多少消息。先把路打通再谈速度。这个取舍我完全认同。还有一个现实原因消息队列的场景主要在企业内部或同一生态内而A2A要面对的恰恰是最分散、最异构的互联网环境只有HTTP具备这种普适性。3. 协作边界的本质信任、状态与安全的博弈说了这么多概念回到标题那句话A2A 1.0真正解决的是协作边界。我得再往深挖一层讲清楚这个边界到底指什么。3.1 能力边界Agent Card是一份外包合同你想让两个Agent协作第一个要解决的是它们怎么知道对方能干什么A2A的方案就是Agent Card。这个卡片本质上是边界两侧的契约。调用方不需要读对方源码不需要专门集成对方SDK只需要拿到Agent Card就知道对方的能力清单和入口然后按协议发起请求。这就像你请外包团队干活合作之前一定先看他们的能力和报价单而不是先让他们把公司内部代码开源给你。Agent Card就是这个报价单和能力说明的组合体。边界清晰了协作才有展开的基础。如果能力声明含糊对方Agent就会像面试者拿到一份含糊的JD要么不敢投要么投错了岗位最后一定是低效的协作。3.2 状态边界Task的所有权决定责任的归属Task状态机的深层含义是定义了状态的所有权边界。A2A的模型里发起方和承接方各自维护对任务的视图协议约定了谁负责推进状态、谁负责处理异常。默认情况下承接方执行Agent负责更新Task进度发起方通过轮询、SSE或Webhook去订阅。一旦任务进入input-required承接方就明确地把一部分控制权交还给发起方我需要更多信息你决定给不给、给什么。这种你来我往的状态流转就是在定义责任的边界哪个环节归谁管、卡住了找谁、交付以什么为准。这个设计在实际系统中非常有用。以前我们做跨系统流程最怕的就是一个请求发出去了后面到底跑没跑、跑到哪一步了、为什么停了全靠猜。有了Task状态机每一步都有明确的状态可查出了问题也能定位到具体环节。这就是状态边界带来的可观测性。3.3 安全边界互信不是协议给的但协议得给互信留位置A2A 1.0规范里认证和授权并没有强制绑定某种具体实现而是要求Agent在Card里通过security字段自报安全策略。这种设计很务实协议永远解决不了组织间的信任问题信任要依靠业务流程和合同的兜底但协议必须为信任的建立留出声明与协商的空间。我在落地时的标准配置是Card里声明Bearer Token认证入口网关统一校验传输链路必须走TLS敏感数据尽量用引用的方式传递而不是把整个上下文复制给对方。这些安全边界的处理才是两个Agent敢在公网上互相喊话的大前提。你甚至可以要求对端Agent在每次请求里携带一个可吊销的短期令牌而不是一把万能钥匙式的长期密钥。安全边界画得越清楚后续出事的半径就越小。3.4 记忆边界上下文该不该跨过边界还有一个很多人会忽略的边界是记忆边界。一个Agent在运行过程中积累了大量上下文、用户意图、历史对话这些是不是都要原封不动地传给协作方我的经验是不要。记忆是Agent最私密也最有价值的资产跨边界传递记忆首先要考虑隐私和合规风险其次对方Agent并不需要你的完整记忆它只需要与当前任务直接相关的输入。正确的做法是按需抽取把任务描述、必要参数、约束条件打包成Message传过去至于这家伙以前跟用户聊过什么跟这个任务没有关系就不要带过去。这不仅是安全考量也是性能和上下文窗口的管理考量。A2A协议本身没有强制约束这一点但协作边界的设计者必须在业务层面把它定死。4. 实战落地搭一个跨Agent协作的最小示例概念说完了直接上手。我以一个最简单的客服场景举例完整跑一遍A2A协作的流程。4.1 场景设计一个客服Agent和一个库存Agent假设我们有两个Agent客服Agent负责跟用户对话理解用户意图但不具备库存查询功能库存Agent专门负责查询商品库存对外暴露A2A接口用户问客服AgentA1001的库存还有多少客服Agent发现自己不会查询库存就根据库存Agent的Agent Card把查询任务通过A2A协议委托给它。库存Agent执行完把库存数量作为Artifact返回。客服Agent拿到结果再组织语言回复用户。这个例子简单但完整覆盖了协作边界的全部要素客服Agent不关心库存Agent的内部实现库存Agent不关心客服Agent的用户上下文。两边只通过协议边界交互。现实中你把这个模式放大就是人力资源系统和财务系统、销售系统和服务系统之间的Agent协作本质上都是同一套逻辑。4.2 服务端实现Agent Card与A2A入口库存Agent的入口我用Python写一个最小实现基于JSON-RPC 2.0处理请求。首先暴露Agent Card然后处理tasks/send和tasks/get两个核心方法from flask import Flask, request, jsonify app Flask(__name__) app.route(/.well-known/agent-card) def agent_card(): card { name: inventory-agent, description: 查询商品库存数量与状态的智能体, url: https://inventory.example.com/a2a, version: 1.0.0, skills: [ { id: check-stock, name: check_stock, description: 输入SKU编号返回商品库存数量和状态, inputModes: [text], outputModes: [json] } ], security: {authentication: bearer} } return jsonify(card) app.route(/a2a, methods[POST]) def a2a_endpoint(): payload request.get_json() method payload.get(method) if method tasks/send: return handle_tasks_send(payload) if method tasks/get: return handle_tasks_get(payload) return jsonify({ jsonrpc: 2.0, id: payload.get(id), error: {code: -32601, message: Method not found} }) if __name__ __main__: app.run(host0.0.0.0, port80)这里我把Agent Card固定在标准路径客户端框架拿到根地址后就能直接发现。实际部署时前面通常会加一层网关做认证校验和限流而不是让Flask直接暴露公网。4.3 客户端实现提交任务与轮询状态客服Agent这边核心逻辑是先拿到库存Agent的Card确认有check-stock这个技能然后构造任务请求并提交最后轮询任务状态直到终态。代码大概长这样import requests import time AGENT_CARD_URL https://inventory.example.com/.well-known/agent-card def delegate_stock_query(sku: str, user_text: str): # 第一步获取并解析Agent Card card requests.get(AGENT_CARD_URL).json() if not any(s[name] check-stock for s in card[skills]): raise RuntimeError(inventory-agent 不具备 check-stock 技能) # 第二步构造并提交任务 payload { jsonrpc: 2.0, id: req-cs-001, method: tasks/send, params: { id: ftask-{int(time.time())}, message: { role: user, parts: [{text: f请查询SKU {sku} 的库存}] } } } resp requests.post(card[url], jsonpayload).json() task resp.get(result, {}) # 第三步轮询状态直到到达终态 while task[status] in (submitted, working, input-required): time.sleep(1) get_payload { jsonrpc: 2.0, id: req-cs-002, method: tasks/get, params: {id: task[id]} } task requests.post(card[url], jsonget_payload).json()[result] if task[status] completed: parts task[artifacts][0][parts] return .join(p.get(text, ) for p in parts) raise RuntimeError(f任务失败: {task.get(error)})这段代码里有几个细节值得注意。第一提交任务前先校验技能这一步看起来多余但正好体现了Agent Card的价值能力发现发生在调用之前而不是调用失败之后。第二任务ID我用时间戳生成这在生产环境不够严谨建议用UUID或者带业务前缀的幂等ID。4.4 把input-required纳入流程的实际效果如果库存Agent发现传进来的SKU格式不对它可以不回failed而是进入input-required向客服Agent追问请提供8位标准SKU编码。客服Agent可以就此回复用户拿到正确的SKU后再通过tasks/send把补充信息作为新Message推进任务。这个过程在传统API里几乎不可想象但在真实业务里价值巨大。因为用户最初的请求经常是模糊的我要查库存和请查A1001的库存之间隔着一次澄清。A2A把这种澄清显式地纳入了任务生命周期双方不用为了一个不完整请求做得罪用户的失败返回也不会因为一次请求就放弃整个任务。这也是我认为A2A最具与人协作神韵的地方。5. 常见问题与排查清单我把这段时间实际踩过的坑整理成了一张速查表希望帮你少走弯路。5.1 典型故障速查症状可能原因排查建议对方Agent长时间没有响应任务卡在working或服务端崩溃先用tasks/get查看状态再查服务端日志确认有没有配置超时机制结果一直轮询不到SSE连接被网关切断或Webhook没注册成功检查网络链路把SSE降级成轮询检查Webhook回调URL是否可达任务状态在completed与failed之间反复跳幂等处理缺失同一任务被重放检查Task ID是否稳定加去重表在日志里按task_id做全链路追踪Agent Card获取失败路径不标准或卡片字段不全确认是否按规范暴露.well-known路径用curl直接验证返回内容上下文越来越大最终超限Message没做裁剪或摘要设计上下文压缩策略长对话先摘要再作为下一轮输入只保留与任务相关的消息5.2 我踩过的三个坑第一个坑是上下文无意识膨胀。最开始我把多轮Message原封不动地攒着等到第20轮交互时上下文窗口直接撑爆。后来改成摘要加最近两轮原文的混合策略问题才缓解。这不是A2A本身的问题但只要做Agent协作就一定会遇到越早设计越省心。第二个坑是Agent Card版本不兼容。我更新了库存Agent的能力描述把check-stock的入参从单个SKU改成支持批量SKU却忘了同步更新Card的最低兼容版本。结果下游Agent拿着旧Card发来单个SKU参数我在服务端又要兼容两套逻辑维护成本直线上升。现在我会在Card里加一个compatibility字段并确保任何能力变更必走版本评审。第三个坑是重试风暴。某个下游服务延迟一高上游Agent的重试机制就疯狂触发两边的日志一会就被刷爆。后来我加了指数退避和最大重试次数并且在全链路日志里贯穿同一个任务ID排查问题一下子轻松很多。这个问题的本质还是边界不清每个Agent都从自己的视角去救火但没人定义重试的边界和上限。6. 冷静一点A2A 1.0没解决什么最后聊点泼冷水的内容。A2A 1.0确实是个好东西但工程上最忌讳的就是拿一把好锤子到处敲钉子。搞清楚它不做什么比搞清楚它做什么更重要。6.1 它不负责编排也不负责让Agent变聪明A2A不告诉你任务该怎么拆、该选哪个Agent、流程怎么编排、冲突怎么仲裁。这些属于LangGraph、CrewAI这类编排框架的范畴。A2A也不负责提升单个Agent的推理能力或生成质量那是模型层面的事情。换句话说它只定义了两个Agent之间对话的标准方言至于这场对话是由谁发起、怎么组织的协议不关心。它甚至不保证对端的Agent真的靠谱。就像签了合同也不代表对方真的能按时交付A2A只是让委托-交付-追责这件事有据可依不代表交付质量自动过关。质量评估、可信度判断、结果验证这些都还要靠业务侧自己去设计。如果你的Agent本身逻辑一团糟A2A不会救你它只会让你的混乱变得更容易传播。6.2 什么时候值得上A2A什么时候别用根据我这段时间的实践大致可以这么判断。值得上A2A的场景首先是跨组织或跨厂商协作双方各自维护自己的Agent和系统边界其次是异构技术栈之间互通一方用Python框架另一方用Java框架对双方来说统一协议比统一框架更现实最后是业务链条上存在明确的上游委托、下游执行、回传结果模式需要把接口标准化。只要命中其中两条A2A就能体现价值。不推荐用A2A的场景也不少。单系统内部的多Agent协同你自己用LangGraph或CrewAI编排性能和可控性都更好没必要再套一层协议对延迟极其敏感的场景A2A基于HTTP和JSON-RPC设计目标是通用性而非低延迟真到毫秒级还是要走专用链路还有就是连能力边界和信任边界都没想清楚的系统协议只是把边界明确化不能凭空生成边界你内部一团乱麻指望A2A救你大概率救不了。我个人这段实践下来最大的体会是A2A 1.0并没有让Agent互相调用这件事从0变成1框架们早就做到了。它做的是把随便怎么调变成了按规矩调。Agent Card让能力有据可查Task状态机让责任有主可依Message和Artifact让交流与交付泾渭分明这些朴素的约定合在一起就是一套可以被机器理解和执行的边界契约。写一张Agent Card的过程其实就是逼自己想清楚三个问题我的Agent到底能做什么、需要对方提供什么、出了问题谁来兜底。这三个问题一旦想明白你用不用A2A都已经赢了大半。