Урок 3. Структурированный вывод через инструменты

Один из самых неожиданных способов применять инструменты — заставить Claude отвечать строго структурированным содержимым, например JSON. Ситуаций море: извлечь сущности из текста, свести данные в таблицу, оценить тональность, классифицировать документ.

Можно просто попросить: «ответь в JSON». Обычно сработает — но потом придётся выковыривать JSON из большой строки с текстом вокруг и каждый раз проверять, что формат ровно тот, который вы ждали. Модель может добавить вступление, обернуть ответ в markdown-блок, переименовать поле, вернуть число строкой.

Хорошая новость: когда Claude хочет вызвать инструмент, он уже отвечает в том самом строгом формате, который мы задали в определении инструмента. Осталось этим воспользоваться.

Идея: инструмент как схема

В прошлом уроке мы дали Claude калькулятор. Когда он захотел его вызвать, в ответе пришло вот такое:

{
    'operand1': 1984135,
    'operand2': 9343116,
    'operation': 'multiply'
}

Подозрительно похоже на JSON, правда?

Если нам нужен структурированный JSON — достаточно описать инструмент, который задаёт нужную форму данных, и рассказать о нём Claude. Всё. Claude ответит, думая, что «вызывает инструмент», а нас интересует только сам структурированный ответ.

Чем это отличается от прошлого урока? Там мы давали Claude доступ к инструменту, Claude хотел его вызвать — и мы действительно вызывали функцию под ним. Здесь мы «обманываем» Claude: рассказываем про инструмент, но никакой функции за ним нет. Инструмент нужен только как способ навязать форму ответа.

Квиз 1

Почему определение инструмента даёт более надёжный JSON, чем просьба «ответь в JSON» обычным текстом?

Пример: анализ тональности

Начнём с простого. Допустим, мы хотим, чтобы Claude оценил тональность текста и вернул объект такой формы:

{
  "negative_score": 0.6,
  "neutral_score": 0.3,
  "positive_score": 0.1
}

Всё, что нужно — описать эту форму через JSON Schema в определении инструмента:

tools = [
    {
        "name": "print_sentiment_scores",
        "description": "Prints the sentiment scores of a given text.",
        "input_schema": {
            "type": "object",
            "properties": {
                "positive_score": {"type": "number", "description": "The positive sentiment score, ranging from 0.0 to 1.0."},
                "negative_score": {"type": "number", "description": "The negative sentiment score, ranging from 0.0 to 1.0."},
                "neutral_score": {"type": "number", "description": "The neutral sentiment score, ranging from 0.0 to 1.0."}
            },
            "required": ["positive_score", "negative_score", "neutral_score"]
        }
    }
]

Теперь рассказываем Claude про инструмент и явно просим им воспользоваться — чтобы он точно его вызвал:

from anthropic import Anthropic
from dotenv import load_dotenv
import json

load_dotenv()
client = Anthropic()

tweet = "I'm a HUGE hater of pickles.  I actually despise pickles.  They are garbage."

query = f"""
<text>
{tweet}
</text>

Only use the print_sentiment_scores tool.
"""

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=4096,
    tools=tools,
    messages=[{"role": "user", "content": query}]
)

В ответе приходит сообщение со stop_reason='tool_use', а внутри — блок ToolUseBlock. Важная часть — его поле input:

input={'positive_score': 0.0, 'negative_score': 0.791, 'neutral_score': 0.209}

Claude «думает», что вызывает инструмент, который дальше как-то использует эти оценки. На самом деле мы просто вынимаем данные и превращаем их в JSON:

import json
json_sentiment = None
for content in response.content:
    if content.type == "tool_use" and content.name == "print_sentiment_scores":
        json_sentiment = content.input
        break

if json_sentiment:
    print("Sentiment Analysis (JSON):")
    print(json.dumps(json_sentiment, indent=2))
else:
    print("No sentiment analysis found in the response.")

Работает. Осталось завернуть это в переиспользуемую функцию, которая принимает твит или статью и возвращает разбор тональности:

def analyze_sentiment(content):

    query = f"""
    <text>
    {content}
    </text>

    Only use the print_sentiment_scores tool.
    """

    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=4096,
        tools=tools,
        messages=[{"role": "user", "content": query}]
    )

    json_sentiment = None
    for content in response.content:
        if content.type == "tool_use" and content.name == "print_sentiment_scores":
            json_sentiment = content.input
            break

    if json_sentiment:
        print("Sentiment Analysis (JSON):")
        print(json.dumps(json_sentiment, indent=2))
    else:
        print("No sentiment analysis found in the response.")

Дальше можно скормить ей что угодно: analyze_sentiment("OMG I absolutely love taking bubble baths soooo much!!!!") или analyze_sentiment("Honestly I have no opinion on taking baths").

Принуждение через tool_choice

