44 KiB
Полные ответы Опуса — изолированная страница /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 «как поправил юзер» = чистый сигнал ошибки. Это удобно: не свободный текст «тут неверно», а конкретная дельта.
Но «типа обучения» — это важная оговорка
Само по себе это не обучение модели. Цикл «учится» только если правки где-то сохраняются и переиспользуются. Если правка живёт в рамках одной сессии и исчезает — система не умнеет, это просто ручная корректировка. Так что ключевое в фиче — не сам акт правки, а накопление и переиспользование правок.
Дальше есть три уровня, и не надо их путать:
- Регрессия (самое дешёвое и полезное сразу) — правки копятся как golden-набор, на нём гоняется промпт, видно реальный % ошибок.
- Prompt-learning — из частых ошибок осознанно дополняется глоссарий/правила/few-shot в промпте. Это и есть практичное «обучение» в вашем случае.
- 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 шт.)
teach_schema.cfm— одноразовый:CREATE TABLE IF NOT EXISTS feedback (…). Зашёл по URL один раз → таблица создана.teach.cfm— сама страница:- список уже разобранных договоров/ДС (читает
contracts+supplements+spec_events); - выбрал ДС → рисует ту же таблицу операций (
Действие/Услуга/Цена/Кол-во/Сумма/Дата), что в твоём примере, но с колонкой⚠и кнопками[✓ всё верно],[➕ пропущена позиция]; - JS отправляет замечание
POST-ом наteach_api.cfm.
- список уже разобранных договоров/ДС (читает
teach_api.cfm— приём замечания: параметризованныйINSERTвfeedback(никакого конкатенированного SQL — защита от инъекций). Возвращает JSON{ok:true}.
Источник данных для таблицы операций —
spec_events(action,new_valuesJSONB,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. Этапы внедрения
teach_schema.cfm→ создать таблицуfeedback.teach.cfm(чтение разобранных ДС + рендер таблицы с⚠) — без сохранения, только UI.teach_api.cfm(параметризованный INSERT) + подключить JS-отправку.- Проверка end-to-end на одном реальном ДС (как твой
допник-1-XXX002). - 2–3 контрольных SQL-запроса агента → убедиться, что данные читаемы и агрегируются.
- (позже, отдельно) первый цикл анализа → правка промпта → регрессия.
9. Открытые вопросы к тебе
/teachработает только по уже разобранным документам (из БД) — или нужна и загрузка-разбор прямо на странице? (загрузка-разбор = переиспользование основного пайплайна, сложнее).- Доступ к
/teach— нужен ли пароль/ограничение, или dev-стенд открыт? service_name— оставляем как есть (тип услуги) или дополнительно маскируем?- Кто reviewer — нужно различать разных проверяющих или достаточно анонимной сессии?
Это план, код не трогал. Скажешь по каким пунктам «делай» (и ответишь на вопросы из §9) — начну с teach_schema.cfm.
ЧАСТЬ 7: Ответ про промпт для Опуса
Текст для Опуса хороший — чёткие требования и правильные ограничения (не трогать пайплайн, без online-training, обезличенность). Но в нынешнем виде Опус ответит обобщённо, потому что в промпте нет контекста вашего стека и текущей схемы. По каждому из 5 вопросов это критично. Что я бы добавил, прежде чем отправлять:
Чего не хватает Опусу для предметного ответа
-
Стек и маршрутизация. Lucee/CFML 6.0, файловая маршрутизация (
teach.cfm→/teach), PostgreSQLbaza. Без этого Опус не поймёт, как «не трогать пайплайн» технически (а ответ — просто новый файл, без правок существующих). -
Текущая схема — особенно для 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. Опус должен это знать, иначе предложит схему, не совпадающую с вашей. -
Форма результата (для Q1/Q2). Таблица операций
Действие / Услуга / Цена / Кол-во / Сумма / Дата, режимыpartial | full_replace. Дайте Опусу реальный пример вывода (твойдопник-1-XXX002). -
Ключевой факт для Q4. Сейчас результат разбора не штампуется версией промпта — этого в промпте нет, а это определяет весь ответ на Q4. Без упоминания Опус посоветует «просто сохраняйте prompt_version», не зная, что её сейчас негде взять, кроме «текущей активной».
-
Как агент потом читает БД (для 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.