Локальный бэкенд для диалогов NPC на LLM: гайд 2026 | AiManual
AiManual Logo Ai / Manual.
02 Июл 2026 Инструмент

Создаем локальный бэкенд для NPC с диалогами NPC-NPC на базе LLM

Как создать бэкенд для диалогов между NPC с помощью локальной LLM. Код, примеры, сравнение с альтернативами. Для разработчиков игр.

NPC больше не читают с бумажки

В 2026 году смотреть на NPC, которые повторяют три заученные фразы, уже просто больно. Игроки хотят живого мира, где стражник может поспорить с кузнецом о погоде, а торговка — шепнуть прохожему сплетню. Но писать тысячи строк диалогов вручную — адский труд. А если NPC должны общаться друг с другом без участия игрока? Тут классические скрипты ломаются.

Решение — локальная LLM. Запускаете модель у себя на сервере, прикручиваете бэкенд, который управляет диалогами NPC-NPC, и получаете динамический мир. Никаких ежемесячных платежей за API, никакой цензуры, полный контроль над данными. Звучит как магия? На деле — пара файлов Python и одна команда docker-compose up.

Краткая анатомия бэкенда NPC-NPC

Представьте себе движок, который сидит между игрой и LLM. Его задачи:

  • Принимать запросы на диалог от двух (или больше) NPC.
  • Формировать промпт с описанием персонажей, контекста, истории.
  • Отправлять запрос к LLM (через Ollama, llama.cpp или LocalAI).
  • Парсить ответ — обычно это структурированный JSON с репликами.
  • Обновлять долговременную память NPC (база фактов, эмоциональные шкалы).
  • Отдавать результат в игровой движок (Unity, Godot, Ren'Py, да хоть консоль).

Самое сложное — заставить LLM не забывать, что сказал NPC две минуты назад. Стандартное окно контекста модели в 32K токенов быстро забивается. Поэтому нужен внешний модуль памяти — об этом мы уже писали в статье «NPC с характером».

Наш бэкенд мы построим на FastAPI + Ollama. Он будет принимать POST-запрос с ID двух NPC, вытаскивать их профили из SQLite, склеивать промпт и возвращать сгенерированный диалог. Вуаля.

Сравнение с альтернативами: почему не готовые решения?

На рынке уже есть проекты, которые делают похожие вещи. Давайте пробежимся по главным:

ИнструментПодходДиалоги NPC-NPCЛокальность
Наш бэкендСамописный на FastAPI + OllamaДа, полностьюДа
SillyTavern AI Game MasterПлагин к SillyTavernОграничен (через карточки персонажей)Да
Personica AIГотовый модуль для Unreal EngineДа, но привязан к UEДа
Open-source RPG с генерацией квестовЦелая RPG на локальной LLMВстроено, но монолитноДа
Облачные решения (Inworld AI, Convai)SaaSДаНет

SillyTavern — отличная штука для текстовых приключений, но встраивать его в игру с нативной поддержкой NPC-NPC сложно: он заточен под игрока-человека. Personica AI решает проблему, но только для Unreal Engine — если ваш движок Godot или собственный, придется пилить свое. А наша реализация — легковесный бэкенд, который можно воткнуть в любой проект через REST.

Пример использования: два NPC обсуждают кражу в таверне

Допустим, в игре есть трактирщик Билл и стражник Джек. Игрок заходит в таверну, слышит их разговор. Вот как это выглядит в коде бэкенда.

1 Структура NPC в базе

{
  "npc_id": "bill",
  "name": "Билл",
  "role": "трактирщик",
  "personality": "ворчливый, жадный, но справедливый",
  "memory": [
    "Вчера у него украли бочонок эля",
    "Подозревает местного вора Лиса"
  ]
}

{
  "npc_id": "jack",
  "name": "Джек",
  "role": "стражник",
  "personality": "ленивый, любит выпить, но службу несет",
  "memory": [
    "Должен найти вора, но не хочет напрягаться"
  ]
}

2 Код бэкенда (FastAPI + Ollama)

from fastapi import FastAPI
from pydantic import BaseModel
import ollama

app = FastAPI()

class DialogRequest(BaseModel):
    npc_a: str
    npc_b: str
    context: str = ""

@app.post("/dialog")
async def generate_dialog(req: DialogRequest):
    profile_a = get_npc_profile(req.npc_a)  # вытаскиваем из БД
    profile_b = get_npc_profile(req.npc_b)
    prompt = f"""Ты — генератор диалогов для RPG. Напиши диалог между двумя NPC.

NPC 1: {profile_a['name']} ({profile_a['role']})
Характер: {profile_a['personality']}
Память: {'; '.join(profile_a['memory'])}

NPC 2: {profile_b['name']} ({profile_b['role']})
Характер: {profile_b['personality']}
Память: {'; '.join(profile_b['memory'])}

Контекст сцены: {req.context}

Ответь строгим JSON-массивом реплик:
[
  {{"speaker": "имя", "text": "реплика"}},
  ...
]
"""
    response = ollama.chat(
        model="llama4:latest",  # на июль 2026 актуальна Llama 4
        messages=[{"role": "user", "content": prompt}]
    )
    # Парсим JSON из ответа модели
    import json
    raw = response['message']['content']
    dialog = json.loads(raw)
    # Обновляем память NPC (например, добавляем факт о разговоре)
    update_memory(req.npc_a, f"Обсуждал кражу с {profile_b['name']}")
    update_memory(req.npc_b, f"Обсуждал кражу с {profile_a['name']}")
    return dialog

3 Что вернет модель

[
  {"speaker": "Билл", "text": "Джек, ты когда уже найдешь того, кто спер мой эль? Я тебе не король, чтобы ждать!"},
  {"speaker": "Джек", "text": "Да ищу я, ищу... Может, это Лис опять? Он вчера крутился у твоих бочек."},
  {"speaker": "Билл", "text": "Лис? Да он же из деревни сбежал позавчера! Ты бы хоть патрулировал, а не пил у меня каждый вечер."}
]

Диалог динамический, зависит от памяти. Если бы Билл не помнил про кражу — разговор был бы о погоде. Модель сама решает, как развивать сцену, а мы лишь подсовываем факты.

Важный нюанс: structured output. Если модель вернет невалидный JSON — бэкенд упадет. Используйте грамматики (например, авторитарный бэкенд с structured output) или библиотеку outlines для гарантии формата.

Как это интегрировать в реальную игру

Бэкенд не привязан к движку. Вы можете дергать его из Unity через UnityWebRequest, из Godot — через HTTPRequest, из Ren'Py — через renpy.http. Вот пример вызова из скрипта Godot (GDScript):

var http = HTTPRequest.new()
func _ready():
    add_child(http)
    http.request_completed.connect(_on_dialog_ready)
    var body = JSON.stringify({"npc_a": "bill", "npc_b": "jack"})
    http.request("http://localhost:8000/dialog", [], HTTPClient.METHOD_POST, body)

func _on_dialog_ready(result, code, headers, body):
    var dialog = JSON.parse_string(body.get_string_from_utf8())
    for line in dialog:
        print(line.speaker + ": " + line.text)

Если игра — мод для S.T.A.L.K.E.R. Anomaly, можно использовать тот же принцип, только вместо HTTP — вызов через асинхронный скрипт Lua. Подробный разбор такой интеграции уже есть в нашей статье.

Кому это реально нужно

Инструмент не для всех. Если вы делаете казуальную игру на пару уровней — проще нарисовать ветки диалогов в Articy. Но если ваш проект — immersive sim, RPG с открытым миром или мод к старой игре (да хоть к Ultima Online — вот пример), то такой бэкенд спасет сотни часов работы.

Типичные сценарии:

  • Инди-команды без сценариста — LLM пишет диалоги сама, вы только правите промпты.
  • Моддеры, оживляющие старые игры — никаких ограничений оригинального движка.
  • Прототипирование: набросали NPC, запустили — через минуту они уже болтают.

Еще один неочевидный юзкейс — тестирование. Запустили пару десятков NPC и смотрите, не повторяются ли диалоги, не зацикливаются ли модели. Это выявит проблемы с памятью и контекстом до релиза.

Модель имеет значение

Не любая LLM подойдет. Для диалогов NPC-NPC важна способность удерживать нить разговора и генерировать короткие, естественные реплики. Из личного опыта: на момент 2026 года для русского языка лучшие результаты дают Llama 4 70B (через Ollama) и Qwen 2.5 32B — они не «забывают» контекст в течение 10-15 реплик даже без внешней памяти. Mistral Large 2 тоже хорош, но чувствителен к формату JSON — приходится допиливать грамматики. Выбор модели мы подробно разбирали в соответствующем гайде.

💡
Не гонитесь за размером. Для диалогов NPC-NPC 7B-модели часто справляются хуже, чем 13B, но 70B требует много VRAM. Золотая середина — 13B–32B. Если бюджет на железо ограничен, попробуйте Llama 4 8B — она удивительно адекватна для своих параметров.

Пару слов про оптимизацию

Бэкенд будет дергаться часто — каждый раз, когда два NPC встречаются. Если у вас 1000 NPC, они могут генерировать диалоги десятками в секунду. Одноядерный CPU не вывезет. Решения:

  • Кешируйте типовые диалоги (например, приветствия) — пусть модель генерирует только уникальные.
  • Используйте очередь задач (Celery или Redis Queue).
  • Запускайте несколько инстансов Ollama для параллельной обработки.
  • Для нетребовательных диалогов ставьте модель поменьше (например, Qwen 2.5 7B), а для ключевых сцен — прогоняйте через большую.

Кстати, если у вас несколько NPC в одной сцене, можно генерировать сразу целую сцену, указав в промпте всех участников. Это дешевле, чем запрашивать пары.

Типичные грабли и как их обойти

  • Модель начинает галлюцинировать имена. Жестко фиксируйте имена в промпте и используйте structured output.
  • Диалоги уходят в бесконечность. Ставьте лимит реплик (например, макс. 6) и параметр max_tokens.
  • NPC забывают, что только что сказали. Включайте последние 2-3 реплики в промпт (скользящее окно).
  • Память забивается мусором. Используйте векторную базу (Chroma) — храните только значимые факты, извлеченные через sumarizer.

Если грабли все равно прилетают — вспомните про авторитарный бэкенд: он перехватывает управление и не дает LLM уйти в разнос.

Итоговая мысль: не делайте «говорящие головы», делайте мир

Локальный бэкенд для диалогов NPC-NPC — не просто замена диалоговому редактору. Это способ сделать игровой мир живым, независимым от игрока. Когда два NPC спорят, торгуются или сплетничают — игрок чувствует, что он не в парке аттракционов, а в настоящем сообществе. И для этого не нужно ждать, пока большая студия наняла армию сценаристов.

Соберите бэкенд за вечер, воткните в свой проект и посмотрите, как заискрят диалоги. А если что-то пойдет не так — у нас на сайте целая серия статей про архитектуру NPC на LLM, эксперименты с Godot, Ren'Py и Unreal. Вперед, мир ждет своих цифровых жителей.

Подписаться на канал