---
title: Определение критериев успеха и создание оценок
url: https://platform.claude.com/docs/ru/test-and-evaluate/develop-tests
description: Определите измеримые критерии успеха для вашего LLM-приложения и создайте оценки для его тестирования — от проверок на точное совпадение до оценивания на основе LLM.
---

Создание успешного приложения на основе «large language model» (большой языковой модели), или LLM, начинается с чёткого определения критериев успеха и последующей разработки оценок для измерения производительности относительно них. Этот цикл занимает центральное место в «prompt engineering» (инженерии подсказок).

<Frame>
  ![Блок-схема инженерии подсказок: test cases (тестовые случаи), preliminary prompt (предварительная подсказка), iterative testing and refinement (итеративное тестирование и доработка), final validation (финальная проверка), ship (выпуск)](https://platform.claude.com/docs/images/how-to-prompt-eng.svg)
</Frame>

## Определите критерии успеха

Хорошие критерии успеха:

* **Конкретные:** Чётко определите, чего вы хотите достичь. Вместо «хорошей производительности» укажите «точную классификацию тональности».

* **Измеримые:** Используйте количественные метрики или чётко определённые качественные шкалы. Числа обеспечивают ясность и масштабируемость, но качественные показатели могут быть ценными, если применяются последовательно *вместе* с количественными.

  * Даже «размытые» темы, такие как этика и безопасность, можно выразить количественно:

    |        | Критерии безопасности                                                                    |
    | ------ | ---------------------------------------------------------------------------------------- |
    | Плохо  | Безопасные выходные данные                                                               |
    | Хорошо | Менее 0,1% выходных данных из 10 000 испытаний помечены фильтром контента как токсичные. |

  <Accordion title="Примеры метрик и методов измерения">
    **Количественные метрики:**

    * Специфичные для задачи: F1-мера, оценка BLEU, перплексия
    * Общие: точность (accuracy), прецизионность (precision), полнота (recall)
    * Операционные: время отклика (мс), время безотказной работы (%)

    **Количественные методы:**

    * A/B-тестирование: сравнение производительности с базовой моделью или более ранней версией.
    * Обратная связь от пользователей: косвенные показатели, такие как доля выполненных задач.
    * Анализ граничных случаев: процент граничных случаев, обработанных без ошибок.

    **Качественные шкалы:**

    * Шкалы Лайкерта: «Оцените связность от 1 (бессмысленно) до 5 (совершенно логично)»
    * Экспертные рубрики: лингвисты оценивают качество перевода по определённым критериям
  </Accordion>

* **Достижимые:** Основывайте свои цели на отраслевых бенчмарках, предыдущих экспериментах, исследованиях в области ИИ или экспертных знаниях. Ваши метрики успеха не должны быть нереалистичными по отношению к текущим возможностям передовых моделей.

* **Релевантные:** Согласуйте критерии с назначением вашего приложения и потребностями пользователей. Высокая точность цитирования может быть критически важна для медицинских приложений, но менее важна для неформальных чат-ботов.

<Accordion title="Пример критериев точности выполнения задачи для анализа тональности">
  |        | Критерии                                                                                                                                                                                                                             |
  | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
  | Плохо  | Модель должна хорошо классифицировать тональность                                                                                                                                                                                    |
  | Хорошо | Модель анализа тональности должна достигать F1-меры не менее 0,85 (Измеримо, Конкретно) на отложенном тестовом наборе\* из 10 000 разнообразных постов в Twitter (Релевантно), что на 5% лучше текущего базового уровня (Достижимо). |

  \*Подробнее об отложенных тестовых наборах в следующем разделе.
</Accordion>

### Распространённые критерии успеха

Вот некоторые критерии, которые могут быть важны для вашего сценария использования. Этот список не является исчерпывающим.

<AccordionGroup>
  <Accordion title="Точность выполнения задачи">
    Насколько хорошо модель должна справляться с задачей? Возможно, вам также потребуется учесть обработку граничных случаев, например, насколько хорошо модель должна работать с редкими или сложными входными данными.
  </Accordion>

  <Accordion title="Согласованность">
    Насколько похожими должны быть ответы модели на схожие типы входных данных? Если пользователь задаёт один и тот же вопрос дважды, насколько важно, чтобы он получил семантически схожие ответы?
  </Accordion>

  <Accordion title="Релевантность и связность">
    Насколько хорошо модель напрямую отвечает на вопросы или инструкции пользователя? Насколько важно, чтобы информация была представлена логично и понятно?
  </Accordion>

  <Accordion title="Тон и стиль">
    Насколько хорошо стиль выходных данных модели соответствует ожиданиям? Насколько её язык подходит для целевой аудитории?
  </Accordion>

  <Accordion title="Сохранение конфиденциальности">
    Какова метрика успеха для того, как модель обращается с личной или конфиденциальной информацией? Может ли она следовать инструкциям не использовать и не раскрывать определённые сведения?
  </Accordion>

  <Accordion title="Использование контекста">
    Насколько эффективно модель использует предоставленный контекст? Насколько хорошо она ссылается на информацию из своей истории и опирается на неё?
  </Accordion>

  <Accordion title="Задержка">
    Каково приемлемое время отклика модели? Это зависит от требований вашего приложения к работе в реальном времени и ожиданий пользователей.
  </Accordion>

  <Accordion title="Цена">
    Каков ваш бюджет на использование модели? Учитывайте такие факторы, как стоимость каждого вызова API, размер модели и частота использования.
  </Accordion>
</AccordionGroup>

Большинство сценариев использования требуют многомерной оценки по нескольким критериям успеха.

<Accordion title="Пример многомерных критериев для анализа тональности">
  |        | Критерии                                                                                                                                                                                                                                                                                              |
  | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | Плохо  | Модель должна хорошо классифицировать тональность                                                                                                                                                                                                                                                     |
  | Хорошо | На отложенном тестовом наборе из 10 000 разнообразных постов в Twitter модель анализа тональности должна достигать:<br />- F1-меры не менее 0,85<br />- 99,5% выходных данных нетоксичны<br />- 90% ошибок вызывают неудобство, а не являются грубыми ошибками\*<br />- 95% времени отклика \< 200 мс |

  \*На практике вы также определили бы, что означают «неудобство» и «грубая ошибка».
</Accordion>

***

## Создайте оценки

### Принципы разработки оценок

1. **Ориентируйтесь на задачу:** Разрабатывайте оценки, отражающие реальное распределение ваших задач. Не забудьте учесть граничные случаи!
   <Accordion title="Примеры граничных случаев">
     * Нерелевантные или отсутствующие входные данные
     * Чрезмерно длинные входные данные или пользовательский ввод
     * \[Сценарии чата] Некачественный, вредоносный или нерелевантный пользовательский ввод
     * Неоднозначные тестовые случаи, в которых даже людям было бы трудно прийти к единому мнению при оценке
   </Accordion>
2. **Автоматизируйте, когда это возможно:** Структурируйте вопросы так, чтобы их можно было оценивать автоматически (например, множественный выбор, совпадение строк, оценивание кодом, оценивание с помощью LLM).
3. **Отдавайте приоритет объёму, а не качеству:** Больше вопросов с автоматическим оцениванием чуть более низкого качества сигнала лучше, чем меньше вопросов с высококачественными оценками, выставленными вручную человеком.

### Примеры оценок

<AccordionGroup>
  <Accordion title="Точность выполнения задачи (анализ тональности) — оценка на точное совпадение">
    **Что измеряется:** Оценки на точное совпадение измеряют, совпадает ли вывод модели с заранее определённым правильным ответом, обычно после нормализации пробелов и регистра. Это простая, однозначная метрика, идеально подходящая для задач с чёткими категориальными ответами, таких как анализ тональности (положительная, отрицательная, нейтральная).

    **Примеры тестовых случаев для оценки:** 1000 твитов с тональностью, размеченной людьми.

    <CodeGroup exclude="shell">
      ```python Python
      tweets = [
          {"text": "This movie was a total waste of time. 👎", "sentiment": "negative"},
          {"text": "The new album is 🔥! Been on repeat all day.", "sentiment": "positive"},
          {
              "text": "I just love it when my flight gets delayed for 5 hours. #bestdayever",
              "sentiment": "negative",
          },  # Edge case: Sarcasm
          {
              "text": "The movie's plot was terrible, but the acting was phenomenal.",
              "sentiment": "mixed",
          },  # Edge case: Mixed sentiment
          # ... ещё 996 твитов
      ]

      client = anthropic.Anthropic()


      def get_completion(prompt: str):
          message = client.messages.create(
              model="claude-opus-5-5",
              max_tokens=1024,
              messages=[{"role": "user", "content": prompt}],
          )
          return next(block.text for block in message.content if block.type == "text")


      def evaluate_exact_match(model_output, correct_answer):
          return model_output.strip().lower() == correct_answer.lower()


      outputs = [
          get_completion(
              f"Classify this as 'positive', 'negative', 'neutral', or 'mixed'. Output only the label: {tweet['text']}"
          )
          for tweet in tweets
      ]
      accuracy = sum(
          evaluate_exact_match(output, tweet["sentiment"])
          for output, tweet in zip(outputs, tweets)
      ) / len(tweets)
      print(f"Sentiment Analysis Accuracy: {accuracy * 100}%")
      ```

      ```typescript TypeScript
      const tweets = [
        { text: "This movie was a total waste of time. 👎", sentiment: "negative" },
        { text: "The new album is 🔥! Been on repeat all day.", sentiment: "positive" },
        {
          text: "I just love it when my flight gets delayed for 5 hours. #bestdayever",
          sentiment: "negative"
        }, // Edge case: Sarcasm
        {
          text: "The movie's plot was terrible, but the acting was phenomenal.",
          sentiment: "mixed"
        } // Edge case: Mixed sentiment
        // ... ещё 996 твитов
      ];

      const client = new Anthropic();

      async function getCompletion(prompt: string): Promise<string> {
        const message = await client.messages.create({
          model: "claude-opus-5-5",
          max_tokens: 1024,
          messages: [{ role: "user", content: prompt }]
        });
        const textBlock = message.content.find((block) => block.type === "text");
        return textBlock ? textBlock.text : "";
      }

      function evaluateExactMatch(modelOutput: string, correctAnswer: string): boolean {
        return modelOutput.trim().toLowerCase() === correctAnswer.toLowerCase();
      }

      let correctCount = 0;
      for (const tweet of tweets) {
        const output = await getCompletion(
          `Classify this as 'positive', 'negative', 'neutral', or 'mixed'. Output only the label: ${tweet.text}`
        );
        if (evaluateExactMatch(output, tweet.sentiment)) {
          correctCount++;
        }
      }
      console.log(`Sentiment Analysis Accuracy: ${(correctCount / tweets.length) * 100}%`);
      ```

      ```csharp C#
      Tweet[] tweets =
      [
          new("This movie was a total waste of time. 👎", "negative"),
          new("The new album is 🔥! Been on repeat all day.", "positive"),
          // Пограничный случай: сарказм
          new("I just love it when my flight gets delayed for 5 hours. #bestdayever", "negative"),
          // Пограничный случай: смешанная тональность
          new("The movie's plot was terrible, but the acting was phenomenal.", "mixed"),
          // ... ещё 996 твитов
      ];

      var client = new AnthropicClient();

      async Task<string> GetCompletion(string prompt)
      {
          var message = await client.Messages.Create(new MessageCreateParams
          {
              Model = Model.ClaudeOpus5_5,
              MaxTokens = 1024,
              Messages = [new() { Role = Role.User, Content = prompt }],
          });
          return ContentText(message);
      }

      bool EvaluateExactMatch(string modelOutput, string correctAnswer)
      {
          return string.Equals(modelOutput.Trim(), correctAnswer, StringComparison.OrdinalIgnoreCase);
      }

      string ContentText(Message message)
      {
          var text = "";
          foreach (var block in message.Content)
          {
              if (block.TryPickText(out var textBlock))
              {
                  text += textBlock.Text;
              }
          }
          return text;
      }

      var correct = 0;
      foreach (var tweet in tweets)
      {
          var output = await GetCompletion(
              $"Classify this as 'positive', 'negative', 'neutral', or 'mixed'. Output only the label: {tweet.Text}");
          if (EvaluateExactMatch(output, tweet.Sentiment))
          {
              correct++;
          }
      }
      Console.WriteLine($"Sentiment Analysis Accuracy: {100.0 * correct / tweets.Length}%");

      record Tweet(string Text, string Sentiment);
      ```

      ```go Go
      var client = anthropic.NewClient()

      func contentText(message *anthropic.Message) string {
      	var text strings.Builder
      	for _, block := range message.Content {
      		if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
      			text.WriteString(textBlock.Text)
      		}
      	}
      	return text.String()
      }

      type tweet struct {
      	Text      string
      	Sentiment string
      }

      var tweets = []tweet{
      	{"This movie was a total waste of time. 👎", "negative"},
      	{"The new album is 🔥! Been on repeat all day.", "positive"},
      	// Пограничный случай: сарказм
      	{"I just love it when my flight gets delayed for 5 hours. #bestdayever", "negative"},
      	// Пограничный случай: смешанная тональность
      	{"The movie's plot was terrible, but the acting was phenomenal.", "mixed"},
      	// ... ещё 996 твитов
      }

      func getCompletion(prompt string) string {
      	message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
      		Model:     anthropic.ModelClaudeOpus5_5,
      		MaxTokens: 1024,
      		Messages: []anthropic.MessageParam{
      			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
      		},
      	})
      	if err != nil {
      		log.Fatal(err)
      	}
      	return contentText(message)
      }

      func evaluateExactMatch(modelOutput, correctAnswer string) bool {
      	return strings.EqualFold(strings.TrimSpace(modelOutput), correctAnswer)
      }

      func main() {
      	correct := 0
      	for _, item := range tweets {
      		output := getCompletion("Classify this as 'positive', 'negative', 'neutral', or 'mixed'. Output only the label: " + item.Text)
      		if evaluateExactMatch(output, item.Sentiment) {
      			correct++
      		}
      	}
      	fmt.Printf("Sentiment Analysis Accuracy: %.1f%%\n", float64(correct)/float64(len(tweets))*100)
      }
      ```

      ```java Java
      record Tweet(String text, String sentiment) {}

      List<Tweet> tweets = List.of(
          new Tweet("This movie was a total waste of time. 👎", "negative"),
          new Tweet("The new album is 🔥! Been on repeat all day.", "positive"),
          // Пограничный случай: сарказм
          new Tweet("I just love it when my flight gets delayed for 5 hours. #bestdayever", "negative"),
          // Пограничный случай: смешанная тональность
          new Tweet("The movie's plot was terrible, but the acting was phenomenal.", "mixed")
          // ... ещё 996 твитов
      );

      AnthropicClient client = AnthropicOkHttpClient.fromEnv();

      String contentText(Message message) {
          var text = new StringBuilder();
          for (var block : message.content()) {
              block.text().ifPresent(textBlock -> text.append(textBlock.text()));
          }
          return text.toString();
      }

      String getCompletion(String prompt) {
          var params = MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(1024L)
              .addUserMessage(prompt)
              .build();
          return contentText(client.messages().create(params));
      }

      boolean evaluateExactMatch(String modelOutput, String correctAnswer) {
          return modelOutput.strip().equalsIgnoreCase(correctAnswer);
      }

      void main() {
          int correct = 0;
          for (var tweet : tweets) {
              var output = getCompletion(
                  "Classify this as 'positive', 'negative', 'neutral', or 'mixed'. Output only the label: " + tweet.text());
              if (evaluateExactMatch(output, tweet.sentiment())) {
                  correct++;
              }
          }
          IO.println("Sentiment Analysis Accuracy: " + (100.0 * correct / tweets.size()) + "%");
      }
      ```

      ```php PHP
      $client = new Client();

      $tweets = [
          ['text' => 'This movie was a total waste of time. 👎', 'sentiment' => 'negative'],
          ['text' => 'The new album is 🔥! Been on repeat all day.', 'sentiment' => 'positive'],
          // Пограничный случай: сарказм
          ['text' => 'I just love it when my flight gets delayed for 5 hours. #bestdayever', 'sentiment' => 'negative'],
          // Пограничный случай: смешанная тональность
          ['text' => "The movie's plot was terrible, but the acting was phenomenal.", 'sentiment' => 'mixed'],
          // ... ещё 996 твитов
      ];

      function getCompletion(Client $client, string $prompt): string
      {
          $message = $client->messages->create(
              model: Model::CLAUDE_OPUS_5_5,
              maxTokens: 1024,
              messages: [
                  [
                      'role' => 'user',
                      'content' => $prompt,
                  ],
              ],
          );
          return contentText($message);
      }

      function evaluateExactMatch(string $modelOutput, string $correctAnswer): bool
      {
          return strtolower(trim($modelOutput)) === strtolower($correctAnswer);
      }

      function contentText($message): string
      {
          $text = '';
          foreach ($message->content as $block) {
              if ($block instanceof \Anthropic\Messages\TextBlock) {
                  $text .= $block->text;
              }
          }
          return $text;
      }

      $correct = 0;
      foreach ($tweets as $tweet) {
          $output = getCompletion(
              $client,
              "Classify this as 'positive', 'negative', 'neutral', or 'mixed'. Output only the label: {$tweet['text']}",
          );
          if (evaluateExactMatch($output, $tweet['sentiment'])) {
              $correct++;
          }
      }
      echo 'Sentiment Analysis Accuracy: ' . (100 * $correct / count($tweets)) . '%' . PHP_EOL;
      ```

      ```ruby Ruby
      client = Anthropic::Client.new

      tweets = [
        { text: "This movie was a total waste of time. 👎", sentiment: "negative" },
        { text: "The new album is 🔥! Been on repeat all day.", sentiment: "positive" },
        # Пограничный случай: сарказм
        { text: "I just love it when my flight gets delayed for 5 hours. #bestdayever", sentiment: "negative" },
        # Пограничный случай: смешанная тональность
        { text: "The movie's plot was terrible, but the acting was phenomenal.", sentiment: "mixed" }
        # ... ещё 996 твитов
      ]

      def content_text(message)
        message.content.filter_map { |block| block.text if block.type == :text }.join
      end

      def get_completion(client, prompt)
        message = client.messages.create(
          model: Anthropic::Model::CLAUDE_OPUS_5_5,
          max_tokens: 1024,
          messages: [
            {
              role: "user",
              content: prompt
            }
          ]
        )
        content_text(message)
      end

      def evaluate_exact_match(model_output, correct_answer)
        model_output.strip.downcase == correct_answer.downcase
      end

      correct = tweets.count do |tweet|
        output = get_completion(
          client,
          "Classify this as 'positive', 'negative', 'neutral', or 'mixed'. Output only the label: #{tweet[:text]}"
        )
        evaluate_exact_match(output, tweet[:sentiment])
      end
      puts "Sentiment Analysis Accuracy: #{100.0 * correct / tweets.length}%"
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Согласованность (FAQ-бот) — оценка косинусного сходства">
    **Что измеряется:** Косинусное сходство измеряет сходство между двумя векторами (в данном случае — эмбеддингами предложений вывода модели, полученными с помощью [Sentence-BERT (SBERT)](https://sbert.net/)) путём вычисления косинуса угла между ними. Значения, близкие к 1, указывают на более высокое сходство. Это идеально подходит для оценки согласованности, поскольку схожие вопросы должны давать семантически схожие ответы, даже если формулировки различаются.

    **Примеры тестовых случаев для оценки:** 50 групп, в каждой из которых несколько перефразированных версий.

    <CodeGroup exclude="shell">
      ```python Python
      from sentence_transformers import SentenceTransformer
      import numpy as np
      # ...
      faq_variations = [
          {
              "questions": [
                  "What's your return policy?",
                  "How can I return an item?",
                  "Wut's yur retrn polcy?",
              ],
              "answer": "Our return policy allows...",
          },  # Edge case: Typos
          {
              "questions": [
                  "I bought something last week, and it's not really what I expected, so I was wondering if maybe I could possibly return it?",
                  "I read online that your policy is 30 days but that seems like it might be out of date because the website was updated six months ago, so I'm wondering what exactly is your current policy?",
              ],
              "answer": "Our return policy allows...",
          },  # Edge case: Long, rambling question
          {
              "questions": [
                  "I'm Jane's cousin, and she said you guys have great customer service. Can I return this?",
                  "Reddit told me that contacting customer service this way was the fastest way to get an answer. I hope they're right! What is the return window for a jacket?",
              ],
              "answer": "Our return policy allows...",
          },  # Edge case: Irrelevant info
          # ... ещё 47 вопросов FAQ
      ]

      client = anthropic.Anthropic()


      def get_completion(prompt: str):
          message = client.messages.create(
              model="claude-opus-5-5",
              max_tokens=2048,
              messages=[{"role": "user", "content": prompt}],
          )
          return next(block.text for block in message.content if block.type == "text")


      def evaluate_cosine_similarity(outputs):
          model = SentenceTransformer("all-MiniLM-L6-v2")
          embeddings = model.encode(outputs)

          norms = np.linalg.norm(embeddings, axis=1)
          cosine_similarities = np.dot(embeddings, embeddings.T) / np.outer(norms, norms)
          return np.mean(cosine_similarities)


      for faq in faq_variations:
          outputs = [get_completion(question) for question in faq["questions"]]
          similarity_score = evaluate_cosine_similarity(outputs)
          print(f"FAQ Consistency Score: {similarity_score * 100}%")
      ```

      ```typescript TypeScript
      import { pipeline } from "@huggingface/transformers";

      const faqVariations = [
        {
          questions: [
            "What's your return policy?",
            "How can I return an item?",
            "Wut's yur retrn polcy?"
          ],
          answer: "Our return policy allows..."
        }, // Edge case: Typos
        {
          questions: [
            "I bought something last week, and it's not really what I expected, so I was wondering if maybe I could possibly return it?",
            "I read online that your policy is 30 days but that seems like it might be out of date because the website was updated six months ago, so I'm wondering what exactly is your current policy?"
          ],
          answer: "Our return policy allows..."
        }, // Edge case: Long, rambling question
        {
          questions: [
            "I'm Jane's cousin, and she said you guys have great customer service. Can I return this?",
            "Reddit told me that contacting customer service this way was the fastest way to get an answer. I hope they're right! What is the return window for a jacket?"
          ],
          answer: "Our return policy allows..."
        } // Edge case: Irrelevant info
        // ... ещё 47 вопросов FAQ
      ];

      const client = new Anthropic();

      async function getCompletion(prompt: string): Promise<string> {
        const message = await client.messages.create({
          model: "claude-opus-5-5",
          max_tokens: 2048,
          messages: [{ role: "user", content: prompt }]
        });
        const textBlock = message.content.find((block) => block.type === "text");
        return textBlock ? textBlock.text : "";
      }

      async function evaluateCosineSimilarity(outputs: string[]): Promise<number> {
        const extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
        const embeddings = (await extractor(outputs, { pooling: "mean", normalize: true })).tolist();

        let total = 0;
        for (const embeddingA of embeddings) {
          for (const embeddingB of embeddings) {
            // Векторы нормализованы, поэтому косинусное сходство равно скалярному произведению
            total += embeddingA.reduce(
              (sum: number, value: number, i: number) => sum + value * embeddingB[i],
              0
            );
          }
        }
        return total / (embeddings.length * embeddings.length);
      }

      for (const faq of faqVariations) {
        const outputs: string[] = [];
        for (const question of faq.questions) {
          outputs.push(await getCompletion(question));
        }
        const similarityScore = await evaluateCosineSimilarity(outputs);
        console.log(`FAQ Consistency Score: ${similarityScore * 100}%`);
      }
      ```

      ```csharp C#
      using System.Numerics.Tensors;
      // ...
      using Microsoft.Extensions.AI;
      using Microsoft.SemanticKernel;

      Faq[] faqVariations =
      [
          new(
              [
                  "What's your return policy?",
                  "How can I return an item?",
                  "Wut's yur retrn polcy?",
              ],
              "Our return policy allows..."), // Edge case: Typos
          new(
              [
                  "I bought something last week, and it's not really what I expected, so I was wondering if maybe I could possibly return it?",
                  "I read online that your policy is 30 days but that seems like it might be out of date because the website was updated six months ago, so I'm wondering what exactly is your current policy?",
              ],
              "Our return policy allows..."), // Edge case: Long, rambling question
          new(
              [
                  "I'm Jane's cousin, and she said you guys have great customer service. Can I return this?",
                  "Reddit told me that contacting customer service this way was the fastest way to get an answer. I hope they're right! What is the return window for a jacket?",
              ],
              "Our return policy allows..."), // Edge case: Irrelevant info
          // ... ещё 47 вопросов FAQ
      ];

      var client = new AnthropicClient();

      // Скачайте onnx/model.onnx и vocab.txt с https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2
      #pragma warning disable SKEXP0070
      var kernel = Kernel.CreateBuilder()
          .AddBertOnnxEmbeddingGenerator("model.onnx", "vocab.txt")
          .Build();
      #pragma warning restore SKEXP0070
      var embeddingGenerator = kernel.GetRequiredService<IEmbeddingGenerator<string, Embedding<float>>>();

      async Task<string> GetCompletion(string prompt)
      {
          var message = await client.Messages.Create(new MessageCreateParams
          {
              Model = Model.ClaudeOpus5_5,
              MaxTokens = 2048,
              Messages = [new() { Role = Role.User, Content = prompt }],
          });
          foreach (var block in message.Content)
          {
              if (block.TryPickText(out var textBlock))
              {
                  return textBlock.Text;
              }
          }
          return "";
      }

      async Task<double> EvaluateCosineSimilarity(List<string> outputs)
      {
          var embeddings = await embeddingGenerator.GenerateAsync(outputs);

          double total = 0;
          foreach (var embeddingA in embeddings)
          {
              foreach (var embeddingB in embeddings)
              {
                  total += TensorPrimitives.CosineSimilarity(embeddingA.Vector.Span, embeddingB.Vector.Span);
              }
          }
          return total / (embeddings.Count * embeddings.Count);
      }

      foreach (var faq in faqVariations)
      {
          var outputs = new List<string>();
          foreach (var question in faq.Questions)
          {
              outputs.Add(await GetCompletion(question));
          }
          var similarityScore = await EvaluateCosineSimilarity(outputs);
          Console.WriteLine($"FAQ Consistency Score: {similarityScore * 100}%");
      }

      record Faq(string[] Questions, string Answer);
      ```

      ```go Go
      import (
      	"context"
      	"fmt"
      	"log"
      	"math"

      // ...
      	"github.com/knights-analytics/hugot"
      	"github.com/knights-analytics/hugot/pipelines"
      )

      var client = anthropic.NewClient()

      type faq struct {
      	Questions []string
      	Answer    string
      }

      var faqVariations = []faq{
      	{
      		[]string{
      			"What's your return policy?",
      			"How can I return an item?",
      			"Wut's yur retrn polcy?",
      		},
      		"Our return policy allows...",
      	}, // Edge case: Typos
      	{
      		[]string{
      			"I bought something last week, and it's not really what I expected, so I was wondering if maybe I could possibly return it?",
      			"I read online that your policy is 30 days but that seems like it might be out of date because the website was updated six months ago, so I'm wondering what exactly is your current policy?",
      		},
      		"Our return policy allows...",
      	}, // Edge case: Long, rambling question
      	{
      		[]string{
      			"I'm Jane's cousin, and she said you guys have great customer service. Can I return this?",
      			"Reddit told me that contacting customer service this way was the fastest way to get an answer. I hope they're right! What is the return window for a jacket?",
      		},
      		"Our return policy allows...",
      	}, // Edge case: Irrelevant info
      	// ... ещё 47 вопросов и ответов (FAQ)
      }

      func getCompletion(prompt string) string {
      	message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
      		Model:     anthropic.ModelClaudeOpus5_5,
      		MaxTokens: 2048,
      		Messages: []anthropic.MessageParam{
      			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
      		},
      	})
      	if err != nil {
      		log.Fatal(err)
      	}
      	for _, block := range message.Content {
      		if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
      			return textBlock.Text
      		}
      	}
      	return ""
      }

      func cosineSimilarity(embeddingA, embeddingB []float32) float64 {
      	var dot, normA, normB float64
      	for i := range embeddingA {
      		dot += float64(embeddingA[i] * embeddingB[i])
      		normA += float64(embeddingA[i] * embeddingA[i])
      		normB += float64(embeddingB[i] * embeddingB[i])
      	}
      	return dot / (math.Sqrt(normA) * math.Sqrt(normB))
      }

      func evaluateCosineSimilarity(embeddingPipeline *pipelines.FeatureExtractionPipeline, outputs []string) float64 {
      	result, err := embeddingPipeline.RunPipeline(context.Background(), outputs)
      	if err != nil {
      		log.Fatal(err)
      	}

      	total := 0.0
      	for _, embeddingA := range result.Embeddings {
      		for _, embeddingB := range result.Embeddings {
      			total += cosineSimilarity(embeddingA, embeddingB)
      		}
      	}
      	return total / float64(len(result.Embeddings)*len(result.Embeddings))
      }

      func main() {
      	ctx := context.Background()
      	session, err := hugot.NewGoSession(ctx)
      	if err != nil {
      		log.Fatal(err)
      	}
      	defer session.Destroy()

      	downloadOptions := hugot.NewDownloadOptions()
      	downloadOptions.OnnxFilePath = "onnx/model.onnx"
      	modelPath, err := hugot.DownloadModel(ctx, "sentence-transformers/all-MiniLM-L6-v2", "./models/", downloadOptions)
      	if err != nil {
      		log.Fatal(err)
      	}
      	embeddingPipeline, err := hugot.NewPipeline(session, hugot.FeatureExtractionConfig{
      		ModelPath: modelPath,
      		Name:      "embeddingPipeline",
      	})
      	if err != nil {
      		log.Fatal(err)
      	}

      	for _, item := range faqVariations {
      		var outputs []string
      		for _, question := range item.Questions {
      			outputs = append(outputs, getCompletion(question))
      		}
      		similarityScore := evaluateCosineSimilarity(embeddingPipeline, outputs)
      		fmt.Printf("FAQ Consistency Score: %v%%\n", similarityScore*100)
      	}
      }
      ```

      ```java Java
      import dev.langchain4j.data.segment.TextSegment;
      import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
      import dev.langchain4j.store.embedding.CosineSimilarity;
      // ...

      record Faq(List<String> questions, String answer) {}

      List<Faq> faqVariations = List.of(
          new Faq(
              List.of(
                  "What's your return policy?",
                  "How can I return an item?",
                  "Wut's yur retrn polcy?"),
              "Our return policy allows..."), // Edge case: Typos
          new Faq(
              List.of(
                  "I bought something last week, and it's not really what I expected, so I was wondering if maybe I could possibly return it?",
                  "I read online that your policy is 30 days but that seems like it might be out of date because the website was updated six months ago, so I'm wondering what exactly is your current policy?"),
              "Our return policy allows..."), // Edge case: Long, rambling question
          new Faq(
              List.of(
                  "I'm Jane's cousin, and she said you guys have great customer service. Can I return this?",
                  "Reddit told me that contacting customer service this way was the fastest way to get an answer. I hope they're right! What is the return window for a jacket?"),
              "Our return policy allows...") // Edge case: Irrelevant info
          // ... ещё 47 вопросов и ответов (FAQ)
      );

      AnthropicClient client = AnthropicOkHttpClient.fromEnv();

      String getCompletion(String prompt) {
          var params = MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(2048L)
              .addUserMessage(prompt)
              .build();
          Message message = client.messages().create(params);
          return message.content().stream()
              .flatMap(block -> block.text().stream())
              .map(textBlock -> textBlock.text())
              .findFirst()
              .orElse("");
      }

      double evaluateCosineSimilarity(List<String> outputs) {
          var embeddingModel = new AllMiniLmL6V2EmbeddingModel();
          var segments = outputs.stream().map(TextSegment::from).toList();
          var embeddings = embeddingModel.embedAll(segments).content();

          double total = 0;
          for (var embeddingA : embeddings) {
              for (var embeddingB : embeddings) {
                  total += CosineSimilarity.between(embeddingA, embeddingB);
              }
          }
          return total / (embeddings.size() * embeddings.size());
      }

      void main() {
          for (var faq : faqVariations) {
              var outputs = faq.questions().stream().map(this::getCompletion).toList();
              double similarityScore = evaluateCosineSimilarity(outputs);
              IO.println("FAQ Consistency Score: " + similarityScore * 100 + "%");
          }
      }
      ```

      ```php PHP
      use function Codewithkyrian\Transformers\Pipelines\pipeline;

      $client = new Client();

      $faqVariations = [
          [
              'questions' => [
                  "What's your return policy?",
                  'How can I return an item?',
                  "Wut's yur retrn polcy?",
              ],
              'answer' => 'Our return policy allows...',
          ], // Edge case: Typos
          [
              'questions' => [
                  "I bought something last week, and it's not really what I expected, so I was wondering if maybe I could possibly return it?",
                  "I read online that your policy is 30 days but that seems like it might be out of date because the website was updated six months ago, so I'm wondering what exactly is your current policy?",
              ],
              'answer' => 'Our return policy allows...',
          ], // Edge case: Long, rambling question
          [
              'questions' => [
                  "I'm Jane's cousin, and she said you guys have great customer service. Can I return this?",
                  "Reddit told me that contacting customer service this way was the fastest way to get an answer. I hope they're right! What is the return window for a jacket?",
              ],
              'answer' => 'Our return policy allows...',
          ], // Edge case: Irrelevant info
          // ... ещё 47 вопросов FAQ
      ];

      function getCompletion(Client $client, string $prompt): string
      {
          $message = $client->messages->create(
              model: Model::CLAUDE_OPUS_5_5,
              maxTokens: 2048,
              messages: [
                  [
                      'role' => 'user',
                      'content' => $prompt,
                  ],
              ],
          );
          foreach ($message->content as $block) {
              if ($block instanceof \Anthropic\Messages\TextBlock) {
                  return $block->text;
              }
          }
          return '';
      }

      function evaluateCosineSimilarity(array $outputs): float
      {
          $extractor = pipeline('embeddings', 'Xenova/all-MiniLM-L6-v2');
          $embeddings = $extractor($outputs, normalize: true, pooling: 'mean');

          $total = 0.0;
          foreach ($embeddings as $embeddingA) {
              foreach ($embeddings as $embeddingB) {
                  // Векторы нормализованы, поэтому косинусное сходство равно скалярному произведению
                  $total += array_sum(array_map(fn ($valueA, $valueB) => $valueA * $valueB, $embeddingA, $embeddingB));
              }
          }
          return $total / count($embeddings) ** 2;
      }

      foreach ($faqVariations as $faq) {
          $outputs = array_map(fn ($question) => getCompletion($client, $question), $faq['questions']);
          $similarityScore = evaluateCosineSimilarity($outputs);
          echo 'FAQ Consistency Score: ' . ($similarityScore * 100) . '%' . PHP_EOL;
      }
      ```

      ```ruby Ruby
      require "informers"

      client = Anthropic::Client.new

      faq_variations = [
        {
          questions: [
            "What's your return policy?",
            "How can I return an item?",
            "Wut's yur retrn polcy?"
          ],
          answer: "Our return policy allows..."
        }, # Edge case: Typos
        {
          questions: [
            "I bought something last week, and it's not really what I expected, so I was wondering if maybe I could possibly return it?",
            "I read online that your policy is 30 days but that seems like it might be out of date because the website was updated six months ago, so I'm wondering what exactly is your current policy?"
          ],
          answer: "Our return policy allows..."
        }, # Edge case: Long, rambling question
        {
          questions: [
            "I'm Jane's cousin, and she said you guys have great customer service. Can I return this?",
            "Reddit told me that contacting customer service this way was the fastest way to get an answer. I hope they're right! What is the return window for a jacket?"
          ],
          answer: "Our return policy allows..."
        } # Edge case: Irrelevant info
        # ... ещё 47 вопросов FAQ
      ]

      def get_completion(client, prompt)
        message = client.messages.create(
          model: Anthropic::Model::CLAUDE_OPUS_5_5,
          max_tokens: 2048,
          messages: [
            {
              role: "user",
              content: prompt
            }
          ]
        )
        text_block = message.content.find { |block| block.is_a?(Anthropic::Models::TextBlock) }
        text_block ? text_block.text : ""
      end

      def evaluate_cosine_similarity(outputs)
        model = Informers.pipeline("embedding", "sentence-transformers/all-MiniLM-L6-v2")
        embeddings = model.(outputs)

        # Векторы нормализованы, поэтому косинусное сходство равно скалярному произведению
        total = embeddings.sum do |embedding_a|
          embeddings.sum { |embedding_b| embedding_a.zip(embedding_b).sum { |value_a, value_b| value_a * value_b } }
        end
        total / embeddings.length**2
      end

      faq_variations.each do |faq|
        outputs = faq[:questions].map { |question| get_completion(client, question) }
        similarity_score = evaluate_cosine_similarity(outputs)
        puts "FAQ Consistency Score: #{similarity_score * 100}%"
      end
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Релевантность и связность (суммаризация) — оценка ROUGE-L">
    **Что измеряется:** ROUGE-L (Recall-Oriented Understudy for Gisting Evaluation — Longest Common Subsequence) оценивает качество сгенерированных резюме. Она измеряет длину наибольшей общей подпоследовательности между резюме-кандидатом и эталонным резюме. Высокие оценки ROUGE-L указывают на то, что сгенерированное резюме передаёт ключевую информацию в связном порядке.

    **Примеры тестовых случаев для оценки:** 200 статей с эталонными резюме.

    <CodeGroup exclude="shell">
      ```python Python
      from rouge import Rouge
      # ...
      articles = [
          {
              "text": "In a groundbreaking study, researchers at MIT...",
              "summary": "MIT scientists discover a new antibiotic...",
          },
          {
              "text": "Jane Doe, a local hero, made headlines last week for saving... In city hall news, the budget... Meteorologists predict...",
              "summary": "Community celebrates local hero Jane Doe while city grapples with budget issues.",
          },  # Edge case: Multitopic
          {
              "text": "You won't believe what this celebrity did! ... extensive charity work ...",
              "summary": "Celebrity's extensive charity work surprises fans",
          },  # Edge case: Misleading title
          # ... ещё 197 статей
      ]

      client = anthropic.Anthropic()


      def get_completion(prompt: str):
          message = client.messages.create(
              model="claude-opus-5-5",
              max_tokens=1024,
              messages=[{"role": "user", "content": prompt}],
          )
          return next(block.text for block in message.content if block.type == "text")


      def evaluate_rouge_l(model_output, true_summary):
          rouge = Rouge()
          scores = rouge.get_scores(model_output, true_summary)
          return scores[0]["rouge-l"]["f"]  # ROUGE-L F1 score


      outputs = [
          get_completion(f"Summarize this article in 1-2 sentences:\n\n{article['text']}")
          for article in articles
      ]
      relevance_scores = [
          evaluate_rouge_l(output, article["summary"])
          for output, article in zip(outputs, articles)
      ]
      print(f"Average ROUGE-L F1 Score: {sum(relevance_scores) / len(relevance_scores)}")
      ```

      ```typescript TypeScript
      const articles = [
        {
          text: "In a groundbreaking study, researchers at MIT...",
          summary: "MIT scientists discover a new antibiotic..."
        },
        {
          text: "Jane Doe, a local hero, made headlines last week for saving... In city hall news, the budget... Meteorologists predict...",
          summary: "Community celebrates local hero Jane Doe while city grapples with budget issues."
        }, // Edge case: Multitopic
        {
          text: "You won't believe what this celebrity did! ... extensive charity work ...",
          summary: "Celebrity's extensive charity work surprises fans"
        } // Edge case: Misleading title
        // ... ещё 197 статей
      ];

      const client = new Anthropic();

      async function getCompletion(prompt: string): Promise<string> {
        const message = await client.messages.create({
          model: "claude-opus-5-5",
          max_tokens: 1024,
          messages: [{ role: "user", content: prompt }]
        });
        const textBlock = message.content.find((block) => block.type === "text");
        return textBlock ? textBlock.text : "";
      }

      // ROUGE-L измеряет наибольшую общую подпоследовательность (LCS) слов между
      // кандидатным и эталонным резюме; здесь выражается как F1-мера. Токенизация
      // упрощена до слов по пробелам; оценки могут отличаться от Python-библиотеки rouge.
      function rougeL(candidate: string, reference: string): number {
        const candidateWords = candidate.toLowerCase().trim().split(/\s+/);
        const referenceWords = reference.toLowerCase().trim().split(/\s+/);

        const lcsLengths: number[][] = Array.from({ length: candidateWords.length + 1 }, () =>
          new Array(referenceWords.length + 1).fill(0)
        );
        for (const [i, candidateWord] of candidateWords.entries()) {
          for (const [j, referenceWord] of referenceWords.entries()) {
            lcsLengths[i + 1][j + 1] =
              candidateWord === referenceWord
                ? lcsLengths[i][j] + 1
                : Math.max(lcsLengths[i][j + 1], lcsLengths[i + 1][j]);
          }
        }
        const lcs = lcsLengths[candidateWords.length][referenceWords.length];

        if (lcs === 0) {
          return 0;
        }
        const precision = lcs / candidateWords.length;
        const recall = lcs / referenceWords.length;
        return (2 * precision * recall) / (precision + recall);
      }

      const relevanceScores: number[] = [];
      for (const article of articles) {
        const output = await getCompletion(
          `Summarize this article in 1-2 sentences:\n\n${article.text}`
        );
        relevanceScores.push(rougeL(output, article.summary));
      }
      const averageScore =
        relevanceScores.reduce((sum, score) => sum + score, 0) / relevanceScores.length;
      console.log(`Average ROUGE-L F1 Score: ${averageScore}`);
      ```

      ```csharp C#
      using System.Text.RegularExpressions;
      // ...
      Article[] articles =
      [
          new("In a groundbreaking study, researchers at MIT...",
              "MIT scientists discover a new antibiotic..."),
          // Пограничный случай: несколько тем
          new("Jane Doe, a local hero, made headlines last week for saving... In city hall news, the budget... Meteorologists predict...",
              "Community celebrates local hero Jane Doe while city grapples with budget issues."),
          // Пограничный случай: вводящий в заблуждение заголовок
          new("You won't believe what this celebrity did! ... extensive charity work ...",
              "Celebrity's extensive charity work surprises fans"),
          // ... ещё 197 статей
      ];

      var client = new AnthropicClient();

      async Task<string> GetCompletion(string prompt)
      {
          var message = await client.Messages.Create(new MessageCreateParams
          {
              Model = Model.ClaudeOpus5_5,
              MaxTokens = 1024,
              Messages = [new() { Role = Role.User, Content = prompt }],
          });
          return ContentText(message);
      }

      // ROUGE-L измеряет наибольшую общую подпоследовательность (LCS) слов между
      // кандидатным и эталонным резюме; здесь выражается как F1-мера. Токенизация
      // упрощена до слов по пробелам; оценки могут отличаться от Python-библиотеки rouge.
      double RougeL(string candidate, string reference)
      {
          var candidateWords = Regex.Split(candidate.ToLowerInvariant().Trim(), @"\s+");
          var referenceWords = Regex.Split(reference.ToLowerInvariant().Trim(), @"\s+");

          var lcsLengths = new int[candidateWords.Length + 1, referenceWords.Length + 1];
          for (var i = 0; i < candidateWords.Length; i++)
          {
              for (var j = 0; j < referenceWords.Length; j++)
              {
                  lcsLengths[i + 1, j + 1] = candidateWords[i] == referenceWords[j]
                      ? lcsLengths[i, j] + 1
                      : Math.Max(lcsLengths[i, j + 1], lcsLengths[i + 1, j]);
              }
          }
          var lcs = lcsLengths[candidateWords.Length, referenceWords.Length];

          if (lcs == 0)
          {
              return 0;
          }
          var precision = (double)lcs / candidateWords.Length;
          var recall = (double)lcs / referenceWords.Length;
          return 2 * precision * recall / (precision + recall);
      }

      string ContentText(Message message)
      {
          var text = "";
          foreach (var block in message.Content)
          {
              if (block.TryPickText(out var textBlock))
              {
                  text += textBlock.Text;
              }
          }
          return text;
      }

      var relevanceScores = new List<double>();
      foreach (var article in articles)
      {
          var output = await GetCompletion($"Summarize this article in 1-2 sentences:\n\n{article.Text}");
          relevanceScores.Add(RougeL(output, article.Summary));
      }
      Console.WriteLine($"Average ROUGE-L F1 Score: {relevanceScores.Average()}");

      record Article(string Text, string Summary);
      ```

      ```go Go
      var client = anthropic.NewClient()

      func contentText(message *anthropic.Message) string {
      	var text strings.Builder
      	for _, block := range message.Content {
      		if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
      			text.WriteString(textBlock.Text)
      		}
      	}
      	return text.String()
      }

      type article struct {
      	Text    string
      	Summary string
      }

      var articles = []article{
      	{
      		"In a groundbreaking study, researchers at MIT...",
      		"MIT scientists discover a new antibiotic...",
      	},
      	// Пограничный случай: несколько тем
      	{
      		"Jane Doe, a local hero, made headlines last week for saving... In city hall news, the budget... Meteorologists predict...",
      		"Community celebrates local hero Jane Doe while city grapples with budget issues.",
      	},
      	// Пограничный случай: вводящий в заблуждение заголовок
      	{
      		"You won't believe what this celebrity did! ... extensive charity work ...",
      		"Celebrity's extensive charity work surprises fans",
      	},
      	// ... ещё 197 статей
      }

      func getCompletion(prompt string) string {
      	message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
      		Model:     anthropic.ModelClaudeOpus5_5,
      		MaxTokens: 1024,
      		Messages: []anthropic.MessageParam{
      			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
      		},
      	})
      	if err != nil {
      		log.Fatal(err)
      	}
      	return contentText(message)
      }

      // ROUGE-L измеряет наибольшую общую подпоследовательность (LCS) слов между
      // кандидатным и эталонным резюме; здесь выражается как F1-мера. Токенизация
      // упрощена до слов по пробелам; оценки могут отличаться от Python-библиотеки rouge.
      func rougeL(candidate, reference string) float64 {
      	candidateWords := strings.Fields(strings.ToLower(candidate))
      	referenceWords := strings.Fields(strings.ToLower(reference))

      	lcsLengths := make([][]int, len(candidateWords)+1)
      	for i := range lcsLengths {
      		lcsLengths[i] = make([]int, len(referenceWords)+1)
      	}
      	for i, candidateWord := range candidateWords {
      		for j, referenceWord := range referenceWords {
      			if candidateWord == referenceWord {
      				lcsLengths[i+1][j+1] = lcsLengths[i][j] + 1
      			} else {
      				lcsLengths[i+1][j+1] = max(lcsLengths[i][j+1], lcsLengths[i+1][j])
      			}
      		}
      	}
      	lcs := lcsLengths[len(candidateWords)][len(referenceWords)]

      	if lcs == 0 {
      		return 0
      	}
      	precision := float64(lcs) / float64(len(candidateWords))
      	recall := float64(lcs) / float64(len(referenceWords))
      	return 2 * precision * recall / (precision + recall)
      }

      func main() {
      	var relevanceScores []float64
      	for _, item := range articles {
      		output := getCompletion("Summarize this article in 1-2 sentences:\n\n" + item.Text)
      		relevanceScores = append(relevanceScores, rougeL(output, item.Summary))
      	}
      	total := 0.0
      	for _, score := range relevanceScores {
      		total += score
      	}
      	fmt.Println("Average ROUGE-L F1 Score:", total/float64(len(relevanceScores)))
      }
      ```

      ```java Java
      record Article(String text, String summary) {}

      List<Article> articles = List.of(
          new Article(
              "In a groundbreaking study, researchers at MIT...",
              "MIT scientists discover a new antibiotic..."),
          // Пограничный случай: несколько тем
          new Article(
              "Jane Doe, a local hero, made headlines last week for saving... In city hall news, the budget... Meteorologists predict...",
              "Community celebrates local hero Jane Doe while city grapples with budget issues."),
          // Пограничный случай: вводящий в заблуждение заголовок
          new Article(
              "You won't believe what this celebrity did! ... extensive charity work ...",
              "Celebrity's extensive charity work surprises fans")
          // ... ещё 197 статей
      );

      AnthropicClient client = AnthropicOkHttpClient.fromEnv();

      String contentText(Message message) {
          var text = new StringBuilder();
          for (var block : message.content()) {
              block.text().ifPresent(textBlock -> text.append(textBlock.text()));
          }
          return text.toString();
      }

      String getCompletion(String prompt) {
          var params = MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(1024L)
              .addUserMessage(prompt)
              .build();
          return contentText(client.messages().create(params));
      }

      // ROUGE-L измеряет наибольшую общую подпоследовательность (LCS) слов между
      // кандидатным и эталонным резюме; здесь приводится как F1-мера. Токенизация
      // упрощена до слов по пробелам; оценки могут отличаться от Python-библиотеки rouge.
      double rougeL(String candidate, String reference) {
          var candidateWords = candidate.toLowerCase().strip().split("\\s+");
          var referenceWords = reference.toLowerCase().strip().split("\\s+");

          var lcsLengths = new int[candidateWords.length + 1][referenceWords.length + 1];
          for (int i = 0; i < candidateWords.length; i++) {
              for (int j = 0; j < referenceWords.length; j++) {
                  lcsLengths[i + 1][j + 1] = candidateWords[i].equals(referenceWords[j])
                      ? lcsLengths[i][j] + 1
                      : Math.max(lcsLengths[i][j + 1], lcsLengths[i + 1][j]);
              }
          }
          int lcs = lcsLengths[candidateWords.length][referenceWords.length];

          if (lcs == 0) {
              return 0;
          }
          double precision = (double) lcs / candidateWords.length;
          double recall = (double) lcs / referenceWords.length;
          return 2 * precision * recall / (precision + recall);
      }

      void main() {
          List<Double> relevanceScores = new ArrayList<>();
          for (var article : articles) {
              var output = getCompletion("Summarize this article in 1-2 sentences:\n\n" + article.text());
              relevanceScores.add(rougeL(output, article.summary()));
          }
          double average = relevanceScores.stream().mapToDouble(Double::doubleValue).average().orElse(0);
          IO.println("Average ROUGE-L F1 Score: " + average);
      }
      ```

      ```php PHP
      $client = new Client();

      $articles = [
          [
              'text' => 'In a groundbreaking study, researchers at MIT...',
              'summary' => 'MIT scientists discover a new antibiotic...',
          ],
          // Пограничный случай: несколько тем
          [
              'text' => 'Jane Doe, a local hero, made headlines last week for saving... In city hall news, the budget... Meteorologists predict...',
              'summary' => 'Community celebrates local hero Jane Doe while city grapples with budget issues.',
          ],
          // Пограничный случай: вводящий в заблуждение заголовок
          [
              'text' => "You won't believe what this celebrity did! ... extensive charity work ...",
              'summary' => "Celebrity's extensive charity work surprises fans",
          ],
          // ... ещё 197 статей
      ];

      function getCompletion(Client $client, string $prompt): string
      {
          $message = $client->messages->create(
              model: Model::CLAUDE_OPUS_5_5,
              maxTokens: 1024,
              messages: [
                  [
                      'role' => 'user',
                      'content' => $prompt,
                  ],
              ],
          );
          return contentText($message);
      }

      // ROUGE-L измеряет наибольшую общую подпоследовательность (LCS) слов между
      // кандидатным и эталонным резюме; здесь выражается как F1-мера. Токенизация
      // упрощена до слов по пробелам; оценки могут отличаться от Python-библиотеки rouge.
      function rougeL(string $candidate, string $reference): float
      {
          $candidateWords = preg_split('/\s+/', strtolower(trim($candidate)));
          $referenceWords = preg_split('/\s+/', strtolower(trim($reference)));

          $lcsLengths = array_fill(0, count($candidateWords) + 1, array_fill(0, count($referenceWords) + 1, 0));
          foreach ($candidateWords as $i => $candidateWord) {
              foreach ($referenceWords as $j => $referenceWord) {
                  $lcsLengths[$i + 1][$j + 1] = $candidateWord === $referenceWord
                      ? $lcsLengths[$i][$j] + 1
                      : max($lcsLengths[$i][$j + 1], $lcsLengths[$i + 1][$j]);
              }
          }
          $lcs = $lcsLengths[count($candidateWords)][count($referenceWords)];

          if ($lcs === 0) {
              return 0.0;
          }
          $precision = $lcs / count($candidateWords);
          $recall = $lcs / count($referenceWords);
          return 2 * $precision * $recall / ($precision + $recall);
      }

      function contentText($message): string
      {
          $text = '';
          foreach ($message->content as $block) {
              if ($block instanceof \Anthropic\Messages\TextBlock) {
                  $text .= $block->text;
              }
          }
          return $text;
      }

      $relevanceScores = [];
      foreach ($articles as $article) {
          $output = getCompletion($client, "Summarize this article in 1-2 sentences:\n\n{$article['text']}");
          $relevanceScores[] = rougeL($output, $article['summary']);
      }
      echo 'Average ROUGE-L F1 Score: ' . (array_sum($relevanceScores) / count($relevanceScores)) . PHP_EOL;
      ```

      ```ruby Ruby
      client = Anthropic::Client.new

      articles = [
        {
          text: "In a groundbreaking study, researchers at MIT...",
          summary: "MIT scientists discover a new antibiotic..."
        },
        # Пограничный случай: несколько тем
        {
          text: "Jane Doe, a local hero, made headlines last week for saving... In city hall news, the budget... Meteorologists predict...",
          summary: "Community celebrates local hero Jane Doe while city grapples with budget issues."
        },
        # Пограничный случай: вводящий в заблуждение заголовок
        {
          text: "You won't believe what this celebrity did! ... extensive charity work ...",
          summary: "Celebrity's extensive charity work surprises fans"
        }
        # ... ещё 197 статей
      ]

      def content_text(message)
        message.content.filter_map { |block| block.text if block.type == :text }.join
      end

      def get_completion(client, prompt)
        message = client.messages.create(
          model: Anthropic::Model::CLAUDE_OPUS_5_5,
          max_tokens: 1024,
          messages: [
            {
              role: "user",
              content: prompt
            }
          ]
        )
        content_text(message)
      end

      # ROUGE-L измеряет наибольшую общую подпоследовательность (LCS) слов между
      # кандидатным и эталонным резюме; здесь выражается как F1-мера. Токенизация
      # упрощена до слов по пробелам; оценки могут отличаться от Python-библиотеки rouge.
      def rouge_l(candidate, reference)
        candidate_words = candidate.downcase.split
        reference_words = reference.downcase.split

        lcs_lengths = Array.new(candidate_words.length + 1) { Array.new(reference_words.length + 1, 0) }
        candidate_words.each_with_index do |candidate_word, i|
          reference_words.each_with_index do |reference_word, j|
            lcs_lengths[i + 1][j + 1] = if candidate_word == reference_word
              lcs_lengths[i][j] + 1
            else
              [lcs_lengths[i][j + 1], lcs_lengths[i + 1][j]].max
            end
          end
        end
        lcs = lcs_lengths[candidate_words.length][reference_words.length]

        return 0.0 if lcs.zero?

        precision = lcs.to_f / candidate_words.length
        recall = lcs.to_f / reference_words.length
        2 * precision * recall / (precision + recall)
      end

      relevance_scores = articles.map do |article|
        output = get_completion(client, "Summarize this article in 1-2 sentences:\n\n#{article[:text]}")
        rouge_l(output, article[:summary])
      end
      puts "Average ROUGE-L F1 Score: #{relevance_scores.sum / relevance_scores.length}"
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Тон и стиль (обслуживание клиентов) — шкала Лайкерта на основе LLM">
    **Что измеряется:** Шкала Лайкерта на основе LLM — это психометрическая шкала, использующая LLM для оценки субъективных установок или восприятия. Здесь она используется для оценки тона ответов по шкале от 1 до 5. Она идеально подходит для оценки тонких аспектов, таких как эмпатия, профессионализм или терпение, которые трудно выразить количественно с помощью традиционных метрик.

    **Примеры тестовых случаев для оценки:** 100 обращений клиентов с целевым тоном (эмпатичный, терпеливый, профессиональный).

    <CodeGroup exclude="shell">
      ```python Python
      inquiries = [
          {
              "text": "This is the third time you've messed up my order. I want a refund NOW!",
              "tone": "empathetic",
          },  # Edge case: Angry customer
          {
              "text": "I tried resetting my password but then my account got locked...",
              "tone": "patient",
          },  # Edge case: Complex issue
          {
              "text": "I can't believe how good your product is. It's ruined all others for me!",
              "tone": "professional",
          },  # Edge case: Compliment as complaint
          # ... ещё 97 запросов
      ]

      client = anthropic.Anthropic()


      def get_completion(prompt: str):
          message = client.messages.create(
              model="claude-opus-5-5",
              max_tokens=2048,
              messages=[{"role": "user", "content": prompt}],
          )
          return next(block.text for block in message.content if block.type == "text")


      def evaluate_likert(model_output, target_tone):
          tone_prompt = f"""Rate this customer service response on a scale of 1-5 for being {target_tone}:
          <response>{model_output}</response>
          1: Not at all {target_tone}
          5: Perfectly {target_tone}
          Output only the number."""

          # Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
          response = client.messages.create(
              model="claude-opus-5-5",
              max_tokens=1024,
              messages=[{"role": "user", "content": tone_prompt}],
          )
          return int(
              next(block.text for block in response.content if block.type == "text").strip()
          )


      outputs = [
          get_completion(f"Respond to this customer inquiry: {inquiry['text']}")
          for inquiry in inquiries
      ]
      tone_scores = [
          evaluate_likert(output, inquiry["tone"])
          for output, inquiry in zip(outputs, inquiries)
      ]
      print(f"Average Tone Score: {sum(tone_scores) / len(tone_scores)}")
      ```

      ```typescript TypeScript
      const inquiries = [
        {
          text: "This is the third time you've messed up my order. I want a refund NOW!",
          tone: "empathetic"
        }, // Edge case: Angry customer
        {
          text: "I tried resetting my password but then my account got locked...",
          tone: "patient"
        }, // Edge case: Complex issue
        {
          text: "I can't believe how good your product is. It's ruined all others for me!",
          tone: "professional"
        } // Edge case: Compliment as complaint
        // ... ещё 97 запросов
      ];

      const client = new Anthropic();

      async function getCompletion(prompt: string): Promise<string> {
        const message = await client.messages.create({
          model: "claude-opus-5-5",
          max_tokens: 2048,
          messages: [{ role: "user", content: prompt }]
        });
        const textBlock = message.content.find((block) => block.type === "text");
        return textBlock ? textBlock.text : "";
      }

      async function evaluateLikert(modelOutput: string, targetTone: string): Promise<number> {
        const tonePrompt = `Rate this customer service response on a scale of 1-5 for being ${targetTone}:
      <response>${modelOutput}</response>
      1: Not at all ${targetTone}
      5: Perfectly ${targetTone}
      Output only the number.`;

        // Как правило, для оценки лучше использовать модель, отличную от той, что сгенерировала оцениваемый вывод
        const response = await client.messages.create({
          model: "claude-opus-5-5",
          max_tokens: 1024,
          messages: [{ role: "user", content: tonePrompt }]
        });
        const textBlock = response.content.find((block) => block.type === "text");
        const scoreText = textBlock ? textBlock.text.trim() : "";
        if (!/^\d+$/.test(scoreText)) {
          throw new Error(`Unexpected rating from grader: ${scoreText}`);
        }
        return Number(scoreText);
      }

      const toneScores: number[] = [];
      for (const inquiry of inquiries) {
        const output = await getCompletion(`Respond to this customer inquiry: ${inquiry.text}`);
        toneScores.push(await evaluateLikert(output, inquiry.tone));
      }
      console.log(
        `Average Tone Score: ${
          toneScores.reduce((sum, score) => sum + score, 0) / toneScores.length
        }`
      );
      ```

      ```csharp C#
      Inquiry[] inquiries =
      [
          // Пограничный случай: рассерженный клиент
          new("This is the third time you've messed up my order. I want a refund NOW!", "empathetic"),
          // Пограничный случай: сложная проблема
          new("I tried resetting my password but then my account got locked...", "patient"),
          // Пограничный случай: комплимент в виде жалобы
          new("I can't believe how good your product is. It's ruined all others for me!", "professional"),
          // ... ещё 97 обращений
      ];

      var client = new AnthropicClient();

      async Task<string> GetCompletion(string prompt)
      {
          var message = await client.Messages.Create(new MessageCreateParams
          {
              Model = Model.ClaudeOpus5_5,
              MaxTokens = 2048,
              Messages = [new() { Role = Role.User, Content = prompt }],
          });
          return ContentText(message);
      }

      async Task<int> EvaluateLikert(string modelOutput, string targetTone)
      {
          var tonePrompt = $"""
              Rate this customer service response on a scale of 1-5 for being {targetTone}:
              <response>{modelOutput}</response>
              1: Not at all {targetTone}
              5: Perfectly {targetTone}
              Output only the number.
              """;

          // Как правило, для оценки лучше использовать другую модель, а не ту, что сгенерировала оцениваемый вывод
          var response = await client.Messages.Create(new MessageCreateParams
          {
              Model = Model.ClaudeOpus5_5,
              MaxTokens = 1024,
              Messages = [new() { Role = Role.User, Content = tonePrompt }],
          });
          return int.Parse(ContentText(response).Trim());
      }

      string ContentText(Message message)
      {
          var text = "";
          foreach (var block in message.Content)
          {
              if (block.TryPickText(out var textBlock))
              {
                  text += textBlock.Text;
              }
          }
          return text;
      }

      var totalScore = 0;
      foreach (var inquiry in inquiries)
      {
          var output = await GetCompletion($"Respond to this customer inquiry: {inquiry.Text}");
          totalScore += await EvaluateLikert(output, inquiry.Tone);
      }
      Console.WriteLine($"Average Tone Score: {(double)totalScore / inquiries.Length}");

      record Inquiry(string Text, string Tone);
      ```

      ```go Go
      var client = anthropic.NewClient()

      func contentText(message *anthropic.Message) string {
      	var text strings.Builder
      	for _, block := range message.Content {
      		if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
      			text.WriteString(textBlock.Text)
      		}
      	}
      	return text.String()
      }

      type inquiry struct {
      	Text string
      	Tone string
      }

      var inquiries = []inquiry{
      	// Пограничный случай: рассерженный клиент
      	{"This is the third time you've messed up my order. I want a refund NOW!", "empathetic"},
      	// Пограничный случай: сложная проблема
      	{"I tried resetting my password but then my account got locked...", "patient"},
      	// Пограничный случай: комплимент в виде жалобы
      	{"I can't believe how good your product is. It's ruined all others for me!", "professional"},
      	// ... ещё 97 обращений
      }

      func getCompletion(prompt string) string {
      	message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
      		Model:     anthropic.ModelClaudeOpus5_5,
      		MaxTokens: 2048,
      		Messages: []anthropic.MessageParam{
      			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
      		},
      	})
      	if err != nil {
      		log.Fatal(err)
      	}
      	return contentText(message)
      }

      func evaluateLikert(modelOutput, targetTone string) int {
      	tonePrompt := fmt.Sprintf(`Rate this customer service response on a scale of 1-5 for being %[1]s:
      <response>%[2]s</response>
      1: Not at all %[1]s
      5: Perfectly %[1]s
      Output only the number.`, targetTone, modelOutput)

      	// Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
      	response, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
      		Model:     anthropic.ModelClaudeOpus5_5,
      		MaxTokens: 1024,
      		Messages: []anthropic.MessageParam{
      			anthropic.NewUserMessage(anthropic.NewTextBlock(tonePrompt)),
      		},
      	})
      	if err != nil {
      		log.Fatal(err)
      	}

      	score, err := strconv.Atoi(strings.TrimSpace(contentText(response)))
      	if err != nil {
      		log.Fatal(err)
      	}
      	return score
      }

      func main() {
      	totalScore := 0
      	for _, item := range inquiries {
      		output := getCompletion("Respond to this customer inquiry: " + item.Text)
      		totalScore += evaluateLikert(output, item.Tone)
      	}
      	fmt.Printf("Average Tone Score: %.1f\n", float64(totalScore)/float64(len(inquiries)))
      }
      ```

      ```java Java
      record Inquiry(String text, String tone) {}

      List<Inquiry> inquiries = List.of(
          // Пограничный случай: рассерженный клиент
          new Inquiry("This is the third time you've messed up my order. I want a refund NOW!", "empathetic"),
          // Пограничный случай: сложная проблема
          new Inquiry("I tried resetting my password but then my account got locked...", "patient"),
          // Пограничный случай: комплимент в виде жалобы
          new Inquiry("I can't believe how good your product is. It's ruined all others for me!", "professional")
          // ... ещё 97 обращений
      );

      AnthropicClient client = AnthropicOkHttpClient.fromEnv();

      String contentText(Message message) {
          var text = new StringBuilder();
          for (var block : message.content()) {
              block.text().ifPresent(textBlock -> text.append(textBlock.text()));
          }
          return text.toString();
      }

      String getCompletion(String prompt) {
          var params = MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(2048L)
              .addUserMessage(prompt)
              .build();
          return contentText(client.messages().create(params));
      }

      int evaluateLikert(String modelOutput, String targetTone) {
          var tonePrompt = """
              Rate this customer service response on a scale of 1-5 for being %1$s:
              <response>%2$s</response>
              1: Not at all %1$s
              5: Perfectly %1$s
              Output only the number.""".formatted(targetTone, modelOutput);

          // Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
          var params = MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(1024L)
              .addUserMessage(tonePrompt)
              .build();
          var judgment = contentText(client.messages().create(params));
          return Integer.parseInt(judgment.strip());
      }

      void main() {
          int totalScore = 0;
          for (var inquiry : inquiries) {
              var output = getCompletion("Respond to this customer inquiry: " + inquiry.text());
              totalScore += evaluateLikert(output, inquiry.tone());
          }
          IO.println("Average Tone Score: " + ((double) totalScore / inquiries.size()));
      }
      ```

      ```php PHP
      $client = new Client();

      $inquiries = [
          // Пограничный случай: рассерженный клиент
          ['text' => "This is the third time you've messed up my order. I want a refund NOW!", 'tone' => 'empathetic'],
          // Пограничный случай: сложная проблема
          ['text' => 'I tried resetting my password but then my account got locked...', 'tone' => 'patient'],
          // Пограничный случай: комплимент в виде жалобы
          ['text' => "I can't believe how good your product is. It's ruined all others for me!", 'tone' => 'professional'],
          // ... ещё 97 обращений
      ];

      function getCompletion(Client $client, string $prompt): string
      {
          $message = $client->messages->create(
              model: Model::CLAUDE_OPUS_5_5,
              maxTokens: 2048,
              messages: [
                  [
                      'role' => 'user',
                      'content' => $prompt,
                  ],
              ],
          );
          return contentText($message);
      }

      function evaluateLikert(Client $client, string $modelOutput, string $targetTone): int
      {
          $tonePrompt = <<<PROMPT
          Rate this customer service response on a scale of 1-5 for being {$targetTone}:
          <response>{$modelOutput}</response>
          1: Not at all {$targetTone}
          5: Perfectly {$targetTone}
          Output only the number.
          PROMPT;

          // Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
          $response = $client->messages->create(
              model: Model::CLAUDE_OPUS_5_5,
              maxTokens: 1024,
              messages: [
                  [
                      'role' => 'user',
                      'content' => $tonePrompt,
                  ],
              ],
          );
          $scoreText = trim(contentText($response));
          if (filter_var($scoreText, FILTER_VALIDATE_INT) === false) {
              throw new \RuntimeException("Unexpected rating from grader: {$scoreText}");
          }
          return (int) $scoreText;
      }

      function contentText($message): string
      {
          $text = '';
          foreach ($message->content as $block) {
              if ($block instanceof \Anthropic\Messages\TextBlock) {
                  $text .= $block->text;
              }
          }
          return $text;
      }

      $totalScore = 0;
      foreach ($inquiries as $inquiry) {
          $output = getCompletion($client, "Respond to this customer inquiry: {$inquiry['text']}");
          $totalScore += evaluateLikert($client, $output, $inquiry['tone']);
      }
      echo 'Average Tone Score: ' . ($totalScore / count($inquiries)) . PHP_EOL;
      ```

      ```ruby Ruby
      client = Anthropic::Client.new

      inquiries = [
        # Пограничный случай: рассерженный клиент
        { text: "This is the third time you've messed up my order. I want a refund NOW!", tone: "empathetic" },
        # Пограничный случай: сложная проблема
        { text: "I tried resetting my password but then my account got locked...", tone: "patient" },
        # Пограничный случай: комплимент в виде жалобы
        { text: "I can't believe how good your product is. It's ruined all others for me!", tone: "professional" }
        # ... ещё 97 обращений
      ]

      def content_text(message)
        message.content.filter_map { |block| block.text if block.type == :text }.join
      end

      def get_completion(client, prompt)
        message = client.messages.create(
          model: Anthropic::Model::CLAUDE_OPUS_5_5,
          max_tokens: 2048,
          messages: [
            {
              role: "user",
              content: prompt
            }
          ]
        )
        content_text(message)
      end

      def evaluate_likert(client, model_output, target_tone)
        tone_prompt = <<~PROMPT
          Rate this customer service response on a scale of 1-5 for being #{target_tone}:
          <response>#{model_output}</response>
          1: Not at all #{target_tone}
          5: Perfectly #{target_tone}
          Output only the number.
        PROMPT

        # Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
        response = client.messages.create(
          model: Anthropic::Model::CLAUDE_OPUS_5_5,
          max_tokens: 1024,
          messages: [
            {
              role: "user",
              content: tone_prompt
            }
          ]
        )
        Integer(content_text(response).strip)
      end

      tone_scores = inquiries.map do |inquiry|
        output = get_completion(client, "Respond to this customer inquiry: #{inquiry[:text]}")
        evaluate_likert(client, output, inquiry[:tone])
      end
      puts "Average Tone Score: #{tone_scores.sum.to_f / tone_scores.length}"
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Сохранение конфиденциальности (медицинский чат-бот) — бинарная классификация на основе LLM">
    **Что измеряется:** Бинарная классификация определяет, принадлежит ли входной объект к одному из двух классов. Здесь она используется для классификации того, содержит ли ответ PHI (защищённую медицинскую информацию) или нет. Этот метод может учитывать контекст и выявлять тонкие или неявные формы PHI, которые системы на основе правил могут пропустить.

    **Примеры тестовых случаев для оценки:** 500 смоделированных запросов пациентов, некоторые из которых содержат PHI.

    <CodeGroup exclude="shell">
      ```python Python
      patient_queries = [
          {"query": "What are the side effects of Lisinopril?", "contains_phi": False},
          {
              "query": "Can you tell me why John Doe, DOB 5/12/1980, was prescribed Metformin?",
              "contains_phi": True,
          },  # Edge case: Explicit PHI
          {
              "query": "If my friend Alice, who was born on July 4, 1985, had diabetes, what...",
              "contains_phi": True,
          },  # Edge case: Hypothetical PHI
          {
              "query": "I'm worried about my son. He's been prescribed the same medication as his father last year.",
              "contains_phi": True,
          },  # Edge case: Implicit PHI
          # ... ещё 496 запросов
      ]

      client = anthropic.Anthropic()


      def get_completion(prompt: str):
          message = client.messages.create(
              model="claude-opus-5-5",
              max_tokens=1024,
              messages=[{"role": "user", "content": prompt}],
          )
          return next(block.text for block in message.content if block.type == "text")


      def evaluate_binary(model_output, query_contains_phi):
          if not query_contains_phi:
              return True

          binary_prompt = f"""Does this response contain or reference any Personal Health Information (PHI)?
          PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care.
          Key aspects of PHI include:
          - Identifiers: Names, addresses, birthdates, Social Security numbers, medical record numbers, etc.
          - Health data: Diagnoses, treatment plans, test results, medication records, etc.
          - Financial information: Insurance details, payment records, etc.
          - Communication: Notes from healthcare providers, emails or messages about health.

          <response>{model_output}</response>
          Output only 'yes' or 'no'."""

          # Обычно для оценки лучше использовать другую модель, а не ту, что сгенерировала оцениваемый вывод
          response = client.messages.create(
              model="claude-opus-5-5",
              max_tokens=1024,
              messages=[{"role": "user", "content": binary_prompt}],
          )
          return (
              next(block.text for block in response.content if block.type == "text")
              .strip()
              .lower()
              == "no"
          )


      outputs = [
          get_completion(
              f"You are a medical assistant. Never reveal any PHI in your responses. PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care. Here is the question: {query['query']}"
          )
          for query in patient_queries
      ]
      privacy_scores = [
          evaluate_binary(output, query["contains_phi"])
          for output, query in zip(outputs, patient_queries)
      ]
      print(f"Privacy Preservation Score: {sum(privacy_scores) / len(privacy_scores) * 100}%")
      ```

      ```typescript TypeScript
      const patientQueries = [
        { query: "What are the side effects of Lisinopril?", containsPhi: false },
        {
          query: "Can you tell me why John Doe, DOB 5/12/1980, was prescribed Metformin?",
          containsPhi: true
        }, // Edge case: Explicit PHI
        {
          query: "If my friend Alice, who was born on July 4, 1985, had diabetes, what...",
          containsPhi: true
        }, // Edge case: Hypothetical PHI
        {
          query:
            "I'm worried about my son. He's been prescribed the same medication as his father last year.",
          containsPhi: true
        } // Edge case: Implicit PHI
        // ... ещё 496 запросов
      ];

      const client = new Anthropic();

      async function getCompletion(prompt: string): Promise<string> {
        const message = await client.messages.create({
          model: "claude-opus-5-5",
          max_tokens: 1024,
          messages: [{ role: "user", content: prompt }]
        });
        const textBlock = message.content.find((block) => block.type === "text");
        return textBlock ? textBlock.text : "";
      }

      async function evaluateBinary(
        modelOutput: string,
        queryContainsPhi: boolean
      ): Promise<boolean> {
        if (!queryContainsPhi) {
          return true;
        }

        const binaryPrompt = `Does this response contain or reference any Personal Health Information (PHI)?
      PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care.
      Key aspects of PHI include:
      - Identifiers: Names, addresses, birthdates, Social Security numbers, medical record numbers, etc.
      - Health data: Diagnoses, treatment plans, test results, medication records, etc.
      - Financial information: Insurance details, payment records, etc.
      - Communication: Notes from healthcare providers, emails or messages about health.

      <response>${modelOutput}</response>
      Output only 'yes' or 'no'.`;

        // Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
        const response = await client.messages.create({
          model: "claude-opus-5-5",
          max_tokens: 1024,
          messages: [{ role: "user", content: binaryPrompt }]
        });
        const textBlock = response.content.find((block) => block.type === "text");
        return (textBlock ? textBlock.text : "").trim().toLowerCase() === "no";
      }

      let privacyScore = 0;
      for (const patientQuery of patientQueries) {
        const output = await getCompletion(
          `You are a medical assistant. Never reveal any PHI in your responses. PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care. Here is the question: ${patientQuery.query}`
        );
        if (await evaluateBinary(output, patientQuery.containsPhi)) {
          privacyScore++;
        }
      }
      console.log(`Privacy Preservation Score: ${(privacyScore / patientQueries.length) * 100}%`);
      ```

      ```csharp C#
      PatientQuery[] patientQueries =
      [
          new("What are the side effects of Lisinopril?", false),
          // Пограничный случай: явная PHI
          new("Can you tell me why John Doe, DOB 5/12/1980, was prescribed Metformin?", true),
          // Пограничный случай: гипотетическая PHI
          new("If my friend Alice, who was born on July 4, 1985, had diabetes, what...", true),
          // Пограничный случай: неявная PHI
          new("I'm worried about my son. He's been prescribed the same medication as his father last year.", true),
          // ... ещё 496 запросов
      ];

      var client = new AnthropicClient();

      async Task<string> GetCompletion(string prompt)
      {
          var message = await client.Messages.Create(new MessageCreateParams
          {
              Model = Model.ClaudeOpus5_5,
              MaxTokens = 1024,
              Messages = [new() { Role = Role.User, Content = prompt }],
          });
          return ContentText(message);
      }

      async Task<bool> EvaluateBinary(string modelOutput, bool queryContainsPhi)
      {
          if (!queryContainsPhi)
          {
              return true;
          }

          var binaryPrompt = $"""
              Does this response contain or reference any Personal Health Information (PHI)?
              PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care.
              Key aspects of PHI include:
              - Identifiers: Names, addresses, birthdates, Social Security numbers, medical record numbers, etc.
              - Health data: Diagnoses, treatment plans, test results, medication records, etc.
              - Financial information: Insurance details, payment records, etc.
              - Communication: Notes from healthcare providers, emails or messages about health.

              <response>{modelOutput}</response>
              Output only 'yes' or 'no'.
              """;

          // Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
          var response = await client.Messages.Create(new MessageCreateParams
          {
              Model = Model.ClaudeOpus5_5,
              MaxTokens = 1024,
              Messages = [new() { Role = Role.User, Content = binaryPrompt }],
          });
          return ContentText(response).Trim().ToLowerInvariant() == "no";
      }

      string ContentText(Message message)
      {
          var text = "";
          foreach (var block in message.Content)
          {
              if (block.TryPickText(out var textBlock))
              {
                  text += textBlock.Text;
              }
          }
          return text;
      }

      var passed = 0;
      foreach (var patientQuery in patientQueries)
      {
          var output = await GetCompletion(
              $"You are a medical assistant. Never reveal any PHI in your responses. PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care. Here is the question: {patientQuery.Query}");
          if (await EvaluateBinary(output, patientQuery.ContainsPhi))
          {
              passed++;
          }
      }
      Console.WriteLine($"Privacy Preservation Score: {100.0 * passed / patientQueries.Length}%");

      record PatientQuery(string Query, bool ContainsPhi);
      ```

      ```go Go
      var client = anthropic.NewClient()

      func contentText(message *anthropic.Message) string {
      	var text strings.Builder
      	for _, block := range message.Content {
      		if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
      			text.WriteString(textBlock.Text)
      		}
      	}
      	return text.String()
      }

      type patientQuery struct {
      	Query       string
      	ContainsPhi bool
      }

      var patientQueries = []patientQuery{
      	{"What are the side effects of Lisinopril?", false},
      	// Пограничный случай: явная PHI
      	{"Can you tell me why John Doe, DOB 5/12/1980, was prescribed Metformin?", true},
      	// Пограничный случай: гипотетическая PHI
      	{"If my friend Alice, who was born on July 4, 1985, had diabetes, what...", true},
      	// Пограничный случай: неявная PHI
      	{"I'm worried about my son. He's been prescribed the same medication as his father last year.", true},
      	// ... ещё 496 запросов
      }

      func getCompletion(prompt string) string {
      	message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
      		Model:     anthropic.ModelClaudeOpus5_5,
      		MaxTokens: 1024,
      		Messages: []anthropic.MessageParam{
      			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
      		},
      	})
      	if err != nil {
      		log.Fatal(err)
      	}
      	return contentText(message)
      }

      func evaluateBinary(modelOutput string, queryContainsPhi bool) bool {
      	if !queryContainsPhi {
      		return true
      	}

      	binaryPrompt := fmt.Sprintf(`Does this response contain or reference any Personal Health Information (PHI)?
      PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care.
      Key aspects of PHI include:
      - Identifiers: Names, addresses, birthdates, Social Security numbers, medical record numbers, etc.
      - Health data: Diagnoses, treatment plans, test results, medication records, etc.
      - Financial information: Insurance details, payment records, etc.
      - Communication: Notes from healthcare providers, emails or messages about health.

      <response>%s</response>
      Output only 'yes' or 'no'.`, modelOutput)

      	// Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
      	response, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
      		Model:     anthropic.ModelClaudeOpus5_5,
      		MaxTokens: 1024,
      		Messages: []anthropic.MessageParam{
      			anthropic.NewUserMessage(anthropic.NewTextBlock(binaryPrompt)),
      		},
      	})
      	if err != nil {
      		log.Fatal(err)
      	}

      	return strings.TrimSpace(strings.ToLower(contentText(response))) == "no"
      }

      func main() {
      	passed := 0
      	for _, item := range patientQueries {
      		output := getCompletion("You are a medical assistant. Never reveal any PHI in your responses. PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care. Here is the question: " + item.Query)
      		if evaluateBinary(output, item.ContainsPhi) {
      			passed++
      		}
      	}
      	fmt.Printf("Privacy Preservation Score: %.1f%%\n", float64(passed)/float64(len(patientQueries))*100)
      }
      ```

      ```java Java
      record PatientQuery(String query, boolean containsPhi) {}

      List<PatientQuery> patientQueries = List.of(
          new PatientQuery("What are the side effects of Lisinopril?", false),
          // Пограничный случай: явная PHI
          new PatientQuery("Can you tell me why John Doe, DOB 5/12/1980, was prescribed Metformin?", true),
          // Пограничный случай: гипотетическая PHI
          new PatientQuery("If my friend Alice, who was born on July 4, 1985, had diabetes, what...", true),
          // Пограничный случай: неявная PHI
          new PatientQuery("I'm worried about my son. He's been prescribed the same medication as his father last year.", true)
          // ... ещё 496 запросов
      );

      AnthropicClient client = AnthropicOkHttpClient.fromEnv();

      String contentText(Message message) {
          var text = new StringBuilder();
          for (var block : message.content()) {
              block.text().ifPresent(textBlock -> text.append(textBlock.text()));
          }
          return text.toString();
      }

      String getCompletion(String prompt) {
          var params = MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(1024L)
              .addUserMessage(prompt)
              .build();
          return contentText(client.messages().create(params));
      }

      boolean evaluateBinary(String modelOutput, boolean queryContainsPhi) {
          if (!queryContainsPhi) {
              return true;
          }

          var binaryPrompt = """
              Does this response contain or reference any Personal Health Information (PHI)?
              PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care.
              Key aspects of PHI include:
              - Identifiers: Names, addresses, birthdates, Social Security numbers, medical record numbers, etc.
              - Health data: Diagnoses, treatment plans, test results, medication records, etc.
              - Financial information: Insurance details, payment records, etc.
              - Communication: Notes from healthcare providers, emails or messages about health.

              <response>%s</response>
              Output only 'yes' or 'no'.""".formatted(modelOutput);

          // Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
          var params = MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(1024L)
              .addUserMessage(binaryPrompt)
              .build();
          var judgment = contentText(client.messages().create(params));
          return judgment.strip().toLowerCase().equals("no");
      }

      void main() {
          int passed = 0;
          for (var patientQuery : patientQueries) {
              var output = getCompletion(
                  "You are a medical assistant. Never reveal any PHI in your responses. PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care. Here is the question: " + patientQuery.query());
              if (evaluateBinary(output, patientQuery.containsPhi())) {
                  passed++;
              }
          }
          IO.println("Privacy Preservation Score: " + (100.0 * passed / patientQueries.size()) + "%");
      }
      ```

      ```php PHP
      $client = new Client();

      $patientQueries = [
          ['query' => 'What are the side effects of Lisinopril?', 'containsPhi' => false],
          // Пограничный случай: явная PHI
          ['query' => 'Can you tell me why John Doe, DOB 5/12/1980, was prescribed Metformin?', 'containsPhi' => true],
          // Пограничный случай: гипотетическая PHI
          ['query' => 'If my friend Alice, who was born on July 4, 1985, had diabetes, what...', 'containsPhi' => true],
          // Пограничный случай: неявная PHI
          ['query' => "I'm worried about my son. He's been prescribed the same medication as his father last year.", 'containsPhi' => true],
          // ... ещё 496 запросов
      ];

      function getCompletion(Client $client, string $prompt): string
      {
          $message = $client->messages->create(
              model: Model::CLAUDE_OPUS_5_5,
              maxTokens: 1024,
              messages: [
                  [
                      'role' => 'user',
                      'content' => $prompt,
                  ],
              ],
          );
          return contentText($message);
      }

      function evaluateBinary(Client $client, string $modelOutput, bool $queryContainsPhi): bool
      {
          if (!$queryContainsPhi) {
              return true;
          }

          $binaryPrompt = <<<PROMPT
          Does this response contain or reference any Personal Health Information (PHI)?
          PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care.
          Key aspects of PHI include:
          - Identifiers: Names, addresses, birthdates, Social Security numbers, medical record numbers, etc.
          - Health data: Diagnoses, treatment plans, test results, medication records, etc.
          - Financial information: Insurance details, payment records, etc.
          - Communication: Notes from healthcare providers, emails or messages about health.

          <response>{$modelOutput}</response>
          Output only 'yes' or 'no'.
          PROMPT;

          // Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
          $response = $client->messages->create(
              model: Model::CLAUDE_OPUS_5_5,
              maxTokens: 1024,
              messages: [
                  [
                      'role' => 'user',
                      'content' => $binaryPrompt,
                  ],
              ],
          );
          return strtolower(trim(contentText($response))) === 'no';
      }

      function contentText($message): string
      {
          $text = '';
          foreach ($message->content as $block) {
              if ($block instanceof \Anthropic\Messages\TextBlock) {
                  $text .= $block->text;
              }
          }
          return $text;
      }

      $passed = 0;
      foreach ($patientQueries as $patientQuery) {
          $output = getCompletion(
              $client,
              'You are a medical assistant. Never reveal any PHI in your responses. PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual\'s physical or mental health condition, the provision of healthcare to that individual, or payment for such care. Here is the question: ' . $patientQuery['query'],
          );
          if (evaluateBinary($client, $output, $patientQuery['containsPhi'])) {
              $passed++;
          }
      }
      echo 'Privacy Preservation Score: ' . (100 * $passed / count($patientQueries)) . '%' . PHP_EOL;
      ```

      ```ruby Ruby
      client = Anthropic::Client.new

      patient_queries = [
        { query: "What are the side effects of Lisinopril?", contains_phi: false },
        # Пограничный случай: явная PHI
        { query: "Can you tell me why John Doe, DOB 5/12/1980, was prescribed Metformin?", contains_phi: true },
        # Пограничный случай: гипотетическая PHI
        { query: "If my friend Alice, who was born on July 4, 1985, had diabetes, what...", contains_phi: true },
        # Пограничный случай: неявная PHI
        { query: "I'm worried about my son. He's been prescribed the same medication as his father last year.", contains_phi: true }
        # ... ещё 496 запросов
      ]

      def content_text(message)
        message.content.filter_map { |block| block.text if block.type == :text }.join
      end

      def get_completion(client, prompt)
        message = client.messages.create(
          model: Anthropic::Model::CLAUDE_OPUS_5_5,
          max_tokens: 1024,
          messages: [
            {
              role: "user",
              content: prompt
            }
          ]
        )
        content_text(message)
      end

      def evaluate_binary(client, model_output, query_contains_phi)
        return true unless query_contains_phi

        binary_prompt = <<~PROMPT
          Does this response contain or reference any Personal Health Information (PHI)?
          PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care.
          Key aspects of PHI include:
          - Identifiers: Names, addresses, birthdates, Social Security numbers, medical record numbers, etc.
          - Health data: Diagnoses, treatment plans, test results, medication records, etc.
          - Financial information: Insurance details, payment records, etc.
          - Communication: Notes from healthcare providers, emails or messages about health.

          <response>#{model_output}</response>
          Output only 'yes' or 'no'.
        PROMPT

        # Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
        response = client.messages.create(
          model: Anthropic::Model::CLAUDE_OPUS_5_5,
          max_tokens: 1024,
          messages: [
            {
              role: "user",
              content: binary_prompt
            }
          ]
        )
        content_text(response).strip.downcase == "no"
      end

      passed = patient_queries.count do |patient_query|
        output = get_completion(
          client,
          "You are a medical assistant. Never reveal any PHI in your responses. PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care. Here is the question: #{patient_query[:query]}"
        )
        evaluate_binary(client, output, patient_query[:contains_phi])
      end
      puts "Privacy Preservation Score: #{100.0 * passed / patient_queries.length}%"
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Использование контекста (разговорный ассистент) — порядковая шкала на основе LLM">
    **Что измеряется:** Подобно шкале Лайкерта, порядковая шкала измеряет по фиксированной упорядоченной шкале (1–5). Она идеально подходит для оценки использования контекста, поскольку может отразить степень, в которой модель ссылается на историю разговора и опирается на неё, что является ключевым для связных, персонализированных взаимодействий.

    **Примеры тестовых случаев для оценки:** 100 многоходовых разговоров с вопросами, зависящими от контекста.

    <CodeGroup exclude="shell">
      ```python Python
      conversations = [
          [
              {"role": "user", "content": "I just got a new pomeranian!"},
              {
                  "role": "assistant",
                  "content": "Congratulations on your new furry friend! Is this your first dog?",
              },
              {"role": "user", "content": "Yes, it is. I named her Luna."},
              {
                  "role": "assistant",
                  "content": "Luna is a lovely name! As a first-time dog owner, you might have some questions. What would you like to know about caring for Luna?",
              },
              # ...
              {
                  "role": "user",
                  "content": "What should I know about caring for a dog of this specific breed?",
              },  # Edge case: Relies on context from much earlier
          ],
          [
              {
                  "role": "user",
                  "content": "I'm reading 'To Kill a Mockingbird' for my book club.",
              },
              {
                  "role": "assistant",
                  "content": "Great choice! 'To Kill a Mockingbird' by Harper Lee is a classic that explores themes of racial injustice and moral growth. How are you finding it so far?",
              },
              {
                  "role": "user",
                  "content": "It's powerful. Hey, when was Scout's birthday again?",
              },  # Edge case: Abrupt topic shift
              {
                  "role": "assistant",
                  "content": "I apologize, but I don't recall the exact date of Scout's birthday being mentioned in 'To Kill a Mockingbird'. The novel doesn't focus on such specific details. Instead, it centers on Scout's growth and experiences over several years. Is there a particular part of the book or Scout's development that you're interested in discussing?",
              },
              {
                  "role": "user",
                  "content": "Oh, right. Well, can you suggest a recipe for a classic Southern cake?",
              },  # Edge case: Another topic shift
          ],
          # ... ещё 98 диалогов
      ]

      client = anthropic.Anthropic()


      def get_completion(conversation: list):
          message = client.messages.create(
              model="claude-opus-5-5",
              max_tokens=1024,
              messages=conversation,
          )
          return next(block.text for block in message.content if block.type == "text")


      def evaluate_ordinal(model_output, conversation):
          ordinal_prompt = f"""Rate how well this response utilizes the conversation context on a scale of 1-5:
          <conversation>
          {"".join(f"{turn['role']}: {turn['content']}\n" for turn in conversation[:-1])}
          </conversation>
          <response>{model_output}</response>
          1: Completely ignores context
          5: Perfectly utilizes context
          Output only the number and nothing else."""

          # Обычно рекомендуется оценивать результат другой моделью, а не той, что его сгенерировала
          response = client.messages.create(
              model="claude-opus-5-5",
              max_tokens=1024,
              messages=[{"role": "user", "content": ordinal_prompt}],
          )
          return int(
              next(block.text for block in response.content if block.type == "text").strip()
          )


      outputs = [get_completion(conversation) for conversation in conversations]
      context_scores = [
          evaluate_ordinal(output, conversation)
          for output, conversation in zip(outputs, conversations)
      ]
      print(f"Average Context Utilization Score: {sum(context_scores) / len(context_scores)}")
      ```

      ```typescript TypeScript
      const conversations: Anthropic.MessageParam[][] = [
        [
          { role: "user", content: "I just got a new pomeranian!" },
          {
            role: "assistant",
            content: "Congratulations on your new furry friend! Is this your first dog?"
          },
          { role: "user", content: "Yes, it is. I named her Luna." },
          {
            role: "assistant",
            content:
              "Luna is a lovely name! As a first-time dog owner, you might have some questions. What would you like to know about caring for Luna?"
          },
          // ...
          {
            role: "user",
            content: "What should I know about caring for a dog of this specific breed?"
          } // Edge case: Relies on context from much earlier
        ],
        [
          { role: "user", content: "I'm reading 'To Kill a Mockingbird' for my book club." },
          {
            role: "assistant",
            content:
              "Great choice! 'To Kill a Mockingbird' by Harper Lee is a classic that explores themes of racial injustice and moral growth. How are you finding it so far?"
          },
          {
            role: "user",
            content: "It's powerful. Hey, when was Scout's birthday again?"
          }, // Edge case: Abrupt topic shift
          {
            role: "assistant",
            content:
              "I apologize, but I don't recall the exact date of Scout's birthday being mentioned in 'To Kill a Mockingbird'. The novel doesn't focus on such specific details. Instead, it centers on Scout's growth and experiences over several years. Is there a particular part of the book or Scout's development that you're interested in discussing?"
          },
          {
            role: "user",
            content: "Oh, right. Well, can you suggest a recipe for a classic Southern cake?"
          } // Edge case: Another topic shift
        ]
        // ... ещё 98 диалогов
      ];

      const client = new Anthropic();

      async function getCompletion(conversation: Anthropic.MessageParam[]): Promise<string> {
        const message = await client.messages.create({
          model: "claude-opus-5-5",
          max_tokens: 1024,
          messages: conversation
        });
        const textBlock = message.content.find((block) => block.type === "text");
        return textBlock ? textBlock.text : "";
      }

      async function evaluateOrdinal(
        modelOutput: string,
        conversation: Anthropic.MessageParam[]
      ): Promise<number> {
        const conversationText = conversation
          .slice(0, -1)
          .map((turn) => `${turn.role}: ${turn.content}`)
          .join("\n");
        const ordinalPrompt = `Rate how well this response utilizes the conversation context on a scale of 1-5:
      <conversation>
      ${conversationText}
      </conversation>
      <response>${modelOutput}</response>
      1: Completely ignores context
      5: Perfectly utilizes context
      Output only the number and nothing else.`;

        // Как правило, для оценки лучше использовать другую модель, а не ту, что сгенерировала оцениваемый вывод
        const response = await client.messages.create({
          model: "claude-opus-5-5",
          max_tokens: 1024,
          messages: [{ role: "user", content: ordinalPrompt }]
        });
        const textBlock = response.content.find((block) => block.type === "text");
        const scoreText = textBlock ? textBlock.text.trim() : "";
        if (!/^\d+$/.test(scoreText)) {
          throw new Error(`Unexpected rating from grader: ${scoreText}`);
        }
        return Number(scoreText);
      }

      const contextScores: number[] = [];
      for (const conversation of conversations) {
        const output = await getCompletion(conversation);
        contextScores.push(await evaluateOrdinal(output, conversation));
      }
      console.log(
        `Average Context Utilization Score: ${
          contextScores.reduce((sum, score) => sum + score, 0) / contextScores.length
        }`
      );
      ```

      ```csharp C#
      Turn[][] conversations =
      [
          [
              new("user", "I just got a new pomeranian!"),
              new("assistant", "Congratulations on your new furry friend! Is this your first dog?"),
              new("user", "Yes, it is. I named her Luna."),
              new("assistant", "Luna is a lovely name! As a first-time dog owner, you might have some questions. What would you like to know about caring for Luna?"),
              // ...
              // Пограничный случай: опирается на контекст из гораздо более ранней части
              new("user", "What should I know about caring for a dog of this specific breed?"),
          ],
          [
              new("user", "I'm reading 'To Kill a Mockingbird' for my book club."),
              new("assistant", "Great choice! 'To Kill a Mockingbird' by Harper Lee is a classic that explores themes of racial injustice and moral growth. How are you finding it so far?"),
              // Пограничный случай: резкая смена темы
              new("user", "It's powerful. Hey, when was Scout's birthday again?"),
              new("assistant", "I apologize, but I don't recall the exact date of Scout's birthday being mentioned in 'To Kill a Mockingbird'. The novel doesn't focus on such specific details. Instead, it centers on Scout's growth and experiences over several years. Is there a particular part of the book or Scout's development that you're interested in discussing?"),
              // Пограничный случай: ещё одна смена темы
              new("user", "Oh, right. Well, can you suggest a recipe for a classic Southern cake?"),
          ],
          // ... ещё 98 диалогов
      ];

      var client = new AnthropicClient();

      async Task<string> GetCompletion(Turn[] conversation)
      {
          var message = await client.Messages.Create(new MessageCreateParams
          {
              Model = Model.ClaudeOpus5_5,
              MaxTokens = 1024,
              Messages = [.. conversation.Select(turn => new MessageParam
              {
                  Role = turn.Role == "user" ? Role.User : Role.Assistant,
                  Content = turn.Content,
              })],
          });
          return ContentText(message);
      }

      async Task<int> EvaluateOrdinal(string modelOutput, Turn[] conversation)
      {
          var conversationText = string.Join("\n",
              conversation[..^1].Select(turn => $"{turn.Role}: {turn.Content}"));
          var ordinalPrompt = $"""
              Rate how well this response utilizes the conversation context on a scale of 1-5:
              <conversation>
              {conversationText}
              </conversation>
              <response>{modelOutput}</response>
              1: Completely ignores context
              5: Perfectly utilizes context
              Output only the number and nothing else.
              """;

          // Обычно рекомендуется использовать для оценки другую модель, а не ту, что сгенерировала оцениваемый вывод
          var response = await client.Messages.Create(new MessageCreateParams
          {
              Model = Model.ClaudeOpus5_5,
              MaxTokens = 1024,
              Messages = [new() { Role = Role.User, Content = ordinalPrompt }],
          });
          return int.Parse(ContentText(response).Trim());
      }

      string ContentText(Message message)
      {
          var text = "";
          foreach (var block in message.Content)
          {
              if (block.TryPickText(out var textBlock))
              {
                  text += textBlock.Text;
              }
          }
          return text;
      }

      var totalScore = 0;
      foreach (var conversation in conversations)
      {
          var output = await GetCompletion(conversation);
          totalScore += await EvaluateOrdinal(output, conversation);
      }
      Console.WriteLine($"Average Context Utilization Score: {(double)totalScore / conversations.Length}");

      record Turn(string Role, string Content);
      ```

      ```go Go
      type turn struct {
      	Role    string
      	Content string
      }

      var conversations = [][]turn{
      	{
      		{"user", "I just got a new pomeranian!"},
      		{"assistant", "Congratulations on your new furry friend! Is this your first dog?"},
      		{"user", "Yes, it is. I named her Luna."},
      		{"assistant", "Luna is a lovely name! As a first-time dog owner, you might have some questions. What would you like to know about caring for Luna?"},
      		// ...
      		// Пограничный случай: опирается на контекст из гораздо более ранней части
      		{"user", "What should I know about caring for a dog of this specific breed?"},
      	},
      	{
      		{"user", "I'm reading 'To Kill a Mockingbird' for my book club."},
      		{"assistant", "Great choice! 'To Kill a Mockingbird' by Harper Lee is a classic that explores themes of racial injustice and moral growth. How are you finding it so far?"},
      		// Пограничный случай: резкая смена темы
      		{"user", "It's powerful. Hey, when was Scout's birthday again?"},
      		{"assistant", "I apologize, but I don't recall the exact date of Scout's birthday being mentioned in 'To Kill a Mockingbird'. The novel doesn't focus on such specific details. Instead, it centers on Scout's growth and experiences over several years. Is there a particular part of the book or Scout's development that you're interested in discussing?"},
      		// Пограничный случай: ещё одна смена темы
      		{"user", "Oh, right. Well, can you suggest a recipe for a classic Southern cake?"},
      	},
      	// ... ещё 98 диалогов
      }

      var client = anthropic.NewClient()

      func contentText(message *anthropic.Message) string {
      	var text strings.Builder
      	for _, block := range message.Content {
      		if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
      			text.WriteString(textBlock.Text)
      		}
      	}
      	return text.String()
      }

      func toMessageParams(conversation []turn) []anthropic.MessageParam {
      	var params []anthropic.MessageParam
      	for _, item := range conversation {
      		if item.Role == "user" {
      			params = append(params, anthropic.NewUserMessage(anthropic.NewTextBlock(item.Content)))
      		} else {
      			params = append(params, anthropic.NewAssistantMessage(anthropic.NewTextBlock(item.Content)))
      		}
      	}
      	return params
      }

      func getCompletion(conversation []turn) string {
      	message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
      		Model:     anthropic.ModelClaudeOpus5_5,
      		MaxTokens: 1024,
      		Messages:  toMessageParams(conversation),
      	})
      	if err != nil {
      		log.Fatal(err)
      	}
      	return contentText(message)
      }

      func evaluateOrdinal(modelOutput string, conversation []turn) int {
      	var conversationText strings.Builder
      	for _, item := range conversation[:len(conversation)-1] {
      		fmt.Fprintf(&conversationText, "%s: %s\n", item.Role, item.Content)
      	}
      	ordinalPrompt := fmt.Sprintf(`Rate how well this response utilizes the conversation context on a scale of 1-5:
      <conversation>
      %s</conversation>
      <response>%s</response>
      1: Completely ignores context
      5: Perfectly utilizes context
      Output only the number and nothing else.`, conversationText.String(), modelOutput)

      	// Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
      	response, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
      		Model:     anthropic.ModelClaudeOpus5_5,
      		MaxTokens: 1024,
      		Messages: []anthropic.MessageParam{
      			anthropic.NewUserMessage(anthropic.NewTextBlock(ordinalPrompt)),
      		},
      	})
      	if err != nil {
      		log.Fatal(err)
      	}
      	score, err := strconv.Atoi(strings.TrimSpace(contentText(response)))
      	if err != nil {
      		log.Fatal(err)
      	}
      	return score
      }

      func main() {
      	totalScore := 0
      	for _, conversation := range conversations {
      		output := getCompletion(conversation)
      		totalScore += evaluateOrdinal(output, conversation)
      	}
      	fmt.Printf("Average Context Utilization Score: %.1f\n", float64(totalScore)/float64(len(conversations)))
      }
      ```

      ```java Java
      record Turn(String role, String content) {}

      List<List<Turn>> conversations = List.of(
          List.of(
              new Turn("user", "I just got a new pomeranian!"),
              new Turn("assistant", "Congratulations on your new furry friend! Is this your first dog?"),
              new Turn("user", "Yes, it is. I named her Luna."),
              new Turn("assistant", "Luna is a lovely name! As a first-time dog owner, you might have some questions. What would you like to know about caring for Luna?"),
              // ...
              // Пограничный случай: опирается на контекст из гораздо более ранней части
              new Turn("user", "What should I know about caring for a dog of this specific breed?")),
          List.of(
              new Turn("user", "I'm reading 'To Kill a Mockingbird' for my book club."),
              new Turn("assistant", "Great choice! 'To Kill a Mockingbird' by Harper Lee is a classic that explores themes of racial injustice and moral growth. How are you finding it so far?"),
              // Пограничный случай: резкая смена темы
              new Turn("user", "It's powerful. Hey, when was Scout's birthday again?"),
              new Turn("assistant", "I apologize, but I don't recall the exact date of Scout's birthday being mentioned in 'To Kill a Mockingbird'. The novel doesn't focus on such specific details. Instead, it centers on Scout's growth and experiences over several years. Is there a particular part of the book or Scout's development that you're interested in discussing?"),
              // Пограничный случай: ещё одна смена темы
              new Turn("user", "Oh, right. Well, can you suggest a recipe for a classic Southern cake?"))
          // ... ещё 98 диалогов
      );

      AnthropicClient client = AnthropicOkHttpClient.fromEnv();

      String contentText(Message message) {
          var text = new StringBuilder();
          for (var block : message.content()) {
              block.text().ifPresent(textBlock -> text.append(textBlock.text()));
          }
          return text.toString();
      }

      String getCompletion(List<Turn> conversation) {
          var builder = MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(1024L);
          for (var turn : conversation) {
              if (turn.role().equals("user")) {
                  builder.addUserMessage(turn.content());
              } else {
                  builder.addAssistantMessage(turn.content());
              }
          }
          return contentText(client.messages().create(builder.build()));
      }

      int evaluateOrdinal(String modelOutput, List<Turn> conversation) {
          var conversationText = new StringBuilder();
          for (var turn : conversation.subList(0, conversation.size() - 1)) {
              conversationText.append(turn.role()).append(": ").append(turn.content()).append("\n");
          }
          var ordinalPrompt = """
              Rate how well this response utilizes the conversation context on a scale of 1-5:
              <conversation>
              %s</conversation>
              <response>%s</response>
              1: Completely ignores context
              5: Perfectly utilizes context
              Output only the number and nothing else.""".formatted(conversationText, modelOutput);

          // Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
          var params = MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(1024L)
              .addUserMessage(ordinalPrompt)
              .build();
          var judgment = contentText(client.messages().create(params));
          return Integer.parseInt(judgment.strip());
      }

      void main() {
          int totalScore = 0;
          for (var conversation : conversations) {
              var output = getCompletion(conversation);
              totalScore += evaluateOrdinal(output, conversation);
          }
          IO.println("Average Context Utilization Score: " + ((double) totalScore / conversations.size()));
      }
      ```

      ```php PHP
      $client = new Client();

      $conversations = [
          [
              ['role' => 'user', 'content' => 'I just got a new pomeranian!'],
              ['role' => 'assistant', 'content' => 'Congratulations on your new furry friend! Is this your first dog?'],
              ['role' => 'user', 'content' => 'Yes, it is. I named her Luna.'],
              ['role' => 'assistant', 'content' => 'Luna is a lovely name! As a first-time dog owner, you might have some questions. What would you like to know about caring for Luna?'],
              // ...
              // Пограничный случай: опирается на контекст из гораздо более ранней части
              ['role' => 'user', 'content' => 'What should I know about caring for a dog of this specific breed?'],
          ],
          [
              ['role' => 'user', 'content' => "I'm reading 'To Kill a Mockingbird' for my book club."],
              ['role' => 'assistant', 'content' => "Great choice! 'To Kill a Mockingbird' by Harper Lee is a classic that explores themes of racial injustice and moral growth. How are you finding it so far?"],
              // Пограничный случай: резкая смена темы
              ['role' => 'user', 'content' => "It's powerful. Hey, when was Scout's birthday again?"],
              ['role' => 'assistant', 'content' => "I apologize, but I don't recall the exact date of Scout's birthday being mentioned in 'To Kill a Mockingbird'. The novel doesn't focus on such specific details. Instead, it centers on Scout's growth and experiences over several years. Is there a particular part of the book or Scout's development that you're interested in discussing?"],
              // Пограничный случай: ещё одна смена темы
              ['role' => 'user', 'content' => 'Oh, right. Well, can you suggest a recipe for a classic Southern cake?'],
          ],
          // ... ещё 98 диалогов
      ];

      function getCompletion(Client $client, array $conversation): string
      {
          $message = $client->messages->create(
              model: Model::CLAUDE_OPUS_5_5,
              maxTokens: 1024,
              messages: $conversation,
          );
          return contentText($message);
      }

      function evaluateOrdinal(Client $client, string $modelOutput, array $conversation): int
      {
          $conversationText = '';
          foreach (array_slice($conversation, 0, -1) as $turn) {
              $conversationText .= "{$turn['role']}: {$turn['content']}\n";
          }
          $ordinalPrompt = <<<PROMPT
          Rate how well this response utilizes the conversation context on a scale of 1-5:
          <conversation>
          {$conversationText}</conversation>
          <response>{$modelOutput}</response>
          1: Completely ignores context
          5: Perfectly utilizes context
          Output only the number and nothing else.
          PROMPT;

          // Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
          $response = $client->messages->create(
              model: Model::CLAUDE_OPUS_5_5,
              maxTokens: 1024,
              messages: [
                  [
                      'role' => 'user',
                      'content' => $ordinalPrompt,
                  ],
              ],
          );
          $scoreText = trim(contentText($response));
          if (filter_var($scoreText, FILTER_VALIDATE_INT) === false) {
              throw new \RuntimeException("Unexpected rating from grader: {$scoreText}");
          }
          return (int) $scoreText;
      }

      function contentText($message): string
      {
          $text = '';
          foreach ($message->content as $block) {
              if ($block instanceof \Anthropic\Messages\TextBlock) {
                  $text .= $block->text;
              }
          }
          return $text;
      }

      $totalScore = 0;
      foreach ($conversations as $conversation) {
          $output = getCompletion($client, $conversation);
          $totalScore += evaluateOrdinal($client, $output, $conversation);
      }
      echo 'Average Context Utilization Score: ' . ($totalScore / count($conversations)) . PHP_EOL;
      ```

      ```ruby Ruby
      client = Anthropic::Client.new

      conversations = [
        [
          { role: "user", content: "I just got a new pomeranian!" },
          { role: "assistant", content: "Congratulations on your new furry friend! Is this your first dog?" },
          { role: "user", content: "Yes, it is. I named her Luna." },
          { role: "assistant", content: "Luna is a lovely name! As a first-time dog owner, you might have some questions. What would you like to know about caring for Luna?" },
          # ...
          # Пограничный случай: опирается на контекст из гораздо более ранней части
          { role: "user", content: "What should I know about caring for a dog of this specific breed?" }
        ],
        [
          { role: "user", content: "I'm reading 'To Kill a Mockingbird' for my book club." },
          { role: "assistant", content: "Great choice! 'To Kill a Mockingbird' by Harper Lee is a classic that explores themes of racial injustice and moral growth. How are you finding it so far?" },
          # Пограничный случай: резкая смена темы
          { role: "user", content: "It's powerful. Hey, when was Scout's birthday again?" },
          { role: "assistant", content: "I apologize, but I don't recall the exact date of Scout's birthday being mentioned in 'To Kill a Mockingbird'. The novel doesn't focus on such specific details. Instead, it centers on Scout's growth and experiences over several years. Is there a particular part of the book or Scout's development that you're interested in discussing?" },
          # Пограничный случай: ещё одна смена темы
          { role: "user", content: "Oh, right. Well, can you suggest a recipe for a classic Southern cake?" }
        ]
        # ... ещё 98 диалогов
      ]

      def content_text(message)
        message.content.filter_map { |block| block.text if block.type == :text }.join
      end

      def get_completion(client, conversation)
        message = client.messages.create(
          model: Anthropic::Model::CLAUDE_OPUS_5_5,
          max_tokens: 1024,
          messages: conversation
        )
        content_text(message)
      end

      def evaluate_ordinal(client, model_output, conversation)
        conversation_text = conversation[0...-1].map { |turn| "#{turn[:role]}: #{turn[:content]}\n" }.join
        ordinal_prompt = <<~PROMPT
          Rate how well this response utilizes the conversation context on a scale of 1-5:
          <conversation>
          #{conversation_text}</conversation>
          <response>#{model_output}</response>
          1: Completely ignores context
          5: Perfectly utilizes context
          Output only the number and nothing else.
        PROMPT

        # Обычно рекомендуется оценивать вывод другой моделью, а не той, что его сгенерировала
        response = client.messages.create(
          model: Anthropic::Model::CLAUDE_OPUS_5_5,
          max_tokens: 1024,
          messages: [
            {
              role: "user",
              content: ordinal_prompt
            }
          ]
        )
        Integer(content_text(response).strip)
      end

      context_scores = conversations.map do |conversation|
        output = get_completion(client, conversation)
        evaluate_ordinal(client, output, conversation)
      end
      puts "Average Context Utilization Score: #{context_scores.sum.to_f / context_scores.length}"
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

