Дата публикации
ai_products

Как Amazon Bedrock ускоряет сложные AI‑воркфлоу: программный вызов инструментов вместо бесконечных промптов

Что нового

Amazon продвигает на Bedrock подход programmatic tool calling (PTC) — по сути, это когда Claude, Llama, Qwen и другие LLM на Bedrock больше не дергают каждый инструмент по одному разу через промпты, а сначала пишут кусок кода (обычно на Python), который уже сам вызывает все нужные инструменты внутри изолированного окружения.

Ключевые новшества:

  • Один вызов модели вместо десятков. Модель один раз генерирует Python‑скрипт, который внутри сам вызывает все нужные API/инструменты. Второй вызов модели — только чтобы красиво сформулировать финальный ответ.
  • Резкое снижение токенов при сложных сценариях с множеством инструментов:
    • Claude Sonnet 4.6 (режим adaptive thinking): 12 739 токенов с PTC против 128 043 без PTC — экономия 90,1%.
    • Claude Opus 4.6: 13 043 против 126 15289,7%.
    • Qwen3‑Coder‑480B: 34 159 против 305 11488,8%.
    • Qwen3‑Next‑80B: 28 878 против 233 33287,6%.
    • deepseek.v3.2 (thinking): 19 543 против 245 96792,1%.
    • MiniMax M2.1 (thinking): 11 787 против 101 99088,4%.
    • Kimi 2.5 (thinking): 10 875 против 148 08592,7%.
    • GLM 4.7 (thinking): 11 550 против 115 82990,0%.
  • Рост точности на задачах с большим объемом данных. Во всех тестах с PTC все модели выдали правильный ответ. Без PTC несколько крупных моделей (Qwen3‑Coder‑480B, Qwen3‑Next‑80B, deepseek.v3.2, MiniMax M2.1, Kimi 2.5, GLM 4.7) ошибались на том же задании.
  • Три варианта внедрения PTC на Amazon Bedrock:
    1. Самостоятельный Docker‑sandbox на Amazon ECS или другом compute.
    2. Управляемый Code Interpreter в Amazon Bedrock AgentCore.
    3. Proxy‑слой, который позволяет использовать Anthropic SDK поверх Bedrock, но с тем же паттерном PTC.

Фокус не на новом API, а на паттерне: модель пишет код, код выполняется в песочнице, в контекст модели возвращается только финальный результат.

Как это работает

Проблема классического tool calling

Пример из статьи: запрос «Какие сотрудники инженерной команды превысили свой Q3 travel‑бюджет?».

Без PTC LLM делает длинный сериализованный workflow:

  1. Вызов инструмента, который возвращает список сотрудников — скажем, 20 человек.
  2. Для каждого из 20 человек — отдельный вызов инструмента get_expenses, каждый отдает по 50–100 строк расходов.
  3. Дополнительные вызовы инструментов, чтобы получить бюджетные лимиты.
  4. Все эти 2000+ строк расходов попадают в контекст модели.
  5. Модель в естественном языке фильтрует, суммирует, сравнивает, формирует ответ.

Минусы такого подхода:

  • Токены: в контекст попадают тысячи записей, хотя модель в итоге выбросит большую часть.
  • Латентность: 20 последовательных вызовов инструментов = 20 полных прогонов модели.
  • Ошибки: LLM, который фильтрует и суммирует тысячи числовых записей «словами», закономерно ошибается там, где 10 строк Python справятся идеально.

Как PTC меняет схему

PTC разворачивает схему наоборот: модель генерирует один блок Python‑кода, который:

  • Параллельно вызывает инструменты.
  • Делает фильтрацию, агрегацию, сравнения внутри кода, а не в естественном языке.
  • Выводит только итоговую сводку через print().

Пример кода, который генерирует модель в PTC‑режиме для того же запроса:

import asyncio
import json

# Step 1: Get team members
team_json = await get_team_members(department="engineering")
team = json.loads(team_json)

# Step 2: Fetch all expense records in parallel
expense_tasks = [
    get_expenses(employee_id=m["id"], quarter="Q3")
    for m in team
]
expenses_results = await asyncio.gather(*expense_tasks)

# Step 3: Filter and check budgets
exceeded = []

