Почему ваши NPC такие тупые?
Вы настраивали диалоговую систему для игры. Подключили Llama 3.2 через Ollama, написали промпт с описанием персонажа. Первые пять реплик - гениально. Шестая - уже странно. К десятой NPC забыл ваше имя. К двадцатой - начинает говорить как поддержка банка.
Проблема не в модели. Проблема в архитектуре памяти, которая сводится к "засунем последние 10 сообщений в контекст и помолимся". Это не память. Это буфер обмена.
Что мы строим: NPC, который помнит всё
Представьте NPC в RPG, который:
- Помнит, что вы украли у него яблоко три игровых дня назад
- Знает, что вы помогли его брату, и теперь доверяет вам больше
- Может вспомнить релевантный диалог из прошлой недели, когда это нужно
- Не требует облачных API и работает на вашем ноутбуке
Звучит как фантастика? Это просто правильная архитектура.
Важный нюанс: я не говорю про "большой контекст". Llama 3.2 с 128К токенов - это здорово, но она всё равно забудет детали через 50 сообщений. Нужна внешняя память.
Архитектура: три слоя вместо одного
Типичная ошибка - пытаться запихнуть всю логику в один промпт. Наша система состоит из трёх независимых слоёв:
1 Векторное хранилище - долговременная память
ChromaDB в режиме in-memory (не нужно ставить отдельный сервер). Каждое значимое событие - диалог, действие, наблюдение - превращается в вектор через sentence-transformers и ложится в базу с метаданными:
import chromadb
from sentence_transformers import SentenceTransformer
# Инициализация - локально, без Docker
chroma_client = chromadb.Client()
collection = chroma_client.create_collection("npc_memory")
# Модель для эмбеддингов - выбираем эффективную
# На 2026 год актуальны модели семейства all-MiniLM
embedder = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
# Сохраняем событие
def save_memory(text, metadata):
embedding = embedder.encode(text).tolist()
collection.add(
documents=[text],
embeddings=[embedding],
metadatas=[metadata],
ids=[str(uuid.uuid4())]
)
2 Система релевантности - что вспоминать прямо сейчас
Здесь большинство падает. Берут текущий диалог, ищут похожие вектора, возвращают топ-5. Результат: NPC вспоминает про яблоки, когда вы говорите о драконах, потому где-то в памяти было "дракон украл яблоко".
Правильный подход - двухэтапный поиск:
def retrieve_relevant_memories(current_context, npc_state):
# Первый этап: семантический поиск
query_embedding = embedder.encode(current_context).tolist()
semantic_results = collection.query(
query_embeddings=[query_embedding],
n_results=10
)
# Второй этап: фильтрация по эмоциональной релевантности
filtered = []
for doc, metadata in zip(semantic_results['documents'][0],
semantic_results['metadatas'][0]):
# Проверяем, актуально ли воспоминание для текущего состояния NPC
if is_emotionally_relevant(metadata, npc_state):
filtered.append(doc)
# Возвращаем не более 3 самых релевантных
return filtered[:3]
def is_emotionally_relevant(memory_metadata, current_state):
"""Память более релевантна, если она связана с текущими эмоциями"""
# Пример: если NPC зол, более релевантны воспоминания о конфликтах
if current_state['mood'] == 'angry':
return memory_metadata.get('emotional_tone') in ['conflict', 'betrayal']
return True
3 Динамика отношений - память меняет поведение
Самое интересное. Каждое взаимодействие не просто сохраняется - оно меняет внутреннее состояние NPC. Система отслеживает:
- Уровень доверия (от -10 до 10)
- Эмоциональный фон (радость, гнев, страх, нейтраль)
- Темы, которые NPC считает важными
- Историю обещаний и их выполнения
class RelationshipSystem:
def __init__(self):
self.trust = 0 # -10 to 10
self.mood = 'neutral'
self.important_topics = set()
def update_from_interaction(self, interaction_text, sentiment_score):
# Анализируем взаимодействие через маленькую модель
# Или простые правила, если ресурсов мало
if 'спасибо' in interaction_text.lower():
self.trust += 1
elif 'дурак' in interaction_text.lower():
self.trust -= 2
self.mood = 'angry'
# Обновляем важные темы
topics = extract_topics(interaction_text)
self.important_topics.update(topics)
# Ограничиваем значения
self.trust = max(-10, min(10, self.trust))
Интеграция с Ollama: два пути генерации памяти
Теперь ключевой момент - как превращать сырые диалоги в структурированные воспоминания. Есть два подхода, и я использую оба одновременно.
Путь 1: Рефлексия после диалога
После каждого значимого взаимодействия маленькая модель (например, Llama 3.2 3B) анализирует, что только что произошло, и создаёт структурированное воспоминание:
async def create_memory_reflection(dialog_text, npc_name):
prompt = f"""
Ты - система памяти NPC {npc_name}. Проанализируй диалог и создай воспоминание.
Диалог: {dialog_text}
Верни JSON:
{{
"summary": "краткое описание события",
"emotional_tone": "neutral/positive/negative",
"importance": 1-10,
"topics": ["список тем"]
}}
"""
response = await ollama.chat(
model='llama3.2:3b', # Маленькая, быстрая модель
messages=[{'role': 'user', 'content': prompt}]
)
memory_data = json.loads(response['message']['content'])
save_memory(dialog_text, memory_data) # Сохраняем в ChromaDB
Путь 2: Периодическая консолидация
Каждые N взаимодействий более крупная модель анализирует все recent memories и создаёт summary - "выжимку" того, что NPC узнал о игроке:
async def consolidate_memories(npc_name, recent_memories):
"""Объединяем несколько воспоминаний в одно мета-воспоминание"""
prompt = f"""
Объедини эти воспоминания NPC {npc_name} в единое понимание:
{recent_memories}
Какие паттерны в поведении игрока ты видишь?
Какие темы наиболее важны?
Как изменилось отношение NPC к игроку?
"""
# Здесь можно использовать модель побольше, т.к. запускается редко
response = await ollama.chat(
model='qwen2.5:7b', # Достаточно умная для анализа
messages=[{'role': 'user', 'content': prompt}]
)
# Сохраняем консолидированное понимание
save_memory(response['message']['content'], {
'type': 'consolidated',
'timestamp': datetime.now().isoformat()
})
Полный цикл взаимодействия
Теперь собираем всё вместе. Вот как выглядит один цикл диалога:
async def npc_dialog_cycle(user_input, npc):
"""Полный цикл: память -> контекст -> ответ -> обновление"""
# 1. Достаём релевантные воспоминания
memories = retrieve_relevant_memories(user_input, npc.state)
# 2. Собираем контекст для модели
context = build_context(
npc_description=npc.description,
current_state=npc.state,
relevant_memories=memories,
recent_chat_history=npc.recent_chat[-5:], # Краткая история
user_input=user_input
)
# 3. Генерируем ответ через Ollama
response = await ollama.chat(
model='llama3.2:8b', # Основная диалоговая модель
messages=[{'role': 'user', 'content': context}],
options={'temperature': 0.7}
)
# 4. Сохраняем взаимодействие в память
await create_memory_reflection(
dialog_text=f"Игрок: {user_input}\nNPC: {response}",
npc_name=npc.name
)
# 5. Обновляем отношения
npc.relationship.update_from_interaction(user_input, analyze_sentiment(user_input))
# 6. Периодическая консолидация (раз в 10 взаимодействий)
if npc.interaction_count % 10 == 0:
recent = get_recent_memories(npc.name, limit=20)
await consolidate_memories(npc.name, recent)
return response
Ошибки, которые всех убивают
Я видел десятки реализаций. Вот что ломает систему чаще всего:
| Ошибка | Почему это проблема | Как исправить |
|---|---|---|
| Сохранять каждую реплику | Векторная база заполняется мусором, поиск становится неточным | Фильтровать по важности. Сохранять только значимые взаимодействия |
| Искать только по последнему сообщению | Упускается контекст всей беседы | Использовать сводку последних 3-4 реплик как поисковый запрос |
| Не учитывать эмоциональное состояние | NPC вспоминает нейтральные факты, когда зол | Добавить фильтрацию по emotional_tone в метаданных |
| Хранить память только в векторах | Теряются временные связи и последовательности | Добавить временную базу (SQLite) для хранения цепочек событий |
Оптимизация под железо
Всё это круто, но что если у вас нет RTX 4090? Работаем с ограничениями:
- Маленькие эмбеддинги: all-MiniLM-L6-v2 вместо больших моделей. 384 измерения вместо 768+
- Квантование моделей: Используйте q4_0 или q5_K_M версии в Ollama. Llama 3.2 3B в q4_0 весит ~1.8GB и работает на CPU
- Ленивая загрузка: Не держите все модели в памяти одновременно. Загружайте model A для диалога, выгружайте, загружайте model B для анализа
- Кэширование эмбеддингов: Не вычисляйте эмбеддинги для одних и тех же фраз повторно
На Raspberry Pi 5 это будет работать. Медленно, но будет. На ноутбуке с 16GB RAM - комфортно.
Что дальше? Эволюция архитектуры
Базовая система работает. Но можно пойти глубже:
- Иерархическая память: Воспоминания образуют граф. "Кража яблока" связана с "встреча с торговцем", который связан с "квест на зелье"
- Прогностические модели: NPC не просто реагирует, а предсказывает, что вы скажете дальше, на основе паттернов
- Мультимодальность: Как в мультимодальном краулере, но для NPC - память включает не только текст, но и "образы" мест и людей
- Распределённые агенты: Несколько NPC с общей памятью. Один видит что-то - все узнают (с поправкой на достоверность)
Самый интересный тренд 2026 года - смешанные системы памяти. Векторные базы для быстрого поиска по смыслу, графовые - для связей, реляционные - для фактов. Temple Vault экспериментирует с файловой системой как памятью, но для NPC пока лучше подходят специализированные хранилища.
Почему это важно за пределами игр
Вы думаете, это только для NPC в играх? Ошибаетесь. Такая же архитектура нужна:
- Персональным ассистентам, которые помнят ваши предпочтения не одну сессию
- Образовательным системам, которые адаптируются к прогрессу ученика
- Клиентским ботам, которые не спрашивают одно и то же каждый раз
- Исследовательским агентам, которые строят долгосрочные гипотезы
Проблема амнезии ИИ - главное препятствие для полезных приложений. ArkOS борется с этим на системном уровне, но для конкретных NPC нужна точечная архитектура.
Начните с простого
Не пытайтесь сразу построить Skyrim с умными NPC. Начните с:
- Одного NPC с описанием на 100 токенов
- ChromaDB в памяти с 50 воспоминаниями
- Простой системы отношений (доверие от -5 до 5)
- Llama 3.2 3B для всего (диалог + анализ)
Когда это заработает - добавляйте слои. Сначала консолидацию памяти. Потом эмоциональную фильтрацию. Затем иерархическую структуру.
Главный секрет не в сложности архитектуры, а в том, чтобы каждая часть делала одну простую вещь, но делала её хорошо. Векторный поиск ищет. Отношения меняются. Модель генерирует. Когда всё пытается делать всё - получается бесполезное месиво.
И последнее: тестируйте на реальных диалогах. Не на придуманных "привет-как дела", а на сложных, многоходовых сценариях. Украсть яблоко, извиниться, помочь по хозяйству, снова украсть - вот где видна разница между буфером и памятью.
Следующий шаг: NPC, которые учатся на своих ошибках. Но это уже другая история.