Tool use (использование инструментов; в OpenAI исторически называлось «function calling») — это механизм, который позволяет языковой модели не просто разговаривать, а выполнять действия. Без tool use бот может только генерировать текст. С tool use он может добавить задачу в БД, сохранить цель, отправить напоминание — потому что у тебя в коде заранее описаны функции, которые модель умеет вызвать.
Anthropic в оригинале формулирует так: «Tool use позволяет модели вызывать функции, которые вы определяете. Модель решает, когда вызвать tool, на основе запроса пользователя и описания tool'а». Концепция у Claude и OpenAI почти одинаковая — различается только формат JSON. Эта адаптация показывает форму OpenAI, потому что у тебя GPT-5.4-mini.
Шаги 3–5 могут повториться несколько раз, если модель решит, что нужно вызвать ещё один tool на основе результата предыдущего. Это называется агентским циклом.
Для твоего бота нужны как минимум три tool'а:
TOOLS = [
{
"type": "function",
"function": {
"name": "add_task",
"description": (
"Добавить задачу в TODO пользователя. "
"Вызывай, когда пользователь явно просит запомнить задачу "
"или поставить напоминание. Не вызывай, если пользователь "
"просто рассказывает о задаче в прошедшем времени."
),
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Краткое название задачи, 1-7 слов"
},
"due_date": {
"type": "string",
"description": (
"Дата в ISO 8601 (YYYY-MM-DD) или null, "
"если пользователь не указал"
),
"nullable": True
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Приоритет, по умолчанию medium"
}
},
"required": ["title"]
}
}
},
{
"type": "function",
"function": {
"name": "save_goal",
"description": (
"Сохранить долгосрочную цель пользователя. "
"Вызывай только после того, как пользователь подтвердил "
"формулировку цели. Не вызывай при первом упоминании."
),
"parameters": {
"type": "object",
"properties": {
"goal": {
"type": "string",
"description": "Цель одной фразой, до 100 символов"
}
},
"required": ["goal"]
}
}
},
{
"type": "function",
"function": {
"name": "get_user_notes",
"description": (
"Получить заметки о пользователе из БД. "
"Вызывай в начале диалога или когда нужен контекст "
"о предыдущих темах."
),
"parameters": {
"type": "object",
"properties": {},
}
}
},
]
async def chat_with_tools(
user_id: int,
user_text: str,
history: list[dict],
) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
*history,
{"role": "user", "content": user_text},
]
while True:
response = await client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=TOOLS,
)
msg = response.choices[0].message
if not msg.tool_calls:
# Финальный ответ — модель не вызвала tools
return msg.content
# Добавляем сообщение модели в историю
messages.append(msg.model_dump(exclude_none=True))
# Выполняем все вызовы (могут быть параллельные)
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = await execute_tool(
user_id=user_id,
name=call.function.name,
args=args,
)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False),
})
# Цикл — модель увидит результаты и сформулирует ответ
async def execute_tool(user_id: int, name: str, args: dict) -> dict:
if name == "add_task":
await db.add_task(user_id, args["title"], args.get("due_date"),
args.get("priority", "medium"))
return {"ok": True, "message": "Задача добавлена"}
if name == "save_goal":
await db.save_goal(user_id, args["goal"])
return {"ok": True, "message": "Цель сохранена"}
if name == "get_user_notes":
notes = await db.get_user_notes(user_id)
return {"notes": notes}
return {"ok": False, "error": f"unknown tool: {name}"}
Этот цикл — каркас. В проде надо добавить: лимит итераций (чтобы
модель не зациклилась в tool-вызовах), обработку ошибок tool'ов,
логирование, и обновление history после ответа.
Модель решает, вызвать tool или нет, по полю
description. Это значит: описание — это
инструкция, не комментарий. Сравни:
Плохо: "Добавляет задачу"
Хорошо: "Добавить задачу в TODO пользователя. Вызывай, когда
пользователь явно просит запомнить задачу или поставить
напоминание. Не вызывай, если пользователь просто рассказывает
о задаче в прошедшем времени."
Хорошее описание содержит: что делает, когда вызывать, когда НЕ вызывать. Третья часть особенно важна — без неё модель будет «на всякий случай» вызывать tool там, где этого не нужно.
Если у тебя priority может быть только
low / medium / high — пиши
enum, не «строка». Иначе получишь "среднее" или
"normal", и в коде придётся это разгребать.
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
}
Поле description у параметра — тоже инструкция для
модели. «Дата в ISO 8601 (YYYY-MM-DD)» точнее, чем просто
"type": "string"; модель будет пытаться угадать
формат, и не всегда удачно.
Современные модели (и Claude, и GPT-5) могут вызвать несколько
tools за один шаг. Например: get_user_notes +
add_task в одном ответе. В msg.tool_calls
придёт массив, и важно обработать все — не первый.
Чтобы выполнять параллельные вызовы действительно параллельно (а
не последовательно), используй asyncio.gather:
results = await asyncio.gather(*[
execute_tool(user_id, c.function.name, json.loads(c.function.arguments))
for c in msg.tool_calls
])
for call, result in zip(msg.tool_calls, results):
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False),
})
Anthropic в оригинале отмечает: «если в промпте недостаточно информации, чтобы заполнить обязательные параметры, более умные модели спросят уточнение, а менее умные — попробуют угадать». Это применимо и к OpenAI.
Например, у tool'а add_task обязательное
поле title, а пользователь написал «поставь
напоминание». Без названия задачи. Хорошая модель спросит «о чём
напоминание?»; средняя может догадаться (и угадать неправильно).
Чтобы стимулировать уточнения, добавь в системный промпт:
Если для вызова tool'а недостаточно данных — задай пользователю
уточняющий вопрос, не угадывай параметры.
Иногда нужно гарантированно вызвать конкретный tool. Например, в
самом начале диалога — обязательно
get_user_notes, чтобы модель не работала вслепую.
OpenAI поддерживает это через
tool_choice:
response = await client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=TOOLS,
tool_choice={"type": "function", "function": {"name": "get_user_notes"}},
)
Использовать аккуратно: если злоупотребить — модель будет «нажимать кнопки» там, где лучше было разговаривать.
add_task и
create_reminder, и они почти одно и то же —
модель будет путать, какой вызвать. Один tool на одну задачу.
{"ok": true,
"task_id": 42}). Модель прочитает структуру и сама
сформулирует ответ пользователю.
{"ok": false, "error":
"..."}. Модель должна знать, что произошло, и сказать
пользователю «не получилось сохранить, попробуем позже», а не
молчать.
max_iterations = 5 и логируй, если сработало.
Тесты на tool use — отдельная история. Минимум:
add_task. Если вызывает — описание слишком
«приглашающее».
asyncio.gather.