2.6 · Tool calling: модель вызывает 3 функции
Неделя 2 · Основы LLM · ⏱ ~4 ч · Практика
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»Tool calling (function calling) — механизм, на котором стоят агенты, MCP, ассистенты с доступом к базам и API, и вообще любая интеграция LLM с реальными системами. Все агентные фреймворки — это цикл из этой главы плюс удобства. На собеседовании почти гарантированно спросят: «Как модель вызывает функцию? Кто её выполняет? Что будет, если функция упала?» — и попросят нарисовать цикл.
Модель не выполняет код
Заголовок раздела «Модель не выполняет код»До сих пор модель только возвращала текст или JSON по схеме (глава 2.2), а что с ним делать, решал наш код. Tool calling делает следующий шаг: модель сама решает, что ей нужна функция, и просит её вызвать. Самое важное: модель ничего не вызывает сама. Она генерирует структурированную просьбу: «вызови get_exchange_rate с аргументами {"base": "USD", "quote": "EUR"}». Выполняет наш код, результат мы возвращаем модели следующим сообщением, и она продолжает.
Пример: поставщик магазина выставил счёт за три лампы по 100 USD, хотим знать сумму в EUR.
наш код модель ───────────────────────────────────────────────────────────── messages=[user: "100 USD в EUR × 3?"], tools=[...] ──► ◄── assistant: tool_use get_exchange_rate{USD,EUR} stop_reason = "tool_use" выполняем get_exchange_rate → 0.92 messages += [assistant(tool_use), user(tool_result 0.92)] ──► ◄── assistant: tool_use calculate{"100*0.92*3"} выполняем → 276 messages += [assistant(tool_use), user(tool_result 276)] ──► ◄── assistant: "100 USD ≈ 92 EUR, ×3 = 276 EUR" stop_reason = "end_turn"Удобная аналогия — RPC, где клиент (модель) только формирует запрос, а сервер (наш код) решает, исполнять ли его. И, как в любом RPC, сервер не доверяет клиенту: аргументы валидируются, опасные действия запрещены или требуют подтверждения.
Определение инструмента
Заголовок раздела «Определение инструмента»Чтобы модель могла попросить инструмент, она должна знать, что он есть. Список инструментов передаётся в каждом запросе. Разберём формат на API Claude — у него самая наглядная структура; вариант DeepSeek и Ollama — в разделе «DeepSeek и Ollama» ниже. У Anthropic инструмент — это имя, описание и JSON Schema входа:
TOOLS = [ { "name": "get_exchange_rate", "description": ( "Get the latest official exchange rate between two fiat currencies " "(ECB reference rates, updated once per working day). " "Use it for any currency conversion. Returns only the latest rate, not historical ones." ), "input_schema": { "type": "object", "properties": { "base": {"type": "string", "description": "ISO 4217 code, e.g. 'USD'"}, "quote": {"type": "string", "description": "ISO 4217 code, e.g. 'EUR'"}, }, "required": ["base", "quote"], "additionalProperties": False, }, "strict": True, # arguments are guaranteed to match the schema },]У OpenAI (Responses API) почти то же: {"type": "function", "name": ..., "description": ..., "parameters": {...}, "strict": True}.
Имя и описание — это промпт. Модель выбирает инструмент только по ним. Хорошее описание отвечает на вопросы: что делает, когда использовать, когда не использовать, что возвращает, какие ограничения (обновляется раз в рабочий день, отдаёт только последний курс). Плохое: "name": "tool1", "description": "rates".
Цикл (agentic loop)
Заголовок раздела «Цикл (agentic loop)»Обмен со схемы выше — это цикл: отправляем запрос, выполняем инструменты, о которых попросила модель, возвращаем результаты и повторяем, пока модель не ответит текстом. Его называют agentic loop — тот самый цикл, на котором стоят агенты. Иллюстративно, в стиле Python SDK Anthropic:
MAX_ITERATIONS = 5
def run(question: str) -> str: messages = [{"role": "user", "content": question}] for _ in range(MAX_ITERATIONS): resp = client.messages.create( model=MODEL, max_tokens=2048, tools=TOOLS, messages=messages, ) messages.append({"role": "assistant", "content": resp.content}) # keep all blocks
if resp.stop_reason != "tool_use": return "".join(b.text for b in resp.content if b.type == "text")
results = [] for block in resp.content: if block.type == "tool_use": results.append(execute(block)) # returns a tool_result dict messages.append({"role": "user", "content": results}) # ALL results in ONE message raise RuntimeError("tool loop did not finish in time")И результат инструмента. DISPATCH — словарь «имя инструмента → Python-функция», validate — наша проверка аргументов (о ней — в разделе про безопасность):
def execute(block) -> dict: try: output = DISPATCH[block.name](**validate(block.name, block.input)) return {"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(output)} except Exception as e: # tool failure is data for the model return {"type": "tool_result", "tool_use_id": block.id, "content": f"Error: {e}", "is_error": True}Ключевые детали:
tool_use_idв результате обязан совпадать сidблокаtool_use;- в историю кладётся весь
resp.content(там могут быть текст, thinking и несколькоtool_use); - результаты всех вызовов одного хода — в одном сообщении
user; - лимит итераций обязателен: модель может зациклиться (повторять один и тот же вызов, получая ту же ошибку).
У OpenAI цикл тот же, другие имена: в response.output приходят элементы {"type": "function_call", "name", "arguments" (JSON-строка), "call_id"}, а результат добавляется во вход как {"type": "function_call_output", "call_id": ..., "output": "..."}. Обратим внимание: у OpenAI arguments — строка, её надо распарсить json.loads.
Параллельные вызовы
Заголовок раздела «Параллельные вызовы»В примере с лампами вызовы шли по одному. Но если инструменты независимы («курс USD/EUR и время в Токио»), модель может вернуть несколько tool_use в одном ответе. Мы выполняем их (можно параллельно — asyncio.gather) и возвращаем все результаты одним сообщением. Если разбить результаты на несколько сообщений, модель «отучивается» от параллельных вызовов. Последовательные зависимые вызовы (сначала курс, потом умножение) естественно идут в разных итерациях цикла.
tool_choice
Заголовок раздела «tool_choice»По умолчанию модель сама решает, звать ли инструмент. Параметр tool_choice управляет, обязана ли она это сделать:
| Значение | Смысл |
|---|---|
auto (по умолчанию) |
Модель решает сама: вызвать инструмент или ответить текстом |
none |
Запретить вызовы (инструменты видны, но не используются) |
any |
Обязательно вызвать какой-то инструмент |
tool + имя |
Обязательно вызвать конкретный инструмент |
Важно: на самых новых моделях Claude (например, Opus 5.5, Sonnet 5.5) принудительные режимы any / tool не поддерживаются и возвращают 400. Там используют auto + явную инструкцию в промпте («используй инструмент X») и strict: true для гарантии формата аргументов; если вызов был нужен только ради JSON — structured outputs (гл. 2.2). На моделях подешевле и постарше принудительный выбор работает. У OpenAI тоже есть tool_choice (auto/none/required/конкретная функция) и флаг parallel_tool_calls.
Ошибки инструментов — это данные
Заголовок раздела «Ошибки инструментов — это данные»Вернёмся к execute(): блок except в нём не случаен. Если API курсов вернул 500 или модель передала "quote": "EURO", цикл ронять нельзя. Возвращаем tool_result с is_error: true и понятным текстом: «Unknown currency code ‘EURO’. Use ISO 4217 codes like ‘EUR’». Хорошие модели исправляются и повторяют вызов. Плохой текст ошибки (Error 22) — модель гадает или сдаётся.
Безопасность: сервер не доверяет клиенту
Заголовок раздела «Безопасность: сервер не доверяет клиенту»Аргументы приходят от модели, а модель читает внешний текст, и в нём могут оказаться чужие инструкции (prompt injection, глава 2.1). Поэтому:
- Валидируем аргументы перед выполнением: типы (
strictпомогает), диапазоны, белые списки (валюты — только из известного списка; часовой пояс — только валидный IANA). - Никакого
eval()для калькулятора: модель (или injection в данных) пришлёт__import__('os').system(...). Выражение парсим безопасно (модульastс белым списком операций) или описываем инструмент как{"a": number, "op": "+|-|*|/", "b": number}. - Минимум прав. Инструмент делает ровно одно дело; у агента нет инструментов, которые ему не нужны.
- Необратимые действия — только с подтверждением человека, а лучше вообще без них. Для нашего магазина: модель получает только функции на чтение — никаких инструментов, которые меняют заказ, оформляют возврат или выдают промокод, и никаких запросов с ключами к платёжному провайдеру — ни в учебных, ни в «демо» проектах. Публичные данные (курсы валют, время) — можно.
- Таймауты на каждый внешний вызов, чтобы зависший API не повесил агента.
Как писать инструменты, которые модель использует хорошо
Заголовок раздела «Как писать инструменты, которые модель использует хорошо»Безопасность отвечает на вопрос «что инструмент может сделать», остаётся вопрос «будет ли модель им правильно пользоваться». Из статьи Anthropic о проектировании инструментов для агентов — главное:
- меньше, но более осмысленных инструментов лучше, чем обёртка над каждым эндпоинтом;
- понятные имена, при множестве инструментов — с префиксом-неймспейсом (
fx_get_rate,time_now); - возвращать то, что нужно модели, а не сырой ответ API на 5 000 токенов; человекочитаемые поля вместо внутренних ID;
- ошибки — с подсказкой, как исправить;
- проверять инструменты evals: смотреть, какие вызовы модель делает и где ошибается.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Механика та же — модель просит, наш код выполняет. Но формат — Chat Completions (как у OpenAI), а не блоки Anthropic, и у DeepSeek есть две ловушки, связанные с режимом размышления.
Цикл на DeepSeek (Chat Completions)
Заголовок раздела «Цикл на DeepSeek (Chat Completions)»Инструмент описывается в обёртке {"type": "function", "function": {...}}, вызов приходит в message.tool_calls, результат возвращается сообщением с ролью tool и tool_call_id. Здесь execute(name, args) принимает имя и уже распарсенные аргументы и возвращает строку: результат в JSON или текст ошибки. Иллюстративно, по гайдам Tool Calls и Thinking Mode (13.11.2026):
TOOLS_OAI = [ {"type": "function", "function": { "name": "get_exchange_rate", "description": "Get the latest official ECB exchange rate between two fiat currencies. " "Use it for any currency conversion. Returns only the latest rate.", "parameters": { "type": "object", "properties": { "base": {"type": "string", "description": "ISO 4217 code, e.g. 'USD'"}, "quote": {"type": "string", "description": "ISO 4217 code, e.g. 'EUR'"}, }, "required": ["base", "quote"], "additionalProperties": False, }, }}, # ... calculate, get_time]
def run(question: str) -> str: messages = [{"role": "user", "content": question}] for _ in range(MAX_ITERATIONS): r = deepseek.chat.completions.create( model="deepseek-flash", messages=messages, tools=TOOLS_OAI, max_tokens=2048, ) choice = r.choices[0] msg = choice.message # Append the whole assistant message: content, tool_calls AND reasoning_content. # With tools in thinking mode, dropping reasoning_content of earlier turns -> 400. messages.append(msg) if choice.finish_reason != "tool_calls": return msg.content or "" for call in msg.tool_calls: # may be several (parallel) args = json.loads(call.function.arguments) # a JSON string, like OpenAI messages.append({ "role": "tool", "tool_call_id": call.id, # must match the call "content": execute(call.function.name, args), # str; errors as text }) raise RuntimeError("tool loop did not finish in time")Отличия от Claude, которые надо держать в голове:
- Нет
is_error. В Chat Completions у сообщенияtoolесть только текст. Ошибку пишем вcontentявно:"ERROR: Unknown currency code 'EURO'. Use ISO 4217 codes like 'EUR'." - Каждый результат — отдельное сообщение
tool, а не все в одномuser, как у Claude. Это не противоречит правилу «все результаты одного хода вместе»: здесь они идут подряд, сразу после сообщенияassistantс вызовами. reasoning_contentназад обязателен. В режиме размышления (он по умолчанию) с инструментами рассуждение всех прошлых ходов надо возвращать в истории; иначе 400. Проще всего класть в историю сам объектmessage, как в коде выше (так делает и документация DeepSeek). Если dict собирается руками — нельзя забыть это поле.tool_choice:none/auto/required/ конкретная функция — ноrequiredи конкретная функция в режиме размышления дают 400. Чтобы принудить вызов, выключаем размышление (extra_body={"thinking": {"type": "disabled"}}). Знакомо? У новых Claude принудительный выбор запрещён всегда, у DeepSeek — только в режиме размышления.- Strict — бета: клиент с
base_url="https://api.deepseek.com/beta"и"strict": trueу функции; урезанное подмножество JSON Schema (все поля вrequired,additionalProperties: false; подробности — глава 2.2). - У DeepSeek есть и встроенные серверные инструменты (
deepseek_web_search,deepseek_read_urlв типахtools) — аналог серверных инструментов Claude. В этой главе они не нужны: цель — свой цикл.
Ollama: tools у маленьких моделей
Заголовок раздела «Ollama: tools у маленьких моделей»Через OpenAI-совместимый эндпоинт код выше работает почти без изменений (меняем клиента и модель; tool_choice там не поддерживается). У нативной библиотеки ollama удобная особенность — можно передать Python-функции прямо в tools, схема построится из сигнатуры и docstring (Tool calling):
def get_time(timezone: str) -> str: """Current time in an IANA timezone, e.g. 'Asia/Tokyo'.""" ...
resp = ollama.chat(model="qwen3:4b", messages=messages, tools=[get_time, calculate], think=False)messages.append(resp.message)for call in resp.message.tool_calls or []: result = DISPATCH[call.function.name](**call.function.arguments) # already a dict here messages.append({"role": "tool", "tool_name": call.function.name, "content": str(result)})Особенности: аргументы в нативном API — уже словарь, а не строка; результат отдаётся с tool_name; модели нужна метка tools в каталоге. Главное — качество: 4B-модель (дефолт курса, подходит для машин с 8 ГБ памяти) заметно чаще выбирает не тот инструмент, путает аргументы или «отвечает из головы». Это не повод отказаться от неё, а повод для хороших описаний, валидации и лимита итераций — ровно то, чему учит глава.
Сравнение
Заголовок раздела «Сравнение»| Claude | OpenAI (Responses) | DeepSeek | Ollama | |
|---|---|---|---|---|
| Определение | name, description, input_schema |
type: function, parameters |
{"type":"function","function":{...parameters}} |
как OpenAI; нативно — можно Python-функции |
| Вызов в ответе | блоки tool_use, stop_reason="tool_use" |
items function_call |
message.tool_calls, finish_reason="tool_calls" |
message.tool_calls |
| Аргументы | объект input |
JSON-строка | JSON-строка | совместимо — строка; нативно — dict |
| Результат | tool_result в одном user |
function_call_output |
сообщения tool с tool_call_id |
tool с tool_call_id / tool_name |
| Флаг ошибки | is_error |
нет | нет — текст | нет — текст |
| Принудительный вызов | на новейших — 400 | required / функция |
required / функция, но в режиме размышления — 400 |
tool_choice не поддерживается (совместимо) |
| Strict | да | да | бета-эндпоинт | не документирован |
| Параллельные вызовы | да | да, parallel_tool_calls |
несколько tool_calls в ответе |
да (зависит от модели) |
| Особое | thinking-блоки в истории | — | reasoning_content в истории обязателен с tools |
качество 4B-модели ниже — валидируем всё |
Что брать для практики
Заголовок раздела «Что брать для практики»- Основной цикл — DeepSeek
deepseek-flashв режиме размышления по умолчанию: сразу столкнёмся сreasoning_contentв истории — это ценный опыт. - Второй прогон сценариев — Ollama
qwen3:4b: увидим, как маленькая модель ошибается в выборе инструментов (шаг с плохими описаниями будет особенно показателен). - Инструменты в этой главе публичные и безопасные (калькулятор, курсы ЕЦБ, время) — их можно гонять через DeepSeek. Когда инструменты начнут читать базу магазина (глава 3.3 · Проект 1: tool calling + Telegram), всё, что они возвращают, тоже уходит в модель: снимок с персональными данными покупателей — только в Ollama, в облако — только демо-база.
Маршрут изучения
Заголовок раздела «Маршрут изучения»- Anthropic: Tool use (≈1 ч). Читаем обзор (как работает цикл, клиентские и серверные инструменты), страницы про реализацию: определение инструментов,
tool_choice, параллельные вызовы, обработку результатов и ошибок (is_error), strict tool use. Обратим внимание на правила форматированияtool_resultи на то, что в SDK есть готовый «tool runner» (бета), который крутит цикл за нас — сегодня пишем цикл руками, чтобы понять механику. - OpenAI: Function calling (≈30 мин). Сравниваем:
function_call/function_call_output,call_id,argumentsкак строка,strict,parallel_tool_calls. Берём: таблицу соответствия терминов Anthropic ↔ OpenAI. - Anthropic: Writing effective tools for agents (≈30 мин). Главное: как выбирать набор инструментов, называть их, возвращать «полезный контекст», писать описания и ошибки, и как оценивать инструменты.
- Для калькулятора: документация модуля
ast(≈10 мин) —ast.parse(expr, mode="eval")и обход узловBinOp,UnaryOp,Constant. Для времени — модульzoneinfo(≈5 мин). - Публичный API курсов валют без ключа — например, Frankfurter (frankfurter.dev; старый домен frankfurter.app перенаправляет туда), курсы ЕЦБ (≈5 мин): находим в его документации эндпоинт последнего курса и формат ответа. Это публичный неподписанный запрос — никаких ключей.
- DeepSeek (≈30 мин): Tool Calls (цикл, strict mode), в Thinking Mode — раздел про инструменты и возврат
reasoning_content, в Create Chat Completion —toolsиtool_choiceс ограничениями в режиме размышления. Берём: что делать, чтобы не получить 400. - Ollama (≈15 мин): Tool calling — передача функций, параллельные вызовы, агентный цикл, стриминг с инструментами. Берём: формат сообщения
toolсtool_name. - Практика (≈2 ч).
На что обратить внимание
Заголовок раздела «На что обратить внимание»- Не положили
tool_useв историю передtool_result→ 400: результат без вызова. - Несовпадение
tool_use_id→ 400 или модель путает результаты. - Результаты параллельных вызовов в разных сообщениях → модель перестаёт вызывать параллельно; часть результатов может «потеряться».
- Нет лимита итераций → бесконечный цикл и счёт. Каждую итерацию и её стоимость логируем.
max_tokensво время вызова. Еслиstop_reason == "max_tokens"и в ответе неполныйtool_use— выполнять его нельзя.- Модель «отвечает из головы» вместо вызова инструмента (особенно с курсами валют — она помнит примерные). Лечится описанием («всегда используй для конвертации: курсы в памяти модели устарели») и системным промптом; на моделях, где можно, —
tool_choice. - Слишком большой результат инструмента съедает контекст и деньги. Обрезаем и форматируем.
- Числа. Модели неплохо, но не идеально считают в уме. Если есть калькулятор, требуем использовать его для любой арифметики.
- JSON аргументов парсим библиотекой (
json.loads/ объект SDK), никогда не сравниваем строки — экранирование у разных моделей разное.
Практика
Заголовок раздела «Практика»exercises/week-02/tools/: tools.py (определения + реализации), loop.py (цикл), run_cases.py (сценарии). Все внешние вызовы — только публичные, без ключей, с таймаутом.
- 1. Калькулятор.
calculate(expression: str) -> floatнаastс белым списком: числа,+ - * / **, скобки, унарный минус. Тесты:"2+2*2" == 6,"__import__('os')"→ValueError,"10**100**100"→ отказ (ограничиваем степень). - 2. Курс валют.
get_exchange_rate(base, quote) -> dictчерезhttpx.get(..., timeout=10)к публичному API. Валидация: коды — 3 заглавные буквы, из списка поддерживаемых API валют. Возвращаем компактно:{"base": "USD", "quote": "EUR", "rate": 0.92, "date": "2026-11-13"}. - 3. Время.
get_time(timezone: str) -> dictнаzoneinfo:{"timezone": "Asia/Tokyo", "iso": "...", "weekday": "Wednesday"}. Невалидный пояс → понятная ошибка с примером валидного. - 4. Схемы трёх инструментов с хорошими описаниями (когда использовать / когда нет / формат) и
strict: true(у DeepSeek он действует только на бета-эндпоинте — шаг 8б; на обычном валидацию делает наш код). - 5. Цикл
run(question)с лимитом 5 итераций, логированием каждой итерации (какие инструменты, аргументы, результат, токены, стоимость черезestimate_cost). - 6. Сценарии в
run_cases.py; для каждого сохраняем трассу вызовов:- «Сколько будет 17.5% от 2340?» → только калькулятор;
- «Который час в Токио и в Нью-Йорке?» → ожидаем два параллельных
get_timeв одном ходе; - «Поставщик продаёт лампу за 100 USD. Сколько в EUR стоят три лампы?» → курс, затем калькулятор (две итерации);
- «Привет, как дела?» → без инструментов.
- 7. Ошибка инструмента. Временно делаем так, чтобы
get_exchange_rateдляEURбросал исключение «Upstream API timeout». Возвращаем модели ошибку (у DeepSeek — текст с префиксомERROR:, у Claude —is_error: true). Смотрим: что модель с ней делает? Повторяет? Сообщает пользователю? Потом возвращаем ошибку с подсказкой («try again later or ask the user») — изменилось ли поведение? - 8. Плохие vs хорошие описания. Заменяем описания на плохие (
"name": "t2", "description": "rate"и т. п.), прогоняем сценарии из шага 6. Записываем: где модель ошиблась в выборе инструмента или ответила из головы. Возвращаем хорошие. - 8б. Ловушки DeepSeek. (а) Собираем историю руками без
reasoning_contentв режиме размышления — получаем 400 и записываем текст ошибки. (б) Ставимtool_choice="required"при включённом размышлении — 400; выключаем размышление — работает. (в) Включаем strict на бета-эндпоинте дляget_exchange_rate. - 8в. Ollama. Прогоняем сценарии шага 6 на Ollama (
qwen3:4b, нативный API, функции переданы напрямую). Таблица: сценарий → DeepSeek верно? → Ollama верно? → какие ошибки у маленькой модели. - 9. (Опционально, для сравнения) Claude и OpenAI. Тот же цикл на Anthropic Messages (блоки
tool_use/tool_result,is_error) и на Responses API — отмечаем отличия в формате. Пробуем принудительныйtool_choiceна новейшей модели Claude — 400. - 10. Заметка
notes/tool-calling.md: схема цикла, 5 правил хороших инструментов, что мы увидели в шагах 7–8.
Готово, когда
Заголовок раздела «Готово, когда»- Модель (DeepSeek) сама выбирает нужный инструмент во всех сценариях шага 6 и правильно комбинирует два инструмента в вопросе про три лампы по 100 USD; для Ollama записано, где она ошибается.
- Цикл на DeepSeek корректно работает в режиме размышления (
reasoning_contentвозвращается в историю). - Цикл ограничен 5 итерациями, ошибки инструментов возвращаются модели как данные (
ERROR:вcontentу DeepSeek,is_errorу Claude), аргументы валидируются,eval()нигде нет. - Трассы вызовов и выводы о плохих/хороших описаниях записаны.
Тест для самопроверки
Заголовок раздела «Тест для самопроверки»Вопрос 1
Кто выполняет функцию при tool calling — модель или код приложения? Что именно генерирует модель?
Код приложения. Модель генерирует структурированный запрос на вызов: имя инструмента и аргументы по схеме.
Вопрос 2
Какой stop_reason означает, что модель ждёт результата инструмента?
b.
Вопрос 3
Модель вернула два блока tool_use в одном ответе. Как правильно вернуть результаты?
Выполнить оба, затем добавить в историю весь ответ assistant и одно сообщение user с двумя tool_result, у каждого свой tool_use_id.
Вопрос 4
Что не так в этом коде?
def calculator(expression: str) -> str: return str(eval(expression))eval исполняет произвольный Python: модель или prompt injection в данных может выполнить любой код. Нужен безопасный парсер на ast с белым списком операций и ограничениями.
Вопрос 5
Что не так в этом цикле?
while True: resp = client.messages.create(model=M, max_tokens=1024, tools=TOOLS, messages=messages) if resp.stop_reason == "end_turn": break for b in resp.content: if b.type == "tool_use": messages.append({"role": "user", "content": [ {"type": "tool_result", "tool_use_id": b.id, "content": run_tool(b)}]})Не добавляется ответ assistant с tool_use в историю (API вернёт ошибку); результаты разных вызовов идут разными сообщениями; нет лимита итераций; не обработаны max_tokens/другие причины остановки (цикл крутится вечно); исключения инструмента не перехватываются.
Вопрос 6
Инструмент упал с исключением. Что лучше: прервать цикл или вернуть модели ошибку? Как?
Вернуть модели tool_result с is_error: true и понятным текстом ошибки с подсказкой; модель может исправить аргументы, попробовать иначе или честно сообщить пользователю.
Вопрос 7
Почему описание инструмента важнее его кода с точки зрения модели?
Модель не видит код — только имя, описание и схему. По ним она решает, вызывать ли инструмент и с какими аргументами.
Вопрос 8
Самой новой модели Claude передали tool_choice={"type": "tool", "name": "get_time"} и получили 400. Почему и что делать?
На новейших моделях Claude принудительный tool_choice (tool/any) не поддерживается. Использовать auto + явную инструкцию в промпте и strict: true, либо structured outputs, если нужен только JSON.
Вопрос 9
Почему инструмент «оформить возврат по заказу» нельзя давать агенту даже в демо, по правилам этого курса — и какой общий принцип за этим стоит?
Аргументы приходят от модели, которая может ошибиться или поддаться prompt injection, а действие необратимо и связано с деньгами. Принцип: минимум прав, модели — только функции на чтение; необратимые действия (изменить заказ, оформить возврат, выдать промокод) — не через LLM или только с явным подтверждением человека (а в этом курсе — никогда).
Вопрос 10
Чем отличаются arguments у OpenAI от input у Anthropic с точки зрения кода?
У OpenAI arguments — JSON-строка, её надо json.loads; у Anthropic input — уже разобранный объект. В обоих случаях после этого аргументы валидируются.
Вопрос 11
Зачем лимит итераций, если модель «и так закончит»?
Модель может зациклиться (повторять вызов с той же ошибкой, «уточнять» бесконечно); лимит защищает от бесконечного счёта и зависаний.
Вопрос 12
Что не так в этом цикле для DeepSeek (режим размышления по умолчанию)?
msg = r.choices[0].messagemessages.append({"role": "assistant", "content": msg.content, "tool_calls": msg.tool_calls})for c in msg.tool_calls: messages.append({"role": "tool", "tool_call_id": c.id, "content": run_tool(c)})В историю не попал reasoning_content — с инструментами в режиме размышления это 400 на следующем запросе (проще класть сам объект msg). Ещё: не проверен finish_reason, нет лимита итераций, аргументы не распарсены и не провалидированы, исключения run_tool не перехвачены.
Вопрос 13
Как вернуть модели ошибку инструмента у DeepSeek, если поля is_error нет?
Текстом в content сообщения tool: явный префикс вроде ERROR: плюс понятное объяснение и подсказка, как исправить.
Вопрос 14
Нужно заставить DeepSeek обязательно вызвать get_time. Что будет с tool_choice={"type": "function", "function": {"name": "get_time"}} по умолчанию и как это починить?
b. Принудительный выбор (required или конкретная функция) в режиме размышления даёт 400; без размышления работает.
Что дальше
Заголовок раздела «Что дальше»Последняя глава недели — [2.7 · Стриминг, ретраи, rate limits, prompt caching (гл. 2.7)](/week-02/2-7/): превратим свои скрипты в надёжную мини-библиотеку llm_utils.py, на которой будут стоять все проекты, включая агентов с инструментами.