Извлечение проверенных полей тикета

JavaScriptBeginner
Практиковаться сейчас

Введение

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

В этой лабораторной работе вы используете JSON Mode вместе с JSON Schema. JSON — это формат данных. Схема — это контракт, который описывает обязательные поля, допустимые типы значений и возможность наличия неожиданных полей. Запрос модели по схеме улучшает форму ответа, но не является границей доверия: результат модели остаётся внешними данными и может быть неполным, некорректным или несовместимым с приложением.

Вы создадите обработчик POST /extract. Worker отправит одну небольшую синтетическую заявку в модель Llama, размещённую в Cloudflare, и запросит четыре поля: категорию, приоритет, краткое резюме и решение о необходимости дальнейших действий. Затем та же схема будет независимо проверена с помощью Ajv до возврата принятой записи Worker'ом. Детерминированные фикстуры подставят некорректный результат модели, чтобы вы могли убедиться: недопустимые данные попадают в ветку ошибки, а не в принятый ответ.

Это третья лабораторная работа курса. Предполагается, что вы знаете: Cloudflare Worker обрабатывает HTTP-запросы, а binding AI предоставляет Workers AI как env.AI. Если вы начали курс с этой работы, сначала выполните Подключение LabEx к вашей учётной записи Cloudflare, чтобы научиться работать с терминалом виртуальной машины, авторизовать Wrangler, проверить свою учебную учётную запись и сохранить её идентификатор.

В лабораторной работе используется @cf/meta/llama-3.3-70b-instruct-fp8-fast, поддерживающая JSON Mode. Все запросы и результаты остаются небольшими. В настоящее время бесплатные учётные записи Workers получают общую суточную квоту в 10 000 Neurons, поэтому Workers Paid не требуется, пока в учётной записи остаётся бесплатная квота. Локальный запуск всё равно обращается к Cloudflare и расходует эту квоту. Если модель или квота недоступны, остановитесь и не отправляйте повторные запросы.

Настройка устанавливает Node.js 22.22.0, локальный Wrangler 4.132.0 и Ajv 8.17.1 в /home/labex/project/ticket-fields. Она добавляет детерминированные тесты и независимые проверки. Настройка не выполняет вход, не вызывает модель, не разворачивает Worker и не создаёт облачные ресурсы. Не закрывайте эту виртуальную машину, пока не удалите временный Worker и не проверите выход из системы.

Авторизуйте виртуальную машину и настройте Worker для извлечения данных

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

Перейдите в подготовленный проект и проверьте закреплённую версию Wrangler:

cd /home/labex/project/ticket-fields
npx wrangler --version

Ожидаемый результат — 4.132.0. Запросите тот же минимальный набор разрешений, который использовался в предыдущих лабораторных работах по Workers AI. Wrangler 4.132.0 проверяет зависимости KV при удалении Worker, поэтому workers_kv:write предотвращает несвязанную ошибку очистки, хотя эта лабораторная работа не создаёт данные KV.

npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write ai:write

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

npx wrangler whoami --json

Убедитесь, что указано loggedIn: true, и прочитайте name и id нужной учётной записи. Сгенерируйте уникальное имя временного Worker:

RUN="labex-c07-a03-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"

Замените YOUR_ACCOUNT_ID фактическим идентификатором этой учётной записи:

cat > wrangler.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "YOUR_ACCOUNT_ID",
  "main": "src/index.js",
  "compatibility_date": "2026-09-16",
  "workers_dev": true,
  "preview_urls": false,
  "observability": {
    "enabled": true,
    "head_sampling_rate": 1
  },
  "ai": {
    "binding": "AI",
    "remote": true
  }
}
JSON

Binding AI становится доступен как env.AI. Параметр remote: true означает, что локальный процесс Worker всё равно обращается к реальной модели, связанной с учётной записью. Observability сохраняет небольшие события жизненного цикла, которые вы изучите после развёртывания. На этом этапе ни инференс, ни развёртывание ещё не выполнялись.

Изучите контракт структурированного вывода

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

Сгенерируйте типы окружения Worker:

npx wrangler types
grep -A4 'interface __BaseEnv_Env' worker-configuration.d.ts

Найдите AI: Ai. Это binding, предоставляемый платформой, а не ключ API модели, сохранённый в исходном коде.

Запись будет содержать четыре поля:

category: billing | account | upload | other
priority: low | medium | high
summary: nonempty text, at most 160 characters
needs_follow_up: true or false

В JSON Schema свойство type задаёт тип значения, enum ограничивает значение известным списком, required перечисляет обязательные поля, а additionalProperties: false отклоняет неожиданные поля. Последнее правило важно: иначе выдуманное поле может незаметно пройти дальше. Схема описывает структуру, но не определяет, верно ли модель интерпретировала тикет; принятые поля всё ещё может потребоваться проверить человеку или последующему бизнес-правилу.

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

grep -nE 'security|priority: 1|internal_note|not-an-object' test/worker.test.mjs

