Files
contracts/History/llm-analysis/sonnet-architecture-answer.md
T

211 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ответ: архитектура сервиса "Сверка договоров"
_Дата: 2026-06-13_
---
## 1. Правильно ли разбиты слои?
Разбивка в целом правильная. Принцип «один файл — одна ответственность» выдержан.
Критических проблем нет, но есть два момента, которые стоит учесть.
### Что хорошо
- `parser.py` — чисто I/O-слой: байты → JSON. Никакой логики.
- `textify.py` — чисто форматирование: JSON → текст. Никакой логики.
- `db.py` — чисто транспорт к БД. Не знает о бизнес-сущностях.
- `test_routes.py` — отдельный Blueprint, не засоряет app.py.
### Что стоит скорректировать
**`db.py` — разделить на транспорт и схему.**
Сейчас там `ensure_db()` — это уже «знание» о схеме. Когда появятся таблицы
(`contracts`, `supplements`, `spec_rows`, `spec_history`), их создание (DDL)
стоит вынести в отдельный `schema.py`. `db.py` остаётся просто `connect()` и `query()`.
**Слой LLM стоит разделить надвое:**
- `llm_client.py` — HTTP-клиент к aillm.ru: отправить промпт → получить строку ответа.
Не знает ни о договорах, ни о спецификациях.
- `extractor.py` — бизнес-логика: взять текст договора, сформировать промпт,
вызвать llm_client, распарсить ответ в строки спецификации.
Это важно: если поменяется LLM — меняем только `llm_client.py`.
Если поменяется формат ответа — только `extractor.py`.
**Итоговый состав слоёв:**
```
parser.py bytes → elements JSON (уже есть, не трогать)
textify.py elements → текст для LLM (уже есть, не трогать)
db.py connect() + query() (уже есть, убрать ensure_db)
schema.py DDL: CREATE TABLE IF NOT EXISTS
upload.py сохранить файл в БД (documents)
llm_client.py HTTP к aillm.ru → строка ответа
extractor.py текст → структурированные строки (промпт + парсинг ответа)
differ.py сравнение строк между допниками → список изменений
api.py Blueprint: /contracts, /supplements, /history
app.py сборка слоёв, Flask-приложение
test_routes.py Blueprint /test (уже есть)
```
---
## 2. В каком порядке создавать
Каждый этап самодостаточен и проверяем до перехода к следующему.
### Этап 1 — основа хранения
`schema.py` → DDL всех таблиц.
Запустить `python schema.py` — таблицы созданы. Проверить через `/test sql`.
### Этап 2 — загрузка файлов
`upload.py` → принять файл, вызвать `parser.parse()`, вызвать `textify.to_text()`,
сохранить в `documents(original_bytes, parsed_text, mime, filename)`.
Добавить POST `/upload` в `app.py`. Проверить curl-ом.
### Этап 3 — LLM клиент
`llm_client.py` → POST к aillm.ru, вернуть строку.
Проверить отдельно: `python llm_client.py` с тестовым промптом.
### Этап 4 — извлечение строк спецификации
`extractor.py` → взять `parsed_text`, сформировать промпт, вызвать `llm_client`,
распарсить ответ в список `spec_row`.
Проверить на одном docx через тест-скрипт.
### Этап 5 — сравнение (diff)
`differ.py` → взять два списка `spec_row`, вернуть изменения.
Это чистая функция: `diff(rows_old, rows_new) → changes`.
Проверить unit-тестом без БД.
### Этап 6 — API
`api.py` → Blueprint с GET/POST для договоров, допников, истории.
Подключить в `app.py`.
---
## 3. Поток данных между слоями
Правило: слои передают данные через простые Python-структуры (dict, list).
Никаких прямых вызовов «через слой» — только соседние слои.
```
Файл (bytes)
parser.parse(bytes, mime) → {"elements": [...]}
textify.to_text(elements) → str
upload.py: сохранить в DB, получить document_id
extractor.extract(parsed_text) → [{"pos": 1, "name": "...", "qty": 10, ...}]
│ (внутри вызывает llm_client.ask(prompt) → str)
schema: сохранить строки в spec_rows(document_id, pos, ...)
differ.diff(rows_v1, rows_v2) → [{"pos": 3, "field": "qty", "old": 5, "new": 10}]
schema: сохранить в spec_history
```
Между слоями **нет импортов друг друга**, кроме:
- `upload.py` импортирует `parser` и `textify` (это нормально — upload оркеструет парсинг)
- `extractor.py` импортирует `llm_client` (клиент — зависимость экстрактора)
- `app.py` и `api.py` импортируют всё — они и есть точки сборки
`db.py` никто не импортирует напрямую, кроме `upload.py`, `extractor.py` и `api.py`.
`schema.py` вызывается только один раз при старте из `app.py`.
---
## 4. Как должен выглядеть app.py
`app.py` — точка входа и сборки. Бизнес-логики ноль.
```python
from flask import Flask
import db, schema
from test_routes import test_bp
from api import api_bp
def create_app():
app = Flask(__name__)
# 1. Инициализация схемы при старте
schema.ensure_schema()
# 2. Регистрация Blueprint-ов
app.register_blueprint(test_bp)
app.register_blueprint(api_bp)
# 3. Системные маршруты
@app.route("/health")
def health():
return "OK", 200
return app
if __name__ == "__main__":
create_app().run(host="0.0.0.0", port=5000)
```
Правило: если в `app.py` появляется `if`, `for` или бизнес-слово — это уже лишнее.
---
## 5. Потенциальные проблемы
### LLM не гарантирует структуру ответа
Самая острая проблема. Модель может вернуть текст в произвольном формате,
сломать JSON, пропустить поля, придумать данные.
**Решение:**
- В `extractor.py` — строгая схема промпта с примером ответа.
- Парсинг ответа через `try/except` с явным возвратом `{"error": "parse_failed", "raw": ответ}`.
- Никогда не падать — помечать строки как `unresolved`.
### Идентификация изменённой строки в допнике
Самая неоднозначная задача: иногда в допнике новое полное состояние,
иногда — дельта. Определить это автоматически сложно.
**Решение для `differ.py`:**
- Сначала попробовать детерминированный diff по позиции/артикулу.
- Если совпадение < порога — пометить как `ambiguous`, не фантазировать.
- Заказчик потом разбирает вручную `ambiguous`-записи.
### Сопоставление артикула с каталогом
Задача нетривиальная, код-имён в спецификациях нет, матч только по описанию.
**Решение:** вынести в отдельный `matcher.py`, реализовать как отдельный шаг после основного пайплайна. Пометить как `optional`, не блокировать основной поток.
### Размер документов vs контекст LLM
Большая спецификация (100+ строк) может не влезть в контекст.
**Решение в `extractor.py`:** разбивать таблицы на чанки, обрабатывать частями,
собирать результат. Это нужно заложить сразу — переделывать потом дороже.
### Транзакционность при загрузке
Файл загружен → парсинг ок → LLM вызов → ошибка → документ в БД наполовину.
**Решение:** хранить в `documents` поле `status` (`uploaded` / `parsed` / `extracted` / `error`).
Обновлять после каждого шага. Зависший `uploaded` — сигнал для повтора.
---
## Итог
| Вопрос | Ответ |
|--------|-------|
| Разбивка слоёв | Правильная. Добавить `schema.py`, разделить LLM на `llm_client` + `extractor` |
| Порядок | schema → upload → llm_client → extractor → differ → api |
| Поток данных | Через dict/list, нет перекрёстных импортов |
| app.py | Только сборка: `ensure_schema()` + `register_blueprint()` |
| Риски | LLM-нестабильность, diff-амбивалентность, чанкинг, транзакционность |
Всё, что не решается надёжно детерминированно — помечать `unresolved`, не фантазировать.