for member, exp_json in zip(team, expenses_results):
    expenses = json.loads(exp_json)
    total_travel = sum(
        e["amount"]
        for e in expenses
        if e["category"] == "travel" and e["status"] == "approved"
    )
    if total_travel > 5000:
        budget_json = await get_custom_budget(user_id=member["id"])
        budget = json.loads(budget_json)
        limit = budget["budget_limit"]
        if total_travel > limit:
            exceeded.append({
                "name": member["name"],
                "spent": total_travel,
                "limit": limit,
                "exceeded_by": total_travel - limit,
            })

# Step 4: Only the summary enters the model's context
print(f"{len(exceeded)} members exceeded budget:")
print(json.dumps(exceeded, indent=2))

Две ключевые детали:

  • asyncio.gather() запускает все 20 вызовов get_expenses параллельно, без 20 отдельных прогонов LLM.
  • Все вычисления — суммы, фильтры, сравнения с лимитами — идут в Python. Модель видит только итоговый print().

Модель вызывают всего два раза:

  1. Сгенерировать код.
  2. Прочитать вывод кода и превратить его в финальный ответ на естественном языке.

Весь «тяжелый» кусок — вызовы инструментов, работа с данными — выполняется в контейнере без дополнительных inference‑циклов.

Вариант 1: Самостоятельный Docker‑sandbox на Amazon ECS

Здесь вы сами поднимаете оркестратор (на ECS, Lambda или любом compute) и Docker‑контейнер, в котором выполняется код.

Компоненты:

  • Оркестратор — код, который:
    • Дергает Amazon Bedrock через InvokeModel.
    • Формирует system‑prompt, где описаны доступные инструменты.
    • Поднимает Docker‑контейнер и общается с ним по stdin/stderr.
  • Docker‑sandbox — изолированный контейнер, который исполняет Python‑код, сгенерированный моделью.

Ключевой прием: вместо того чтобы описывать бизнес‑инструменты в tool_config Bedrock, вы вшиваете описание инструментов в system‑prompt и просите модель писать код, который эти инструменты вызывает.

System‑prompt

Сердце PTC — system‑prompt, который объясняет модели, что она работает в режиме «пишу код, а не сразу отвечаю»:

# Code Execution Environment Description

## Core Function
You can use the `execute_code` tool to run Python code. The code can call asynchronous tool functions.

{tools_doc}

## Key Rules

### 1. Stateless Environment
- Each `execute_code` call is a fresh environment.
- Variables are not retained between calls.
- All operations must be completed in a single code block.

### 2. Basic Syntax
- Tool calls must use `await`.
- Use `print()` to output results.
- Data processing, filtering, and aggregation are allowed.

## Best Practices

### Correct: One code block completes all tasks

import json
import asyncio

data = await get_orders(days=7)
orders = json.loads(data)

tasks = [get_detail(id=o['id']) for o in orders]
details = await asyncio.gather(*tasks)

for order, detail in zip(orders, details):
    print(f"{order['name']}: {detail}")

### Incorrect: Multiple code blocks

# First execution
data = await get_orders()

# Second execution - NameError: data does not exist
for item in data:
    pass

Модель учится:

  • Писать один цельный блок кода.
  • Всегда вызывать инструменты через await.
  • Возвращать результат через print().

SandboxExecutor и runner‑скрипт

SandboxExecutor:

  • Стартует Docker‑контейнер для каждого запуска кода.
  • Передает в контейнер сгенерированный моделью Python‑файл.
  • Читает stderr/stdout, чтобы поймать запросы к инструментам и финальный вывод.

Внутри контейнера запускается runner‑скрипт, который:

  • Оборачивает код модели в async‑контекст.
  • Генерирует async‑функции для каждого инструмента (get_team_members, get_expenses и т.д.).
  • При вызове инструмента:
    • Пишет в stderr структурированное сообщение о вызове.
    • Ждет по stdin результат от оркестратора.

Поддерживаются два режима:

  • Single mode — один запуск кода, контейнер завершает работу.
  • Loop mode — контейнер живет дольше, принимает несколько запусков, может держать состояние между вызовами.

IPC‑протокол

Чтобы отделить разные типы сообщений в текстовом потоке, используют маркеры:

  • __PTC_TOOL_CALL__ / __PTC_END_CALL__ — обрамляют JSON с именем инструмента и аргументами.
  • __PTC_OUTPUT__ — помечает финальный вывод кода.

Сценарий:

  1. Код вызывает await get_team_members(...).
  2. Сгенерированная функция пишет в stderr:
    • __PTC_TOOL_CALL__.
    • JSON с именем инструмента и аргументами.
    • __PTC_END_CALL__.
  3. Оркестратор читает stderr, парсит JSON, вызывает реальный инструмент, пишет результат в stdin.
  4. Runner‑скрипт читает stdin, возвращает результат в вызывающий код.

