Amazon Bedrock 上实现程序化工具调用的三种路径

2026-05-19 32 预计阅读时间:1 分钟
来源:aws.amazon.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:14 分钟

模型能"说"要调用工具,但谁来真正执行代码、返回结果?Amazon Bedrock 近期推出的 Programmatic Tool Calling(PTC)把这个问题推到了开发者面前:你需要一个沙箱来安全地跑代码,再把输出喂回模型。选哪种沙箱方案,直接决定了你的控制粒度、运维成本和开发体验。

下面拆解三种实现方式——自建 ECS Docker 沙箱、Bedrock AgentCore Code Interpreter 托管方案、以及 Anthropic SDK 代理路径——并给出可落地的配置示例。

为什么 PTC 不等于普通 Tool Calling

传统 Tool Calling 是模型输出一个 JSON 结构,你的应用层解析后调用外部 API,再把结果拼回对话。PTC 的区别在于:模型请求的是执行一段代码(通常是 Python),而不是调一个 REST 接口。这意味着你需要一个能安全运行任意代码的执行环境,而不是一个 API 网关。

核心挑战有三:

  • 安全性:用户间接提供的代码不能逃逸沙箱、访问宿主机资源。
  • 延迟:代码执行结果要快速回流,否则多轮对话体验会崩。
  • 状态管理:执行环境是否跨轮次保持(比如保留已导入的库和变量)。

三种方案分别在这些维度上做了不同取舍。

方案一:ECS 上的自托管 Docker 沙箱

把代码执行塞进 ECS Task,每个请求启动一个隔离容器,执行完销毁。你拥有完全控制权——镜像内容、网络策略、资源限额全由你定。

代价是运维负担:你要自己处理容器生命周期、日志收集、弹性伸缩,还要确保容器间网络隔离。

下面是一个最小化的 ECS Task Definition 和配套的执行代理微服务骨架:

# task-definition.yaml — ECS Fargate 沙箱任务定义
awsEcsTaskDefinition:
  family: ptc-sandbox
  networkMode: awsvpc
  requiresCompatibilities:
    - FARGATE
  cpu: "512"
  memory: "1024"
  containerDefinitions:
    - name: sandbox-runner
      image: "123456789012.dkr.ecr.us-east-1.amazonaws.com/ptc-sandbox:latest"
      essential: true
      resourceRequirements:
        - type: GPU
          value: "0"          # 不需要 GPU,纯 CPU 执行
      environment:
        - name: MAX_EXEC_SECONDS
          value: "30"         # 执行超时上限
        - name: MEMORY_LIMIT_MB
          value: "512"
      linuxParameters:
        initProcessEnabled: true
      logConfiguration:
        logDriver: awslogs
        options:
          awslogs-group: /ecs/ptc-sandbox
          awslogs-region: us-east-1
          awslogs-stream-prefix: sandbox

沙箱镜像的 Dockerfile 关键片段——只装必要的库,不装网络工具:

FROM python:3.11-slim

# 只安装计算类库,不装 requests/httpx 等网络库
RUN pip install --no-cache-dir numpy pandas sympy

# 切到非 root 用户
RUN useradd --create-home sandbox
USER sandbox
WORKDIR /home/sandbox

# 执行入口:从 stdin 读代码,写结果到 stdout
COPY run_code.py /home/sandbox/run_code.py
ENTRYPOINT ["python", "/home/sandbox/run_code.py"]

run_code.py 的核心逻辑——受限执行、超时保护:

import sys
import json
import signal
import resource

MAX_MEMORY = 512 * 1024 * 1024  # 512 MB

def limit_memory():
    resource.setrlimit(resource.RLIMIT_AS, (MAX_MEMORY, MAX_MEMORY))

def timeout_handler(signum, frame):
    raise TimeoutError("Execution exceeded time limit")

def run(code: str, timeout_sec: int = 30):
    limit_memory()
    signal.signal(signal.SIGALRM, timeout_handler)
    signal.alarm(timeout_sec)
    try:
        # 只暴露安全内建函数
        safe_globals = {"__builtins__": {
            "print": print, "len": len, "range": range,
            "int": int, "float": float, "str": str,
            "list": list, "dict": dict, "set": set,
            "True": True, "False": False, "None": None,
        }}
        # 注入常用库
        safe_globals.update(
            __import__("numpy").__dict__,
            __import__("pandas").__dict__,
        )
        exec(code, safe_globals)
        signal.alarm(0)
    except TimeoutError:
        print(json.dumps({"error": "timeout", "status": "failed"}))
    except Exception as e:
        print(json.dumps({"error": str(e), "status": "failed"}))
    finally:
        signal.alarm(0)

if __name__ == "__main__":
    code_input = sys.stdin.read()
    run(code_input)

