AWS Machine Learning Blog

Retrofit, don’t rebuild: Agentic overlays for transforming legacy enterprise services

8.5内容质量
Retrofit, don’t rebuild: Agentic overlays for transforming legacy enterprise services

TL;DR · AI 摘要

AWS 提出 Agentic Overlays 技术,通过封装现有 REST 服务,使其兼容 A2A 协议,无需重构业务逻辑。

核心要点

  • Agentic Overlays 可将 REST 服务封装为 A2A 兼容的代理,无需重写业务逻辑。
  • A2A 协议支持自主代理间的协作与任务协调,适用于复杂企业系统。
  • AWS 提供参考架构和示例代码,便于企业快速实现 Agentic Overlays。

结构提纲

按章节快速跳转。

  1. 文章提出 Agentic Overlays 技术,用于将现有 REST 服务转换为 A2A 兼容的代理。

  2. REST 适用于确定性客户端-服务器集成,而 A2A 适用于自主代理间的协作与任务协调。

  3. REST 与 A2A 基于正交范式,使企业难以将现有服务迁移到标准化代理通信中。

  4. Agentic Overlays 是一种封装层,使 REST 服务兼容 A2A 协议,无需重写业务逻辑。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Agentic Overlays
    • 目标
      • 兼容 A2A 协议
      • 无需重写业务逻辑
    • 技术背景
      • REST 与 A2A 的差异
      • 企业系统迁移挑战

金句 / Highlights

值得收藏与分享的关键句。

#A2A#REST#AWS#微服务#AI
打开原文

无需重建,只需改造:通过智能代理层实现对传统企业服务的升级 | 人工智能

无需重建,只需改造:通过智能代理层实现对传统企业服务的升级

本文中所表达的观点是作者的观点,而非思科的观点。

企业架构长期以来一直以 REST API 和微服务为中心。这些系统稳定、经过充分测试,并且深度嵌入生产环境。它们并不是为代理与代理(Agent-to-Agent,A2A)通信而设计的,而这是自主代理之间通过结构化消息进行协作、推理和协调的新兴标准。在缺乏通用代理协议的早期阶段,这种设计方式是可行的,但这也意味着许多现有代理现在处于新兴的 A2A 框架之外。如今的挑战不再仅仅是将 A2A 添加到传统服务中。你还需要将这些基于 REST 的代理引入标准化的代理之间协作世界。

AWS 与作者之间的这项技术合作中,我们提出了一种务实的解决方案:智能代理层(agentic overlays)。智能代理层是一种轻量级的封装层,它可以将传统的基于 REST 的服务转化为能够参与 A2A 交互的代理。它们还可以将 REST API 作为与模型上下文协议(Model Context Protocol,MCP)兼容的工具进行暴露。通过这些方式,企业可以在不重写业务逻辑、不复制代码、也不运行并行基础设施的情况下,将 A2A 功能添加到现有的 REST 服务中。这通过将现有服务作为代理进行复用,减少了基础设施中的代理蔓延问题。我们提供了参考架构和示例代码,展示了如何构建智能代理层。

背景:REST 与 A2A 的对比

REST API 是为确定性的客户端-服务器集成而设计的。客户端调用一个明确定义的端点,传递参数,并接收一个可预测的响应,通常是在由 HTTP 语义控制的无状态请求-响应流程中。这使得 REST 非常适合通过明确的合同、强大的兼容性和操作简便性来暴露业务功能(如创建、读取、更新和删除)。

A2A 是为自主代理之间的互操作性而设计的。代理通过元数据(如代理卡)发现彼此,协商能力,并通过结构化消息(通常通过 JSON-RPC)交换信息,以协调多步骤任务。与 REST 优化稳定的服务接口和直接执行不同,A2A 优化了基于推理的协调、面向任务的消息传递和代理协作。其结果是系统可以跨多个服务进行规划、委派和组合操作,而不是调用孤立的端点。

在 A2A 标准化之前,企业通常将代理部署为基于 REST 或专有服务。他们将这些代理视为常规 API,其中特定于代理的逻辑嵌入在请求-响应端点中。因此,今天许多现有的代理并不是原生支持 A2A 的,这带来了新的迁移挑战:在不重写其核心逻辑的情况下,使这些代理能够使用标准化的 A2A 协议进行互操作。