Оркестратор: основной цикл

Ниже — ключевой пример оркестратора, который связывает Bedrock и Docker‑sandbox:

import boto3
import json
import subprocess
import tempfile
import os

# ── Configuration ──
MODEL_ID = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
REGION = "us-west-2"
SANDBOX_IMAGE = "ptc-sandbox"
SYSTEM_PROMPT = "..."  # Full system prompt as shown above

TOOLS = [
    {
        "name": "execute_code",
        "description": "Execute Python code in a sandboxed environment.",
        "input_schema": {
            "type": "object",
            "properties": {
                "code": {"type": "string", "description": "Python code to execute."}
            },
            "required": ["code"],
        },
    }
]

# ── Bedrock call ──

def call_bedrock(client, messages):
    body = json.dumps(
        {
            "anthropic_version": "bedrock-2023-05-31",
            "max_tokens": 4096,
            "system": [{"type": "text", "text": SYSTEM_PROMPT}],
            "tools": TOOLS,
            "messages": messages,
        }
    )

    response = client.invoke_model(
        modelId=MODEL_ID,
        contentType="application/json",
        accept="application/json",
        body=body,
    )
    return json.loads(response["body"].read())


# ── Sandbox execution ──

def execute_in_sandbox(code):
    """Run code in a hardened Docker container. Returns stdout."""

    with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
        f.write("import json\n\n" + code)
        tmp_path = f.name

    try:
        result = subprocess.run(
            [
                "docker",
                "run",
                "--rm",
                "--network",
                "none",
                "--read-only",
                "--tmpfs",
                "/tmp:size=64m",
                "--user",
                "sandbox",
                "--cap-drop",
                "ALL",
                "--memory",
                "256m",
                "--cpus",
                "0.5",
                "-v",
                f"{tmp_path}:/sandbox/user_code.py:ro",
                SANDBOX_IMAGE,
            ],
            capture_output=True,
            text=True,
            timeout=30,
        )

        return result.stdout.strip() if result.returncode == 0 else result.stderr.strip()
    finally:
        os.unlink(tmp_path)


# ── PTC orchestration loop ──

client = boto3.client("bedrock-runtime", region_name=REGION)

query = "Which engineering team members exceeded their Q3 travel budget?"

# Step 1: Send user query — model generates Python code
messages = [{"role": "user", "content": query}]
response = call_bedrock(client, messages)

# Step 2: Extract code from tool_use block
for block in response["content"]:
    if block["type"] == "tool_use":
        code = block["input"]["code"]
        tool_id = block["id"]

# Step 3: Execute in Docker sandbox
output = execute_in_sandbox(code)

# Step 4: Send sandbox output back as tool_result
messages.append({"role": "assistant", "content": response["content"]})
messages.append(
    {
        "role": "user",
        "content": [
            {"type": "tool_result", "tool_use_id": tool_id, "content": output}
        ],
    }
)

# Step 5: Model interprets the result and produces final answer
final = call_bedrock(client, messages)

for block in final["content"]:
    if block["type"] == "text":
        print(block["text"])

На выходе вы получаете финальный текстовый ответ, а модель за это время была вызвана ровно дважды.

Безопасность Docker‑sandbox

Пример команды, которая запускает контейнер с жесткими ограничениями:

docker run --rm \
  --network none \
  --read-only \
  --tmpfs /tmp:size=64m \
  --user sandbox \
  --cap-drop ALL \
  --memory 256m \
  --cpus 0.5 \
  -v /path/to/code.py:/sandbox/user_code.py:ro \
  ptc-sandbox

Что это дает:

  • Нет сетевого доступа (--network none).
  • Файловая система только для чтения, временный tmpfs 64 МБ.
  • Запуск не от root (--user sandbox).
  • Все Linux‑capabilities сброшены.
  • Жесткие лимиты по памяти и CPU.

Код, который пишет модель, не может выйти из песочницы, сломать хост или выжрать все ресурсы.

Вариант 2: Управляемый PTC через Amazon Bedrock AgentCore Code Interpreter

Если не хочется управлять Docker и ECS, Amazon предлагает Code Interpreter в составе Bedrock AgentCore.

Архитектура похожа на self‑hosted вариант, но есть отличия:

  • Песочницу управляет Amazon.
  • Инструменты загружаются как Python‑функции внутрь сессии Code Interpreter, а не вызываются через IPC.
  • Модель генерирует код, который напрямую вызывает эти функции.