Пока мы «заставляем» Claude вызвать print_sentiment_scores промптом: пишем Only use the print_sentiment_scores tool. Обычно это срабатывает, но есть способ надёжнее — параметр tool_choice:

tool_choice={"type": "tool", "name": "print_sentiment_scores"}

Так мы говорим Claude, что он обязан ответить вызовом именно этого инструмента. Добавляем в функцию:

    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=4096,
        tools=tools,
        tool_choice={"type": "tool", "name": "print_sentiment_scores"},
        messages=[{"role": "user", "content": query}]
    )

Подробнее tool_choice разберём в одном из следующих уроков.

Квиз 2

Что делает tool_choice={"type": "tool", "name": "print_sentiment_scores"}?

Пример: извлечение сущностей

Тот же приём — но теперь достаём из текста людей, организации и локации:

tools = [
    {
        "name": "print_entities",
        "description": "Prints extract named entities.",
        "input_schema": {
            "type": "object",
            "properties": {
                "entities": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string", "description": "The extracted entity name."},
                            "type": {"type": "string", "description": "The entity type (e.g., PERSON, ORGANIZATION, LOCATION)."},
                            "context": {"type": "string", "description": "The context in which the entity appears in the text."}
                        },
                        "required": ["name", "type", "context"]
                    }
                }
            },
            "required": ["entities"]
        }
    }
]

text = "John works at Google in New York. He met with Sarah, the CEO of Acme Inc., last week in San Francisco."

query = f"""
<document>
{text}
</document>

Use the print_entities tool.
"""

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=4096,
    tools=tools,
    messages=[{"role": "user", "content": query}]
)

json_entities = None
for content in response.content:
    if content.type == "tool_use" and content.name == "print_entities":
        json_entities = content.input
        break

if json_entities:
    print("Extracted Entities (JSON):")
    print(json.dumps(json_entities, indent=2))
else:
    print("No entities found in the response.")

Приём тот же: рассказываем Claude про инструмент, чтобы получить ответ нужной формы, потом вынимаем данные — и готово. И помните: в таком сценарии полезно явно сказать, что мы хотим вызова конкретного инструмента — Use the print_entities tool.

Пример посложнее: разбор статьи

Возьмём задачу поинтереснее. С помощью Python-пакета wikipedia вытянем целую статью и попросим Claude вернуть:

  • основной предмет статьи;
  • краткое содержание;
  • список ключевых слов и тем;
  • список категорий (развлечения, политика, бизнес и т. д.) с оценкой, насколько сильно статья относится к каждой.

Для статьи про Уолта Диснея ожидаемый результат выглядел бы примерно так:

{
  "subject": "Walt Disney",
  "summary": "Walter Elias Disney was an American animator, film producer, and entrepreneur...",
  "keywords": [
    "Walt Disney",
    "animation",
    "film producer",
    "entrepreneur",
    "Disneyland",
    "theme parks",
    "television"
  ],
  "categories": [
    {"name": "Entertainment", "score": 0.9},
    {"name": "Business", "score": 0.7},
    {"name": "Technology", "score": 0.6}
  ]
}

Стратегия прежняя: описываем инструмент, который «подсказывает» форму ответа. Не забудьте pip install wikipedia.

import wikipedia

# определение инструмента
tools = [
    {
        "name": "print_article_classification",
        "description": "Prints the classification results.",
        "input_schema": {
            "type": "object",
            "properties": {
                "subject": {
                    "type": "string",
                    "description": "The overall subject of the article",
                },
                "summary": {
                    "type": "string",
                    "description": "A paragaph summary of the article"
                },
                "keywords": {
                    "type": "array",
                    "items": {
                        "type": "string",
                        "description": "List of keywords and topics in the article"
                    }
                },
                "categories": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string", "description": "The category name."},
                            "score": {"type": "number", "description": "The classification score for the category, ranging from 0.0 to 1.0."}
                        },
                        "required": ["name", "score"]
                    }
                }
            },
            "required": ["subject", "summary", "keywords", "categories"]
        }
    }
]

# функция, которая генерирует JSON по теме статьи
def generate_json_for_article(subject):
    page = wikipedia.page(subject, auto_suggest=True)
    query = f"""
    <document>
    {page.content}
    </document>

    Use the print_article_classification tool. Example categories are Politics, Sports, Technology, Entertainment, Business.
    """

    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=4096,
        tools=tools,
        messages=[{"role": "user", "content": query}]
    )

    json_classification = None
    for content in response.content:
        if content.type == "tool_use" and content.name == "print_article_classification":
            json_classification = content.input
            break

    if json_classification:
        print("Text Classification (JSON):")
        print(json.dumps(json_classification, indent=2))
    else:
        print("No text classification found in the response.")

Проверить можно на чём угодно: generate_json_for_article("Jeff Goldblum"), generate_json_for_article("Octopus"), generate_json_for_article("Herbert Hoover").