图 1:基于 REST 的应用程序

上图展示了一个基于 REST 的应用程序。REST API 堆栈可能是一个单体,也可能是一组分布式端点。REST API 控制器不需要是一个显式的代理,将请求委托给应用程序外部。它可以是框架本身的一部分。例如,在 Flask 应用程序中,框架默认提供了控制器抽象,这通常在使用 @app.route() 或 Flask 的 RESTful 扩展时可以看到。这里的思路是通过一组端点来捕获 REST 堆栈。

解决方案方法

在本节中,我们将讨论您可以采用的不同方法,以向遗留的企业系统添加代理能力。我们将它们与使用代理覆盖的方法进行比较。

维护独立的 REST 和 A2A 堆栈:一种方法是开发和维护两种并行的方式来暴露相同的功能。这可能意味着:

  • 两组端点:/api/v2/... 和 /a2a/...。
  • 两种认证、验证和错误映射的实现(除非谨慎地重用)。
  • 两个部署流水线(构建、测试、发布、回滚)。
  • 双重可观测性工作:分别为两个路径生成日志、指标和追踪。
  • 不一致性风险更高。A2A 对于由 REST 执行的相同操作可能会返回不同的输出。
  • 随着时间推移,成本和操作复杂性更高。

图 2:独立的 REST 和 A2A 堆栈

分离的堆栈,但共享业务逻辑:重构现有端点意味着您更改当前 REST API 的代码结构(有时是行为),以便可以被新的接口(如 A2A)重用。而不是保持 REST 端点原样,您通常通过将业务逻辑提取到共享服务中,并更新控制器和处理程序以调用这些服务,来重新组织它们。即使外部 REST 路径保持不变,重构可能会引入回归、行为漂移和大量的测试负担。

图 3:分离的堆栈,共享业务逻辑

核心思想:代理覆盖层

代理覆盖层是一个轻量级的包装层,使基于 REST 的服务能够参与 A2A 通信。覆盖层:

  • 将代理消息转换为 REST 负载,反之亦然。
  • 将 REST 端点暴露为代理任务或工具。

最重要的是,A2A 不是一个新的 API。它是对现有 API 的新接口。底层的 REST 服务保持不变。

在应用程序内部添加代理覆盖层

在此方法中,您有两个端点集合:/api/v2/... 和 /a2a/...(REST 与 A2A),如以下图表所示,但构建、测试、发布和回滚的部署流水线只有一个。使用这种模式,传统的 REST API 端点可以转换为代理端点,而无需重写核心业务逻辑。服务的部署过程不会改变。对于相同的主机和端口,您添加新的路由,尽管系统可能需要扩展以处理增加的流量。

你可以直接使用代理技能进行路由。MCP 服务器可以用于调用外部服务,但代理技能可以在代理范围内直接路由请求,而无需将 API 导入到 MCP 服务器作为技能。无论你拥有哪些端点,都可以将其作为代理技能暴露出来,而无需单独的 MCP 服务器。

图 4:应用程序中的代理叠加层

这种方法通过将现有服务重新用作代理,减少了基础设施中的代理扩散。这种设计模式非常适合需要有限功能范围的监督代理,这些代理需要同时具备基于 REST 和代理的能力,例如意图分类和路由。

代理叠加层示例实现

作为概念验证,本节展示了如何将一个使用 Flask 的示例遗留 REST 计算器服务通过叠加层移植到代理系统中。对于叠加层,我们添加了标准的 A2A 组件(或路由),例如知名的代理卡片、代理消息端点、能力、技能和健康状态。我们还引入了一种消息转换设计模式,该模式将代理消息转换为 REST API 消息,然后从代理发出 REST 调用。A2A 消息转换工作流程如下:

  • 接收 JSON-RPC 2.0 请求。
  • 将 A2A 任务映射到 REST 端点。
  • 转发身份验证头。
  • 内部调用 REST 端点。
  • 将 REST 响应转换为 JSON-RPC 格式。

