- Рабочий compare-пайплайн не трогаем: новая фича должна жить полностью изолированно.
- Цель: отдельная страница с тем же UI-скелетом, что и compare results, но с возможностью оставлять замечания по строкам таблицы операций и сохранять их в БД для дальнейшего анализа агентом.
## 1. Минимальная схема `feedback`
Для MVP достаточно плоских колонок для агрегации и `JSONB` для значений:
- Разбор новых загрузок на этой странице; работать только по уже разобранным документам из БД.
## 3. Связь с исходной операцией
Правильная связь — логическая, без FK и без вмешательства в рабочий pipeline: `(contract_id, supplement_id, event_seq)`.
Почему так:
-`spec_events` уже хранит операции как `spec_events(contract_id, supplement_id, seq, action, new_values, ...)`.
-`feedback` просто повторяет эти значения как обычные поля.
- Без FK исключается риск связать feedback с жизненным циклом боевых событий или сломать вставки при повторном разборе.
Если `event_seq` когда-то переедет из-за пересборки данных, в `llm_value` / `correct_value` нужно сохранять снимок операции (`action` + `new_values`), чтобы замечание оставалось интерпретируемым.
## 4. `prompt_version`
Версию промпта хранить нужно, иначе нельзя считать регрессию между изменениями.
Но сейчас результат разбора не штампуется версией промпта, поэтому для MVP предлагается практический компромисс:
-`/teach` при сохранении замечания пишет текущую активную версию промпта из БД.
- Это честно помечается как версия на момент проверки, а не на момент исходного разбора.
- Правильная фиксация `prompt_version` в результате разбора — отдельная задача основного pipeline, не часть isolated feedback flow.
## 5. Что исключить
### PII / обезличивание
- Не хранить название/ИНН клиента, номер договора, ФИО, подписантов, реквизиты, email, телефоны.
- Не хранить оригинальный текст документа и байты файла.
### Изоляция рабочих данных
- Не использовать `ALTER` существующих таблиц.
- Не добавлять FK, триггеры или write-логики в `spec_events`, `spec_current`, `contracts`, `supplements`.
-`/teach` должен писать только в `feedback` через отдельный параметризованный endpoint.
## Итог
Опус подтвердил, что MVP должен быть:
1. Отдельной страницей с тем же UX-скелетом, что и compare results.
2. Отдельным write endpoint и отдельной таблицей `feedback`.
3. Полностью изолированным от рабочего compare-пайплайна.
4. Приспособленным для SQL-аналитики агентом через плоские колонки + `JSONB`.
Главный принцип: это не online-training, а сбор структурированного feedback для последующего анализа и планирования изменений.
# Ответ Опуса — режим «исправление ошибок / обучение»
Дата: 26.06.2025 | В ответ на обсуждение feedback-цикла.
---
## Суть
Режим: **юзер загружает документы → система выдаёт результат → юзер правит ошибки → правки сохраняются**.
Это не «обучение модели», а **накопление эталонных данных** (golden dataset) через естественный интерфейс исправления. Юзер не размечает абстрактно — он правит конкретный неверный результат.
1.**Структурировать тип ошибки:** «пропущена строка», «неверная цена», «UPDATE вместо ADD», «ложный дубликат»
2.**Версия промпта** при каждом результате — чтобы знать актуальность ошибки
3.**Конфиденциальность** — реальные договоры с реквизитами копятся в БД, обсудить с заказчиком
---
**Решение:** отложить. Позже вернуться и спроектировать.
---
# Часть 2 — Конкретный UI для отметки ошибок
Дата: 26.06.2025 | Опус изучил реальный вывод (карточка + таблица операций) и предложил привязку к элементам интерфейса.
## Привязка: не к абзацам, а к строкам таблицы операций
Результат — **таблица операций** (`Действие / Услуга / Цена / Кол-во / Сумма / Дата`), а не текст-простыня. Замечание цепляется к строке таблицы, а не к абзацу исходника.
### Как выглядит (на реальном примере)
```
✓ допник-1-XXX002-01200_3.docx — 2 оп., partial (14с) [✓ всё верно]
Один клик = положительный сигнал, без расписывания.
### `[➕ Система пропустила позицию]` — под таблицей
Для случая, когда услуга была в документе, но LLM её не извлекла. Открывает форму ввода пропущенной строки.
## Что уходит в базу (обезличенно)
```
operation: ADD
service: "Облачный диск Valo Cloud, в составе…" ← без названия клиента
field: price
llm_value: 68700
correct: 68000
error_type: wrong_price
prompt_version: v1.0.178
```
Никаких названий компаний, ФИО, № договора — только структура услуги и числа.
## Почему так, а не поле у каждого абзаца
- Реальный вывод — **таблица**, а не текст. Абзацев нет, есть операции
- Привязка к операции даёт агрегацию по типу ошибки
- Готовая дельта «LLM выдала X → правильно Y» как обучающий сигнал
- Ровная укладка в событийную модель (`apply_events.cfm`)
---
**Решение:** отложить. Ждать «делай» для реализации.
---
# Часть 3 — Текст для заказчика
**Этап опытной эксплуатации (обучение системы)**
На первое время предлагаем режим проверки: вы загружаете реальные документы, система выдаёт результат, а вы отмечаете ошибки — что распознано неверно (пропущена строка, неверная цена, неправильное сопоставление и т.п.).
Эти отметки **в обезличенном виде** (без названий компаний, ФИО, реквизитов и иных персональных данных) накапливаются в базе и используются для дальнейшей настройки и обучения системы.
Это позволит откалибровать сервис на ваших реальных договорах, а не на тестовых примерах, и системно повышать точность.
Посмотрел, как сейчас устроено: промпт в 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: Ответ про привязку замечаний к строкам таблицы
Дай гляну, как сейчас выглядит вывод результата — чтобы предложить привязку замечаний к реальным элементам интерфейса, а не абстрактно.
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с) [✓ всё верно]
**Кнопка `[✓ всё верно]`** вверху карточки — если разбор документа корректен целиком. Один клик = положительный сигнал, ничего расписывать не надо.
**Кнопка `➕ Система пропустила позицию`** — для случая, которого нет в таблице (услуга была в документе, но 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
Это план, не правка кода — изучу структуру, чтобы план лёг на реальный код, а не в воздух. Читаю ключевые файлы.
Изучил структуру. Lucee-приложение с файловой маршрутизацией (`teach.cfm` → `/teach`), PostgreSQL `baza`, есть событийная модель (`spec_events`, `spec_current`) и уже готовый read-only SQL-эндпоинт `/db/query?sql=…` в db.cfc — через него агент и будет читать БД. Вот детальный план.
---
# План: страница `/teach` (опросник + обучение)
## 1. Принцип изоляции — что НЕ трогаем
Маршрутизация в Lucee файловая → **новый путь = новый файл**, существующий код не редактируется вообще:
| существующие таблицы (`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.
```sql
CREATETABLEIFNOTEXISTSfeedback(
idUUIDPRIMARYKEYDEFAULTgen_random_uuid(),
created_atTIMESTAMPTZDEFAULTnow(),
-- привязка (внутренняя трассировка, в обучающий экспорт НЕ идёт)
contract_idUUID,-- FK-логически на contracts, без жёсткого constraint
supplement_idUUID,
event_seqINTEGER,-- какая операция ДС (NULL для "пропущено"/"документ в целом")
Каждая запись самодостаточна: видно **что было** и **как надо** → готовая обучающая пара.
## 5. Обезличивание
-В`feedback`**не копируем** название клиента, № договора, ФИО, реквизиты.
- Храним только: `service_name` (тип услуги — «WAF Positive Technologies»), числа, тип ошибки, версию промпта.
-`contract_id/supplement_id` — это UUID (не имя), для внутренней трассировки. В **обучающий экспорт** агента эти id не включаются — только структурные поля.
- Для заказчика формулировка: *«в обезличенном виде, без названий компаний и персональных данных»* — соответствует фактической схеме.
## 6. Как агент читает и анализирует (потом)
Через уже существующий `/db/query?sql=…` (read-only) или напрямую psql. Примеры:
**Системные слабости промпта:**
```sql
SELECTerror_type,field,count(*)ASn
FROMfeedbackWHEREverdict='error'
GROUPBYerror_type,fieldORDERBYnDESC;
```
**Качество по версии промпта (регрессия):**
```sql
SELECTprompt_version,
count(*)FILTER(WHEREverdict='correct')ASok,
count(*)FILTER(WHEREverdict='error')ASerr
FROMfeedbackGROUPBYprompt_version;
```
**Выгрузка обучающих пар (для правки промпта/few-shot):**
Дальше агент: смотрит агрегат → предлагает правку промпта/глоссария → прогоняет на накопленных парах → сравнивает метрику до/после. **Никакой авто-инъекции** правок в промпт — только осознанное batch-обновление.
## 7. Версия промпта — нюанс
Сейчас результат разбора **не штампуется** версией промпта. Варианты:
- (минимум, без правки основного кода) `/teach` пишет в `prompt_version`**текущую активную** версию промпта на момент замечания — приблизительно, с оговоркой;
- (правильно, отдельной задачей позже) при разборе сохранять `prompt_version` в результат — но это уже касается основного пайплайна, делать отдельно и по «делай».
Флажок: на старте берём активную версию, точность привязки уточним позже.
## 8. Этапы внедрения
1.`teach_schema.cfm` → создать таблицу `feedback`.
2.`teach.cfm` (чтение разобранных ДС + рендер таблицы с `⚠`) — без сохранения, только UI.
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.** У вас уже есть событийная модель:
Связь с исходной операцией = `(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 для значений:
```sql
CREATETABLEIFNOTEXISTSfeedback(
idUUIDPRIMARYKEYDEFAULTgen_random_uuid(),
created_atTIMESTAMPTZDEFAULTnow(),
-- связь с исходной операцией (логическая, без FK)
- инлайн-редактирование значений прямо в ячейке (пока хватает «правильное значение» в форме);
- дашборд статистики на самой странице (агрегация — задача агента через 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.
Храним только тип услуги (`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`.
# Анализ ответа Опуса — zip_source, UI-режимы, промпты
Дата: 26.06.2025 | Ответ на [opus-request-zip-plan.md](../Files/opus-request-zip-plan.md)
---
## 1. Оценка ответа в целом
**Качество: высокое.** Опус прочитал реальный код, разобрался в архитектуре, дал конкретные диффы по слоям. Не «размышления вообще», а точные строки и функции. 95% рекомендаций — правильные.
**Что упущено:**
- Lucee-слой (`upload.cfm`, `api.cfm`) — тоже участвует в upload, но Опус его не проанализировал
-`confidence` — предлагает сохранять в БД, но не говорит где именно брать (LLM возвращает? парсить из промпта?)
- Порог 100 файлов для «Потока» — спорный, обсудим ниже
---
## 2. По пунктам
### 2.1. `zip_source` — ✅ СОГЛАСЕН полностью
План по слоям правильный. Ключевые моменты:
- **`zip_source` не участвует в classify/group/compare** — верно. Чисто визуальный атрибут.
- **Формат: имя ZIP с расширением** — да, `«Ромашка.zip»`.
- **PK не трогаем (UUID)**, «ID = zip/filename» только для отображения — верно.
- **Дедупликация по паре `(zip_source, name)`** — ⚠️ самый критичный момент. Опус прав: если не сменить ключ, одноимённые файлы из разных ZIP будут перезаписываться. Но **надо проверить**: текущий код в `addRegularFile()` (files.js) ищет по `f.name`. При добавлении `zip_source` нужно либо:
- Ключ = `zip_source + "/" + filename` (как предлагает заказчик)
- Или ключ = `(zip_source || "") + filename`
Я за вариант с конкатенацией в одну строку — проще для сравнения.
- **unzip.py не трогаем** — верно. Имя ZIP уже есть на фронте (`file.name`).
### 2.2. Вариант отображения — ✅ СОГЛАСЕН (Вариант А)
Заголовок-секция ZIP + отступ `padding-left: 24px`.
- Просто, без нового состояния
- Соответствует тому что описал заказчик
- Опциональное сворачивание — да, но не в первой итерации
### 2.3. Два UI-режима — ⚠️ ЧАСТИЧНО СОГЛАСЕН
**Плюсы:**
- Бэкенд не меняется — правильно
-`state.ui.mode` — хорошее место
- Сводный отчёт («зелёное сворачиваем, красное показываем») — отличная идея
- Авто-определение + ручной override — разумно
**Спорные моменты:**
- **Порог 100 файлов** — слишком низкий для автоматического предложения. При 100 файлах текущий UI работает нормально (скролл, 50vh). Реальный болевой порог — **200-300+**. Предлагаю порог **200**.
- **«Поток» сейчас не нужен.** Если заказчик работает с 5-50 файлами, весь Stream Mode — оверинжиниринг. Но архитектурно заложить `state.ui.mode` — дёшево и правильно.
### 2.4. Промпты — ✅ СОГЛАСЕН, с уточнениями
#### Classify — проблемы А-Д:
**А. Counterparty / блок про стороны НУБЕС** — 🔴 КРИТИЧНАЯ.
Опус абсолютно прав. Промпт сейчас не говорит что НУБЕС = Исполнитель. LLM возвращает случайную сторону. Чинится одной вставкой в промпт. Делать **первым**.
НО: Опус предлагает «ИНН 7727... (взять у заказчика)». Это перебор. Достаточно:
```
НУБЕС известен как: «НУБЕС», «ООО НУБЕС», «ООО "НУБЕС"», «Nubes».
counterparty — ВСЕГДА вторая сторона, НИКОГДА не НУБЕС.
```
**Б. parent_number у contract** — ✅ верно. `parent_number = null` для contract.
**В. Мусорные документы** — ✅ верно. Добавить примеры в `doc_type=other`.
**Д. confidence** — ⚠️ спорно. Опус говорит «добавить колонку и показывать low в отчёте». Но `confidence` сейчас даже не сохраняется. Предлагаю **сначала убрать из промпта** (меньше путаницы), а потом, когда будет реальная потребность — добавить и колонку, и парсинг.
#### Compare (diff) — ✅ СОГЛАСЕН
- «UNRESOLVED вместо дубль-ADD при сомнении» — верно
-`temperature=0.1` для diff — проверить (скорее всего уже)
-`full_replace` → автоматическое удаление старых строк на стороне Python — **умная идея**, снижает нагрузку на LLM
#### Разные промпты под сценарии — ✅ СОГЛАСЕН
Не нужно. Один classify + один diff. Меньше рассинхрона.
### 2.5. Гомоглифы — ⚠️ ОСТОРОЖНО
Опус предлагает:
-`С/C → C` (латиница)
-`О/0` — «трактовать осторожно»
-`Ё → Е`
**Моё мнение:**
-`С→C` и `Ё→Е` — **опасно**. Это меняет семантику номера. `МЭС-123` ≠ `МЭC-123`. Лучше: **не заменять, а добавить второй проход сравнения** — если точное совпадение не найдено, попробовать с гомоглифами. Или нормализовать ОБА варианта (и кириллицу, и латиницу) к единому представлению, но сохранять оригинал для отображения.
- Конкретно для `Ё`: да, `Ё→Е` допустимо (в делопроизводстве Ё часто заменяют на Е). Но лучше сделать настраиваемым.
### 2.6. Приоритеты — ✅ СОГЛАСЕН с корректировкой
| Что | Приоритет Опуса | Моя оценка |
|-----|----------------|-----------|
| Counterparty в промпте | P0 | P0 ✅ |
| doc_type=other мусор | P0 | P0 ✅ |
| Гомоглифы | P0 | P1 ⚠️ (осторожно, не ломать) |
| zip_source | P1 | P1 ✅ |
| Группировка по ZIP в UI | P1 | P1 ✅ |
| Режим «Поток» | P2 | P3 (отложить, нет потребности) |
| confidence в БД | P3 | P3 (или убрать из промпта) |
| diff-UNRESOLVED | P3 | P2 (дёшево, большой эффект) |
---
## 3. Что Опус упустил
### 3.1. Lucee-слой
`upload.cfm` и `api.cfm` на Lucee тоже обрабатывают загрузку. Если файл идёт через Lucee (а не напрямую на VM), `zip_source` нужно прокинуть и там. Надо проверить — идёт ли upload через Lucee или напрямую на VM.
**Факт:** судя по `index.cfm`, JS грузится с VM (`contracts.kube5s.ru/static/app.js`), а upload идёт на `VM_API + '/upload'`. Значит Lucee в upload **не участвует**. `zip_source` в Lucee не нужен. Опус оказался прав молча.
### 3.2. Промпты в БД vs хардкод
Опус верно заметил: «промпты берутся из БД (Lucee), fallback — хардкод. Менять надо в БД через интерфейс промптов». Это **критично важно для исполнителя**: если просто поправить `FALLBACK_EXTRACT`/`FALLBACK_DIFF` в `llm_prompt.py` — в проде ничего не изменится, потому что используется версия из БД.
**Порядок правки промптов:**
1. Сначала в БД через UI (`/prompt.cfm`)
2. Потом в хардкоде (для fallback)
### 3.3. Порог для «Потока»
100 файлов — слишком консервативно. Таблица с `max-height: 50vh` и `overflow-y: auto` нормально работает при 100-150 файлах. Предлагаю **200** как порог для автопредложения. Но лучше — **сделать настраиваемым** (константа в начале app.js).
1. Правка classify-промпта: блок про стороны НУБЕС + parent_number=null + примеры мусора
2. Правка diff-промпта: UNRESOLVED вместо дубль-ADD, temperature проверка
**Что делаем дальше (P1):**
3.`zip_source` сквозь все слои (БД → Python → JS)
4. Группировка по ZIP в таблице (Вариант А)
5. Дедупликация по `(zip_source, filename)`
**Что откладываем:**
6. Режим «Поток» — пока нет потребности
7. Гомоглифы — нужно больше примеров от заказчика
8. confidence — убрать из промпта, вернуть когда будет нужно
**Главный риск (ещё раз):** дедупликация в `addRegularFile()`. Без правки ключа на `(zip_source, name)` — фича сломается на первом же случае одинаковых имён в разных ZIP.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.