Интеграция LLM в MS Word с помощью VBA

от автора

Введение

Работа с текстом — это, пожалуй, одна из главных областей применения больших языковых моделей (LLM). Существует много способов редактирования текста. аналитики например часто работают с разметкой markdown — такой текст почти ничего не весит, с ним легко работать в любом текстовом редакторе и его легко можно сгенерировать при помощи скриптов. Но не секрет, что для подавляющего большинства пользователей редактор Word по‑прежнему остается основным инструментом. Мой личный опыт работы с текстом таков — свои тексты и научные отчеты я готовлю в редакторской системе Quarto, иногда в чистом markdown. Готовый текст рендерю в docx и уже затем выполняю чистовую доработку в MS Word. И вот здесь могут возникать трудности — если с чистым markdown можно без проблем работать при помощи встроенных в текстовый редактор (я использую Visual studio code) инструментов LLM, то в Word их нет. Вернее есть, но использовать их в России по целому ряду причин невозможно.

Я давно хотел решить эту проблему и сделать так, чтобы LLM был всегда под рукой, прямо в редакторе Word. В итоге родился небольшой пет‑проект — набор VBA‑макросов для MS Word, который добавляет функционал работы с любыми LLM через OpenAI‑совместимый API.

Идея и возможности

Основная идея проста: дать возможность взаимодействовать с LLM, не выходя из привычного интерфейса Microsoft Word. Инструмент должен быть гибким и работать с любым сервисом, у которого есть API, совместимый с OpenAI (это сейчас практически все популярные модели, включая те, что можно запустить локально).

Проект включает два ключевых сценария использования, реализованных в виде отдельных макросов:

  • Одиночный запрос (RunLLMQuery). Быстрый режим для работы с выделенным фрагментом. Вы выделяете текст в документе, запускаете макрос, вводите свой промпт (например, «исправь грамматические ошибки» или «перепиши в более деловом стиле»), и LLM обрабатывает текст, а результат заменяет исходный выделенный фрагмент. Это удобно, если нужно что‑то исправить в тексте — например, расставить запятые, перевести на английский язык. Или вот, только что написал почти целое предложение на заметив, что пишу в английской раскладке — вместо переписывания можно просто попросить LLM изменить раскладку текста с английской на русскую.

  • Чат‑интерфейс (RunLLMChat). Полноценный диалог с LLM в отдельном окне, прямо как в веб‑версиях. Вы можете задавать вопросы, уточнять, обсуждать свои идеи, а затем одним нажатием кнопки вставить последний ответ модели в документ. Это подходит для «мозгового штурма», генерации идей или когда нужно обсудить сложный текст по частям.

Одиночный запрос к модели

Одиночный запрос к модели

Этот запрос заменит текст набранный латиницей на русский текст «Этот текст написан по‑русски, но в английской раскладке»

Чат с моделью

Чат с моделью

А здесь мы уже при помощи чата с LLM просим перевести текст на немецкий язык.

Ключевые особенности

Помимо основной функциональности, я в проекте реализовал важные с технической точки зрения вещи:

● Совместимость с любым OpenAI‑совместимым API. Вы не привязаны к конкретному провайдеру. Можно указать любой API_URL, MODEL_ID и API_KEY — и работать с ChatGPT, DeepSeek, Mistral, локальными моделями через Ollama или любым другим совместимым сервисом.

● Хранение настроек в реестре Windows. Все конфиденциальные данные (API‑ключ) и настройки (URL, модель, системный промпт) сохраняются в ветке HKCU\Software\LLMWordMacro\. Это безопасно и не требует прав администратора. Для настройки есть специальный макрос ConfigureLLMSettings.

● Для еще большего удобства там же — в макросе ConfigureLLMSettings можно указать системный промпт.

● Инструмент реализован как шаблон Word (.dotm). Т.е. его без проблем можно подключить к любому документу

Лично я в качестве хаба для подключения к LLM использую сервис aitunnel.ru, но вы можете выбрать любой другой — оригинальные OpenAI или даже локальные модели.

Настройка LLM, шаг 1 - указание провайдера API

Настройка LLM, шаг 1 — указание провайдера API
Настройка LLM, шаг 2 - указание названия модели

Настройка LLM, шаг 2 — указание названия модели
Настройка LLM, шаг 3 - указание ключа API

Настройка LLM, шаг 3 — указание ключа API
Настройка LLM, шаг 4 - Системный промпт

Настройка LLM, шаг 4 — Системный промпт

Технические детали: Как это работает под капотом

В основе лежат классические технологии автоматизации Microsoft Office:

● VBA (Visual Basic for Applications) — язык программирования, встроенный в продукты Office. Он позволяет управлять документом, его содержимым и создавать пользовательские формы.

● HTTP‑запросы из VBA. Для общения с API я использовал объект WinHttp.WinHttpRequest.5.1 (или MSXML2.XMLHTTP), который отправляет POST‑запросы с данными в формате JSON и получает ответы от LLM.

