Files
contracts/History/llm-analysis/opus-teach-full-responses.md
T

44 KiB
Raw Blame History

Полные ответы Опуса — изолированная страница /teach (feedback + обучение)

Дата: 26.06.2026 | Сохранено post-factum из чат-сессии.


ЧАСТЬ 1: Ответ про few-shot и эталонный датасет

Хороший вопрос. Дай я сначала посмотрю, как сейчас устроены эти «6 файлов» и формирование промпта, чтобы мнение было по делу.

Searching codebase for "формирование промпта для LLM на основе эталонных файлов few-shot examples"

Searched for regex 6 (файл|file|example|эталон|reference), 1 result

Searched for regex few.?shot|эталон|example|пример|reference|опорн (**/contracts-app/**/*.py), no results

Searched for files matching **/llm_prompt.py, 1 match

Searched for files matching **/prompt*.{py,cfm}, 2 matches

Searched for regex opus_cases|out_100files|6 файл|six, 2 results

Read file:///home/naeel/nubes/contracts/contractor/deploy/llm_prompt.py#1-1, lines 1 to 120

Read file:///home/naeel/nubes/contracts/testgen/opus_cases

Посмотрел, как сейчас устроено: промпт в llm_prompt.py — это хардкод-инструкция с доменным глоссарием и парой ручных few-shot примеров (ПРИМЕР 1, ПРИМЕР 2), а «6 файлов» — это по сути синтетические кейсы из opus_cases для проверки.

Моё мнение по фиче.

Идея здравая, но есть важная развилка

Реальные проверенные документы — это очень ценно. Синтетика (gen_opus.py) не воспроизводит грязь реальных договоров: кривые таблицы, объединённые ячейки, «мощность в составе названия», нестандартные формулировки ДС, опечатки в суммах. На 6 искусственных файлах легко получить иллюзию качества, которая рассыпется на первом же настоящем пакете.

Но ключевой вопрос: что именно строить из архива? Тут смешаны две разные вещи.

1. Эталонный датасет (input → проверенный output) — да, делать

Самое полезное. Но важно: «загрузить архив документов» недостаточно. Чтобы это работало, на каждый документ нужен проверенный правильный результат (итоговая спецификация / список ops). Без эталонного ответа это просто куча PDF — не на чем калибровать.

То есть фича должна быть не «загрузи архив», а «загрузи архив + зафиксируй проверенную спецификацию как ground-truth». Тогда получаем regression-набор: прогнал промпт → сравнил с эталоном → видишь реальный процент ошибок.

2. Авто-генерация промпта из этих файлов — осторожно

Полностью «формировать промпт» из загруженных файлов я бы не делал. Риски:

  • Раздувание контекста — если пихать реальные договоры целиком как few-shot, промпт пухнет, растёт цена/латентность, и модель начинает копировать формат конкретного образца.
  • Переобучение под layout — закладываешь форматы 3-х клиентов, ломается на четвёртом.
  • Конфиденциальность — реальные договоры с реквизитами/ценами уезжают в LLM на каждом запросе. Это надо явно проговорить с заказчиком.

Лучше из архива извлекать паттерны, а не сам текст: новые единицы измерения, варианты названий услуг, типы формулировок ДС («изложить в новой редакции» и т.п.) — и обогащать ими глоссарий и правила в промпте. 1-2 самых показательных кейса можно добавить как few-shot, остальное — в eval-набор.

Что я бы предложил по факту

  • Разделить два контура: (а) golden dataset из реальных пар вход→проверенный_выход для регрессии; (б) сам промпт — оставить компактным, обогащать его выводами из анализа датасета, а не сырыми файлами.
  • Фича «загрузка архива» → ведёт в разметку/верификацию (человек подтверждает правильный результат), а не сразу в промпт.
  • На этом наборе гонять метрику (precision/recall по ops), и менять промпт только если метрика растёт.

Резюме: направление правильное — уходить от 6 синтетических файлов надо. Но ценность не в «архиве документов», а в архиве проверенных пар документ↔результат. И этим калибровать/тестировать, а не механически генерировать промпт.

Это обсуждение — код не трогаю. Скажешь «делай» по конкретному пункту — приступлю.


ЧАСТЬ 2: Ответ про «типа обучение» (feedback-learning цикл)