第 0 步:请求-响应格式

本节比较了 REST 和 A2A 协议的请求-响应格式,使用以下各节中演示的计算器示例。

REST 与 A2A 输入请求:

REST

A2A

{ “operation”: “add”, “operands”: [5, 3] }

{ “jsonrpc”: “2.0”, “method”: “SendMessage”, “params”: { “message”: { “role”: “user”, “parts”: [ { “kind”: “data”, “data”: { “operation”: “add”, “operands”: [5, 3] } } ] } }, “id”: 1 }

REST 与 A2A 输出响应:

{“result”: 8}

{ “jsonrpc”: “2.0”, “result”: { “messageId”: “uuid”, “contextId”: “uuid”, “role”: “agent”, “parts”: [{“kind”: “data”, “data”: {“result”: 8}}], “kind”: “message”, “metadata”: {} }, “id”: 1 }

第 1 步:设置代理

在此步骤中,你将创建一个带有知名代理卡片和加载的代理技能的计算器代理示例。build_agent_card 函数动态构建代理卡片。

code
"""
A2A 请求转换器 - 计算器示例。

此模块实现请求转换器模式,以在现有的计算器 REST API 上提供 A2A
(JSON-RPC 2.0) 兼容性。

A2A 规范 0.3 兼容性:
- 代理卡片:GET /.well-known/agent-card.json
- JSON-RPC 端点:POST /a2a
- 方法:SendMessage, SendStreamingMessage
- 消息格式:{ "message": { "parts": [{ "kind": "data", "data": {...} }] } }
"""

# 默认 A2A API URL(如需覆盖,请使用 build_agent_card(url))
A2A_API_URL = "http://localhost:5000/a2a"

EXECUTE_TIMEOUT_SECONDS = 30

# 从 JSON 文件加载技能
_SKILLS_FILE = Path(__file__).parent / "skills.json"
_SKILLS_CACHE: Optional[List[Dict[str, Any]]] = None
python
def _load_skills() -> List[Dict[str, Any]]:
    """
    从 skills.json 文件加载技能。
    首次加载后,技能会被缓存以避免重复读取文件。
    """
    global _SKILLS_CACHE
    if _SKILLS_CACHE is not None:
        return _SKILLS_CACHE
    try:
        with open(_SKILLS_FILE) as f:
            _SKILLS_CACHE = json.load(f)
            return _SKILLS_CACHE
    except FileNotFoundError:
        logger.error(f"未找到技能文件: {_SKILLS_FILE}")
        return []
    except json.JSONDecodeError as e:
        logger.error(f"技能文件中的 JSON 无效: {e}")
        return []

def build_agent_card(api_url: Optional[str] = None) -> Dict[str, Any]:
    """构建 A2A Agent Card(v0.3.0 格式),并支持可配置的 API URL。"""
    if api_url is None:
        api_url = A2A_API_URL
    return {
        "name": "Calculator Agent",
        "description": "支持基本算术运算的简单计算器",
        "supportedInterfaces": [
            {"url": api_url, "protocolBinding": "JSONRPC", "protocolVersion": "0.3"},
        ],
        "provider": {
            "organization": "Example Organization",
            "url": "",
        },
        "version": "1.0.0",
        "capabilities": {
            "streaming": False,
            "pushNotifications": False,
            "extendedAgentCard": False,
        },
        "defaultInputModes": ["text/plain", "application/json"],
        "defaultOutputModes": ["text/plain", "application/json"],
        "skills": _load_skills(),
    }

# 动态构建 Agent Card
AGENT_CARD = build_agent_card()

第 2 步:实现内部 REST 调用器

