⟨/⟩ 40_Arch/ADR/ADR_009_RecognitionModelIntegration.cs

169 строк · в начало

using System;
using Ban.Sdaid.Notation.Documents;
using Ban.Sdaid.Icd.Backend.Recognition;

namespace Ban.Sdaid.Icd.Arch.Adr
{
    /// <summary>
    /// Решение: как Сервис обращается к модели распознавания за значениями полей —
    /// один синхронный вызов на страницу по OpenAI-совместимому протоколу, схема ответа
    /// принуждается параметром запроса.
    /// </summary>
    public class ADR_009_RecognitionModelIntegration : IAdrDocument, IHasStructuralLinks
    {
        public static string _refName = "ADR-009 «Обращение к модели распознавания»";

        public string Name => "ADR-009. Обращение к модели распознавания";

        public string Description =>
            @"Как формируется запрос к модели, как он передаётся и как ответ превращается в распознанные поля. Первая редакция намеренно минимальна: цель — получать отклики и на них докручивать качество.";

        public string Version => "0.1";
        public string Status => "proposed";

        public string[] Comments => new[]
        {
            "2026-08-18. Заведён при проработке распознавания пакета ввода.",
        };

        public Type? Supersedes => null;

        public SdaidLink[] Links => new[]
        {
            Rel.Uses<FieldExtractionPrompt>(),
        };

        public static string S1_Context = $"""
            ## Контекст

            По {nameof(ADR_007_InputWithoutPlan)} распознавание стало обязательным звеном: пользователь
            больше не сообщает Сервису ничего, кроме кадров, и всё остальное Сервис добывает сам.
            Значит нужен способ обратиться к модели за значениями полей и вернуть результат
            в проверяемом виде — со значением, уверенностью и указанием, откуда прочитано
            ({nameof(ADR_005_FieldValueSourceBinding)}).

            Что уже есть: структура запроса, инструкция модели, каталог полей страницы 1 формы
            «Издания из фондов БАН» и схема ответа — {nameof(FieldExtractionPrompt)}.

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

            Отсюда предмет решения: **минимальный работающий путь до отклика модели**.

            ### За рамками

            Опознание вида ИК, выбор формы и распределение кадров по страницам решаются отдельно.
            Здесь считаем, что к моменту вызова форма известна, кадр отнесён к странице формы,
            и каталог полей этой страницы собран.
            """;

        public static string S2_Decision = $"""
            ## Решение

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

            **Протокол — OpenAI-совместимый** `POST /v1/chat/completions`: изображение передаётся
            как `image_url` с data:-URI, схема ответа — параметром запроса (`response_format`
            с JSON Schema), а не текстом в промте. Такой контракт поддерживают и облачные провайдеры,
            и локальные серверы (llama.cpp, vLLM), поэтому смена модели не меняет код.

            **Модель — локальная, в контуре библиотеки.** Работаем с развёрнутой на своём
            оборудовании Qwen; изображения карт наружу не передаются. Адрес, имя модели, температура
            и таймаут задаются настройкой, а не запросом: пользователь модель не выбирает,
            в домене её нет. Тот же контракт принимают облачные провайдеры — это оставляет путь
            к сравнению качества, но прод-вариантом облако не рассматривается.

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

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

            **Сбой не разбирается автоматически.** Таймаут, недоступность модели, ответ не по схеме
            после одного повтора — пакет переводится в состояние «ошибка» и остаётся в очереди.
            Повторную обработку запускает пользователь командой, как и при любой другой правке пакета.

            ### Чего в первой редакции нет

            Сознательно не делаем — до появления фактов, требующих обратного:

            | Не делаем | Почему отложено |
            |---|---|
            | Кэш разбора по содержимому | повторов пока единицы; кэш прячет как раз то, что мы хотим наблюдать |
            | Разделение OCR и извлечения | сначала надо увидеть, справляется ли одна модель |
            | Переспрос более сильной моделью при низкой уверенности | нечем задать порог: распределение уверенности неизвестно |
            | Своя дообученная модель | дообучение требует эталонного набора, которого нет |
            | Пакетная отправка нескольких страниц одним вызовом | экономия неощутима при последовательной обработке пакетов |
            """;

        public static string S3_Rationale = """
            ## Обоснование

            Решение выбрано по критерию «сколько времени до первого наблюдаемого отклика»,
            а не «насколько хорошо оно масштабируется». Пока не увидим, как модель читает наши карты,
            все прочие критерии опираются на догадки.

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

            Принуждение схемой — единственное усложнение, на которое идём сразу. Оно ничего не стоит
            (параметр запроса) и снимает целый класс работы: разбор свободного текста, ретраи
            на «модель ответила прозой», рассинхрон каталога и ответа.

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

        public static string S4_Consequences = """
            ## Следствия

            **Время обработки пакета складывается из страниц.** Две страницы — два вызова подряд,
            десятки секунд. Для очереди это приемлемо, для интерактивной подсказки при вводе — нет;
            если такая подсказка понадобится, потребуется другое решение.

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

            **Появляется, что версионировать.** Промт и модель становятся частью результата: одна
            и та же карта, разобранная разными версиями, даёт разные значения. Пока это только
            хранится; правила «пересчитать всё при смене промта» нет и не нужно.

            **Правки пользователя и повторная обработка ещё не примирены.** Решение фиксирует, что
            повтор запускается командой, но не то, как исправленное значение переживает повтор.
            Это отдельный вопрос, он поднят в эпике обработки пакета.
            """;

        public static string S5_Alternatives = """
            ## Рассмотренные альтернативы

            **Свой сервис с дообученной моделью.** В соседнем проекте библиотеки распознавание сделано
            так: дообученная модель под конкретную задачу, поднятая своим сервисом с собственным API.
            Отвергнуто как первый шаг: дообучение требует размеченного эталонного набора, которого
            у нас нет и который можно собрать только после того, как заработает базовый разбор.
            Как второй шаг остаётся открытым — и тем более достижимым, что контракт вызова
            от модели не зависит.

            **Разделить OCR и извлечение полей.** Локальная модель читает текст со словными
            координатами, вторая модель извлекает из текста значения; область поля тогда вычисляется
            точно, сопоставлением со словами, а не доверием к модели. Отвергнуто для первой редакции:
            два звена вместо одного, и оба надо отлаживать одновременно. Это первый кандидат
            на усложнение, если окажется, что области смещены.

            **Отправлять все страницы карты одним вызовом.** Модель видит карту целиком и может
            связать данные разных страниц. Отвергнуто: усложняет каталог и схему ответа
            (поля надо приписывать страницам), а выигрыш неочевиден — страницы наших форм
            самостоятельны.
            """;
    }
}