● Парсинг JSON. Я реализовал довольно примитивную (но вполне подходящую для пет‑проекта) логику для извлечения текста ответа из стандартного JSON‑ответа OpenAI‑совместимых API.

● Пользовательская форма. Для чата создал форму (LLM_chat.frm), которая обеспечивает интерфейс для ввода сообщений и отображения истории диалога.

Не буду сильно утомлять конкретной реализацией на VBA. Посмотреть ее можно в репозитории https://github.com/Obsidian‑pb/llm_4_word_vba Остановлюсь на некоторых нюансах.

Состав проекта VBA

Состав проекта VBA

Проект состоит из модуля LLM_work, и пользовательской формы LLM_chat. В коде пожалуй стоит обратить внимание на основную функцию отправки и обработки запроса CallLLM и функции обработки JSON.

Функция CallLLM — Вызов LLM через HTTP
Public Function CallLLM(ByVal prompt As String, ByRef answer As String) As Boolean    On Error GoTo ErrHandler       Dim http As Object    Dim payload As String    Dim responseText As String    Dim startTime As Single    Dim waitMs As Long    ' Формируем запрос в JSON к модели    payload = BuildJsonPayload(prompt)       ' ServerXMLHTTP избегает проблемы вложенного COM-цикла сообщений,    ' которая есть у синхронного XMLHTTP — не заходит повторно в     ' события Visio/LLM_chat.    Set http = CreateObject("MSXML2.ServerXMLHTTP")    http.Open "POST", LLM_API_URL, True    http.setRequestHeader "Content-Type", "application/json; charset=utf-8"    http.setRequestHeader "Authorization", "Bearer " & LLM_API_KEY       ' Таймауты (мс): resolve, connect, send, receive    http.setTimeouts 10000, 10000, 30000, 120000       ' Отправляем тело запроса    http.send payload       ' Опрашиваем readyState без заморозки UI + спиннер    Dim pollCounter As Long    startTime = Timer    Do While http.readyState <> 4        DoEvents        pollCounter = pollCounter + 1        UpdateSpinner pollCounter        ' Абсолютный лимит — 3 минуты, на случай зависания сервера        If Timer - startTime > 180 Then            Debug.Print "CallLLM: timeout (180s)"            CallLLM = False            Exit Function        End If    Loop    If http.Status <> 200 Then        Log "CallLLM: HTTP " & http.Status & " " & http.StatusText        CallLLM = False        Exit Function    End If       ' Получаем ответ    responseText = http.responseText    answer = ExtractContentFromJson(responseText)       CallLLM = (answer <> "")    Exit Function   ErrHandler:    CallLLM = FalseEnd Function

Что делает код:

  1. Собирает JSON‑тело через BuildJsonPayload с системным + пользовательским сообщением

  2. Использует MSXML2.ServerXMLHTTP (асинхронный режим, чтобы не блокировать события VBA‑форм)

  3. Устанавливает таймауты: 10c resolve, 10c connect, 30c send, 120c receive

  4. Крутит цикл опроса readyState с DoEvents + спиннером на форме чата (чтобы было видно, что модель думает, а не зависла)

  5. При HTTP 200 — парсит JSON через ExtractContentFromJson (ищет choices[0].message.content)

  6. Возвращает True при успехе, False при ошибке/таймауте/не-200