调用侧的编排逻辑:Bedrock 返回 tool_use 后,你的应用启动 ECS Task,把代码通过 stdin 注入,读 stdout 结果,再调用 Bedrock 的 tool_result 接口回传。整个过程需要你写一个调度器来管理 Task 的启停和结果轮询。

适合谁:对安全合规有硬性要求、需要自定义镜像内容(比如装了公司内部计算库)、愿意承担运维成本的团队。

方案二:Bedrock AgentCore Code Interpreter——托管沙箱

Amazon Bedrock AgentCore Code Interpreter 是 AWS 提供的托管代码执行环境。你不需要管容器、不需要管镜像,只需要在 Agent 配置里声明启用 Code Interpreter,Bedrock 自动处理沙箱创建、代码执行和结果回传。

配置方式在 Agent 定义中添加 codeInterpreter capability:

import boto3

client = boto3.client("bedrock-agent")

response = client.create_agent(
    agentName="ptc-agent-with-interpreter",
    agentResourceRoleArn="arn:aws:iam::123456789012:role/BedrockAgentRole",
    idleSessionTTLInSeconds=600,
    instruction="You are a data analysis assistant. When asked to compute, generate and run Python code using the code interpreter.",
    capabilities=[
        {
            "codeInterpreter": {
                "enabled": True
            }
        }
    ],
)

agent_id = response["agent"]["agentId"]
print(f"Agent created: {agent_id}")

调用侧更简单——你只需要正常发起对话,模型自动决定是否调用 Code Interpreter,执行结果由托管层注入回对话流:

import boto3

client = boto3.client("bedrock-agent-runtime")

response = client.invoke_agent(
    agentId="YOUR_AGENT_ID",
    agentAliasId="TSTALIASID",
    sessionId="session-001",
    inputText="计算 1 到 1000 的所有质数之和,并给出前 10 个质数",
)

# 流式读取事件,Code Interpreter 的执行结果已自动嵌入
for event in response["completion"]:
    if "chunk" in event:
        print(event["chunk"]["bytes"].decode())

取舍:你失去了对执行环境的完全控制——不能自定义预装库、不能限制内存到精确值、不能审计容器日志的每一行。换来的是零运维和更快的上线速度。

适合谁:快速验证 PTC 能力、不想运维基础设施、对执行环境定制需求不高的场景。

方案三:Anthropic SDK 代理路径

有些团队已经深度使用 Anthropic 的 Python SDK(anthropic 包),习惯了 client.beta.tools.use() 的开发范式。直接迁移到 Bedrock 的 SDK 意味着改写大量调用代码。

第三种方案是在中间架一个轻量代理:你的应用继续用 Anthropic SDK 的接口风格发请求,代理层把请求翻译成 Bedrock 的 PTC 格式,再把 Bedrock 的沙箱执行结果翻译回 Anthropic SDK 的 tool_result 格式。

代理的核心转换逻辑示意:

from fastapi import FastAPI, Request
import boto3
import json

app = FastAPI()
bedrock = boto3.client("bedrock-runtime")

ANTHROPIC_TO_BEDROCK_MODEL = {
    "claude-3-5-sonnet-20241022": "anthropic.claude-3-5-sonnet-20241022-v2:0",
}

@app.post("/v1/messages")
async def proxy_messages(request: Request):
    body = await request.json()

    # 把 Anthropic SDK 的 tool 定义翻译成 Bedrock toolConfig
    anthropic_tools = body.get("tools", [])
    bedrock_tools = []
    for t in anthropic_tools:
        if t["type"] == "custom" and t["name"] == "code_interpreter":
            bedrock_tools.append({
                "toolSpec": {
                    "name": t["name"],
                    "description": t["description"],
                    "inputSchema": {
                        "json": t["input_schema"]
                    }
                }
            })

    model_id = ANTHROPIC_TO_BEDROCK_MODEL.get(
        body["model"], "anthropic.claude-3-5-sonnet-20241022-v2:0"
    )

    bedrock_response = bedrock.converse(
        modelId=model_id,
        messages=translate_messages(body["messages"]),
        toolConfig={"tools": bedrock_tools} if bedrock_tools else None,
        inferenceConfig={
            "maxTokens": body.get("max_tokens", 4096),
            "temperature": body.get("temperature", 0.0),
        },
    )

    # 把 Bedrock 响应翻译回 Anthropic SDK 格式
    return translate_response(bedrock_response)