code
def invoke_rest_endpoint(
    endpoint: str,
    json_data: Optional[Dict] = None,
    http_method: str = "POST"
) -> Tuple[Optional[Dict], int]:
    """
    通过真实的 HTTP 请求调用内部 REST 端点。

    使用 requests.post/get 调用正在运行的服务器。这确保了
    中间件、装饰器和标头都能被正确测试。

    参数:
        endpoint: REST 端点路径(例如 "/api/v1/calculate")
        json_data: POST/PUT 请求体
        http_method: HTTP 方法(GET、POST 等)

    返回:
        元组(response_data, status_code)
    """
    try:
        base_url = request.host_url.rstrip("/")
        url = f"{base_url}{endpoint}"

        headers = {"Content-Type": "application/json"}
        auth_header = request.headers.get("Authorization")
        if auth_header:
            headers["Authorization"] = auth_header

        logger.info(f"适配器: 代理到 REST {http_method} {url}")

        if http_method.upper() == "POST":
            response = http_requests.post(
                url, json=json_data, headers=headers,
                timeout=EXECUTE_TIMEOUT_SECONDS
            )
        elif http_method.upper() == "GET":
            response = http_requests.get(
                url, headers=headers,
                timeout=EXECUTE_TIMEOUT_SECONDS
            )
        else:
            response = http_requests.request(
                http_method, url, json=json_data, headers=headers,
                timeout=EXECUTE_TIMEOUT_SECONDS
            )

        logger.info(f"适配器: REST 返回 {response.status_code}")
        return response.json(), response.status_code

    except http_requests.RequestException as e:
        logger.error(f"适配器: 调用 REST 端点时出错: {e}", exc_info=True)
        return {"error": "内部服务器错误"}, 500

第 3 步:实现消息负载提取(A2A 规范 0.3)

code
def extract_message_payload(message: Dict) -> Optional[Dict]:
    """
    从 A2A 消息部分中提取负载(规范 0.3 格式)。

    预期格式:
    {
        "message": {
            "parts": [{"kind": "data", "data": {"operation": "add", "operands": [5, 3]}}]
        }
    }

    返回:
        提取的负载数据作为字典,如果未找到则返回 None
    """
    try:
        parts = message.get("parts", [])
        for part in parts:
            if isinstance(part, dict) and part.get("kind") == "data":
                return part.get("data")
        return None
    except Exception as e:
        logger.error(f"提取消息负载时出错:{e}")
        return None

def build_a2a_message(message_id: str, context_id: str, content: Any) -> Dict:
    """
    构建符合 A2A 规范的消息对象(A2A 规范 0.3)。

    响应格式:
    {
        "messageId": "uuid",
        "contextId": "uuid",
        "role": "agent",
        "parts": [{"kind": "data", "data": {...}}],
        "kind": "message",
        "metadata": {}
    }
    """
    if isinstance(content, dict):
        parts = [{"kind": "data", "data": content}]
    else:
        parts = [{"kind": "text", "text": str(content)}]

    return {
        "messageId": message_id,
        "contextId": context_id,
        "role": "agent",
        "parts": parts,
        "kind": "message",
        "metadata": {}
    }

关于使用服务器发送事件(SSE)进行流式传输的说明:

前面的 extract_message_payload() 函数对于 SendMessageSendStreamingMessage 都以相同的方式工作。

对于即时操作(如我们的计算器),这两种方法都返回单个结果。对于长时间运行的操作(例如报告生成或数据分析),SSE 流式传输允许服务器推送增量更新。

第 4 步:实现 JSON-RPC 响应构建器

code
# 根据 JSON-RPC 2.0 规范定义的错误代码
class JsonRpcError:
    PARSE_ERROR = -32700
    INVALID_REQUEST = -32600
    METHOD_NOT_FOUND = -32601
    INVALID_PARAMS = -32602
    INTERNAL_ERROR = -32603

def jsonrpc_error(code: int, message: str, data: Any = None, request_id: Any = None) -> Dict:
    """构建 JSON-RPC 2.0 错误响应。"""
    response = {
        "jsonrpc": "2.0",
        "error": {"code": code, "message": message},
        "id": request_id
    }
    if data is not None:
        response["error"]["data"] = data
    return response

def jsonrpc_success(result: Any, request_id: Any = None) -> Dict:
    """构建 JSON-RPC 2.0 成功响应。"""
    return {
        "jsonrpc": "2.0",
        "result": result,
        "id": request_id
    }

第 5 步:SendMessage — A2A 到 REST 的委托