Обратите внимание на вложенность: keywords — массив строк, categories — массив объектов со своими required-полями. JSON Schema позволяет описать структуру любой глубины, и Claude будет ей следовать.

Квиз 3

Что мы делаем с ответом Claude в этом приёме?

Упражнения

Упражнение 3.1 — Переводчик

По той же стратегии напишите функцию translate, которая принимает слово или фразу и возвращает структурированный JSON: исходная фраза на английском плюс переводы на испанский, французский, японский и арабский.

Вызов translate("how much does this cost") должен давать примерно такое:

{
  "english": "how much does this cost",
  "spanish": "¿cuánto cuesta esto?",
  "french": "combien ça coûte?",
  "japanese": "これはいくらですか",
  "arabic": "كم تكلفة هذا؟"
}

Подсказка: чтобы результат печатался читаемо, а не в виде escape-последовательностей, используйте print(json.dumps(translations_from_claude, ensure_ascii=False, indent=2)).

Решение упражненияСначала попробуйте сами — потом сверьтесь
tools = [
    {
        "name": "print_translations",
        "description": "Prints the translations of a given phrase.",
        "input_schema": {
            "type": "object",
            "properties": {
                "english": {"type": "string", "description": "The original phrase in English."},
                "spanish": {"type": "string", "description": "The phrase translated into Spanish."},
                "french": {"type": "string", "description": "The phrase translated into French."},
                "japanese": {"type": "string", "description": "The phrase translated into Japanese."},
                "arabic": {"type": "string", "description": "The phrase translated into Arabic."}
            },
            "required": ["english", "spanish", "french", "japanese", "arabic"]
        }
    }
]

def translate(phrase):
    query = f"""
    <phrase>
    {phrase}
    </phrase>

    Use the print_translations tool.
    """

    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=4096,
        tools=tools,
        tool_choice={"type": "tool", "name": "print_translations"},
        messages=[{"role": "user", "content": query}]
    )

    for content in response.content:
        if content.type == "tool_use" and content.name == "print_translations":
            print(json.dumps(content.input, ensure_ascii=False, indent=2))
            return content.input

    print("No translations found in the response.")

Функции print_translations в коде нет и не нужно: мы забираем content.input и работаем с ним как с обычным словарём. tool_choice здесь страхует от ситуации, когда Claude решит ответить просто текстом.

Упражнение 3.2 — Схема для карточки товара

Опишите инструмент, который извлекает из свободного описания товара структуру: название (title), цену числом (price), валюту (currency) и массив характеристик features — каждая с полями name и value. Проверьте на любом тексте объявления.

Решение упражненияСначала попробуйте сами — потом сверьтесь
tools = [
    {
        "name": "print_product_card",
        "description": "Prints structured product data extracted from a description.",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string", "description": "The product title."},
                "price": {"type": "number", "description": "The numeric price of the product."},
                "currency": {"type": "string", "description": "The currency code, e.g. USD, EUR, KZT."},
                "features": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string", "description": "The feature name."},
                            "value": {"type": "string", "description": "The feature value."}
                        },
                        "required": ["name", "value"]
                    }
                }
            },
            "required": ["title", "price", "currency", "features"]
        }
    }
]

def extract_product(description):
    query = f"""
    <document>
    {description}
    </document>

    Use the print_product_card tool.
    """

    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=4096,
        tools=tools,
        tool_choice={"type": "tool", "name": "print_product_card"},
        messages=[{"role": "user", "content": query}]
    )

    for content in response.content:
        if content.type == "tool_use" and content.name == "print_product_card":
            print(json.dumps(content.input, ensure_ascii=False, indent=2))
            return content.input

Ключевое здесь — "type": "number" у цены: так вы получите число, а не строку «1 990 ₸». И описания полей — они работают как инструкции для модели, не ленитесь их писать.

Что запомнить

  • Определение инструмента — это заодно и схема ответа. Вызов инструмента приходит уже в нужной структуре.
  • Функции под инструментом может не быть вообще: нас интересует только поле input у блока tool_use.
  • В промпте полезно явно написать: «используй такой-то инструмент». Надёжнее — параметр tool_choice.
  • JSON Schema описывает вложенные структуры любой глубины: массивы, объекты, обязательные поля.
  • Описания полей в схеме — это инструкции для модели. Чем точнее, тем предсказуемее результат.

Дочитали и сделали упражнения? Зафиксируйте прогресс — отметка сохранится в вашем браузере.

Перевод и адаптация урока «Structured Outputs» курса Tool Use © Anthropic, лицензия CC BY-NC 4.0. Перевод: Дарья Воронкина (@aishipuchka). Материал изменён: переведён на русский, названия моделей актуализированы, добавлены квизы и упражнения. Используется некоммерчески.