Комплаенс
на сайте
Полный процесс работы с юридическими документами через агентов — от задачи привести продукт в соответствие закону до фиксации согласий в базе, которую можно предъявить на проверке.
Для правовой части важны три факта об этом сервисе. У пользователей есть личные кабинеты. Кроме них на площадке работают партнёры-организации — по закону каждая самостоятельный оператор персональных данных. И онлайн-оплаты нет: расчёты идут мимо площадки.
Делал я это с ИИ, и работа естественно разбилась на две части. Первая — собрать и проверить саму базу документов: тексты, нормы, непротиворечивость. Вторая, как логичное продолжение, — внедрить её в продукт, потому что готовый и выверенный документ без записи о согласии остаётся просто бумагой.
Разведка раньше текстов
Не бросаться писать с первого сообщения. Сначала функциональный бриф по живому коду: кто участники и какой у каждого статус, что с деньгами, какие данные собираются и куда уходят, что из согласий уже реализовано. У меня собственная разработка под мои проекты — модуль строит графы всех зависимостей проекта и его функций, даже там, где абсолютно разный язык кода. Перед стартом агенты с помощью графа за пару минут собрали полный функционал сайта, ориентированный под задачу.
У площадки, например, нет платёжного шлюза — один этот факт снимает целый пласт требований, который ИИ навесил бы на «сервис с оплатой» по умолчанию. Бриф стал фундаментом: документы строились от единой картины бизнеса, а не от семи независимых догадок о том, что это за сервис.
Параллельная сборка и сведение
Бриф ушел в Cowork внутри приложения Claude на компьютере, там заведен специально проект для решения юридических вопросов с чёткими инструкциями. Документы я передал основному агенту, который дальше раскидал их по субагентам: семь на черновики, пять на правки. Быстро, но с проблемами — агенты писали независимо, не видя соседние тексты, и статус площадки, термины, роль партнёров начали расходиться от документа к документу: где-то «сервис», где-то «платформа», одна роль описана по-разному. По отдельности каждый текст складный, вместе пакет противоречит сам себе.
Поэтому после параллельной сборки обязателен сводный проход на непротиворечивость — это часть работы, а не опция.
Проверка: норма только из первоисточника
Ни одной правовой нормы из головы. Каждую статью — через поиск по первоисточнику, в текст идёт ссылка. База знаний модели обрывается во времени, а тон у неё одинаковый и когда она права, и когда цитирует прошлогоднюю редакцию. Одна из юридических баз отдала нужную статью в битой кодировке — чистый текст не вытащил, разбирал структуру и добивал формулировки из других источников.
Сам пакет я проверял программно — грепом по маркерам (статус оператора, старые формулировки, незакрытые заглушки), а не вычитывая тысячу семьсот строк подряд. То есть такой объём я не даю читать агенту для валидации: он начнёт плыть и путаться даже с самыми точными инструкциями, поэтому сначала мы с ним собираем маркеры проверки, и он делает это программно. Это честная граница: греп находит только то, что догадался искать, и не заметит юридически тонкую неточность. Поэтому финальная человеческая вычитка не убирается — ИИ готовит и проверяет, но подпись ставит человек.
Чему верить из выданного
Отдельный шаг «что принять, а что перепроверить» нужен потому, что на доклады модели о себе нельзя опираться без сверки.
Первое: она не отделяет «я проверила» от «мне дали на вход». В бриф попало утверждение, требовавшее подтверждения, — модель вписала его в документ как факт, «подтвердить» сама не пометила. Лечится только тем, что заранее просишь помечать допущения отдельным списком.
Второе: она переоценивает сделанное. На переводе пакета агенты отрапортовали готовый результат, который не совпал с файлами — число разделов в отчёте расходилось с реальностью. Сверять пришлось по самим файлам, а не по их «готово».
Отсюда разделение ролей: позицию (что публикуем, объём, спорные решения) держит человек, сбор и проверку по источникам берёт модель, а её результат сверяется по факту, не по отчёту.
Что отдать своему агенту
Правила, которые снимают часть ошибок на этом этапе. Дайте агенту в начале:
Работаем над юридическими документами. Правила:
1. Ни одной нормы по памяти. Каждую статью проверяй по первоисточнику и давай
ссылку. Не уверен в редакции, пиши "нужно подтвердить", не выдавай за факт.
2. Непроверенное из входных данных помечай как допущение, отдельным списком:
"это принято на веру, подтвердить".
3. Отчитываясь о сделанном, не пиши "готово" по памяти. Приводи проверяемый
результат (что и где сделано), чтобы можно было сверить по факту.
4. Если документов несколько, держи единые термины, статус и роли во всех.
В конце сведи их между собой на непротиворечивость.
Длинные тире в промпте заменены запятыми и двоеточиями — так удобнее в рабочем файле. На текст статьи это не распространяется.
Документы собраны и выверены — но это пока тексты. Когда я сверял их с продуктом, выяснилось главное, и оно не про формулировки: согласия пользователей нигде не фиксировались. Человек ставил галочку при регистрации, но записи не оставалось — ни какой текст принял, ни когда, ни с какого устройства. Доказать при проверке, что согласие было, нечем. Вот здесь документ и превращается из бумаги в рабочий: за текстами должна стоять запись.
Повторная сверка — уже внутри проекта
Эту сверку я вёл не на глаз. После сбора документов в Cowork весь пакет ушёл на повторную валидацию — уже к Claude Code, который работает прямо внутри проекта. Бриф с собранным функционалом, тот же, что уходил в Cowork, у него уже был на руках. Задача — сверить документы и прописанные в них юридические нюансы с фактическим функционалом.
Дальше он исследовал готовность кодовой базы и объём необходимых правок, составил план, согласовал его со мной и учёл новые вводные. И только после этого началась собственно фиксация — журнал согласий.
Журнал согласий
Одна таблица, куда пишется неизменяемая запись на каждое согласие:
CREATE TABLE user_consents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
consent_type TEXT NOT NULL CHECK (consent_type IN
('offer_policy','personal_data','marketing')),
text_version TEXT NOT NULL,
accepted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
accepted_ip TEXT,
accepted_ua TEXT,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Главный инвариант — revoked_at. Отзыв согласия не удаляет строку, а проставляет дату отзыва; исходная запись остаётся навсегда. Нужно уметь показать не только что согласие есть сейчас, но и что оно было в прошлом и когда отозвано. Удалить строку — потерять доказательство.
text_version — вторая опора. В неё пишется версия принятого текста, привязанная к дате редакции документов. Поправил формулировку оферты — версия сменилась, и по каждой записи видно, какую именно редакцию человек принял. Без этого «он соглашался» повисает: соглашался с чем?
Миграцию накатываешь под пишущей ролью приложения, не под суперпользователем:
SET ROLE app_rw; -- пишущая роль приложения
CREATE TABLE ... ;
RESET ROLE;
Две галочки, которые нельзя слить
На форме регистрации — две обязательные галочки, раздельно:
offerAccepted: z.literal(true, 'Необходимо принять оферту и политику'),
consentGiven: z.literal(true, 'Необходимо согласие на обработку ПД'),
marketingConsent: z.boolean().optional(), // по умолчанию выкл
z.literal(true) — не «галочка по желанию», а жёсткое условие: без неё форма не проходит валидацию вообще. Принятие оферты и согласие на обработку персональных данных — разные галочки специально: закон требует, чтобы согласие на обработку данных было самостоятельным актом, его нельзя зашить в «я принимаю условия» (статья 9 152-ФЗ). Маркетинг — третья, опциональная, по умолчанию снятая. Заодно закрыли прореху: партнёры-организации раньше регистрировались вообще без согласия на обработку данных.
Запись, которая не валит регистрацию
Факт согласия пишется после создания аккаунта. Тонкость: сбой записи в журнал не должен ронять саму регистрацию — но и потеряться молча не должен.
const results = await Promise.allSettled(
types.map((t) => recordConsent({ accountId, consentType: t, textVersion, ip, ua }))
);
results.forEach((r, i) => {
if (r.status === 'rejected') {
console.error(`consent ${types[i]} failed for ${accountId}`, r.reason);
captureException(r.reason, { tags: { area: 'consent-audit' } }); // в Sentry
}
});
allSettled, а не Promise.all: упавшая запись одного согласия не отменяет остальные и не бросает наверх. Человек регистрируется в любом случае, но каждая несостоявшаяся запись летит в лог и в Sentry — потому что пропажа доказательной записи это то, о чём надо узнать сразу, а не на проверке. Сам recordConsent всегда делает INSERT новой строки, не обновляет старую: повторный accept или новая версия текста — новая запись, история накапливается.
Бэкфилл старым пользователям
У всех, кто зарегистрировался до раздела /legal, доказательной записи нет вообще, а это самая массовая часть базы. Их закрывает миграция, которая реконструирует запись из факта регистрации:
INSERT INTO user_consents (account_id, consent_type, text_version, accepted_at, accepted_ip, accepted_ua)
SELECT a.id, ct.consent_type, 'legacy-backfill', a.created_at, NULL, NULL
FROM accounts a
CROSS JOIN (VALUES ('offer_policy'), ('personal_data')) AS ct(consent_type)
WHERE a.deleted_at IS NULL
AND <у аккаунта есть реальный профиль>
AND NOT EXISTS (
SELECT 1 FROM user_consents uc
WHERE uc.account_id = a.id AND uc.consent_type = ct.consent_type
);
Несколько решений в этих строках:
accepted_at = a.created_at— дата согласия равна дате регистрации, когда оно фактически давалось. Не «сегодня», а честная историческая дата.accepted_ip / accepted_ua = NULL— их в старом флоу не сохраняли, подставлять выдуманные нельзя.text_version = 'legacy-backfill'— маркер: запись реконструирована, а не получена свежим явным кликом. Видно, где живое согласие, а где восстановленное.CROSS JOIN (VALUES ...)— на каждый аккаунт сразу две строки (оферта и ПД) без дублирования запроса.NOT EXISTS— идемпотентность: миграцию можно гонять повторно, второй раз ничего не задвоит.deleted_at IS NULLи фильтр на реальный профиль — удалённые и пустые аккаунты пропускаются. Маркетинг бэкфиллится отдельно и только тем, у кого стоял флаг согласия на рассылку.
Текст — из кода наружу
Сами формулировки документов вынесены из кода в отдельные файлы и рендерятся через markdown с санитайзером. Меняется формулировка — правится один файл, версия согласия сдвигается, формы регистрации не трогаются. Юрист правит текст, разработчик не лезет в разметку.
Что в итоге
Первая часть дала выверенную базу документов: тексты собраны и сведены, нормы проверены по источникам, спорные решения остались за мной. Вторая превратила их из бумаги в работающие: согласие фиксируется неизменяемой записью с версией и временем, отзыв сохраняет историю, старые пользователи закрыты бэкфиллом. Документ теперь подкреплён записью, которую можно предъявить.
ИИ собрал тексты почти сам. Но проверка по источникам, сверка его докладов по факту и инженерия фиксации — работа в паре с человеком, который держит позицию и ставит подпись.
// Обсуждение
Можно писать анонимно. Укажите email, чтобы получать уведомления об ответах.