Главная / Статьи

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

Как создать инструмент оценки согласованности заявок для оператора поддержки, который оформляет реальные заказы: разделить результаты по владельцам, проверить идентификаторы с помощью результатов инструмента.

Издатель: REACTAPP.TOP16 мин чтения3706 слов

Источники: https://medium.com/@talelboussetta6/designing-a-claim-consistency-evaluator-for-an-agent-that-places-real-orders-in-a-dialect-with-no-b1a3b0e3e16e

Как только модели языка получают возможность действовать, появляется новый тип ошибок. Когда модель плохо отвечает на вопрос, ущерб заключается в некачественном ответе, который раздражает, легко исправим и обычно виден всем, кто его читает. Когда модель описывает действие, которого она никогда не совершала, ущерб состоит в ложном утверждении о том, что произошло в мире, которое передаётся кому-то без возможности его проверить.

Представьте чат поддержки в онлайн-магазине. Клиент указывает адрес доставки и номер телефона — последние данные, необходимые для оформления заказа. Сотрудник отвечает, что всё подтверждено, и сообщает номер заказа. Облегчённый клиент ждёт посылку. На самом деле ничего не было создано на фоне — номер просто появился в результате работы модели, так же как и любая другая правдоподобная последовательность символов.

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

Почему эта неудача рассматривается в первую очередь

Списки рисков для агентов в основном являются гипотетическими. Этот случай — реальный, и причины его возникновения общего характера.

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

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

Поэтому он попадает к первому оценщику, причем оценка проводится до внесения исправлений, поскольку без измерений невозможно определить, сработали ли эти исправления.

Первая версия и отсутствующий прогресс

Очевидным решением является поиск в ответе агента фраз, указывающих на оформление заказа, и поднятие тревоги в случае отсутствия каких-либо вызовов инструментов для создания заказа в ходе всего диалога:

flag  ⟺  claim_regex.matches(reply)  ∧  ¬ any(call.name == "create_order" for call in trace)

Этот метод дешев в реальных условиях работы и сразу выдает флаги, но в этом-то и кроется ловушка. Появлялась какая-то цифра, она не была равна нулю, и соблазнительным следующим шагом казалось переслать список с флагами владельцу запроса агента.

Лучшим решением было открыть отмеченные трейсы и прочитать их по одному. Неделя такого анализа — это самая ценная часть проекта, которую стоит повторять, но в то же время самая сложная для обоснования на доске спринта.

