Как построить автономного ИИ-финансового директора для малого бизнеса: архитектура на ADK, реальные грабли и 40 проверок без сбоев. Начните сейчас!
Представьте, что у вашего малого бизнеса есть финансовый директор уровня Fortune 500, который работает круглосуточно, не требует зарплаты и не спит. Звучит фантастически? Я построил такого агента, и в этой статье расскажу, как это сделать. Вы узнаете, как спроектировать автономную систему, которая сама проверяет отчётность, находит проблемы и доводит задачи до конца, останавливаясь только там, где нужно человеческое решение.
Это не очередной чат-бот с инструментами. Это полноценный финансовый отдел для бизнеса, который не может нанять команду. Владелец общается с ИИ-CFO, а за кулисами три специалиста — казначей, бухгалтер и аналитик — каждое утро проверяют книги, закрывают месяц и готовят управленческую отчётность. Владелец никогда не говорит со специалистами напрямую, и это ключевое архитектурное решение.
Главный принцип: автономия не означает безграничные полномочия. Система может сама проверять, рассчитывать, мониторить и рекомендовать, но когда действие меняет книги, она останавливается и ждёт явного одобрения человека. Этот раздел между работой, которую агент может делать, и решениями, которые бизнес должен принимать, стал центральным правилом проекта.
На момент написания статьи система провела 40 автоматических проверок за 8 дней подряд с нулём сбоев, работая с реальной песочницей QuickBooks.
Google Agent Development Kit предлагает три модели оркестрации, и самое интересное — решить, что куда поместить. Я перепробовал два варианта и чуть не попал в ловушку.
Первая версия позволяла каждому агенту вызывать инструменты для получения данных. Но промпт должен был просить данные и надеяться, что агент их получит. Если агент пропускал вызов инструмента, он проверял ничего и сообщал, что всё в порядке. Это ужасный сбой: его невозможно отличить от успеха.
Поэтому сбор данных стал обычными Python-функциями, встроенными в граф:
review_workflow = Workflow(
name="cfo_daily_review",
edges=[
(START, fetch_performance, gather),
(START, fetch_trend, gather),
(START, fetch_cash, gather),
(START, fetch_receivables, gather),
(START, fetch_ledger_quality, gather),
(START, fetch_open_findings, gather),
(gather, treasurer, judged),
(gather, accountant, judged),
(gather, analyst, judged),
(judged, store_findings),
],
)Шесть вызовов, ноль вызовов модели, и они выполняются, потому что подключены к START, а не потому что модель решила их вызвать. Специалисты начинают работу только после того, как факты собраны.
Обратите внимание на два объединения: gather собирает все данные, чтобы каждый специалист видел полную картину, а judged ждёт всех трёх специалистов, чтобы ничего не сохранять при частичном обзоре. Это урок, который я усвоил на горьком опыте, когда тайм-аут одного специалиста приводил к ложному закрытию его находок.
Ещё одна тихая победа: Runner(node=workflow) запускается с new_message=None. В системе нет фальшивого промпта "пожалуйста, запусти проверку". Граф просто работает.
Суб-агенты ADK по умолчанию используют режим chat. В этом режиме координатор получает только transfer_to_agent — последовательную передачу всего диалога одному специалисту, который никогда не возвращается. Подумайте, что это значит: владелец задаёт вопрос о деньгах, и казначей, которого он никогда не видел, отвечает и удерживает диалог.
Решение: режим single_turn. В этом режиме ADK даёт CFO один инструмент делегирования на каждого специалиста, запускает нужные подмножества параллельно, возвращает результаты, и CFO отвечает своим голосом. Специалисты никогда не обращаются к пользователю напрямую.
cfo = Agent(
name="cfo",
sub_agents=[chat_treasurer, chat_accountant, chat_analyst],
...
)
chat_treasurer = Agent(
name="treasurer",
mode="single_turn", # <- весь дизайн в одном флаге
tools=[get_cash_position, get_receivables, get_open_findings],
...
)Мне нравится, что продуктовое решение — владелец нанял CFO, а не комитет — обеспечивается фреймворком, а не вежливым промптом.
Закрытие месяца — главный рабочий процесс, и его ключевая особенность — остановка и ожидание человека, возможно, несколько дней. Многие системы имитируют это: флаг статуса, цикл опроса, задача, которая просыпается и спрашивает "уже одобрено?". ADK 2 предоставляет настоящий примитив:
@node(rerun_on_resume=True)
async def await_approval(ctx, node_input):
case = _load_case()
answer = ctx.resume_inputs.get(case.interrupt_id)
if answer is None:
# ... сохранить предложения, составить письмо об одобрении ...
yield RequestInput(
interrupt_id=case.interrupt_id,
message=f"Одобрить корректировки закрытия {case.period_label}?",
response_schema={"type": "string"},
)
return # выполнение действительно останавливается здесь
approved = str(answer).strip().lower() in ("approve", "approved", "yes")
yield Event(output={"approved": approved, "plan": node_input})Перед тем как строить что-то поверх, я провёл спайк как жёсткий тест в двух реально отдельных процессах:
процесс 1 [prepare] сканирование периода (дорогая работа)
[ask_approval] пауза на 'close-approval'
⏸ ПАУЗА
процесс 2 --- ВОЗОБНОВЛЕНИЕ (2 предыдущих события) ---
[ask_approval] возобновлён с ответом 'approve'
[act] РЕШЕНО -> 'approve'
✓ возобновлён из холодного процесса и завершёнВажная строка — та, которая не напечаталась. prepare не запускался повторно. Рабочий процесс возобновился на том узле, где остановился, зная только session id и interrupt id — ровно то положение, в котором окажется ссылка одобрения по электронной почте через два дня.
Два совета для тех, кто строит такое:
Во время съёмки демо я нажал кнопку "Не сейчас" на странице одобрения, ожидая вернуться позже. Закрытие продолжилось и пометило месяц закрытым. Ничего не дошло до QuickBooks — защита сработала. Но месяц был подписан, и аналитик подготовил управленческую отчётность для книг, которые никогда не были исправлены.
Причина: возобновление однонаправленно. Любой ответ перезапускает выполнение, и граф идёт до конца. "Отклонить" было не способом остаться на паузе, а способом выполнить весь рабочий процесс с установленным флагом.
Исправление — перестать относиться к этому как к решению:
// "Не сейчас" означает вернуться позже, поэтому НЕ возобновляем workflow
if (decision !== 'approve') {
location.href = signedIn ? '/inside' : '/login';
return;
}И эндпоинт теперь отказывается от любого другого ответа, так что ни один клиент не может подписать месяц без одобрения. Есть два варианта, а не три, потому что фреймворк поддерживает только два.
Я мог бы замокать бухгалтерскую систему, но не стал, и почти всё, что я узнал, пришло из-за того, что API не соглашался со мной.
В плане счетов "Sprinklers and Drip Systems" существует дважды: как доход и как расход. Сопоставление только по имени привело к тому, что строка счёта попала в счёт дохода, создав отрицательную выручку. Я этого не нашёл — агент нашёл в утренней проверке: "Отрицательная выручка −$288.00". Это было странное и довольно хорошее ощущение. Исправление — фильтр AccountType при каждой проводке затрат.
Язык запросов QuickBooks похож на SQL, но не является им. 'Amy''s' возвращает 400, а 'Amy\'s' — совпадение. Подвох в том, что неудачный поиск не вызывает исключение — поэтому поиск поставщика молча ничего не нашёл и создал дубликат поставщика.
Я написал client.post(f"/bill?operation=delete", ...). httpx заменяет строку запроса URL своими params, поэтому operation=delete был удалён, и каждое удаление стало молчаливым no-op с возвратом 200. Удаления теперь проверяют status == "Deleted" в ответе.
Если датировать сегодняшним днём, июльские корректировки ничего не исправляли в июле и молча добавляли $2,000 в август. Ни одну из этих ошибок нельзя было найти рассуждениями. Все они нашлись запуском системы на реальных данных каждый день.
Неудобный вопрос для любой агентной системы: как вы знаете, что она не выдумывает? Мой ответ — уменьшить задачу так, чтобы конфабуляции негде было жить.
Дашборд можно собрать за полдня. А запись, показывающая один и тот же счёт, наблюдаемый 43 раза за семь дней подряд, нельзя собрать задним числом.
Три раза, и все три были пойманы чтением её вывода, а не какой-то системой защиты:
Последний случай самый поучительный. Это не была ошибка модели. Это была моя ошибка.
Начните с малого: возьмите один бизнес-процесс, например, ежедневную проверку кассы, и реализуйте его как граф с жёстко заданным сбором данных. Убедитесь, что каждый числовой факт вычисляется кодом, а модель только интерпретирует. Затем добавьте человеческое одобрение через RequestInput, и только потом расширяйте до полного закрытия месяца. Помните: автономия — это не безграничные полномочия. Останавливайтесь перед изменениями книг, и вы получите систему, которой можно доверять.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →