Как стать автором
Обновить

Мне надоело заполнять Word формы и теперь это делает ИИ

Уровень сложностиСредний
Время на прочтение10 мин
Количество просмотров8.2K

Привет, Хабр! Сегодня расскажу про автоматизированную технологию заполнения Word форм используя ИИ

TLDR: Весь исходный код здесь

Введение

Я подаюсь на грант Студенческий стартап от Фонда Содействия Инновациям с другим своим ИИ проектом. Но мне так лень со скрупулёзностью сверять правильность заполнения каждого поля и придумывать формулировки к для меня очевидным полям. Поэтому я захотел поручить ИИ сделать это за меня, скормив ему файлы о моем проекте

В целом это общая проблема - заполнение типовых анкет, резюме, договоров, заявлений, отчетов, счетов на основе информации из других документов

На данный момент апреля 2025 года, ИИ сложно получается в прямом редактировании Word (хотя сейчас весь мир работает над возможностью ИИ взаимодействовать со средой - это и новый автономный ИИ агент Manus, и не очень применимый Operator от OpenAI, и попытки управлять через встроенные API приложений MCP протоколом). А пытаться генерировать word целиком через ChatGPT не очень хорошая идея - он все генерирует в формате markdown без сохранения исходной разметки и форматирования документа

Но у LLM хорошо получается выдавать текст в определенном формате. Поэтому задача сводится к тому, чтобы спросить ИИ по заполнению каждого поля и вставить результат в предопределенные местозаполнители в Word форме

Решение

Разобью задачу на несколько отдельных пунктов

  1. Просканировать исходную пустую форму и составить json файл характеристик (метаданных) каждого поля в ней

  2. Разметить форму местозаполнителями. Проверить на фейковых данных

  3. В цикле просить ИИ заполнить поле по данным характеристикам. Собрать результаты в словарь id_поля: ответ. Подставить результаты в форму по id_поля

Разберем все по этапам

Этап 1. Анализ формы

Прежде чем что-то заполнять, нужно проанализировать форму

Берем Word форму. Если нет, переконвертируем из pdf, например здесь. Смотрим. Обычно форма состоит из секций (раздел о себе, раздел бюджета, раздел календарного план), а они из полей. Для каждого поля нужно формировать уникальный id в рамках секции, описание, тип, формат, ограничения по заполнению

Структура любой формы состоит из секций и полей
Структура любой формы состоит из секций и полей

Одни и те же поля могут повторяться, то есть быть в виде списка. Например список “Список ссылок соц сетей” или “список целей”

Бывают комплексные поля. То есть те, которые состоят из нескольких. Например в некоторых полях ФИО заполняется не одной строкой, а 3 отдельными - фамилия, имя, отчество. Бывают таблицы. В своей сути, их можно представить просто как список комплексных полей

Короче любую форму можно описать с помощью следующих Pydantic классов (файл models.py):

from enum import Enum

from pydantic import BaseModel, Field


class FormFieldType(Enum):
    """Тип поля:
    select - выбрать один вариант из предложенных
    multiselect - выбрать несколько вариантов из предложенных
    text - текстовое поле
    number - числовое поле
    boolean - указать "Да" или "Нет"
    file - прикрепить файл
    link - прикрепить ссылку
    date - указать дату
    phone - указать номер телефона
    email - указать email
    complex - состоит из нескольких полей других типов
    """
    Select = "select"
    Multiselect = "multiselect"
    Text = "text"
    Number = "number"
    Boolean = "boolean"
    File = "file"
    Link = "link"
    Date = "date"
    Phone = "phone"
    Email = "email"
    Complex = "complex"


class FormField(BaseModel):
    """Поле в форме. Может содержать внутри себя другие поля"""
    id: str = Field(description="Цифровой номер (присвой текстовой id если нет номера)")
    description: str = Field(description="Описание и подсказки для заполнения поля")
    type: FormFieldType = Field(description="Тип поля")
    required: bool = Field(description="Поле обязательно для заполнения")

    options: list[str] | None = Field(description="Варианты выбора (для типа select)", default=None)
    maxLength: int | None = Field(description="Максимальная длина текста в символах (для типа text)", default=None)
    is_repeated: bool = Field(description="Поле повторяется несколько раз?", default=False)
    maxCount: int | None = Field(description="Максимальное количество", default=None)
    fileTypes: list[str] | None = Field(description="Разрешенные форматы файлов (для типа file)", default=None)
    dateFormat: str | None = Field(description="Формат возвращаемой даты (для типа date)", default=None)
    fields: list["FormField"] | None = Field(description="Внутренние поля (для типа complex)", default=None)


class FormSection(BaseModel):
    """Секция формы"""
    id: str = Field(description="Цифровой номер (или название) секции в документе")
    title: str = Field(description="Заголовок секции")
    fields: list[FormField] = Field(description="Заполняемые поля секции")

class FillForm(BaseModel):
    """Форма для заполнения"""
    title: str = Field(description="Заголовок формы")
    sections: list[FormSection] = Field(description="Секции, из которых состоит форма")

Для каждого Pydantic атрибута пишу поле description и составляю подробную документацию для нейросети. Да, кстати, анализировать форму и формировать json файл с полным описанием полей будет тоже нейросеть. Мы подадим эту Pydantic модель и используем structured_output мод, который гарантированно вернет нам в структурированном виде

Использую Claude Sonnet 3.7 для этой задачи. Во-первых она понимает значения по умолчанию default=… (которые open ai и gemini почему-то не принимают). Во-вторых у нее достаточно большой максимальное количество входных (200К) и выходных (64К) токенов

Можно попробовать gemini (она бесплатна в использовании!), но у нее ограничение на выходные токены в 8192 символов - для длинных форм не сработает

Все нейросетки использую с Proxy или поставщиками: GPTunnel.ru, Proxyapi.ru, OpenRouter.ai. Также использую Azure Document Intelligence - он позволяет извлекать содержимое .docx файла в обычную строку, которую можно подать на вход нейросети для анализа (инструкция как настроить). Кстати, Microsoft дает 500 страниц разбора текста бесплатно за месяц что для личных целей более чем достаточно

Вот схема пайплайна разбора заявки. Администратор - тот, кто готовит форму. Он запускает программу на форму, где из нее извлекается текст через Azure Document Intelligence, который затем подается ИИ Claude Sonnet 3.7. Он в свою очередь возвращает структурированный вывод, который мы можем сохранить в формате json. Также Администратор должен подготовить Шаблон (см. этап 2)

 Схема пайплайна разбора заявки
Схема пайплайна разбора заявки

Вот код. Использую библиотеку langchain для удобного использования AzureAIDocumentIntelligenceLoader и ChatAnthropic со структурированным выводом

from langchain_anthropic import ChatAnthropic
from langchain_community.document_loaders import AzureAIDocumentIntelligenceLoader
from langchain_core.prompts import PromptTemplate

from config import AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT, AZURE_DOCUMENT_INTELLIGENCE_KEY
from models import FillForm

form_analyzer_template = """
Есть следующая форма для заявки:
{doc}
---
Напиши json структуру полей этой формы. Постарайся максимально точно отразить заполняемые поля по данной структуре, удобной для последующего автоматического заполнения, при этом не вводя лишние поля.
Дели форму на секции и поля. Не обращай внимания на подпункты
В ответе перечисли только те поля структуры, которые не равны null или значению по-умолчанию
"""

form_analyzer_prompt = PromptTemplate(
    template=form_analyzer_template,
    input_varialbes=["doc"],
)

model = ChatAnthropic(model='claude-3-7-sonnet-latest', temperature=0, timeout=None, max_tokens=64000)


form_analyzer_chain = (form_analyzer_prompt | model.with_structured_output(FillForm)).with_config(
    run_name="Form Analyzer")


async def parse_document(doc: bytes) -> FillForm:
    loader = AzureAIDocumentIntelligenceLoader(
        api_endpoint=AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT,
        api_key=AZURE_DOCUMENT_INTELLIGENCE_KEY,
        bytes_source=doc,
        api_model="prebuilt-layout"
    )

    documents = await loader.aload()
    logger.logger("Azure Document Intelligence разобран")
    document_content = documents[0].page_content

    return await form_analyzer_chain.ainvoke({"doc": document_content})

Запускаю код на своей форме. Готово! Сохраненный json файл с метаданными полей представляет что-то подобное

Этап 2. Подготовка шаблона для заполнения

Дальше встает вопрос - как можно заполнить word документ через Python? Поможет библиотека docxtpl - смесь шаблонизатора Jinja2 и редактора Word файлов python-docx. Мы будем использовать сгенерированные нейросетью idшники в качестве тегов в исходной форме.

Следуя Jinja2-like синтаксису вручную проставляем каждый тег в документе. Имя тега формируем как field_{<id секции>_<id поля>. Для таблиц немного посложнее, но можно посмотреть мой пример в репозитории и примеры из документации.

Получаю список id-шников с помощью написанной утилитки

Размечаю Word файл

 Фрагмент размеченной Word формы. Заполняемые поля помечаю желтым цветом, чтобы различать, что было сгенерировано ИИ
Фрагмент размеченной Word формы. Заполняемые поля помечаю желтым цветом, чтобы различать, что было сгенерировано ИИ

Отлично! Теперь могу объявлять объект шаблона doc_template = DocxTemplate(template_path). У него есть удобный метод doc_template.get_undeclared_template_variables() , который позволяет получить все теги в шаблоне. Используя его написал утилиту проверки файла на совпадение с json метаданными. Запускаем ее

Исправляем неправильные теги. Запускаем проверку снова

Супер. Можно пробовать заполнить данными. Заполнение Word формы в docxtpl делается через метод doc_template.render(data), куда в качестве data нужно передать словарь “тэг”: значение. Затем заполненный уже документ можно сохранить как файл через doc_template.save(output_path)

Для заполнения размеченной формы фейковыми данными тоже есть скрипт

 Скрипт не очень оригинально но заполнил форму фейковыми данными. На этом этапе нужно просмотреть все поля - если какие-то из них пустые, значит где то еще есть ошибки
Скрипт не очень оригинально но заполнил форму фейковыми данными. На этом этапе нужно просмотреть все поля - если какие-то из них пустые, значит где то еще есть ошибки

Все подготовили, теперь переходим к самому интересному - заполнение с ИИ

Этап 3. Заполнение ИИ

Как я сказал в введении, имеет смысл заполнять поля спрашивая ChatGPT по одному. При этом важно это делать в одном чате, чтобы модель понимала контекст и заполняла форму взаимосвязано. Формировать промпт мы будем автоматически, исходя из метаданных полей.

Чтобы заполняемые данные были подкреплены реальными документами (описания, презентации, уставы, чеки) нам нужно также сделать RAG - Retrieval Augmented Generation, когда перед генерацией ИИ каждый раз на ему на вход подаются несколько актуальных к вопросу документов.

Все перечисленные идеи изображены на следующей схеме

Для реализации мы можем воспользоваться API от OpenAI. Помимо обычного Responses API, компания предоставляет также возможность создавать свои RAG через File Search API. По прайсу это совсем не дороже вызова, а если после заполнения чистить файлы, то выйдет еще дешевле. Дополнительные ссылки: поддерживаемые разрешения файлов и видео про OpenAI vector_store

 Алгоритм ИИ заполнения формы используя API OpenAI
Алгоритм ИИ заполнения формы используя API OpenAI

Давайте сделаем это. Устанавливаю Python клиент openai. Прописываю функции загрузки файлов и работы с vector_store. В sdk от openai есть удобный метод client.vector_stores.files.upload_and_poll, которая загружает файл и сразу прикрепляет его к векторному хранилищу. Воспользуемся ей, передав id ранее созданного vector_store

async def add_files(client: AsyncOpenAI, vector_store_id: str, files: list[str]) -> list[str]:
    errors = []
    files_ids = []
    for file in files:
        with open(file, "rb") as f:
            file_content = (file.split("/")[-1], f.read())
        try:
            file_response = await client.vector_stores.files.upload_and_poll(vector_store_id=vector_store_id, file=file_content)
        except Exception as e:
            errors.append((file, e))
        else:
            files_ids.append(file_response.id)
            if file_response.status == "failed":
                errors.append((file, file_response.last_error.message))
    if errors:
        raise IncorrectFilesException(files_ids, errors)
    return files_ids


async def delete_files(client: AsyncOpenAI, files_ids: list[str]) -> None:
    for file_id in files_ids:
        try:
            await client.files.delete(file_id)
        except:
            continue


async def delete_vector_store(client: AsyncOpenAI, vector_store_id: str):
    try:
        await client.vector_stores.delete(vector_store_id=vector_store_id)
    except:
        pass


async def clear_openai_storage(client: AsyncOpenAI, files_ids, vector_store_id):
    if files_ids:
        await delete_files(client, files_ids)
        logger.info("files deleted")
    if vector_store_id:
        await delete_vector_store(client, vector_store_id)
        logger.info("vector store deleted")

Код генерации одного поля. Здесь в системном промпте задаю ИИ формат ответа “<мысли> ОТВЕТ: <значение>”. В качестве пользовательского промпта генерирую требования под метаданные поля. В prompt_ai для удобства вынес непосредственно код обращения к API генерации. Также добавил механизм повторения неудачных запросов для надежности

Всю историю генераций вручную накапливаю с помощью списка messages. Конечный ответ парсится из строки возвращаемой ИИ по ключевому слову “Ответ:”

SYSTEM_PROMPT = """Ты помощник в заполнении формы "{}". Ты помогаешь пользователю заполнять форму, отвечая на его вопросы по проекту на основе документов. Если ответа на вопрос в документах не указано, оставь ответ пустым.
Прочитай внимательно требования пользователя к ответу, подумай шаг за шагом, напиши свои рассуждения и в конце дай ясный ответ соответствующий требованиям в формате \"Ответ: <твой ответ>\"
Твои ответы будут напрямую вставлены в форму. Начинай отвечать на вопросы пользователя"""

@traceable
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=20, max=80))
async def prompt_ai(client: AsyncOpenAI, messages: list[dict[str, str]], vector_store_id: str) -> str:
    response = await client.responses.create(
        model=MODEL,
        input=messages,
        temperature=0,
        tools=[{
            "type": "file_search",
            "vector_store_ids": [vector_store_id]
        }]
    )
    return response.output[-1].content[0].text