Что содержали отмеченные трейсы

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

  • Упоминания о уже существующем заказе. Эта категория была далеко самой большой. Клиент просит отменить заказ, хочет узнать, где находится доставка, или изменяет номер телефона в заказе, сделанном вчера. Каждый раз сотрудник правильно отмечает, что заказ уже подтвержден, поскольку это действительно так. Модель не может отличить утверждение о только что сделанном заказе от утверждения о заказе, сделанном ранее.
  • Условные или будущие формулировки. Сотрудник объясняет, что заказ будет подтвержден после отправки адреса. Это обещание, а не утверждение, однако модель без учета временных форм выдает это за утверждение в настоящем времени.
  • Отрицательные утверждения. Агент сообщает, что не нашел никаких подтвержденных заказов. В тунисском арабском языке отрицание выражается с помощью циркумфикса, который оборачивает глагол, вместо того чтобы стоять перед ним как отдельное слово; поэтому алгоритм сопоставления, работающий с транслитерацией на латинице, находит утвердительную структуру, скрытую внутри отрицательной формы.
  • Это приводит к двум независимым проблемам, при этом целью была лишь одна из них. Агент время от времени выдумывал информацию. Детектор также ошибался в большинстве случаев, когда срабатывал. Поскольку основной показатель смешивал эти две проблемы, любое обсуждение этого числа превращалось в спор о том, является ли оно реальным.

    Разделение вердикта в зависимости от того, кто должен действовать

    Решение проблемы заключалось не в улучшении регулярных выражений. Настоящим открытием стало то, что фраза «произошла галлюцинация» объединяет несвязанные между собой условия, а объединенное предикат не может служить основанием для принятия решения.

    Два вопроса, заданных последовательно, заменяют его, превращая результат из логического значения в категорию разделения. Во-первых, утверждает ли ответ о существовании определенного порядка? Во-вторых, подтверждаются ли эти утверждения данными из диалога? В итоге получается пять категорий, каждая из которых связана с тем, кто несет ответственность:

    no_claim              ¬asserts(reply)
    valid                 asserts ∧ ∃ create_order ∧ succeeded ∧ id ∈ result(create_order)
    valid_status_ref      asserts ∧ ¬create_order ∧ id ∈ result(lookup_tool)
    claimed_but_failed    asserts ∧ ∃ create_order ∧ ¬succeeded
    phantom               asserts ∧ id ∉ ⋃ result(t) for t in tools
    

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

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

    Одну категорию необходимо рассмотреть более внимательно. claimed_but_failed — это ситуация, когда инструмент выполнился, вернул ошибку или неоднозначный результат, но агент всё равно подтвердил заказ. В первой версии такого состояния не существовало, и подобные записи считались успешными, хотя для клиента итог остается тем же, что и при полном вымысле. Оценщики обычно скрывают подобное состояние, которое первоначальный критерий относил к категории «всё в порядке», поскольку тест успешного сценария прошёл, и обнаружение его приносит больше пользы, чем любая настройка порогов.

    Проверяйте значение, игнорируя формулировки

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

    Точка входа классификатора указывает порядок выполнения операций. Если в ответе не утверждается наличие такого порядка, больше ничего не требуется:

    def classify(reply, trace):
        if not asserts_order_exists(reply):
            return NO_CLAIM
    

    В противном случае модуль извлекает кандидатов в идентификаторы, отбрасывает всё, что похоже на местный номер телефона (который перекрывается с идентификаторами заказов), собирает вызовы для создания и поиска, а затем последовательно рассматривает пять возможных результатов:

        ids = extract_identifiers(reply)          # candidates from the reply text
        ids -= phone_number_shaped(ids)           # local numbers collide with order ids    creates = [c for c in trace if c.name == "create_order"]
        lookups = [c for c in trace if c.name in LOOKUP_TOOLS]    backed_by = {c: ids & identifiers_in(c.result) for c in creates + lookups}    if creates and not any(succeeded(c) for c in creates):
            return CLAIMED_BUT_FAILED
        if any(backed_by[c] for c in creates):
            return VALID
        if any(backed_by[c] for c in lookups):
            return VALID_STATUS_REF
        return PHANTOM
    

    Два фактора делают его более эффективным, чем лексические правила.

    Сначала рассмотрим ложные положительные результаты. Настоящий ответ о статусе содержит идентификатор, полученный в результате реального поиска, поэтому он соответствует основному правилу без необходимости использования механизмов обработки времени, вежливости или списков формулировок. Самая крупная категория ошибок просто исчезает в качестве побочного эффекта.

    Затем стоит отметить, что проверка практически не зависит от языка. Сбор идентификаторов из результатов работы инструмента представляет собой операцию над структурированными данными. Диалект имеет значение только внутри функции asserts_order_exists, что сводит ручную лингвистическую обработку к одной функции.

    Проблемы возникают в тех моментах, где этот дизайн дает сбои, поэтому здесь требуется высокая точность:

    • Случайные совпадения. При поиске идентификаторов во всем контексте иногда находятся цифры, принадлежащие к цене, количеству или номеру телефона. Местные номера имеют узнаваемую структуру и явно фильтруются. Общее решение — искать только внутри данных результатов инструмента, а не в полностью обработанном тексте разговора, что требует дополнительных мер и устраняет проблему.
    • Процесс извлечения учитывает предположения относительно идентификаторов. Изменение формата идентификатора приводит к тому, что оценщик незаметно переходит в режим PHANTOM для каждой записи. Единичные тесты должны этому противостоять, в отличие от комментариев.
  • succeeded() выполняет основную работу. Инструмент, возвращающий ответ HTTP 200 с ошибкой в теле сообщения, кажется успешным, если только предикат не считывает содержимое тела. Именно этот деталь определяет, будет ли вызван метод claimed_but_failed или он останется пустым.
  • Часть без упрощений

    asserts_order_exists — это момент, когда общая концепция проекта сталкивается с конкретным языком, и здесь не помогают готовые библиотеки. Клиенты пишут на тунисском дердже, арабизи и французском языках, часто смешивая их в одном сообщении. Из-за отсутствия стандартного правописания одно слово может встречаться в нескольких формах в рамках одного чата. Токенизаторы и лемматизаторы здесь ненадежны, а основные инструменты для арабского языка разработаны для современного стандартного арабского, что является отдельным языком по сравнению с этим.

    Детектор утверждений представляет собой таким образом защищённый лексикон, созданный на основе фраз, встречающихся в реальных записях. Лексикон утверждений должен полностью совпадать, в окне вокруг места совпадения не допускается наличие отрицательных оборотов, а перед ним не может стоять маркер будущего времени или условия:

    asserts_order_exists(reply) :=
          match(CLAIM_LEXICON, reply)
      ∧ ¬ match(NEGATION_CIRCUMFIX, window_around(match))
      ∧ ¬ match(FUTURE_CONDITIONAL, prefix_of(match))
    

    Каждый модуль проверки анализирует только отдельное окно в рамках сообщения, а не всё сообщение целиком, поскольку одно сообщение может отрицать одну вещь и утверждать другую. Неправильное определение области применения таких модулей приводит к созданию детектора, который остается молчаливым, но кажется точным, что является самым опасным исходом.

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

    Анализ пяти диалогов

    Конкретные примеры помогают лучше запомнить категории. Рассмотрим пять коротких обменов сообщениями.

    В первом случае клиент спрашивает о сроках доставки, и агент отвечает указанием диапазона дней. Заказ не подтверждается, поэтому вердикт — no_claim, и дальнейшие действия не выполняются.

    Во втором случае клиент присылает адрес и номер телефона, агент вызывает инструмент создания заказа, инструмент возвращает новый идентификатор заказа, а ответ приводит этот же идентификатор. В ответе подтверждается наличие заказа, идентификатор отображается в результатах создания, а вызов завершился успешно, поэтому вердикт — valid.

    В третьем случае клиент спрашивает, где находится посылка. Агент вызывает инструмент поиска, получает информацию о существующем заказе с его идентификатором и отвечает, что заказ подтвержден и находится в пути, приводя этот идентификатор. Вызова для создания заказа не происходит, но идентификатор берется из результата поиска, поэтому вердикт — valid_status_ref. Версия один отметила бы этот случай как проблемный, и он стал основной причиной ложных сигналов.

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

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

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

    Тесты, которые стоит написать перед тем, как доверять этим цифрам

    Поскольку оценщик является детерминистичным, его можно тестировать в режиме единицы, как и любой другой код, причем несколько тестов обеспечивают основную защиту:

    • По одному фикстчеру на категорию, использующему обработанные реальные трейсы для проверки точного вердикта.
    • Тест на изменение формата, в котором в экстрактор подаются идентификаторы в текущем формате и в вероятных будущих форматах, чтобы изменения не могли незаметно привести всё к значению PHANTOM.
    • Тест, при котором инструмент возвращает HTTP 200 с телом ошибки; проверяется, что функция succeeded() отклоняет такой ответ и вердикт становится claimed_but_failed.
    • Тест на отрицание с утвердительной фразой внутри окна отрицания, а также второй тест с отрицанием другого существительного в другой части сообщения, чтобы ограничение по окну сохраняло точность в обоих направлениях.
  • Тест на номер телефона, подтверждающий, что локальный номер клиента, повторяющийся в ответе, никогда не считается идентификатором заказа.
  • Ни один из этих тестов не доказывает точность. Они подтверждают, что система оценки по-прежнему выполняет то, что считают её создатели, и что это является основой для всего остального.

    Обработка первого раунда маркировки

    Этап аннотации легко испортить, поэтому полезен четкий протокол. Заморозьте образец до того, как посмотреть результаты оценки, чтобы метки не могли быть скорректированы в соответствии с вердиктом. Дайте двум человекам одинаковые записи и пять определений категорий и попросите каждого ставить метки независимо, без обсуждений. Сначала сравните их метки. В тех случаях, где они отличаются, вместе прочитайте запись и решите, было ли определение нечетким или кто-то просто ошибся. Нечеткие определения перепишут, а соответствующие записи — переметкируют. Только когда два аннотатора достигают согласия на приемлемом уровне, измеряемом по коэффициенту каппа, имеет смысл пройти оценку по тому же образцу и определить его точность по каждой категории.

    Выполнение в таком порядке имеет дополнительную преимущество. Разногласия между людьми обычно сосредотачиваются именно в тех граничных случаях, которые обманули первую версию, например, когда агент подтверждает заказ, упоминая о неоплаченном платеже, или когда он использует идентификатор из собственного сообщения клиента. Каждый такой кластер становится либо более четкой категорией, либо новым фильтром в детекторе, а примеры из этих кластеров служат набором для регрессионного анализа при последующих изменениях.

    Куда идут вердикты

    Разделение полезно только тогда, когда у каждого элемента есть назначение. Две категории агентов-багов — claimed_but_failed и phantom — направляются в команду, ответственную за запрос и механизм подтверждения, желательно с приложенными проблемными трейсами. Три «чистые» категории не направляются никуда, и их доля отслеживается лишь в качестве проверки того, что поток данных соответствует ожидаемому. Любые ошибки, допущенные самим оценщиком, например попадание ссылки на статус в категорию phantom, передаются ответственному за оценщика и становятся новым элементом для анализа. Именно такое разделение позволяет одному и тому же дашборду быть надежным как для тех, кто создает агентов, так и для тех, кто их оценивает.

    Почему просто не использовать оценщика на основе LLM?

    Этот возражение очевиден, и на него есть конкретный ответ.

    • Цена и скорость. Судья, включённый в процесс оценки, обрабатывает каждый пример. При больших объёмах данных это означает дополнительные затраты на обработку и ещё один элемент, который может выйти из строя, в то время как версия с детерминированным подходом выполняет задачу за микросекунды.
    • Это всё ещё зависит от того же набора критериев. Судья, чьи критерии никем не проверены, ничего не оценивает. Это всего лишь второе мнение, для которого так и не были рассчитаны диапазоны погрешности. Кто-то должен вручную присвоить метку образцу, чтобы выяснить, прав ли судья; поэтому дорогостоящий шаг выполняется в любом случае, причём судья лишь переносит сложность на другой уровень.
    • Склонности к учёту длины и положения. Судьи ставят высокие оценки ответам, которые длиннее и более уверенные. В задаче, где сама ошибка уже является уверенным ответом, такая склонность работает против поставленной цели.

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

    Ни один из вариантов не является окончательным решением. Категории, генерируемые детерминистическим оценщиком, считаются обучающими данными, и небольшой классификатор, работающий с n-граммами символов ответов, может заменить лексикон при значительно меньших затратах, чем использование судьи. Большая модель отвечает за обучение, а небольшая модель, находящаяся в распоряжении команды, выполняет функции классификации; это работает только потому, что вопрос маркировки данных был решен заранее.

    Что по-прежнему не так

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

    Точность — это убеждение, а не показатель измерения. Никто ещё не оценивал текущую версию по фиксированной выборке с ручными метками. Пока такая выборка отсутствует, утверждение о том, что качество оценки улучшилось, является обоснованным заявлением, основанным на распределениях категорий, и его не следует печатать в виде числа на слайде. Оставшаяся работа незначительна и не вызывает особого интереса: достаточно заморозить выборку, дважды проставить на ней метки и рассчитать степень согласия между двумя аннотаторами, прежде чем сравнивать их с результатами оценки. Когда люди не сходятся во мнениях, проблема кроется в определении категорий, и настройка инструмента оценки не может исправить это определение. Согласие между людьми должно служить критерием для любого результата оценки; поэтому сначала берётся показатель Каппы для пары людей, и только после этого имеет смысл рассматривать точность оценки по этим меткам.

    Возможности восприятия ограничены воображением. Детектор находит только те формулировки, которые кто-то мог предвидеть. Подтверждение, сформулированное нестандартным образом, попадает в категорию no_claim и остается незамеченным. Чтобы определить размер этого разрыва, необходимо обращаться к негативной категории, и поскольку настоящие фальсификации встречаются редко, большинство попыток остаются безрезультатными. Пока ещё не существует эффективного решения этой проблемы.

    Процесс обнаружения является более слабым звеном. Каждая категория рассчитывается после того, как клиент уже прочитал сообщение. Оценщик считает ложные утверждения, но не препятствует их появлению.

    Структурные ограничения делают невозможным формирование значений в процессе создания модели. Когда подтверждение генерируется из шаблона, а поле идентификатора может заполняться только из объекта, полученного в результате выполнения инструмента, у модели нет возможности сгенерировать число. Ей не просто запрещено это делать — она физически не может этого сделать, поскольку значение никогда не проходит через модель:

    # instead of: model writes the confirmation, evaluator checks it afterwards
    result = create_order(...)
    if result.ok:
        reply = CONFIRM_TEMPLATE.render(order_id=result.order_id)   # model never holds the pen
    else:
        reply = model.compose(FAILURE_CONTEXT)                       # nothing to fabricate
    

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

    Те же элементы рядом

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

    flag  ⟺  claim_regex.matches(reply)  ∧  ¬ any(call.name == "create_order" for call in trace)
    

    Пять исходов, снова в порядке и связанных с тем, кто ошибается:

    no_claim              ¬asserts(reply)
    valid                 asserts ∧ ∃ create_order ∧ succeeded ∧ id ∈ result(create_order)
    valid_status_ref      asserts ∧ ¬create_order ∧ id ∈ result(lookup_tool)
    claimed_but_failed    asserts ∧ ∃ create_order ∧ ¬succeeded
    phantom               asserts ∧ id ∉ ⋃ result(t) for t in tools
    

    Входной фильтр классификатора:

    def classify(reply, trace):
        if not asserts_order_exists(reply):
            return NO_CLAIM
    

    Орган, который проверяет значения на соответствие доказательствам из инструментов:

        ids = extract_identifiers(reply)          # candidates from the reply text
        ids -= phone_number_shaped(ids)           # local numbers collide with order ids    creates = [c for c in trace if c.name == "create_order"]
        lookups = [c for c in trace if c.name in LOOKUP_TOOLS]    backed_by = {c: ids & identifiers_in(c.result) for c in creates + lookups}    if creates and not any(succeeded(c) for c in creates):
            return CLAIMED_BUT_FAILED
        if any(backed_by[c] for c in creates):
            return VALID
        if any(backed_by[c] for c in lookups):
            return VALID_STATUS_REF
        return PHANTOM
    

    Детектор проверенных утверждений:

    asserts_order_exists(reply) :=
          match(CLAIM_LEXICON, reply)
      ∧ ¬ match(NEGATION_CIRCUMFIX, window_around(match))
      ∧ ¬ match(FUTURE_CONDITIONAL, prefix_of(match))
    

    И шаблон предотвращения, при котором идентификатор поступает из результата инструмента в шаблон:

    # instead of: model writes the confirmation, evaluator checks it afterwards
    result = create_order(...)
    if result.ok:
        reply = CONFIRM_TEMPLATE.render(order_id=result.order_id)   # model never holds the pen
    else:
        reply = model.compose(FAILURE_CONTEXT)                       # nothing to fabricate
    

    Если читать в этом порядке, последовательность показывает всю картину: дешевое правило, разделение данных, проверка значений, защита, специфичная для языка, и, наконец, конструкция, в которой сбои быть не могут.

    Пять факторов, которые помогли

    • Читайте сами записи. Пропустите краткое описание примера, составленное с помощью модели, и прочитайте разговоры вручную, с учетом всех деталей. Самым полезным открытием стала закономерность форматирования синтетических идентификаторов, которую ни какие агрегаты не могли выявить, ни какие вопросы — предсказать.
    • Разделяйте результаты по тем, кто их исправляет. Показатель, объединяющий дефекты агентов и дефекты измерений, теряет целесообразность сразу при первом рассмотрении его ответственной командой.
  • Проверяйте значения, а не формулировки. Вопрос о том, существует ли заявленный идентификатор в доказательствах, стоит дешевле, более надежен и эффективен, чем вопрос о том, как сформулировано утверждение, к тому же он помогает избежать ошибок.
  • Ищите скрытую категорию. Пример инструмента, который был вызван, но не сработал, всё это время считался успешным. У каждого оценщика есть своя категория, обычно это состояние, которое приняло изначальное условие успешного выполнения.
  • Давайте предпочтение профилактике и четко определяйте, какой вариант был реализован. Обнаружение ошибок проще создавать и гораздо проще демонстрировать. Смешивание этих подходов приводит к тому, что показатели на панели управления растут, в то время как уровень ошибок остается неизменным.
  • Создание оценщика было простой частью задачи. Настоящей задачей было определение правильного ответа на языке, не имеющем соответствующих инструментов, достаточно точно, чтобы его могла проверить машина и чтобы два человека смогли согласиться с ним.