Эти фикстуры не расходуют Neurons. Они позволяют тесту надёжно проверить случаи, которые не следует специально получать повторными реальными запросами к модели.

Создайте обработчик извлечения с проверкой

На этом шаге вы реализуете схему, запрос к модели и проверку на стороне приложения. Только ветка, успешно прошедшая проверку Ajv, возвращает record.

Создайте точку входа Worker:

cat > src/index.js <<'JS'
import Ajv from "ajv";

const MODEL = "@cf/meta/llama-3.3-70b-instruct-fp8-fast";
const MAX_TICKET = 1200;

export const TICKET_SCHEMA = {
  type: "object",
  properties: {
    category: { type: "string", enum: ["billing", "account", "upload", "other"] },
    priority: { type: "string", enum: ["low", "medium", "high"] },
    summary: { type: "string", minLength: 1, maxLength: 160 },
    needs_follow_up: { type: "boolean" }
  },
  required: ["category", "priority", "summary", "needs_follow_up"],
  additionalProperties: false
};

const ajv = new Ajv({ allErrors: true });
const isTicketRecord = ajv.compile(TICKET_SCHEMA);

function json(data, status = 200) {
  return Response.json(data, { status });
}

async function readTicket(request) {
  const contentType = request.headers.get("content-type") || "";
  if (!contentType.toLowerCase().includes("application/json")) {
    return { error: json({ error: "json_required" }, 415) };
  }

  const raw = await request.text();
  if (raw.length > 2048) {
    return { error: json({ error: "ticket_too_large" }, 413) };
  }

  let body;
  try {
    body = JSON.parse(raw);
  } catch {
    return { error: json({ error: "invalid_json" }, 400) };
  }

  const ticket = typeof body?.ticket === "string" ? body.ticket.trim() : "";
  if (!ticket) return { error: json({ error: "invalid_ticket" }, 400) };
  if (ticket.length > MAX_TICKET) {
    return { error: json({ error: "ticket_too_large" }, 413) };
  }
  return { ticket };
}

async function extractTicket(request, env) {
  const parsed = await readTicket(request);
  if (parsed.error) return parsed.error;

  const requestId = crypto.randomUUID();
  const details = { requestId, model: MODEL };

  let result;
  try {
    result = await env.AI.run(MODEL, {
      messages: [
        {
          role: "system",
          content: "Extract support-ticket fields. Use only evidence in the ticket. Keep the summary short and do not add fields."
        },
        { role: "user", content: parsed.ticket }
      ],
      response_format: {
        type: "json_schema",
        json_schema: TICKET_SCHEMA
      },
      max_tokens: 160,
      temperature: 0
    });
  } catch {
    console.error(JSON.stringify({ event: "ticket_extraction_failed", ...details }));
    return json({ error: "model_unavailable", requestId }, 502);
  }

  const candidate = result?.response;
  if (!isTicketRecord(candidate)) {
    console.error(JSON.stringify({ event: "ticket_output_rejected", ...details }));
    return json({ error: "invalid_model_output", requestId }, 502);
  }

  console.log(JSON.stringify({ event: "ticket_output_accepted", ...details }));
  return json({ record: candidate, requestId });
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (request.method === "GET" && url.pathname === "/health") {
      return json({ status: "ok" });
    }
    if (request.method === "POST" && url.pathname === "/extract") {
      return extractTicket(request, env);
    }
    return json({ error: "not_found" }, 404);
  }
};
JS

Worker никогда не записывает в журнал ни тикет, ни возвращённые поля. Идентификатор запроса связывает ответ клиента с событием принятия, отклонения или сбоя, не копируя содержимое обращения в данные Observability. Подробности ошибок Ajv также не попадают в ответ клиента, поскольку могут раскрыть внутреннюю логику проверки; клиент получает стабильный контракт invalid_model_output.

Запустите детерминированные тесты:

node --test test/worker.test.mjs

Ожидайте пять пройденных тестов. Один тест подставляет семь некорректных кандидатов через поддельный binding AI и требует, чтобы в каждом ответе отсутствовал record. Затем соберите настоящий Worker, не выполняя развёртывание:

npx wrangler deploy --dry-run

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

Выполните один реальный запрос со структурированным результатом

На этом шаге вы запустите Worker в виртуальной машине и выполните один настоящий запрос в JSON Mode. Слово «локальный» относится к обработчику запроса; binding AI по-прежнему использует выбранную учётную запись Cloudflare и расходует часть её суточной квоты.

Запустите Wrangler в фоновом режиме и сохраните идентификатор процесса:

npx wrangler dev --port 8787 > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid

Дождитесь ответа от маршрута проверки, который не обращается к AI:

for attempt in $(seq 1 30); do
  if curl --silent --fail http://127.0.0.1:8787/health; then
    break
  fi
  sleep 1
done

Отправьте один понятный синтетический тикет:

curl --silent --show-error http://127.0.0.1:8787/extract \
  --header 'Content-Type: application/json' \
  --data '{"ticket":"Customer cannot upload a PDF and needs help before today’s deadline."}'

