Введение

Работа с текстом — это, пожалуй, одна из главных областей применения больших языковых моделей (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 = False
End 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 = s
End 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 = s
End 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