code
def handle_send_message(data: Dict) -> Tuple[Any, int]:
    """
    处理 SendMessage —— 直接传递到 /api/v1/calculate。
    适配器不会检查或根据负载内容进行路由。
    """
    request_id = data.get("id")
    params = data.get("params", {})
    message = params.get("message", {})
    context_id = message.get("contextId") or generate_id()
    message_id = generate_id()

    payload = extract_message_payload(message)
    if not payload:
        return jsonify(jsonrpc_error(
            JsonRpcError.INVALID_PARAMS,
            "无效参数:消息中未找到 message.parts 的数据。",
            request_id=request_id
        )), 400

将负载原样传递到单个 REST 端点

rest_response, status = invoke_rest_endpoint( endpoint="/api/v1/calculate", json_data=payload, http_method="POST" )

if 200 <= status < 300: a2a_message = build_a2a_message(message_id, context_id, rest_response) return jsonify(jsonrpc_success(a2a_message, request_id)), 200 else: error_message = "操作失败" if isinstance(rest_response, dict): error_message = (rest_response.get("error") or rest_response.get("details") or "操作失败") error_code = (JsonRpcError.INVALID_PARAMS if 400 <= status < 500 else JsonRpcError.INTERNAL_ERROR) return jsonify(jsonrpc_error( error_code, error_message, data=rest_response, request_id=request_id )), status

我们需要显式地添加路由,因为这是 a2a

def generate_id() -> str: """为消息/上下文 ID 生成 UUID。""" return str(uuid.uuid4())

code

### 第 6 步:设置 A2A 路由(规范 0.3)

官方的 A2A SDK 提供了适用于 FastAPI 和 Starlette 应用的 A2A 库,这些库可以抽象出添加 A2A 特定路由的复杂性。虽然 A2A 库也适用于 Flask 应用,但我们在示例代码中没有使用它们。我们希望让您可以轻松理解如何在 Flask 应用上托管 A2A 覆盖层。以下代码片段添加了 A2A 所需的路由。

def setup_a2a_routes(app: Flask) -> None: """在 Flask 应用中注册 A2A 协议 v0.3 路由。""" app.add_url_rule("/.well-known/agent-card.json", "get_agent_card", get_agent_card, methods=["GET"]) app.add_url_rule("/a2a/capabilities", "get_capabilities", get_capabilities, methods=["GET"]) app.add_url_rule("/a2a/health", "a2a_health", a2a_health, methods=["GET"]) app.add_url_rule("/a2a", "a2a_jsonrpc", _handle_jsonrpc, methods=["POST"]) logger.info("A2A 协议 v0.3 路由已注册")

code

### 第 7 步:最后,在您的应用中进行初始化

app/main.py

from flask import Flask from app.rest_api import rest_api from app.a2a_adapter import setup_a2a_routes

def create_app(): app = Flask(__name__)

注册现有的 REST API

app.register_blueprint(rest_api)

添加 A2A 协议支持(请求转换器模式)

setup_a2a_routes(app) app.logger.info("通过请求转换器模式启用 A2A 协议")

return app

code

### 第 8 步:运行您的应用

从项目根目录运行以下命令。

python -m venv venv source venv/bin/activate # 在 Windows 上:venv\Scripts\activate pip install -r requirements.txt python -m app.main

code

## 使用 Amazon Bedrock AgentCore Gateway 添加代理覆盖层

Amazon Bedrock AgentCore 是一项服务,用于在无需管理基础设施的情况下构建、连接和优化代理。如以下图表所示,AgentCore Gateway 可以通过作为端点和服务的单一接入点,将代理覆盖层与应用解耦。这种分离使得一个代理覆盖层可以服务于多个服务或应用,而不仅仅是一个。AgentCore Gateway 每个网关最多支持 10 个目标,并且可以原生集成到现有的 AWS 服务中,并支持 OpenAPI 端点。

图 5:使用 Amazon Bedrock AgentCore Gateway 解耦代理层