Функция BuildJsonPayload — Формирование JSON‑тела запроса
' Формирование JSON-тела запросаPrivate Function BuildJsonPayload(ByVal prompt As String) As String    Dim escUser As String    Dim escSystem As String    Dim messagesJson As String    escUser = JsonEscape(prompt)    escSystem = JsonEscape(LLM_SYSTEM_PROMPT)    ' Формируем массв messages, опционально добавляя системный промпт    If Trim(LLM_SYSTEM_PROMPT) <> "" Then        messagesJson = _            "{""role"":""system"",""content"":""" & escSystem & """}," & _            "{""role"":""user"",""content"":""" & escUser & """}"    Else        messagesJson = _            "{""role"":""user"",""content"":""" & escUser & """}"    End If    ' Формат под ваш API (OpenAI-совместимый)    BuildJsonPayload = _        "{" & _        """model"":""" & LLM_MODEL_ID & """," & _        """messages"":[" & messagesJson & "]" & _        "}"End Function
Функция JsonEscape — Простейший экранировщик для JSON‑строки
Private Function JsonEscape(ByVal s As String) As String    s = Replace(s, "\", "\\")    s = Replace(s, Chr(34), "\" & Chr(34))    s = Replace(s, vbBack, "\b")    s = Replace(s, vbFormFeed, "\f")    s = Replace(s, vbCr, "\r")    s = Replace(s, vbLf, "\n")    s = Replace(s, vbTab, "\t")    ' Удаляем остальные управляющие символы (ASCII 0–31, кроме вышеперечисленных)    Dim i As Long    For i = 0 To 31        If InStr(s, Chr(i)) > 0 Then            s = Replace(s, Chr(i), "\u" & Right$("0000" & Hex(AscW(Chr(i))), 4))        End If    Next i    JsonEscape = sEnd Function
Функция ExtractContentFromJson — Разбор JSON‑ответа
Private Function ExtractContentFromJson(ByVal json As String) As String    Dim key As String    Dim pos As Long    Dim startPos As Long    Dim endPos As Long    Dim tmp As String        ' 1. Находим блок "message":{"role":...,"content":"..."}    key = """message"":{"    pos = InStr(1, json, key, vbTextCompare)    If pos = 0 Then        ExtractContentFromJson = ""        Exit Function    End If        ' 2. Отрезаем всё до "message":{, чтобы сократить строку    tmp = Mid$(json, pos + Len(key))        ' 3. Внутри этого блока ищем "content":"..."    key = """content"":"""    pos = InStr(1, tmp, key, vbTextCompare)    If pos = 0 Then        ExtractContentFromJson = ""        Exit Function    End If        startPos = pos + Len(key)    endPos = startPos        ' 4. Ищем завершающую кавычку, учитывая возможные экранированные \"    Do While endPos <= Len(tmp)        If Mid$(tmp, endPos, 1) = """" Then            ' Проверяем, не экранирована ли кавычка            If Mid$(tmp, endPos - 1, 1) <> "\" Then                Exit Do            End If        End If        endPos = endPos + 1    Loop        If endPos > Len(tmp) Then        ExtractContentFromJson = ""        Exit Function    End If        ExtractContentFromJson = JsonUnescape(Mid$(tmp, startPos, endPos - startPos))End Function
Функция JsonUnescape — Обратное преобразование для \n, \“ и \\
Private Function JsonUnescape(ByVal s As String) As String    ' \\ -> \    s = Replace(s, "\\", Chr(92))    ' \" -> "    s = Replace(s, "\" & Chr(34), Chr(34))    ' \n -> CRLF    s = Replace(s, "\n", vbCrLf)    JsonUnescape = sEnd Function

Как это запустить и настроить за 5 минут

Весь процесс предельно прост. Я описал его в репозитории. Вот два основных пути:

Первый. Самый быстрый способ (использовать готовый шаблон):

  • Скачайте файл doc.dotm из репозитория.

  • Откройте ваш документ Word.

  • Перейдите в Файл → Параметры → Надстройки.

  • Внизу в выпадающем списке Управление выберите Шаблоны и нажмите Перейти.

В открывшемся окне нажмите Добавить и укажите путь к скачанному файлу doc.dotm. Готово! Все макросы уже подключены.

Подключение шаблона. 1 - добавить шаблон из того места куда он был скачан (если это не было сделано ранее), 2 - поставить галочку

Подключение шаблона. 1 — добавить шаблон из того места куда он был скачан (если это не было сделано ранее), 2 — поставить галочку

Второй. Импорт в существующий документ:

  • Откройте свой документ и запустите редактор VBA (Alt+F11).

  • В меню редактора выберите Файл → Импорт и поочередно импортируйте файлы LLM_work.bas и LLM_chat.frm из папки vba репозитория.

  • Сохраните документ.

После подключения макросов следует сделать следующее:

  • Настроить макросы. Запустите макрос ConfigureLLMSettings (через Разработчик → Макросы или Alt+F8) и введите свои данные: API URL, ID модели и ваш секретный ключ.

  • Настроить ленту для удобства (по желанию). Чтобы не запускать макросы каждый раз через Alt+F8, их лучше вынести на ленту Word. Создайте новую вкладку LLM, в ней группу и добавьте нужные команды:

  • LLM_work.RunLLMQuery

  • LLM_work.RunLLMChat

  • LLM_work.ConfigureLLMSettings

Настройка ленты для использования макросов

Настройка ленты для использования макросов

Заключение

Не буду лукавить — при написании проекта активно пользовался тем же LLM, но в целом, учитывая, что просто хотелось попробовать «А получится ли сделать LLM‑чат в word», получилось вполне удобоваримо. Ну и польза от проекта для меня лично оказалась вполне ощутимой — насколько проще стало работать с простым текстом, когда не нужно перекидывать его из редактора в чат LLM и обратно!

Вместе с тем есть и ряд сложностей.

  • Самое главное — да, мы можем редактировать текст, но не можем его стилизовать, мы не можем установить заголовки, выделить жирным или курсивом, не можем сделать текст подстрочным и так далее

  • Мы не можем работать с таблицами — это тоже очень важно

  • Мы не можем передать LLM изображения из текста

  • Мы не можем загрузить сторонние файлы…

Эти функции реализованы в инструментах самого редактора Word и для их применения следует использовать tools, а это уже отдельная история.

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

Полезные ссылки:

● Репозиторий проекта: https://github.com/Obsidian‑pb/llm_4_word_vba

● Скачать шаблон doc.dotm: https://cloud.mail.ru/public/3W4K/annsCvYGG

ссылка на оригинал статьи https://habr.com/ru/articles/1055296/