Пример кода на Python с использованием bedrock-agentcore:

import boto3
import json

bedrock = boto3.client("bedrock-runtime", region_name="us-west-2")
agentcore = boto3.client("bedrock-agentcore", region_name="us-west-2")

# Start a Code Interpreter session
session = agentcore.start_code_interpreter_session(
    codeInterpreterIdentifier="aws.codeinterpreter.v1",
    name="ptc-tools",
    sessionTimeoutSeconds=900,
)

session_id = session["sessionId"]

# Pre-load tool functions into the sandbox.
# Replace this string with your actual tool function definitions.
tool_functions_code = """

def get_team_members(department):
    # Your implementation here — return JSON string
    pass


def get_expenses(employee_id, quarter="Q3"):
    # Your implementation here — return JSON string
    pass


def get_custom_budget(user_id):
    # Your implementation here — return JSON string
    pass


print("Tools loaded.")
"""

agentcore.invoke_code_interpreter(
    codeInterpreterIdentifier="aws.codeinterpreter.v1",
    sessionId=session_id,
    name="executeCode",
    arguments={"language": "python", "code": tool_functions_code},
)

Дальше вы можете просить модель писать код, который вызывает get_team_members, get_expenses, get_custom_budget — они уже загружены в сессию.

Сравнение с self‑hosted подходом:

  • Инфраструктура — полностью на стороне AWS.
  • Настройка окружения — стандартный runtime, меньше контроля, но и меньше забот.
  • Вызовы инструментов — идут внутри песочницы, без IPC до клиента.
  • Сеть по умолчанию выключена, есть PUBLIC‑режим при необходимости.

Вариант 3: Anthropic SDK через proxy поверх Bedrock

Если команда привыкла к Anthropic SDK, но хочет использовать Amazon Bedrock как backend, можно поставить прокси‑слой на ECS.

Этот proxy:

  • Принимает вызовы Anthropic API.
  • Переводит их в InvokeModel Bedrock.
  • Управляет Docker‑sandbox и PTC‑протоколом так же, как в self‑hosted варианте.

На стороне разработчика код почти не меняется — кроме base_url:

import anthropic

# Point the Anthropic SDK at the proxy deployed on ECS.
# The proxy translates these calls to Bedrock InvokeModel under the hood.
client = anthropic.Anthropic(
    api_key="your-proxy-api-key",  # API key configured in the proxy
    base_url="http://your-proxy-url.com",  # Your proxy's ECS endpoint
)

# Define PTC tools — same format as Anthropic's native PTC API
ptc_tools = [
    {"type": "code_execution_20250825", "name": "code_execution"},
    {
        "name": "get_team_members",
        "description": "Get department team member list",
        "input_schema": {
            "type": "object",
            "properties": {"department": {"type": "string"}},
            "required": ["department"],
        },
        "allowed_callers": ["code_execution_20250825"],
    }
    # Add get_expenses, get_custom_budget similarly
]

response = client.beta.messages.create(
    model="claude-sonnet-4-5-20250929",  # Proxy routes to Bedrock model
    betas=["advanced-tool-use-2025-11-20"],
    tools=ptc_tools,
    messages=[
        {
            "role": "user",
            "content": "Which team members exceeded Q3 travel budget?",
        }
    ],
)

# The proxy handles sandbox execution and tool call interception transparently.

Proxy полностью берет на себя оркестрацию PTC и работу с Docker‑sandbox.

Что это значит для вас

Где PTC дает максимальный эффект

  1. Обработка больших массивов данных.

    • Пример из эксперимента: 8 инженеров, у каждого по 20–50 расходов за квартал, каждая запись — 15+ полей.
    • С PTC модель не читает тысячи строк расходов. Все вычисления идут в Python внутри песочницы.
  2. Точные числовые расчеты и проверки.

    • Стандартный travel‑бюджет: $5000 за квартал.
    • У некоторых сотрудников — индивидуальные лимиты.
    • Задача: найти тех, кто превысил $5000, затем проверить, есть ли у них кастомный лимит, и сравнить с ним.
    • Python‑код делает это без ошибок округления и «галлюцинаций».
  3. Оркестрация многошаговых бизнес‑процессов.

    • Множественные вызовы CRM/ERP/внутренних API.
    • Параллельные запросы, сложная логика переходов.
    • Легче выразить это кодом, чем длинной цепочкой tool‑вызовов через промпты.
  4. Сценарии с повышенными требованиями к приватности.

    • Сырые данные (например, расходы сотрудников) остаются в контейнере в вашем AWS‑аккаунте.
    • В контекст модели попадает только агрегированный результат.