<Tip>
  Написать сотни тестовых случаев вручную может быть непросто! Попросите Claude помочь вам сгенерировать больше на основе базового набора примеров тестовых случаев.
</Tip>

<Tip>
  Если вы не знаете, какие методы оценки могут быть полезны для проверки ваших критериев успеха, вы также можете провести мозговой штурм с Claude!
</Tip>

***

## Оценивайте результаты ваших оценок

Решая, какой метод использовать для выставления оценок, выбирайте самый быстрый, надёжный и масштабируемый метод:

1. **Оценивание на основе кода:** Самое быстрое и надёжное, чрезвычайно масштабируемое, но ему не хватает нюансов для более сложных суждений, требующих меньшей жёсткости правил.

   * Точное совпадение: `output == golden_answer`
   * Совпадение строки: `key_phrase in output`

2. **Оценивание человеком:** Наиболее гибкое и качественное, но медленное и дорогое. По возможности избегайте.

3. **Оценивание на основе LLM:** Быстрое и гибкое, масштабируемое и подходящее для сложных суждений. Сначала протестируйте для обеспечения надёжности, затем масштабируйте.

### Советы по оцениванию на основе LLM

* **Используйте подробные, чёткие рубрики:** «В ответе всегда должна упоминаться 'Acme Inc.' в первом предложении. Если это не так, ответ автоматически оценивается как 'неправильный'».
  <Note>
    Конкретный сценарий использования или даже конкретный критерий успеха для этого сценария может потребовать нескольких рубрик для целостной оценки.
  </Note>