Да, это сильнее предыдущего варианта — и вот почему, плюс где подвох.

Почему это лучше «загрузки архива»

Главная проблема прошлого варианта была: откуда взять «проверенный правильный ответ». Здесь она решается сама собой — интерфейс исправления ошибок и есть инструмент разметки. Юзер не размечает абстрактно, а правит конкретный результат системы. На выходе автоматически копится то самое: пара «вход → проверенный выход». Это правильный способ построить эталонный датасет.

И что важно — у вас уже есть подходящая структура под это. Результат LLM — это поток операций (ADD / UPDATE / DELETE / UNRESOLVED) в llm_prompt.py, а применяются они через событийную модель (apply_events.cfm). Значит правка юзера — это исправленный поток операций, а разница «что выдала LLM» vs «как поправил юзер» = чистый сигнал ошибки. Это удобно: не свободный текст «тут неверно», а конкретная дельта.

Но «типа обучения» — это важная оговорка

Само по себе это не обучение модели. Цикл «учится» только если правки где-то сохраняются и переиспользуются. Если правка живёт в рамках одной сессии и исчезает — система не умнеет, это просто ручная корректировка. Так что ключевое в фиче — не сам акт правки, а накопление и переиспользование правок.

Дальше есть три уровня, и не надо их путать:

  1. Регрессия (самое дешёвое и полезное сразу) — правки копятся как golden-набор, на нём гоняется промпт, видно реальный % ошибок.
  2. Prompt-learning — из частых ошибок осознанно дополняется глоссарий/правила/few-shot в промпте. Это и есть практичное «обучение» в вашем случае.
  3. Fine-tuning модели — реальное дообучение. Дорого, нужен объём (сотни-тысячи примеров), отдельная инфраструктура. На старте не нужно.

Где подвох — не вкручивать правки в промпт автоматически

Соблазн: «каждую правку юзера сразу добавлять в промпт». Так делать нельзя:

  • переобучение под конкретные форматы;
  • рост контекста и цены;
  • конфликтующие правки от разных юзеров → промпт деградирует.

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

Что бы я ещё заложил в дизайн

  • Структурировать тип ошибки, а не свободный текст: «пропущена строка» / «неверная цена» / «надо было UPDATE, а не ADD» / «ложный дубликат вместо UNRESOLVED». Тогда правки можно агрегировать и видеть системные слабости промпта, а не разбирать вручную.
  • Фиксировать версию промпта, на которой получен результат — иначе через месяц непонятно, актуальна ли ещё ошибка.
  • Учесть конфиденциальность: реальные договоры с реквизитами/ценами копятся в БД — проговорить с заказчиком.

Резюме: режим «загрузил → система выдала → юзер поправил → правка сохранилась» — да, делать стоит, это естественный сборщик эталонных данных и ловушка системных ошибок. Только «обучение» держать осознанным (batch-обновление промпта + регрессия), а не авто-инъекцией каждой правки.

Код не трогаю — это обсуждение. Скажешь «делай» — распишу конкретную реализацию.


ЧАСТЬ 3: Короткий вариант для заказчика

Вот короткий вариант для заказчика:


Этап опытной эксплуатации (обучение системы)

На первое время предлагаем режим проверки: вы загружаете реальные документы, система выдаёт результат, а вы отмечаете ошибки — что распознано неверно (пропущена строка, неверная цена, неправильное сопоставление и т.п.).

Эти отметки в обезличенном виде (без названий компаний, ФИО, реквизитов и иных персональных данных) накапливаются в базе и используются для дальнейшей настройки и обучения системы.

Это позволит откалибровать сервис на ваших реальных договорах, а не на тестовых примерах, и системно повышать точность.


Если нужно ещё короче (одним абзацем) или, наоборот, добавить пункт про согласие на обработку данных — скажи, поправлю.


ЧАСТЬ 4: Ответ про привязку замечаний к строкам таблицы

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

Read file:///home/naeel/nubes/contracts/contractor/deploy/services

Read memory file:///home/naeel/.vscode-server/data/User/workspaceStorage/dc98ff3e658364e1826ee6585ebe4b5e/GitHub.copilot-chat/memory-tool/memories/repo/contracts-two-panels.md