Где PTC не нужен

  • Простые Q&A, генерация текста, короткие цепочки инструментов (1–2 вызова) — overhead с песочницей не окупится.
  • Задачи, где важен не код, а творческое письмо, маркетинговые тексты, идеи — здесь обычное tool calling или вообще чистый LLM вполне достаточны.

Практические рекомендации

  • Если у вас сложные аналитические запросы к внутренним данным (финансы, логистика, маркетинг‑отчеты) — PTC заметно снижает токены и уменьшает риск ошибок.
  • Если вы строите агента, который дергает десятки API — проще дать модели писать код‑оркестратор, чем пытаться описать весь сценарий в промптах.
  • Если вы в России:
    • Amazon Bedrock официально доступен в регионах AWS, которые могут требовать обходных путей для доступа из РФ.
    • Вероятнее всего, понадобится корпоративный аккаунт AWS и, возможно, VPN/прокси для работы с консолями и API.

Место на рынке

По сути, Amazon предлагает не конкурента GPT‑4o или Claude Sonnet, а паттерн работы с уже существующими моделями на Bedrock.

Сравнение по фактам из экспериментов:

  • Токены:
    • На одинаковой задаче PTC сокращает потребление токенов на 87–92% для восьми разных моделей.
    • Для Claude Sonnet 4.6: 12 739 токенов с PTC против 128 043 без него.
  • Точность:
    • С PTC все протестированные модели (Claude Sonnet 4.6, Claude Opus 4.6, Qwen3‑Coder‑480B, Qwen3‑Next‑80B, deepseek.v3.2, MiniMax M2.1, Kimi 2.5, GLM 4.7) выдали правильный ответ по списку сотрудников, превысивших бюджет.
    • Без PTC часть этих же моделей ошибалась на том же наборе данных.

Так как в материале нет прямых сравнений скорости и стоимости с GPT‑4o, GPT‑4.1 или другими продуктами OpenAI, их корректно сравнивать нельзя. Но по цифрам токенов видно, что один и тот же модельный backend на Bedrock работает заметно экономичнее и точнее, если переключиться с классического tool calling на PTC.

Для кого это особенно интересно:

  • Команды, которые уже используют Amazon Bedrock и хотят выжать максимум из Claude, Qwen, Llama и других моделей без смены провайдера.
  • Разработчики, привыкшие к Anthropic SDK, но желающие запускать inference и код в своем AWS‑аккаунте — через proxy‑слой.
  • Enterprise‑команды с жесткими требованиями по приватности и контролю окружения, которым важен self‑hosted Docker‑sandbox с понятной моделью безопасности.

Как запустить: базовые шаги

Ниже — минимальный чек‑лист, если вы хотите попробовать PTC на Bedrock.

  1. Выбрать подход:

    • Нужен полный контроль окружения, свои Python‑пакеты, кастомная безопасность — идите в self‑hosted Docker на ECS.
    • Хотите минимальной эксплуатации — используйте AgentCore Code Interpreter.
    • Уже пишете на Anthropic SDK — поднимите proxy на ECS, который переведет вызовы в Bedrock.
  2. Собрать system‑prompt:

    • Описать execute_code как инструмент.
    • Встроить документацию по бизнес‑инструментам (get_team_members, get_expenses, get_custom_budget и т.п.) в prompt.
    • Показать модели примеры корректного и некорректного кода.
  3. Настроить sandbox:

    • Для self‑hosted — собрать Docker‑образ ptc-sandbox с Python и runner‑скриптом.
    • Ограничить сеть, ресурсы, права доступа, как в примере docker run выше.
  4. Реализовать оркестратор:

    • Использовать пример кода с call_bedrock и execute_in_sandbox.
    • Добавить реальную реализацию бизнес‑инструментов и обработку IPC‑сообщений.
  5. Прогнать свой реальный сценарий:

    • Взять типичную для вас задачу: аудит расходов, отчет по продажам, агрегация логов.
    • Сравнить токены и ошибки с PTC и без него.

Если вы уже строите AI‑агентов на Bedrock, переход к PTC — это не смена стека, а смена архитектурного паттерна. Модель та же, инструменты те же, но теперь между ними — Python‑код, а не цепочка промптов.


Читайте также