Построчная форензик-обработка банковских выписок организаций: разбор файлов разного формата в единую таблицу операций, счётные агрегаты по счёту и построчная оценка операций языковой моделью с указанием кода схемы.
Проект описан в двух документах, и они — источник истины, а не этот файл:
| документ | о чём |
|---|---|
| docs/SPEC.md | что система делает, промпт судьи, инварианты |
| docs/PLAN.md | этапы, критерии готовности, что уже сделано |
| docs/DOMAIN_TYPOLOGY.md | схемы C1–C9, за которыми следит судья |
| docs/OPEN_ITEMS.md | открытые вопросы, требующие решения человека |
| docs/EVAL.md | как измеряется качество судьи |
Пакеты ставятся только в локальный .venv и только через uv pip —
глобальный pip в этом проекте не используется.
uv venv
uv pip install -e .
Модель не устанавливается локально: судья обращается к vLLM-серверу по URL
(config/config.yaml, секция judge). Пакет vllm ставить не нужно.
billeval scan --detect # найти файлы и определить формат
billeval parse # разобрать в таблицу операций
billeval aggregate # счётные агрегаты по счёту
billeval prefetch-orgs # справки об организациях (Э6, необязательно)
billeval judge # построчная оценка моделью
billeval report # отчёт
prefetch-orgs в цепочке необязателен: судья штатно работает в режиме «профиль
неизвестен» (§8.8 SPEC). Но профиль держит полноту вердиктов — см. следующий
раздел.
Полезное по ходу:
billeval check-config # что загружено из config/, оба контура моделей
billeval stats # что в базе
billeval judge --dry-run # сколько уйдёт запросов, без обращения к модели
billeval eval # метрики судьи на эталоне из БД
billeval arbitrate --dry-run # файлы, где формат не определился правилом (§5.1)
billeval fix-rows --dry-run # строки, отсеянные как «ни даты, ни суммы» (§5.6)
Судья и Э6 ходят в разные эндпоинты, и путать их не следует:
судья (judge, eval) | Э6 (prefetch-orgs, arbitrate, fix-rows) | |
|---|---|---|
| эндпоинт | локальный vLLM, VLLM_* | DeepSeek по умолчанию, DEEPSEEK_* |
| запросов на прогон | ≈27 000 | единицы-десятки |
| зачем | следование инструкции, воспроизводимость | знание о мире (ЕГРЮЛ, ОКВЭД) |
Метрики качества судьи (§8.12 SPEC) измерены на Qwen — смена модели судьи
обнуляет эталон, поэтому ENRICH_PROVIDER переключает только Э6. Все адреса и
ключи живут в .env, в config/config.yaml стоят подстановки ${…}.
billeval check-config печатает оба контура рядом.
config/org_profiles.yaml — не украшение отчёта. Заполненность этого файла
напрямую держит полноту вердиктов suspicious: при пустом профиле включается
§8.8 SPEC, уверенность жёстко ограничивается 0.6, и спорная операция уходит с
suspicious на attention. Замер 2026-07-30 на эталоне: 0.38 без профилей
против 0.62–0.75 с профилями при одном и том же промпте.
Обратная сторона — ошибочный профиль хуже пустого. Пустой заставляет судью сомневаться; неверный заставляет уверенно судить по чужой мерке. Единственный пропуск в замере появился именно с профилем: зная, что организация — ЧОО, модель объяснила 200 000 ₽ на личный счёт физлица «штатной процедурой». Поэтому профили сверяются, а спорное помечается как спорное, а не сглаживается.
Текущее состояние: шесть организаций сверены по агрегаторам ЕГРЮЛ
(saby.ru, spark-interfax, РБК, list-org, audit-it) 2026-07-30 и 2026-08-01.
История сверки, отклонённые сведения и разбор по каждой организации —
docs/changes/2026-08-01-org-profiles-verification.md; сам конфиг содержит
только данные. Главное незакрытое: состав участников ОРЛАНа (ответы
источников противоречат) и ГРОМ-ТОЛЬЯТТИ (скрыт) — у обоих стоит явный
unknown. Связей между организациями набора по формальным основаниям ЕГРЮЛ
не установлено, и автоматически связанность не устанавливается (§8а.2).
billeval prefetch-orgs (Э6)Стадия Э6 делает то же самое командой. Провайдер по умолчанию — DeepSeek,
ключ и адрес берутся из .env (DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL):
billeval prefetch-orgs --dry-run # о ком будут справки, без обращения к сети
billeval prefetch-orgs # запросить и записать в org_profile
billeval prefetch-orgs --provider perplexity # переключить провайдера
billeval orgs --profiles # что получено и что дойдёт до судьи
Что стадия гарантирует, и почему это важнее удобства:
Null, судья
работает по §8.8, billeval check-config называет недостающую переменную;После первого прогона профили стоит перечитать глазами. Сейчас в
config/org_profiles.yaml руками сверено пять организаций, и там есть то, чего
в автоматической справке может не оказаться: конкурсное производство «КОБРЫ» и
помеченный спорным статус «ЛЕКС». Для оценки операций эти обстоятельства важнее
ОКВЭДа. Если сетевая справка их потеряла — --on-conflict=keep возвращает
ручную версию в силу, ничего не удаляя.
Процедура ниже остаётся полезной, когда нужен не автоматический профиль, а проверяемые поля со ссылками и отдельный перечень расхождений между источниками — то есть когда сверку принимает человек.
Шаг 1. Собрать список ИНН из базы.
sqlite3 billeval.db \
"SELECT inn, name_normalized FROM organization WHERE inn IS NOT NULL AND inn <> '';"
Шаг 2. Отдать модели этот промпт, подставив список ИНН.
Ты сверяешь данные юридических лиц по открытым источникам РФ.
Для каждого ИНН из списка найди в открытых источниках (ЕГРЮЛ и агрегаторы:
rusprofile, list-org, checko, saby, spark-interfax, РБК Компании) и верни
JSON-массив объектов строго такой формы:
{
"inn": "...",
"name_full": "полное наименование как в реестре",
"ogrn": "...",
"kpp": "...",
"registered": "ГГГГ-ММ-ДД",
"address": "юридический адрес",
"director": "ФИО и должность с датой назначения",
"capital": "уставный капитал",
"okved_primary": "код и ТОЧНАЯ формулировка из классификатора",
"okved_extra": ["код — формулировка", "..."],
"licenses": "лицензии (Росгвардия, МЧС и пр.) или null",
"status": "действующая | ликвидирована | в ликвидации | банкротство",
"status_date": "ГГГГ-ММ-ДД или null",
"sources": ["URL", "URL"],
"conflicts": "чем именно источники противоречат друг другу, или null"
}
Требования, от которых зависит польза ответа:
1. НЕ ДОГАДЫВАЙСЯ. Если поля нет в источниках — ставь null. Пустое поле
безопасно, выдуманное — нет: эти данные идут в промпт, по которому
оцениваются операции, и ошибка в профиле искажает вывод по всем операциям
организации.
2. Формулировку ОКВЭД приводи ДОСЛОВНО из классификатора, не по смыслу.
Например, код 80.10 звучит как «Деятельность охранных служб, в том числе
частных» — а не «Деятельность частных охранных служб».
3. Каждый объект — минимум ДВА независимых источника. Где источники
расходятся (статус, адрес, руководитель), не выбирай удобный: заполни
поле "conflicts" и опиши расхождение с обеими версиями.
4. Различай однофамильцев. В РФ десятки ЧОО с названиями «Кобра», «Орлан»,
«Гром». Ключ — ИНН и только ИНН; совпадение по названию не принимается.
5. Отдельно отмечай банкротство, ликвидацию и дисквалификацию руководителя:
для оценки операций это важнее ОКВЭДа.
Список ИНН:
<сюда список из шага 1>
Шаг 3. Перенести ответ в конфиги.
До модели-судьи доходят только поля activity, okved_primary, region и
source — блок <организация> промпта, см.
prompt.py:168-175; собирает их
org_view в context.py. Остальное в YAML
читает человек. Поэтому:
okved_primary — код и дословная формулировка;activity — вид деятельности плюс то, что меняет оценку операций:
банкротство, ликвидация, профильные неохранные виды деятельности
(у «ЦСМ ГРОМ» это электромонтаж — из-за него платежи за монтаж и материалы
профильны, а не посторонни);region — регион и город;source — откуда данные и на какую дату; спорный статус пишется прямо
здесь, чтобы судья видел неопределённость, а не мнимую точность;status — состояние юрлица (действующая, ликвидирована, конкурсное
производство) с датой; факт, а не повод для вывода;founders, director, management_company — формальные сведения из
реестра; ИНН физлица указывается, если раскрыт. Незакрытое пишется явным
unknown: «не найдено» ≠ «нет»;address, registration_date — косвенные признаки, сами по себе ничего не
доказывающие;Чего в activity быть не должно. Пересказа выписок («по выпискам —
охранные услуги»), ранжирования («крупнейшая по обороту») и указаний, что
считать подозрительным. Это выводы, которые судья обязан сделать сам; поле не
проверяется кодом никак, и что в него вписано — то модель и прочтёт.
Связанность организаций не устанавливается автоматически и списка аффилированных ИНН больше нет. Общие учредители, директора и адреса заносятся в профили как факты, а вывод о группе делает судья и читатель отчёта. Взаимные платежи связанности не доказывают — именно на них держался удалённый список.
Шаг 4. Проверить, что конфиг подхватился, и перемерить.
billeval check-config # должно показать 6 профилей
billeval eval # полнота suspicious не должна упасть
Если после сверки полнота падает — смотреть не промпт, а профиль: скорее всего в него попало утверждение, которым модель теперь оправдывает операции.
.venv/bin/python -m pytest -q
Тесты судьи работают на MockBackend и в сеть не ходят: обращение к живой
модели даёт разброс до 13 % между прогонами и в тестах бесполезно.
Построчная форензик-обработка банковских выписок организаций: разбор файлов разного формата в единую таблицу операций, счётные агрегаты по счёту и построчная оценка операций языковой моделью с указанием кода схемы.
Проект описан в двух документах, и они — источник истины, а не этот файл:
| документ | о чём |
|---|---|
| docs/SPEC.md | что система делает, промпт судьи, инварианты |
| docs/PLAN.md | этапы, критерии готовности, что уже сделано |
| docs/DOMAIN_TYPOLOGY.md | схемы C1–C9, за которыми следит судья |
| docs/OPEN_ITEMS.md | открытые вопросы, требующие решения человека |
| docs/EVAL.md | как измеряется качество судьи |
Пакеты ставятся только в локальный .venv и только через uv pip —
глобальный pip в этом проекте не используется.
uv venv
uv pip install -e .
Модель не устанавливается локально: судья обращается к vLLM-серверу по URL
(config/config.yaml, секция judge). Пакет vllm ставить не нужно.
billeval scan --detect # найти файлы и определить формат
billeval parse # разобрать в таблицу операций
billeval aggregate # счётные агрегаты по счёту
billeval prefetch-orgs # справки об организациях (Э6, необязательно)
billeval judge # построчная оценка моделью
billeval report # отчёт
prefetch-orgs в цепочке необязателен: судья штатно работает в режиме «профиль
неизвестен» (§8.8 SPEC). Но профиль держит полноту вердиктов — см. следующий
раздел.
Полезное по ходу:
billeval check-config # что загружено из config/, оба контура моделей
billeval stats # что в базе
billeval judge --dry-run # сколько уйдёт запросов, без обращения к модели
billeval eval # метрики судьи на эталоне из БД
billeval arbitrate --dry-run # файлы, где формат не определился правилом (§5.1)
billeval fix-rows --dry-run # строки, отсеянные как «ни даты, ни суммы» (§5.6)
Судья и Э6 ходят в разные эндпоинты, и путать их не следует:
судья (judge, eval) | Э6 (prefetch-orgs, arbitrate, fix-rows) | |
|---|---|---|
| эндпоинт | локальный vLLM, VLLM_* | DeepSeek по умолчанию, DEEPSEEK_* |
| запросов на прогон | ≈27 000 | единицы-десятки |
| зачем | следование инструкции, воспроизводимость | знание о мире (ЕГРЮЛ, ОКВЭД) |
Метрики качества судьи (§8.12 SPEC) измерены на Qwen — смена модели судьи
обнуляет эталон, поэтому ENRICH_PROVIDER переключает только Э6. Все адреса и
ключи живут в .env, в config/config.yaml стоят подстановки ${…}.
billeval check-config печатает оба контура рядом.
config/org_profiles.yaml — не украшение отчёта. Заполненность этого файла
напрямую держит полноту вердиктов suspicious: при пустом профиле включается
§8.8 SPEC, уверенность жёстко ограничивается 0.6, и спорная операция уходит с
suspicious на attention. Замер 2026-07-30 на эталоне: 0.38 без профилей
против 0.62–0.75 с профилями при одном и том же промпте.
Обратная сторона — ошибочный профиль хуже пустого. Пустой заставляет судью сомневаться; неверный заставляет уверенно судить по чужой мерке. Единственный пропуск в замере появился именно с профилем: зная, что организация — ЧОО, модель объяснила 200 000 ₽ на личный счёт физлица «штатной процедурой». Поэтому профили сверяются, а спорное помечается как спорное, а не сглаживается.
Текущее состояние: шесть организаций сверены по агрегаторам ЕГРЮЛ
(saby.ru, spark-interfax, РБК, list-org, audit-it) 2026-07-30 и 2026-08-01.
История сверки, отклонённые сведения и разбор по каждой организации —
docs/changes/2026-08-01-org-profiles-verification.md; сам конфиг содержит
только данные. Главное незакрытое: состав участников ОРЛАНа (ответы
источников противоречат) и ГРОМ-ТОЛЬЯТТИ (скрыт) — у обоих стоит явный
unknown. Связей между организациями набора по формальным основаниям ЕГРЮЛ
не установлено, и автоматически связанность не устанавливается (§8а.2).
billeval prefetch-orgs (Э6)Стадия Э6 делает то же самое командой. Провайдер по умолчанию — DeepSeek,
ключ и адрес берутся из .env (DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL):
billeval prefetch-orgs --dry-run # о ком будут справки, без обращения к сети
billeval prefetch-orgs # запросить и записать в org_profile
billeval prefetch-orgs --provider perplexity # переключить провайдера
billeval orgs --profiles # что получено и что дойдёт до судьи
Что стадия гарантирует, и почему это важнее удобства:
Null, судья
работает по §8.8, billeval check-config называет недостающую переменную;После первого прогона профили стоит перечитать глазами. Сейчас в
config/org_profiles.yaml руками сверено пять организаций, и там есть то, чего
в автоматической справке может не оказаться: конкурсное производство «КОБРЫ» и
помеченный спорным статус «ЛЕКС». Для оценки операций эти обстоятельства важнее
ОКВЭДа. Если сетевая справка их потеряла — --on-conflict=keep возвращает
ручную версию в силу, ничего не удаляя.
Процедура ниже остаётся полезной, когда нужен не автоматический профиль, а проверяемые поля со ссылками и отдельный перечень расхождений между источниками — то есть когда сверку принимает человек.
Шаг 1. Собрать список ИНН из базы.
sqlite3 billeval.db \
"SELECT inn, name_normalized FROM organization WHERE inn IS NOT NULL AND inn <> '';"
Шаг 2. Отдать модели этот промпт, подставив список ИНН.
Ты сверяешь данные юридических лиц по открытым источникам РФ.
Для каждого ИНН из списка найди в открытых источниках (ЕГРЮЛ и агрегаторы:
rusprofile, list-org, checko, saby, spark-interfax, РБК Компании) и верни
JSON-массив объектов строго такой формы:
{
"inn": "...",
"name_full": "полное наименование как в реестре",
"ogrn": "...",
"kpp": "...",
"registered": "ГГГГ-ММ-ДД",
"address": "юридический адрес",
"director": "ФИО и должность с датой назначения",
"capital": "уставный капитал",
"okved_primary": "код и ТОЧНАЯ формулировка из классификатора",
"okved_extra": ["код — формулировка", "..."],
"licenses": "лицензии (Росгвардия, МЧС и пр.) или null",
"status": "действующая | ликвидирована | в ликвидации | банкротство",
"status_date": "ГГГГ-ММ-ДД или null",
"sources": ["URL", "URL"],
"conflicts": "чем именно источники противоречат друг другу, или null"
}
Требования, от которых зависит польза ответа:
1. НЕ ДОГАДЫВАЙСЯ. Если поля нет в источниках — ставь null. Пустое поле
безопасно, выдуманное — нет: эти данные идут в промпт, по которому
оцениваются операции, и ошибка в профиле искажает вывод по всем операциям
организации.
2. Формулировку ОКВЭД приводи ДОСЛОВНО из классификатора, не по смыслу.
Например, код 80.10 звучит как «Деятельность охранных служб, в том числе
частных» — а не «Деятельность частных охранных служб».
3. Каждый объект — минимум ДВА независимых источника. Где источники
расходятся (статус, адрес, руководитель), не выбирай удобный: заполни
поле "conflicts" и опиши расхождение с обеими версиями.
4. Различай однофамильцев. В РФ десятки ЧОО с названиями «Кобра», «Орлан»,
«Гром». Ключ — ИНН и только ИНН; совпадение по названию не принимается.
5. Отдельно отмечай банкротство, ликвидацию и дисквалификацию руководителя:
для оценки операций это важнее ОКВЭДа.
Список ИНН:
<сюда список из шага 1>
Шаг 3. Перенести ответ в конфиги.
До модели-судьи доходят только поля activity, okved_primary, region и
source — блок <организация> промпта, см.
prompt.py:168-175; собирает их
org_view в context.py. Остальное в YAML
читает человек. Поэтому:
okved_primary — код и дословная формулировка;activity — вид деятельности плюс то, что меняет оценку операций:
банкротство, ликвидация, профильные неохранные виды деятельности
(у «ЦСМ ГРОМ» это электромонтаж — из-за него платежи за монтаж и материалы
профильны, а не посторонни);region — регион и город;source — откуда данные и на какую дату; спорный статус пишется прямо
здесь, чтобы судья видел неопределённость, а не мнимую точность;status — состояние юрлица (действующая, ликвидирована, конкурсное
производство) с датой; факт, а не повод для вывода;founders, director, management_company — формальные сведения из
реестра; ИНН физлица указывается, если раскрыт. Незакрытое пишется явным
unknown: «не найдено» ≠ «нет»;address, registration_date — косвенные признаки, сами по себе ничего не
доказывающие;Чего в activity быть не должно. Пересказа выписок («по выпискам —
охранные услуги»), ранжирования («крупнейшая по обороту») и указаний, что
считать подозрительным. Это выводы, которые судья обязан сделать сам; поле не
проверяется кодом никак, и что в него вписано — то модель и прочтёт.
Связанность организаций не устанавливается автоматически и списка аффилированных ИНН больше нет. Общие учредители, директора и адреса заносятся в профили как факты, а вывод о группе делает судья и читатель отчёта. Взаимные платежи связанности не доказывают — именно на них держался удалённый список.
Шаг 4. Проверить, что конфиг подхватился, и перемерить.
billeval check-config # должно показать 6 профилей
billeval eval # полнота suspicious не должна упасть
Если после сверки полнота падает — смотреть не промпт, а профиль: скорее всего в него попало утверждение, которым модель теперь оправдывает операции.
.venv/bin/python -m pytest -q
Тесты судьи работают на MockBackend и в сеть не ходят: обращение к живой
модели даёт разброс до 13 % между прогонами и в тестах бесполезно.