diff --git a/History/opus-questions.md b/History/opus-questions.md new file mode 100644 index 0000000..c0da924 --- /dev/null +++ b/History/opus-questions.md @@ -0,0 +1,122 @@ +# Вопросы к Opus 4.8 по проекту Сверка договоров + +**Дата:** 2026-06-23 +**Версия:** v1.0.107 + +--- + +## Что за проект + +Сервис для сверки договоров облачного провайдера. Юзер загружает договоры/допники (docx/pdf), парсер извлекает текст, LLM сравнивает с накопленной спецификацией по цепочке (Event Sourcing). На выходе — какие услуги добавились/изменились/удалились. + +**Стек:** Lucee CFML 6.0 (сервер приложений) + PostgreSQL 15 + Python 3 на отдельной ВМ (nginx + convert_server.py). LLM: gpt-oss-120b через api.aillm.ru (бесплатный, без структурного вывода). Фронт: CFML-страницы + JavaScript (EventSource для SSE). + +**Масштаб:** сейчас 1 тестовый пользователь. В планах Keycloak, мульти-юзеры. + +**Производительность:** 5 файлов (3-35 KB docx) × последовательные вызовы LLM = 80-100 секунд. Каждый вызов LLM: 6-50 секунд. + +--- + +## Вопрос 1: Параллельные вызовы LLM + +**Текущая схема:** цикл по supplements последовательно. Для каждого: взять текущую spec_current → textify документа → вызвать LLM → получить ops → apply_events → следующий. + +```python +for s in supps: + cur = get_current_spec() # из БД + text = textify(elements_json) # текст документа + ops = call_llm(cur, text) # HTTP к LLM, 6-50с + apply_events(ops) # HTTP к Lucee + # следующий видит обновлённую spec_current +``` + +**Проблема:** 5 файлов × (12+20+38+7+6)с = 80-100с. LLM-вызовы занимают 95% времени. + +**Вопрос:** Можно ли распараллелить вызовы LLM, если известно что первый файл = исходная спецификация (база), а остальные — допники к ней? То есть: файл1 → LLM → накопили. Затем файлы 2-5 запустить параллельно, каждый сравнивается с результатом файла1? Или есть архитектурный паттерн лучше? Что будет с seq в spec_events при параллельных apply_events? + +--- + +## Вопрос 2: Event Sourcing seq без гонки + +**Текущая схема:** в apply_events.cfm (Lucee): +```cfm +SELECT COALESCE(MAX(seq),0) FROM spec_events WHERE contract_id=? FOR UPDATE + + +``` + +FOR UPDATE не работает с агрегатной функцией MAX() в PostgreSQL 15 — падает с ошибкой. Убрали. + +**Структура:** `spec_events(contract_id, seq)`, UNIQUE(contract_id, seq). Seq монотонный в рамках контракта. + +**Вопрос:** Как правильно сделать атомарный инкремент seq без гонки? Варианты: +1. `CREATE SEQUENCE spec_events_seq_{contract_id}` — но это динамический DDL на каждый контракт +2. `LOCK TABLE spec_events IN EXCLUSIVE MODE` — грубо +3. `INSERT ... SELECT COALESCE(MAX(seq),0)+1, ...` — атомарно ли? +4. Отдельная таблица-счётчик с `UPDATE ... RETURNING` + +Что рекомендовано для PostgreSQL + CFML? + +--- + +## Вопрос 3: name_hash — стабильность идентификации строк + +**Текущая схема:** строка спецификации идентифицируется хэшем: +```sql +md5(lower(trim(name)) || coalesce(date_start, '')) +``` +Вычисляется в INSERT (БД) из данных, которые LLM вернула в new_row. + +Для UPDATE/DELETE LLM возвращает target_hash — указывает какую строку менять/удалить. + +**Проблема:** LLM может написать название услуги чуть иначе в разных файлах: +- Файл1: "Аренда стойко-места, в составе: Номинальная мощность – 10 кВт" +- Файл2: "Аренда стойко-места, в составе: Номинальная мощность - 10 кВт" (другое тире) +- Хэши разные → LLM видит ADD/DELETE вместо UPDATE + +**Вопрос:** Как стабилизировать? Варианты: +1. Нормализовать name перед хэшированием (убрать пунктуацию, лишние пробелы) +2. LLM возвращает не target_hash, а явно target_name + date_start, а хэш вычисляется сервером +3. Fuzzy matching: если target_hash не найден, искать близкое имя через `levenshtein()` или `similarity()` +4. Хранить исходный name из первого появления и использовать его как ключ + +Что правильнее архитектурно? + +--- + +## Вопрос 4: UPDATE/DELETE без имён в UI + +**Проблема:** в результатах сравнения ADD-строки показывают все поля (услуга, цена, кол-во, сумма, дата). UPDATE и DELETE — пустые ячейки. + +**Причина:** LLM для UPDATE/DELETE возвращает только target_hash и new_values (для UPDATE). Имени услуги нет — оно было в исходной спецификации. UI строит таблицу из d.ops, где для ADD есть new_row.name, а для UPDATE/DELETE — нет. + +**Вопрос:** Где правильнее решать: +1. В промпте LLM — требовать всегда возвращать name в new_values для UPDATE и в отдельном поле для DELETE +2. В Python (convert_server.py) — перед отправкой ops в SSE, дополнить их именами из spec_current (доп. запрос к БД) +3. В UI (JavaScript) — перед рендерингом запрашивать spec_current и резолвить имена + +Что даст меньше запросов и меньше шансов рассинхронизации? + +--- + +## Вопрос 5: Мульти-юзеры и рост данных + +**Планы:** Keycloak, каждый юзер имеет свои контракты и промпты. + +**Таблицы:** +- `documents`, `supplements`, `contracts` — растут с каждым файлом +- `spec_events` — растёт с каждым сравнением (каждый ADD/UPDATE/DELETE — строка) +- `spec_current` — текущая спецификация, O(услуг в контракте) +- `prompts` — одна таблица на всех (в плане: `user_id + timestamp` как ID) + +**Вопросы:** +1. `prompts` — одна таблица с `user_id TEXT` норм для масштаба 10-100 юзеров? Или партиционировать? +2. `spec_events` — для 100 контрактов по 20 услуг с 5 версиями каждый = 10K строк. Нужно ли партиционирование сейчас? +3. `documents` хранит бинарные байты (`original_bytes BYTEA`). Для 1000 документов по 30KB = 30MB — норм в PostgreSQL или вынести в S3-совместимое хранилище? + +--- + +## Важно +- **НЕ делай код. Только анализ и рекомендации.** +- Ответь кратко по каждому вопросу: рекомендуемое решение и почему. +- Если нужно уточнение — спроси, но проект небольшой, контекста выше достаточно.