Files
contracts/History/opus-questions.md

8.1 KiB
Raw Permalink Blame History

Вопросы к 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 → следующий.

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):

<cfquery>SELECT COALESCE(MAX(seq),0) FROM spec_events WHERE contract_id=? FOR UPDATE</cfquery>
<cfset seq = result + 1>
<!-- цикл по ops, каждый seq++ -->

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 — стабильность идентификации строк

Текущая схема: строка спецификации идентифицируется хэшем:

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-совместимое хранилище?

Важно

  • НЕ делай код. Только анализ и рекомендации.
  • Ответь кратко по каждому вопросу: рекомендуемое решение и почему.
  • Если нужно уточнение — спроси, но проект небольшой, контекста выше достаточно.