СОЗДАНИЕ ДС С ЛОКАЛЬНОЙ 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.