СОЗДАНИЕ ДС С ЛОКАЛЬНОЙ LLM. ЧАСТЬ 5: ВЗАИМОДЕЙСТВИЕ С API

Мы уже обсудили данные, грамматику и промпты. Теперь поговорим о транспорте. Как именно наш Python-скрипт передает информацию локальной модели и получает ответ?

В отличие от облачных решений (ChatGPT, Claude), где мы зависим от интернет-соединения и биллинга, локальная модель работает на нашем железе. Но чтобы не переписывать код под каждую новую нейросеть, мы используем стандартный протокол.

1. ЛОКАЛЬНЫЙ СЕРВЕР (OPENAI-COMPATIBLE)

Большинство современных инструментов для запуска локальных моделей (LM Studio, vLLM, Ollama, llama.cpp) умеют притворяться сервером OpenAI. Это значит, что они поднимают локальный веб-сервер (обычно http://localhost:1234 или 8000) и принимают запросы в том же формате, что и официальный API OpenAI.

Это огромное преимущество. В коде не нужно использовать специфические библиотеки для torch или transformers. Мы просто отправляем обычный POST-запрос с JSON.

2. РЕАЛИЗАЦИЯ В КОДЕ (БЕЗ ЛИШНИХ БИБЛИОТЕК)

Вместо установки тяжелой библиотеки openai (которая тянет за собой кучу зависимостей), я использую стандартный легковесный модуль requests. Это делает скрипт более портативным и простым для аудита.

Вот ключевая функция call_llm из моего проекта:

def call_llm(contract_json, hints=None):
    payload = {
        "model": "qwen3-vl-30b-a3b-thinking",  # Имя модели, загруженной в сервер
        "messages": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": USER_PROMPT_WITH_CONTEXT}
        ],
        "temperature": 0.0,      # Ноль креативности, максимум логики
        "max_tokens": -1,        # Не ограничиваем длину ответа
        "response_format": ADDENDUM_SCHEMA  # Та самая JSON-схема
    }

    # Отправка запроса
    r = requests.post("http://localhost:1234/v1/chat/completions", json=payload, timeout=900.0)
    r.raise_for_status()
    
    # Парсинг ответа
    data = r.json()
    content = data["choices"][0]["message"]["content"]
    return json.loads(content)

3. УПРАВЛЕНИЕ НАДЕЖНОСТЬЮ (RETRIES)

Локальный сервер — штука капризная. Модель может “захлебнуться” в памяти, видеокарта может перегреться, или генерация может занять слишком много времени. Чтобы скрипт не падал при первой ошибке, я обернул вызов в цикл повторных попыток (retries).

for i in range(5):
    try:
        # ... попытка запроса ...
        return json.loads(content)
    except Exception as e:
        if i == 4: # Последняя попытка провалилась
            print(f"LLM error: {e}")
            return None
        time.sleep(2 * (i + 1)) # Экспоненциальная задержка

Если сервер временно перегружен, скрипт подождет 2 секунды, потом 4, потом 6… Это позволяет “протолкнуть” тяжелые запросы даже при высокой нагрузке на GPU.

4. ТАЙМАУТЫ

Обратите внимание на timeout=900.0 (15 минут). Локальные модели, особенно большие (как Qwen 30B) и особенно в режиме “Thinking” (когда они рассуждают перед ответом), могут думать долго. Обычный таймаут в 30-60 секунд здесь не подойдет. Лучше подождать лишнюю минуту, чем оборвать соединение, когда ответ был уже почти готов.

5. ПОДГОТОВКА КОНТЕКСТА

Важный нюанс: мы не отправляем модели PDF-файл или DOCX. Мы отправляем чистый JSON с текстом договора. Это экономит количество токенов (контекстное окно) и ускоряет обработку. В переменную contract_json мы кладем только то, что действительно нужно модели: список глав, текст пунктов, реквизиты. Лишний мусор отсекается на этапе подготовки данных.

В заключительной части мы соберем всё вместе и посмотрим, как полученный JSON превращается в красивый документ Word.

enes