Searched for regex target_id|r1|new_row|action.*ADD|операц|spec|строк (**/contractor/view.cfm), no results

Searched for files matching **/contracts-app/site/templates/**, no matches

Не «у каждого абзаца» — это привязка не к тому объекту. Результат работы системы — это не текст-простыня, а таблица спецификации: набор строк-услуг, каждая получена операцией LLM (ADD/UPDATE/DELETE/UNRESOLVED) и имеет свой id (r1, r2…). Замечание логично цеплять к строке результата, а не к абзацу исходника.

Как я бы это сделал

Уровень 1 — отметка на строке (основное). У каждой строки итоговой таблицы — маленькая иконка «⚠ ошибка». Клик открывает мини-форму с выбором типа ошибки (не свободный текст):

  • неверная цена / объём / сумма
  • неверное наименование
  • лишняя строка (надо было удалить)
  • пропущена строка (есть в документе — нет в результате)
  • неверное сопоставление (надо UPDATE существующей, а не новая ADD; или наоборот)
  • неверный тип ДС / режим (partial vs новая редакция)

Плюс необязательное поле «комментарий» свободным текстом.

Уровень 2 — отметка «пропущено». Случай «строки вообще нет в результате» не покрывается иконкой на строке. Нужна отдельная кнопка « Система пропустила позицию» над/под таблицей.

Уровень 3 — общая оценка (опционально). Одна кнопка вверху: «Результат в целом верный / есть ошибки». Дёшево, но даёт быстрый сигнал по проценту удачных разборов.

Почему так, а не поле у абзаца

  • Структурированный фидбек агрегируется. «Тип ошибки = неверное сопоставление встречается в 40% ДС» → понятно, что чинить в промпте. Свободный текст у абзацев придётся разбирать вручную, аналитики ноль.
  • Замечание = дельта операции. Юзер фактически правит строку → разница «что выдала LLM» vs «как должно быть» и есть обучающий сигнал. Это ложится на вашу событийную модель (apply_events.cfm) почти без натяжки.
  • Меньше шума. Поле ввода под каждым абзацем визуально перегружает и провоцирует расплывчатые комментарии.

Что копится в базе (обезличенно)

На каждое замечание:

  • тип ошибки + (опц.) комментарий
  • что выдала система по этой строке
  • как поправил юзер (правильное значение)
  • версия промпта
  • без названий компаний/ФИО/реквизитов — только структура и числа

Как это сформулировать заказчику (1 фраза)

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

Хочешь — распишу конкретную реализацию (поля в БД, эндпоинт сохранения, изменения в шаблоне вывода). Код пока не трогаю, жду «делай».


ЧАСТЬ 5: Ответ про UI с примером на допник-1-XXX002

Отлично, теперь предметно. Твой вывод — это карточка документа + таблица операций (Действие / Услуга / Цена / Кол-во / Сумма / Дата). Замечания цепляем именно к строкам этой таблицы. Вот как это выглядело бы на твоём примере:

✓ допник-1-XXX002-01200_3.docx — 2 оп., partial (14с)        [✓ всё верно]
+2 ~0 -0

Действие  Услуга                                  Цена       Кол-во  Сумма      Дата         ⚠
ADD       WAF: Positive Technologies, в составе…  104021.67  1       104021.67  2026-03-30   [⚠]
ADD       Облачный диск Valo Cloud, в составе…    68700      1       68700      2026-02-01   [⚠]

                                          [ ➕ Система пропустила позицию ]

Клик по [⚠] в строке → раскрывается мини-форма прямо под строкой:

Тип ошибки:  ( ) неверная цена/сумма
             ( ) неверное кол-во
             ( ) неверное наименование услуги
             (•) лишняя строка — этой операции быть не должно
             ( ) неверное действие (должно быть UPDATE/DELETE, а не ADD)
             ( ) неверная дата
Правильное значение: [_____________]  (необязательно)
Комментарий:         [_____________]  (необязательно)
                                              [ Сохранить ]

Кнопка [✓ всё верно] вверху карточки — если разбор документа корректен целиком. Один клик = положительный сигнал, ничего расписывать не надо.

Кнопка ➕ Система пропустила позицию — для случая, которого нет в таблице (услуга была в документе, но LLM её не извлекла). Открывает форму ввода пропущенной строки.

Что уходит в базу по одному замечанию (обезличенно)

На примере второй строки, если юзер укажет «неверная цена, правильно 68000»:

operation: ADD
service:   "Облачный диск Valo Cloud, в составе…"   ← наименование услуги, без названия клиента
field:     price
llm_value: 68700
correct:   68000
error_type: wrong_price
prompt_version: v1.0.178

Никаких «ЗАО ХХХ002», № договора, ФИО — только структура услуги и числа.

Почему так, а не поле под каждым абзацем

Твой реальный вывод подтверждает: это таблица, а не текст. Поле у «абзаца» некуда вешать — абзацев тут нет, есть операции. А привязка к операции даёт:

  • агрегацию по типу ошибки (видно, что чинить в промпте),
  • готовую дельту «LLM выдала X → правильно Y» как обучающий сигнал,
  • ровную укладку в вашу событийную модель (apply_events.cfm).

Хочешь — распишу конкретику реализации (колонка в таблице вывода, эндпоинт сохранения, структура таблицы в БД). Код не трогаю, жду «делай».


ЧАСТЬ 6: ДЕТАЛЬНЫЙ ПЛАН — страница /teach

Это план, не правка кода — изучу структуру, чтобы план лёг на реальный код, а не в воздух. Читаю ключевые файлы.

Read file:///home/naeel/nubes/contracts/contractor/db.cfc

Read file:///home/naeel/nubes/contracts/contractor/Application.cfc

Read memory file:///home/naeel/.vscode-server/data/User/workspaceStorage/dc98ff3e658364e1826ee6585ebe4b5e/GitHub.copilot-chat/memory-tool/memories/repo/contracts-deploy.md

Read memory file:///home/naeel/.vscode-server/data/User/workspaceStorage/dc98ff3e658364e1826ee6585ebe4b5e/GitHub.copilot-chat/memory-tool/memories/repo/contracts-lucee-test.md

Изучил структуру. Lucee-приложение с файловой маршрутизацией (teach.cfm/teach), PostgreSQL baza, есть событийная модель (spec_events, spec_current) и уже готовый read-only SQL-эндпоинт /db/query?sql=… в db.cfc — через него агент и будет читать БД. Вот детальный план.


План: страница /teach (опросник + обучение)

1. Принцип изоляции — что НЕ трогаем

Маршрутизация в Lucee файловая → новый путь = новый файл, существующий код не редактируется вообще:

Не трогаем Добавляем
api.cfm, process*.cfm, apply_events.cfm, db.cfc, Application.cfc, upload.cfm, view.cfm teach.cfm, teach_api.cfm, teach_schema.cfm
существующие таблицы (spec_events, contracts…) одна новая таблица feedback

Никаких ALTER существующих таблиц, никаких правок в db.cfc/schema(). Только чтение чужих таблиц + запись в свою.

2. Новые файлы (3 шт.)

  1. teach_schema.cfm — одноразовый: CREATE TABLE IF NOT EXISTS feedback (…). Зашёл по URL один раз → таблица создана.
  2. teach.cfm — сама страница:
    • список уже разобранных договоров/ДС (читает contracts + supplements + spec_events);
    • выбрал ДС → рисует ту же таблицу операций (Действие/Услуга/Цена/Кол-во/Сумма/Дата), что в твоём примере, но с колонкой и кнопками [✓ всё верно], [➕ пропущена позиция];
    • JS отправляет замечание POST-ом на teach_api.cfm.
  3. teach_api.cfm — приём замечания: параметризованный INSERT в feedback (никакого конкатенированного SQL — защита от инъекций). Возвращает JSON {ok:true}.

Источник данных для таблицы операций — spec_events (action, new_values JSONB, comment, seq). При реализации сверим, что карточка «2 оп., partial» строится именно отсюда.

3. Схема таблицы feedback (ядро — продумано под анализ агентом)

Главные поля вынесены отдельными колонками (не в JSON), чтобы агент агрегировал простым SQL; значения — в JSONB.

CREATE TABLE IF NOT EXISTS feedback (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    created_at      TIMESTAMPTZ DEFAULT now(),

    -- привязка (внутренняя трассировка, в обучающий экспорт НЕ идёт)
    contract_id     UUID,           -- FK-логически на contracts, без жёсткого constraint
    supplement_id   UUID,
    event_seq       INTEGER,        -- какая операция ДС (NULL для "пропущено"/"документ в целом")

    -- уровень и вердикт
    scope           TEXT NOT NULL,  -- 'row' | 'missed' | 'document'
    verdict         TEXT,           -- 'correct' | 'error'

    -- суть ошибки (для агрегации)
    error_type      TEXT,           -- wrong_price|wrong_qty|wrong_name|wrong_date|
                                    -- extra_row|missed_row|wrong_action|wrong_mode
    field           TEXT,           -- price|qty|sum|name|date_start|action|mode

    -- обучающий сигнал: что выдала система vs как правильно
    service_name    TEXT,           -- наименование услуги (обезличено)
    llm_value       JSONB,          -- что выдала LLM
    correct_value   JSONB,          -- что указал юзер

    -- контекст результата
    prompt_version  TEXT,           -- версия промпта на момент разбора
    doc_mode        TEXT,           -- partial | full_replace
    comment         TEXT,           -- свободный комментарий юзера (необяз.)
    reviewer        TEXT            -- обезличенный id сессии/юзера (необяз.)
);
CREATE INDEX IF NOT EXISTS idx_feedback_error ON feedback(error_type);
CREATE INDEX IF NOT EXISTS idx_feedback_prompt ON feedback(prompt_version);

4. Что пишется по каждому типу замечания

Действие юзера scope verdict error_type llm_value → correct_value
[✓ всё верно] на карточке document correct
на строке: неверная цена row error wrong_price {"price":68700}{"price":68000}
: лишняя строка row error extra_row вся операция → null
: должно быть UPDATE, а не ADD row error wrong_action {"action":"ADD"}{"action":"UPDATE"}
[➕ пропущена позиция] missed error missed_row null{name,price,qty,sum,date}
неверный режим ДС document error wrong_mode {"mode":"partial"}{"mode":"full_replace"}

Каждая запись самодостаточна: видно что было и как надо → готовая обучающая пара.

5. Обезличивание

  • В feedback не копируем название клиента, № договора, ФИО, реквизиты.
  • Храним только: service_name (тип услуги — «WAF Positive Technologies»), числа, тип ошибки, версию промпта.
  • contract_id/supplement_id — это UUID (не имя), для внутренней трассировки. В обучающий экспорт агента эти id не включаются — только структурные поля.
  • Для заказчика формулировка: «в обезличенном виде, без названий компаний и персональных данных» — соответствует фактической схеме.

6. Как агент читает и анализирует (потом)

Через уже существующий /db/query?sql=… (read-only) или напрямую psql. Примеры:

Системные слабости промпта:

SELECT error_type, field, count(*) AS n
FROM feedback WHERE verdict='error'
GROUP BY error_type, field ORDER BY n DESC;

Качество по версии промпта (регрессия):

SELECT prompt_version,
       count(*) FILTER (WHERE verdict='correct') AS ok,
       count(*) FILTER (WHERE verdict='error')   AS err
FROM feedback GROUP BY prompt_version;

Выгрузка обучающих пар (для правки промпта/few-shot):

SELECT service_name, error_type, llm_value, correct_value
FROM feedback WHERE verdict='error' AND scope='row';

Дальше агент: смотрит агрегат → предлагает правку промпта/глоссария → прогоняет на накопленных парах → сравнивает метрику до/после. Никакой авто-инъекции правок в промпт — только осознанное batch-обновление.

7. Версия промпта — нюанс

Сейчас результат разбора не штампуется версией промпта. Варианты:

  • (минимум, без правки основного кода) /teach пишет в prompt_version текущую активную версию промпта на момент замечания — приблизительно, с оговоркой;
  • (правильно, отдельной задачей позже) при разборе сохранять prompt_version в результат — но это уже касается основного пайплайна, делать отдельно и по «делай».

Флажок: на старте берём активную версию, точность привязки уточним позже.

8. Этапы внедрения

  1. teach_schema.cfm → создать таблицу feedback.
  2. teach.cfm (чтение разобранных ДС + рендер таблицы с ) — без сохранения, только UI.
  3. teach_api.cfm (параметризованный INSERT) + подключить JS-отправку.
  4. Проверка end-to-end на одном реальном ДС (как твой допник-1-XXX002).
  5. 2–3 контрольных SQL-запроса агента → убедиться, что данные читаемы и агрегируются.
  6. (позже, отдельно) первый цикл анализа → правка промпта → регрессия.

9. Открытые вопросы к тебе

  1. /teach работает только по уже разобранным документам (из БД) — или нужна и загрузка-разбор прямо на странице? (загрузка-разбор = переиспользование основного пайплайна, сложнее).
  2. Доступ к /teach — нужен ли пароль/ограничение, или dev-стенд открыт?
  3. service_name — оставляем как есть (тип услуги) или дополнительно маскируем?
  4. Кто reviewer — нужно различать разных проверяющих или достаточно анонимной сессии?

Это план, код не трогал. Скажешь по каким пунктам «делай» (и ответишь на вопросы из §9) — начну с teach_schema.cfm.


ЧАСТЬ 7: Ответ про промпт для Опуса

Текст для Опуса хороший — чёткие требования и правильные ограничения (не трогать пайплайн, без online-training, обезличенность). Но в нынешнем виде Опус ответит обобщённо, потому что в промпте нет контекста вашего стека и текущей схемы. По каждому из 5 вопросов это критично. Что я бы добавил, прежде чем отправлять:

Чего не хватает Опусу для предметного ответа

  1. Стек и маршрутизация. Lucee/CFML 6.0, файловая маршрутизация (teach.cfm/teach), PostgreSQL baza. Без этого Опус не поймёт, как «не трогать пайплайн» технически (а ответ — просто новый файл, без правок существующих).

  2. Текущая схема — особенно для Q3. У вас уже есть событийная модель:

    • spec_events(contract_id, supplement_id, seq, action, target_hash, new_values JSONB, comment, status)
    • spec_current, contracts, supplements.

    Связь с исходной операцией = (contract_id, supplement_id, event_seq), и это уже отвечает на Q3: ничего в пайплайне трогать не надо, фидбек просто ссылается на seq логически, без FK-constraint. Опус должен это знать, иначе предложит схему, не совпадающую с вашей.

  3. Форма результата (для Q1/Q2). Таблица операций Действие / Услуга / Цена / Кол-во / Сумма / Дата, режимы partial | full_replace. Дайте Опусу реальный пример вывода (твой допник-1-XXX002).

  4. Ключевой факт для Q4. Сейчас результат разбора не штампуется версией промпта — этого в промпте нет, а это определяет весь ответ на Q4. Без упоминания Опус посоветует «просто сохраняйте prompt_version», не зная, что её сейчас негде взять, кроме «текущей активной».

  5. Как агент потом читает БД (для Q1). Есть read-only эндпоинт /db/query?sql=… в db.cfc. Это аргумент за «плоские колонки error_type/field/prompt_version + значения в JSONB» — чтобы агрегировалось обычным SQL.

Мелочи по формулировкам

  • Q5 стоит разнести: отдельно PII (имена/реквизиты/№ договора — не хранить) и отдельно изоляция рабочих данных (никаких ALTER/FK на боевые таблицы, только своя feedback). Сейчас они слиты в один вопрос.
  • Добавь явно: «предложи минимальный набор error_type как enum-значения» — иначе Опус может уйти в свободный текст ошибок, который потом не агрегируется.

Хочешь — могу либо (а) переписать твой промпт для Опуса с этим контекстом, либо (б) сам ответить на все 5 вопросов по вашей реальной схеме (у меня он есть). Скажи, что полезнее.


ЧАСТЬ 8: Финальный предметный ответ по 5 вопросам

Понял, отвечаю по существу — по вашей реальной схеме.

1. Минимальная схема feedback для MVP

Плоские колонки для агрегации + JSONB для значений:

CREATE TABLE IF NOT EXISTS feedback (
    id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    created_at     TIMESTAMPTZ DEFAULT now(),

    -- связь с исходной операцией (логическая, без FK)
    contract_id    UUID,
    supplement_id  UUID,
    event_seq      INTEGER,        -- NULL для missed/document

    scope          TEXT NOT NULL,  -- 'row' | 'missed' | 'document'
    verdict        TEXT NOT NULL,  -- 'correct' | 'error'
    error_type     TEXT,           -- enum-значения, см. ниже
    field          TEXT,           -- price|qty|sum|name|date_start|action|mode

    service_name   TEXT,           -- наименование услуги (обезличено)
    llm_value      JSONB,          -- что выдала система
    correct_value  JSONB,          -- как правильно

    prompt_version TEXT,
    doc_mode       TEXT,           -- partial | full_replace
    comment        TEXT
);
CREATE INDEX IF NOT EXISTS idx_feedback_error  ON feedback(error_type);
CREATE INDEX IF NOT EXISTS idx_feedback_prompt ON feedback(prompt_version);

Фиксированный набор error_type (enum как CHECK или просто документированный список): wrong_price, wrong_qty, wrong_name, wrong_date, extra_row, missed_row, wrong_action, wrong_mode.

Всё. Ни истории версий записи, ни статусов модерации в MVP не нужно.

2. UI первой версии vs отложить

В MVP:

  • колонка у каждой строки таблицы операций → мини-форма: выбор error_type (радио) + опц. «правильное значение» + опц. комментарий;
  • кнопка [✓ всё верно] на карточке ДС (дешёвый положительный сигнал, scope=document, verdict=correct);
  • кнопка [➕ пропущена позиция] (scope=missed).

Отложить:

  • инлайн-редактирование значений прямо в ячейке (пока хватает «правильное значение» в форме);
  • дашборд статистики на самой странице (агрегация — задача агента через SQL, не UI);
  • модерация/удаление чужих замечаний, роли, авторизация по пользователям;
  • разбор-на-лету (загрузка нового документа) — работаем по уже разобранным из БД.

3. Связь с исходной операцией без вмешательства в pipeline

Ключ — логическая ссылка (contract_id, supplement_id, event_seq), БЕЗ внешнего ключа на spec_events. У вас операции уже лежат в spec_events(contract_id, supplement_id, seq, action, new_values…)seq стабилен в рамках договора. feedback просто хранит эти три значения как обычные поля.

Почему без FK:

  • FK на spec_events = риск, что повторный разбор/перезапись событий уронит вставку фидбека или каскадом заденет данные — это и есть «вмешательство». Логическая ссылка развязывает таблицы полностью.
  • feedback — отдельная таблица, ничего в боевых не меняем (никаких ALTER, никаких триггеров).

Если seq теоретически может переехать при reset/reparse — дополнительно дублируем в llm_value снимок операции (action + new_values), чтобы замечание оставалось интерпретируемым даже без живого spec_events. Этого достаточно.

4. prompt_version — хранить сразу, но честно

Хранить да — без неё нельзя мерить регрессию (стало лучше/хуже после правки промпта). Но: сейчас результат разбора версией промпта не штампуется, взять её из результата негде.

Поэтому для MVP, не трогая pipeline:

  • /teach на момент сохранения замечания читает текущую активную версию промпта из БД (ту, что Lucee отдаёт как активную) и пишет в prompt_version.
  • Это приближение: оно верно, если промпт не менялся между разбором и проверкой. Помечаем это как «версия на момент проверки», не «на момент разбора».

Правильная привязка (штамповать prompt_version в результат при разборе) — отдельная задача по основному пайплайну, делается позже и по явному решению, не в этом изолированном flow.

5. Что обязательно исключить

PII / обезличивание — не хранить:

  • название/ИНН клиента, № договора, ФИО, подписантов, реквизиты, email/телефоны;
  • оригинальный текст документа и байты файла.

Храним только тип услуги (service_name), числа, тип ошибки, версию промпта. contract_id/supplement_id — это UUID (не имена), для внутренней трассировки; в обучающий экспорт агента они не попадают.

Изоляция рабочих данных — исключить из дизайна:

  • любые ALTER существующих таблиц, FK-constraint'ы на боевые таблицы, триггеры;
  • запись в spec_events / spec_current / contracts / supplements/teach только читает их, пишет исключительно в feedback;
  • никакого generic-SQL write-эндпоинта на публичной странице — только параметризованный INSERT в teach_api.cfm (защита от инъекций).

Это ответ-проектирование, код не трогаю. Скажешь «делай» — начну с teach_schema.cfm (создание таблицы), затем teach.cfm и teach_api.cfm.