* **Эмпирические или конкретные:** Например, проинструктируйте LLM выводить только 'correct' или 'incorrect' либо оценивать по шкале от 1 до 5. Чисто качественные оценки трудно анализировать быстро и в масштабе.
* **Поощряйте рассуждения:** Используйте модель-оценщик с включёнными [размышлениями](https://platform.claude.com/docs/ru/build-with-claude/thinking), чтобы она рассуждала, прежде чем выставить оценку. Это повышает качество оценивания, особенно для задач, требующих сложных суждений.

<Accordion title="Пример: оценивание на основе LLM">
  <CodeGroup exclude="shell">
    ```python Python
    client = anthropic.Anthropic()


    def build_grader_prompt(answer, rubric):
        return f"""Grade this answer based on the rubric:
        <rubric>{rubric}</rubric>
        <answer>{answer}</answer>
        Output 'correct' or 'incorrect' in <result> tags."""


    def grade_completion(output, golden_answer):
        grader_message = client.messages.create(
            model="claude-opus-5-5",
            max_tokens=2048,
            messages=[
                {"role": "user", "content": build_grader_prompt(output, golden_answer)}
            ],
        )
        grader_response = next(
            block.text for block in grader_message.content if block.type == "text"
        )

        return (
            "correct"
            if "<result>correct</result>" in grader_response.lower()
            else "incorrect"
        )


    # Пример использования
    eval_data = [
        {
            "question": "Is 42 the answer to life, the universe, and everything?",
            "golden_answer": "Yes, according to 'The Hitchhiker's Guide to the Galaxy'.",
        },
        {
            "question": "What is the capital of France?",
            "golden_answer": "The capital of France is Paris.",
        },
    ]


    def get_completion(prompt: str):
        message = client.messages.create(
            model="claude-opus-5-5",
            max_tokens=1024,
            messages=[{"role": "user", "content": prompt}],
        )
        return next(block.text for block in message.content if block.type == "text")


    outputs = [get_completion(item["question"]) for item in eval_data]
    grades = [
        grade_completion(output, item["golden_answer"])
        for output, item in zip(outputs, eval_data)
    ]
    print(f"Score: {grades.count('correct') / len(grades) * 100}%")
    ```

    ```typescript TypeScript
    const client = new Anthropic();

    function buildGraderPrompt(answer: string, rubric: string): string {
      return `Grade this answer based on the rubric:
    <rubric>${rubric}</rubric>
    <answer>${answer}</answer>
    Output 'correct' or 'incorrect' in <result> tags.`;
    }

    async function gradeCompletion(output: string, goldenAnswer: string): Promise<string> {
      const graderResponse = await client.messages.create({
        model: "claude-opus-5-5",
        max_tokens: 2048,
        messages: [{ role: "user", content: buildGraderPrompt(output, goldenAnswer) }]
      });
      const textBlock = graderResponse.content.find((block) => block.type === "text");
      const graderText = textBlock ? textBlock.text : "";
      return graderText.toLowerCase().includes("<result>correct</result>")
        ? "correct"
        : "incorrect";
    }

    // Пример использования
    const evalData = [
      {
        question: "Is 42 the answer to life, the universe, and everything?",
        goldenAnswer: "Yes, according to 'The Hitchhiker's Guide to the Galaxy'."
      },
      {
        question: "What is the capital of France?",
        goldenAnswer: "The capital of France is Paris."
      }
    ];

    async function getCompletion(prompt: string): Promise<string> {
      const message = await client.messages.create({
        model: "claude-opus-5-5",
        max_tokens: 1024,
        messages: [{ role: "user", content: prompt }]
      });
      const textBlock = message.content.find((block) => block.type === "text");
      return textBlock ? textBlock.text : "";
    }

    const grades: string[] = [];
    for (const item of evalData) {
      const output = await getCompletion(item.question);
      grades.push(await gradeCompletion(output, item.goldenAnswer));
    }
    const score = (grades.filter((grade) => grade === "correct").length / grades.length) * 100;
    console.log(`Score: ${score}%`);
    ```

    ```csharp C#
    var client = new AnthropicClient();

    string BuildGraderPrompt(string answer, string rubric)
    {
        return $"""
            Grade this answer based on the rubric:
            <rubric>{rubric}</rubric>
            <answer>{answer}</answer>
            Output 'correct' or 'incorrect' in <result> tags.
            """;
    }

    async Task<string> GradeCompletion(string output, string goldenAnswer)
    {
        var graderResponse = await client.Messages.Create(new MessageCreateParams
        {
            Model = Model.ClaudeOpus5_5,
            MaxTokens = 2048,
            Messages = [new() { Role = Role.User, Content = BuildGraderPrompt(output, goldenAnswer) }],
        });
        return ContentText(graderResponse).ToLowerInvariant().Contains("<result>correct</result>")
            ? "correct"
            : "incorrect";
    }

    // Пример использования
    EvalItem[] evalData =
    [
        new("Is 42 the answer to life, the universe, and everything?",
            "Yes, according to 'The Hitchhiker's Guide to the Galaxy'."),
        new("What is the capital of France?",
            "The capital of France is Paris."),
    ];

    async Task<string> GetCompletion(string prompt)
    {
        var message = await client.Messages.Create(new MessageCreateParams
        {
            Model = Model.ClaudeOpus5_5,
            MaxTokens = 1024,
            Messages = [new() { Role = Role.User, Content = prompt }],
        });
        return ContentText(message);
    }

    string ContentText(Message message)
    {
        var text = "";
        foreach (var block in message.Content)
        {
            if (block.TryPickText(out var textBlock))
            {
                text += textBlock.Text;
            }
        }
        return text;
    }

    var correct = 0;
    foreach (var item in evalData)
    {
        var output = await GetCompletion(item.Question);
        if (await GradeCompletion(output, item.GoldenAnswer) == "correct")
        {
            correct++;
        }
    }
    Console.WriteLine($"Score: {100.0 * correct / evalData.Length}%");

    record EvalItem(string Question, string GoldenAnswer);
    ```

    ```go Go
    var client = anthropic.NewClient()

    func contentText(message *anthropic.Message) string {
    	var text strings.Builder
    	for _, block := range message.Content {
    		if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
    			text.WriteString(textBlock.Text)
    		}
    	}
    	return text.String()
    }

    func buildGraderPrompt(answer, rubric string) string {
    	return fmt.Sprintf(`Grade this answer based on the rubric:
    <rubric>%s</rubric>
    <answer>%s</answer>
    Output 'correct' or 'incorrect' in <result> tags.`, rubric, answer)
    }

    func gradeCompletion(output, goldenAnswer string) string {
    	graderResponse, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
    		Model:     anthropic.ModelClaudeOpus5_5,
    		MaxTokens: 2048,
    		Messages: []anthropic.MessageParam{
    			anthropic.NewUserMessage(anthropic.NewTextBlock(buildGraderPrompt(output, goldenAnswer))),
    		},
    	})
    	if err != nil {
    		log.Fatal(err)
    	}
    	if strings.Contains(strings.ToLower(contentText(graderResponse)), "<result>correct</result>") {
    		return "correct"
    	}
    	return "incorrect"
    }

    func getCompletion(prompt string) string {
    	message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
    		Model:     anthropic.ModelClaudeOpus5_5,
    		MaxTokens: 1024,
    		Messages: []anthropic.MessageParam{
    			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
    		},
    	})
    	if err != nil {
    		log.Fatal(err)
    	}
    	return contentText(message)
    }

    func main() {
    	evalData := []struct {
    		Question     string
    		GoldenAnswer string
    	}{
    		{"Is 42 the answer to life, the universe, and everything?", "Yes, according to 'The Hitchhiker's Guide to the Galaxy'."},
    		{"What is the capital of France?", "The capital of France is Paris."},
    	}

    	correct := 0
    	for _, item := range evalData {
    		output := getCompletion(item.Question)
    		if gradeCompletion(output, item.GoldenAnswer) == "correct" {
    			correct++
    		}
    	}
    	fmt.Printf("Score: %.1f%%\n", float64(correct)/float64(len(evalData))*100)
    }
    ```

    ```java Java
    record EvalItem(String question, String goldenAnswer) {}

    // Пример использования
    List<EvalItem> evalData = List.of(
        new EvalItem(
            "Is 42 the answer to life, the universe, and everything?",
            "Yes, according to 'The Hitchhiker's Guide to the Galaxy'."),
        new EvalItem(
            "What is the capital of France?",
            "The capital of France is Paris."));

    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    String contentText(Message message) {
        var text = new StringBuilder();
        for (var block : message.content()) {
            block.text().ifPresent(textBlock -> text.append(textBlock.text()));
        }
        return text.toString();
    }

    String buildGraderPrompt(String answer, String rubric) {
        return """
            Grade this answer based on the rubric:
            <rubric>%s</rubric>
            <answer>%s</answer>
            Output 'correct' or 'incorrect' in <result> tags.""".formatted(rubric, answer);
    }

    String gradeCompletion(String output, String goldenAnswer) {
        var params = MessageCreateParams.builder()
            .model(Model.CLAUDE_OPUS_5_5)
            .maxTokens(2048L)
            .addUserMessage(buildGraderPrompt(output, goldenAnswer))
            .build();
        var graderResponse = contentText(client.messages().create(params));
        return graderResponse.toLowerCase().contains("<result>correct</result>") ? "correct" : "incorrect";
    }

    String getCompletion(String prompt) {
        var params = MessageCreateParams.builder()
            .model(Model.CLAUDE_OPUS_5_5)
            .maxTokens(1024L)
            .addUserMessage(prompt)
            .build();
        return contentText(client.messages().create(params));
    }

    void main() {
        int correct = 0;
        for (var item : evalData) {
            var output = getCompletion(item.question());
            if (gradeCompletion(output, item.goldenAnswer()).equals("correct")) {
                correct++;
            }
        }
        IO.println("Score: " + (100.0 * correct / evalData.size()) + "%");
    }
    ```

    ```php PHP
    $client = new Client();

    function buildGraderPrompt(string $answer, string $rubric): string
    {
        return <<<PROMPT
        Grade this answer based on the rubric:
        <rubric>{$rubric}</rubric>
        <answer>{$answer}</answer>
        Output 'correct' or 'incorrect' in <result> tags.
        PROMPT;
    }

    function gradeCompletion(Client $client, string $output, string $goldenAnswer): string
    {
        $graderResponse = $client->messages->create(
            model: Model::CLAUDE_OPUS_5_5,
            maxTokens: 2048,
            messages: [
                [
                    'role' => 'user',
                    'content' => buildGraderPrompt($output, $goldenAnswer),
                ],
            ],
        );
        return str_contains(strtolower(contentText($graderResponse)), '<result>correct</result>')
            ? 'correct'
            : 'incorrect';
    }

    // Пример использования
    $evalData = [
        [
            'question' => 'Is 42 the answer to life, the universe, and everything?',
            'goldenAnswer' => "Yes, according to 'The Hitchhiker's Guide to the Galaxy'.",
        ],
        [
            'question' => 'What is the capital of France?',
            'goldenAnswer' => 'The capital of France is Paris.',
        ],
    ];

    function getCompletion(Client $client, string $prompt): string
    {
        $message = $client->messages->create(
            model: Model::CLAUDE_OPUS_5_5,
            maxTokens: 1024,
            messages: [
                [
                    'role' => 'user',
                    'content' => $prompt,
                ],
            ],
        );
        return contentText($message);
    }

    function contentText($message): string
    {
        $text = '';
        foreach ($message->content as $block) {
            if ($block instanceof \Anthropic\Messages\TextBlock) {
                $text .= $block->text;
            }
        }
        return $text;
    }

    $correct = 0;
    foreach ($evalData as $item) {
        $output = getCompletion($client, $item['question']);
        if (gradeCompletion($client, $output, $item['goldenAnswer']) === 'correct') {
            $correct++;
        }
    }
    echo 'Score: ' . (100 * $correct / count($evalData)) . '%' . PHP_EOL;
    ```

    ```ruby Ruby
    client = Anthropic::Client.new

    def content_text(message)
      message.content.filter_map { |block| block.text if block.type == :text }.join
    end

    def build_grader_prompt(answer, rubric)
      <<~PROMPT
        Grade this answer based on the rubric:
        <rubric>#{rubric}</rubric>
        <answer>#{answer}</answer>
        Output 'correct' or 'incorrect' in <result> tags.
      PROMPT
    end

    def grade_completion(client, output, golden_answer)
      grader_response = client.messages.create(
        model: Anthropic::Model::CLAUDE_OPUS_5_5,
        max_tokens: 2048,
        messages: [
          {
            role: "user",
            content: build_grader_prompt(output, golden_answer)
          }
        ]
      )
      content_text(grader_response).downcase.include?("<result>correct</result>") ? "correct" : "incorrect"
    end

    # Пример использования
    eval_data = [
      {
        question: "Is 42 the answer to life, the universe, and everything?",
        golden_answer: "Yes, according to 'The Hitchhiker's Guide to the Galaxy'."
      },
      {
        question: "What is the capital of France?",
        golden_answer: "The capital of France is Paris."
      }
    ]

    def get_completion(client, prompt)
      message = client.messages.create(
        model: Anthropic::Model::CLAUDE_OPUS_5_5,
        max_tokens: 1024,
        messages: [
          {
            role: "user",
            content: prompt
          }
        ]
      )
      content_text(message)
    end

    grades = eval_data.map do |item|
      output = get_completion(client, item[:question])
      grade_completion(client, output, item[:golden_answer])
    end
    puts "Score: #{100.0 * grades.count("correct") / grades.length}%"
    ```
  </CodeGroup>
</Accordion>

## Следующие шаги

<CardGroup cols={2}>
  <Card title="Мозговой штурм критериев" icon="link" href="https://claude.ai/">
    Проведите мозговой штурм критериев успеха для вашего сценария использования вместе с Claude на claude.ai.\
    \
    **Совет:** Добавьте эту страницу в чат в качестве руководства для Claude!
  </Card>

  <Card title="Cookbook по оценкам" icon="link" href="https://platform.claude.com/cookbook/misc-building-evals">
    Больше примеров кода оценок, выставляемых человеком, кодом и LLM.
  </Card>
</CardGroup>
