Архитектура NPC с памятью на Ollama: векторные базы и динамика отношений | AiManual
AiManual Logo Ai / Manual.
21 Фев 2026 Гайд

NPC с характером: как строить локальных агентов с памятью, которая помнит даже оскорбления

Полное руководство по созданию локальных NPC с долговременной памятью на Ollama, ChromaDB, системой релевантности и динамикой социальных связей.

Почему ваши 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: два пути генерации памяти

Теперь ключевой момент - как превращать сырые диалоги в структурированные воспоминания. Есть два подхода, и я использую оба одновременно.

💡
На 2026 год Llama 3.2 3B инференс отлично справляется с такой задачей на CPU. Не нужно тянуть 70B модель для анализа эмоций.

Путь 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 - комфортно.

Что дальше? Эволюция архитектуры

Базовая система работает. Но можно пойти глубже:

  1. Иерархическая память: Воспоминания образуют граф. "Кража яблока" связана с "встреча с торговцем", который связан с "квест на зелье"
  2. Прогностические модели: NPC не просто реагирует, а предсказывает, что вы скажете дальше, на основе паттернов
  3. Мультимодальность: Как в мультимодальном краулере, но для NPC - память включает не только текст, но и "образы" мест и людей
  4. Распределённые агенты: Несколько NPC с общей памятью. Один видит что-то - все узнают (с поправкой на достоверность)

Самый интересный тренд 2026 года - смешанные системы памяти. Векторные базы для быстрого поиска по смыслу, графовые - для связей, реляционные - для фактов. Temple Vault экспериментирует с файловой системой как памятью, но для NPC пока лучше подходят специализированные хранилища.

Почему это важно за пределами игр

Вы думаете, это только для NPC в играх? Ошибаетесь. Такая же архитектура нужна:

  • Персональным ассистентам, которые помнят ваши предпочтения не одну сессию
  • Образовательным системам, которые адаптируются к прогрессу ученика
  • Клиентским ботам, которые не спрашивают одно и то же каждый раз
  • Исследовательским агентам, которые строят долгосрочные гипотезы

Проблема амнезии ИИ - главное препятствие для полезных приложений. ArkOS борется с этим на системном уровне, но для конкретных NPC нужна точечная архитектура.

Начните с простого

Не пытайтесь сразу построить Skyrim с умными NPC. Начните с:

  1. Одного NPC с описанием на 100 токенов
  2. ChromaDB в памяти с 50 воспоминаниями
  3. Простой системы отношений (доверие от -5 до 5)
  4. Llama 3.2 3B для всего (диалог + анализ)

Когда это заработает - добавляйте слои. Сначала консолидацию памяти. Потом эмоциональную фильтрацию. Затем иерархическую структуру.

💡
Не забудьте про техники управления контекстом. Даже с внешней памятью нужно правильно формировать промпт.

Главный секрет не в сложности архитектуры, а в том, чтобы каждая часть делала одну простую вещь, но делала её хорошо. Векторный поиск ищет. Отношения меняются. Модель генерирует. Когда всё пытается делать всё - получается бесполезное месиво.

И последнее: тестируйте на реальных диалогах. Не на придуманных "привет-как дела", а на сложных, многоходовых сценариях. Украсть яблоко, извиниться, помочь по хозяйству, снова украсть - вот где видна разница между буфером и памятью.

Следующий шаг: NPC, которые учатся на своих ошибках. Но это уже другая история.