Как мы построили агента, который подаёт твою декларацию по НДС
Autofiling полностью готовит твою UStVA и отправляет её в налоговую. Сложность была не в модели – а в том, чтобы сделать стохастического агента достаточно безопасным для подачи настоящей налоговой декларации. Вот как мы это построили.
- Категория
- Общее
- Обновлено
- Автор
- Stan Kharlap
Каждый квартал – или каждый месяц, если ты крупнее – немецкий бизнес обязан отправлять предварительную декларацию по НДС (Umsatzsteuervoranmeldung) через ELSTER в Finanzamt. У неё жёсткий срок подачи, она не прощает ошибок в арифметике, а промах стоит реальных денег. Большинство бухгалтерских программ помогают тебе заполнить формуляр. Мы хотели, чтобы софт просто делал декларацию – собирал цифры, проверял их и подавал – а человек сохранял полный контроль над тем единственным моментом, который действительно важен.
Эта функция называется 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 рендерится в другой хеш, сохранённое подтверждение становится недействительным, и прогон отказывается продолжать на основе устаревшего согласия. Ты никогда не подтверждаешь «план агента». Ты подтверждаешь ровно эти цифры – и система понимает это буквально.
Отправка, которая не может подать дважды
Эта часть нравится мне больше всего, потому что она скучна ровно так, как надо. Шаг отправки сознательно не упакован в одну большую транзакцию. Вот настоящий докстринг из раннера:
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: к какому workflow она относилась, какой провайдер и какая модель работали, хеш входа и выхода, стоимость, задержка и точная PromptVersion, которая её породила. Выбор модели для каждого workflow – сам по себе политика: ModelPolicy с потолками по стоимости, задержке и уровню риска плюс запасная модель – а не константа, зарытая в код.
Эта бухгалтерия – не церемония. Именно она позволяет нам месяцы спустя ответить на вопрос «почему этот прогон выдал именно эту цифру?» настоящим ответом, а не пожатием плеч. Она превращает стоимость и задержку в дашборд, а не в сюрприз. И поскольку каждый промпт версионирован, а каждое реальное исправление можно заморозить в кейс для оценки, это фундамент, который позволяет агенту измеримо становиться лучше со временем – причём что выкатывать, решает человек, а не модель.
Скучные части – это и есть продукт
Было бы легко построить более зрелищную версию: окно чата, где ты просишь агента «подать мой НДС», и он подаёт. И это было бы неправильно, потому что в тот момент, когда декларация окажется ошибочной, магия демо становится чьей-то проблемой с Finanzamt.
Поэтому дизайн наклонён в другую сторону. Агент делает муторный, чреватый ошибками сбор и сверку, которые ни один человек не любит. Он отказывается угадывать там, где должен спросить. Он показывает тебе точный payload и делает тебя тем, кто его подписывает. И он относится к самому акту отправки с паранойей, которую заслуживает юридическая декларация. Такую форму имеет каждый агент, которого мы строим в Norman: полностью автоматизировать скучную работу, оставить человеку решение с риском и уметь доказать каждый шаг между ними.
Norman берет операционную финансовую работу на себя
От invoicing до bookkeeping: Norman организует повторяющиеся финансовые процессы так, чтобы вы успевали к дедлайнам с меньшим объемом ручной работы.