Ожидайте JSON-ответ с полями record и requestId. Точные значения категории, приоритета, формулировка резюме и решение о дальнейших действиях могут отличаться. Важно, чтобы запись содержала ровно четыре поля, а каждое значение соответствовало схеме.

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

curl --silent --show-error --write-out '\nHTTP %{http_code}\n' \
  http://127.0.0.1:8787/extract \
  --header 'Content-Type: application/json' \
  --data '{"ticket":""}'

Ожидайте {"error":"invalid_ticket"} и HTTP-код 400. Проверка входных данных защищает вызов модели, а проверка результата защищает запись приложения. Это разные границы.

Разверните Worker и изучите принятый результат

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

kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true

Разверните Worker:

npx wrangler deploy

Сохраните точный URL workers.dev, напечатанный Wrangler:

WORKER_URL="https://YOUR_WORKER_URL"

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

curl --silent --show-error "$WORKER_URL/extract" \
  --header 'Content-Type: application/json' \
  --data '{"ticket":"Customer cannot upload a PDF and needs help before today’s deadline."}'

Убедитесь, что публичный ответ снова содержит под record ровно поля, предусмотренные схемой. Одного успешного HTTP-кода недостаточно: независимая проверка также проверяет каждое возвращённое поле и развернутый binding AI.

Откройте Cloudflare Dashboard и перейдите в Workers & Pages → Overview → ваш Worker labex-c07-a03-.... Проверьте его binding, затем откройте Observability → Logs. Найдите ticket_output_accepted, разверните событие и подтвердите его model, requestId и имя события. Журнал намеренно не содержит тикет и извлечённую запись.

Представленный ниже binding относится к одному временному отладочному запуску. Диаграмма и таблица связывают имя AI с Workers AI — это видимый в Dashboard эквивалент env.AI внутри Worker. Имя вашего Worker будет другим.

Binding Workers AI с именем AI в Worker извлечения данных

В том же запуске после публичного запроса и независимых проверок было записано 3 Success и 0 Errors. Это примерные значения, а не обязательные количества. Важно, что выбранный Worker успешно обработал видимые запросы /extract.

Успешные события Worker извлечения без ошибок вызова

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

Событие жизненного цикла структурированного вывода с ограниченным содержимым

Затем откройте Workers AI и изучите использование моделей за сегодня. Найдите модель Llama 3.3 и убедитесь, что выполненные запросы остаются в пределах бесплатной квоты Workers Free — 10 000 Neurons. Данные Dashboard могут появляться с задержкой, поэтому немного подождите и не повторяйте инференс только для того, чтобы принудительно обновить график или журнал.

В примере для учётной записи отображалось 261.63/10k Neurons для модели Llama. Это значение включает предыдущие упражнения по подготовке курса в той же учебной учётной записи, поэтому оно не отражает стоимость только этой лабораторной работы, и у вас будет другое значение. Контрольная точка — остаться в пределах бесплатной квоты, а не получить число из примера.

Суточная бесплатная квота Workers AI и использование модели Llama

Значения в Dashboard относятся к этому временному запуску. Учебные цели — проверить точную идентичность Worker, его binding AI, событие принятого результата с ограниченным содержимым и использование бесплатной квоты. Если представление Dashboard обновляется с задержкой, основными источниками остаются проверки через CLI, API и среду выполнения.

Удалите Worker и выйдите из системы

На этом шаге вы удалите временный Worker, а затем отмените авторизацию этой виртуальной машины. Использование Workers AI хранится на уровне учётной записи, поэтому удаление Worker убирает его endpoint, но не стирает запись об использовании и не изменяет тарифный план учётной записи.

Удалите Worker с точным именем из wrangler.jsonc:

npx wrangler delete

Подтверждайте удаление только после того, как Wrangler покажет уникальное имя labex-c07-a03-... этой лабораторной работы. Команда должна завершиться сообщением Successfully deleted. Обновите Workers & Pages → Overview и убедитесь, что это имя отсутствует.

Пока виртуальная машина всё ещё авторизована, выполните независимую проверку управления:

python3 .labex/verify.py deleted

Только после появления результата PASS: deleted удалите сохранённые полномочия виртуальной машины:

npx wrangler logout
npx wrangler whoami --json

Проверьте, что указано loggedIn: false. Отсутствие локального файла, закрытая вкладка браузера или сетевая ошибка не доказывают удаление облачного ресурса или выход из системы.

Резюме

Вы создали endpoint Workers AI, который запрашивает структурированные поля тикета с помощью JSON Mode и JSON Schema. Вы узнали, почему запрошенная форма не равна доверенным данным, использовали Ajv как независимую границу приложения и с помощью некорректных фикстур доказали, что результат модели с недопустимой структурой никогда не становится принятой записью. Вы выполнили один настоящий локальный и развёрнутый запрос в Workers Free, связали принятое событие с Observability в Dashboard, удалили временный Worker и вышли из системы на новой виртуальной машине.