Как мы собрали агента, который сам подаёт твою декларацию по НДС
Autofiling полностью готовит и подаёт твою немецкую UStVA, от начала до конца. Сложным было никогда не само модель, а то, как сделать стохастического агента достаточно безопасным, чтобы он совершал юридически значимую подачу. Вот как устроен дизайн.
- Категория
- Общее
- Обновлено
- Автор
- Stan Kharlap
Каждый квартал, а если ты крупнее, то каждый месяц, немецкий бизнес обязан отправить свою декларацию по НДС, Umsatzsteuervoranmeldung, в налоговую через ELSTER. У неё фиксированный срок, она не прощает арифметических ошибок, и ошибка стоит реальных денег. Большинство бухгалтерских программ помогают тебе заполнить форму. Мы хотели, чтобы софт просто делал декларацию: собирал цифры, проверял их и подавал, пока человек прочно держит под контролем тот единственный момент, который важен.
Эта функция называется Autofiling, и самое интересное в её создании почти не имело отношения к языковой модели. Модель, которая умеет читать чеки и складывать НДС, это базовая планка. Инженерная задача была противоположностью демо: как позволить стохастическому агенту совершить необратимый юридический акт от чужого имени и при этом иметь возможность потом доказать, что он сделал ровно то, что нужно?
Подача это юридический акт, а не чат
Первый рефлекс при любой агентной фиче: подключить модель к паре инструментов и отпустить её. Для чат-бота нормально. Для налоговой декларации такой дизайн это ответственность:
- Подача не идемпотентна. Отправить UStVA в ELSTER дважды это не дублирующая строка, которую можно потом подчистить, это две декларации. Сбой в неудачную миллисекунду не должен приводить к повторной подаче.
- «Скорее всего верно» это не планка. Результат должен быть проверяемым и точным. Никто не подписывает цифру, которую не может увидеть.
- Это привязано к сроку. Агент не может тихо зависнуть. Если чего-то не хватает, это должно всплыть как конкретная, решаемая задача: сейчас, а не в день сдачи.
Поэтому Autofiling это не промпт с привешенными инструментами. Это конечный автомат, внутри которого работает агент, где каждый переход, затрагивающий внешний мир, спроектирован из допущения, что процесс может упасть в любой точке.
Пайплайн, а не чёрный ящик
Прогон Autofiling проходит через явные, сохранённые состояния: SCHEDULED → COLLECTING → RECONCILING → READY_FOR_APPROVAL → APPROVED → SUBMITTING → FILED, плюс два аварийных выхода, NEEDS_INPUT и FAILED, на случай, когда реальность не идёт навстречу. Состояние лежит в базе данных, а не в голове агента, поэтому прогон всегда можно возобновить и всегда можно проаудировать.
Готовность, а не «на глаз»
Прежде чем декларация станет достойна превью, прогон входит в RECONCILING, и именно здесь агент отрабатывает свой хлеб, будучи хорошим пессимистом. Вместо того чтобы самоуверенно подать то, что у него есть, он ищет причины, по которым подача может оказаться неверной, и превращает каждую в типизированный блокер.
Блокеры это не предупреждения свободным текстом. Это замкнутый набор машиночитаемых типов: uncategorized_transactions, unverified_transactions, possible_duplicates, unclaimed_input_vat, vat_conflict, expired_bank_sync, missing_tax_settings, elster_validation_error и ещё с десяток, каждый со степенью серьёзности info, warning или blocking. Блокер blocking останавливает прогон и толкает его в NEEDS_INPUT с конкретной, решаемой задачей («14 транзакций в этом периоде без категории») вместо расплывчатого «что-то не так». Информационные блокеры едут вместе с превью, так что человек их видит, но его не останавливают.
Смысл в том, что агента поощряют за то, что он выносит проблемы на поверхность, а не прячет их за самоуверенным ответом. Декларация, которую пока не следует подавать, никогда не доходит до точки, где это стало бы возможно.
Ты подтверждаешь payload, а не обещание
Когда прогон чист, он рендерит превью: точную ELSTER-payload, которая была бы передана, отрисованную через тот же движок отчётов, что делает настоящую подачу, просто в тестовом режиме. Затем эта payload хешируется, и хеш сохраняется на прогоне как preview_payload_hash.
Подтверждение привязывается к этому хешу. Когда человек подтверждает, мы записываем approved_payload_hash вместе с тем, кто подтвердил и когда. С этого момента оба хеша должны совпадать. Если что-то выше по потоку меняется: приходит поздний чек, исправляется категория, сдвигаются настройки НДС, payload перерендеривается в другой хеш, сохранённое подтверждение аннулируется, и прогон отказывается идти дальше на устаревшем согласии. Ты никогда не подтверждаешь «план агента». Ты подтверждаешь именно этот набор цифр, и система обеспечивает это буквально.
Подача, которая не может отправить дважды
Эта часть нравится мне больше всего, потому что она скучна ровно правильным образом. Шаг подачи намеренно не обёрнут в одну большую транзакцию. Вот настоящий docstring из раннера:
def submit_approved_run(run: AutoFilingRun) -> AutoFilingRun:
"""Submit an approved run to ELSTER.
Deliberately NOT one big transaction: the ELSTER POST is a non-idempotent
external effect. SUBMITTING is committed *before* the external call, and
the result is committed in its own transaction afterwards — so a crash
can never roll the DB back to a state that would re-file the same UStVA.
"""
Ход, по порядку:
- В транзакции под блокировкой строки перепроверяется всё: статус, что подтверждённый хеш всё ещё совпадает с текущей payload, что аккаунт всё ещё имеет право подавать, затем прогон переводится в
SUBMITTINGи коммитится. - Только после этого делается внешний вызов ELSTER, вне какой-либо транзакции.
- Итог, поданная декларация или конкретная ошибка, коммитится в собственной транзакции.
Поскольку SUBMITTING устойчиво до сетевого вызова, процесс, умерший в середине подачи, просыпается в состоянии, которое говорит «это уже было в полёте», и путь восстановления проверяет, принял ли ELSTER отчёт, прежде чем вообще отправить снова. Временный сбой провайдера откатывает прогон обратно в APPROVED, чтобы свипер мог его повторить, до ограниченного числа попыток, ни разу не потеряв подтверждение. Единственное, чего не может случиться никогда, это подать одну и ту же декларацию дважды. Всё остальное восстановимо.
Каждый шаг задокументирован
Под Autofiling лежит наш агентный слой, и вся его работа в том, чтобы сделать агента читаемым. Каждая единица работы модели: чтение документа, набросок исправления, подготовка декларации, записывается как AgentRun: к какому воркфлоу она относилась, какой провайдер и модель отработали, хеш входа и выхода, стоимость, задержка и точная PromptVersion, которая её произвела. Выбор модели на воркфлоу это сам по себе политика: ModelPolicy с потолками по стоимости, задержке и уровню риска плюс запасная модель, а не константа, закопанная в коде.
Эта бухгалтерия не церемония. Именно она позволяет нам месяцы спустя ответить на вопрос «почему этот прогон выдал именно эту цифру?» реальным ответом, а не пожатием плеч. Именно она превращает стоимость и задержку в дашборд, а не в сюрприз. И поскольку каждый промпт версионируется, а каждое реальное исправление можно заморозить в оценочный кейс, это и есть основа, которая позволяет агенту измеримо улучшаться со временем, где решает, что выкатывать, человек, а никогда не модель.
Скучные части и есть продукт
Было бы легко собрать более броскую версию этого: чат-окно, где ты просишь агента «подай мою декларацию по НДС», и он это делает. Это было бы и неправильной вещью для выпуска, потому что в тот момент, когда декларация оказывается неверной, демо-магия становится чьей-то проблемой с налоговой.
Поэтому дизайн клонится в другую сторону. Агент делает нудную, чреватую ошибками работу по сбору и проверке, которую не любит ни один человек. Он отказывается угадывать, когда должен спросить. Он показывает тебе точную payload и делает подписантом тебя. И он относится к акту подачи с паранойей, которой заслуживает юридически значимая декларация. Такую форму имеет каждый агент, которого мы строим в Norman: полностью автоматизировать скучную работу, оставить человеку решение, которое несёт риск, и иметь возможность доказать каждый шаг между ними.
Norman берет операционную финансовую работу на себя
От invoicing до bookkeeping: Norman организует повторяющиеся финансовые процессы так, чтобы вы успевали к дедлайнам с меньшим объемом ручной работы.