Проблема: последовательные вызовы тормозят агента
Представь: пользователь просит «Сравни погоду в Москве, Лондоне и Токио». Агент без parallel tool calling делает это так:
- Первый запрос к API → модель возвращает
get_weather("Москва") - Выполняем функцию → второй запрос → модель возвращает
get_weather("Лондон") - Выполняем функцию → третий запрос → модель возвращает
get_weather("Токио") - Выполняем функцию → четвёртый запрос → финальный ответ
Итого: 4 round-trip к API, время — сумма всех ожиданий. С parallel tool calling та же задача решается за 2 round-trip: в первом ответе модель возвращает все три вызова сразу, ты выполняешь функции параллельно и отдаёшь все результаты в одном сообщении.
Теория: как работает parallel tool calling
Parallel tool calling — не отдельная функция API, которую нужно «включить». Это поведение самой модели: когда она видит, что несколько инструментов можно вызвать независимо друг от друга, она возвращает несколько блоков tool_use в одном ответе вместо одного.
На уровне протокола разница только в одном месте: в ответе модели несколько tool_use блоков, и все их tool_result должны вернуться в одном сообщении с ролью user.
Структура ответа с несколькими tool_use блоками
content: [
{
type: "tool_use",
id: "toolu_01A",
name: "get_weather",
input: { city: "Москва" }
},
type: "tool_use", ← второй блок в том же ответе
id: "toolu_02B",
name: "get_weather",
input: { city: "Лондон" }
},
{
type: "tool_use", ← третий блок
id: "toolu_03C",
name: "get_weather",
input: { city: "Токио" }
}
Структура ответа с несколькими tool_result блоками
Все результаты нужно вернуть в одном сообщении с ролью user. Каждый блок tool_result связан со своим вызовом через tool_use_id. Порядок не важен, но все вызовы должны быть закрыты:
{
type: "tool_result",
tool_use_id: "toolu_01A",
content: '{"city":"Москва","temp":-3,"desc":"Снег"}'
},
{ type: "tool_result", tool_use_id: "toolu_03C", content: '{"city":"Токио","temp":12,"desc":"Ясно"}' }
Если в ответе модели было 3 блока tool_use — все 3 блока tool_result обязаны вернуться в одном сообщении. API вернёт ошибку, если ты отправишь их по одному или пропустишь хотя бы один.
Обработка нескольких tool_use блоков
Код из предыдущего урока уже готов к параллельным вызовам — главное не делать next() для поиска первого блока, а обходить все блоки в цикле. Единственное новое требование: собрать все tool_result и отправить их вместе.
import json
import anthropic
client = anthropic.Anthropic()
def get_weather(city: str) -> dict:
# Заглушка — в реальности HTTP-запрос к weather API
data = {"Москва": (-3, "Снег"), "Лондон": (8, "Дождь"), "Токио": (12, "Ясно")}
temp, desc = data.get(city, (0, "Неизвестно"))
return {"city": city, "temp": temp, "description": desc}
TOOLS = [{
"name": "get_weather",
"description": "Возвращает текущую погоду в указанном городе.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "Название города на русском языке"}
},
"required": ["city"]
}
}]
HANDLERS = {"get_weather": get_weather}
def run_agent(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
for _ in range(10):
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=TOOLS,
messages=messages
)
if response.stop_reason == "end_turn":
return response.content[0].text
if response.stop_reason == "tool_use":
# Собираем результаты ВСЕХ tool_use блоков
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
handler = HANDLERS.get(block.name)
try:
result = handler(**block.input) if handler else {"error": f"unknown tool: {block.name}"}
except Exception as e:
result = {"error": str(e)}
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id, # каждый ID уникален
"content": json.dumps(result, ensure_ascii=False)
})
# Возвращаем ВСЕ результаты в одном сообщении
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
raise RuntimeError("Превышен лимит итераций")
print(run_agent("Сравни погоду в Москве, Лондоне и Токио"))
# Модель сгенерирует 3 tool_use блока за один ответ
Конкурентное выполнение функций через asyncio
Parallel tool calling сокращает количество round-trip к Claude API. Но если сами функции делают HTTP-запросы — они по-прежнему выполняются последовательно в синхронном коде. Чтобы выжать максимум, нужно запускать их конкурентно через asyncio.gather().
import json
import asyncio
import anthropic
client = anthropic.AsyncAnthropic() # async-клиент
# Async-версии инструментов
async def get_weather(city: str) -> dict:
await asyncio.sleep(0) # в реальности: await httpx.AsyncClient().get(...)
data = {"Москва": (-3, "Снег"), "Лондон": (8, "Дождь"), "Токио": (12, "Ясно")}
temp, desc = data.get(city, (0, "Неизвестно"))
return {"city": city, "temp": temp, "description": desc}
ASYNC_HANDLERS = {"get_weather": get_weather}
async def execute_tools_concurrently(tool_use_blocks: list) -> list:
"""Запускаем все функции конкурентно через asyncio.gather"""
async def run_one(block):
handler = ASYNC_HANDLERS.get(block.name)
try:
result = await handler(**block.input) if handler else {"error": f"unknown: {block.name}"}
except Exception as e:
result = {"error": str(e)}
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result, ensure_ascii=False)
}
# Все функции стартуют одновременно — ждём самую медленную
return await asyncio.gather(*[run_one(b) for b in tool_use_blocks])
async def run_agent_async(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
for _ in range(10):
response = await client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=[{
"name": "get_weather",
"description": "Погода в городе.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string", "description": "Город на русском"}},
"required": ["city"]
}
}],
messages=messages
)
if response.stop_reason == "end_turn":
return response.content[0].text
if response.stop_reason == "tool_use":
tool_blocks = [b for b in response.content if b.type == "tool_use"]
# Конкурентное выполнение: все функции параллельно
tool_results = await execute_tools_concurrently(tool_blocks)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
raise RuntimeError("Превышен лимит итераций")
# Запуск
result = asyncio.run(run_agent_async("Сравни погоду в Москве, Лондоне и Токио"))
print(result)
Когда модель использует параллельные вызовы
Модель сама решает — вызывать инструменты последовательно или параллельно. Понимание этой логики помогает писать описания инструментов так, чтобы модель чаще делала правильный выбор.
get_weather × 3 параллельно, если видит в запросе несколько городов.search(), затем summarize(результат).В description инструмента явно укажи, что он не имеет побочных эффектов и не зависит от других вызовов: «Каждый вызов независим. Можно вызывать одновременно с другими инструментами этого типа». Это подсказка модели, что инструменты безопасно выполнять параллельно.
Типичные ошибки
b for b in response.content
if b.type == "tool_use"
)
# обрабатываем только block
tool_use блоки проигнорированы. API получит tool_result только для одного ID — вернёт ошибку.for block in response.content:
if block.type != "tool_use":
continue
tool_results.append(...)
tool_result в одном ответе. API доволен.result = execute(block)
messages.append(
{"role":"user","content":[result]}
)
call_api(messages) # ← отдельный запрос!
tool_result для одного шага обязаны быть в одном сообщении. API вернёт ошибку.for block in tool_use_blocks:
tool_results.append(execute(block))
messages.append({
"role": "user",
"content": tool_results # все сразу
})
result = fetch_data(block.input)
# каждый вызов ждёт предыдущего
# 800 + 600 + 1000 = 2400ms
fetch_data(b.input)
for b in tool_use_blocks
])
# max(800, 600, 1000) = 1000ms
Шпаргалка
response.content — норма. Обходи все блоки циклом, не бери первый через next().role: "user". Каждый блок связан с вызовом через уникальный tool_use_id.AsyncAnthropic() и await везде в цепочке.description инструмента укажи, что вызовы независимы — это помогает модели принять решение о параллельном вызове.Практическое задание
-
Проверь параллельность. Реализуй инструмент
get_weather(city)с искусственной задержкой черезasyncio.sleep(random.uniform(0.5, 1.5)). Запроси агента: «Погода в Москве, Лондоне, Токио, Берлине и Париже». Измерь время синхронной и асинхронной версии черезtime.time(). Сколько выходит ускорение? -
Разные инструменты параллельно. Добавь второй инструмент
get_exchange_rate(currency). Запроси: «Какой курс доллара и евро, и что у них с погодой в столицах?». Посмотри сколько и каких блоковtool_useмодель вернёт за один ответ. Обработай все корректно. -
Зависимые вызовы. Создай два инструмента:
search_user(name)иget_user_orders(user_id). Запроси: «Покажи заказы пользователя Иван Петров». Убедись, что модель вызывает их последовательно (сначала ищет пользователя, потом берёт его ID для заказов), а не параллельно. Сравни с запросом на двух разных пользователей — там ли модель пойдёт параллельно?
Ты освоил весь цикл tool calling: протокол, описание схем и параллельные вызовы. Следующая тема — Обработка ошибок при вызове инструментов: что делать когда функция падает, как передавать ошибки модели и строить устойчивых агентов.