def translate_messages(anthropic_msgs):
    """把 Anthropic 消息格式转为 Bedrock 消息格式"""
    bedrock_msgs = []
    for msg in anthropic_msgs:
        role = msg["role"]
        content_blocks = []
        for c in msg.get("content", []):
            if c["type"] == "text":
                content_blocks.append({"text": c["text"]})
            elif c["type"] == "tool_use":
                content_blocks.append({
                    "toolUse": {
                        "toolUseId": c["id"],
                        "name": c["name"],
                        "input": c["input"],
                    }
                })
            elif c["type"] == "tool_result":
                content_blocks.append({
                    "toolResult": {
                        "toolUseId": c["tool_use_id"],
                        "content": [{"text": c["content"]}],
                        "status": "success" if not c.get("is_error") else "error",
                    }
                })
        bedrock_msgs.append({"role": role, "content": content_blocks})
    return bedrock_msgs


def translate_response(bedrock_resp):
    """把 Bedrock converse 响应翻译回 Anthropic Messages API 格式"""
    output = bedrock_resp["output"]["message"]
    content = []
    for block in output["content"]:
        if "text" in block:
            content.append({"type": "text", "text": block["text"]})
        elif "toolUse" in block:
            content.append({
                "type": "tool_use",
                "id": block["toolUse"]["toolUseId"],
                "name": block["toolUse"]["name"],
                "input": block["toolUse"]["input"],
            })
    return {
        "id": bedrock_resp["ResponseMetadata"]["RequestId"],
        "type": "message",
        "role": output["role"],
        "content": content,
        "model": bedrock_resp["modelId"],
        "stop_reason": map_stop_reason(bedrock_resp["stopReason"]),
        "usage": {
            "input_tokens": bedrock_resp["usage"]["inputTokens"],
            "output_tokens": bedrock_resp["usage"]["outputTokens"],
        },
    }


def map_stop_reason(reason):
    mapping = {
        "tool_use": "tool_use",
        "end_turn": "end_turn",
        "max_tokens": "max_tokens",
    }
    return mapping.get(reason, reason)

部署这个代理到 Lambda 或 ECS 后,你的现有代码只需改一行——把 base_url 指向代理:

from anthropic import Anthropic

# 原来的调用:client = Anthropic()
# 改为指向代理:
client = Anthropic(base_url="https://your-proxy.example.com/v1")

response = client.beta.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=4096,
    tools=[{
        "type": "custom",
        "name": "code_interpreter",
        "description": "Execute Python code in a sandbox",
        "input_schema": {
            "type": "object",
            "properties": {
                "code": {"type": "string", "description": "Python code to execute"}
            },
            "required": ["code"],
        },
    }],
    messages=[{"role": "user", "content": "用 Python 计算 Fibonacci 数列前 20 项"}],
)

取舍:代理层引入了额外延迟(一次 HTTP 转发 + 格式转换),而且你需要自己维护翻译逻辑的完整性——Anthropic 和 Bedrock 的消息格式差异会随版本演进扩大。但团队不需要重写调用代码,迁移成本最低。

适合谁:已有 Anthropic SDK 集成、想渐进迁移到 Bedrock、暂时不愿大规模改写调用层的团队。

选型对照

维度 ECS 自托管 AgentCore Code Interpreter Anthropic SDK 代理
控制粒度 完全自定义镜像、资源限额 托管层决定,有限配置 取决于后端选哪种沙箱
运维成本 高(容器管理、伸缩、日志) 低(AWS 全托管) 中(维护代理服务)
上线速度 慢(需构建镜像+调度器) 快(Agent 配置即启用) 中(需部署代理+调试翻译)
安全审计 完全可控,可逐行审计 依赖 AWS 审计能力 代理层可加日志,沙箱取决于后端
SDK 体验 Bedrock SDK 原生 Bedrock Agent SDK 原生 Anthropic SDK 原生

落地建议

  1. 先用托管方案验证。AgentCore Code Interpreter 几行配置就能跑通 PTC 的完整链路,先确认模型在你的场景下确实能稳定生成可执行代码,再决定是否需要自建沙箱。

  2. 自建沙箱时先锁网络。ECS Task 放到私有子网,Security Group 只允许出站到 Bedrock API 端点,不允许 SSH 入站。镜像里不装 curlwget 等网络工具——代码执行环境应该是一个计算孤岛。

  3. 代理路径要加版本对齐测试。Anthropic Messages API 和 Bedrock Converse API 的字段映射不是一劳永逸的。每次 Bedrock 或 Anthropic SDK 升级,跑一遍集成测试确认翻译层没有遗漏新字段。

  4. 给沙箱加输出大小限制。不管哪种方案,都要限制 stdout/stderr 的返回体积(比如 100 KB),防止模型生成的代码意外输出海量数据,撑爆回传通道。

PTC 把"模型想执行代码"这件事从概念变成了工程问题。三种方案的取舍本质上是控制权与便利性的交换——选哪条路,取决于你愿意为控制权付出多少运维成本,以及你的团队对哪种 SDK 体验更熟悉。


相关推荐