企业级应用程序通常需要协调多个服务来处理复杂任务。例如,一个计算器应用程序处理“计算 2 * (3 + 4)”。如以下图表所示,系统首先查询一个运算顺序端点(如 /api/order-of-ops/...)以确定评估顺序。然后,它依次调用一个端点(如 /api/arithmetic/...)来计算“3 + 4”,接着是“2 * 7”。如果为每个服务添加代理层,会引入额外的开销。相反,您可以将这两个服务整合到一个代理层中,根据需要协调调用。

您可以将代理层与应用程序分离,以便根据功能而非仅根据应用程序来组织代理层。代理层作为代理方式与您的应用程序和系统进行交互,作为代理方式与功能进行交互。

除了 AgentCore Gateway 之外,AgentCore 的其他功能还可以简化代理层的监控、迭代和部署。AgentCore Identity 处理代理和网关组件的认证,支持 OAuth 2.0 提供商,并提供对 Okta、GitHub 和 Slack 的托管集成。AgentCore Observability 通过指标、日志和跨度可视化监控代理性能。您可以查看工具调用和延迟等高层数据,或使用与 Amazon CloudWatch 的原生集成,检查跨组件的详细执行路径。AgentCore Runtime 通过容器镜像部署模型,无论是开源模型、自定义模型,还是 Amazon Bedrock LLM(如 Nova 和 Anthropic Claude),而无需您管理大型语言模型(LLM)基础设施。

图 6:使用 Amazon Bedrock AgentCore 功能解耦代理层 —— 运行时、网关、身份和可观测性

通过使用 AgentCore 的不同功能,您可以简化代理和代理层的部署,并轻松集成到现有的 AWS 堆栈中。由于它是一个托管服务,它还可以减少实现代理层所需的工作量。

## 与合作伙伴共同加速企业 AI 采用

这种代理层模式代表了作者与 AWS 之间更广泛的协作,以帮助企业在现有基础设施和新兴 AI 能力之间架起桥梁。成功的 AI 采用需要务实的解决方案,既要尊重现有投资,又要支持渐进式转型。AWS 和作者正在共同开发参考架构、实现模式和工具,使企业能够在不进行大规模基础设施更换的情况下采用 A2A 通信。代理层模式体现了这一理念:保留已有的有效部分,在需要时进行扩展,并提供清晰的迁移路径,以最小化风险并最大化价值。

## 结论

代理层为企业提供了一条务实的路径,使其能够采用代理到代理(Agent-to-Agent)通信,而无需放弃其现有的 REST API 投资。通过添加一个轻量的翻译层,将 A2A 消息转换为 REST 负载,组织可以在新兴的代理生态系统中参与进来,同时保留稳定且经过生产测试的业务逻辑。两种实现模式提供了灵活性,以满足组织的不同需求:适用于特定使用场景的内部应用代理层,以及适用于企业级部署的 AgentCore 网关。无论您是启用一个单一的监督代理,还是协调复杂的多服务工作流,代理层都能减少并行基础设施的操作开销,以及全面重构所带来的回归风险。

当企业从确定性的 REST API 过渡到基于推理的代理系统时,关键的洞察始终是:A2A 并不是一个全新的 API,而是对现有 API 的一种新接口。这种观点将采用的挑战从重新构建一切转变为逐步适配,使组织能够更快地实现 AI 价值,同时有效管理风险。对于准备探索代理层的组织,计算器示例提供了一个具体的起点,而 AgentCore 则为生产部署提供了基础设施。

## 下一步

- 评估您的架构 – 审计 REST 服务,寻找适合 A2A 启用的候选服务。内部应用代理层适用于单一服务代理。AgentCore 网关适用于多服务工作流。

- 查看参考实现 – Flask 计算器示例展示了翻译模式,包括代理卡片的设置、消息提取、REST 调用和响应构建。

- 探索 Amazon Bedrock AgentCore – AgentCore 网关、身份验证和可观测性为生产级代理层提供了基础设施。

- 加入 A2A 社区 – A2A 协议规范和 SDK 文档可在 [a2a-protocol.org](https://a2a-protocol.org) 获取,支持 Flask、FastAPI 和 Starlette 应用的库也一并提供。

企业 AI 的未来并不在于替换现有系统,而在于通过代理能力来扩展它们。代理层使这一未来今天即可实现。

## 作者简介

'"`