- Дата публикации
Как 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 152 — 89,7%.
- Qwen3‑Coder‑480B: 34 159 против 305 114 — 88,8%.
- Qwen3‑Next‑80B: 28 878 против 233 332 — 87,6%.
- deepseek.v3.2 (thinking): 19 543 против 245 967 — 92,1%.
- MiniMax M2.1 (thinking): 11 787 против 101 990 — 88,4%.
- Kimi 2.5 (thinking): 10 875 против 148 085 — 92,7%.
- GLM 4.7 (thinking): 11 550 против 115 829 — 90,0%.
- Рост точности на задачах с большим объемом данных. Во всех тестах с PTC все модели выдали правильный ответ. Без PTC несколько крупных моделей (Qwen3‑Coder‑480B, Qwen3‑Next‑80B, deepseek.v3.2, MiniMax M2.1, Kimi 2.5, GLM 4.7) ошибались на том же задании.
- Три варианта внедрения PTC на Amazon Bedrock:
- Самостоятельный Docker‑sandbox на Amazon ECS или другом compute.
- Управляемый Code Interpreter в Amazon Bedrock AgentCore.
- Proxy‑слой, который позволяет использовать Anthropic SDK поверх Bedrock, но с тем же паттерном PTC.
Фокус не на новом API, а на паттерне: модель пишет код, код выполняется в песочнице, в контекст модели возвращается только финальный результат.
Как это работает
Проблема классического tool calling
Пример из статьи: запрос «Какие сотрудники инженерной команды превысили свой Q3 travel‑бюджет?».
Без PTC LLM делает длинный сериализованный workflow:
- Вызов инструмента, который возвращает список сотрудников — скажем, 20 человек.
- Для каждого из 20 человек — отдельный вызов инструмента
get_expenses, каждый отдает по 50–100 строк расходов. - Дополнительные вызовы инструментов, чтобы получить бюджетные лимиты.
- Все эти 2000+ строк расходов попадают в контекст модели.
- Модель в естественном языке фильтрует, суммирует, сравнивает, формирует ответ.
Минусы такого подхода:
- Токены: в контекст попадают тысячи записей, хотя модель в итоге выбросит большую часть.
- Латентность: 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().
Модель вызывают всего два раза:
- Сгенерировать код.
- Прочитать вывод кода и превратить его в финальный ответ на естественном языке.
Весь «тяжелый» кусок — вызовы инструментов, работа с данными — выполняется в контейнере без дополнительных inference‑циклов.
Вариант 1: Самостоятельный Docker‑sandbox на Amazon ECS
Здесь вы сами поднимаете оркестратор (на ECS, Lambda или любом compute) и Docker‑контейнер, в котором выполняется код.
Компоненты:
- Оркестратор — код, который:
- Дергает Amazon Bedrock через
InvokeModel. - Формирует system‑prompt, где описаны доступные инструменты.
- Поднимает Docker‑контейнер и общается с ним по stdin/stderr.
- Дергает Amazon Bedrock через
- 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__— помечает финальный вывод кода.
Сценарий:
- Код вызывает
await get_team_members(...). - Сгенерированная функция пишет в stderr:
__PTC_TOOL_CALL__.- JSON с именем инструмента и аргументами.
__PTC_END_CALL__.
- Оркестратор читает stderr, парсит JSON, вызывает реальный инструмент, пишет результат в stdin.
- 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). - Файловая система только для чтения, временный
tmpfs64 МБ. - Запуск не от 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.
- Переводит их в
InvokeModelBedrock. - Управляет 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 дает максимальный эффект
-
Обработка больших массивов данных.
- Пример из эксперимента: 8 инженеров, у каждого по 20–50 расходов за квартал, каждая запись — 15+ полей.
- С PTC модель не читает тысячи строк расходов. Все вычисления идут в Python внутри песочницы.
-
Точные числовые расчеты и проверки.
- Стандартный travel‑бюджет: $5000 за квартал.
- У некоторых сотрудников — индивидуальные лимиты.
- Задача: найти тех, кто превысил $5000, затем проверить, есть ли у них кастомный лимит, и сравнить с ним.
- Python‑код делает это без ошибок округления и «галлюцинаций».
-
Оркестрация многошаговых бизнес‑процессов.
- Множественные вызовы CRM/ERP/внутренних API.
- Параллельные запросы, сложная логика переходов.
- Легче выразить это кодом, чем длинной цепочкой tool‑вызовов через промпты.
-
Сценарии с повышенными требованиями к приватности.
- Сырые данные (например, расходы сотрудников) остаются в контейнере в вашем 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.
-
Выбрать подход:
- Нужен полный контроль окружения, свои Python‑пакеты, кастомная безопасность — идите в self‑hosted Docker на ECS.
- Хотите минимальной эксплуатации — используйте AgentCore Code Interpreter.
- Уже пишете на Anthropic SDK — поднимите proxy на ECS, который переведет вызовы в Bedrock.
-
Собрать system‑prompt:
- Описать
execute_codeкак инструмент. - Встроить документацию по бизнес‑инструментам (
get_team_members,get_expenses,get_custom_budgetи т.п.) в prompt. - Показать модели примеры корректного и некорректного кода.
- Описать
-
Настроить sandbox:
- Для self‑hosted — собрать Docker‑образ
ptc-sandboxс Python и runner‑скриптом. - Ограничить сеть, ресурсы, права доступа, как в примере
docker runвыше.
- Для self‑hosted — собрать Docker‑образ
-
Реализовать оркестратор:
- Использовать пример кода с
call_bedrockиexecute_in_sandbox. - Добавить реальную реализацию бизнес‑инструментов и обработку IPC‑сообщений.
- Использовать пример кода с
-
Прогнать свой реальный сценарий:
- Взять типичную для вас задачу: аудит расходов, отчет по продажам, агрегация логов.
- Сравнить токены и ошибки с PTC и без него.
Если вы уже строите AI‑агентов на Bedrock, переход к PTC — это не смена стека, а смена архитектурного паттерна. Модель та же, инструменты те же, но теперь между ними — Python‑код, а не цепочка промптов.