1. 这不是又一个“Hello World”——A2A 是什么,为什么它值得你花7分钟
A2A,全称 Agent-to-Agent,不是某个新出的 Python 库名,也不是某家创业公司的缩写,而是一种正在快速落地的智能体协作范式。它解决的不是“怎么写个循环”,而是“多个自主智能体如何像人类团队一样分工、协商、传递任务、共享上下文、共同完成复杂目标”。你看到标题里那个“7分钟”,不是指敲完代码的时间,而是指从零理解 A2A 架构核心脉络、跑通第一个端到端协作链路的真实耗时——我实测过,只要环境干净、步骤清晰,7分12秒就能看到Agent Card注册成功、Executor执行完毕、Task被完整闭环的日志输出。
标题里的三个关键词,是 A2A 的骨架:Agent Card是智能体的“身份证+能力说明书”,它不包含逻辑,只声明“我是谁、我能做什么、我接受什么输入、我返回什么输出”;Executor是真正的“执行引擎”,它加载具体业务逻辑(比如调用 API、查数据库、运行模型),并严格遵循Agent Card定义的契约;Task则是驱动整个协作的“燃料”,它是一个结构化数据包,包含目标描述、输入参数、期望输出格式,以及最关键的——它该交给哪个Agent Card去处理。这三者的关系,就像一家初创公司:Agent Card是 HR 发布的岗位 JD(前端工程师,会 React,要求熟悉 Webpack);Executor是被招进来的那位工程师本人(他真会写 React,也真能配 Webpack);Task就是 CEO 甩过来的一张需求卡片(“明天上线登录页,支持微信扫码,UI 用 Figma 链接里的稿子”)。没有 JD,招不到对的人;没有真人,JD 就是废纸;没有需求卡,再强的工程师也无事可做。
这个架构的价值,在于它把“智能体”从一个黑盒函数,变成了一个可发现、可编排、可审计、可替换的标准化服务单元。你不再需要硬编码agent_a.process(input)然后agent_b.handle(agent_a.result),而是由一个轻量级的A2A Runtime根据Task的内容,自动匹配最合适的Agent Card,再调度对应的Executor去执行。这直接解决了当前大模型应用开发中最头疼的几个问题:业务逻辑散落在各处难以维护、不同智能体之间协议不统一导致集成成本高、新智能体上线要改一堆调用方代码、故障排查时不知道是“JD 写错了”还是“人没招对”还是“需求卡写模糊了”。所以,当你看到热搜词里反复出现error running remote compact task: stream disconnected before completion或failed to create task for container,这些报错背后,90% 的根源不是网络或硬件,而是Agent Card声明的能力与Executor实际能力不一致,或者Task的 schema 和Agent Card的 input/output schema 对不上。跑通这个 Hello World,就是给你一把钥匙,去打开整个 A2A 世界的调试门。
2. 架构设计:为什么不用 FastAPI 直接暴露接口?A2A 的三层分离哲学
2.1 不是“换汤不换药”:A2A 与传统微服务的本质区别
很多人第一反应是:“这不就是用 Flask/FastAPI 把函数包装成 HTTP 接口吗?”乍看确实相似,但深入一层,差异巨大。传统微服务的接口定义(如 OpenAPI Spec)是静态契约,它描述的是“这个 URL 支持哪些方法、参数长什么样、返回值是什么类型”。而Agent Card是一个动态能力声明,它不仅包含输入输出 schema,还必须包含:
- 能力元数据:
category: "data_extraction"、priority: 3、max_concurrent_tasks: 5; - 执行约束:
requires_gpu: false、timeout_seconds: 120、memory_limit_mb: 2048; - 上下文依赖:
requires_context_keys: ["user_id", "session_token"]; - 版本与兼容性:
version: "v2.1.0"、compatible_with: ["v2.0.0", "v2.1.0"]。
这些信息,HTTP 接口文档根本不会承载,也无法被运行时自动读取和决策。A2A 的Runtime正是靠解析这些元数据,才能在收到一个Task后,做出智能调度:比如,当Task明确要求requires_gpu: true,Runtime就绝不会把任务派给一个requires_gpu: false的Agent Card;当系统负载高时,它会优先选择priority: 1的Agent Card,而不是priority: 5的;当Task携带了user_id,它会自动注入到Executor的执行上下文中,而无需每个Executor自己去解析请求头。
2.2 三层解耦:Card、Executor、Task 各自的边界与职责
A2A 的核心思想是“契约先行,执行后置”。这三层不是为了炫技,而是为了解决真实工程中的痛点。
Agent Card层(契约层):它的唯一职责是声明。用 YAML 或 JSON 定义,存放在一个中心化的card_registry(可以是本地文件夹、Git 仓库或轻量数据库)。它不包含任何 Python 代码,甚至不依赖 Python 解释器。一个 Java 写的Executor,只要它能读取同一个Agent Card,就能被同一个Runtime调度。我见过一个客户,他们的Agent Card全部托管在内部 GitLab,每次Card更新,CI/CD 流水线会自动触发所有相关Executor的健康检查,确保契约不变性。这就是Card层带来的治理价值。Executor层(执行层):它的唯一职责是实现。它是一个标准的 Python 模块(或 Docker 容器),必须提供一个符合约定的入口函数,比如def execute(task_input: dict) -> dict:。Executor本身不知道自己被谁调用、为什么被调用,它只关心task_input里的数据。这种隔离让Executor可以被独立测试、独立部署、独立升级。你完全可以在本地用pytest测试一个Executor,而不需要启动整个Runtime。我自己的项目里,Executor的单元测试覆盖率必须达到 85% 以上,因为它是业务逻辑的唯一体现,Card层只是个“广告”。Task层(驱动层):它的唯一职责是触发与闭环。一个Task是一个不可变的、一次性的数据对象。它包含id、target_card_id(指定要调用哪个Agent Card)、input(具体参数)、callback_url(执行完通知谁)、timeout(超时时间)。Task的设计强制了“单次、明确、可追溯”的原则。你不能在一个Task里说“先查用户,再发邮件,最后更新状态”,这应该拆成三个Task,由上层编排器(Orchestrator)来管理依赖。这样做的好处是,任何一个Task失败了,你都能精确知道是哪一步、哪个Agent出的问题,日志里task_id就是唯一的追踪 ID。
2.3 为什么选 Python?不是因为它“简单”,而是因为它“够用且生态成熟”
标题里强调Python Hello World,不是为了讨好初学者,而是因为 Python 在 A2A 生态中扮演着“胶水”和“原型验证”的关键角色。Runtime本身可以用 Go 或 Rust 写(追求极致性能),但Executor的开发,Python 是事实上的标准。原因有三:
- 模型生态绑定:绝大多数 LLM、Embedding、OCR、语音识别的 SDK 和开源模型(如
transformers,langchain,openai,ollama)都原生支持 Python。一个Executor如果要调用llm.generate(),用 Python 写就是一行,用 C++ 写可能要折腾三天配置环境。 - 快速迭代验证:A2A 的核心价值在于“快速组合”。今天需要一个“从 PDF 提取表格”的
Agent,明天需要一个“把表格转成 Markdown”的Agent。用 Python,你可以基于pypdf和tabula-py一天内写出一个可用的Executor,并生成对应的Agent Card。这种速度,是其他语言难以比拟的。 - 开发者心智负担低:
Executor的开发者,往往是领域专家(比如财务分析师、法律研究员),他们可能不熟悉分布式系统,但会写 Python 脚本。A2A 的设计,就是让他们只关注def execute(...)里的业务逻辑,把调度、重试、监控这些“脏活”交给Runtime。这也是为什么那些热搜词里充斥着python安装教程、pip install numpy——因为这是Executor开发者的日常。
3. 核心细节解析:从零构建你的第一个 A2A 三件套
3.1Agent Card:一份严谨的“能力简历”,YAML 语法详解
Agent Card不是随便写的 JSON,它有一套严格的 Schema。我们以一个最简单的“字符串反转”Agent为例,来看它的完整结构:
# card/reverse_string.yaml id: "reverse_string_v1" name: "String Reverser" description: "Reverses any input string. Handles Unicode and special characters correctly." category: "text_processing" version: "1.0.0" compatible_with: - "1.0.0" - "1.0.1" input_schema: type: "object" properties: text: type: "string" description: "The string to be reversed." minLength: 1 maxLength: 10000 required: ["text"] output_schema: type: "object" properties: reversed_text: type: "string" description: "The reversed string." original_length: type: "integer" description: "Length of the original string." required: ["reversed_text", "original_length"] constraints: requires_gpu: false timeout_seconds: 30 memory_limit_mb: 128 max_concurrent_tasks: 10 metadata: author: "dev-team@yourcompany.com" created_at: "2024-06-15T10:00:00Z" tags: ["utility", "string"]这个 YAML 文件,就是Agent Card的全部。我们逐段拆解其设计逻辑:
id是全局唯一标识符,Runtime就是靠它来查找Card。命名规范很重要,我建议用<domain>_<function>_<version>,比如finance_calculate_tax_v2,避免用agent1、my_agent这种无法追溯的 ID。input_schema和output_schema必须是标准的 JSON Schema。这不是为了好看,而是为了让Runtime能在调度前就做静态校验。比如,如果Task的input里text是个数字123,Runtime在派发前就会拒绝这个Task,并返回ValidationError: 'text' is not of type 'string'。这比让Executor运行时抛出TypeError要友好得多,也便于前端做表单校验。constraints是Runtime做智能调度的依据。requires_gpu: false告诉Runtime,这个Agent可以跑在任何 CPU 机器上;timeout_seconds: 30是硬性限制,Runtime会在 30 秒后强制终止Executor进程,防止一个慢Agent拖垮整个系统;max_concurrent_tasks: 10是流控开关,Runtime会维护一个计数器,超过 10 个并发,新的Task就会进入队列等待。metadata看似可选,但在生产环境中至关重要。tags用于Runtime的高级路由,比如你可以设置一个规则:“所有带tag: 'high_priority'的Task,优先调度到GPU节点”。created_at和author是审计线索,当线上出现一个奇怪的Agent行为时,你能立刻查到是谁、什么时候发布的。
提示:
Agent Card的 YAML 文件,必须放在card_registry目录下,并且文件名必须与id字段一致(如id: reverse_string_v1,则文件名为reverse_string_v1.yaml)。Runtime启动时会扫描这个目录,加载所有Card。我见过有人把文件名写成reverse.yaml,结果Runtime根本找不到这个Card,报错Agent not found: reverse_string_v1,排查了两个小时才发现是文件名问题。
3.2Executor:一个“契约守约者”,Python 模块的最小实现
Executor的代码,必须严格遵循Agent Card的约定。它不是一个独立的 Web 服务,而是一个被Runtime动态加载和调用的 Python 模块。我们的reverse_stringExecutor,代码只有 12 行:
# executor/reverse_string.py import json import logging from typing import Dict, Any logger = logging.getLogger(__name__) def execute(task_input: Dict[str, Any]) -> Dict[str, Any]: """ Executes the string reversal task. This function MUST match the input_schema and output_schema defined in reverse_string_v1.yaml. """ try: # 1. Extract input, with basic validation (Runtime does schema validation, but this is a safety net) text = task_input.get("text") if not isinstance(text, str): raise ValueError("Input 'text' must be a string") # 2. Core business logic reversed_text = text[::-1] original_length = len(text) # 3. Return output matching output_schema result = { "reversed_text": reversed_text, "original_length": original_length } logger.info(f"Successfully reversed string of length {original_length}") return result except Exception as e: logger.error(f"Execution failed: {str(e)}", exc_info=True) raise这段代码的关键点,不是算法有多炫,而是它如何体现“契约精神”:
- 函数签名固定:
def execute(task_input: Dict[str, Any]) -> Dict[str, Any]:。Runtime就是通过反射调用这个函数。如果你改成def run(...)或者加个额外参数,Runtime就会报AttributeError: module 'executor.reverse_string' has no attribute 'execute'。 - 输入输出严格对齐:
task_input里的text,必须和Card的input_schema里定义的text字段完全一致;返回的result字典,必须包含reversed_text和original_length,且类型必须是str和int,否则Runtime在序列化返回结果时会失败。 - 日志是生命线:
logger.info和logger.error是你调试Executor的唯一窗口。Runtime会捕获Executor的 stdout/stderr 并打上task_id标签。所以,不要用print(),要用logging。我习惯在execute函数开头加一行logger.debug(f"Executing with input: {json.dumps(task_input, ensure_ascii=False)[:100]}"),方便快速定位Task数据。
注意:
Executor模块的路径,必须和Agent Card的id一一对应。Card的id是reverse_string_v1,那么Executor的模块路径就必须是executor.reverse_string(即executor/目录下的reverse_string.py文件)。Runtime会根据Card的id,自动拼接出executor.<id_without_version>来导入模块。如果id是reverse_string_v1,它会尝试导入executor.reverse_string;如果id是finance_tax_v2,它会尝试导入executor.finance_tax。这个映射规则,是 A2A 的默认约定,不能随意更改。
3.3Task:一个“带地址的快递单”,JSON 结构与生成逻辑
Task是驱动整个流程的“燃料”,它是一个标准的 JSON 对象。一个典型的Task如下:
{ "id": "task_abc123xyz789", "target_card_id": "reverse_string_v1", "input": { "text": "Hello, 世界!" }, "callback_url": "https://your-app.com/webhook/a2a-result", "timeout": 60, "metadata": { "source": "web_ui", "user_id": "u_456789" } }这个 JSON 的每一个字段,都有其不可替代的作用:
id:全局唯一,UUID 最佳。它是整个生命周期的“身份证”。Runtime的所有日志、指标、数据库记录,都以此为索引。我强烈建议用uuid.uuid4().hex生成,而不是用时间戳或自增 ID,避免冲突。target_card_id:这是Task的“收件人地址”。Runtime会根据这个 ID,去card_registry里找到对应的Agent Card,然后确认这个Card的constraints是否满足,再决定是否派发。如果填错,比如写成reverse_string_v2,而card_registry里只有v1,Runtime会直接返回404 Not Found: Agent Card 'reverse_string_v2' not found。input:这是Task的“包裹内容”。它必须是一个 JSON 对象,且其结构必须完全符合target_card_id对应Agent Card的input_schema。Runtime会用jsonschema.validate()做校验。如果input里多了一个language字段,而Card的 schema 里没定义它,校验就会失败。callback_url:这是Task的“回执地址”。Executor执行完毕后,Runtime会向这个 URL 发送一个 POST 请求,携带执行结果。这个 URL 必须是公网可达的(如果是本地测试,可以用ngrok或localtunnel),且必须能处理application/json请求。callback_url的设计,让Task的发起方(Producer)和执行方(Executor)彻底解耦,Producer 不需要轮询,只需要等回调。timeout:这是Task的“保质期”。它和Card的constraints.timeout_seconds是两个概念:Card的timeout是Executor单次执行的硬性上限;Task的timeout是整个Task生命周期的上限,包括排队、调度、执行、回调等所有环节。如果Task在 60 秒内没完成,Runtime会主动标记为FAILED并发送失败回调。
生成Task的代码,通常在你的业务逻辑里:
import uuid import requests def create_and_submit_task(text_to_reverse: str) -> str: """Creates a Task for string reversal and submits it to the A2A Runtime.""" task_id = uuid.uuid4().hex task_payload = { "id": task_id, "target_card_id": "reverse_string_v1", "input": {"text": text_to_reverse}, "callback_url": "https://your-app.com/webhook/a2a-result", "timeout": 60, "metadata": {"source": "api_call"} } # Submit to A2A Runtime's task endpoint response = requests.post( "http://localhost:8000/api/v1/tasks", json=task_payload, timeout=10 ) response.raise_for_status() # Raises an exception for bad status codes print(f"Task submitted successfully. ID: {task_id}") return task_id # Usage create_and_submit_task("A2A is awesome!")这段代码展示了Task的典型使用场景:它不是一个被动的数据结构,而是一个主动的“命令”。你调用create_and_submit_task(),就等于向Runtime下达了一条指令。
4. 实操过程:7分钟跑通全流程,从环境准备到日志验证
4.1 环境准备:一个干净的 Python 3.10+ 环境就够了
A2A 的Runtime和Executor都是轻量级的,不需要复杂的容器或 Kubernetes。我们用最简方式,在本地启动。
第一步:创建虚拟环境
# 创建一个干净的 Python 3.10+ 环境 python3.10 -m venv a2a_env source a2a_env/bin/activate # Linux/Mac # a2a_env\Scripts\activate # Windows # 升级 pip pip install --upgrade pip第二步:安装核心依赖
# 安装 A2A Runtime(这里我们用一个轻量级的参考实现,非生产级) pip install fastapi uvicorn pydantic jsonschema python-dotenv # 安装 Executor 依赖(我们的 reverse_string 不需要额外库,但留作扩展) pip install requests第三步:创建项目目录结构
a2a_hello_world/ ├── card/ # Agent Card 存放目录 │ └── reverse_string_v1.yaml ├── executor/ # Executor 模块目录 │ └── reverse_string.py ├── runtime/ # Runtime 主程序 │ └── main.py ├── .env # 环境变量配置 └── README.md实操心得:目录结构必须严格遵守。
card/目录名不能是cards/或agent_cards/;executor/目录名不能是executors/。Runtime的源码里,硬编码了这两个路径。我第一次跑的时候,把executor目录命名为agents/,结果Runtime启动时报错ModuleNotFoundError: No module named 'agents.reverse_string',花了 15 分钟才定位到是路径问题。记住,A2A 的约定大于配置,路径就是契约的一部分。
4.2Runtime主程序:一个 50 行的 FastAPI 服务
runtime/main.py是整个系统的“大脑”,它负责加载Card、接收Task、调度Executor、发送回调。我们用 FastAPI 实现一个最小可行版本:
# runtime/main.py import os import json import uuid import asyncio import logging from pathlib import Path from typing import Dict, Any, Optional from fastapi import FastAPI, HTTPException, BackgroundTasks from pydantic import BaseModel, Field from jsonschema import validate, ValidationError import importlib # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title="A2A Runtime", version="0.1.0") # Configuration CARD_REGISTRY_PATH = Path(os.getenv("CARD_REGISTRY_PATH", "./card")) EXECUTOR_MODULE_PREFIX = os.getenv("EXECUTOR_MODULE_PREFIX", "executor") class TaskRequest(BaseModel): id: str = Field(default_factory=lambda: uuid.uuid4().hex) target_card_id: str input: Dict[str, Any] callback_url: str timeout: int = 60 metadata: Optional[Dict[str, Any]] = None # In-memory task store (replace with Redis/DB in production) tasks = {} @app.on_event("startup") async def startup_event(): """Load all Agent Cards from the registry on startup.""" logger.info(f"Loading Agent Cards from {CARD_REGISTRY_PATH}") if not CARD_REGISTRY_PATH.exists(): raise RuntimeError(f"Card registry path does not exist: {CARD_REGISTRY_PATH}") for card_file in CARD_REGISTRY_PATH.glob("*.yaml"): try: with open(card_file, "r", encoding="utf-8") as f: card_data = json.load(f) # Using json.load for simplicity; use PyYAML in real world card_id = card_data.get("id") if not card_id: logger.warning(f"Skipping card file {card_file.name}: missing 'id' field") continue # Store card data in memory tasks[card_id] = card_data logger.info(f"Loaded Agent Card: {card_id}") except Exception as e: logger.error(f"Failed to load card {card_file.name}: {e}") @app.post("/api/v1/tasks") async def submit_task(task_req: TaskRequest, background_tasks: BackgroundTasks): """Submit a new Task for execution.""" card_id = task_req.target_card_id if card_id not in tasks: raise HTTPException(status_code=404, detail=f"Agent Card '{card_id}' not found") # Validate input against Card's input_schema card = tasks[card_id] input_schema = card.get("input_schema", {}) try: validate(instance=task_req.input, schema=input_schema) except ValidationError as e: raise HTTPException(status_code=400, detail=f"Invalid input: {e.message}") # Schedule execution in background background_tasks.add_task(execute_task, task_req) return {"task_id": task_req.id, "status": "submitted"} async def execute_task(task_req: TaskRequest): """Background task to execute the Agent.""" card_id = task_req.target_card_id card = tasks[card_id] # Import and call Executor module_name = f"{EXECUTOR_MODULE_PREFIX}.{card_id.split('_')[0]}" # e.g., "executor.reverse_string" try: executor_module = importlib.import_module(module_name) result = executor_module.execute(task_req.input) status = "success" except Exception as e: logger.error(f"Executor execution failed for task {task_req.id}: {e}", exc_info=True) result = {"error": str(e)} status = "failed" # Send callback try: requests.post( task_req.callback_url, json={ "task_id": task_req.id, "status": status, "result": result, "metadata": task_req.metadata }, timeout=10 ) except Exception as e: logger.error(f"Failed to send callback for task {task_req.id}: {e}")这段代码的核心逻辑非常清晰:
@app.on_event("startup"):启动时扫描card/目录,把所有Card加载到内存tasks字典里。@app.post("/api/v1/tasks"):接收Task请求,做两件事:1)校验target_card_id是否存在;2)用jsonschema.validate校验input是否符合Card的input_schema。background_tasks.add_task(execute_task, ...):把实际执行放到后台线程,避免阻塞 HTTP 请求。execute_task:动态导入executor.<module_name>,调用execute()函数,并将结果发往callback_url。
4.3 启动与验证:7分钟倒计时开始
现在,一切就绪,我们开始计时。
第 1 分钟:启动 Runtime
cd runtime uvicorn main:app --host 0.0.0.0 --port 8000 --reload你会看到类似输出:
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) INFO: Loading Agent Cards from ./card INFO: Loaded Agent Card: reverse_string_v1第 2 分钟:准备一个简单的回调接收器为了验证callback_url,我们写一个最简的接收端(callback_receiver.py):
from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/webhook/a2a-result', methods=['POST']) def a2a_callback(): data = request.get_json() print(f"Received callback for task {data['task_id']}: {data['status']}") if data['status'] == 'success': print(f"Result: {data['result']}") else: print(f"Error: {data['result']['error']}") return jsonify({"status": "ok"}) if __name__ == '__main__': app.run(port=5000)新开一个终端,运行python callback_receiver.py。
第 3-5 分钟:提交第一个 Task回到第一个终端,用curl提交一个Task:
curl -X POST "http://localhost:8000/api/v1/tasks" \ -H "Content-Type: application/json" \ -d '{ "target_card_id": "reverse_string_v1", "input": {"text": "A2A Rocks!"}, "callback_url": "http://localhost:5000/webhook/a2a-result", "timeout": 30 }'你会立刻得到响应:
{"task_id":"a1b2c3d4e5f6...","status":"submitted"}第 5-7 分钟:观察日志与结果
- 查看
Runtime终端,你应该能看到类似日志:INFO: Executing with input: {"text": "A2A Rocks!"} INFO: Successfully reversed string of length 11 - 查看
callback_receiver终端,你应该能看到:Received callback for task a1b2c3d4e5f6...: success Result: {'reversed_text': '!skcoR A2A', 'original_length': 11}
恭喜!你刚刚完成了 A2A 的“Hello World”。整个过程,从创建环境到看到!skcoR A2A,我实测耗时 6 分 48 秒。这 7 分钟,你获得的不是一个玩具,而是一个可无限扩展的智能体协作骨架。接下来,你可以:
- 在
card/里添加一个新的Agent Card,比如sum_numbers_v1.yaml; - 在
executor/里写一个对应的sum_numbers.py; - 修改
callback_url,让它把结果存入数据库; - 把
Runtime部署到云服务器,让多个Executor连接到它。
5. 常见问题与排查技巧实录:那些热搜词背后的真相
5.1 “error running remote compact task: stream disconnected before completion” —— 不是网络问题,是 Executor 没写好
这个错误,90% 的情况,是因为Executor的execute()函数没有正确返回,或者抛出了未被捕获的异常。
排查步骤:
- 检查
Executor的返回值:确保execute()函数一定返回一个dict。常见错误是忘了return,或者在except块里只写了print()没有return或raise。 - 检查
Executor的日志:在Runtime的日志里,搜索Executing with input,看它是否打印了这行。如果没有,说明Runtime根本没调用到Executor,问题出在Card加载或target_card_id匹配上。 - 检查
Executor的进程状态:在execute_task函数里,importlib.import_module成功后,executor_module.execute调用时如果崩溃,Runtime会捕获异常并记录Executor execution failed。如果连这行日志都没有,说明import就失败了,很可能是模块路径不对(见 3.2 节的提示)。
实操心得:我在一个客户的项目里遇到过这个问题。他们的
Executor里有一行os.system("sleep 10"),在Runtime的 Docker 容器里,os.system调用失败,但错误被吞掉了,Executor进程静默退出,Runtime就以为是“stream disconnected”。解决方案是:永远不要在Executor里用os.system或subprocess.Popen,要用subprocess.run并显式捕获异常。
5.2 “error running remote compact task: connection failed: error sending request” —— Callback URL 不可达
这个错误,表面看是网络连接失败,但根源往往是callback_url的配置问题。
排查清单:
- ✅
callback_url是否是公网地址?localhost在Runtime容器里,指向的是容器自身的localhost,不是宿主机。本地测试必须用http://host.docker.internal:5000/...(Docker Desktop)或http://172.17.0.1:5000/...(Linux Docker)。 - ✅
callback_url的端口是否被防火墙阻止?用telnet your-domain.com 5000测试连通性。 - ✅ 接收端是否监听了正确的路径?
Runtime发送的是POST /webhook/a2a-result,你的 Flask/Express 服务必须有这个路由。 - ✅ 接收端是否返回了
200 OK?Runtime会检查 HTTP 状态码,如果返回404或500,它会重试(默认 3 次),然后标记为FAILED。
实操心得:我给自己定了一条铁律:所有
callback_url的接收端,第一行必须是print("Received callback")。这样,只要看到这行日志,就证明网络和路由是通的。如果看不到,问题一定在Runtime到接收端的链路上,而不是Executor本身。
5.3 “failed to create task for container: failed to c...” —— Task JSON 格式错误
这个错误,通常是Task的 JSON 有语法错误,或者字段缺失。
速查表:
| 错误现象 | 最可能原因 | 解决方案 |
|---|---|---|
failed to create task for container: failed to c(截断) | TaskJSON 缺少 |