async def prompt_ai_field(client: AsyncClient, field_key: str, field: dict, messages: list, vector_store_id: str, extra_description="") -> dict[str, Any]:
    prompt = "Вопрос: " + generate_prompt(field, extra_description)
    messages.append({"role": "user", "content": prompt})
    logger.info(f"{field_key} PROMPT: {prompt}")
    ai_message_text = await prompt_ai(client, messages, vector_store_id)
    logger.info(f"{field_key} AI RESPONSE: {ai_message_text}")
    messages.append({"role": "assistant", "content": ai_message_text})
    output = parse_answer(field, ai_message_text)
    return {field_key: output}

Итак, подготовив отдельно части, вот весь алгоритм заполнения формы. В нем предусмотрен fallback механизм - любой исход выполнения кода будет прежде чем завершать программу обязательно чистить vector_store и files в openai

async def word_fill_command(schema_path: str, template_path: str, file_paths: list[str], output_path: str):
    # Загрузка JSON схемы
    with open(schema_path, 'r', encoding='utf-8') as f:
        input_data = json.load(f)

    vector_store_id = None
    files_ids = None
    client = None

    try:
        client = create_client()

        vector_store_id = await create_vector_store(client)
        logger.info("vector store created")
        files_ids = await add_files(client, vector_store_id, file_paths)
        logger.info("files uploaded")
        answers = await ai_generate_fill_form(client, input_data, vector_store_id)
        doc_template = DocxTemplate(template_path)
        doc_template.render(answers)
        doc_template.save(output_path)
    except IncorrectFilesException as files_ex:
        print(f"❌ {files_ex}", file=sys.stderr)
        files_ids = files_ex.files_ids
    except RetryError as e:
        print(f"Достигнуты лимиты OpenAI", file=sys.stderr)
    except Exception as ex:
        print(f"❌ {ex}", file=sys.stderr)
    else:
        print(f"✅ Форма успешно заполнена")
    finally:
        await clear_openai_storage(client, files_ids, vector_store_id)

Запуск

Опробуем код на заполнение моей формы. Запускаю. Спустя 19 минут форма была полностью заполнена, учитывая что она состоит из 73-ех разных вопросов

 Фрагмент заполненной формы
Фрагмент заполненной формы

По цене на заполнение одной формы моделью gpt4.1-mini потратилось $0.74 за input + $0,04 за output + $0,23 за RAG = $1.01 в сумме

Дальнейшие улучшения

Не хочется сливать свои уязвимые данные третьему лицу OpenAI? Без проблем - разворачиваем локальную модель на Ollama, делаем RAG с ним и убираем все внешние API нейросетей. Кстати можно попробовать еще Yandex AI Assistant API - он как и OpenAI предоставляет создание RAG и генерации на его основе

Не нравиться CLI? Оформляем в виде сервера на Fast API, делаем клиент-серверную архитектуру, пишем удобный интерфейс под десктоп, веб.

Заключение

Итак в этом хабре я детально рассказал технологию автоматического заполнения любой Word формы используя ИИ и на примере заполнил свою форму. ИИ не панацея - он галлюцинирует, допускает ошибки и не пишет идеально. Но в качестве наброска, первой черновой версии, которая потом будет проходить через человека - работает на ура. Весь исходный код вы можете найти здесь.

Подписывайтесь на мой телеграм канал - рассказываю про разное интересное применение ИИ в бизнес-задачах подобно этой статье

Пишите свои идеи и мнения в комментариях. Спасибо!

Теги:
Хабы:
+8
Комментарии12

Публикации

Работа

Data Scientist
38 вакансий

Ближайшие события